文章内容
项目开发中,经常会遇到定时任务的场景,Spring提供了@Scheduled注解,方便进行定时任务的开发。
要使用@Scheduled注解,首先需要在启动类添加@EnableScheduling,启用Spring的计划任务执行功能,这样可以在容器中的任何Spring管理的bean上检测@Scheduled注解,执行计划任务。
一、注解定义
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @Target ({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention (RetentionPolicy.RUNTIME) @Documented @Repeatable (Schedules. class ) public @interface Scheduled { String cron() default "" ; String zone() default "" ; long fixedDelay() default - 1 ; String fixedDelayString() default "" ; long fixedRate() default - 1 ; String fixedRateString() default "" ; long initialDelay() default - 1 ; String initialDelayString() default "" ; } |
二、参数说明
参数 | 参数说明 | 示例 |
cron | 任务执行的cron表达式 | 0/1 * * * * ? |
zone | cron表达时解析使用的时区,默认为服务器的本地时区,使用java.util.TimeZone#getTimeZone(String)方法解析 | GMT-8:00 |
fixedDelay | 上一次任务执行结束到下一次执行开始的间隔时间,单位为ms | 1000 |
fixedDelayString | 上一次任务执行结束到下一次执行开始的间隔时间,使用java.time.Duration#parse解析 | PT15M |
fixedRate | 以固定间隔执行任务,即上一次任务执行开始到下一次执行开始的间隔时间,单位为ms,若在调度任务执行时,上一次任务还未执行完毕,会加入worker队列,等待上一次执行完成后立即执行下一次任务 | 2000 |
fixedRateString | 与fixedRate逻辑一致,只是使用java.time.Duration#parse解析 | PT15M |
initialDelay | 首次任务执行的延迟时间 | 1000 |
initialDelayString | 首次任务执行的延迟时间,使用java.time.Duration#parse解析 | PT15M |
1、cron
该参数接收一个cron表达式,cron表达式是一个字符串,字符串以5或6个空格隔开,分开共6或7个域,每一个域代表一个含义。
2、zone
时区,接收一个java.util.TimeZone#ID。cron表达式会基于该时区解析。默认是一个空字符串,即取服务器所在地的时区。比如我们一般使用的时区Asia/Shanghai。该字段我们一般留空。
3、fixedDelay
上一次执行完毕时间点之后多长时间再执行。
1 | @Scheduled (fixedDelay = 5000 ) //上一次执行完毕时间点之后5秒再执行 |
4、fixedDelayString
与 fixedDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
1 | @Scheduled (fixedDelayString = "5000" ) //上一次执行完毕时间点之后5秒再执行 |
占位符的使用(配置文件中有配置:time.fixedDelay=5000):
1 2 3 4 | @Scheduled (fixedDelayString = "${time.fixedDelay}" ) void testFixedDelayString() { System.out.println( "Execute at " + System.currentTimeMillis()); } |
5、fixedRate
上一次开始执行时间点之后多长时间再执行。
1 | @Scheduled (fixedRate = 5000 ) //上一次开始执行时间点之后5秒再执行 |
6、fixedRateString
与 fixedRate 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
7、initialDelay
第一次延迟多长时间后再执行。
1 | @Scheduled (initialDelay= 1000 , fixedRate= 5000 ) //第一次延迟1秒后执行,之后按fixedRate的规则每5秒执行一次 |
8、initialDelayString
与 initialDelay 意思相同,只是使用字符串的形式。唯一不同的是支持占位符。
三、cron表达式语法
1 | [秒] [分] [小时] [日] [月] [周] [年] |
注:[年]不是必须的域,可以省略[年],则一共6个域
序号 | 说明 | 必填 | 允许填写的值 | 允许的通配符 |
1 | 秒 | 是 | 0-59 | , – * / |
2 | 分 | 是 | 0-59 | , – * / |
3 | 时 | 是 | 0-23 | , – * / |
4 | 日 | 是 | 1-31 | , – * ? / L W |
5 | 月 | 是 | 1-12 / JAN-DEC | , – * / |
6 | 周 | 是 | 1-7 or SUN-SAT | , – * ? / L # |
7 | 年 | 否 | 1970-2099 | , – * / |
1、通配符说明
- *:表示所有值。 例如:在分的字段上设置 *,表示每一分钟都会触发。
- ?:表示不指定值。使用的场景为不需要关心当前设置这个字段的值。例如:要在每月的10号触发一个操作,但不关心是周几,所以需要周位置的那个字段设置为”?” 具体设置为 0 0 0 10 * ?
- –:表示区间。例如 在小时上设置 “10-12”,表示 10,11,12点都会触发。
- ,:表示指定多个值,例如在周字段上设置 “MON,WED,FRI” 表示周一,周三和周五触发
- /:用于递增触发。如在秒上面设置”5/15” 表示从5秒开始,每增15秒触发(5,20,35,50)。 在日字段上设置’1/3’所示每月1号开始,每隔三天触发一次。
- L:表示最后的意思。在日字段设置上,表示当月的最后一天(依据当前月份,如果是二月还会依据是否是润年[leap]), 在周字段上表示星期六,相当于”7”或”SAT”。如果在”L”前加上数字,则表示该数据的最后一个。例如在周字段上设置”6L”这样的格式,则表示“本月最后一个星期五”
- W:表示离指定日期的最近那个工作日(周一至周五). 例如在日字段上置”15W”,表示离每月15号最近的那个工作日触发。如果15号正好是周六,则找最近的周五(14号)触发, 如果15号是周未,则找最近的下周一(16号)触发.如果15号正好在工作日(周一至周五),则就在该天触发。如果指定格式为 “1W”,它则表示每月1号往后最近的工作日触发。如果1号正是周六,则将在3号下周一触发。(注,”W”前只能设置具体的数字,不允许区间”-“)。
- #:序号(表示每月的第几个周几),例如在周字段上设置”6#3”表示在每月的第三个周六.注意如果指定”#5”,正好第五周没有周六,则不会触发该配置(用在母亲节和父亲节再合适不过了) ;小提示:’L’和 ‘W’可以一组合使用。如果在日字段上设置”LW”,则表示在本月的最后一个工作日触发;周字段的设置,若使用英文字母是不区分大小写的,即MON与mon相同。
示例:
每隔5秒执行一次:*/5 * * * * ?
每隔1分钟执行一次:0 */1 * * * ?
每天23点执行一次:0 0 23 * * ?
每天凌晨1点执行一次:0 0 1 * * ?
每月1号凌晨1点执行一次:0 0 1 1 * ?
每月最后一天23点执行一次:0 0 23 L * ?
每周星期六凌晨1点实行一次:0 0 1 ? * L
在26分、29分、33分执行一次:0 26,29,33 * * * ?
每天的0点、13点、18点、21点都执行一次:0 0 0,13,18,21 * * ?
2、cron表达式使用占位符
cron属性接收的cron表达式支持占位符。
配置文件:
1 2 3 | time: cron: */5 * * * * * interval: 5 |
每5秒执行一次:
1 2 3 4 5 6 7 8 9 | @Scheduled (cron= "${time.cron}" ) void testPlaceholder1() { System.out.println( "Execute at " + System.currentTimeMillis()); } @Scheduled (cron= "*/${time.interval} * * * * *" ) void testPlaceholder2() { System.out.println( "Execute at " + System.currentTimeMillis()); } |
四、使用详解
1、定时任务同步/异步执行
- 定时任务执行默认是单线程模式,会创建一个本地线程池,线程池大小为1。当项目中有多个定时任务时,任务之间会相互等待,同步执行
源码:
01 02 03 04 05 06 07 08 09 10 11 | // org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks if ( this .taskScheduler == null ) { this .localExecutor = Executors.newSingleThreadScheduledExecutor(); this .taskScheduler = new ConcurrentTaskScheduler( this .localExecutor); } // java.util.concurrent.Executors#newSingleThreadScheduledExecutor() public static ScheduledExecutorService newSingleThreadScheduledExecutor() { return new DelegatedScheduledExecutorService ( new ScheduledThreadPoolExecutor( 1 )); } |
代码示例:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | @Slf4j @Component public class RunIntervalTestScheduler { @Scheduled (cron = "0/1 * * * * ?" ) public void singleThreadTest1() { log.info( "singleThreadTest1" ); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos( 1 )); } @Scheduled (cron = "0/1 * * * * ?" ) public void singleThreadTest2() { log.info( "singleThreadTest2" ); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos( 1 )); } @Scheduled (cron = "0/1 * * * * ?" ) public void singleThreadTest3() { log.info( "singleThreadTest3" ); LockSupport.parkNanos(TimeUnit.SECONDS.toNanos( 1 )); } } |
- 通过实现SchedulingConfigurer接口,手动创建线程池,配置期望的线程数量
示例代码:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 | @Configuration public class ScheduledConfig implements SchedulingConfigurer { /** * 任务执行线程池大小 */ private static final int TASK_POOL_SIZE = 50 ; /** * 线程名 */ private static final String TASK_THREAD_PREFIX = "test-task-" ; @Override public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { ThreadPoolTaskScheduler taskPool = new ThreadPoolTaskScheduler(); taskPool.setPoolSize(TASK_POOL_SIZE); taskPool.setThreadNamePrefix(TASK_THREAD_PREFIX); taskPool.initialize(); scheduledTaskRegistrar.setTaskScheduler(taskPool); } } |
此时任务的执行已经异步化,从自定义线程池中分配线程执行任务,在实际应用中需要考虑实际任务数量,创建相应大小的线程池。
2、fixedRate/fixedDelay区别
- fixedRate是配置上一次任务执行开始到下一次执行开始的间隔时间,不会等待上一次任务执行完成就会调度下一次任务,将其放入等待队列中
代码示例:
01 02 03 04 05 06 07 08 09 10 | @Slf4j @Component public class RunIntervalTestScheduler { @Scheduled (initialDelay = 1000 , fixedRate = 1000 ) public void fixedRate() throws Exception { log.info( "fixedRate run" ); TimeUnit.SECONDS.sleep( 3 ); } } |
任务配置的fixedRate为1s,执行日志打印的时间间隔都是3s左右,也就是上一次执行完成后,紧接着就执行下一次任务。
- fixedDelay是配置的上一次任务执行结束到下一次执行开始的间隔时间,也就是说会等待上一次任务执行结束后,延迟间隔时间,再执行下一次任务
代码示例:
01 02 03 04 05 06 07 08 09 10 | @Slf4j @Component public class RunIntervalTestScheduler { @Scheduled (initialDelay = 1000 , fixedDelay = 1000 ) public void fixedDelay() throws Exception { log.info( "fixedDelay run" ); TimeUnit.SECONDS.sleep( 3 ); } } |
任务配置的fixedDelay为1s,执行日志打印的时间间隔都是4s左右,也就是上一次执行完成后,延迟1s后执行下一次任务。
- cron表达式如果配置为类似每秒执行、每分钟执行(例:0/1 * * * * ?, 每秒执行),调度跟fixedDelay是一致的,也是在上一次任务执行结束后,等待间隔时间
代码示例:
01 02 03 04 05 06 07 08 09 10 | @Slf4j @Component public class RunIntervalTestScheduler { @Scheduled (cron = "0/1 * * * * ?" ) public void cronRun() throws Exception{ log.info( "cron run" ); TimeUnit.SECONDS.sleep( 3 ); } } |
执行日志打印的时间间隔都是4s左右,也就是上一次执行完成后,延迟1s后执行下一次任务。
- cron表达式如果配置为固定时间执行(例:1 * * * * ?, 秒数为1时执行),若上一次任务没有执行完,则不会调度本次任务,跳过本次执行,等待下一次执行周期
代码示例:
01 02 03 04 05 06 07 08 09 10 | @Slf4j @Component public class RunIntervalTestScheduler { @Scheduled (cron = "1 * * * * ?" ) public void cronRun() throws Exception{ log.info( "cron run" ); TimeUnit.SECONDS.sleep( 70 ); } } |
上一次任务未执行完毕,则跳过了本次执行。
五、源码解析
配置了@Scheduled注解的方法,Spring的处理是通过注册ScheduledAnnotationBeanPostProcessor来执行,将不同配置参数的任务分配给不同的handler处理,核心代码如下:
1、org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | @Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof AopInfrastructureBean || bean instanceof TaskScheduler || bean instanceof ScheduledExecutorService) { // Ignore AOP infrastructure such as scoped proxies. return bean; } Class<?> targetClass = AopProxyUtils.ultimateTargetClass(bean); if (! this .nonAnnotatedClasses.contains(targetClass) && AnnotationUtils.isCandidateClass(targetClass, Arrays.asList(Scheduled. class , Schedules. class ))) { Map<Method, Set<Scheduled>> annotatedMethods = MethodIntrospector.selectMethods(targetClass, (MethodIntrospector.MetadataLookup<Set<Scheduled>>) method -> { Set<Scheduled> scheduledMethods = AnnotatedElementUtils.getMergedRepeatableAnnotations( method, Scheduled. class , Schedules. class ); return (!scheduledMethods.isEmpty() ? scheduledMethods : null ); }); if (annotatedMethods.isEmpty()) { this .nonAnnotatedClasses.add(targetClass); if (logger.isTraceEnabled()) { logger.trace( "No @Scheduled annotations found on bean class: " + targetClass); } } else { // Non-empty set of methods annotatedMethods.forEach((method, scheduledMethods) -> scheduledMethods.forEach(scheduled -> processScheduled(scheduled, method, bean))); if (logger.isTraceEnabled()) { logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName + "': " + annotatedMethods); } } } return bean; } |
2、org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#processScheduled
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | /** * Process the given {@code @Scheduled} method declaration on the given bean. * @param scheduled the @Scheduled annotation * @param method the method that the annotation has been declared on * @param bean the target bean instance * @see #createRunnable(Object, Method) */ protected void processScheduled(Scheduled scheduled, Method method, Object bean) { try { Runnable runnable = createRunnable(bean, method); boolean processedSchedule = false ; String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required" ; Set<ScheduledTask> tasks = new LinkedHashSet<>( 4 ); // Determine initial delay long initialDelay = scheduled.initialDelay(); String initialDelayString = scheduled.initialDelayString(); if (StringUtils.hasText(initialDelayString)) { Assert.isTrue(initialDelay < 0 , "Specify 'initialDelay' or 'initialDelayString', not both" ); if ( this .embeddedValueResolver != null ) { initialDelayString = this .embeddedValueResolver.resolveStringValue(initialDelayString); } if (StringUtils.hasLength(initialDelayString)) { try { initialDelay = parseDelayAsLong(initialDelayString); } catch (RuntimeException ex) { throw new IllegalArgumentException( "Invalid initialDelayString value " " + initialDelayString + " " - cannot parse into long" ); } } } // Check cron expression String cron = scheduled.cron(); if (StringUtils.hasText(cron)) { String zone = scheduled.zone(); if ( this .embeddedValueResolver != null ) { cron = this .embeddedValueResolver.resolveStringValue(cron); zone = this .embeddedValueResolver.resolveStringValue(zone); } if (StringUtils.hasLength(cron)) { Assert.isTrue(initialDelay == - 1 , "'initialDelay' not supported for cron triggers" ); processedSchedule = true ; if (!Scheduled.CRON_DISABLED.equals(cron)) { TimeZone timeZone; if (StringUtils.hasText(zone)) { timeZone = StringUtils.parseTimeZoneString(zone); } else { timeZone = TimeZone.getDefault(); } tasks.add( this .registrar.scheduleCronTask( new CronTask(runnable, new CronTrigger(cron, timeZone)))); } } } // At this point we don't need to differentiate between initial delay set or not anymore if (initialDelay < 0 ) { initialDelay = 0 ; } // Check fixed delay long fixedDelay = scheduled.fixedDelay(); if (fixedDelay >= 0 ) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true ; tasks.add( this .registrar.scheduleFixedDelayTask( new FixedDelayTask(runnable, fixedDelay, initialDelay))); } String fixedDelayString = scheduled.fixedDelayString(); if (StringUtils.hasText(fixedDelayString)) { if ( this .embeddedValueResolver != null ) { fixedDelayString = this .embeddedValueResolver.resolveStringValue(fixedDelayString); } if (StringUtils.hasLength(fixedDelayString)) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true ; try { fixedDelay = parseDelayAsLong(fixedDelayString); } catch (RuntimeException ex) { throw new IllegalArgumentException( "Invalid fixedDelayString value " " + fixedDelayString + " " - cannot parse into long" ); } tasks.add( this .registrar.scheduleFixedDelayTask( new FixedDelayTask(runnable, fixedDelay, initialDelay))); } } // Check fixed rate long fixedRate = scheduled.fixedRate(); if (fixedRate >= 0 ) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true ; tasks.add( this .registrar.scheduleFixedRateTask( new FixedRateTask(runnable, fixedRate, initialDelay))); } String fixedRateString = scheduled.fixedRateString(); if (StringUtils.hasText(fixedRateString)) { if ( this .embeddedValueResolver != null ) { fixedRateString = this .embeddedValueResolver.resolveStringValue(fixedRateString); } if (StringUtils.hasLength(fixedRateString)) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true ; try { fixedRate = parseDelayAsLong(fixedRateString); } catch (RuntimeException ex) { throw new IllegalArgumentException( "Invalid fixedRateString value " " + fixedRateString + " " - cannot parse into long" ); } tasks.add( this .registrar.scheduleFixedRateTask( new FixedRateTask(runnable, fixedRate, initialDelay))); } } // Check whether we had any attribute set Assert.isTrue(processedSchedule, errorMessage); // Finally register the scheduled tasks synchronized ( this .scheduledTasks) { Set<ScheduledTask> regTasks = this .scheduledTasks.computeIfAbsent(bean, key -> new LinkedHashSet<>( 4 )); regTasks.addAll(tasks); } } catch (IllegalArgumentException ex) { throw new IllegalStateException( "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); } } |
3、org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * Schedule all registered tasks against the underlying * {@linkplain #setTaskScheduler(TaskScheduler) task scheduler}. */ proected void scheduleTasks() { if ( this .taskScheduler == null ) { this .localExecutor = Executors.newSingleThreadScheduledExecutor(); this .taskScheduler = new ConcurrentTaskScheduler( this .localExecutor); } if ( this .triggerTasks != null ) { for (TriggerTask task : this .triggerTasks) { addScheduledTask(scheduleTriggerTask(task)); } } if ( this .cronTasks != null ) { for (CronTask task : this .cronTasks) { addScheduledTask(scheduleCronTask(task)); } } if ( this .fixedRateTasks != null ) { for (IntervalTask task : this .fixedRateTasks) { addScheduledTask(scheduleFixedRateTask(task)); } } if ( this .fixedDelayTasks != null ) { for (IntervalTask task : this .fixedDelayTasks) { addScheduledTask(scheduleFixedDelayTask(task)); } } } |