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

【Spring】Spring Task详解

概念

Spring Task 是 Spring 框架中用于实现任务调度的一个模块,它基于标准的 Java 定时任务机制(如 ScheduledExecutorService),提供了更加简洁和强大的功能。与 Quartz 等复杂的调度框架相比,Spring Task 更加轻量级,适合于中小型项目或简单的定时任务需求。

@Scheduled基本使用

@Scheduled 是 Spring Task 中的核心注解,用于实现定时逻辑。

@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(Schedules.class)
public @interface Scheduled {

    String CRON_DISABLED = ScheduledTaskRegistrar.CRON_DISABLED;


    // cron 表达式
    String cron() default "";

    // 时区
    String zone() default "";

    // 固定延迟执行
    long fixedDelay() default -1;

    String fixedDelayString() default "";

    // 固定频率执行
    long fixedRate() default -1;

    /**
     * Execute the annotated method with a fixed period between invocations.
     * <p>The time unit is milliseconds by default but can be overridden via
     * {@link #timeUnit}.
     * @return the period as a String value &mdash; for example, a placeholder
     * or a {@link java.time.Duration#parse java.time.Duration} compliant value
     * @since 3.2.2
     */
    String fixedRateString() default "";

    // 延迟启动
    long initialDelay() default -1;

    
    String initialDelayString() default "";

    
    TimeUnit timeUnit() default TimeUnit.MILLISECONDS;

}

1、固定延迟执行

import org.springframework.scheduled.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class MyTask {

    @Scheduled(fixedDelay = 5000) // 每次任务完成后等待 5 秒再执行下一次
    public void taskWithFixedDelay() {
        System.out.println("任务执行:fixedDelay");
    }
}

2、固定频率执行

@Scheduled(fixedRate = 5000) // 每隔 5 秒执行一次
public void taskWithFixedRate() {
    System.out.println("任务执行:fixedRate");
}

3、延迟启动

@Scheduled(initialDelay = 10000, fixedRate = 5000) // 启动后延迟 10 秒,然后每隔 5 秒执行一次
public void taskWithInitialDelay() {
    System.out.println("任务执行:initialDelay");
}

4、cron 表达式

@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点执行
public void taskWithCron() {
    System.out.println("任务执行:cron");
}

需要指出的是,使用 @Scheduled 注解的方法,没有形参和返回值:

定时任务的设计初衷是执行一些独立的、无状态的操作,例如:

  • 定时清理缓存
  • 定时生成报表
  • 定时发送邮件

这些操作通常依赖于内部的状态或全局资源(如数据库、文件系统),而不是通过方法参数传递数据。

定时任务的执行结果通常有以下几种处理方式:

  • 直接输出日志 :将结果打印到控制台或写入日志文件。
  • 更新状态 :将结果保存到数据库或其他持久化存储中。
  • 触发其他操作 :通过事件机制或消息队列通知其他模块。

由于这些处理方式都不需要返回值,因此 @Scheduled 方法通常被设计为 void 类型。

cron表达式

cron 表达式是一个字符串,分为 6 或 7 个域。每个域代表一个含义,以空格隔开,有如下两种语法格式:

  • 秒 分 时 每月第几天 月份 周几 年份
  • 秒 分 时 每月第几天 月份 周几

1、秒、分、时

该域可以出现,- * /0-59的整数

  • *:表示匹配该域的任意值,在秒域中使用 * 表示每秒钟都会出发
  • ,:表示列出枚举值,在秒域中使用5,20表示在 5 秒和 20 秒各触发一次
  • -:表示范围,在秒域使用5-20,表示从 5 秒到 20 秒每秒触发一次
  • /:表示起始时间开始触发,然后每隔固定时间触发一次,在秒域中使用0/5,表示第 0 秒触发一次,5 秒、10 秒……各触发一次。

2、日期:DayOfMonth

