Java 定时任务

一、ScheduledThreadPool

ScheduledThreadPool是JDK自带的类,可以用来替代Timer类实现定时任务。一个Timer只能执行一个任务,而一个ScheduledThreadPool却可以同时执行多个定时任务。用法很简单,直接看例子:

  1. public class ScheduledThreadPoolService {
  2. private Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
  3. // 参数代表可以同时执行的定时任务个数
  4. private ScheduledExecutorService service = Executors.newScheduledThreadPool(3);
  5. /**
  6. * schedule:延时2秒执行一次任务
  7. */
  8. public void task0() {
  9. service.schedule(() -> {
  10. logger.info("task0-start");
  11. sleep(2);
  12. logger.info("task0-end");
  13. }, 2, TimeUnit.SECONDS);
  14. }
  15. /**
  16. * scheduleAtFixedRate:2秒后,每间隔4秒执行一次任务
  17. * 注意,如果任务的执行时间(例如6秒)大于间隔时间,则会等待任务执行结束后直接开始下次任务
  18. */
  19. public void task1() {
  20. service.scheduleAtFixedRate(() -> {
  21. logger.info("task1-start");
  22. sleep(2);
  23. logger.info("task1-end");
  24. }, 2, 4, TimeUnit.SECONDS);
  25. }
  26. /**
  27. * scheduleWithFixedDelay:2秒后,每次延时4秒执行一次任务
  28. * 注意,这里是等待上次任务执行结束后,再延时固定时间后开始下次任务
  29. */
  30. public void task2() {
  31. service.scheduleWithFixedDelay(() -> {
  32. logger.info("task2-start");
  33. sleep(2);
  34. logger.info("task2-end");
  35. }, 2, 4, TimeUnit.SECONDS);
  36. }
  37. private void sleep(long time) {
  38. try {
  39. TimeUnit.SECONDS.sleep(time);
  40. } catch (InterruptedException e) {
  41. e.printStackTrace();
  42. }
  43. }
  44. }

和线程池的使用类似,也是通过Executors类来创建ScheduledThreadPool,常用的方法例子中都有说明,简单的定时任务使用ScheduledThreadPool实现就可以了,也不用引入其它依赖,还是挺方便的。

二、@Scheduled

@Scheduled是Spring框架的定时任务实现,相比JDK的ScheduledThreadPool功能更加强大。可以先创建一个Spring Boot项目,在启动类上添加@EnableScheduling注解,使@Scheduled生效,开启定时任务:

  1. @SpringBootApplication
  2. @EnableScheduling
  3. public class LearnSpringbootApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(LearnSpringbootApplication.class, args);
  6. }
  7. }

