再見 Spring Task,這款定時任務(wù)老而彌堅!
二哥編程知識星球 (戳鏈接加入)正式上線了,來和 220 多名 小伙伴一起打怪升級吧!這是一個 Java 學(xué)習(xí)指南 + 編程實戰(zhàn)的私密圈子,你可以向二哥提問、幫你制定學(xué)習(xí)計劃、跟著二哥一起做實戰(zhàn)項目,沖沖沖。
Java程序員進階之路網(wǎng)址:https://tobebetterjavaer.com
定時任務(wù)的應(yīng)用場景其實蠻常見的,比如說:
數(shù)據(jù)備份 訂單未支付則自動取消 定時爬取數(shù)據(jù) 定時推送信息 定時發(fā)布文章 等等(想不出來了,只能等等來湊,??,反正只要等的都需要定時,怎么樣,這波圓場可以吧)
編程喵??實戰(zhàn)項目里需要做一個定時發(fā)布文章的功能,一開始我想用 Spring Task,于是研究了一番,發(fā)現(xiàn) Spring Task 用起來確實簡單,但對于復(fù)雜業(yè)務(wù)卻也無能為力。
于是我就把注意力放到了 Quartz 上面,這是一款老而彌堅的開源任務(wù)調(diào)度框架。

記得我在 14 年開發(fā)大宗期貨交易平臺的時候就用到了它,每天凌晨定時需要統(tǒng)計一波交易數(shù)據(jù),生成日報報表,當(dāng)時配合 Cron 表達式用的。
可惜后來平臺穩(wěn)定了,新的政策出來了,直接把大宗期貨交易滅了。于是我發(fā)財?shù)臋C會也隨著破滅了。想想都覺得可惜,哈哈哈。

