另一方面,自定义业务作业存储库使我们能够更好地控制作业生命周期,使我们能够在自己的域模型中启用、禁用或将作业标记为已完成。
正确的选择取决于需求:如果我们只需要可靠的调度,那么 Quartz 的持久性就足够了;如果作业状态是业务工作流的一部分,那么自定义存储库可能更合适。
在构建 Spring Web 应用程序时,我们经常需要安排重复执行的任务或作业,例如发送电子邮件、生成报告或按特定间隔处理数据。Quartz Scheduler因其强大而灵活的调度功能而成为处理此类任务的热门选择。
Spring Web 应用程序的一个关键挑战是确保已调度的 Quartz 作业在应用程序重启后仍然持久化,并无缝维护其状态和调度。 通常有两种方法可以实现这一点:
- 让 Quartz 本身使用其 JDBC JobStore 处理持久性。
- 在自定义业务表中维护作业定义,并在启动时将其加载到 Quartz 中。
在本教程中,我们将探讨这两种方法。
问题
在 Spring Web 应用程序中,开发人员可以集成 Quartz 来管理计划作业。一个关键要求是将作业和触发器的详细信息存储在数据库中,以便在应用程序关闭或重启时不会丢失。
在生产环境中,应用程序经常会因为维护、更新或意外崩溃而重启。如果 Quartz 作业仅存储在内存中(默认行为),则它们会在重启期间丢失,导致执行失败或需要手动重新调度。
将作业持久化到数据库中可以确保作业的连续性,使调度程序能够从中断的地方继续执行。这对于必须按照精确时间表运行的任务(例如每日报告或时效性通知)尤为重要。
Maven依赖项
让我们首先将spring-boot-starter-quartz、spring-boot-starter-data-jdbc、spring-boot-starter-data-jpa和h2依赖项导入到我们的pom.xml中:
org.springframework.boot
spring-boot-starter-quartz
3.3.2
org.springframework.boot
spring-boot-starter-data-jdbc
3.3.2
org.springframework.boot
spring-boot-starter-data-jpa
3.3.2
com.h2database
h2
2.2.224
使用Quartz的JDBC JobStore
Quartz 通过其自己的模式( QRTZ_*表)提供内置持久性。当使用 JDBC 配置时,Quartz 会自动将所有作业、触发器和调度元数据存储在数据库中:
spring.quartz.job-store-type=jdbc
在开发过程中,我们可以使用基于 H2 文件的数据库来模拟重启后的持久化。推荐的方法是让 Spring Boot 创建一次模式,然后禁用模式初始化,这样我们的作业数据就可以被保留下来:
spring.datasource.url=jdbc:h2:file:~/quartz-db;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
First run: let Spring create the Quartz schema
spring.quartz.jdbc.initialize-schema=always
Restart runs: disable schema initialization to preserve existing data
spring.quartz.jdbc.initialize-schema=never
使用此设置,Quartz 在首次运行时将在~/quartz-db.mv.db中创建所需的QRTZ_*表。在后续重启时,我们将设置切换为“ never ”,这样现有的表和作业数据就不会被删除或重新初始化,从而允许 Quartz 自动重新加载已调度的作业。这样,作业在应用程序重启后仍能继续运行,我们可以使用一个简单的基于 H2 文件的数据库安全地测试恢复行为。
定义 Quartz 作业
让我们通过实现Job接口来创建一个作业:
public class SampleJob implements Job {
@Override
public void execute(JobExecutionContext context) {
System.out.println("Executing SampleJob at " + System.currentTimeMillis());
}
}
然后,我们使用JobDetail类定义SampleJob的实例:
@Bean
public JobDetail sampleJobDetail() {
return JobBuilder.newJob(SampleJob.class)
.withIdentity("sampleJob", "group1")
.storeDurably()
.requestRecovery(true)
.build();
}
我们使用storeDurably()将作业持久化到 Quartz 数据库中。 此外,我们设置了requestRecovery(true)以确保在应用程序执行过程中崩溃时能够重试。
现在,我们需要定义一个触发器:
@Bean
public Trigger sampleTrigger(JobDetail sampleJobDetail) {
return TriggerBuilder.newTrigger()
.forJob(sampleJobDetail)
.withIdentity("sampleTrigger", "group1")
.withSchedule(CronScheduleBuilder.cronSchedule("0/30 * * * * ?")) // every 30s
.build();
}
我们用 cron 表达式( 0/30 * * * * ? )定义触发器,安排作业每30秒运行一次。
重启时重新初始化作业
当 Quartz 使用 JDBC 作业存储时,JobDetail及其关联的Trigger都会持久保存在 Quartz 的 schema 中。应用程序重启时,Quartz 会自动从数据库重新加载它们,因此无需自定义初始化逻辑。
让我们创建一个单元测试:
@Test
void givenSampleJob_whenSchedulerRestart_thenSampleJobIsReloaded() throws Exception {
// Given
JobKey jobKey = new JobKey("sampleJob", "group1");
TriggerKey triggerKey = new TriggerKey("sampleTrigger", "group1");
JobDetail jobDetail = scheduler.getJobDetail(jobKey);
assertNotNull(jobDetail, "SampleJob exists in running scheduler");
Trigger trigger = scheduler.getTrigger(triggerKey);
assertNotNull(trigger, "SampleTrigger exists in running scheduler");
// When
scheduler.standby();
Scheduler restartedScheduler = applicationContext.getBean(Scheduler.class);
restartedScheduler.start();
// Then
assertTrue(restartedScheduler.isStarted(), "Scheduler should be running after restart");
JobDetail reloadedJob = restartedScheduler.getJobDetail(jobKey);
assertNotNull(reloadedJob, "SampleJob should be reloaded from DB after restart");
Trigger reloadedTrigger = restartedScheduler.getTrigger(triggerKey);
assertNotNull(reloadedTrigger, "SampleTrigger should be reloaded from DB after restart");
}
此测试确保 Quartz 在模拟重启后能够正确地从其持久存储中重新加载作业和触发器。首先,我们验证正在运行的调度程序中是否存在sampleJob及其关联的sampleTrigger 。
接下来,我们将调度器置于待机模式,并从 Spring 上下文中获取一个新的Scheduler实例,模拟应用程序重启。启动新的调度器后,我们断言作业和触发器都再次可用。这证实了 Quartz 会自动从数据库恢复计划任务,无需额外的初始化逻辑。
使用自定义业务作业存储库
虽然 Quartz 提供了内置的持久化功能,但有时这还不够。在许多应用程序中,我们需要对作业的业务生命周期进行更多控制。例如,将它们标记为已启用、已禁用或已完成。在这些情况下,Quartz 的作业存储无法捕获足够的业务上下文,因此我们引入了自己的表来明确管理作业。
定义自定义作业表
我们可以首先创建一个 JPA 实体来表示我们业务领域中的工作:
@Entity
public class ApplicationJob {
@Id
private Long id;
private String name;
private boolean enabled;
private Boolean completed;
}
该表独立于 Quartz 的QRTZ_*模式,完全由我们控制。它允许我们跟踪 Quartz 无法处理的作业元数据,例如作业是否已明确标记为已完成。
播种业务工作
首次启动时,我们可以预先在表中填充作业定义。例如,一个简单的播种机可能会在表为空时插入一条记录:
@Component
public class DataSeeder implements CommandLineRunner {
private final ApplicationJobRepository repository;
public DataSeeder(ApplicationJobRepository repository) {
this.repository = repository;
}
@Override
public void run(String... args) {
if (repository.count() == 0) {
ApplicationJob job = new ApplicationJob();
job.setName("simpleJob");
job.setEnabled(true);
job.setCompleted(false);
repository.save(job);
}
}
}
此步骤模拟一个真实的系统,其中可以通过管理 UI 或业务工作流定义或管理作业。
启动时重新初始化作业
接下来,我们需要将业务存储库与 Quartz 连接起来。在应用程序启动时,监听器可以查询ApplicationJob表并动态安排作业:
@Component
public class JobInitializer implements ApplicationListener {
@Autowired
private ApplicationJobRepository jobRepository;
@Autowired
private Scheduler scheduler;
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
for (ApplicationJob job : jobRepository.findAll()) {
if (job.isEnabled() && (job.getCompleted() == null || !job.getCompleted())) {
JobDetail detail = JobBuilder.newJob(SampleJob.class)
.withIdentity(job.getName(), "appJobs")
.storeDurably()
.build();
Trigger trigger = TriggerBuilder.newTrigger()
.forJob(detail)
.withSchedule(SimpleScheduleBuilder.simpleSchedule()
.withIntervalInSeconds(30)
.repeatForever())
.build();
try {
scheduler.scheduleJob(detail, trigger);
} catch (SchedulerException e) {
throw new RuntimeException(e);
}
}
}
}
}
重启后,Quartz 不会自动重新加载这些作业,因为它们没有存储在 Quartz 自己的 schema 中。相反,JobInitializer会运行,查询业务表,并确保只有标记为已启用但尚未完成的作业才会在 Quartz 中被调度。
JobInitializer使用scheduler.scheduleJob ()方法重新安排作业。
与 Quartz 内置的持久化机制相比,这种方式虽然重复了调度逻辑,但却提供了更细粒度的控制。当作业定义属于业务模型的一部分,并且需要与其他领域数据一起管理时,这种方式尤其有用。