然后就是定义任务类,用@Scheduled配置具体的定时规则:

  1. @Service
  2. public class ScheduleService {
  3. private Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
  4. /**
  5. * fixedRate:每间隔2秒执行一次任务
  6. * 注意,默认情况下定时任务是在同一线程同步执行的,如果任务的执行时间(例如6秒)大于间隔时间,则会等待任务执行结束后直接开始下次任务
  7. */
  8. @Scheduled(fixedRate = 2000)
  9. public void task0() {
  10. logger.info("task0-start");
  11. sleep(6);
  12. logger.info("task0-end");
  13. }
  14. /**
  15. * fixedDelay:每次延时2秒执行一次任务
  16. * 注意,这里是等待上次任务执行结束后,再延时固定时间后开始下次任务
  17. */
  18. @Scheduled(fixedDelay = 2000)
  19. public void task1() {
  20. logger.info("task1-start");
  21. sleep(6);
  22. logger.info("task1-end");
  23. }
  24. /**
  25. * initialDelay:首次任务启动的延时时间
  26. */
  27. @Scheduled(initialDelay = 2000, fixedDelay = 3000)
  28. public void task2() {
  29. logger.info("task2-start");
  30. sleep(6);
  31. logger.info("task2-end");
  32. }
  33. private void sleep(long time) {
  34. try {
  35. TimeUnit.SECONDS.sleep(time);
  36. } catch (InterruptedException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. }

fixedRatefixedDelayinitialDelay属性的时间单位是毫秒,用法也比较简单,可参考代码的注释。
这里重点看使用@Scheduledcron属性定义定时任务的时间表达式,它的功能更加丰富。先看一个简单的例子:

  1. @Service
  2. public class ScheduleService {
  3. ...................
  4. /**
  5. * 每天晚上23:59执行一次
  6. */
  7. @Scheduled(cron = "0 59 23 * * ?")
  8. public void task3() {
  9. logger.info("task3-start");
  10. sleep(6);
  11. logger.info("task3-end");
  12. }
  13. ...................
  14. }

接下来具体看cron属性对应时间表达式的定义规则:
1、按顺序依次是:秒、分、时、日、月、周,中间用空格间隔
2、月、周可以用数字或英文单词的前三个字母表示
3、日和周可能会冲突,因此两个可以有一个配置为?
4、常用通配符的含义:

  • 表示任意值,例如在秒字段上设置,表示每秒都触发
  • ?表示不指定值,只能出现在日或者周的位置,用于处理日和周可能存在的冲突,例如2020年8月15是周六,如果又在周的位置上指定为周一,那就会产生冲突到导致定时任务失效。如果不关心日或者周的时候,也可以将其设置为?
  • -表示时间区间,例如在秒上设置1-3,表示第1、2、3秒都会触发
  • /表示时间间隔,例如在秒上设置2/4,表示从第2秒开始每间隔4秒触发一次
  • ,表示列举多个值,例如MON,WED,FRI表示周一、周三、周五触发

5、几个表达式的例子:

  • 0 0 9 * * ?:每天早上9点触发
  • 0 0/30 * * * ?:每30分钟触发一次
  • 0 30 18 ?* MON-FRI:每周一到周五的18:30分触发
  • 0 10 12 1,15 * ?:每月1号、15号的12:10触发
  • 0 0/10 7-8 1,15 * ?:每月1号、15号早上7点到8点每10分钟触发一次

6、注意,不同版本的Spring对cron属性的支持有所差异,例如较新的版本中cron属性只支持6个字段,不包含年份;同时#LW通配符也不支持。
7、注意,@Scheduled默认会使用同一线程同步的去执行所有定时任务,要打破这一点,可以在Spring Boot启动类上配置@EnableAsync注解来开启异步线程可用,然后在定时任务方法上配置@Async注解,则会使用异步线程去执行定时任务。

三、Quartz

除了上边的方式之外,还有更好的选择,那就是Quartz,在Spring Boot中集成Quartz需要先添加如下Maven依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-quartz</artifactId>
  4. <version>2.3.2.RELEASE</version>
  5. </dependency>

@Scheduled一样,也需要在启动类添加@EnableScheduling注解来开启定时任务:

  1. @SpringBootApplication
  2. @EnableScheduling
  3. public class LearnSpringbootApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(LearnSpringbootApplication.class, args);
  6. }
  7. }

这样准备工作就完成了。接下来先了解几个重要的概念:

  • Job:定义具体要执行的任务
  • JobDetail:配置要执行任务的描述信息,即如何去定位要执行的Job,每次执行任务时,都会根据JobDetail创建一个Job对象,避免任务并发执行时访问同一个Job对象产生问题
  • Trigger:触发器,配置任务执行的时间规则,需要和一个JobDetail关联起来
  • Scheduler:调度器,它维护了一个JobDetailTrigger的注册表,当任务关联的触发器到达预定的时间,调度器会去执行任务

接下来通过Quartz实现定时任务,可以按照上边几个重要概念的顺序来完成。

1、定义Job

定义Job有两种方式,
第一种是直接定义任务类,并注册到Spring IoC容器中:

  1. @Service
  2. public class QuartzJob {
  3. private Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
  4. public void hello(){
  5. logger.info("job0-start");
  6. Utils.sleep(TimeUnit.SECONDS, 4);
  7. logger.info("job0-end");
  8. }
  9. }

