Spring定时任务介绍


在程序应用中常常会涉及到一些定时任务,如在凌晨等服务低峰期定时执行一些耗时任务,让资源利用实现最大化。在 Spring Boot 中同样也提供定时任务的管理能力,通过注解方式即可实现便捷的定时任务配置。

一、定时任务

1. 注解介绍

通过 @Scheduled 注解即可实现定时任务开启,其作用于方法上,将会根据配置的周期定时执行目标方法,注意需要在项目启动类上添加 @EnableScheduling 注解才能生效。

如下示例中开始了一个定时任务每间隔 10 秒打印一次当前系统时间。

@Component
public class ScheduleTask {
    
    private final Logger logger = LoggerFactory.getLogger(ScheduleTask.class);

    /**
     * 定时任务,每 10 秒打印一次时间
     */
    @Scheduled(fixedRate = 10000)
    public void reportCurrentTime() {
        logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
    }
}

2. 注解参数

在上述的示例中在注解中通过参数 fixedRate 设置了任务间隔周期,下面介绍一下其它常用参数。

(1) fixedRate

fixedRate 用于指定定时任务的间隔时间,如之前的示例中每间隔 10 秒打印一次时间。

(2) fixedDelay

fixedDelay 的类似于 fixedRate,但其计时方式是从上次任务结束后开始。

举个简单的例子,我们创建了一个任务执行完毕需要耗时五分钟,分别使用 fixedRatefixedDelay 创建了两个定时设置每两分钟执行一次。在第一次定时任务触发之后,当过去两分钟后使用 fixedRate 创建的任务将会再次触发,而使用 fixedDelay 创建的任务则需要等待上一次任务执行即三分钟后,然后再等待两分钟后才会触发下一轮。

(3) timeUnit

fixedRatefixedDelay 默认的时间单位都是毫秒,可通过 timeUnit 指定单位从而提高代码的可读性。

如下面的示例中通过 fixedRate 搭配 timeUnit 设置每隔 10 秒打印一次时间。

@Scheduled(fixedRate = 10, timeUnit = TimeUnit.SECONDS)
public void reportTime1() {
    logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
}

二、Cron解析

1. 基本格式

@Scheduled 注解中除了上述的参数外,还支持通过 cron 表达式来配置周期,通过配置 cron 表达式可实现灵活的周期配置,其通过七个 * 用于控制执行频率。

不同位置 * 代表含义如下:

  • 第一个 * 表示秒:取值范围 0-59
  • 第二个 * 表示分钟:取值范围 0-59
  • 第三个 * 表示小时:取值范围 0-23
  • 第四个 * 表示天数:取值范围 1-31
  • 第五个 * 表示月份:取值范围 1-12
  • 第六个 * 表示每周:取值范围 0-6
  • 第七个 * 表示每年,非必填通常省略。

如下示例中通过 cron 表达式设置了每隔 10 秒打印一次时间。

// 每隔 10 秒打印时间
@Scheduled(cron = "0/10 * * * * *")
public void reportCurrentTime() {
    logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
}

cron 表达式同时其支持使用 ${} 方式从 YML 文件读取配置,示例如下:

// 从配置文件读取配置
@Scheduled(cron = "${scheduled.time}")
public void reportCurrentTime() {
    logger.info("定时任务 - 当前时间:" + System.currentTimeMillis()));
}

2. 解析方式

当通过 @Schdule 注解配置后任务将有 Spring 执行统一调度,在 Spring 中提供了 CronTrigger 用于解析 cron 表达式,其通过 nextExecutionTime() 获取下一次执行的时间。

其中 SimpleTriggerContext 用于定义任务的上下文,其包含三个参数 lastScheduledExecutionTimelastActualExecutionTimelastCompletionTime,分别标识上次执行时间、实际执行时间与完成时间。若需要解析计算 cron 表达式两个时间点之间的间隔,可通过连续触发 nextExecutionTime() 计算二者的差值即可。

@Test
public void test1() {
    String express = "0 0/5 * * * *";
    CronTrigger trigger = new CronTrigger(express);

    Date now = new Date();
    System.out.println("Current time" + now);
    SimpleTriggerContext context = new SimpleTriggerContext(now, now, now);
    // 计算下次触发时间
    Date nextDate = trigger.nextExecutionTime(context);
    System.out.println("Next execution time: " + nextDate);
}

三、源码解读

1. 实现方式

为了更详细的了解 @Scheduled 注解的底层实现,下面通过一个简单的示例来分析其实现原理。这边通过 @Scheduled 注解配置一个每间隔 3000 毫秒打印一次当前时间的定时任务,并在 log.info() 输出该任务执行断点并启动项目。

@Slf4j
@Component
public class TestSchedule {

    @Scheduled(fixedRate = 3_000L)
    public void demo1() {
        log.info("1");
    }
}

完成后启动项目在 IDEA 控制台查看堆栈日志,可以看到项目启动后通过 ScheduledAnnotationBeanPostProcessorfinishRegistration() 注册并执行的配置了的定时任务。