時光荏苒,Quartz 發(fā)展到現(xiàn)在,已經(jīng)可以和 Spring Boot 項目無縫銜接了,今天我們就來實戰(zhàn)一把。
Timer
JDK 1.3 就開始支持的一種定時任務(wù)的實現(xiàn)方式。內(nèi)部通過 TaskQueue 的類來存放定時任務(wù),用起來比較簡單,但缺陷比較多,比如說一個 Timer 就會起一個線程,任務(wù)多了性能就非常差,再比如說如果執(zhí)行任務(wù)期間某個 TimerTask 耗時比較久,就會影響其他任務(wù)的調(diào)度。
@Slf4j
public?class?TimerDemo?{
????public?static?void?main(String[]?args)?{
????????TimerTask?task?=?new?TimerTask()?{
????????????@Override
????????????public?void?run()?{
????????????????log.debug("當(dāng)前時間{}線程名稱{}",?DateTime.now(),
????????????????????????Thread.currentThread().getName());
????????????}
????????};
????????log.debug("當(dāng)前時間{}線程名稱{}",?DateTime.now(),
????????????????Thread.currentThread().getName());
????????Timer?timer?=?new?Timer("TimerDemo");
????????timer.schedule(task,1000L);
????}
}
代碼跑起來后的日志如下所示:
13:11:45.268?[main]?DEBUG?top.springtask.TimerDemo?-?當(dāng)前時間2022-04-27?13:11:45線程名稱main
13:11:46.280?[TimerDemo]?DEBUG?top.springtask.TimerDemo?-?當(dāng)前時間2022-04-27?13:11:46線程名稱TimerDemo
ScheduledThreadPoolExecutor
JDK 1.5 開始提供的的定時任務(wù),它繼承了 ThreadPoolExecutor,實現(xiàn)了 ScheduledExecutorService 接口,所以支持并發(fā)場景下的任務(wù)執(zhí)行。同時,優(yōu)化了 Timer 的缺陷。不過,由于使用了隊列來實現(xiàn)定時器,就有出入隊列、調(diào)整堆等操作,所以定時不是非常非常準確(吹毛求疵)。
@Slf4j
public?class?ScheduledThreadPoolExecutorDemo?{
????public?static?void?main(String[]?args)?throws?InterruptedException?{
????????TimerTask?task?=?new?TimerTask()?{
????????????@Override
????????????public?void?run()?{
????????????????log.debug("當(dāng)前時間{}線程名稱{}",?DateTime.now(),
????????????????????????Thread.currentThread().getName());
????????????}
????????};
????????log.debug("當(dāng)前時間{}線程名稱{}",?DateTime.now(),
????????????????Thread.currentThread().getName());
????????ScheduledExecutorService?executorService?=?Executors.newScheduledThreadPool(3);
????????executorService.scheduleAtFixedRate(task,?1000L,1000L,?TimeUnit.MILLISECONDS);
????????Thread.sleep(1000+1000*4);
????????executorService.shutdown();
????}
}
輸出結(jié)果如下所示:
14:43:41.740?[main]?DEBUG?top.springtask.ScheduledThreadPoolExecutorDemo?-?當(dāng)前時間2022-04-27?14:43:41線程名稱main
14:43:42.752?[pool-1-thread-1]?DEBUG?top.springtask.ScheduledThreadPoolExecutorDemo?-?當(dāng)前時間2022-04-27?14:43:42線程名稱pool-1-thread-1
14:43:43.748?[pool-1-thread-1]?DEBUG?top.springtask.ScheduledThreadPoolExecutorDemo?-?當(dāng)前時間2022-04-27?14:43:43線程名稱pool-1-thread-1
14:43:44.749?[pool-1-thread-2]?DEBUG?top.springtask.ScheduledThreadPoolExecutorDemo?-?當(dāng)前時間2022-04-27?14:43:44線程名稱pool-1-thread-2
14:43:45.749?[pool-1-thread-2]?DEBUG?top.springtask.ScheduledThreadPoolExecutorDemo?-?當(dāng)前時間2022-04-27?14:43:45線程名稱pool-1-thread-2
14:43:46.749?[pool-1-thread-2]?DEBUG?top.springtask.ScheduledThreadPoolExecutorDemo?-?當(dāng)前時間2022-04-27?14:43:46線程名稱pool-1-thread-2
Spring Task
Spring Task 是 Spring 提供的輕量級定時任務(wù)工具,也就意味著不需要再添加第三方依賴了,相比其他第三方類庫更加方便易用。
好像關(guān)于 Spring Task,沒有其他廢話可說了,我們來直接上手。
第一步,新建配置類 SpringTaskConfig,并添加 @EnableScheduling注解開啟 Spring Task。
@Configuration
@EnableScheduling
public?class?SpringTaskConfig?{
}
當(dāng)然了,也可以不新建這個配置類,直接在主類上添加 @EnableScheduling 注解。
@SpringBootApplication
@EnableScheduling
public?class?CodingmoreSpringtaskApplication?{
?public?static?void?main(String[]?args)?{
??SpringApplication.run(CodingmoreSpringtaskApplication.class,?args);
?}
}
第二步,新建定時任務(wù)類 CronTask,使用 @Scheduled 注解注冊 Cron 表達式執(zhí)行定時任務(wù)。
@Slf4j
@Component
public?class?CronTask?{
????@Scheduled(cron?=?"0/1?*?*???*??")
????public?void?cron()?{
????????log.info("定時執(zhí)行,時間{}",?DateUtil.now());
????}
}
啟動服務(wù)器端,發(fā)現(xiàn)每隔一秒鐘會打印一次日志,證明 Spring Task 的 cron 表達式形式已經(jīng)起效了。

默認情況下,@Scheduled 創(chuàng)建的線程池大小為 1,如果想增加線程池大小的話,可以讓 SpringTaskConfig 類實現(xiàn) SchedulingConfigurer 接口,通過 setPoolSize 增加線程池大小。
@Configuration
@EnableScheduling
public?class?SpringTaskConfig?implements?SchedulingConfigurer?{
????@Override
????public?void?configureTasks(ScheduledTaskRegistrar?taskRegistrar)?{
????????ThreadPoolTaskScheduler?threadPoolTaskScheduler?=?new?ThreadPoolTaskScheduler();
????????threadPoolTaskScheduler.setPoolSize(10);
????????threadPoolTaskScheduler.setThreadNamePrefix("my-scheduled-task-pool-");
????????threadPoolTaskScheduler.initialize();
????????taskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
????}
}
服務(wù)熱部署完成后,會在控制臺看到這樣的信息:

可以確認自定義線程池大小已經(jīng)生效了,有的任務(wù)用的是線程led-task-pool-3,有的是線程led-task-pool-7,跑時間長了,可以發(fā)現(xiàn) led-task-pool-1 到 led-task-pool-10 的都有。