第二种是继承QuartzJobBean,重写executeInternal方法,这种方式可以接受JobDetail传递的参数`:

  1. public class QuartzJob2 extends QuartzJobBean {
  2. private Logger logger = LoggerFactory.getLogger(getClass().getSimpleName());
  3. @Override
  4. protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {
  5. logger.info("job1-start");
  6. Utils.sleep(TimeUnit.SECONDS, 4);
  7. // 获取参数
  8. JobDataMap jobDataMap = jobExecutionContext.getMergedJobDataMap();
  9. String date = jobDataMap.getString("date");
  10. logger.info("参数:" + date);
  11. logger.info("job1-end");
  12. }
  13. }

这样就把JobDetail和之前定义的QuartzJob关联起来了。

2、配置JobDetail

JobDetail可以使用MethodInvokingJobDetailFactoryBean或者JobDetailFactoryBean配置,配置工作需要在一个Spring配置类中完成,可以定义一个QuartzConfig配置类,首先看MethodInvokingJobDetailFactoryBean的使用:

  1. @Configuration
  2. public class QuartzConfig {
  3. @Bean
  4. public MethodInvokingJobDetailFactoryBean methodInvokingJobDetailFactoryBean() {
  5. MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
  6. // 指定任务类在IoC容器中的Bean名称
  7. bean.setTargetBeanName("quartzJob");
  8. // 指定要执行的方法名称
  9. bean.setTargetMethod("hello");
  10. return bean;
  11. }
  12. }

这样就把JobDetail和之前QuartzJob所定义的任务关联起来了,接下来看JobDetailFactoryBean

  1. @Configuration
  2. public class QuartzConfig {
  3. @Bean
  4. public JobDetailFactoryBean jobDetailFactoryBean() {
  5. JobDetailFactoryBean bean = new JobDetailFactoryBean();
  6. // 指定任务类名称
  7. bean.setJobClass(QuartzJob2.class);
  8. // 准备参数
  9. JobDataMap jobDataMap = new JobDataMap();
  10. jobDataMap.put("date", "2020-8-16");
  11. // 传递参数
  12. bean.setJobDataMap(jobDataMap);
  13. return bean;
  14. }
  15. }

这样就把JobDetail和之前定义的QuartzJob2关联起来了,同时传递了参数。

3、配置Trigger

Trigger同样定义在QuartzConfig配置类里,常用的TriggerSimpleTriggerCronTrigger等,它们分别可以通过SimpleTriggerFactoryBeanCronTriggerFactoryBean来完成配置,先用SimpleTriggerFactoryBean配置的触发器关联MethodInvokingJobDetailFactoryBean配置的JobDetail

  1. @Configuration
  2. public class QuartzConfig {
  3. @Bean
  4. public SimpleTriggerFactoryBean simpleTriggerFactoryBean() {
  5. SimpleTriggerFactoryBean bean = new SimpleTriggerFactoryBean();
  6. bean.setRepeatCount(10);
  7. bean.setRepeatInterval(2000);
  8. // 关联JobDetail
  9. bean.setJobDetail(methodInvokingJobDetailFactoryBean().getObject());
  10. return bean;
  11. }
  12. }

SimpleTriggerFactoryBean的用法比较简单
再用CronTriggerFactoryBean配置的触发器关联JobDetailFactoryBean配置的JobDetail

  1. @Configuration
  2. public class QuartzConfig {
  3. @Bean
  4. public CronTriggerFactoryBean cronTriggerFactoryBean() {
  5. CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
  6. bean.setCronExpression("0/2 * * * * ? 2020");
  7. // 关联JobDetail
  8. bean.setJobDetail(jobDetailFactoryBean().getObject());
  9. return bean;
  10. }
  11. }

CronTriggerFactoryBean可以实现类似Spring中@Scheduledcron表达式的功能,同时支持了年份的配置。

4、配置Scheduler

最后一步就是通过SchedulerFactoryBean来配置Scheduler,来注册Trigger

  1. @Configuration
  2. public class QuartzConfig {
  3. @Bean
  4. public SchedulerFactoryBean schedulerFactoryBean() {
  5. SchedulerFactoryBean bean = new SchedulerFactoryBean();
  6. // 注册两个Trigger
  7. bean.setTriggers(simpleTriggerFactoryBean().getObject(), cronTriggerFactoryBean().getObject());
  8. return bean;
  9. }
  10. }

Quartz会使用异步线程去执行定时任务,不会出现像@Scheduled中定时任务在同一线程同步执行的情况。
到这里Spring Boot集成Quartz整个流程就完成了,运行项目就可以看到定时任务按照预期的效果执行。