该域中可以出现,- * / ? L W C8 个字符,以及1-31的整数。

  • c:表示和当前日期相关联,如果在该域使用5c ,则在执行当天的 5 日后执行,且每月的那天都会执行。比如执行日是 10 号,则每月的 15 号都会触发。
  • L:表示最后,在该域使用 L,表示每月的最后一天触发。
  • W:表示工作日,在该域用 15W,表示在最接近本月第 15 天的工作日出发,如果 15 号是周六则 14 号触发;如果 15 号是周日则 16 号出发;如果 15 号是周二,则 15 号触发。 注:该用法只会在当前月计算,不会到下月触发,比如在该域用 31W,31 号是周日,那么在 29 号触发,而不是下月 1 号。在该域用 LW,表示这个月的最后一个工作日触发。
  • ?:在无法确定是哪一天时使用。

3、月份

该域中可出现,- * / 四个字符,以及1-12的整数或JAN-DEC的单词缩写。

4、星期:DayOfWeek

可出现,- * / ? L # C 8 个字符,以及1-7的整数或SUN-SAT单词的缩写,1 代表星期天,7 代表周六。

  • #:# 前面代表星期几,#后面代表一个月的第几周。如5#3表示一个月第三周的星期四。

5、年份

  • ,- * / 4 个字符,以及1970~2099的整数,该域可以省略,表示每年都会触发。

源码分析

Spring Task 使用 ScheduledThreadPoolExecutor 定义工作线程,默认是单线程的,如果在项目中有多个定时任务,可以配置为多个核心线程。

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;

@Configuration
public class TaskConfig {

    @Bean
    public ThreadPoolTaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(10); // 设置线程池大小为 10
        scheduler.setThreadNamePrefix("TaskScheduler-");
        return scheduler;
    }
}

Spring Task 的任务调度机制主要包括以下几个步骤:

  1. 扫描任务 :通过 ScheduledAnnotationBeanPostProcessor 扫描带有 @Scheduled 注解的方法。
  2. 解析任务 :解析注解中的属性(如 cron、fixedRate、fixedDelay),生成对应的任务对象。
  3. 注册任务 :将任务对象注册到 TaskScheduler 中。
  4. 执行任务 :调度器根据任务规则安排任务的执行。

一、任务扫描

通过 ScheduledAnnotationBeanPostProcessor#postProcessAfterInitialization 实现。

ScheduledAnnotationBeanPostProcessor 实现了 BeanPostProcessor 接口,在 Spring 容器完成初始化 Bean 后会扫描所有带有 @Scheduled 注解的方法。

在 ScheduledAnnotationBeanPostProcessor 类中,postProcessAfterInitialization 方法负责扫描 Bean 中的 @Scheduled 注解:

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> scheduledAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(
                      method, Scheduled.class, Schedules.class);
                return (!scheduledAnnotations.isEmpty() ? scheduledAnnotations : 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, scheduledAnnotations) ->
                scheduledAnnotations.forEach(scheduled -> processScheduled(scheduled, method, bean)));
          if (logger.isTraceEnabled()) {
             logger.trace(annotatedMethods.size() + " @Scheduled methods processed on bean '" + beanName +
                   "': " + annotatedMethods);
          }
       }
    }
    return bean;
}

说明:

  • ①:如果 Bean 是 AopInfrastructureBean、TaskScheduler 或 ScheduledExecutorService 类型,则直接返回,不做进一步处理。这是因为这些类型的 Bean 通常是 Spring 内部使用的基础设施组件,不需要额外处理。
  • ②:使用 AnnotationUtils.isCandidateClass 判断目标类是否可能包含 @Scheduled 或 @Schedules 注解。
    • 使用 MethodIntrospector.selectMethods 遍历目标类的所有方法。
    • 对于每个方法,使用 AnnotatedElementUtils.getMergedRepeatableAnnotations 提取 @Scheduled 或 @Schedules 注解。
    • 如果方法上存在注解,则将其存入 annotatedMethods 映射表中。
  • ③:如果没有找到任何带有 @Scheduled 注解的方法,则将目标类加入 nonAnnotatedClasses 集合,避免下次重复扫描
  • ④:如果找到了符合条件的方法,则对每个方法及其注解调用 processScheduled 方法进行解析。

二、任务解析

通过 ScheduledAnnotationBeanPostProcessor#processScheduled 实现。

