Spring Scheduler 定时任务常用方法详解

本贴最后更新于 707 天前,其中的信息可能已经沧海桑田

1. 普通定时任务

新建一个 springboot 项目,在启动类上添加注解 @EnableScheduling
image.png

然后新建定时任务类

import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Component @Slf4j public class SimpleSchedule { public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; @Scheduled(cron = "0/10 * * * * ? ") public void testScheduler() { log.info("定时任务开始时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS))); try { log.info("定时任务具体业务逻辑,模拟业务逻辑处理......"); System.out.println("静态定时任务执行啦!!!!!!!"); Thread.sleep(1000); } catch (InterruptedException e) { log.error("定时任务执行失败", e); Thread.currentThread().interrupt(); } log.info("定时任务结束时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS))); } }

启动 spring, 可以看到定时任务运行情况
image.png

2.动态定时任务

首先我们将 SimpleSchedule 类中的定时任务时间改一下 @Scheduled(cron = "0 0 1 * * ?")//每天晚上一点执行 防止影响后面的日志查看。

在正常的使用中,难免会遇到需要修改定时设置的时候,这时候就需要动态定时任务,为了方便测试,我们先将 cron 表达式写在配置文件 application.yml 中

server: port: 8080 scheduler: # 定时任务的时间 testCron: 0/10 * * * * ?

然后编写动态定时任务类,需要实现接口:org.springframework.scheduling.annotation.SchedulingConfigurer
从接口代码上看,其包含注解 @FunctionalInterface 是函数式接口,是可以使用 lambda 表达式来实现,我们实现他的代码
image.png