Spring Task 除了支持 Cron 表達式,還有 fixedRate(固定速率執(zhí)行)、fixedDelay(固定延遲執(zhí)行)、initialDelay(初始延遲)三種用法。
/**
?*?fixedRate:固定速率執(zhí)行。每5秒執(zhí)行一次。
?*/
@Scheduled(fixedRate?=?5000)
public?void?reportCurrentTimeWithFixedRate()?{
????log.info("Current?Thread?:?{}",?Thread.currentThread().getName());
????log.info("Fixed?Rate?Task?:?The?time?is?now?{}",?DateUtil.now());
}
/**
?*?fixedDelay:固定延遲執(zhí)行。距離上一次調(diào)用成功后2秒才執(zhí)。
?*/
@Scheduled(fixedDelay?=?2000)
public?void?reportCurrentTimeWithFixedDelay()?{
????try?{
????????TimeUnit.SECONDS.sleep(3);
????????log.info("Fixed?Delay?Task?:?The?time?is?now?{}",DateUtil.now());
????}?catch?(InterruptedException?e)?{
????????e.printStackTrace();
????}
}
/**
?*?initialDelay:初始延遲。任務(wù)的第一次執(zhí)行將延遲5秒,然后將以5秒的固定間隔執(zhí)行。
?*/
@Scheduled(initialDelay?=?5000,?fixedRate?=?5000)
public?void?reportCurrentTimeWithInitialDelay()?{
????log.info("Fixed?Rate?Task?with?Initial?Delay?:?The?time?is?now?{}",?DateUtil.now());
}
不過,fixedRate 有個坑,假如某個方法的定時器設(shè)定的固定速率是每5秒執(zhí)行一次,這個方法現(xiàn)在要執(zhí)行下面四個任務(wù),四個任務(wù)的耗時是:6s、6s、 2s、 3s,任務(wù)會如何執(zhí)行呢(單線程環(huán)境下)?
2022-04-27?15:25:52.400??INFO?4343?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:25:52
2022-04-27?15:25:58.401??INFO?4343?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:25:58
2022-04-27?15:26:00.407??INFO?4343?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:26:00
2022-04-27?15:26:04.318??INFO?4343?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:26:04
第一個任務(wù)開始的相對時間是第 0 秒,但由于執(zhí)行了 6 秒,所以原來應(yīng)該是第 5 秒執(zhí)行的任務(wù),延遲到第 6 秒才開始執(zhí)行,第三個任務(wù)延遲了 12 秒,原本應(yīng)該是第 10 秒執(zhí)行,第三個任務(wù)沒有延遲,正常 15 秒后執(zhí)行。
假如我們使用 @EnableAsync 注解開啟多線程環(huán)境的話,結(jié)果會怎么樣呢?
2022-04-27?15:33:01.385??INFO?4421?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:33:01
2022-04-27?15:33:07.390??INFO?4421?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:33:07
2022-04-27?15:33:09.391??INFO?4421?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:33:09
2022-04-27?15:33:13.295??INFO?4421?---?[led-task-pool-1]?c.codingmore.component.PublishPostTask???:?Fixed?Rate?Task?:?The?time?is?now?2022-04-27?15:33:13
關(guān)于 Cron 表達式
這里順帶普及一下 Cron 表達式,在定時任務(wù)中會經(jīng)常會遇到。Cron 這個詞來源于希臘語 chronos,原意也就是時間。
Cron 表達式是一個含有時間意義的字符串,以 5 個空格隔開,分成 6 個時間元素。舉幾個例子就一目了然了。
| 示例 | 說明 |
|---|---|
0 15 10 ? * * | 每天上午10:15執(zhí)行任務(wù) |
0 0 10,14,16 * * ? | 每天10 點、14 點、16 點執(zhí)行任務(wù) |
0 0 12 ? * 3 | 每個星期三中午 12 點執(zhí)行任務(wù) |
0 15 10 15 * ? | 每月 15 日上午 10 點 15 執(zhí)行任務(wù) |
Cron 的語法格式可以總結(jié)為:
Seconds Minutes Hours DayofMonth Month DayofWeek
每個時間元素的取值范圍,以及可出現(xiàn)的特殊字符如下所示。
| 時間元素 | 取值范圍 | 可出現(xiàn)的特殊字符 |
|---|---|---|
| 秒 | [0,59] | *,-/ |
| 分鐘 | [0,59] | *,-/ |
| 小時 | [0,59] | *,-/ |
| 日期 | [0,31] | *,-/?LW |
| 月份 | [1,12] | *,-/ |
| 星期 | [1,7] | *,-/?L# |
特殊字符的含義和示例如下所示。
| 特殊字符 | 含義 | 示例 |
|---|---|---|
* | 所有可能的值 | 很好理解,月域中為每個月,星期域中每個星期幾 |
, | 枚舉的值 | 很好理解,小時域中 10,14,16,就表示這幾個小時可選 |
- | 范圍 | 很好理解,分鐘域中 10-19,就表示 10-19 分鐘每隔一分鐘執(zhí)行一次 |
/ | 指定數(shù)值的增量 | 很好理解,分鐘域中 0/15,就表示每隔 15 分鐘執(zhí)行一次 |
? | 不指定值 | 很好理解,日期域指定了星期域就不能指定值,反之亦然,因為日期域和星期域?qū)儆跊_突關(guān)系 |
L | 單詞 Last 的首字母 | 很好理解,日期域和星期域支持,表示月的最后一天或者星期的最后一天 |
W | 除周末以外的工作日 | 很好理解,僅日期域支持 |
# | 每個月的第幾個星期幾 | 很好理解,僅星期域支持,4#2表示某月的第二個星期四 |
關(guān)于 Quartz
Quartz 是一款功能強大的開源的任務(wù)調(diào)度框架,在 GitHub 上已經(jīng)累計有 5k+ 的 star 了。小到單機應(yīng)用,大到分布式,都可以整合 Quartz。

