在本文中,我们将看看如何使用Quartz框架来调度任务。Quartz是Java应用程序调度库的事实标准。Quartz支持在特定时间运行作业、重复作业执行、将作业存储在数据库中以及Spring集成。

用于调度的Spring注解

在 Spring 应用程序中使用 Quartz 最简单的方法是使用@Scheduled注解。接下来,我们将考虑一个 Spring Boot 应用程序的示例。让我们添加必要的依赖项build.gradle

:::tips implementation ‘org.springframework.boot:spring-boot-starter-quartz’

:::

并考虑一个例子
  1. package quartzdemo.tasks;
  2. import org.springframework.scheduling.annotation.Scheduled;
  3. import org.springframework.stereotype.Component;
  4. import java.util.Date;
  5. @Component
  6. public class PeriodicTask {
  7. @Scheduled(cron = "0/5 * * * * ?")
  8. public void everyFiveSeconds() {
  9. System.out.println("Periodic task: " + new Date());
  10. }
  11. }
此外,@Scheduled要使注解起作用,您需要使用@EnableScheduling注解添加配置。
  1. package quartzdemo;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. import org.springframework.scheduling.annotation.EnableScheduling;
  5. @SpringBootApplication
  6. @EnableScheduling
  7. public class QuartzDemoApplication {
  8. public static void main(String[] args) {
  9. SpringApplication.run(QuartzDemoApplication.class, args);
  10. }
  11. }
结果将是每五秒在控制台中输出一个文本。

:::tips Periodic task: Thu Jul 07 18:24:50 EDT 2022

Periodic task: Thu Jul 07 18:24:55 EDT

2022 Periodic task: Thu Jul 07 18:25:00 EDT 2022

:::

@Scheduled注解支持以下参数
  • fixedRate- 允许您以指定的固定间隔运行任务。
  • fixedDelay- 在最后一次调用完成和下一次调用开始之间以固定延迟执行任务。
  • initialDelay- 该参数用于fixedRatefixedDelay在第一次执行具有指定延迟的任务之前等待。
  • cron- 使用 cron-string 设置任务执行计划。还支持宏@yearly(or @annually)、@monthly@weekly@daily(or @midnight) 和@hourly.
默认情况下fixedRatefixedDelayinitialDelay以毫秒为单位设置。这可以使用timeUnit参数进行更改,将值设置NANOSECONDSDAYS 此外,您可以在 @Scheduled 注释中使用属性:

application.properties

  1. cron-string=0/5 * * * * ?

PeriodicTask.java

  1. @Component
  2. public class PeriodicTask {
  3. @Scheduled(cron = "${cron-string}")
  4. public void everyFiveSeconds() {
  5. System.out.println("Periodic task: " + new Date());
  6. }
  7. }
要使用属性,您可以使用fixedRateString, fixedDelayString, 和initialDelayString参数来代替 fixedRate,fixedDelay和initialDelay相应的。

使用Quartz

在前面的例子中,我们执行了定时任务,但同时我们不能动态设置作业的开始时间,也不能给它传递参数。要解决这些问题,可以直接使用 Quartz。 下面列出了主要的 Quartz 接口:
  • Job是由包含我们希望执行的业务逻辑的类实现的接口
  • JobDetails定义Job与之相关的实例和数据
  • Trigger描述作业执行的时间表
  • Scheduler是主要的 Quartz 界面,为作业和触发器提供所有操作和搜索操作
要直接使用 Quartz,无需使用@EnableScheduling注解定义配置org.springframework.boot:spring-boot-starter-quartz,您可以使用org.quartz-scheduler:quartz. 让我们定义 SimpleJob 类:
  1. package quartzdemo.jobs;
  2. import org.quartz.Job;
  3. import org.quartz.JobExecutionContext;
  4. import java.text.MessageFormat;
  5. public class SimpleJob implements Job {
  6. @Override
  7. public void execute(JobExecutionContext context) {
  8. System.out.println(MessageFormat.format("Job: {0}", getClass()));
  9. }
  10. }
要实现Job接口,您只需要实现一个execute接受JobExecutionContext类型参数的方法。JobExecutionContext包含有关作业实例、触发器、调度程序的信息以及有关作业执行的其他信息。
现在让我们定义一个作业实例:
  1. JobDetail job = JobBuilder.newJob(SimpleJob.class).build();
