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
     */
}
相关推荐
aircrushin16 小时前
端到端AI决策架构如何重塑实时协作体验?
前端·javascript·后端
苦瓜小生16 小时前
【黑马点评学习笔记 | 实战篇 】| 6-Redis消息队列
redis·笔记·后端
大傻^17 小时前
LangChain4j Spring Boot Starter:自动配置与声明式 Bean 管理
java·人工智能·spring boot·spring·langchain4j
沐硕17 小时前
《基于改进协同过滤与多目标优化的健康饮食推荐系统设计与实现》
java·python·算法·fastapi·多目标优化·饮食推荐·改进协同过滤
yhole17 小时前
springboot 修复 Spring Framework 特定条件下目录遍历漏洞(CVE-2024-38819)
spring boot·后端·spring
BingoGo17 小时前
Laravel 13 正式发布 使用 Laravel AI 无缝平滑升级
后端·php
愣头不青17 小时前
560.和为k的子数组
java·数据结构
共享家952717 小时前
Java入门(String类)
java·开发语言
l软件定制开发工作室17 小时前
Spring开发系列教程(34)——打包Spring Boot应用
java·spring boot·后端·spring·springboot
0xDevNull17 小时前
Spring Boot 循环依赖解决方案完全指南
java·开发语言·spring