在使用 Quartz 之前,讓我們先來搞清楚 4 個核心概念:
Job:任務(wù),要執(zhí)行的具體內(nèi)容。 JobDetail:任務(wù)詳情,Job 是它要執(zhí)行的內(nèi)容,同時包含了這個任務(wù)調(diào)度的策略和方案。 Trigger:觸發(fā)器,可以通過 Cron 表達式來指定任務(wù)執(zhí)行的時間。 Scheduler:調(diào)度器,可以注冊多個 JobDetail 和 Trigger,用來調(diào)度、暫停和刪除任務(wù)。
整合 Quartz
Quartz 存儲任務(wù)的方式有兩種,一種是使用內(nèi)存,另外一種是使用數(shù)據(jù)庫。內(nèi)存在程序重啟后就丟失了,所以我們這次使用數(shù)據(jù)庫的方式來進行任務(wù)的持久化。
第一步,在 pom.xml 文件中添加 Quartz 的 starter。
????org.springframework.boot
????spring-boot-starter-quartz
????2.6.7
第二步,在 application.yml 添加 Quartz 相關(guān)配置,配置說明直接看注釋。
spring:
??quartz:
????job-store-type:?jdbc?#?默認為內(nèi)存?memory?的方式,這里我們使用數(shù)據(jù)庫的形式
????wait-for-jobs-to-complete-on-shutdown:?true?#?關(guān)閉時等待任務(wù)完成
????overwrite-existing-jobs:?true?#?可以覆蓋已有的任務(wù)
????jdbc:
??????initialize-schema:?never?#?是否自動使用?SQL?初始化?Quartz?表結(jié)構(gòu)
????properties:?#?quartz原生配置
??????org:
????????quartz:
??????????scheduler:
????????????instanceName:?scheduler?#?調(diào)度器實例名稱
????????????instanceId:?AUTO?#?調(diào)度器實例ID自動生成
??????????#?JobStore?相關(guān)配置
??????????jobStore:
????????????class:?org.quartz.impl.jdbcjobstore.JobStoreTX?#?JobStore?實現(xiàn)類
????????????driverDelegateClass:?org.quartz.impl.jdbcjobstore.StdJDBCDelegate?#?使用完全兼容JDBC的驅(qū)動
????????????tablePrefix:?QRTZ_?#?Quartz?表前綴
????????????useProperties:?false?#?是否將JobDataMap中的屬性轉(zhuǎn)為字符串存儲
??????????#?線程池相關(guān)配置
??????????threadPool:
????????????threadCount:?25?#?線程池大小。默認為?10?。
????????????threadPriority:?5?#?線程優(yōu)先級
????????????class:?org.quartz.simpl.SimpleThreadPool?#?指定線程池實現(xiàn)類,對調(diào)度器提供固定大小的線程池
Quartz 默認使用的是內(nèi)存的方式來存儲任務(wù),為了持久化,我們這里改為 JDBC 的形式,并且指定 spring.quartz.jdbc.initialize-schema=never,這樣我們可以手動創(chuàng)建數(shù)據(jù)表。因為該值的另外兩個選項ALWAYS和EMBEDDED都不太符合我們的要求:
ALWAYS:每次都初始化 EMBEDDED:只初始化嵌入式數(shù)據(jù)庫,比如說 H2、HSQL
那手動創(chuàng)建數(shù)據(jù)表的 SQL 語句去哪里找呢?
GitHub 地址:https://github.com/quartz-scheduler/quartz/tree/master/quartz-core/src/main/resources/org/quartz/impl/jdbcjobstore
為了方便小伙伴們下載,我把它放在了本教程的源碼里面了:

如果使用 Intellij IDEA 旗艦版的話,首次打開 SQL 文件的時候會提示你指定數(shù)據(jù)源。在上圖中,我配置了本地的 MySQL 數(shù)據(jù)庫,導(dǎo)入成功后可以在數(shù)據(jù)庫中查看到以下數(shù)據(jù)表:

Quartz數(shù)據(jù)庫核心表如下:
| Table Name | Description |
|---|---|
| QRTZ_CALENDARS | 存儲Quartz的Calendar信息 |
| QRTZ_CRON_TRIGGERS | 存儲CronTrigger,包括Cron表達式和時區(qū)信息 |
| QRTZ_FIRED_TRIGGERS | 存儲與已觸發(fā)的Trigger相關(guān)的狀態(tài)信息,以及相聯(lián)Job的執(zhí)行信息 |
| QRTZ_PAUSED_TRIGGER_GRPS | 存儲已暫停的Trigger組的信息 |
| QRTZ_SCHEDULER_STATE | 存儲少量的有關(guān)Scheduler的狀態(tài)信息,和別的Scheduler實例 |
| QRTZ_LOCKS | 存儲程序的悲觀鎖的信息 |
| QRTZ_JOB_DETAILS | 存儲每一個已配置的Job的詳細信息 |
| QRTZ_JOB_LISTENERS | 存儲有關(guān)已配置的JobListener的信息 |
| QRTZ_SIMPLE_TRIGGERS | 存儲簡單的Trigger,包括重復(fù)次數(shù)、間隔、以及已觸的次數(shù) |
| QRTZ_BLOG_TRIGGERS | Trigger作為Blob類型存儲 |
| QRTZ_TRIGGER_LISTENERS | 存儲已配置的TriggerListener的信息 |
| QRTZ_TRIGGERS | 存儲已配置的Trigger的信息 |
剩下的就是對 Quartz 的 scheduler、jobStore 和 threadPool 配置。
第三步,創(chuàng)建任務(wù)調(diào)度的接口 IScheduleService,定義三個方法,分別是通過 Cron 表達式來調(diào)度任務(wù)、指定時間來調(diào)度任務(wù),以及取消任務(wù)。
public?interface?IScheduleService?{
????/**
?????*?通過?Cron?表達式來調(diào)度任務(wù)
?????*/
????String?scheduleJob(Class?extends?Job>?jobBeanClass,?String?cron,?String?data);
????/**
?????*?指定時間來調(diào)度任務(wù)
?????*/
????String?scheduleFixTimeJob(Class?extends?Job>?jobBeanClass,?Date?startTime,?String?data);
????/**
?????*?取消定時任務(wù)
?????*/
????Boolean?cancelScheduleJob(String?jobName);
}
第四步,創(chuàng)建任務(wù)調(diào)度業(yè)務(wù)實現(xiàn)類 ScheduleServiceImpl,通過Scheduler、CronTrigger、JobDetail的API來實現(xiàn)對應(yīng)的方法。
@Slf4j
@Service
public?class?ScheduleServiceImpl?implements?IScheduleService?{
????private?String?defaultGroup?=?"default_group";
????@Autowired
????private?Scheduler?scheduler;
????@Override
????public?String?scheduleJob(Class?extends?Job>?jobBeanClass,?String?cron,?String?data)?{
????????String?jobName?=?UUID.fastUUID().toString();
????????JobDetail?jobDetail?=?JobBuilder.newJob(jobBeanClass)
????????????????.withIdentity(jobName,?defaultGroup)
????????????????.usingJobData("data",?data)
????????????????.build();
????????//創(chuàng)建觸發(fā)器,指定任務(wù)執(zhí)行時間
????????CronTrigger?cronTrigger?=?TriggerBuilder.newTrigger()
????????????????.withIdentity(jobName,?defaultGroup)
????????????????.withSchedule(CronScheduleBuilder.cronSchedule(cron))
????????????????.build();
????????//?調(diào)度器進行任務(wù)調(diào)度
????????try?{
????????????scheduler.scheduleJob(jobDetail,?cronTrigger);
????????}?catch?(SchedulerException?e)?{
????????????log.error("任務(wù)調(diào)度執(zhí)行失敗{}",?e.getMessage());
????????}
????????return?jobName;
????}
????@Override
????public?String?scheduleFixTimeJob(Class?extends?Job>?jobBeanClass,?Date?startTime,?String?data)?{
????????//日期轉(zhuǎn)CRON表達式
????????String?startCron?=?String.format("%d?%d?%d?%d?%d???%d",
????????????????DateUtil.second(startTime),
????????????????DateUtil.minute(startTime),
????????????????DateUtil.hour(startTime,?true),
????????????????DateUtil.dayOfMonth(startTime),
????????????????DateUtil.month(startTime)?+?1,
????????????????DateUtil.year(startTime));
????????return?scheduleJob(jobBeanClass,?startCron,?data);
????}
????@Override
????public?Boolean?cancelScheduleJob(String?jobName)?{
????????boolean?success?=?false;
????????try?{
????????????//?暫停觸發(fā)器
????????????scheduler.pauseTrigger(new?TriggerKey(jobName,?defaultGroup));
????????????//?移除觸發(fā)器中的任務(wù)
????????????scheduler.unscheduleJob(new?TriggerKey(jobName,?defaultGroup));
????????????//?刪除任務(wù)
????????????scheduler.deleteJob(new?JobKey(jobName,?defaultGroup));
????????????success?=?true;
????????}?catch?(SchedulerException?e)?{
????????????log.error("任務(wù)取消失敗{}",?e.getMessage());
????????}
????????return?success;
????}
}
第五步,定義好要執(zhí)行的任務(wù),繼承 QuartzJobBean 類,實現(xiàn) executeInternal 方法,這里只定義一個定時發(fā)布文章的任務(wù)。
@Slf4j
@Component
public?class?PublishPostJob?extends?QuartzJobBean?{
????@Autowired
????private?IScheduleService?scheduleService;
????@Autowired
????private?IPostsService?postsService;
????@Override
????protected?void?executeInternal(JobExecutionContext?jobExecutionContext)?throws?JobExecutionException?{
????????Trigger?trigger?=?jobExecutionContext.getTrigger();
????????JobDetail?jobDetail?=?jobExecutionContext.getJobDetail();
????????JobDataMap?jobDataMap?=?jobDetail.getJobDataMap();
????????Long?data?=?jobDataMap.getLong("data");
????????log.debug("定時發(fā)布文章操作:{}",data);
????????//?獲取文章的?ID后獲取文章,更新文章為發(fā)布的狀態(tài),還有發(fā)布的時間
????????boolean?success?=?postsService.updatePostByScheduler(data);
????????//完成后刪除觸發(fā)器和任務(wù)
????????if?(success)?{
????????????log.debug("定時任務(wù)執(zhí)行成功,開始清除定時任務(wù)");
????????????scheduleService.cancelScheduleJob(trigger.getKey().getName());
????????}
????}
}
第六步,發(fā)布文章的接口里 PostsServiceImpl 添加定時發(fā)布的任務(wù)調(diào)度方法。
@Service
public?class?PostsServiceImpl?extends?ServiceImpl<PostsMapper,?Posts>?implements?IPostsService?{
????private?void?handleScheduledAfter(Posts?posts)?{
????????//?文章已經(jīng)保存為草稿了,并且拿到了文章?ID
????????//?調(diào)用定時任務(wù)
????????String?jobName?=?scheduleService.scheduleFixTimeJob(PublishPostJob.class,?posts.getPostDate(),?posts.getPostsId().toString());
????????LOGGER.debug("定時任務(wù){(diào)}開始執(zhí)行",?jobName);
????}
}
好,我們現(xiàn)在啟動服務(wù),通過Swagger 來測試一下,注意設(shè)置文章的定時發(fā)布時間。

