Чем реализовать генерацию отчета по расписанию и отправку уведомлений в Java
Что проверяют
Знаете ли вы инструменты планирования в экосистеме Java и, главное, помните ли, что задача по расписанию в приложении, поднятом в нескольких экземплярах, выполнится в каждом из них. Второй слой: умеете ли вы разделить генерацию и доставку.
Как отвечать
Разложите ответ на три части.
Триггер. В Spring-приложении самое дешёвое это @EnableScheduling и метод с @Scheduled(cron = ...). Этого хватает, пока инстанс один. Дальше варианты: Quartz с хранилищем заданий в базе умеет кластерный режим, узлы договариваются между собой; либо распределённая блокировка на время выполнения поверх базы или Redis; либо расписание выносится наружу, в планировщик инфраструктуры, а приложение отдаёт обычную точку входа.
Генерация. Для табличных выгрузок Apache POI, для вёрстки и PDF JasperReports. Большой отчёт нельзя собирать целиком в памяти: у POI есть потоковая реализация книги, а данные тянут курсором или постранично, а не методом, который читает всю таблицу.
Доставка. Письмо отправляется через JavaMailSender, вложение прикладывается MimeMessageHelper. Отправку стоит отделить от генерации: файл кладётся в хранилище, а задание на отправку идёт отдельным шагом, тогда повтор после сбоя SMTP не пересчитывает отчёт заново.
Добавьте про эксплуатацию: журнал запусков, идемпотентность по периоду отчёта, оповещение о пропущенном запуске. Без этого молча не отправленный отчёт заметит только получатель.
Где спотыкаются
Отвечают одной аннотацией и забывают про горизонтальное масштабирование. Три инстанса это три одинаковых письма каждому получателю, и обнаруживается это только на проде.
Второе: не помнят, что планировщик Spring по умолчанию однопоточный. Долгая генерация блокирует все остальные задачи по расписанию, и они молча сдвигаются. Лечится своим пулом потоков, а также пониманием разницы между фиксированной задержкой и фиксированным периодом.
Третье: часовой пояс. Расписание считается по зоне виртуальной машины, если зона не указана явно, и после переезда в контейнер с UTC отчёт уезжает на несколько часов. Рядом та же ошибка с границами периода: их берут от текущего времени вместо явных начала и конца отчётного интервала.