import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import org.springframework.scheduling.support.CronTrigger; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; /** * @author windxiao */ @Component @Slf4j @Getter @Setter public class ConfigSchedule implements SchedulingConfigurer { public static final String YYYY_MM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"; @Value("${scheduler.testCron}") private String testCron; @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { // 使用cron表达式可以动态地设置循环间隔 taskRegistrar.addTriggerTask(() -> { log.info(">>>>>>动态定时任务开始时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS))); try { // 定时任务具体业务逻辑,模拟业务逻辑处理 log.info("动态定时任务具体业务逻辑,模拟业务逻辑处理......"); System.out.println("动态定时任务执行啦!!!!!!!"); Thread.sleep(1000); } catch (InterruptedException e) { log.error("动态定时任务处理失败", e); Thread.currentThread().interrupt(); } log.info(">>>>>>动态定时任务结束时间: {}", LocalDateTime.now().format(DateTimeFormatter.ofPattern(YYYY_MM_DD_HH_MM_SS))); }, triggerContext -> { // 使用CronTrigger触发器,可动态修改cron表达式来操作循环规则 CronTrigger cronTrigger = new CronTrigger(testCron); return cronTrigger.nextExecutionTime(triggerContext); }); } }

因为要动态设置,我们给他写一个 controller 来实现动态设置

import com.example.demo01.scheduler.ConfigSchedule; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * @author windxiao */ @Slf4j @RestController @RequestMapping("scheduler") public class ScheduleController { @Autowired ConfigSchedule configSchedule; @GetMapping("/set") public void setCron(@RequestParam("cron") String cron) { configSchedule.setTestCron(cron); } }

用 controller 记得导入 spring-boot-starter-web 模块

然后我们启动项目
image.png

可以看到,启动后每 10s 执行一次,我们通过 postman 修改时间
image.png

再看后台日志
image.png

发现定时任务已经被修改为 5s 执行一次

3.同步的定时任务

首先我们还是将上一个定时任务时间换一下,换成 0 0 1 * * ?

我们在一个类中添加两个定时任务,一个每 15 秒执行一次,假设他要执行 10 秒;
第二个每 3 秒执行一次,代码如下:

import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.text.SimpleDateFormat; import java.util.Date; /** * @author windxiao */ @Slf4j @Component public class BlockSchedule { /** * 定时任务1,每15秒执行一次,会执行10秒(造成10秒阻塞) * @throws InterruptedException InterruptedException */ @Scheduled(cron = "0/15 * * * * ?") public void taskCron1() throws InterruptedException { SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); log.info("taskCron1 - 执行定时任务【0/15 * * * * ?】,时间: " + dateFormat.format(new Date())); //模拟延时 Thread.sleep(10*1000); } /** * 定时任务2,每3秒执行一次 */ @Scheduled(cron = "0/3 * * * * ?") public void taskCron2(){ SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); log.info("taskCron2 - 执行定时任务【0/3 * * * * ?】,时间: " + dateFormat.format(new Date())); } }

先想一想,这段代码的执行结果。

然后我们在看他的执行结果:
image.png

在日志里能看到,先执行了 taskCron1,然后等 taskCron1 的 10s 执行完成后,才执行的 taskCron2,taskCron2 在 taskCron1 执行时被阻塞了,没能执行,那在这种情况下,我们怎么才能让 taskCron2 正常执行呢?
解决方案就是并发处理定时任务,每个线程执行一个定时任务,那自然不会阻塞,所以我们创建一个线程池,并且把他注册到 spring 容器中,代码如下:

import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.Executor; import java.util.concurrent.ThreadPoolExecutor; /** * @author windxiao */ @Slf4j @EnableAsync @Configuration public class ScheduleThreadPool { @Bean(name = "asyncScheduleServiceExecutor") public Executor asyncScheduleServiceExecutor() { // SpringBoot项目,可使用Spring提供的对 ThreadPoolExecutor 封装的线程池 ThreadPoolTaskExecutor: ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // 配置核心线程数 executor.setCorePoolSize(5); // 配置最大线程数 executor.setMaxPoolSize(10); // 配置队列大小 executor.setQueueCapacity(20); // 配置线程池中的线程的名称前缀(方便排查问题) executor.setThreadNamePrefix("async-scheduler-service-"); // 配置线程拒绝策略 // rejection-policy:当pool已经达到max size的时候,如何处理新任务 // 1.CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行。 // "该策略既不会抛弃任务,也不会抛出异常,而是将任务回推到调用者。"顾名思义,在饱和的情况下,调用者会执行该任务(而不是由多线程执行) // 2.AbortPolicy:拒绝策略,直接拒绝抛出异常 // 3.DiscardPolicy:丢弃任务,但是不抛出异常。可以配合这种模式进行自定义的处理方式 // 4.DiscardOldestPolicy:丢弃队列最早的未处理任务,然后重新尝试执行任务 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); //执行初始化 executor.initialize(); return executor; } }

上面的代码使用了 @EnableAsync 注解,这个注解是 Spring 提供的,当线程池被这个注解标记后,这个线程池就开始了异步支持,再给其他需要异步支持的方法上添加 @Async 即可。
线程池创建之后,我们就需要修改 BlockSchedule 中的方法,给 taskCron1()和taskCron2() 添加 @Async(value = "asyncScheduleServiceExecutor")

@Async(value = "asyncScheduleServiceExecutor") @Scheduled(cron = "0/15 * * * * ?") public void taskCron1() throws InterruptedException { SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); log.info("taskCron1 - 执行定时任务【0/15 * * * * ?】,时间: " + dateFormat.format(new Date())); //模拟延时 Thread.sleep(10*1000); } @Async(value = "asyncScheduleServiceExecutor") @Scheduled(cron = "0/3 * * * * ?") public void taskCron2(){ SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); log.info("taskCron2 - 执行定时任务【0/3 * * * * ? 】,时间: " + dateFormat.format(new Date())); }

然后再执行,看日志的结果:

image.png
如图可见,两个定时任务都在正常运行,没有阻塞的情况。

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3196 引用 • 8215 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    948 引用 • 1460 回帖
  • Scheduler
    2 引用 • 1 回帖
  • cron
    11 引用 • 3 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...