«1. Обзор

В предыдущей статье мы продемонстрировали, как планировать задачи в Spring с помощью аннотации @Scheduled. В этой статье мы покажем, как добиться того же, используя службу таймера в приложении Jakarta EE для каждого случая, представленного в предыдущей статье.

2. Включите поддержку расписания

В приложении Jakarta EE нет необходимости включать поддержку задач на время. Служба таймера — это служба, управляемая контейнером, которая позволяет приложениям вызывать методы, запланированные для событий, зависящих от времени. Например, приложению может потребоваться запускать некоторые ежедневные отчеты в определенный час, чтобы генерировать статистику.

Существует два типа таймеров:

    Программные таймеры: служба таймера может быть внедрена в любой компонент (кроме сеансового компонента с отслеживанием состояния), а бизнес-логика должна быть помещена в метод, аннотированный @Timeout. Таймер может быть инициализирован методом, аннотированным @PostConstruct bean-компонентов, или он также может быть инициализирован простым вызовом метода.
    Автоматические таймеры: бизнес-логика размещается в любом методе, аннотированном с помощью @Schedule или @Schedules. Эти таймеры инициализируются сразу после запуска приложения.

Итак, давайте начнем с нашего первого примера.

3. Планирование задачи с фиксированной задержкой

В Spring это делается просто с помощью аннотации @Scheduled(fixedDelay = 1000). В этом случае продолжительность между окончанием последнего выполнения и началом следующего выполнения является фиксированной. Задача всегда ожидает завершения предыдущей.

Сделать то же самое в Jakarta EE немного сложнее, потому что нет аналогичного встроенного механизма, тем не менее, аналогичный сценарий можно реализовать с небольшим количеством дополнительного кода. Давайте посмотрим, как это делается:

@Singleton
public class FixedTimerBean {

    @EJB
    private WorkerBean workerBean;

    @Lock(LockType.READ)
    @Schedule(second = "*/5", minute = "*", hour = "*", persistent = false)
    public void atSchedule() throws InterruptedException {
        workerBean.doTimerWork();
    }
}
@Singleton
public class WorkerBean {

    private AtomicBoolean busy = new AtomicBoolean(false);

    @Lock(LockType.READ)
    public void doTimerWork() throws InterruptedException {
        if (!busy.compareAndSet(false, true)) {
            return;
        }
        try {
            Thread.sleep(20000L);
        } finally {
            busy.set(false);
        }
    }
}

Как видите, таймер должен срабатывать каждые пять секунд. Однако метод, запущенный в нашем случае, имитировал 20-секундное время отклика при вызове sleep() в текущем потоке.

Как следствие, контейнер будет продолжать вызывать doTimerWork() каждые пять секунд, но условие, поставленное в начале метода, busy.compareAndSet(false, true), немедленно вернется, если предыдущий вызов не завершился. При этом мы гарантируем, что следующая задача будет выполнена только после завершения предыдущей.

4. Планирование задачи с фиксированной скоростью

Один из способов сделать это — использовать службу таймера, которая внедряется с помощью @Resource и настраивается в методе, аннотированном @PostConstruct. Метод с аннотацией @Timeout будет вызываться по истечении времени таймера.

@Startup
@Singleton
public class ProgrammaticAtFixedRateTimerBean {

    @Inject
    Event<TimerEvent> event;

    @Resource
    TimerService timerService;

    @PostConstruct
    public void initialize() {
        timerService.createTimer(0,1000, "Every second timer with no delay");
    }

    @Timeout
    public void programmaticTimout(Timer timer) {
        event.fire(new TimerEvent(timer.getInfo().toString()));
    }
}

Как упоминалось в предыдущей статье, начало выполнения задачи не ожидает завершения предыдущего выполнения. Эту опцию следует использовать, когда каждое выполнение задачи является независимым. Следующий фрагмент кода создает таймер, который срабатывает каждую секунду:

@Startup
@Singleton
public class ScheduleTimerBean {

    @Inject
    Event<TimerEvent> event;

    @Schedule(hour = "*", minute = "*", second = "*/5", info = "Every 5 seconds timer")
    public void automaticallyScheduled(Timer timer) {
        fireEvent(timer);
    }


    private void fireEvent(Timer timer) {
        event.fire(new TimerEvent(timer.getInfo().toString()));
    }
}

Другой способ — использовать аннотацию @Scheduled. В следующем фрагменте кода мы запускаем таймер каждые пять секунд:

5. Планирование задачи с начальной задержкой

@Startup
@Singleton
public class ProgrammaticWithInitialFixedDelayTimerBean {

    @Inject
    Event<TimerEvent> event;

    @Resource
    TimerService timerService;

    @PostConstruct
    public void initialize() {
        timerService.createTimer(10000, 5000, "Delay 10 seconds then every 5 seconds timer");
    }

    @Timeout
    public void programmaticTimout(Timer timer) {
        event.fire(new TimerEvent(timer.getInfo().toString()));
    }
}

Если ваш сценарий использования требует, чтобы таймер запускался с задержкой, мы также можем сделать это. В этом случае Jakarta EE позволяет использовать службу таймера. Давайте рассмотрим пример, в котором таймер имеет начальную задержку 10 секунд, а затем срабатывает каждые пять секунд:

Метод createTimer, используемый в нашем примере, использует следующую сигнатуру java.io.Serializable info), где initialDuration — это количество миллисекунд, которое должно пройти до первого уведомления об истечении срока действия таймера, а intervalDuration — это количество миллисекунд, которое должно пройти между уведомлениями об истечении срока действия таймера.

В этом примере мы используем initialDuration, равное 10 секундам, и intervalDuration, равное пяти секундам. Задача будет выполняться в первый раз после значения initialDuration и будет продолжать выполняться в соответствии с intervalDuration.

6. Планирование задачи с использованием выражений Cron

@Schedules ({
   @Schedule(dayOfMonth="Last"),
   @Schedule(dayOfWeek="Fri", hour="23")
})
public void doPeriodicCleanup() { ... }

«Все планировщики, которые мы видели, как программные, так и автоматические, позволяют использовать выражения cron. Рассмотрим пример:

В этом примере метод doPeriodicCleanup() будет вызываться каждую пятницу в 23:00 и в последний день месяца.

7. Заключение

В этой статье мы рассмотрели различные способы планирования задач в среде Jakarta EE, используя в качестве отправной точки предыдущую статью, где примеры были сделаны с использованием Spring.