Код-ревью 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.

Что должен найти ревьюер

  1. Ошибкастрока 11
    Prototype внедрён в singleton один раз: все вызовы daily() пишут в один и тот же ReportBuilder, строки копятся между отчётами и пользователями.
  2. Ошибкастрока 13
    Изменяемый счётчик в singleton без синхронизации: generated++ теряет обновления при параллельных запросах. Нужен AtomicLong или метрика.
  3. Ошибкастрока 14
    SimpleDateFormat не потокобезопасен, в общем поле синглтона даст испорченные даты. Нужен DateTimeFormatter.
  4. Ошибкастрока 22
    @Transactional на @PostConstruct не работает: метод вызывается на самом объекте до создания прокси, транзакции не будет.
  5. Ошибкастрока 31
    Из-за общего builder результат build() содержит строки прошлых отчётов, а параллельные вызовы ещё и ломают ArrayList.

Разбор

Корень проблем в том, что ReportService singleton, а всё изменяемое внутри него разделяется между потоками. Builder нужно получать на каждый вызов через ObjectProvider<ReportBuilder> или просто создавать локально, счётчик заменить метрикой или AtomicLong, формат даты на неизменяемый DateTimeFormatter. Прогрев перенести в обработчик ApplicationReadyEvent с вызовом через прокси.

Ответьте сами, а не прочитайте

В тренажёре вопрос попадёт в повторения и вернётся, когда начнёт забываться. Плюс билеты с зачётом: 20 вопросов, не больше 2 ошибок.