并创建一个将在五秒后触发的触发器:
  1. Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5)
  2. .atZone(ZoneId.systemDefault()).toInstant());
  3. Trigger trigger = TriggerBuilder.newTrigger()
  4. .startAt(afterFiveSeconds)
  5. .build();
另外,创建一个调度程序:
  1. SchedulerFactory schedulerFactory = new StdSchedulerFactory();
  2. Scheduler scheduler = schedulerFactory.getScheduler();
在“待机”模式下Scheduler初始化,所以我们必须调用start方法:
  1. scheduler.start();
现在我们可以安排作业的执行:
  1. scheduler.scheduleJob(job, trigger);
更进一步,在创建时JobDetails,让我们添加额外的数据并在执行作业时使用它:

QuartzDemoApplication.java

  1. @SpringBootApplication
  2. public class QuartzDemoApplication {
  3. public static void main(String[] args) {
  4. SpringApplication.run(QuartzDemoApplication.class, args);
  5. onStartup();
  6. }
  7. private static void onStartup() throws SchedulerException {
  8. JobDetail job = JobBuilder.newJob(SimpleJob.class)
  9. .usingJobData("param", "value") // add a parameter
  10. .build();
  11. Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5)
  12. .atZone(ZoneId.systemDefault()).toInstant());
  13. Trigger trigger = TriggerBuilder.newTrigger()
  14. .startAt(afterFiveSeconds)
  15. .build();
  16. SchedulerFactory schedulerFactory = new StdSchedulerFactory();
  17. Scheduler scheduler = schedulerFactory.getScheduler();
  18. scheduler.start();
  19. scheduler.scheduleJob(job, trigger);
  20. }
  21. }

SimpleJob.java

  1. public class SimpleJob implements Job {
  2. @Override
  3. public void execute(JobExecutionContext context) {
  4. JobDataMap dataMap = context.getJobDetail().getJobDataMap();
  5. String param = dataMap.getString("param");
  6. System.out.println(MessageFormat.format("Job: {0}; Param: {1}",
  7. getClass(), param));
  8. }
  9. }
需要注意的是,添加到的所有值都JobDataMap必须是可序列化的。

工作商店

Quartz 将有关JobDetail、Trigger的数据和其他信息存储在JobStore. 默认情况下,JobStore使用内存。这意味着如果我们在它们被触发之前已经安排了任务并关闭了应用程序(例如,在重新启动或崩溃时),那么它们将永远不会再次执行。Quartz 还支持 JDBC-JobStore 在数据库中存储信息。
在使用 JDBC-JobStore 之前,需要在 Quartz 将使用的数据库中创建表。默认情况下,这些表的前缀为QRTZ_.
Quartz 源代码包含用于为各种数据库(如 Oracle、Postgres、MS SQL Server、MySQL 等)创建表的SQL 脚本,并且还有一个用于 Liquibase 的现成 XML 文件。
spring.quartz.jdbc.initialize-schema=always此外,通过指定属性,可以在启动应用程序时自动创建 QRTZ 表。
为简单起见,我们将使用第二种方法和 H2 数据库。让我们配置一个数据源,使用 JDBCJobStore 并在 application.properties 中创建 QRTZ 表:

:::tips spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password= spring.quartz.job-store-type=jdbc spring.quartz.jdbc.initialize-schema=always

:::

要考虑这些设置,必须将调度程序创建为 Spring bean:
  1. package quartzdemo;
  2. import org.quartz.*;
  3. import org.springframework.boot.CommandLineRunner;
  4. import org.springframework.boot.SpringApplication;
  5. import org.springframework.boot.autoconfigure.SpringBootApplication;
  6. import org.springframework.context.annotation.Bean;
  7. import org.springframework.scheduling.annotation.EnableScheduling;
  8. import org.springframework.scheduling.quartz.SchedulerFactoryBean;
  9. import quartzdemo.jobs.SimpleJob;
  10. import java.time.LocalDateTime;
  11. import java.time.ZoneId;
  12. import java.util.Date;
  13. @SpringBootApplication
  14. @EnableScheduling
  15. public class QuartzDemoApplication {
  16. public static void main(String[] args) {
  17. SpringApplication.run(QuartzDemoApplication.class, args);
  18. }
  19. @Bean()
  20. public Scheduler scheduler(SchedulerFactoryBean factory) throws SchedulerException {
  21. Scheduler scheduler = factory.getScheduler();
  22. scheduler.start();
  23. return scheduler;
  24. }
  25. @Bean
  26. public CommandLineRunner run(Scheduler scheduler) {
  27. return (String[] args) -> {
  28. JobDetail job = JobBuilder.newJob(SimpleJob.class)
  29. .usingJobData("param", "value") // add a parameter
  30. .build();
  31. Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5)
  32. .atZone(ZoneId.systemDefault()).toInstant());
  33. Trigger trigger = TriggerBuilder.newTrigger()
  34. .startAt(afterFiveSeconds)
  35. .build();
  36. scheduler.scheduleJob(job, trigger);
  37. };
  38. }
  39. }

