spring成神之路第三十八篇:@Scheduled & @EnableScheduling 定時器詳解
spring中 @Scheduled & @EnableScheduling
這2個註解,可以用來快速開發定時器,使用特別的簡單。
如何使用?
用法
1、需要定時執行的方法上加上@Scheduled註解,這個註解中可以指定定時執行的規則,稍後詳細介紹。
2、Spring容器中使用@EnableScheduling開啟定時任務的執行,此時spring容器才可以識別@Scheduled標註的方法,然後自動定時執行。
案例
db中有很多需要推送的任務,然後將其檢索出來,推送到手機端,來個定時器,每秒一次從庫中檢測需要推送的訊息,然後推送到手機端。
package com.javacode2018.scheduled.demo1; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Componentpublic class PushJob { //推送方法,每秒執行一次 @Scheduled(fixedRate = 1000) public void push() throws InterruptedException { System.out.println("模擬推送訊息," + System.currentTimeMillis()); } }
來個spring配置類,需要使用@EnableScheduling
標註
package com.javacode2018.scheduled.demo1; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.scheduling.annotation.EnableScheduling; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @ComponentScan @EnableScheduling//在spring容器中啟用定時任務的執行 public class MainConfig1 { @Bean public ScheduledExecutorService scheduledExecutorService() { return Executors.newScheduledThreadPool(20); } }
測試類
package com.javacode2018.scheduled; import com.javacode2018.scheduled.demo1.MainConfig1; import org.junit.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.util.concurrent.TimeUnit;public class ScheduledTest { @Test public void test1() throws InterruptedException { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); context.register(MainConfig1.class); context.refresh(); //休眠一段時間,房子junit自動退出 TimeUnit.SECONDS.sleep(10000); } }
執行輸出,每秒會輸出一次,如下:
模擬推送訊息,1595840822998 模擬推送訊息,1595840823998 模擬推送訊息,1595840824998 模擬推送訊息,1595840825998 模擬推送訊息,1595840826998 模擬推送訊息,1595840827998 模擬推送訊息,1595840828998
@Scheduled配置定時規則
@Scheduled可以用來配置定時器的執行規則,非常強大,@Scheduled中主要有8個引數,我們一一來了解一下。
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Repeatable(Schedules.class) public @interface Scheduled { String cron() default ""; String zone() default ""; long fixedDelay() default -1; String fixedDelayString() default ""; long fixedRate() default -1; String fixedRateString() default ""; long initialDelay() default -1; String initialDelayString() default ""; }
1. cron
該引數接收一個cron表示式
,cron表示式
是一個字串,字串以5或6個空格隔開,分開共6或7個域,每一個域代表一個含義。
cron表示式語法
[秒][分][小時][日][月][周][年]
“注:[年]不是必須的域,可以省略[年],則一共6個域
序號 | 說明 | 必填 | 允許填寫的值 | 允許的萬用字元 |
---|---|---|---|---|
1 | 秒 | 是 | 0-59 | , - * / |
2 | 分 | 是 | 0-59 | , - * / |
3 | 時 | 是 | 0-23 | , - * / |
4 | 日 | 是 | 1-31 | , - * ? / L W |
5 | 月 | 是 | 1-12 / JAN-DEC | , - * / |
6 | 周 | 是 | 1-7 or SUN-SAT | , - * ? / L # |
7 | 年 | 否 | 1970-2099 | , - * / |
萬用字元說明:
*
表示所有值。例如:在分的欄位上設定 *,表示每一分鐘都會觸發。
?
表示不指定值。使用的場景為不需要關心當前設定這個欄位的值。例如:要在每月的10號觸發一個操作,但不關心是周幾,所以需要周位置的那個欄位設定為”?” 具體設定為 0 0 0 10 * ?
-
表示區間。例如 在小時上設定 “10-12”,表示 10,11,12點都會觸發。
,
表示指定多個值,例如在周欄位上設定 “MON,WED,FRI” 表示週一,週三和週五觸發
/
用於遞增觸發。如在秒上面設定”5/15” 表示從5秒開始,每增15秒觸發(5,20,35,50)。在日欄位上設定’1/3’所示每月1號開始,每隔三天觸發一次。
L
表示最後的意思。在日欄位設定上,表示當月的最後一天(依據當前月份,如果是二月還會依據是否是潤年[leap]), 在周欄位上表示星期六,相當於”7”或”SAT”。如果在”L”前加上數字,則表示該資料的最後一個。例如在周欄位上設定”6L”這樣的格式,則表示“本月最後一個星期五”
W
表示離指定日期的最近那個工作日(週一至週五). 例如在日欄位上置”15W”,表示離每月15號最近的那個工作日觸發。如果15號正好是週六,則找最近的週五(14號)觸發, 如果15號是周未,則找最近的下週一(16號)觸發.如果15號正好在工作日(週一至週五),則就在該天觸發。如果指定格式為 “1W”,它則表示每月1號往後最近的工作日觸發。如果1號正是週六,則將在3號下週一觸發。(注,”W”前只能設定具體的數字,不允許區間”-“)。
#
序號(表示每月的第幾個周幾),例如在周欄位上設定”6#3”表示在每月的第三個週六.注意如果指定”#5”,正好第五週沒有周六,則不會觸發該配置(用在母親節和父親節再合適不過了) ;小提示:’L’和 ‘W’可以一組合使用。如果在日欄位上設定”LW”,則表示在本月的最後一個工作日觸發;周欄位的設定,若使用英文字母是不區分大小寫的,即MON與mon相同。
示例
每隔5秒執行一次:*/5 * * * * ?
每隔1分鐘執行一次:0 */1 * * * ?
每天23點執行一次:0 0 23 * * ?
每天凌晨1點執行一次:0 0 1 * * ?
每月1號凌晨1點執行一次:0 0 1 1 * ?
每月最後一天23點執行一次:0 0 23 L * ?
每週星期六凌晨1點實行一次:0 0 1 ? * L
在26分、29分、33分執行一次:0 26,29,33 * * * ?
每天的0點、13點、18點、21點都執行一次:0 0 0,13,18,21 * * ?
cron表示式使用佔位符
另外,cron
屬性接收的cron表示式
支援佔位符。
如:配置檔案:
time: cron: */5 * * * * * interval: 5
每5秒執行一次:
@Scheduled(cron="${time.cron}") void testPlaceholder1() { System.out.println("Execute at " + System.currentTimeMillis()); } @Scheduled(cron="*/${time.interval} * * * * *") void testPlaceholder2() { System.out.println("Execute at " + System.currentTimeMillis()); }
2. zone
時區,接收一個java.util.TimeZone#ID
。cron表示式
會基於該時區解析。預設是一個空字串,即取伺服器所在地的時區。比如我們一般使用的時區Asia/Shanghai
。該欄位我們一般留空。
3. fixedDelay
上一次執行完畢時間點之後多長時間再執行。
如:
@Scheduled(fixedDelay = 5000) //上一次執行完畢時間點之後5秒再執行
4. fixedDelayString
與 3. fixedDelay
意思相同,只是使用字串的形式。唯一不同的是支援佔位符。
如:
@Scheduled(fixedDelayString = "5000") //上一次執行完畢時間點之後5秒再執行
佔位符的使用(配置檔案中有配置:time.fixedDelay=5000)
@Scheduled(fixedDelayString = "${time.fixedDelay}") void testFixedDelayString() { System.out.println("Execute at " + System.currentTimeMillis()); }
5. fixedRate
上一次開始執行時間點之後多長時間再執行。
如:
@Scheduled(fixedRate = 5000) //上一次開始執行時間點之後5秒再執行
6. fixedRateString
與 fixedRate
意思相同,只是使用字串的形式,唯一不同的是支援佔位符。
7. initialDelay
第一次延遲多長時間後再執行。
如:
@Scheduled(initialDelay=1000, fixedRate=5000) //第一次延遲1秒後執行,之後按fixedRate的規則每5秒執行一次
8. initialDelayString
與 initialDelay
意思相同,只是使用字串的形式,唯一不同的是支援佔位符。
@Schedules註解
這個註解不用多解釋,看一下原始碼就知道作用了,當一個方法上面需要同時指定多個定時規則的時候,可以通過這個來配置
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Schedules { Scheduled[] value(); }
如:
//2個定時器,500毫秒的,1000毫秒的 @Schedules({@Scheduled(fixedRate = 500), @Scheduled(fixedRate = 1000)}) public void push3() { }
為定時器定義執行緒池
定時器預設情況下使用下面的執行緒池來執行定時任務的
new ScheduledThreadPoolExecutor(1)
只有一個執行緒,相當於只有一個幹活的人,如果需要定時執行的任務太多,這些任務只能排隊執行,會出現什麼問題?
如果有些任務耗時比較長,導致其他任務排隊時間比較長,不能有效的正常執行,直接影響到業務。
看下面程式碼,2個方法,都使用了@Scheduled(fixedRate = 1000)
,表示每秒執行一次,而push1
方法中模擬耗時2秒,方法會中打印出執行緒名稱、時間等資訊,一會注意觀察輸出
package com.javacode2018.scheduled.demo2; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.concurrent.TimeUnit; @Component public class PushJob { //推送方法,每秒執行一次 @Scheduled(fixedRate = 1000) public void push1() throws InterruptedException { //休眠2秒,模擬耗時操作 TimeUnit.SECONDS.sleep(2); System.out.println(Thread.currentThread().getName() + " push1 模擬推送訊息," + System.currentTimeMillis()); } //推送方法,每秒執行一次 @Scheduled(fixedRate = 1000) public void push2() { System.out.println(Thread.currentThread().getName() + " push2 模擬推送訊息," + System.currentTimeMillis()); } }
執行輸出
pool-1-thread-1 push1 模擬推送訊息,1595902615507 pool-1-thread-1 push2 模擬推送訊息,1595902615507 pool-1-thread-1 push1 模擬推送訊息,1595902617507 pool-1-thread-1 push2 模擬推送訊息,1595902617507 pool-1-thread-1 push1 模擬推送訊息,1595902619508 pool-1-thread-1 push2 模擬推送訊息,1595902619508
注意上面的輸出,執行緒名稱都是pool-1-thread-1
,並且有個問題,push2中2次輸出時間間隔是2秒,這就是由於執行緒池中只有一個執行緒導致了排隊執行而產生的問題。
可以通過自定義定時器中的執行緒池來解決這個問題,定義一個ScheduledExecutorService
型別的bean,名稱為taskScheduler
@Bean public ScheduledExecutorService taskScheduler() { //設定需要並行執行的任務數量 int corePoolSize = 20; return new ScheduledThreadPoolExecutor(corePoolSize); }
此時問題就解決了,再次執行一下上面案例程式碼,結果如下,此時執行緒名稱不一樣了,且push2執行正常了
pool-1-thread-2 push2 模擬推送訊息,1595903154636 pool-1-thread-2 push2 模擬推送訊息,1595903155636 pool-1-thread-1 push1 模擬推送訊息,1595903156636 pool-1-thread-3 push2 模擬推送訊息,1595903156636 pool-1-thread-1 push2 模擬推送訊息,1595903157636
原始碼 & 原理
從EnableScheduling
註解開始看,這個註解會匯入SchedulingConfiguration
類
SchedulingConfiguration
是一個配置類,內部定義了ScheduledAnnotationBeanPostProcessor
型別的bean
ScheduledAnnotationBeanPostProcessor
是一個bean後置處理器,內部有個postProcessAfterInitialization
方法,spring中任何bean在初始化完畢之後,會自動呼叫postProcessAfterInitialization
方法,而ScheduledAnnotationBeanPostProcessor
在這個方法中會解析bean中標註有@Scheduled
註解的方法,這些方法也就是需要定時執行的方法。
ScheduledAnnotationBeanPostProcessor
還實現了一個介面:SmartInitializingSingleton
,SmartInitializingSingleton
中有個方法afterSingletonsInstantiated
會在spring容器中所有單例bean初始化完畢之後呼叫,定期器的裝配及啟動都是在這個方法中進行的。
org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor#afterSingletonsInstantiate
案例原始碼
https://gitee.com/javacode2018/spring-series
路人甲java所有案例程式碼以後都會放到這個上面,大家watch一下,可以持續關注動態。
來源:https://mp.weixin.qq.com/s?__biz=MzA5MTkxMDQ4MQ==&mid=2648935890&idx=2&sn=f8a8e01e7399161621152b2e4caa8128&scene=21#wechat_redirect