Код-ревью Middle Бины: скоупы и жизненный цикл обновлено
Код-ревью: сборщик отчётов
Вопрос
Найдите проблемы со скоупами, состоянием и жизненным циклом бинов.
@Component
@Scope("prototype")
public class ReportBuilder {
private final List<String> rows = new ArrayList<>();
public void add(String row) { rows.add(row); }
public String build() { return String.join("\n", rows); }
}
@Service
public class ReportService {
private final ReportBuilder builder;
private final ReportRepository repo;
private int generated;
private final SimpleDateFormat fmt = new SimpleDateFormat("dd.MM.yyyy");
public ReportService(ReportBuilder builder, ReportRepository repo) {
this.builder = builder;
this.repo = repo;
}
@PostConstruct
@Transactional
public void warmUp() {
repo.preloadTemplates();
}
public String daily(LocalDate day) {
generated++;
builder.add("Отчёт за " + fmt.format(java.sql.Date.valueOf(day)));
repo.findRows(day).forEach(builder::add);
return builder.build();
}
}
Коротко
Корень проблем в том, что ReportService singleton, а всё изменяемое внутри него разделяется между потоками. Builder нужно получать на каждый вызов через ObjectProvider<ReportBuilder или просто создавать локально, счётчик заменить метрикой или AtomicLong, формат даты на неизменяемый DateTimeFormatter.
Что должен найти ревьюер
- Ошибкастрока 11Prototype внедрён в singleton один раз: все вызовы daily() пишут в один и тот же ReportBuilder, строки копятся между отчётами и пользователями.
- Ошибкастрока 13Изменяемый счётчик в singleton без синхронизации:
generated++теряет обновления при параллельных запросах. Нужен AtomicLong или метрика. - Ошибкастрока 14SimpleDateFormat не потокобезопасен, в общем поле синглтона даст испорченные даты. Нужен DateTimeFormatter.
- Ошибкастрока 22@Transactional на @PostConstruct не работает: метод вызывается на самом объекте до создания прокси, транзакции не будет.
- Ошибкастрока 31Из-за общего builder результат build() содержит строки прошлых отчётов, а параллельные вызовы ещё и ломают ArrayList.
Разбор
Корень проблем в том, что ReportService singleton, а всё изменяемое внутри него разделяется между потоками. Builder нужно получать на каждый вызов через ObjectProvider<ReportBuilder> или просто создавать локально, счётчик заменить метрикой или AtomicLong, формат даты на неизменяемый DateTimeFormatter. Прогрев перенести в обработчик ApplicationReadyEvent с вызовом через прокси.
Ответьте сами, а не прочитайте
В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.