线程池配置

Quartz 在单独的线程中运行每个任务,您可以为调度程序配置线程池。还需要注意的是,默认情况下,通过@Scheduled注解和直接通过 Quartz 启动的任务是在不同的线程池中启动的。我们可以确保这一点:

PeriodicTask.java

  1. @Component
  2. public class PeriodicTask {
  3. @Scheduled(cron = "${cron-string}")
  4. public void everyFiveSeconds() {
  5. System.out.println(MessageFormat.format("Periodic task: {0}; Thread: {1}",
  6. new Date().toString(), Thread.currentThread().getName()));
  7. }
  8. }

SimpleJob.java

  1. public class SimpleJob implements Job {
  2. @Override
  3. public void execute(JobExecutionContext context) {
  4. JobDataMap dataMap = context.getJobDetail().getJobDataMap();
  5. String param = dataMap.getString("param");
  6. System.out.println(MessageFormat.format("Job: {0}; Param: {1}; Thread: {2}",
  7. getClass(), param, Thread.currentThread().getName()));
  8. }
  9. }
输出将是:

:::tips Periodic task: Thu Jul 07 19:22:45 EDT 2022; Thread: scheduling-1 Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: quartzScheduler_Worker-1 Periodic task: Thu Jul 07 19:22:50 EDT 2022; Thread: scheduling-1 Periodic task: Thu Jul 07 19:22:55 EDT 2022; Thread: scheduling-1 Periodic task: Thu Jul 07 19:23:00 EDT 2022; Thread: scheduling-1

:::

任务的线程池@Scheduled只包含一个线程。
让我们更改 @Scheduled 任务的调度程序设置:
  1. package quartzdemo;
  2. import org.springframework.context.annotation.Configuration;
  3. import org.springframework.scheduling.annotation.SchedulingConfigurer;
  4. import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
  5. import org.springframework.scheduling.config.ScheduledTaskRegistrar;
  6. @Configuration
  7. public class SchedulingConfiguration implements SchedulingConfigurer {
  8. @Override
  9. public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
  10. ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
  11. threadPoolTaskScheduler.setPoolSize(10);
  12. threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
  13. threadPoolTaskScheduler.initialize();
  14. taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
  15. }
  16. }
输出现在将是这样的:

:::tips Periodic task: Thu Jul 07 19:44:10 EDT 2022; Thread: my-scheduled-task-pool-1 Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: quartzScheduler_Worker-1 Periodic task: Thu Jul 07 19:44:15 EDT 2022; Thread: my-scheduled-task-pool-1 Periodic task: Thu Jul 07 19:44:20 EDT 2022; Thread: my-scheduled-task-pool-2

:::

如您所见,这些设置仅影响使用注释设置的任务。 现在让我们更改直接使用 Quartz 的调度程序的设置。这可以通过两种方式完成:通过属性文件或通过创建 bean SchedulerFactoryBeanCustomizer 让我们使用第一种方法。如果我们没有通过 Spring 初始化 Quartz,我们将不得不在 quartz.properties 文件中注册属性。在我们的例子中,我们需要在 application.properties 中注册属性,并spring.quartz.properties.为其添加前缀。

:::tips

application.properties

spring.quartz.properties.org.quartz.threadPool.threadNamePrefix=my-scheduler_Worker spring.quartz.properties.org.quartz.threadPool.threadCount=25

:::

让我们启动应用程序。现在输出将是这样的:

:::tips Periodic task: Sat Jul 23 10:45:55 MSK 2022; Thread: my-scheduled-task-pool-1 Job: class quartzdemo.jobs.SimpleJob; Param: value; Thread: my-scheduler_Worker-1

:::

现在调用启动任务的线程my-scheduler_Worker-1。

多个调度器