解析有两个工作:

  • 负责解析 @Scheduled 注解中定义的任务规则(如 cron 表达式、fixedRate、fixedDelay 等),并封装为 Trigger,表示下一次要执行的时间。
  • 将扫描到的定时方法通过反射得到 Method 类对象,并封装到 Runnable 的实现类中,重写 run 方法,在任务调度器执行任务时,调用定时方法。

将 Trigger 和 Runnable 封装为任务,注册到任务调度器中。

解析 @Scheduled 注解的 4 种属性:延迟启动 initialDelay、cron 表达式、固定延迟 fixedDelay、固定频率 fixedRate。

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

       // ①:解析延迟启动 initialDelay
       long initialDelay = convertToMillis(scheduled.initialDelay(), scheduled.timeUnit());
       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 = convertToMillis(initialDelayString, scheduled.timeUnit());
             }
             catch (RuntimeException ex) {
                throw new IllegalArgumentException(
                      "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into long");
             }
          }
       }
        
       // ② 解析 cron 表达式
       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;
       }

       // ③:解析固定延迟 fixedDelay
       long fixedDelay = convertToMillis(scheduled.fixedDelay(), scheduled.timeUnit());
       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 = convertToMillis(fixedDelayString, scheduled.timeUnit());
             }
             catch (RuntimeException ex) {
                throw new IllegalArgumentException(
                      "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into long");
             }
             tasks.add(this.registrar.scheduleFixedDelayTask(new FixedDelayTask(runnable, fixedDelay, initialDelay)));
          }
       }

       // ④:解析固定频率 fixedRate
       long fixedRate = convertToMillis(scheduled.fixedRate(), scheduled.timeUnit());
       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 = convertToMillis(fixedRateString, scheduled.timeUnit());
             }
             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());
    }
}

以解析 cron 表达式为例,解析出 cron 表达式内容、带有 @Scheduled 注解的方法后,将反射得到的注解方法 Method 类的对象和当前的 Bean 封装到 Runnable 接口实现类 ScheduledMethodRunnable 中:

Runnable runnable = createRunnable(bean, method);
protected Runnable createRunnable(Object target, Method method) {
    Assert.isTrue(method.getParameterCount() == 0, "Only no-arg methods may be annotated with @Scheduled");
    Method invocableMethod = AopUtils.selectInvocableMethod(method, target.getClass());
    return new ScheduledMethodRunnable(target, invocableMethod);
}
public ScheduledMethodRunnable(Object target, Method method) {
    this.target = target;
    this.method = method;
}

重写 run 方法:通过反射设置 @Scheduled 注解方法的可访问性,然后调用方法。当定时任务调度器(如 TaskScheduler)执行这个 Runnable 时,实际上会调用 ScheduledMethodRunnable.run() 方法,从而触发目标方法的执行。

@Override
public void run() {
    try {
       ReflectionUtils.makeAccessible(this.method);
       this.method.invoke(this.target);
    }
    catch (InvocationTargetException ex) {
       ReflectionUtils.rethrowRuntimeException(ex.getTargetException());
    }
    catch (IllegalAccessException ex) {
       throw new UndeclaredThrowableException(ex);
    }
}

三、任务注册

通过 ScheduledTaskRegistrar 实现。

ScheduledTaskRegistrar 负责管理所有的调度任务,并将其注册到 TaskScheduler 中。

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

说明:

  • 如果没有显式配置任务调度器 TaskScheduler,会默认使用单线程的 ScheduledThreadPoolExecutor。如果项目中有多个定时任务,最好配置为多线程的。

其中,使用 LinkedHashSet 存储任务,以保证任务的顺序执行

private final Set<ScheduledTask> scheduledTasks = new LinkedHashSet<>(16);

LinkedHashSet 底层基于 LinkedHashMap 实现,LinkedHashMap 通过以下两种数据结构结合实现了高效的存储和有序性维护:

  • 哈希表 :用于快速查找、插入和删除操作。
  • 双向链表 :用于维护元素的插入顺序。

具体来说:

  • 哈希表 :与 HashMap 类似,LinkedHashMap 使用数组 + 链表/红黑树的结构存储键值对。
  • 双向链表 :每个键值对节点除了存储在哈希表中外,还会被链接到一个双向链表中,用于记录插入顺序。

