当前位置: 首页 > news >正文

SpringBoot定时任务@Scheduled注解详解

SpringBoot定时任务@Scheduled注解详解

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 "";
    ​
    }
    ​

参数说明

  •  

源码解析

  • 配置了@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 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;
    }
    ​

  • 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 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());
        }
    }
     
  • 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-";
    ​
        @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);
        }
    }
    ​

  • 任务执行结果:

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

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

  • 执行结果:

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

相关文章:

  • Gen-LaneNet论文精读总结
  • Spring Cloud Alibaba全家桶——微服务网关Gateway组件
  • 基于微信PC端小程序抓包方法
  • SQL Server 实现邮件发送功能(配置步骤及存储过程源码)
  • 刘禹锡最经典诗文10首,每一首都是千古名作,读懂受益一生
  • mybatisplus快速实现动态数据源切换
  • Vue-条件渲染的基本使用(v-if,v-show,v-else相关指令使用)
  • PMP项目管理认证第一节(备考阶段准备)
  • 【数据结构】TopK,堆排序, --堆的初始化与应用
  • 自然语言处理: 知识图谱的十年
  • 关于加解密、加签、验签等
  • 分布式事务问题
  • 终于解决了悬疑好多年的低版本CAD VBA不能在高版本CAD使用的问题
  • SpringBoot 项目的创建与启动
  • 百科创建:7种有效的百科词条创建技巧
  • Apache Pulsar 2.1 重磅发布
  • es6(二):字符串的扩展
  • ES6简单总结(搭配简单的讲解和小案例)
  • Eureka 2.0 开源流产,真的对你影响很大吗?
  • iOS动画编程-View动画[ 1 ] 基础View动画
  • Java-详解HashMap
  • Laravel Telescope:优雅的应用调试工具
  • mysql_config not found
  • PHP 7 修改了什么呢 -- 2
  • Protobuf3语言指南
  • webpack4 一点通
  • yii2权限控制rbac之rule详细讲解
  • 多线程 start 和 run 方法到底有什么区别?
  • 开源SQL-on-Hadoop系统一览
  • 前端代码风格自动化系列(二)之Commitlint
  • 如何实现 font-size 的响应式
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 腾讯视频格式如何转换成mp4 将下载的qlv文件转换成mp4的方法
  • 原生JS动态加载JS、CSS文件及代码脚本
  • 《TCP IP 详解卷1:协议》阅读笔记 - 第六章
  • ​DB-Engines 12月数据库排名: PostgreSQL有望获得「2020年度数据库」荣誉?
  • ​secrets --- 生成管理密码的安全随机数​
  • # 数据结构
  • ## 临床数据 两两比较 加显著性boxplot加显著性
  • ### Error querying database. Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException
  • #Linux杂记--将Python3的源码编译为.so文件方法与Linux环境下的交叉编译方法
  • (22)C#传智:复习,多态虚方法抽象类接口,静态类,String与StringBuilder,集合泛型List与Dictionary,文件类,结构与类的区别
  • (31)对象的克隆
  • (cos^2 X)的定积分,求积分 ∫sin^2(x) dx
  • (c语言版)滑动窗口 给定一个字符串,只包含字母和数字,按要求找出字符串中的最长(连续)子串的长度
  • (Java岗)秋招打卡!一本学历拿下美团、阿里、快手、米哈游offer
  • (LeetCode 49)Anagrams
  • (pt可视化)利用torch的make_grid进行张量可视化
  • (附源码)springboot助农电商系统 毕业设计 081919
  • (转)Android学习笔记 --- android任务栈和启动模式
  • (转)清华学霸演讲稿:永远不要说你已经尽力了
  • .apk文件,IIS不支持下载解决
  • .Family_物联网
  • .NET Core 将实体类转换为 SQL(ORM 映射)
  • .Net MVC4 上传大文件,并保存表单