查看 Quartz 的數(shù)據(jù)表 qrtz_cron_triggers,發(fā)現(xiàn)任務(wù)已經(jīng)添加進來了。

qrtz_job_details 表里也可以查看具體的任務(wù)詳情。

文章定時發(fā)布的時間到了之后,在日志里也可以看到 Quartz 的執(zhí)行日志。

再次查看 Quartz 數(shù)據(jù)表 qrtz_cron_triggers 和 qrtz_job_details 的時候,也會發(fā)現(xiàn)定時任務(wù)已經(jīng)清除了。
整體上來說,Spring Boot 整合 Quartz還是非常絲滑的,配置少,步驟清晰,比 Spring Task 更強大,既能針對內(nèi)存也能持久化,所以大家在遇到定時任務(wù)的時候完全可以嘗試一把。
完整的功能在編程喵實戰(zhàn)項目中已經(jīng)實現(xiàn)了,可以把編程喵導(dǎo)入到本地嘗試一下。
業(yè)務(wù)梳理
簡單來梳理一下編程喵定時發(fā)布文章的業(yè)務(wù)。
1)用戶在發(fā)布文章的時候可以選擇定時發(fā)布,如果選擇定時發(fā)布,那么就要設(shè)置定時發(fā)布的時間,暫時規(guī)定至少十分鐘以后可以定時。
2)當(dāng)管理端用戶選擇了定時發(fā)布,那么在保存文章的時候,文章狀態(tài)要先設(shè)置為草稿狀態(tài),對前端用戶是不可見的狀態(tài)。
3)保存文章的時候通知 Quartz,我有一個任務(wù),你需要在某個規(guī)定的時間去執(zhí)行。
scheduleService.scheduleFixTimeJob(PublishPostJob.class,?posts.getPostDate(),?posts.getPostsId().toString());
4)Quartz 收到這個通知后,就會在數(shù)據(jù)庫中寫入任務(wù),具體的任務(wù)是到指定時間把文章從草稿的狀態(tài)轉(zhuǎn)為發(fā)布狀態(tài),這時候,前端用戶就可以看得見文章了。
//?獲取文章的?ID后獲取文章,更新文章為發(fā)布的狀態(tài),還有發(fā)布的時間
boolean?success?=?postsService.updatePostByScheduler(data);
同時,將任務(wù)清除。
//?暫停觸發(fā)器
scheduler.pauseTrigger(new?TriggerKey(jobName,?defaultGroup));
//?移除觸發(fā)器中的任務(wù)
scheduler.unscheduleJob(new?TriggerKey(jobName,?defaultGroup));
//?刪除任務(wù)
scheduler.deleteJob(new?JobKey(jobName,?defaultGroup));
整個過程就完成了。Quartz 是如何實現(xiàn)定時發(fā)布文章的呢?其實也是通過 Cron 表達式。
CronTrigger?cronTrigger?=?TriggerBuilder.newTrigger()
????????????????.withIdentity(jobName,?defaultGroup)
????????????????.withSchedule(CronScheduleBuilder.cronSchedule(cron))
????????????????.build();
也就是當(dāng)我們傳入一個指定時間后,通過計算,計算出 Cron 表達式。
String?startCron?=?String.format("%d?%d?%d?%d?%d???%d",
????????????????DateUtil.second(startTime),
????????????????DateUtil.minute(startTime),
????????????????DateUtil.hour(startTime,?true),
????????????????DateUtil.dayOfMonth(startTime),
????????????????DateUtil.month(startTime)?+?1,
????????????????DateUtil.year(startTime));
在 Quartz 中,有兩類線程:Scheduler調(diào)度線程和任務(wù)執(zhí)行線程。
任務(wù)執(zhí)行線程:Quartz不會在主線程(QuartzSchedulerThread)中處理用戶的Job。Quartz把線程管理的職責(zé)委托給ThreadPool,一般的設(shè)置使用SimpleThreadPool。SimpleThreadPool創(chuàng)建了一定數(shù)量的WorkerThread實例來使得Job能夠在線程中進行處理。WorkerThread是定義在SimpleThreadPool類中的內(nèi)部類,它實質(zhì)上就是一個線程。 QuartzSchedulerThread調(diào)度主線程:QuartzScheduler被創(chuàng)建時創(chuàng)建一個QuartzSchedulerThread實例。
源碼路徑
編程喵:https://github.com/itwanger/coding-more codingmore-springtask:https://github.com/itwanger/codingmore-learning codingmore-quartz:https://github.com/itwanger/codingmore-learning
沒有什么使我停留——除了目的,縱然岸旁有玫瑰、有綠蔭、有寧靜的港灣,我是不系之舟。
推薦閱讀:

