Spring Task从入门到精通:定时任务开发完整教程

大家好,我是加洛斯。作为一名程序员👨‍💻,我深深信奉费曼学习法------教,是最好的学 📚。这里是我的知识笔记与分享,旨在把复杂的东西讲明白。如果发现有误🔍,万分欢迎你帮我指出来!废话不多说,正文开始 👇

一、基础知识

Spring TaskSpring 框架提供的任务调度模块,用于实现定时任务功能。它是 Spring3.0 后引入的,基于注解和配置的方式,简化了任务调度的实现。

  • 轻量级:无需引入额外的调度框架(如 Quartz)
  • 注解驱动 :通过 @Scheduled 注解即可定义定时任务
  • 集成简单:与 Spring 框架无缝集成
  • 支持 Cron 表达式:提供灵活的定时规则定义

Spring Task的依赖是org.springframework.boot,这个在我们创建项目的时候就存在的,一般情况下来讲我们不需要额外引入,但是以防万一我们还是在pom文件中查找一下是否存在。

xml 复制代码
<!-- Spring Boot 项目依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

启用定时任务:想要开启定时任务我们需要在main方法中添加注解@EnableScheduling

java 复制代码
@SpringBootApplication
@EnableScheduling  // 开启定时任务支持
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

我们来简单的举个小例子来讲述一下,看这个例子当中我们是不是有很多注解不明白?我们来详细的讲一下。

java 复制代码
@Component
public class TaskTest {
    // 固定频率执行,每5秒执行一次
    @Scheduled(fixedRate = 5000)
    public void executeFixedRate() {
        System.out.println("固定频率任务执行: " + new Date());
    }