四、任务执行

通过 TaskScheduler 实现。

TaskScheduler 负责实际的任务调度和执行,Spring 默认使用 ThreadPoolTaskScheduler 实现。

在 ThreadPoolTaskScheduler 类中,schedule 方法负责根据任务规则安排任务的执行:

@Override
public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
    ScheduledExecutorService executor = getScheduledExecutor();
    return new ReschedulingRunnable(task, trigger, executor).schedule();
}
  • 触发器机制:Trigger 接口定义了任务的触发规则(如 Cron 表达式或固定间隔)。
  • 任务执行:调度器会根据触发器的规则安排任务的执行时间,并在指定时间调用 Runnable 的 run 方法。

具体来说,ReschedulingRunnable 会使用 Trigger 接口的 nextExecutionTime 方法计算任务的下一次执行时间,并记录时间差。然后,由 ScheduledThreadPoolExecutor 中的线程在指定时间后执行任务。

总结

  • 任务扫描:先扫描带有 @Scheduled 注解的方法
  • 任务解析:扫描到的方法被分为两部分,方法通过反射封装到 Runnable 中,作为任务在任务调度器执行任务时被调用;注解信息如 cron 表达式被封装为 Trigger,在调度器执行任务时会通过 Trigger 计算任务下次执行的时间。两者组装为一个 Task。
  • 任务注册:ScheduledTaskRegistrar 会将任务 Task 保存到一个 LinkedHashSet,以保证任务执行顺序。
  • 任务执行:任务调度器 TaskScheduler,通过 Trigger 获取任务下次执行的时间,然后通过 ScheduledThreadPoolExecutor 线程池中的线程执行任务,会调用 Runnable 中重写 run 方法,也就是执行定时方法。注意,任务调度器默认使用单线程的 ScheduledThreadPoolExecutor,如果项目中有多个定时任务,可以配置为多线程的。

优点&适用场景

优点:

  1. 简单易用 Spring Task 提供了注解驱动的开发方式,无需复杂的配置即可快速实现定时任务。
  2. 轻量级 不依赖外部库,直接集成在 Spring 框架中,适合中小型项目。
  3. 灵活的调度规则 支持多种调度方式,包括固定延迟、固定频率和 Cron 表达式。

缺点

  1. 功能有限 相较于 Quartz 等专业调度框架,Spring Task 的功能较为基础,不支持分布式任务调度。
  2. 单机限制 Spring Task 默认运行在单个 JVM 中,无法直接支持分布式环境下的任务调度。

适用场景

  1. 小型项目 对于不需要复杂调度逻辑的小型项目,Spring Task 是一个很好的选择。
  2. 简单的定时任务 如定时清理缓存、定期生成报表、定时发送邮件等。

http://www.kler.cn/a/597651.html

相关文章:

  • java设计模式之建造者模式《装修启示录》​
  • MAC-在使用@Async注解的方法时,分布式锁管理和释放
  • 嵌入式开发之STM32学习笔记day08
  • Mac:Ant 下载+安装+环境配置(详细讲解)
  • Web3如何影响未来的社交平台:去中心化社交的兴起
  • 区块链在医疗数据共享中的应用:解锁安全与透明的新维度
  • 广度优先搜索(BFS) vs 深度优先搜索(DFS):算法对比与 C++ 实现
  • 洛谷 P10108 [GESP202312 六级] 闯关游戏 题解
  • Android Studio控制台中文乱码解决方案
  • Webpack vs Vite:深度对比与实战示例,如何选择最佳构建工具?
  • LeetCode热题100精讲——Top1:两数之和【哈希】
  • 如何编写一个Spring Boot Starter
  • Ubuntu YOLO5 环境安装
  • UI设计中的大数据可视化:解锁数据背后的秘密
  • 基于深度学习的图像分割项目实践:从理论到应用
  • 基于WebAssembly的浏览器密码套件
  • 【技术选型】三大 Python Web 框架全面对比
  • React学习(进阶)
  • github如何为开源项目作出贡献
  • 为什么后端路由需要携带 /api 作为前缀?前端如何设置基础路径 /api?