本文參考自Spring官方文檔 34. Task Execution and Scheduling。
在程序中常常有定時任務(wù)的需求,例如每隔一周生成一次報表、每個月月末清空用戶積分等等。Spring也提供了相應(yīng)的支持,我們可以非常方便的按時執(zhí)行任務(wù)。
項目準(zhǔn)備
這里我使用Gradle來建立項目,然后在build.gradle
中添加下面一行。springVersion的值是目前最新的Spring版本'4.3.7.RELEASE'
。使用Maven的話也添加相應(yīng)的行。spring-context會自動引入spring-core等幾個最基本的依賴。
compile group: 'org.springframework', name: 'spring-context', version: springVersion
定時任務(wù)屬于Spring的核心支持部分,所以我們不需要再添加其他的依賴了。所以定時任務(wù)功能既可以在命令行程序中使用,也可以在Java Web程序中使用。當(dāng)然后者可能使用的更廣泛一些(畢竟Web程序需要一直運行的嘛)。
這里我們定義兩個任務(wù),后面會讓它們可以定時執(zhí)行。
public interface IService {
void doService();
}
public class SimpleService implements IService {
@Override
public void doService() {
LocalTime time = LocalTime.now();
System.out.println("This is a simple service:" + time);
}
}
public class ExpensiveTaskService implements IService {
@Override
public void doService() {
try {
Thread.sleep(TimeUnit.SECONDS.toMillis(1));
LocalTime time = LocalTime.now();
System.out.println("This is an expensive task:" + time);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Spring的任務(wù)抽象
TaskExecutor
TaskExecutor
接口是任務(wù)執(zhí)行接口,類似于java.util.concurrent.Executor
,該接口只有一個方法execute(Runnable task)
,用于執(zhí)行任務(wù)。
Spring提供了一組TaskExecutor的實現(xiàn),詳細(xì)列表可以看這里34.2.1. TaskExecutor types。要使用它們也很簡單,直接注冊為Spring Bean,然后注入到程序中即可使用。
TaskScheduler
TaskScheduler
接口是定時器的抽象,它的源代碼如下??梢钥吹剑摻涌诎艘唤M方法用于指定任務(wù)執(zhí)行的時間。
public interface TaskScheduler {
ScheduledFuture schedule(Runnable task, Trigger trigger);
ScheduledFuture schedule(Runnable task, Date startTime);
ScheduledFuture scheduleAtFixedRate(Runnable task, Date startTime, long period);
ScheduledFuture scheduleAtFixedRate(Runnable task, long period);
ScheduledFuture scheduleWithFixedDelay(Runnable task, Date startTime, long delay);
ScheduledFuture scheduleWithFixedDelay(Runnable task, long delay);
}
Spring提供了兩個實現(xiàn),一是TimerManagerTaskScheduler
,會將任務(wù)代理到CommonJ TimerManager實例。第二個是ThreadPoolTaskScheduler
,當(dāng)我們不需要管理線程的時候就可以使用該類。而且它還同時實現(xiàn)了TaskExecutor
接口,所以一個ThreadPoolTaskScheduler
實例即可同時用于執(zhí)行定時任務(wù)。
Trigger
在定時器接口的方法中我們可以發(fā)現(xiàn)一個方法接受Trigger接口, 而Trigger也是一個接口,抽象了觸發(fā)任務(wù)執(zhí)行的觸發(fā)器。
Trigger接口有兩個實現(xiàn),先說說比較簡單的一個PeriodicTrigger
。它直接按照給定的時間間隔觸發(fā)任務(wù)執(zhí)行。更常用的一個觸發(fā)器是CronTrigger
,它使用Cron表達(dá)式指定何時執(zhí)行任務(wù)。下面是Spring官方的一個例子。
scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));
關(guān)于Cron表達(dá)式的信息可以參考這篇博客QuartZ Cron表達(dá)式。另外還有一個可以在線生成Cron表達(dá)式的網(wǎng)站:CroMaker,不過好像需要XX才能訪問。而且好像Spring不支持第二個星期一這樣的定時器設(shè)置,所以如果有這樣的需求,需要使用Quartz。
配置任務(wù)
任務(wù)配置既可以使用Java配置,也可以使用XML配置。不管使用哪種方法,首先需要將要執(zhí)行的方法所在的類配置為Spring Bean。例如下面就用XML配置注冊了兩個要執(zhí)行的任務(wù)。
<bean id="simpleService" class="yitian.study.service.SimpleService"/>
<bean id="expensiveTaskService"
class="yitian.study.service.ExpensiveTaskService"/>
Java配置
定時任務(wù)
首先看看Java配置。我們需要在配置類上添加@EnableScheduling,如果需要異步的定時任務(wù),還需要添加@Async。
@Configuration
@EnableAsync
@EnableScheduling
public class TaskConfiguration {
}
然后在要執(zhí)行的方法上添加@Scheduled注解。@Scheduled注解有幾個參數(shù),任務(wù)會在相應(yīng)參數(shù)的時間下執(zhí)行。cron參數(shù)指定Cron表達(dá)式;fixedDelay指定任務(wù)執(zhí)行的間隔,單位是毫秒;initialDelay指定當(dāng)程序啟動后多長時間開始執(zhí)行第一次任務(wù),單位是毫秒;zone指定任務(wù)執(zhí)行時間所在的時區(qū)。下面的例子簡單的指定了每隔一秒重復(fù)執(zhí)行一次任務(wù)。
public class SimpleService implements IService {
@Scheduled(fixedDelay = 1000)
@Override
public void doService() {
LocalTime time = LocalTime.now();
System.out.println("This is a simple service:" + time);
}
}
異步任務(wù)
然后是異步任務(wù),如果任務(wù)執(zhí)行時間比較長的話,我們可以考慮使用異步的任務(wù)。當(dāng)調(diào)用異步任務(wù)的時候,異步方法直接返回,異步任務(wù)會交由相應(yīng)的任務(wù)執(zhí)行器來執(zhí)行。在Spring中標(biāo)記異步方法很簡單,直接在方法上使用@Async注解。如果需要指定異步方法使用的執(zhí)行器,可以向注解傳遞執(zhí)行器的名稱。異步方法可以返回空值。
@Async("otherExecutor")
void doSomething(String s) {
}
但是如果異步方法想返回其他值的話,就必須使用Future。不過不僅是java.util.concurrent.Future
,異步方法還可以返回Spring的org.springframework.util.concurrent.ListenableFuture
和JDK8的java.util.concurrent.CompletableFuture
類型。
@Async
Future<String> returnSomething(int i) {
}
異步方法不僅可以用于定時任務(wù)中,在Spring的其他地方也可以使用。例如Spring Data JPA可以使用@Async編寫異步的查詢方法。
需要注意,異步方法沒有對應(yīng)的XML配置,如果我們想讓方法是異步的,只能使用注解。當(dāng)然也不是完全不行,不過就比較麻煩了,你需要使用AsyncExecutionInterceptor
和AOP配合才能達(dá)到類似的效果。
如果需要處理異步方法的異常,我們需要實現(xiàn)一個AsyncUncaughtExceptionHandler
。下面的異步異常處理器簡單的打印異常信息。
public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
ex.printStackTrace();
}
}
然后通過實現(xiàn)AsyncConfigurer
接口(Java配置方式)或者task:annotation-driven
(XML配置方式)的exception-handler
元素來配置。
XML配置
Spring提供了task命名空間,讓配置定時任務(wù)非常簡單。
定時器
task:scheduler
會注冊一個ThreadPoolTaskScheduler
定時器,它只有一個屬性線程池大小。默認(rèn)是1,我們需要根據(jù)任務(wù)的數(shù)量指定一個合適的大小。
<task:scheduler id="threadPoolTaskScheduler"
pool-size="10"/>
執(zhí)行器
task:executor
會注冊一個ThreadPoolTaskExecutor
執(zhí)行器,我們可以使用它的相關(guān)屬性來配置該執(zhí)行器。默認(rèn)情況下執(zhí)行隊列是無限的,可能會導(dǎo)致JVM使用完所有內(nèi)存。因此我們最好指定一個確定的數(shù)值。還有一個rejection-policy
屬性,指定執(zhí)行器隊列滿時的執(zhí)行策略:默認(rèn)是AbortPolicy
,直接拋出異常;如果當(dāng)系統(tǒng)忙時丟棄某些任務(wù)是可接受的,可以使用DiscardPolicy
或DiscardOldestPolicy
策略;當(dāng)系統(tǒng)負(fù)載較重時還可以使用CallerRunsPolicy
,它不會將任務(wù)交給執(zhí)行器線程,而是讓調(diào)用者線程來執(zhí)行該任務(wù)。最后一個就是keep-alive
屬性,也就是超出線程池數(shù)量 線程完成任務(wù)之后的存活時間,單位是秒。
<task:executor id="threadPoolTaskExecutor"
pool-size="10"
queue-capacity="10"/>
執(zhí)行任務(wù)
執(zhí)行任務(wù)很簡單,使用<task:scheduled-tasks>
指定要執(zhí)行的Bean和方法即可。
<task:scheduled-tasks>
<task:scheduled ref="simpleService" method="doService"
cron="*/1 * * * * *"/>
<task:scheduled ref="expensiveTaskService" method="doService"
cron="*/2 * * * * *"/>
</task:scheduled-tasks>
要設(shè)置定時的話,只需要指定相應(yīng)的屬性即可。
<task:scheduled-tasks scheduler="myScheduler">
<task:scheduled ref="beanA" method="methodA" fixed-delay="5000" initial-delay="1000"/>
<task:scheduled ref="beanB" method="methodB" fixed-rate="5000"/>
<task:scheduled ref="beanC" method="methodC" cron="*/5 * * * * MON-FRI"/>
</task:scheduled-tasks>
<task:scheduler id="myScheduler" pool-size="10"/>
Quartz集成
Quartz是一個定時任務(wù)的庫。Spring也提供了它的支持。Quartz的使用方法請查閱相應(yīng)文檔。這里只簡單介紹一下。
Spring的Quartz集成在spring-context-support
包中,它還需要Spring事務(wù)的支持。因此我們需要下面這樣的依賴聲明。
compile group: 'org.springframework', name: 'spring-tx', version: springVersion
compile group: 'org.springframework', name: 'spring-context-support', version: springVersion
compile group: 'org.quartz-scheduler', name: 'quartz', version: '2.2.3'
定義任務(wù)
Quartz的任務(wù)需要繼承Quartz的Job接口。所以一個典型的任務(wù)可以寫成這樣。
public class QuartzService implements IService, Job {
@Override
public void doService() {
System.out.println("This is a quartz service");
}
@Override
public void execute(JobExecutionContext context) throws JobExecutionException {
System.out.println("Do something in execute method of quartz");
}
}
JobDetailFactoryBean
JobDetailFactoryBean用來定義實現(xiàn)了Job接口的任務(wù)。如果需要添加更多信息,可以使用jobDataAsMap
屬性設(shè)置。
<bean id="jobDetail"
class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
<property name="jobClass" value="yitian.study.service.QuartzService"/>
<property name="jobDataAsMap">
<map>
<entry key="timeout" value="10"/>
</map>
</property>
</bean>
MethodInvokingJobDetailFactoryBean
如果任務(wù)沒有實現(xiàn)Job接口,也可以執(zhí)行,這時候需要使用MethodInvokingJobDetailFactoryBean。如果存在任務(wù)對象,使用targetObject
屬性,如果有任務(wù)類,使用targetClass
屬性。
<bean id="methodJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="quartzService"/>
<property name="targetMethod" value="doService"/>
<property name="concurrent" value="true"/>
</bean>
觸發(fā)器
有了任務(wù),就可以定義觸發(fā)器了。觸發(fā)器有兩個:SimpleTriggerFactoryBean
,以指定的間隔重復(fù)執(zhí)行任務(wù);CronTriggerFactoryBean
,以給定的Cron表達(dá)式執(zhí)行任務(wù)。Quartz的Cron表達(dá)式比Spring 的強大,它支持第幾個星期幾這樣的Cron表達(dá)式。
<bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerFactoryBean">
<property name="jobDetail" ref="jobDetail"/>
<property name="startDelay" value="0"/>
<property name="repeatInterval" value="1000"/>
</bean>
<bean id="cronTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="methodJobDetail"/>
<property name="cronExpression" value="*/2 * * * * ?"/>
</bean>
執(zhí)行任務(wù)
有了觸發(fā)器,我們就可以執(zhí)行任務(wù)了。注冊一個SchedulerFactoryBean
,然后將觸發(fā)器的Bean引用傳入即可。
<bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="cronTrigger"/>
<ref bean="simpleTrigger"/>
</list>
</property>
</bean>