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

【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. 简单的定时任务 如定时清理缓存、定期生成报表、定时发送邮件等。

相关文章:

  • DeepSeek-V3到DeepSeek-R1的演进
  • 如何在Visual Studio和 .NET 7中使用C#配置代理服务器进行网页抓取,并使用HtmlAgilityPack进行HTML解析
  • React学习笔记20
  • 【分布式】冰山(Iceberg)与哈迪(Hudi)对比的基准测试
  • 开发语言漫谈-groovy
  • 二分查找------练习1
  • 使用C++在Qt框架下调用DeepSeek的API接口实现自己的简易桌面小助手
  • mysql5.7及mysql8的一些特性
  • 人工智能(AI)系统化学习路线
  • 在 ASP .NET Core 9.0 中使用 Scalar 创建漂亮的 API 文档
  • 干货!三步搞定 DeepSeek 接入 Siri
  • 给语言模型增加知识逻辑校验智能,识别网络信息增量的垃圾模式
  • 对立统一规律揭示的核心内容
  • AI-Talk开发板之更换串口引脚
  • 算法题(104):数的划分
  • Vue.js 应用的入口文件
  • STM32F103C8T6 -MINI核心板
  • C# SolidWorks 二次开发 -各种菜单命令增加方式
  • 建筑安全员考试:“知识拓展” 关键词驱动的深度备考攻略
  • 物理环境与安全
  • 反制美国钢铝关税!印度拟对美国部分商品征收关税
  • 外交部:正确认识和对待历史是检验日本能否恪守和平发展承诺的重要标准
  • AI观察|从万元到百万元,DeepSeek一体机江湖混战
  • 时隔近4年再出征!长三丙成功发射通信技术试验卫星十九号
  • 甩掉“肥胖刺客”,科学减重指南来了
  • 瑞士联邦主席凯勒-祖特尔、联邦副主席帕姆兰会见何立峰