1、Spring調(diào)度的兩種方式
Spring提供了兩種后臺任務(wù)的方法,分別是:
- 調(diào)度任務(wù),@Schedule
- 異步任務(wù),@Async
當(dāng)然,使用這兩個是有條件的,需要在spring應(yīng)用的上下文中聲明
<task:annotation-driven/>當(dāng)然,如果我們是基于java配置的,需要在配置哪里加多EnableScheduling和@EnableAsync 就像下面這樣
1
2
3
4
5
|
@EnableScheduling @EnableAsync public class WebAppConfig { .... } |
除此之外,還是有第三方庫可以調(diào)用的,例如Quartz.
2、@Schedule
先看下@Schedule怎么調(diào)用再說
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public final static long ONE_DAY = 24 * 60 * 60 * 1000 ; public final static long ONE_HOUR = 60 * 60 * 1000 ; @Scheduled (fixedRate = ONE_DAY) public void scheduledTask() { System.out.println( " 我是一個每隔一天就會執(zhí)行一次的調(diào)度任務(wù)" ); } @Scheduled (fixedDelay = ONE_HOURS) public void scheduleTask2() { System.out.println( " 我是一個執(zhí)行完后,隔一小時就會執(zhí)行的任務(wù)" ); } @Scheduled (initialDelay= 1000 , fixedRate= 5000 ) public void doSomething() { // something that should execute periodically } @Scheduled (cron = "0 0/1 * * * ? " ) public void ScheduledTask3() { System.out.println( " 我是一個每隔一分鐘就就會執(zhí)行的任務(wù)" ); } |
需要注意的
- 關(guān)于最后一個,在指定時間執(zhí)行的任務(wù),里面使用的是Cron表達(dá)式,同時我們看到了兩個不一樣的面孔fixedDelay& fixedRate,前者fixedDelay表示在指定間隔運行程序,例如這個程序在今晚九點運行程序,跑完這個方法后的一個小時,就會再執(zhí)行一次,而后者fixedDelay者是指,這個函數(shù)每隔一段時間就會被調(diào)用(我們這里設(shè)置的是一天),不管再次調(diào)度的時候,這個方法是在運行還是結(jié)束了。而前者就要求是函數(shù)運行結(jié)束后開始計時的,這就是兩者區(qū)別。
- 這個還有一個initialDelay的參數(shù),是第一次調(diào)用前需要等待的時間,這里表示被調(diào)用后的,推遲一秒再執(zhí)行,這適合一些特殊的情況。
- 我們在serviceImpl類寫這些調(diào)度任務(wù)時候,也需要在這些我們定義的serviceInterface的借口中寫多這個接口,要不然會爆 but not found in any interface(s) for bean JDK proxy.Either pull the method up to an interface or
3、@Async
有時候我們會調(diào)用一些特殊的任務(wù),任務(wù)會比較耗時,重要的是,我們不管他返回的后果。這時候我們就需要用這類的異步任務(wù)啦,調(diào)用后就讓他去跑,不堵塞主線程,我們繼續(xù)干別的。代碼像下面這樣:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
public void AsyncTask(){ @Async public void doSomeHeavyBackgroundTask( int sleepTime) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } @Async public Future<String> doSomeHeavyBackgroundTask() { try { Thread.sleep( 3000 ); } catch (InterruptedException e) { e.printStackTrace(); } return null ; } public void printLog() { System.out.println( " i print a log ,time=" + System.currentTimeMillis()); } } |
我們寫個簡單的測試類來測試下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
@RunWith (SpringJUnit4ClassRunner. class ) @WebAppConfiguration @ContextConfiguration (classes = AsycnTaskConfig. class ) //要聲明@EnableASync public class AsyncTaskTest { @Autowired AsyncTask asyncTask; @Test public void AsyncTaskTest() throws InterruptedException { if (asyncTask != null ) { asyncTask.doSomeHeavyBackgroundTask( 4000 ); asyncTask.printLog(); Thread.sleep( 5000 ); } } } |
這感覺比我們手動開多一個線程方便多了,不想異步的話直接把@Async去掉就可以了,另外如果你想要返回個結(jié)果的,這需要加多個Future<>,關(guān)于這個Future,完全可以寫多幾篇文章介紹,順便把FutureTask介紹了。如果想修改Spring boot的默認(rèn)線程池配置,可以實現(xiàn)AsyncConfigurer.
需要注意的:
相對于@scheduled,這個可以有參數(shù)和返回個結(jié)果,因為這個是我們調(diào)用的,而調(diào)度的任務(wù)是spring調(diào)用的。
異步方法不能內(nèi)部調(diào)用,只能像上面那樣,外部調(diào)用,否則就會變成阻塞主線程的同步任務(wù)啦!這個坑我居然跳下去了!例如下面這樣的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
public void AsyncTask(){ public void fakeAsyncTaskTest(){ doSomeHeavyBackgroundTask( 4000 ); printLog(); //你會發(fā)現(xiàn),當(dāng)你像這樣內(nèi)部調(diào)用的時候,居然是同步執(zhí)行的,不是異步的!! } @Async public void doSomeHeavyBackgroundTask( int sleepTime) { try { Thread.sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } public void printLog() { System.out.println( " i print a log " ); } } |
- 另外一點就是不要重復(fù)的掃描,這也會導(dǎo)致異步無效,具體的可以看這個stackoveflow的spring-async-not-working Issue。
- 關(guān)于異常處理,難免在這個異步執(zhí)行過程中有異常發(fā)生,對于這個問題,spring提供的解決方案如下,實現(xiàn)
1
2
3
4
5
6
7
|
AsyncUncaughtExceptionHandler接口。 public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { // handle exception } } |
寫好我們的異常處理后,我們需要配置一下,告訴spring,這個異常處理就是我們在運行異步任務(wù)時候,拋出錯誤時的異常終結(jié)者
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { @Bean public AsyncTask asyncBean() { return new AsyncTask(); } @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize( 7 ); executor.setMaxPoolSize( 42 ); executor.setQueueCapacity( 11 ); executor.setThreadNamePrefix( "MyExecutor-" ); executor.initialize(); return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new MyAsyncUncaughtExceptionHandler(); } } |
4、Quartz登場
處理這兩個外,還有一個和spring整合的第三方庫叫Quartz
看了下官網(wǎng)的使用簡介,也是挺逗的,現(xiàn)在都習(xí)慣用maven,gradle之類來關(guān)系這些依賴了,他還叫人下載,也是不知為何,詳情點擊->http://quartz-scheduler.org/documentation/quartz-2.2.x/quick-start
估計有可能是因為沒再維護(hù)了的原因吧,看了下,最新版2.2居然是Sep, 2013更新的…
居然是停更的,不過Quartz作為一個企業(yè)級應(yīng)用的任務(wù)調(diào)度框架,還是一個可以的候選項目的。
這里不鋪開講,有興趣就去官網(wǎng)看下吧。整體用起來感覺是沒有spring自己的后臺任務(wù)方便,不過也可以接受,只需要簡單的配置就可以使用了。
@Scheduled 和@Async的使用
如題,今天在知乎突然看到一份關(guān)于springboot自帶調(diào)度器的問題思考,有這么一段內(nèi)容“在使用@Scheduled注解時,如果不自己重新配置調(diào)度器,那么就會使用默認(rèn)的,從而會導(dǎo)致一些調(diào)度執(zhí)行上的問題”;聯(lián)系到自己在程序中使用時沒有關(guān)注到這個問題,因此仔細(xì)測試研究一番,最終了解了其中的一些關(guān)鍵思想。
首先,需要了解@Scheduled 和@Async這倆注解的區(qū)別:
@Scheduled 任務(wù)調(diào)度注解,主要用于配置定時任務(wù);springboot默認(rèn)的調(diào)度器線程池大小為 1。
@Async 任務(wù)異步執(zhí)行注解,主要用于方法上,表示當(dāng)前方法會使用新線程異步執(zhí)行;springboot默認(rèn)執(zhí)行器線程池大小為100。
所以,如果在使用springboot定時器時,如果有多個定時任務(wù)時,在使用默認(rèn)的調(diào)度器配置,就會出現(xiàn)排隊現(xiàn)象,因為同時只能有一個任務(wù)在執(zhí)行,這個時候當(dāng)一個任務(wù)掛死,那后面的定時任務(wù)就不能有效執(zhí)行了;
解決辦法就是自定義調(diào)度器,有兩種方式:
方法一:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
@Bean public TaskScheduler scheduledExecutorService() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize( 10 ); scheduler.setThreadNamePrefix( "scheduled-thread-" ); //設(shè)置線程池關(guān)閉的時候等待所有任務(wù)都完成再繼續(xù)銷毀其他的Bean scheduler.setWaitForTasksToCompleteOnShutdown( true ); //設(shè)置線程池中任務(wù)的等待時間,如果超過這個時候還沒有銷毀就強(qiáng)制銷毀,以確保應(yīng)用最后能夠被關(guān)閉,而不是阻塞住 scheduler.setAwaitTerminationSeconds( 60 ); //這里采用了CallerRunsPolicy策略,當(dāng)線程池沒有處理能力的時候,該策略會直接在 execute 方法的調(diào)用線程中運行被拒絕的任務(wù);如果執(zhí)行程序已關(guān)閉,則會丟棄該任務(wù) scheduler.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy()); return scheduler; } |
方法二:
1
2
3
4
5
6
7
8
9
10
11
|
@Configuration public class ScheduledConfig implements SchedulingConfigurer { public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(setExecutor()); } @Bean (destroyMethod= "shutdown" ) public Executor setExecutor(){ return Executors.newScheduledThreadPool( 10 ); // 10個線程來處理。 } } |
上述自定義調(diào)度器的方式,會有一個問題:當(dāng)有足夠的空余線程時,多任務(wù)時并行執(zhí)行,但是同一定時任務(wù)仍會同步執(zhí)行(當(dāng)定時任務(wù)的執(zhí)行時間大于每次執(zhí)行的時間間隔時即可發(fā)現(xiàn));
配合@Async 注解使用,這樣在每次執(zhí)行定時任務(wù)時就新開一個線程,異步非阻塞運行;同時使用這兩個注解的效果,相當(dāng)于@Scheduled僅僅負(fù)責(zé)調(diào)度,而@Async指定的executor負(fù)責(zé)任務(wù)執(zhí)行,不再使用調(diào)度器中的執(zhí)行器來執(zhí)行任務(wù)(由實際測試結(jié)果來猜測的,并沒有找到對應(yīng)的源碼邏輯,待后續(xù)補充)。
自定義執(zhí)行器配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Bean ( "taskExecutor" ) public ThreadPoolTaskExecutor taskExecutor(){ ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(corePoolSize); executor.setMaxPoolSize(maxPoolSize); executor.setQueueCapacity(queueCapacity); executor.setKeepAliveSeconds(keepAliveTime); executor.setThreadNamePrefix(threadNamePrefix); // 線程池對拒絕任務(wù)的處理策略 executor.setRejectedExecutionHandler( new ThreadPoolExecutor.CallerRunsPolicy()); // 初始化 executor.initialize(); return executor; } |
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持服務(wù)器之家。
原文鏈接:https://www.cnblogs.com/slimer/p/6401394.html