SpringBoot 中使用 @Scheduled 实现定时任务

540 阅读12分钟

1. 配置方法

在启动类上添加@EnableScheduling注解,表示开启定时任务。

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@EnableAsync
@EnableScheduling
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

新建一个类,在类上添加@Component注解,使该类成为被 Spring 管理的 Bean。

在类中建一个测试方法,将@Scheduled注解添加到方法上,表示定时任务要执行的方法。注意定时任务方法不能有返回值和参数。

使用该注解的定时任务默认是单线程串行执行的,使用的线程池叫做taskScheduler,线程 ID 为scheduling-1。如果开启多个定时任务(也就是多个方法都使用了注解@Scheduled),任务的执行时机会受上一个任务执行时间的影响。

如果需要开启多个定时任务,且要求它们之间的执行互不影响,需要在方法上添加@Async注解(不指定线程池则默认使用线程池task,线程 ID 为task-x。也可以指定自定义线程池),则可以多线程执行定时任务。使用@Async注解的话需要在启动类上添加@EnableAsync注解,表示开启并行执行。

因为 @Transactional 和 @Async 注解的实现都是基于 Spring 的 AOP,而 AOP 的实现是基于动态代理模式实现的。所以方法一定要从另一个类中进行调用,也就是从类的外部调用,类的内部调用是无效的。并且 @Async 注解的方法必须是 public 方法,返回值只能为 void 或者 Future。

package com.example.demo.schedule;

import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class Test {
    @Async
    @Scheduled(...)
    void test() {
        ...
    }
}

2. 使用示例

2.1. fixedRate

连续两次任务的开始时间间隔为2s。如果不使用线程池,则单线程执行,那么两次任务执行开始时间间隔为4s,所以单线程保证不了这个2s间隔。因为任务执行耗时(4s)超过了定时间隔(2s),所以会影响下一次任务的执行。所以需要使用线程池,多线程执行,则连续两次任务执行开始时间间隔为2s,符合预期。

@Async(value = "scheduleThreadPool")
@Scheduled(fixedRate = 2000)
public void fixedRate() {
    log.info("定时间隔任务 fixedRate = {}", LocalDateTime.now());
    try {
        Thread.sleep(4_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.2. fixedDelay

下次任务的开始时间距离上次任务的结束时间间隔为2s。这种适合使用单线程,不适合使用线程池,单线程间隔则为6s。用了线程池,和这个特性相背离了。

@Scheduled(fixedDelay = 2_000)
public void fixedDelay() {
    log.info("延迟定时间隔任务 fixedDelay = {}", LocalDateTime.now());
    try {
        Thread.sleep(4_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.3. initialDelay

首次延迟10s后执行 fixedRate 类型间隔任务,也可以配置为 fixedDelay 类型间隔任务。控件第一次执行之前要延迟的毫秒数

@Scheduled(initialDelay = 10_000, fixedDelay = 1_000)
public void initialDelay() {
    log.info("首次延迟定时间隔任务 initialDelay = {}", LocalDateTime.now());
}

2.4. cron 表达式

使用 cron 表达式指定定时任务执行策略,这里使用线程池也是为了防止任务执行耗时超过了定时间隔,就会影响下一次任务的执行。

@Async(value = "scheduleThreadPool")
@Scheduled(cron = "0/2 * * * * *")
public void testCron() {
    log.info("测试表达式定时任务 testCron = {}", LocalDateTime.now());
    try {
        Thread.sleep(4_000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

2.4.1. 格式

{秒数} {分钟} {小时} {日期} {月份} {星期} {年份(可为空)}

一个 cron 表达式有至少 6 个(也可能 7 个)有空格分隔的时间元素。其中每个元素可以是一个值(如 6),一个连续区间(9-12),一个间隔时间(8-18/4)(/4 表示每隔 4 小时),一个列表(1,3,5),通配符。由于 “月份中的日期”“星期中的日期” 这两个元素互斥的,必须要对其中一个设置 ?

2.4.2. 占位符

{秒数} 和 {分钟}

允许值范围:0~59,不允许为空值,若值不合法,调度器将抛出 SchedulerException 异常。

"*" 代表每隔 1 秒钟触发;

"," 代表在指定的秒数触发,比如 "0,15,45" 代表 0 秒、15 秒和 45 秒时触发任务;

"-" 代表在指定的范围内触发,比如 "25-45" 代表从 25 秒开始触发到 45 秒结束触发,每隔 1 秒触发 1 次。

"/" 代表触发步进(step),"/" 前面的值代表初始值("" 等同 "0"),后面的值代表偏移量,比如:"0/20" 或者 "/20" 代表从 0 秒钟开始,每隔 20 秒钟触发 1 次,即 0 秒触发 1 次,20 秒触发 1 次,40 秒触发 1 次;"5/20" 代表 5 秒触发 1 次,25 秒触发 1 次,45 秒触发 1 次;"10-45/20"代表在 [10,45] 内每步进 20 秒命中的时间点触发,即 10 秒触发 1 次,30 秒触发 1 次。

{小时}

允许值范围:0~23,不允许为空值,若值不合法,调度器将抛出 SchedulerException 异常,占位符和秒数一样。

{日期}

允许值范围:1~31,不允许为空值,若值不合法,调度器将抛出 SchedulerException 异常。

{星期}

允许值范围:1~7(SUN-SAT),1 代表星期天(一星期的第一天),以此类推,7 代表星期六(一星期的最后一天),不允许为空值,若值不合法,调度器将抛出 SchedulerException 异常。

{年份}

允许值范围:1970~2099,允许为空,若值不合法,调度器将抛出 SchedulerException 异常。

注意:除了 {日期}{星期} 可以使用 "?" 来实现互斥,表达无意义的信息之外,其他占位符都要有具体的时间含义,且依赖关系为:年->月->日期(星期)->小时->分钟->秒数

特殊符号

有些子表达式能包含一些范围或列表。例如:子表达式(天(星期))可以为 "MON-FRI","MON,WED,FRI","MON-WED,SAT"。

"*" 字符代表所有可能的值。因此,"*" 在子表达式(月)里表示每个月的含义,"*" 在子表达式(天(星期))表示星期的每一天。

"/" 字符用来指定数值的增量。例如:在子表达式(分钟)里的 "0/15" 表示从第 0 分钟开始,每 15 分钟;在子表达式(分钟)里的 "3/20" 表示从第 3 分钟开始,每 20 分钟(它和 "3,23,43" 的含义一样)。

"?" 字符仅被用于天(月)和天(星期)两个子表达式,表示不指定值。当 2 个子表达式其中之一被指定了值以后,为了避免冲突,需要将另一个子表达式的值设为 "?"。

"L" 字符仅被用于天(月)和天(星期)两个子表达式,它是单词“last”的缩写。但是它在两个子表达式里的含义是不同的。在天(月)子表达式中,"L" 表示一个月的最后一天,在天(星期)子表达式中,"L" 表示一个星期的最后一天,也就是 SAT。如果在 "L" 前有具体的内容,它就具有其他的含义了。例如:"6L" 表示这个月的倒数第 6 天,"FRIL" 表示这个月的最后一个星期五。

注意:在使用 "L" 参数时,不要指定列表或范围,因为这会导致问题。

2.4.3. 示例

"30 * * * * ?" 每 30 秒(半分钟)触发任务。

"30 10 * * * ?" 每小时的 10 分 30 秒触发任务

"30 10 1 * * ?" 每天 1 点 10 分 30 秒触发任务

"30 10 1 20 * ?" 每月 20 号 1 点 10 分 30 秒触发任务

"30 10 1 20 10 ? *" 每年 10 月 20 号 1 点 10 分 30 秒触发任务

"30 10 1 20 10 ? 2011" 2011 年 10 月 20 号 1 点 10 分 30 秒触发任务

"30 10 1 ? 10 * 2011" 2011 年 10 月每天 1 点 10 分 30 秒触发任务

"30 10 1 ? 10 SUN 2011" 2011 年 10 月每周日 1 点 10 分 30 秒触发任务

"15,30,45 * * * * ?" 每 15 秒,30 秒,45 秒时触发任务

"15-45 * * * * ?" 15 到 45 秒内,每秒都触发任务

"15/5 * * * * ?" 每分钟的第 15 秒开始触发,每隔 5 秒触发一次

"15-30/5 * * * * ?" 每分钟的 15 秒到 30 秒之间开始触发,每隔 5 秒触发一次

"0 0/3 * * * ?" 每小时的第 0 分 0 秒开始,每三分钟触发一次

"0 15 10 ? * MON-FRI" 星期一到星期五的 10 点 15 分 0 秒触发任务

"0 15 10 L * ?" 每个月最后一天的 10 点 15 分 0 秒触发任务

"0 15 10 LW * ?" 每个月最后一个工作日的 10 点 15 分 0 秒触发任务

"0 15 10 ? * 5L" 每个月最后一个星期四的 10 点 15 分 0 秒触发任务

"0 15 10 ? * 5#3" 每个月第三周的星期四的 10 点 15 分 0 秒触发任务

"0 0 10,14,16 * * ?" 每天 10 点,14 点,16 点 0 分 0 秒触发任务

"0 0/30 9-17 * * ?" 朝九晚五工作时间内每半小时触发任务

"0 0 12 ? * WED" 每个星期三中午 12 点 0 分 0 秒触发任务

"0 0 12 * * ?" 每天中午 12 点 0 分 0 秒触发任务

"0 15 10 ? * *" 每天上午 10:15 触发任务

"0 15 10 * * ?" 每天上午 10:15 触发任务

"0 15 10 * * ? *" 每天上午 10:15 触发任务

"0 15 10 * * ? 2005" 2005 年的每天上午 10:15 触发任务

"0 * 14 * * ?" 在每天下午 2 点到下午 2:59 期间的每 1 分钟触发一次任务

"0 0/5 14 * * ?" 在每天下午 2 点到下午 2:55 期间的每 5 分钟触发一次任务

"0 0/5 14,18 * * ?" 在每天下午 2 点到 2:55 期间和下午 6 点到 6:55 期间的每 5 分钟触发一次任务

"0 0-5 14 * * ?" 在每天下午 2 点到下午 2:05 期间的每 1 分钟触发一次任务

"0 10,44 14 ? 3 WED" 每年三月的星期三的下午 2:10 和 2:44 分别触发一次任务

"0 15 10 ? * MON-FRI" 周一至周五的上午 10:15 触发一次任务

"0 15 10 15 * ?" 每月 15 日上午 10:15 触发一次任务

"0 15 10 L * ?" 每月最后一日的上午 10:15 触发一次任务

"0 15 10 ? * 6L" 每月的最后一个星期五上午 10:15 触发一次任务

"0 15 10 ? * 6L 2002-2005" 2002 年至 2005 年的每月的最后一个星期五上午 10:15 触发一次任务

"0 15 10 ? * 6#3" 每月的第三个星期五上午 10:15 触发一次任务

3. 源码注释

package org.springframework.scheduling.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

import org.springframework.scheduling.config.ScheduledTaskRegistrar;

/**
 * 标记在要调度的方法上的注解。 
 * 必须指定 {cron}、{fixedDelay} 或 {fixedRate} 这三个属性中的一个。
 *
 * 使用该注解的方法必须是没有参数的。 
 * 方法的返回类型通常是 {void}; 如果不是,则通过调度程序调用时将忽略返回值。
 *
 * {@Scheduled} 注解的处理是通过注册一个 {ScheduledAnnotationBeanPostProcessor} 来执行的。
 * 这可以手动完成,或者更方便的是,通过 {<task:annotation-driven/>} XML 元素或 {@EnableScheduling} 注解来完成。
 *
 * 此注解可用作元注解,以创建具有属性覆盖的自定义组合注解。
 *
 * @author Mark Fisher
 * @author Juergen Hoeller
 * @author Dave Syer
 * @author Chris Beams
 * @author Victor Brown
 * @author Sam Brannen
 * @since 3.0
 * @see EnableScheduling
 * @see ScheduledAnnotationBeanPostProcessor
 * @see Schedules
 */
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

   /**
    * 一个特殊的 cron 表达式值,指示禁用的触发器:{@value}。 
    * 这主要用于与 ${...} 占位符一起使用,允许外部禁用相应的预定方法。
    *
    * @since 5.1
    * @see ScheduledTaskRegistrar#CRON_DISABLED
    */
   String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;


   /**
    * 一个类似 cron 的表达式,扩展了通常的 UN*X 定义以包括在秒、分钟、小时、月中的某天、月和周中的某天的触发器。
    * 例如,{"0 * * * * MON-FRI"} 表示工作日每分钟一次(在分钟的顶部 - 第 0 秒)。
    * 从左到右读取的字段解释如下。
    * 
    * second
    * minute
    * hour
    * day of month
    * month
    * day of week
    * 
    * 特殊值 {#CRON_DISABLED "-"} 表示禁用的 cron 触发器,主要用于由 ${...} 占位符解析的外部指定值。
    *
    * @return 可以解析为 cron 调度的表达式
    * @see org.springframework.scheduling.support.CronExpression#parse(String)
    */
   String cron() default "";

   /**
    * 解析 cron 表达式时将使用的时区。
    * 默认情况下,此属性为空字符串(即,将使用服务器的本地时区)。
    *
    * @return {java.util.TimeZone#getTimeZone(String)} 接受的区域 id,或表示服务器默认时区的空字符串
    * @since 4.0
    * @see org.springframework.scheduling.support.CronTrigger#CronTrigger(String, java.util.TimeZone)
    * @see java.util.TimeZone
    */
   String zone() default "";

   /**
    * 在最后一次调用结束和下一次调用开始之间的固定时间段内执行注解方法。
    * 时间单位默认为毫秒,但可以通过 {#timeUnit} 重写。
    *
    * @return the delay(延时)
    */
   long fixedDelay() default -1;

   /**
    * 在最后一次调用结束和下一次调用开始之间的固定时间段内执行注解方法。
    * 时间单位默认为毫秒,但可以通过 {#timeUnit} 重写。
    * 
    * @return the delay as a String value —— 例如,占位符或符合 {java.time.Duration#parse java.time.Duration} 的值。
    * @since 3.2.2
    */
   String fixedDelayString() default "";

   /**
    * 在两次调用之间以固定的时间间隔执行注解方法。
    * 时间单位默认为毫秒,但可以通过 {#timeUnit} 重写。
    * 
    * @return the period(周期)
    */
   long fixedRate() default -1;

   /**
    * 在两次调用之间以固定的时间间隔执行注解方法。
    * 时间单位默认为毫秒,但可以通过 {#timeUnit} 重写。
    * 
    * @return the period as a String value —— 例如,占位符或符合 {java.time.Duration#parse java.time.Duration} 的值。
    * @since 3.2.2
    */
   String fixedRateString() default "";

   /**
    * 首次执行 {#fixedRate} 或 {#fixedDelay} 任务之前要延迟的时间单位数。
    * 时间单位默认为毫秒,但可以通过 {#timeUnit} 重写。
    *
    * @return the initial
    * @since 3.2
    */
   long initialDelay() default -1;

   /**
    * 首次执行 {#fixedRate} 或 {#fixedDelay} 任务之前要延迟的时间单位数。
    * 时间单位默认为毫秒,但可以通过 {#timeUnit} 重写。
    * 
    * @return the initial delay as a String value —— 例如,占位符或符合 {java.time.Duration#parse java.time.Duration} 的值。
    * @since 3.2.2
    */
   String initialDelayString() default "";

   /**
    * 用于 {#fixedDelay}、{#fixedDelayString}、{#fixedRate}、{#fixedRateString}、{#initialDelay} 和 {#initialDelayString} 的 {TimeUnit}。
    * 默认为 {TimeUnit#MILLISECONDS}。
    * 对于 {#cron() cron 表达式} 和通过 {#fixedDelayString}、{#fixedRateString} 或 {#initialDelayString} 提供的 {java.time.Duration} 值,此属性将被忽略。
    *
    * @return 要使用的 {TimeUnit}
    * @since 5.3.10
    */
   TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

}