Spring Boot 定時(shí)任務(wù)
1. 前言
定時(shí)任務(wù)絕對(duì)是實(shí)際項(xiàng)目中的剛需。
- 我們想監(jiān)控一個(gè)重點(diǎn)服務(wù)的運(yùn)行狀態(tài),可以每隔 1 分鐘調(diào)用下該服務(wù)的心跳接口,調(diào)用失敗時(shí)即發(fā)出告警信息;
- 我們想每天凌晨的時(shí)候,將所有商品的庫存置滿,以免早上忘記添加庫存影響銷售;
- 我們想在每個(gè)周六的某個(gè)時(shí)段進(jìn)行打折促銷。
在以上的案例中,或者是指定時(shí)間間隔,或者是指定時(shí)間節(jié)點(diǎn),按設(shè)定的任務(wù)進(jìn)行某種操作,這就是定時(shí)任務(wù)了。
在 Spring Boot 中實(shí)現(xiàn)定時(shí)任務(wù)簡單而靈活,本節(jié)我們來體驗(yàn)下。
2. Spring Task 定時(shí)任務(wù)
Spring Task 是 Spring Boot 內(nèi)置的定時(shí)任務(wù)模塊,可以滿足大部分的定時(shí)任務(wù)場景需求。
通過為方法添加一個(gè)簡單的注解,即可按設(shè)定的規(guī)則定時(shí)執(zhí)行該方法。
下面就演示下 Spring Boot 中使用 Spring Task 的具體方法。
2.1 使用 Spring Initializr 創(chuàng)建項(xiàng)目
Spring Boot 版本選擇 2.2.5 ,Group 為 com.imooc , Artifact 為 spring-boot-task ,生成項(xiàng)目后導(dǎo)入 Eclipse 開發(fā)環(huán)境。
2.2 開啟定時(shí)任務(wù)
在啟動(dòng)類上添加 @EnableScheduling
注解,開啟定時(shí)任務(wù)功能。
實(shí)例:
@SpringBootApplication
@EnableScheduling // 開啟定時(shí)任務(wù)
public class SpringBootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootTaskApplication.class, args);
}
}
2.3 通過注解設(shè)定定時(shí)任務(wù)
新建 MySpringTask 任務(wù)類,添加 @Component
注解注冊(cè) Spring 組件,定時(shí)任務(wù)方法需要在 Spring 組件類才能生效。
注意類中方法添加了 @Scheduled
注解,所以會(huì)按照 @Scheduled
注解參數(shù)指定的規(guī)則定時(shí)執(zhí)行。
實(shí)例:
/**
* 任務(wù)類
*/
@Component
public class MySpringTask {
/**
* 每2秒執(zhí)行1次
*/
@Scheduled(fixedRate = 2000)
public void fixedRateMethod() throws InterruptedException {
System.out.println("fixedRateMethod:" + new Date());
Thread.sleep(1000);
}
}
上面例子執(zhí)行情況如下,可見是每隔 2 秒執(zhí)行 1 次。
fixedRateMethod:Fri May 15 22:04:52 CST 2020
fixedRateMethod:Fri May 15 22:04:54 CST 2020
fixedRateMethod:Fri May 15 22:04:56 CST 2020
實(shí)例:
/**
* 任務(wù)類
*/
@Component
public class MySpringTask {
/**
* 執(zhí)行結(jié)束2秒后執(zhí)行下次任務(wù)
*/
@Scheduled(fixedDelay = 2000)
public void fixedDelayMethod() throws InterruptedException {
System.out.println("fixedDelayMethod:" + new Date());
Thread.sleep(1000);
}
}
上面的例子執(zhí)行情況如下,每次打印后先等待 1 秒,然后方法執(zhí)行結(jié)束 2 秒后再次執(zhí)行任務(wù),所以是每 3 秒打印 1 行內(nèi)容。
fixedDelayMethod:Fri May 15 22:08:26 CST 2020
fixedDelayMethod:Fri May 15 22:08:29 CST 2020
fixedDelayMethod:Fri May 15 22:08:32 CST 2020
2.4 使用 Cron 表達(dá)式
@Scheduled
也支持使用 Cron 表達(dá)式, Cron 表達(dá)式可以非常靈活地設(shè)置定時(shí)任務(wù)的執(zhí)行時(shí)間。以本節(jié)開頭的兩個(gè)需求為例:
- 我們想監(jiān)控一個(gè)重點(diǎn)服務(wù)的運(yùn)行狀態(tài),可以每隔 1 分鐘調(diào)用下該服務(wù)的心跳接口,調(diào)用失敗時(shí)即發(fā)出告警信息;
- 我們想在每天凌晨的時(shí)候,將所有商品的庫存置滿,以免早上忘記添加庫存影響銷售。
對(duì)應(yīng)的定時(shí)任務(wù)實(shí)現(xiàn)如下:
實(shí)例:
/**
* 任務(wù)類
*/
@Component
public class MySpringTask {
/**
* 在每分鐘的00秒執(zhí)行
*/
@Scheduled(cron = "0 * * * * ?")
public void jump() throws InterruptedException {
System.out.println("心跳檢測:" + new Date());
}
/**
* 在每天的00:00:00執(zhí)行
*/
@Scheduled(cron = "0 0 0 * * ?")
public void stock() throws InterruptedException {
System.out.println("置滿庫存:" + new Date());
}
}
Cron 表達(dá)式并不難理解,從左到右一共 6 個(gè)位置,分別代表秒、時(shí)、分、日、月、星期,以秒為例:
- 如果該位置上是
0
,表示在第 0 秒執(zhí)行; - 如果該位置上是
*
,表示每秒都會(huì)執(zhí)行; - 如果該位置上是
?
,表示該位置的取值不影響定時(shí)任務(wù),由于月份中的日和星期可能會(huì)發(fā)生意義沖突,所以日、 星期中需要有一個(gè)配置為?
。
按照上面的理解,cron = "0 * * * * ?"
表示在每分鐘的 00 秒執(zhí)行、cron = "0 0 0 * * ?"
表示在每天的 00:00:00 執(zhí)行。
Tips:Cron 表達(dá)式的描述能力很強(qiáng),此處只是簡單提及,感興趣的同學(xué)可以自行查閱相關(guān)資料了解更多信息。
3. Quartz 定時(shí)任務(wù)
Spring Task 已經(jīng)可以滿足絕大多數(shù)項(xiàng)目對(duì)定時(shí)任務(wù)的需求,但是在企業(yè)級(jí)應(yīng)用這個(gè)領(lǐng)域,還有更加強(qiáng)大靈活的 Quartz 框架可供選擇。
舉個(gè)例子,當(dāng)我們想根據(jù)數(shù)據(jù)庫中的配置,動(dòng)態(tài)地指定商品打折的時(shí)間區(qū)間時(shí),就可以利用 Quartz 框架來實(shí)現(xiàn)。 OK ,接下來我們就來具體完整實(shí)現(xiàn)下。
3.1 使用 Spring Initializr 創(chuàng)建項(xiàng)目
Spring Boot 版本選擇 2.2.5 ,Group 為 com.imooc , Artifact 為 spring-boot-quartz ,生成項(xiàng)目后導(dǎo)入 Eclipse 開發(fā)環(huán)境。
3.2 引入項(xiàng)目依賴
需要引入 Quartz 框架相關(guān)依賴。
實(shí)例:
<!-- Quartz -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
3.3 開啟定時(shí)任務(wù)
同樣需要,在啟動(dòng)類上添加 @EnableScheduling
注解,開啟定時(shí)任務(wù)功能。
實(shí)例:
@SpringBootApplication
@EnableScheduling // 開啟定時(shí)任務(wù)
public class SpringBootQuartzApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootQuartzApplication.class, args);
}
}
3.4 Quartz 定時(shí)任務(wù)開發(fā)
Quartz 定時(shí)任務(wù)需要通過 Job 、 Trigger 、 JobDetail 來設(shè)置。
- Job:具體任務(wù)操作類
- Trigger:觸發(fā)器,設(shè)定執(zhí)行任務(wù)的時(shí)間
- JobDetail:指定觸發(fā)器執(zhí)行的具體任務(wù)類及方法
我們先開發(fā)一個(gè) Job 組件:
實(shí)例:
/**
* 打折任務(wù)
*/
@Component // 注冊(cè)到容器中
public class DiscountJob {
/**
* 執(zhí)行打折
*/
public void execute() {
System.out.println("更新數(shù)據(jù)庫中商品價(jià)格,統(tǒng)一打5折");
}
}
然后在配置類中設(shè)定 Trigger 及 JobDetail 。
實(shí)例:
/**
* 定時(shí)任務(wù)配置
*/
@Configuration
public class QuartzConfig {
/**
* 配置JobDetail工廠組件,生成的JobDetail指向discountJob的execute()方法
*/
@Bean
MethodInvokingJobDetailFactoryBean jobFactoryBean() {
MethodInvokingJobDetailFactoryBean bean = new MethodInvokingJobDetailFactoryBean();
bean.setTargetBeanName("discountJob");
bean.setTargetMethod("execute");
return bean;
}
/**
* 觸發(fā)器工廠
*/
@Bean
CronTriggerFactoryBean cronTrigger() {
CronTriggerFactoryBean bean = new CronTriggerFactoryBean();
// Corn表達(dá)式設(shè)定執(zhí)行時(shí)間規(guī)則
bean.setCronExpression("0 0 8 ? * 7");
// 執(zhí)行JobDetail
bean.setJobDetail(jobFactoryBean().getObject());
return bean;
}
}
具體分析下上面的代碼:
- 觸發(fā)器設(shè)定的 Corn 表達(dá)式為
0 0 8 ? * 7
,表示每周六的 08:00:00 執(zhí)行 1 次; - 觸發(fā)器指定的 JobDetail 為 jobFactoryBean 工廠的一個(gè)對(duì)象,而 jobFactoryBean 指定的對(duì)象及方法為 discountJob 與 execute () ;
- 所以每周六的 8 點(diǎn),就會(huì)運(yùn)行 discountJob 組件的 execute () 方法 1 次;
- Corn 表達(dá)式和執(zhí)行任務(wù)、方法均以參數(shù)形式存在,這就意味著我們完全可以根據(jù)文件或數(shù)據(jù)庫配置動(dòng)態(tài)地調(diào)整執(zhí)行時(shí)間和執(zhí)行的任務(wù);
- 最后,周六 8 點(diǎn)的時(shí)候,商品都打了 5 折,別忘了促銷結(jié)束的時(shí)候恢復(fù)價(jià)格啊。
4. 小結(jié)
Spring Boot 可以利用一個(gè)簡單的注解,快速實(shí)現(xiàn)定時(shí)任務(wù)的功能。
說實(shí)話我第一次使用 @Scheduled
注解時(shí),完全被這種開箱即用型的簡潔震撼了,我的感受是:似乎不能更加簡潔了。
如果感覺 Spring Task 提供的定時(shí)任務(wù)機(jī)制還不足以滿足需求,Spring Boot 還可以方便地集成 Quartz 框架來幫忙。
開箱即用滿足不了,還可以即插即用,確實(shí)夠人性化的。