如果您需要创建多个具有不同参数的调度程序,则必须定义多个SchedulerFactoryBeans. 让我们看一个例子。
  1. package quartzdemo;
  2. import quartzdemo.jobs.SimpleJob;
  3. import org.quartz.*;
  4. import org.springframework.beans.factory.annotation.Qualifier;
  5. import org.springframework.boot.CommandLineRunner;
  6. import org.springframework.boot.SpringApplication;
  7. import org.springframework.boot.autoconfigure.SpringBootApplication;
  8. import org.springframework.context.annotation.Bean;
  9. import org.springframework.scheduling.annotation.EnableScheduling;
  10. import org.springframework.scheduling.quartz.SchedulerFactoryBean;
  11. import javax.sql.DataSource;
  12. import java.time.LocalDateTime;
  13. import java.time.ZoneId;
  14. import java.util.Date;
  15. import java.util.Properties;
  16. @SpringBootApplication
  17. @EnableScheduling
  18. public class QuartzDemoApplication {
  19. public static void main(String[] args) {
  20. SpringApplication.run(QuartzDemoApplication.class, args);
  21. }
  22. @Bean("customSchedulerFactoryBean1")
  23. public SchedulerFactoryBean customSchedulerFactoryBean1(DataSource dataSource) {
  24. SchedulerFactoryBean factory = new SchedulerFactoryBean();
  25. Properties properties = new Properties();
  26. properties.setProperty("org.quartz.threadPool.threadNamePrefix", "my-custom-scheduler1_Worker");
  27. factory.setQuartzProperties(properties);
  28. factory.setDataSource(dataSource);
  29. return factory;
  30. }
  31. @Bean("customSchedulerFactoryBean2")
  32. public SchedulerFactoryBean customSchedulerFactoryBean2(DataSource dataSource) {
  33. SchedulerFactoryBean factory = new SchedulerFactoryBean();
  34. Properties properties = new Properties();
  35. properties.setProperty("org.quartz.threadPool.threadNamePrefix", "my-custom-scheduler2_Worker");
  36. factory.setQuartzProperties(properties);
  37. factory.setDataSource(dataSource);
  38. return factory;
  39. }
  40. @Bean("customScheduler1")
  41. public Scheduler customScheduler1(@Qualifier("customSchedulerFactoryBean1") SchedulerFactoryBean factory) throws SchedulerException {
  42. Scheduler scheduler = factory.getScheduler();
  43. scheduler.start();
  44. return scheduler;
  45. }
  46. @Bean("customScheduler2")
  47. public Scheduler customScheduler2(@Qualifier("customSchedulerFactoryBean2") SchedulerFactoryBean factory) throws SchedulerException {
  48. Scheduler scheduler = factory.getScheduler();
  49. scheduler.start();
  50. return scheduler;
  51. }
  52. @Bean
  53. public CommandLineRunner run(@Qualifier("customScheduler1") Scheduler customScheduler1,
  54. @Qualifier("customScheduler2") Scheduler customScheduler2) {
  55. return (String[] args) -> {
  56. Date afterFiveSeconds = Date.from(LocalDateTime.now().plusSeconds(5).atZone(ZoneId.systemDefault()).toInstant());
  57. JobDetail jobDetail1 = JobBuilder.newJob(SimpleJob.class).usingJobData("param", "value1").build();
  58. Trigger trigger1 = TriggerBuilder.newTrigger().startAt(afterFiveSeconds).build();
  59. customScheduler1.scheduleJob(jobDetail1, trigger1);
  60. JobDetail jobDetail2 = JobBuilder.newJob(SimpleJob.class).usingJobData("param", "value2").build();
  61. Trigger trigger2 = TriggerBuilder.newTrigger().startAt(afterFiveSeconds).build();
  62. customScheduler2.scheduleJob(jobDetail2, trigger2);
  63. };
  64. }
  65. }
输出:

:::tips Job: class quartzdemo.jobs.SimpleJob; Param: value2; Thread: my-custom-scheduler2_Worker-1 Job: class quartzdemo.jobs.SimpleJob; Param: value1; Thread: my-custom-scheduler1_Worker-1

:::

结论

Quartz 是一个用于自动执行计划任务的强大框架。它既可以在简单直观的 Spring 注解的帮助下使用,也可以通过精细的定制和调整来使用,从而为复杂的问题提供解决方案。

原文标题:How to Schedule Jobs With Quartz in Spring Boot