Spring Task定時任務開發(fā)實戰(zhàn)教程
大家好,我是加洛斯。作為一名程序員????,我深深信奉費曼學習法——教,是最好的學 ??。這里是我的知識筆記與分享,旨在把復雜的東西講明白。如果發(fā)現有誤??,萬分歡迎你幫我指出來!廢話不多說,正文開始 ??
一、基礎知識
Spring Task 是 Spring 框架提供的任務調度模塊,用于實現定時任務功能。它是 Spring3.0 后引入的,基于注解和配置的方式,簡化了任務調度的實現。
- 輕量級:無需引入額外的調度框架(如 Quartz)
- 注解驅動:通過
@Scheduled注解即可定義定時任務 - 集成簡單:與 Spring 框架無縫集成
- 支持 Cron 表達式:提供靈活的定時規(guī)則定義
Spring Task的依賴是org.springframework.boot,這個在我們創(chuàng)建項目的時候就存在的,一般情況下來講我們不需要額外引入,但是以防萬一我們還是在pom文件中查找一下是否存在。
<!-- Spring Boot 項目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>啟用定時任務:想要開啟定時任務我們需要在main方法中添加注解@EnableScheduling。
@SpringBootApplication
@EnableScheduling // 開啟定時任務支持
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
我們來簡單的舉個小例子來講述一下,看這個例子當中我們是不是有很多注解不明白?我們來詳細的講一下。
@Component
public class TaskTest {
// 固定頻率執(zhí)行,每5秒執(zhí)行一次
@Scheduled(fixedRate = 5000)
public void executeFixedRate() {
System.out.println("固定頻率任務執(zhí)行: " + new Date());
}
// 固定延遲執(zhí)行,上次執(zhí)行完成后延遲3秒執(zhí)行
@Scheduled(fixedDelay = 3000)
public void executeFixedDelay() {
System.out.println("固定延遲任務執(zhí)行: " + new Date());
try {
Thread.sleep(1000); // 模擬任務執(zhí)行時間
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 初始延遲2秒后開始執(zhí)行,然后每10秒執(zhí)行一次
@Scheduled(initialDelay = 2000, fixedRate = 10000)
public void executeWithInitialDelay() {
System.out.println("帶初始延遲的任務執(zhí)行: " + new Date());
}
}
二、@Scheduled注解參數詳解
@Scheduled是 Spring Framework 3.0 引入的注解,用于聲明一個方法是定時任務方法。這是 Spring Task 調度的核心注解。
//基本使用
@Component
public class MyScheduledTasks {
@Scheduled(fixedRate = 5000)
public void myTask() {
// 任務邏輯
}
}2.1 fixedRate(固定頻率執(zhí)行)
作用:以固定頻率執(zhí)行任務,不管上一次任務是否完成。
- 固定速率:無論上次任務是否完成,到時間就會啟動新執(zhí)行
- 相對開始時間:從上次開始執(zhí)行的時間點計算下一次執(zhí)行,上一次任務開始執(zhí)行后,間隔指定時間就會觸發(fā)下一次執(zhí)行
- 獨立線程:默認使用單線程,可能發(fā)生任務重疊
@Scheduled(fixedRate = ) 后面可以接受多種類型的值,最常見的是long類型的毫秒值與TimeUnit時間單位(Spring 5.3+)
@Component
@Slf4j
public class FixedRateExample {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 每1秒執(zhí)行一次
*/
@Scheduled(fixedRate = 1000)
public void fixedRateTask() {
int count = counter.incrementAndGet();
log.info("fixedRate 任務開始執(zhí)行,第{}次", count);
try {
// 模擬任務執(zhí)行時間
Thread.sleep(5000); // 任務執(zhí)行需要5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("fixedRate 任務結束執(zhí)行,第{}次", count);
}
}
觀察上述代碼,我們可以看到,這段代碼并沒有并發(fā)執(zhí)行 這是為什么呢?
這是因為 Spring 默認使用單線程的線程池執(zhí)行定時任務。
- @Scheduled 默認行為:
- Spring 的
@Scheduled默認使用ThreadPoolTaskScheduler,默認配置下只有一個線程 - 默認線程池名稱是
scheduling-1(從日志中可以看到)
- Spring 的
- fixedRate 的原理:
fixedRate = 1000意味著 每隔1秒應該開始一次新的執(zhí)行- 但是如果前一個任務還在執(zhí)行,且沒有可用線程,新任務會在隊列中等待
- 在日志中,任務都是串行執(zhí)行的,因為只有一個線程
如果你確定這段代碼可以并且需要并發(fā)執(zhí)行,那么有多種方案可以解決當前的問題。我們最推薦的就是使用異步注解。
@Component
@Slf4j
@EnableAsync // 啟用異步支持
public class TaskTest {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 每1秒執(zhí)行一次
*/
@Async // 添加異步注解
@Scheduled(fixedRate = 1000)
public void fixedRateTask() {
int count = counter.incrementAndGet();
log.info("fixedRate 任務開始執(zhí)行,第{}次", count);
try {
// 模擬任務執(zhí)行時間
Thread.sleep(5000); // 任務執(zhí)行需要5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("fixedRate 任務結束執(zhí)行,第{}次", count);
}
}
可以看到啟動異步注解后,我們的定時任務并發(fā)執(zhí)行了。但是有一個問題,@Async 注解本身不會固定開啟幾個線程,它的行為取決于你如何配置線程池。在這種情況下在這種情況下:
- Spring 會使用
SimpleAsyncTaskExecutor - 每個異步任務都會創(chuàng)建一個新線程(無限制)
- 線程不會重用(執(zhí)行完就銷毀)
- 這可能會導致創(chuàng)建大量線程,有內存泄露風險
所以我們需要配置線程池控制線程數量
spring:
task:
execution:
pool:
core-size: 3 # 核心線程數
max-size: 5 # 最大線程數
keep-alive: 60s # 空閑線程存活時間
thread-name-prefix: async-以防有人不知道多線程策略,這里簡單講一下
任務提交順序:
- 先使用核心線程(corePoolSize)
- 核心線程滿了 → 放入隊列(queueCapacity)
- 隊列滿了 → 創(chuàng)建新線程直到最大線程數(maxPoolSize)
- 都滿了 → 執(zhí)行拒絕策略
示例配置(core=5, max=10, queue=25):
- 同時處理5個任務 → 5個核心線程
- 又來第6-30個任務 → 放入隊列(25個)
- 又來第31-40個任務 → 創(chuàng)建新線程(總共10個線程)
- 再來任務 → 執(zhí)行拒絕策略

這樣看我們最多只啟用了三個線程,線程過少可能也會導致線程阻塞,所以我們需要根據實際情況進行修改。
2.2 fixedDelay(固定延遲執(zhí)行)
作用:上一次任務執(zhí)行完成后,延遲指定時間再執(zhí)行下一次。
- 保證任務串行執(zhí)行
- 不會出現并發(fā)執(zhí)行的情況
@Component
@Slf4j
public class FixedDelayExample {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 上次任務執(zhí)行完成后,延遲3秒再執(zhí)行
*/
@Scheduled(fixedDelay = 3000)
public void fixedDelayTask() {
int count = counter.incrementAndGet();
log.info("fixedDelay 任務開始執(zhí)行,第{}次", count);
try {
// 模擬任務執(zhí)行時間
Thread.sleep(5000); // 任務執(zhí)行需要5秒
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
log.info("fixedDelay 任務結束執(zhí)行,第{}次", count);
}
}2.3 initialDelay(初始延遲)
作用:應用啟動后,延遲指定時間再開始執(zhí)行第一次任務。
- 通常與 fixedRate 或 fixedDelay 配合使用
- 避免應用啟動時立即執(zhí)行所有任務
@Component
@Slf4j
@EnableAsync // 啟用異步支持
public class TaskTest {
private AtomicInteger counter = new AtomicInteger(0);
/**
* 應用啟動30秒后開始執(zhí)行,之后每10秒執(zhí)行一次
*/
@Scheduled(initialDelay = 30000, fixedRate = 10000)
public void taskWithInitialDelay() {
log.info("帶初始延遲的任務執(zhí)行,當前時間: {}",
LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_TIME));
}
}2.4 Cron 表達式
Cron 表達式是一個字符串,由 6 或 7 個字段組成,用于定義定時任務的執(zhí)行時間。
注意:Spring中的Cron表達式與標準Unix Cron的主要區(qū)別:
- 秒字段:Spring支持秒級精度(第一位),標準Cron沒有
- 星期幾:Spring中1-7表示周一到周日(或MON-SUN),0和7表示周日
// 每分鐘的第30秒執(zhí)行
@Scheduled(cron = "30 * * * * *")
public void everyMinuteAt30() {
log.info("每分鐘的第30秒執(zhí)行");
}
我們可以看到cron后面接了一堆***,這又是什么意思呢?我們看下面的圖

特殊字符說明
| 字符 | 含義 | 示例 | 說明 |
|---|---|---|---|
| * | 任意值 | * * * * * * | 每秒執(zhí)行 |
| ? | 不指定值 | 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 中,?用于 “不關心”或“無特定值” 的字段。它只能用在你不想指定具體值的字段上。僅用于日和星期字段:這是一個非常重要的規(guī)則說明。它告訴你?這個符號只能出現在“日期(日)”字段和“星期幾”字段中。
為什么有這個規(guī)則?
因為在 Cron 表達式的邏輯中, “日期(日)”和“星期幾”本質上表達的是同一種概念(一個月的哪一天) 。如果你同時指定了“每月15號”和“每個星期二”,調度器就會困惑:你到底是想在“每月15號”運行,還是“每個星期二”運行,還是“既是15號又是星期二”才運行?
為了消除這種歧義,Quartz 規(guī)定:
在星期字段使用
*實際上是允許的,它被視為“不指定具體星期幾”真正的限制是:不能同時指定具體的日和具體的星期
- 錯誤:
* * * 15 * MON(指定了15號和周一) - 正確:
* * * 15 * ?或* * * ? * MON - 可以:
* * * * * *(日和星期都是*,這是允許的)
- 錯誤:
這些個表達式認識就好,只要不是關小黑屋那種的開發(fā)就沒必須死磕,現在ai這么發(fā)達,直接把你想要的發(fā)給ai生成或者解析就行。
來看一堆例子
@Component
@Slf4j
public class CronExample {
/**
* 基本Cron表達式示例
*/
// 每5秒執(zhí)行一次
@Scheduled(cron = "*/5 * * * * *")
public void everyFiveSeconds() {
log.info("每5秒執(zhí)行一次");
}
// 每分鐘的第30秒執(zhí)行
@Scheduled(cron = "30 * * * * *")
public void everyMinuteAt30() {
log.info("每分鐘的第30秒執(zhí)行");
}
// 每小時的第15分鐘執(zhí)行
@Scheduled(cron = "0 15 * * * *")
public void everyHourAt15() {
log.info("每小時的第15分鐘執(zhí)行");
}
// 每天10:30執(zhí)行
@Scheduled(cron = "0 30 10 * * *")
public void dailyAt10_30() {
log.info("每天10:30執(zhí)行");
}
/**
* 復雜的Cron表達式示例
*/
// 工作日的9點到17點,每半小時執(zhí)行
@Scheduled(cron = "0 */30 9-17 * * MON-FRI")
public void workHoursTask() {
log.info("工作時間任務執(zhí)行");
}
// 每月1號凌晨2點執(zhí)行
@Scheduled(cron = "0 0 2 1 * ?")
public void monthlyTask() {
log.info("每月任務執(zhí)行");
}
// 每個季度的第一天凌晨3點執(zhí)行
@Scheduled(cron = "0 0 3 1 1,4,7,10 ?")
public void quarterlyTask() {
log.info("季度任務執(zhí)行");
}
/**
* 特殊字符用法
*/
// 'L' 表示最后一天
@Scheduled(cron = "0 0 23 L * ?")
public void lastDayOfMonth() {
log.info("每月最后一天23:00執(zhí)行");
}
// 'W' 表示最近的工作日
@Scheduled(cron = "0 0 12 15W * ?")
public void nearestWeekday() {
log.info("每月15號最近的工作日12:00執(zhí)行");
}
// '#' 表示第幾個星期幾
@Scheduled(cron = "0 0 10 ? * 2#2") // 每月第二個星期一10:00
public void secondMondayOfMonth() {
log.info("每月第二個星期一執(zhí)行");
}
/**
* 從配置文件讀取Cron表達式
*/
@Scheduled(cron = "${task.cron.expression:0 0/30 * * * *}")
public void configurableCronTask() {
log.info("可配置的Cron任務執(zhí)行");
}
/**
* 使用Zone指定時區(qū)
*/
@Scheduled(cron = "0 0 9 * * *", zone = "Asia/Shanghai")
public void timezoneSpecificTask() {
log.info("上海時區(qū)每天9:00執(zhí)行");
}
/**
* 動態(tài)Cron表達式(使用SpEL)
*/
@Scheduled(cron = "#{@cronExpressionProvider.getCron()}")
public void dynamicCronTask() {
log.info("動態(tài)Cron表達式任務執(zhí)行");
}
}三、參數組合使用
3.1 參數優(yōu)先級說明
cron參數優(yōu)先級最高,如果設置了 cron,其他時間參數會被忽略fixedRate和fixedDelay不能同時使用initialDelay只能與fixedRate或fixedDelay一起使用
@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 執(zhí)行
}
/**
* Cron表達式與其他參數組合(cron優(yōu)先級高,其他參數會被忽略)
*/
@Scheduled(cron = "0 */5 * * * *", fixedRate = 10000)
public void cronWithOtherParams() {
log.info("設置了cron和fixedRate,只會按照cron執(zhí)行");
// fixedRate 參數會被忽略
}
}四、動態(tài)配置參數
4.1 使用配置文件
# 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分鐘@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("數據同步任務執(zhí)行");
}
/**
* 使用默認值的配置
*/
@Scheduled(fixedRateString = "${scheduled.tasks.cache-refresh.fixed-rate:600000}")
public void cacheRefreshTask() {
log.info("緩存刷新任務執(zhí)行");
}
}4.2 使用SpEL表達式
@Component
@Slf4j
public class SpELExample {
private final Environment environment;
public SpELExample(Environment environment) {
this.environment = environment;
}
/**
* 使用SpEL讀取環(huán)境變量
*/
@Scheduled(fixedRateString = "#{${task.interval.base} * ${task.interval.multiplier:1}}")
public void spelCalculationTask() {
log.info("SpEL計算間隔的任務執(zhí)行");
}
/**
* 根據條件動態(tài)設置間隔
*/
@Scheduled(fixedDelayString = "#{environment.getProperty('task.mode') == 'fast' ? 1000 : 5000}")
public void conditionalIntervalTask() {
log.info("條件間隔任務執(zhí)行");
}
/**
* 調用Bean方法獲取配置
*/
@Scheduled(cron = "#{@scheduleConfig.getDataExportCron()}")
public void dataExportTask() {
log.info("數據導出任務執(zhí)行");
}
/**
* 復雜的SpEL表達式
*/
@Scheduled(
initialDelayString = "#{T(java.lang.Math).random() * 10000}",
fixedRateString = "#{5000 + T(java.lang.Math).random() * 5000}"
)
public void randomIntervalTask() {
log.info("隨機間隔任務執(zhí)行");
}
}
@Component
class ScheduleConfig {
@Value("${app.environment}")
private String environment;
public String getDataExportCron() {
if ("prod".equals(environment)) {
return "0 0 2 * * *"; // 生產環(huán)境:每天凌晨2點
} else {
return "0 */30 * * * *"; // 測試環(huán)境:每30分鐘
}
}
}五、其他知識與注意事項
5.1 多個@Scheduled注解
Spring 4.0 開始支持同一個方法上使用多個 @Scheduled 注解:
@Component
@Slf4j
public class MultipleSchedulesExample {
/**
* 同一個方法多個調度規(guī)則
* 方法會在多個時間點被調用
*/
@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("多調度任務執(zhí)行,當前時間: {}", 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 下面
@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 事務傳播級別
* 每次執(zhí)行都是獨立的事務
*/
@Scheduled(fixedRate = 60000) // 每分鐘
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStatistics() {
// 更新統(tǒng)計信息,即使失敗也不影響其他事務
log.info("更新統(tǒng)計信息");
}
}5.3 常見陷阱
@Component
@Slf4j
public class CommonPitfalls {
/**
* 陷阱1:長時間運行的任務阻塞其他任務
* 解決方案:使用異步執(zhí)行或調整線程池
*/
@Scheduled(fixedRate = 1000)
public void longRunningTask() {
try {
Thread.sleep(5000); // 任務執(zhí)行5秒
log.info("長時間任務完成");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* 陷阱2:任務拋出異常導致后續(xù)任務不再執(zhí)行
* 解決方案:捕獲異常并處理
*/
@Scheduled(fixedDelay = 2000)
public void exceptionProneTask() {
try {
// 可能拋出異常的代碼
if (Math.random() > 0.8) {
throw new RuntimeException("隨機異常");
}
log.info("任務正常執(zhí)行");
} catch (Exception e) {
log.error("任務執(zhí)行異常,但已處理", e);
// 可以選擇重試、記錄日志、發(fā)送告警等
}
}
/**
* 陷阱3:不正確的Cron表達式
*/
@Scheduled(cron = "0 0 * * * *") // 正確:每小時執(zhí)行
public void correctCron() {
log.info("正確的Cron任務");
}
// @Scheduled(cron = "0 0 * * *") // 錯誤:缺少秒字段
// public void incorrectCron() {
// log.info("這個不會執(zhí)行");
// }
/**
* 陷阱4:Bean未實例化就嘗試調度
* 確保@Component或@Service注解被掃描到
*/
/**
* 陷阱5:忘記啟用調度
* 必須在配置類上添加 @EnableScheduling
*/
}到此這篇關于Spring Task定時任務開發(fā)實戰(zhàn)教程的文章就介紹到這了,更多相關Spring Task定時任務內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Django rest framework使用類視圖實現首頁API
這篇文章主要介紹了Django rest framework使用類視圖實現首頁API,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-08-08
Java下SpringBoot創(chuàng)建定時任務詳解
這篇文章主要介紹了Java下SpringBoot創(chuàng)建定時任務詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07

