Spring Task 是 Spring 框架提供的轻量级定时任务工具,它无需依赖额外的第三方库(如 Quartz),直接集成在 Spring 核心包中。在 SpringBoot 中,Spring Task 提供了自动配置支持,只需少量注解即可快速实现定时任务功能。
一、Spring Task 概述
1.1 核心优势
- 轻量级:无需额外依赖,Spring 核心包自带
- 简单易用:基于注解配置,上手快
- 集成度高:与 Spring 生态无缝集成
- 功能完善:支持多种触发方式、异步执行、异常处理等
1.2 适用场景
- 数据定时备份与清理
- 定时发送邮件/短信通知
- 定时统计报表生成
- 系统状态定时监控
- 定时同步第三方数据
二、案例:5分钟实现第一个定时任务
2.1 环境准备
创建一个标准的 SpringBoot 项目,无需添加额外依赖(SpringBoot 2.x 及以上版本已默认包含 spring-context 依赖)。
2.2 开启定时任务支持
在 SpringBoot 启动类上添加 @EnableScheduling 注解,开启定时任务自动配置:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling // 开启定时任务支持
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApplication.class, args);
}
}
2.3 创建定时任务类
创建一个普通的 Spring Bean,在需要定时执行的方法上添加 @Scheduled 注解:
java
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@Component
public class SimpleTask {
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 每隔5秒执行一次
@Scheduled(fixedRate = 5000)
public void printCurrentTime() {
System.out.println("当前时间:" + LocalDateTime.now().format(formatter));
}
}
2.4 运行测试
启动 SpringBoot 应用,控制台会每隔5秒输出一次当前时间,说明定时任务已成功运行。
三、核心注解详解
3.1 @EnableScheduling
- 作用:开启 Spring 定时任务的自动配置
- 位置:通常添加在启动类或配置类上
- 原理 :该注解会导入
SchedulingConfiguration配置类,自动注册ScheduledAnnotationBeanPostProcessor,用于扫描带有@Scheduled注解的方法并创建定时任务
3.2 @Scheduled
@Scheduled 是最核心的注解,用于标记一个方法为定时任务方法。它提供了多种属性来配置任务的执行规则:
| 属性 | 类型 | 说明 |
|---|---|---|
| cron(重点) | String | 使用 cron 表达式定义执行规则 |
| fixedRate | long | 从上一次执行开始时间算起,间隔指定毫秒数执行 |
| fixedDelay | long | 从上一次执行结束时间算起,间隔指定毫秒数执行 |
| initialDelay | long | 首次执行延迟的毫秒数 |
| zone | String | cron 表达式使用的时区,默认使用服务器本地时区 |
四、定时任务触发方式详解
4.1 fixedRate 固定频率执行
特点 :从上一次任务开始执行的时间点开始计算,间隔指定时间后执行下一次任务。
java
// 每隔5秒执行一次,无论上一次任务执行了多久
@Scheduled(fixedRate = 5000)
public void fixedRateTask() {
System.out.println("fixedRate任务执行:" + LocalDateTime.now());
}
注意 :如果任务执行时间超过了 fixedRate 设置的间隔时间,下一次任务会立即执行,不会等待。例如:任务执行需要8秒,fixedRate=5秒,那么任务会连续执行,没有间隔。
4.2 fixedDelay 固定延迟执行
特点 :从上一次任务执行结束的时间点开始计算,间隔指定时间后执行下一次任务。
java
// 上一次任务执行结束后,延迟3秒执行下一次
@Scheduled(fixedDelay = 3000)
public void fixedDelayTask() {
System.out.println("fixedDelay任务执行:" + LocalDateTime.now());
}
适用场景:任务执行时间不固定,需要确保两次任务之间有固定的间隔时间。
4.3 initialDelay 首次执行延迟
initialDelay 可以与 fixedRate 或 fixedDelay 配合使用,指定应用启动后,首次执行任务的延迟时间。
java
// 应用启动后延迟10秒,然后每隔5秒执行一次
@Scheduled(initialDelay = 10000, fixedRate = 5000)
public void initialDelayTask() {
System.out.println("initialDelay任务执行:" + LocalDateTime.now());
}
4.4 cron 表达式(最灵活)
cron 表达式是一种强大的时间表达式,可以精确到秒,定义复杂的执行规则。
cron 表达式结构
cron 表达式由6或7个字段组成,空格分隔:
秒 分 时 日 月 周 [年]
- 年字段是可选的,通常省略
- 每个字段可以使用特殊字符表示不同的含义
常用特殊字符
*:表示该字段的所有可能值?:表示不指定值,用于日和周字段互斥-:表示范围,:表示枚举多个值/:表示步长
常用 cron 表达式示例
java
// 每天凌晨0点执行
@Scheduled(cron = "0 0 0 * * ?")
public void dailyTask() {}
// 每天上午9点到下午6点,每隔半小时执行一次
@Scheduled(cron = "0 0/30 9-18 * * ?")
public void workHourTask() {}
// 每周一至周五的上午10点15分执行
@Scheduled(cron = "0 15 10 ? * MON-FRI")
public void weekdayTask() {}
// 每月1号凌晨1点执行
@Scheduled(cron = "0 0 1 1 * ?")
public void monthlyTask() {}
// 每10秒执行一次
@Scheduled(cron = "*/10 * * * * ?")
public void every10SecondsTask() {}
在线 cron 表达式生成工具
五、任务执行器(TaskExecutor)配置
5.1 默认执行器的问题
Spring Task 默认使用单线程的任务执行器。如果有多个定时任务,它们会排队执行,一个任务阻塞会导致其他任务延迟执行。
5.2 自定义线程池
创建一个配置类,实现 SchedulingConfigurer 接口,自定义任务执行器:
java
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
@Configuration
public class SchedulerConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
// 核心线程数
scheduler.setPoolSize(10);
// 线程名前缀
scheduler.setThreadNamePrefix("task-scheduler-");
// 等待任务完成后关闭
scheduler.setWaitForTasksToCompleteOnShutdown(true);
// 等待时间
scheduler.setAwaitTerminationSeconds(60);
// 初始化
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
}
5.3 配置参数说明
setPoolSize(int poolSize):设置线程池核心线程数,建议根据任务数量和执行时间调整setThreadNamePrefix(String prefix):设置线程名前缀,方便日志排查setWaitForTasksToCompleteOnShutdown(boolean):设置是否等待任务完成后关闭线程池setAwaitTerminationSeconds(int seconds):设置等待任务完成的最长时间
六、异步定时任务
6.1 需求情况
即使配置了多线程的任务执行器,同一个任务的多次执行仍然是串行的。如果任务执行时间较长,会导致任务堆积。
使用异步定时任务可以让同一个任务的多次执行并行处理。
6.2 开启异步支持
在启动类或配置类上添加 @EnableAsync 注解:
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
@EnableAsync // 开启异步支持
public class TaskApplication {
public static void main(String[] args) {
SpringApplication.run(TaskApplication.class, args);
}
}
6.3 创建异步定时任务
在定时任务方法上添加 @Async 注解:
java
import org.springframework.scheduling.annotation.Async;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
public class AsyncTask {
@Async
@Scheduled(fixedRate = 1000)
public void asyncTask() throws InterruptedException {
System.out.println("异步任务开始执行:" + Thread.currentThread().getName());
// 模拟任务执行3秒
Thread.sleep(3000);
System.out.println("异步任务执行结束:" + Thread.currentThread().getName());
}
}
运行后会发现,即使任务执行需要3秒,仍然会每隔1秒启动一个新的任务,每个任务在不同的线程中执行。
6.4 自定义异步线程池
默认的异步线程池可能无法满足生产环境需求,建议自定义异步线程池:
java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class AsyncConfig {
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 核心线程数
executor.setCorePoolSize(10);
// 最大线程数
executor.setMaxPoolSize(20);
// 队列容量
executor.setQueueCapacity(200);
// 线程存活时间
executor.setKeepAliveSeconds(60);
// 线程名前缀
executor.setThreadNamePrefix("async-task-");
// 拒绝策略:由调用线程执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
executor.initialize();
return executor;
}
}
使用时指定线程池名称:
java
@Async("taskExecutor")
@Scheduled(fixedRate = 1000)
public void asyncTask() {}
七、动态定时任务
7.1 为什么需要动态定时任务
使用 @Scheduled 注解配置的定时任务是静态的,一旦应用启动,执行规则就无法修改。
动态定时任务允许在应用运行时修改任务的执行时间、暂停、恢复和删除任务。
7.2 实现方式
通过 ScheduledTaskRegistrar 注册动态任务:
java
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import java.util.concurrent.ScheduledFuture;
@Configuration
public class DynamicTaskConfig implements SchedulingConfigurer {
private ScheduledTaskRegistrar taskRegistrar;
private ScheduledFuture<?> future;
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
this.taskRegistrar = taskRegistrar;
}
// 添加任务
public void addTask(Runnable task, String cron) {
if (future != null) {
future.cancel(true);
}
future = taskRegistrar.getScheduler().schedule(task, new CronTrigger(cron));
}
// 暂停任务
public void stopTask() {
if (future != null) {
future.cancel(true);
}
}
}
7.3 动态修改任务执行时间
创建一个控制器,提供接口来动态修改任务执行时间:
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
@RestController
public class DynamicTaskController {
@Autowired
private DynamicTaskConfig dynamicTaskConfig;
@GetMapping("/updateTask")
public String updateTask(@RequestParam String cron) {
Runnable task = () -> System.out.println("动态任务执行:" + LocalDateTime.now());
dynamicTaskConfig.addTask(task, cron);
return "任务已更新,新的cron表达式:" + cron;
}
@GetMapping("/stopTask")
public String stopTask() {
dynamicTaskConfig.stopTask();
return "任务已暂停";
}
}
八、任务异常处理
8.1 默认异常处理
默认情况下,如果定时任务方法抛出异常,该任务会终止执行,不会影响其他任务。
8.2 全局异常处理
实现 ErrorHandler 接口,创建全局异常处理器:
java
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.util.ErrorHandler;
import java.lang.reflect.Method;
@Configuration
public class TaskExceptionConfig implements SchedulingConfigurer, AsyncConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setPoolSize(10);
scheduler.setThreadNamePrefix("task-scheduler-");
// 设置异常处理器
scheduler.setErrorHandler(new TaskErrorHandler());
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return new AsyncTaskErrorHandler();
}
// 同步任务异常处理器
public static class TaskErrorHandler implements ErrorHandler {
@Override
public void handleError(Throwable t) {
System.err.println("同步定时任务执行异常:" + t.getMessage());
t.printStackTrace();
}
}
// 异步任务异常处理器
public static class AsyncTaskErrorHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
System.err.println("异步定时任务执行异常:" + method.getName());
ex.printStackTrace();
}
}
}
8.3 方法级异常处理
在定时任务方法内部使用 try-catch 块处理异常:
java
@Scheduled(fixedRate = 5000)
public void taskWithExceptionHandling() {
try {
// 业务逻辑
int result = 1 / 0;
} catch (Exception e) {
System.err.println("任务执行异常:" + e.getMessage());
// 记录日志、发送告警等
}
}
九、任务并发与幂等性
9.1 并发问题
在以下场景中,定时任务可能会出现并发执行的问题:
- 使用异步定时任务
- 应用集群部署
- 任务执行时间超过执行间隔
9.2 幂等性设计
为了避免并发执行导致的数据问题,定时任务必须保证幂等性。常用的实现方式:
- 数据库唯一约束:在关键表上添加唯一索引
- 分布式锁:使用 Redis、Zookeeper 等实现分布式锁
- 状态机:记录任务执行状态,避免重复执行
9.3 Redis 分布式锁实现
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class IdempotentTask {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String LOCK_KEY = "task:lock:idempotent";
private static final long LOCK_EXPIRE = 30; // 锁过期时间,秒
@Scheduled(fixedRate = 5000)
public void idempotentTask() {
// 尝试获取锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(LOCK_KEY, "locked", LOCK_EXPIRE, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 执行业务逻辑
System.out.println("获取到锁,执行任务");
} finally {
// 释放锁
redisTemplate.delete(LOCK_KEY);
}
} else {
System.out.println("未获取到锁,跳过本次执行");
}
}
}
十、集群环境下的定时任务
10.1 集群环境的问题
在集群环境下,每个应用实例都会执行定时任务,导致任务重复执行。
10.2 解决方案
- 分布式锁:如上面的 Redis 分布式锁实现
- 任务调度中心:使用 XXL-Job、Elastic-Job 等专业的分布式任务调度框架
- 单独部署:将定时任务单独部署在一个实例上
10.3 推荐方案
对于简单的定时任务,使用 Redis 分布式锁即可满足需求。对于复杂的任务调度需求,建议使用专业的分布式任务调度框架,如 XXL-Job。
十一、常见问题
11.1 定时任务不执行
可能原因:
- 忘记添加
@EnableScheduling注解 - 定时任务类没有被 Spring 扫描到(没有添加
@Component等注解) - 方法不是 public 的
- 方法有参数
- 方法返回值不是 void
解决方案:检查以上几点,确保配置正确。
11.2 任务执行时间不准确
可能原因:
- 使用了默认的单线程执行器,任务被阻塞
- 服务器时间不准确
- cron 表达式时区设置不正确
解决方案:
- 配置多线程任务执行器
- 校准服务器时间
- 在
@Scheduled注解中指定时区:@Scheduled(cron = "0 0 0 * * ?", zone = "Asia/Shanghai")
11.3 任务重复执行
可能原因:
- 应用集群部署
- 任务执行时间超过执行间隔
- 异步任务没有控制并发
解决方案:
- 使用分布式锁
- 调整任务执行间隔
- 控制异步任务的并发数
十二、总结
- 合理配置线程池:根据任务数量和执行时间调整线程池大小,避免任务阻塞
- 使用异步任务:对于执行时间较长的任务,使用异步执行
- 保证幂等性:所有定时任务都应该设计为幂等的
- 异常处理:添加全局异常处理和方法级异常处理,避免任务终止
- 日志记录:在任务执行前后记录详细日志,方便问题排查
- 监控告警:对任务执行情况进行监控,异常时及时告警
- 避免长任务:尽量将长任务拆分为多个短任务
- 集群环境使用分布式锁:避免任务重复执行