1. 概述

在之前的文章中,我们演示了如何使用Spring中的@Scheduled注解来安排任务。本文将展示如何在Jakarta EE应用中通过定时服务来实现与上述文章中相同场景的任务调度。

2. 启用定时任务支持

在Jakarta EE应用中,无需额外启用定时任务支持。定时服务是容器管理的服务,允许应用程序调用基于时间事件的预定方法。例如,一个应用可能需要在每天的某个时间运行一些报告以生成统计数据。

定时器有两种类型:

  • 程序化定时器:定时服务可以注入到任何非状态ful会话bean中,业务逻辑应放在带有@Timeout注解的方法中。定时器可以通过bean的@PostConstruct方法初始化,也可以通过调用一个方法来初始化。

  • 自动定时器:业务逻辑放在带有@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()));
    }
}

另一种方式是使用@Scheduled注解。下面的代码片段显示每五秒触发一次定时器:

@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()));
    }
}

5. 带有初始延迟的调度任务

如果任务需要延迟启动,Jakarta EE也支持。例如,定时器可以在10秒后开始,然后每五秒触发一次:

@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()));
    }
}

在示例中使用的createTimer方法签名是createTimer(long initialDuration, long intervalDuration, java.io.Serializable info),其中initialDuration是第一次定时器到期通知前必须经过的毫秒数,intervalDuration是两次定时器到期通知之间的时间间隔。

在这个例子中,我们使用了10秒的initialDuration和5秒的intervalDuration。任务将在initialDuration值后首次执行,并根据intervalDuration持续执行。

6. 使用Cron表达式调度任务

我们看到的所有调度器,无论是程序化还是自动的,都支持Cron表达式。让我们看一个例子:

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

在这个例子中,doPeriodicCleanup()方法将在每周五的23:00以及每月最后一天被调用。

7. 总结

本文探讨了在Jakarta EE环境中使用各种方法安排任务,以之前使用Spring进行的示例为基础。

代码示例可在GitHub仓库中找到。


« 上一篇: Java 插件Lombok用法