SpringBoot定时任务@Scheduled注解详解

项目开发中,经常会遇到定时任务的场景,Spring提供了@Scheduled注解,方便进行定时任务的开发

概述

要使用@Scheduled注解,首先需要在启动类添加@EnableScheduling,启用Spring的计划任务执行功能,这样可以在容器中的任何Spring管理的bean上检测@Scheduled注解,执行计划任务

注解定义

@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

源码解析

配置了@Scheduled注解的方法,Spring的处理是通过注册ScheduledAnnotationBeanPostProcessor来执行,将不同配置参数的任务分配给不同的handler处理,核心代码如下

  • org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization
@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 methodsannotatedMethods.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;
}
  • org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#processScheduled
/*** 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 delaylong 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 expressionString 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 anymoreif (initialDelay < 0) {initialDelay = 0;}// Check fixed delaylong 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 ratelong 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 setAssert.isTrue(processedSchedule, errorMessage);// Finally register the scheduled taskssynchronized (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());}
}
  • org.springframework.scheduling.config.ScheduledTaskRegistrar#scheduleTasks
/*** 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));}}
}

使用详解

定时任务同步/异步执行

定时任务执行默认是单线程模式,会创建一个本地线程池,线程池大小为1。当项目中有多个定时任务时,任务之间会相互等待,同步执行
源码:

// 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));
}

代码示例:

@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));}
}

执行结果:

可以看到,默认情况下,三个任务串行执行,都使用pool-1-thread-1同一个线程池,并且线程只有一个

可以通过实现SchedulingConfigurer接口,手动创建线程池,配置期望的线程数量

示例代码:

@Configuration
public class ScheduledConfig implements SchedulingConfigurer {/*** 任务执行线程池大小*/private static final int TASK_POOL_SIZE = 50;/*** 线程名*/private static final String TASK_THREAD_PREFIX = "test-task-";@Overridepublic void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {ThreadPoolTaskScheduler taskPool = new ThreadPoolTaskScheduler();taskPool.setPoolSize(TASK_POOL_SIZE);taskPool.setThreadNamePrefix(TASK_THREAD_PREFIX);taskPool.initialize();scheduledTaskRegistrar.setTaskScheduler(taskPool);}
}

任务执行结果:

此时任务的执行已经异步化,从自定义线程池中分配线程执行任务,在实际应用中需要考虑实际任务数量,创建相应大小的线程池

fixedRate/fixedDelay区别

  • fixedRate是配置上一次任务执行开始到下一次执行开始的间隔时间,不会等待上一次任务执行完成就会调度下一次任务,将其放入等待队列中

代码示例:

@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是配置的上一次任务执行结束到下一次执行开始的间隔时间,也就是说会等待上一次任务执行结束后,延迟间隔时间,再执行下一次任务

代码示例:

@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是一致的,也是在上一次任务执行结束后,等待间隔时间

代码示例:

@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时执行),若上一次任务没有执行完,则不会调度本次任务,跳过本次执行,等待下一次执行周期

代码示例:

@Slf4j
@Component
public class RunIntervalTestScheduler {@Scheduled(cron = "1 * * * * ?")public void cronRun() throws Exception{log.info("cron run");TimeUnit.SECONDS.sleep(70);}}

执行结果:

上一次任务未执行完毕,则跳过了本次执行

SpringBoot定时任务@Scheduled注解详解相关推荐

  1. spring定时任务@Scheduled注解详解

    1.@Scheduled注解 常见参数: 1.fixedRate参数,传入一个时间间隔,单位是毫秒,这里是指每5s执行一次 @Scheduled(fixedRate = 5000)public voi ...

  2. Spring定时任务 - @Schedule注解详解

    一.@Schedule注解一览 @Scheduled注解共有8个属性(其中有3组只是不同类型的相同配置)和一个常量CRON_DISABLED,源码如下: /*** 标记要调度的方法的注释. 必须准确指 ...

  3. SpringBoot Test及注解详解(含Mockito)

    一.版本差异 Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库,在 Spring Boot 2.2.0 版本之前,spring-boot-starter-test ...

  4. @Scheduled注解详解

    文章目录 1.注解源码 2.注解参数 3.示例 cron zone fixedRate fixedRateString fixedDelay fixedDelayString initialDelay ...

  5. SpringBoot 定时任务cron表达式详解

    cron表达式 顺序 秒 分钟 小时 日期 月份 星期 年(可选) 取值 0-59 0-59 0-23 1-30(31) 1-12 1-7允许特殊字符 , - * / , - * / , - * / ...

  6. 26.SpringBoot事务注解详解

    转自:https://www.cnblogs.com/kesimin/p/9546225.html @Transactional spring 事务注解 1.简单开启事务管理 @EnableTrans ...

  7. spring-boot注解详解(一)

    spring-boot注解详解(一) @SpringBootApplication @SpringBootApplication = (默认属性)@Configuration + @EnableAut ...

  8. 【SpringBoot 】SpringBoot注解详解

    [SpringBoot ]SpringBoot注解详解 一.注解(annotations)列表  @SpringBootApplication:包含了@ComponentScan.@Configura ...

  9. spring-boot注解详解(七)

    @Configuration 从Spring3.0,@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被Annotat ...

最新文章

  1. Android 通过onTouchEvent判断是否为双击事件
  2. GraphQL报错:error: NetworkError when attempting to fetch resource.
  3. bytes数组转string指定编码_一篇文章弄懂Python中所有数组数据类型
  4. ZSH and oh-my-zsh 强强联合
  5. Linux查看文件内容的6种命令
  6. Watchman 的安装
  7. 初学者的SDN学习之路
  8. 解决打印机问题的方法
  9. 计算机等级考试四级--数据库原理
  10. 基于Raft深度优化,腾讯云金融级消息队列CMQ高可靠算法详解
  11. 在Java中实现有账号密码的Http代理访问
  12. 1.TPC-C测试工具一——TPC-C简介
  13. 程序员成功之路 ——The road ahead for programmer(演讲稿)(转)
  14. QT的QTreeWidget节点触发事件(一)
  15. windows搭建Git服务器之Bonobo Git Server
  16. Elasticsearch+X-pack和Java Transport方式连接
  17. 23、基于51单片机温控风扇红外遥控智能温度控制系统设计
  18. SQL Server 2000数据库程序设计
  19. Python爬虫requests模块在线生成请求头
  20. Java计算排名通用方法

热门文章

  1. 我在工地抬杠,抬到了我梦寐以求的996!TvT~
  2. 开心截图js 复习,让梦想照进现实哈
  3. Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provid
  4. jQuery动画效果之-fadeTo
  5. jquery动画效果 animate() 自定义动画 stop() 停止运行动画 fadeIn()/fadeOut()/fadeTo() 淡入淡出
  6. 创业必看——开公司的流程
  7. 基于vue Ant-Design 的表单设计器,快速开发
  8. 什么是API//DLL//NT架构//MessageBoxA函数//WOW64
  9. 特征工程特征预处理归一化与标准化、鸢尾花种类预测代码实现
  10. Java是什么意思?什么是java?