    // 固定延迟执行,上次执行完成后延迟3秒执行
    @Scheduled(fixedDelay = 3000)
    public void executeFixedDelay() {
        System.out.println("固定延迟任务执行: " + new Date());
        try {
            Thread.sleep(1000); // 模拟任务执行时间
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 初始延迟2秒后开始执行,然后每10秒执行一次
    @Scheduled(initialDelay = 2000, fixedRate = 10000)
    public void executeWithInitialDelay() {
        System.out.println("带初始延迟的任务执行: " + new Date());
    }
}

二、@Scheduled注解参数详解

@ScheduledSpring Framework 3.0 引入的注解,用于声明一个方法是定时任务方法。这是 Spring Task 调度的核心注解。

java 复制代码
//基本使用
@Component
public class MyScheduledTasks {
    
    @Scheduled(fixedRate = 5000)
    public void myTask() {
        // 任务逻辑
    }
}

2.1 fixedRate(固定频率执行)

作用:以固定频率执行任务,不管上一次任务是否完成。

  1. 固定速率:无论上次任务是否完成,到时间就会启动新执行
  2. 相对开始时间 :从上次开始执行的时间点 计算下一次执行,上一次任务开始执行后,间隔指定时间就会触发下一次执行
  3. 独立线程:默认使用单线程,可能发生任务重叠

@Scheduled(fixedRate = ) 后面可以接受多种类型的值,最常见的是long类型的毫秒值与TimeUnit时间单位(Spring 5.3+)

java 复制代码
@Component
@Slf4j
public class FixedRateExample {
    
    private AtomicInteger counter = new AtomicInteger(0);
    /**
     * 每1秒执行一次
     */
    @Scheduled(fixedRate = 1000)
    public void fixedRateTask() {
        int count = counter.incrementAndGet();
        log.info("fixedRate 任务开始执行,第{}次", count);

        try {
            // 模拟任务执行时间
            Thread.sleep(5000); // 任务执行需要5秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        log.info("fixedRate 任务结束执行,第{}次", count);
    }
}

观察上述代码,我们可以看到,这段代码并没有并发执行 这是为什么呢?

这是因为 Spring 默认使用单线程的线程池执行定时任务

  1. @Scheduled 默认行为
    • Spring 的 @Scheduled 默认使用 ThreadPoolTaskScheduler,默认配置下只有一个线程
    • 默认线程池名称是 scheduling-1(从日志中可以看到)
  2. fixedRate 的原理
    • fixedRate = 1000 意味着 每隔1秒应该开始一次新的执行
    • 但是如果前一个任务还在执行,且没有可用线程,新任务会在队列中等待
    • 在日志中,任务都是串行执行的,因为只有一个线程

如果你确定这段代码可以并且需要并发执行,那么有多种方案可以解决当前的问题。我们最推荐的就是使用异步注解

java 复制代码
@Component
@Slf4j
@EnableAsync  // 启用异步支持
public class TaskTest {
    private AtomicInteger counter = new AtomicInteger(0);
    /**
     * 每1秒执行一次
     */
    @Async  // 添加异步注解
    @Scheduled(fixedRate = 1000)
    public void fixedRateTask() {
        int count = counter.incrementAndGet();
        log.info("fixedRate 任务开始执行,第{}次", count);

        try {
            // 模拟任务执行时间
            Thread.sleep(5000); // 任务执行需要5秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        log.info("fixedRate 任务结束执行,第{}次", count);
    }
}

可以看到启动异步注解后,我们的定时任务并发执行了。但是有一个问题,@Async 注解本身不会固定开启几个线程,它的行为取决于你如何配置线程池。在这种情况下在这种情况下:

  • Spring 会使用 SimpleAsyncTaskExecutor
  • 每个异步任务都会创建一个新线程(无限制)
  • 线程不会重用(执行完就销毁)
  • 这可能会导致创建大量线程,有内存泄露风险

所以我们需要配置线程池 控制线程数量

yaml 复制代码
spring:
  task:
    execution:
      pool:
        core-size: 3      # 核心线程数
        max-size: 5      # 最大线程数
        keep-alive: 60s   # 空闲线程存活时间
      thread-name-prefix: async-

以防有人不知道多线程策略,这里简单讲一下

任务提交顺序:

  1. 先使用核心线程(corePoolSize)
  2. 核心线程满了 → 放入队列(queueCapacity)
  3. 队列满了 → 创建新线程直到最大线程数(maxPoolSize)
  4. 都满了 → 执行拒绝策略

示例配置(core=5, max=10, queue=25):

  • 同时处理5个任务 → 5个核心线程
  • 又来第6-30个任务 → 放入队列(25个)
  • 又来第31-40个任务 → 创建新线程(总共10个线程)
  • 再来任务 → 执行拒绝策略

这样看我们最多只启用了三个线程,线程过少可能也会导致线程阻塞,所以我们需要根据实际情况进行修改。

2.2 fixedDelay(固定延迟执行)

作用:上一次任务执行完成后,延迟指定时间再执行下一次。

  • 保证任务串行执行
  • 不会出现并发执行的情况
java 复制代码
@Component
@Slf4j
public class FixedDelayExample {
    
    private AtomicInteger counter = new AtomicInteger(0);
    
    /**
     * 上次任务执行完成后,延迟3秒再执行
     */
    @Scheduled(fixedDelay = 3000)
    public void fixedDelayTask() {
        int count = counter.incrementAndGet();
        log.info("fixedDelay 任务开始执行,第{}次", count);
        
        try {
            // 模拟任务执行时间
            Thread.sleep(5000); // 任务执行需要5秒
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        
        log.info("fixedDelay 任务结束执行,第{}次", count);
    }
}

2.3 initialDelay(初始延迟)

作用:应用启动后,延迟指定时间再开始执行第一次任务。

  • 通常与 fixedRate 或 fixedDelay 配合使用
  • 避免应用启动时立即执行所有任务
java 复制代码
@Component
@Slf4j
@EnableAsync  // 启用异步支持
public class TaskTest {
    private AtomicInteger counter = new AtomicInteger(0);
    /**
     * 应用启动30秒后开始执行,之后每10秒执行一次
     */
    @Scheduled(initialDelay = 30000, fixedRate = 10000)
    public void taskWithInitialDelay() {
        log.info("带初始延迟的任务执行,当前时间: {}",
                LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
    }
}

2.4 Cron 表达式

Cron 表达式是一个字符串,由 6 或 7 个字段组成,用于定义定时任务的执行时间。

注意:Spring中的Cron表达式与标准Unix Cron的主要区别:

  • 秒字段:Spring支持秒级精度(第一位),标准Cron没有
  • 星期几:Spring中1-7表示周一到周日(或MON-SUN),0和7表示周日
java 复制代码
// 每分钟的第30秒执行
@Scheduled(cron = "30 * * * * *")
public void everyMinuteAt30() {
    log.info("每分钟的第30秒执行");
}

我们可以看到cron后面接了一堆***,这又是什么意思呢?我们看下面的图

特殊字符说明

字符 含义 示例 说明
* 任意值 * * * * * * 每秒执行
? 不指定值 0 0 12 * * ? 仅用于日和星期字段
- 范围 0 0 10-12 * * * 10点、11点、12点
, 多个值 0 0 10,14,16 * * * 10点、14点、16点
/ 步长 0 0/5 * * * * 每5分钟
L 最后 0 0 0 L * ? 每月最后一天
W 工作日 0 0 0 15W * ? 最近的工作日
# 第几个星期几 0 0 0 ? * 2#3 每月第3个周一
  • 不指定值 :这对应表达式中的 ? 问号。在 Cron 中,? 用于 "不关心"或"无特定值" 的字段。它只能用在你不想指定具体值的字段上。
  • 仅用于日和星期字段 :这是一个非常重要的规则说明 。它告诉你 ? 这个符号只能出现在"日期(日)"字段和"星期几"字段中

为什么有这个规则?

因为在 Cron 表达式的逻辑中, "日期(日)"和"星期几"本质上表达的是同一种概念(一个月的哪一天) 。如果你同时指定了"每月15号"和"每个星期二",调度器就会困惑:你到底是想在"每月15号"运行,还是"每个星期二"运行,还是"既是15号又是星期二"才运行?

为了消除这种歧义,Quartz 规定:

  1. 在星期字段使用 * 实际上是允许的,它被视为"不指定具体星期几"

  2. 真正的限制是:不能同时指定具体的日和具体的星期

    • 错误:* * * 15 * MON (指定了15号和周一)
    • 正确:* * * 15 * ?* * * ? * MON
    • 可以* * * * * * (日和星期都是 *,这是允许的)

这些个表达式认识就好,只要不是关小黑屋那种的开发就没必须死磕,现在ai这么发达,直接把你想要的发给ai生成或者解析就行。

来看一堆例子

java 复制代码
@Component
@Slf4j
public class CronExample {
    
    /**
     * 基本Cron表达式示例
     */
    
    // 每5秒执行一次
    @Scheduled(cron = "*/5 * * * * *")
    public void everyFiveSeconds() {
        log.info("每5秒执行一次");
    }
    
    // 每分钟的第30秒执行
    @Scheduled(cron = "30 * * * * *")
    public void everyMinuteAt30() {
        log.info("每分钟的第30秒执行");
    }
    
    // 每小时的第15分钟执行
    @Scheduled(cron = "0 15 * * * *")
    public void everyHourAt15() {
        log.info("每小时的第15分钟执行");
    }
    
    // 每天10:30执行
    @Scheduled(cron = "0 30 10 * * *")
    public void dailyAt10_30() {
        log.info("每天10:30执行");
    }
    
    /**
     * 复杂的Cron表达式示例
     */
    
    // 工作日的9点到17点,每半小时执行
    @Scheduled(cron = "0 */30 9-17 * * MON-FRI")
    public void workHoursTask() {
        log.info("工作时间任务执行");
    }
    
    // 每月1号凌晨2点执行
    @Scheduled(cron = "0 0 2 1 * ?")
    public void monthlyTask() {
        log.info("每月任务执行");
    }
    
    // 每个季度的第一天凌晨3点执行
    @Scheduled(cron = "0 0 3 1 1,4,7,10 ?")
    public void quarterlyTask() {
        log.info("季度任务执行");
    }
    
    /**
     * 特殊字符用法
     */
    
    // 'L' 表示最后一天
    @Scheduled(cron = "0 0 23 L * ?")
    public void lastDayOfMonth() {
        log.info("每月最后一天23:00执行");
    }
    
    // 'W' 表示最近的工作日
    @Scheduled(cron = "0 0 12 15W * ?")
    public void nearestWeekday() {
        log.info("每月15号最近的工作日12:00执行");
    }
    
    // '#' 表示第几个星期几
    @Scheduled(cron = "0 0 10 ? * 2#2")  // 每月第二个星期一10:00
    public void secondMondayOfMonth() {
        log.info("每月第二个星期一执行");
    }
    
    /**
     * 从配置文件读取Cron表达式
     */
    @Scheduled(cron = "${task.cron.expression:0 0/30 * * * *}")
    public void configurableCronTask() {
        log.info("可配置的Cron任务执行");
    }
    
    /**
     * 使用Zone指定时区
     */
    @Scheduled(cron = "0 0 9 * * *", zone = "Asia/Shanghai")
    public void timezoneSpecificTask() {
        log.info("上海时区每天9:00执行");
    }
    
    /**
     * 动态Cron表达式(使用SpEL)
     */
    @Scheduled(cron = "#{@cronExpressionProvider.getCron()}")
    public void dynamicCronTask() {
        log.info("动态Cron表达式任务执行");
    }
}

三、参数组合使用

3.1 参数优先级说明

  • cron 参数优先级最高,如果设置了 cron,其他时间参数会被忽略
  • fixedRatefixedDelay 不能同时使用
  • initialDelay 只能与 fixedRatefixedDelay 一起使用
java 复制代码
@Component
@Slf4j
public class ParameterCombinationExample {
    
    /**
     * 正确的组合:initialDelay + fixedRate
     */
    @Scheduled(initialDelay = 10000, fixedRate = 5000)
    public void validCombination1() {
        log.info("initialDelay + fixedRate 组合任务");
    }
    
    /**
     * 正确的组合:initialDelay + fixedDelay
     */
    @Scheduled(initialDelay = 5000, fixedDelay = 3000)
    public void validCombination2() {
        log.info("initialDelay + fixedDelay 组合任务");
    }
    
    /**
     * 错误的组合:fixedRate + fixedDelay (编译不会报错,但运行时只会生效一个)
     */
    @Scheduled(fixedRate = 5000, fixedDelay = 3000)
    public void invalidCombination() {
        log.warn("fixedRate 和 fixedDelay 同时设置,这是不推荐的!");
        // 实际上只会按照 fixedDelay 执行
    }
    
    /**
     * Cron表达式与其他参数组合(cron优先级高,其他参数会被忽略)
     */
    @Scheduled(cron = "0 */5 * * * *", fixedRate = 10000)
    public void cronWithOtherParams() {
        log.info("设置了cron和fixedRate,只会按照cron执行");
        // fixedRate 参数会被忽略
    }
}

四、动态配置参数

4.1 使用配置文件

yaml 复制代码
# application.yml
scheduled:
  tasks:
    data-sync:
      enabled: true
      cron: "0 */30 * * * *"
      initial-delay: 10000
    report-generation:
      enabled: false
      cron: "0 0 2 * * *"
    cache-refresh:
      fixed-rate: 300000  # 5分钟
java 复制代码
@Component
@Slf4j
@ConditionalOnProperty(name = "scheduled.tasks.data-sync.enabled", havingValue = "true")
public class ConfigurableScheduledTask {
    
    /**
     * 从YAML配置文件读取所有参数
     */
    @Scheduled(
        cron = "${scheduled.tasks.data-sync.cron}",
        initialDelayString = "${scheduled.tasks.data-sync.initial-delay:0}"
    )
    public void dataSyncTask() {
        log.info("数据同步任务执行");
    }
    
    /**
     * 使用默认值的配置
     */
    @Scheduled(fixedRateString = "${scheduled.tasks.cache-refresh.fixed-rate:600000}")
    public void cacheRefreshTask() {
        log.info("缓存刷新任务执行");
    }
}

4.2 使用SpEL表达式

java 复制代码
@Component
@Slf4j
public class SpELExample {
    
    private final Environment environment;
    
    public SpELExample(Environment environment) {
        this.environment = environment;
    }
    
    /**
     * 使用SpEL读取环境变量
     */
    @Scheduled(fixedRateString = "#{${task.interval.base} * ${task.interval.multiplier:1}}")
    public void spelCalculationTask() {
        log.info("SpEL计算间隔的任务执行");
    }
    
    /**
     * 根据条件动态设置间隔
     */
    @Scheduled(fixedDelayString = "#{environment.getProperty('task.mode') == 'fast' ? 1000 : 5000}")
    public void conditionalIntervalTask() {
        log.info("条件间隔任务执行");
    }
    
    /**
     * 调用Bean方法获取配置
     */
    @Scheduled(cron = "#{@scheduleConfig.getDataExportCron()}")
    public void dataExportTask() {
        log.info("数据导出任务执行");
    }
    
    /**
     * 复杂的SpEL表达式
     */
    @Scheduled(
        initialDelayString = "#{T(java.lang.Math).random() * 10000}",
        fixedRateString = "#{5000 + T(java.lang.Math).random() * 5000}"
    )
    public void randomIntervalTask() {
        log.info("随机间隔任务执行");
    }
}

@Component
class ScheduleConfig {
    
    @Value("${app.environment}")
    private String environment;
    
    public String getDataExportCron() {
        if ("prod".equals(environment)) {
            return "0 0 2 * * *";  // 生产环境:每天凌晨2点
        } else {
            return "0 */30 * * * *";  // 测试环境:每30分钟
        }
    }
}

五、其他知识与注意事项

5.1 多个@Scheduled注解

Spring 4.0 开始支持同一个方法上使用多个 @Scheduled 注解:

java 复制代码
@Component
@Slf4j
public class MultipleSchedulesExample {
    
    /**
     * 同一个方法多个调度规则
     * 方法会在多个时间点被调用
     */
    @Scheduled(cron = "0 0 9 * * *")   // 每天9点
    @Scheduled(cron = "0 0 18 * * *")  // 每天18点
    @Scheduled(cron = "0 0/30 9-18 * * MON-FRI")  // 工作日每30分钟
    public void multiScheduledTask() {
        log.info("多调度任务执行,当前时间: {}", LocalDateTime.now());
    }
    
    /**
     * 使用 @Schedules 注解(与上面等价)
     */
    @Schedules({
        @Scheduled(fixedRate = 5000),
        @Scheduled(fixedDelay = 10000),
        @Scheduled(cron = "0 0 12 * * *")
    })
    public void usingSchedulesAnnotation() {
        log.info("使用 @Schedules 注解的任务");
    }
}

5.2 与 @Transactional 结合

事务注解要放在 @Scheduled 下面

java 复制代码
@Component
@Slf4j
public class TransactionalScheduledTask {
    
    @Autowired
    private UserRepository userRepository;
    
    /**
     * 带事务的定时任务
     * 注意:事务注解要放在 @Scheduled 下面
     */
    @Scheduled(cron = "0 0 4 * * *")  // 每天凌晨4点
    @Transactional
    public void cleanupInactiveUsers() {
        log.info("开始清理非活跃用户");
        
        try {
            // 删除30天未登录的用户
            LocalDate cutoffDate = LocalDate.now().minusDays(30);
            int deletedCount = userRepository.deleteInactiveUsers(cutoffDate);
            
            log.info("清理完成,删除 {} 个非活跃用户", deletedCount);
        } catch (Exception e) {
            log.error("清理非活跃用户失败", e);
            // 事务会自动回滚
            throw e;
        }
    }
    
    /**
     * 使用 REQUIRES_NEW 事务传播级别
     * 每次执行都是独立的事务
     */
    @Scheduled(fixedRate = 60000)  // 每分钟
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateStatistics() {
        // 更新统计信息,即使失败也不影响其他事务
        log.info("更新统计信息");
    }
}

5.3 常见陷阱

java 复制代码
@Component
@Slf4j
public class CommonPitfalls {
    
    /**
     * 陷阱1:长时间运行的任务阻塞其他任务
     * 解决方案:使用异步执行或调整线程池
     */
    @Scheduled(fixedRate = 1000)
    public void longRunningTask() {
        try {
            Thread.sleep(5000);  // 任务执行5秒
            log.info("长时间任务完成");
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    
    /**
     * 陷阱2:任务抛出异常导致后续任务不再执行
     * 解决方案:捕获异常并处理
     */
    @Scheduled(fixedDelay = 2000)
    public void exceptionProneTask() {
        try {
            // 可能抛出异常的代码
            if (Math.random() > 0.8) {
                throw new RuntimeException("随机异常");
            }
            log.info("任务正常执行");
        } catch (Exception e) {
            log.error("任务执行异常,但已处理", e);
            // 可以选择重试、记录日志、发送告警等
        }
    }
    
    /**
     * 陷阱3:不正确的Cron表达式
     */
    @Scheduled(cron = "0 0 * * * *")  // 正确:每小时执行
    public void correctCron() {
        log.info("正确的Cron任务");
    }
    
    // @Scheduled(cron = "0 0 * * *")  // 错误:缺少秒字段
    // public void incorrectCron() {
    //     log.info("这个不会执行");
    // }
    
    /**
     * 陷阱4:Bean未实例化就尝试调度
     * 确保@Component或@Service注解被扫描到
     */
    
    /**
     * 陷阱5:忘记启用调度
     * 必须在配置类上添加 @EnableScheduling
     */
}
相关推荐
用户2055405915052 小时前
嵌入式项目之温湿度闹钟
后端
用户298698530142 小时前
C# 中如何从 URL 下载 Word 文档:基于 Spire.Doc 的高效解决方案
后端·c#·.net
月明长歌2 小时前
【码道初阶】Leetcode155踩坑最小栈问题:最小栈:算法对了,却输给了 Java 的 “==“?
java·算法·
小飞Coding2 小时前
你写的 equals() 和 hashCode(),正在悄悄吃掉你的数据!
java·后端
想用offer打牌2 小时前
一站式了解http1.1,http2.0和http3.0
后端·网络协议·面试
dragoooon342 小时前
[C++——lesson26.「多态」]
java·c++·学习方法·多态
计算机学姐2 小时前
基于SSM的网上花店销售系统【2026最新】
java·vue.js·mysql·java-ee·tomcat·intellij-idea·mybatis
.墨迹.2 小时前
汇总笔试题
java
用户68545375977692 小时前
别再用低效方式读取数据了,这4种Pandas方法让你效率提升10倍
后端