为了更清楚的了解的代码执行栈,这里先画了一个简易的流程图。当应用启动时,通过 finishRegistration() 方法调用 resolveSchedulerBean() 获取并注册线程池 taskScheduler 实例,然后随后在 afterPropertiesSet() 中调用 scheduleTasks() 方法执行定时任务。

finishRegistration() 中执行线程池实例的获取注册,并调用 afterPropertiesSet() 触发定时任务执行。
finishRegistration()

resolveSchedulerBean() 核心逻辑即获取定时任务线程池实例,通过传入的 TaskScheduler.class 类型从 IOC 容器中获取 taskScheduler 实例,并将该 bean 实例封装为 NamedBeanHolder 对象。
resolveSchedulerBean()

众所周知,IOC 实现核心即为缓存 Map 记录不同 bean 实例,而 resolveNamedBean() 通过 getBeanNamesForType() 获取 bean 实例名称再有 resolveNamedBean() 实例化对象,需要注意 DefaultListableBeanFactory 中对 resolveNamedBean() 实现了重载要进行区别。

2. 线程配置

在上述提到的实例化线程池实例时,其通过 AbstractBeanFactorydoGetBean() 获取已经缓存的实例化对。通过断点可以看到返回实例其核心线程数为 1,也就意味着其为单线程执行任务。

下面通过示例进行验证,创建两个定时任务,在任务一通过 while(true) 循环执行模拟任务耗时,任务二则每间隔一秒执行简单的日志输出。

@Slf4j
@Component
public class TestSchedule {

    private final AtomicInteger i = new AtomicInteger(0);

    @Scheduled(fixedRate = 3_000L)
    public void demo1() {
        while (true) {
            try {
                int data = i.get();
                if (data > 100) {
                    break;
                }
                log.info(String.valueOf(data));
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
            }
        }
        i.incrementAndGet();
    }

    @Scheduled(fixedRate = 1000L)
    public void demo2() {
        log.info("2");
    }
}

启动程序后查看日志可以看到,demo2 在执行一次之后再无任务输出,表明此时的线程资源已被 demo1 所占用,也验证了上述的单线程的结果。

在之前解析源码的时候提到了定时任务的线程池所用的实例名为 taskScheduler,因此,当程序中涉及到定时任务时,通常手动注入 taskScheduler 线程池实例对象防止线程资源占用的情况。

@Configuration
public class ExecutorConfig {

    @Bean("taskScheduler")
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setThreadNamePrefix("test-");
        // 核心线程数
        scheduler.setPoolSize(8);
        // 空闲线程等待时间
        scheduler.setAwaitTerminationSeconds(60);
        // 取消任务时是否将其从队列中删除
        scheduler.setRemoveOnCancelPolicy(true);
        // 关闭线程池时等待任务执行完毕后
        scheduler.setWaitForTasksToCompleteOnShutdown(true);
        return scheduler;
    }
}

同样以刚才的代码为例,在添加 taskScheduler 实例后重启程序,可以看到此时的日志输出已经正常打印两个定时任务的信息。

四、任务启停

除了上述提到的定时注解,在 Spring Boot 中同时提供了其它一系列类实现一次性的任务执行。

Spring 中提供了 ApplicationRunnerCommandLineRunner 两个类可实现系统启动时触发任务,在项目启动时将会触发二者的 run 方法,通常用于系统的热点数据预加载与数据缓存载入。同理也提供了 ApplicationListener 类可项目停止时触发,常用于资源或缓存的释放与清除。

1. ApplicationRunner

新建类并实现 ApplicationRunner 接口,当 Spring 工程启动时将会执行 run() 方法体的内容,方法体内容仅会触发一次。

通常在项目中利用其实现数据预热功能,或者加载缓存数据至内存之中。

@Component
public class RunnerTask1 implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        try {
            System.out.println(">>> Start from ApplicationRunner, Current time: " + System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2. CommandLineRunner

CommandLineRunner 的使用方式与作用效果与 ApplicationRunner 类似,都是在项目启动时执行。

二者区别在于 run() 方法参数不同,ApplicationRunner 的对应参数是对象,其信息更为详尽,而 CommandLineRunner 的参数则为字符数组。

@Component
public class RunnerTask2 implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        try {
            System.out.println(">>> Start from CommandLineRunner, Current time: " + System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

3. DisposableBean

一个类若实现了 DisposableBean 接口,则当其 bean 对象销毁之前则会触发 destroy() 方法。

@Component
public class TestClass implements DisposableBean {

    @Override
    public void destroy() throws Exception {
        try {
            System.out.println(">>> Destroy from DisposableBean, Current time: " + System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4. ApplicationListener

ApplicationListener 作用与 ApplicationRunner 相反,其在程序关闭之前将会触发 onApplicationEvent() 方法体中的内容,同样也仅执行一次,通常在其中实现资源的释放与对象的销毁工作。

@Component
public class ShutdownTask implements ApplicationListener<ContextClosedEvent> {

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        try {
            System.out.println(">>>Shutdown from ApplicationListener, Current time: " + System.currentTimeMillis());
            TimeUnit.SECONDS.sleep(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录