Spring 定時任務

yeats2008 7年前發布 | 81K 次閱讀 Spring Quartz JEE框架

在程序中常常有定時任務的需求,例如每隔一周生成一次報表、每個月月末清空用戶積分等等。Spring也提供了相應的支持,我們可以非常方便的按時執行任務。

項目準備

這里我使用Gradle來建立項目,然后在 build.gradle 中添加下面一行。springVersion的值是目前最新的Spring版本 '4.3.7.RELEASE' 。使用Maven的話也添加相應的行。spring-context會自動引入spring-core等幾個最基本的依賴。

compile group: 'org.springframework', name: 'spring-context', version: springVersion

定時任務屬于Spring的核心支持部分,所以我們不需要再添加其他的依賴了。所以定時任務功能既可以在命令行程序中使用,也可以在Java Web程序中使用。當然后者可能使用的更廣泛一些(畢竟Web程序需要一直運行的嘛)。

這里我們定義兩個任務,后面會讓它們可以定時執行。

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的任務抽象

TaskExecutor

TaskExecutor 接口是任務執行接口,類似于 java.util.concurrent.Executor ,該接口只有一個方法 execute(Runnable task) ,用于執行任務。

Spring提供了一組TaskExecutor的實現,詳細列表可以看這里 34.2.1. TaskExecutor types 。要使用它們也很簡單,直接注冊為Spring Bean,然后注入到程序中即可使用。

TaskScheduler

TaskScheduler 接口是定時器的抽象,它的源代碼如下。可以看到,該接口包含了一組方法用于指定任務執行的時間。

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提供了兩個實現,一是 TimerManagerTaskScheduler ,會將任務代理到CommonJ TimerManager實例。第二個是 ThreadPoolTaskScheduler ,當我們不需要管理線程的時候就可以使用該類。而且它還同時實現了 TaskExecutor 接口,所以一個 ThreadPoolTaskScheduler 實例即可同時用于執行定時任務。

Trigger

在定時器接口的方法中我們可以發現一個方法接受Trigger接口, 而Trigger也是一個接口,抽象了觸發任務執行的觸發器。

Trigger接口有兩個實現,先說說比較簡單的一個 PeriodicTrigger 。它直接按照給定的時間間隔觸發任務執行。更常用的一個觸發器是 CronTrigger ,它使用Cron表達式指定何時執行任務。下面是Spring官方的一個例子。

scheduler.schedule(task, new CronTrigger("0 15 9-17 * * MON-FRI"));

關于Cron表達式的信息可以參考這篇博客 QuartZ Cron表達式 。另外還有一個可以在線生成Cron表達式的網站: CroMaker ,不過好像需要XX才能訪問。而且好像Spring不支持第二個星期一這樣的定時器設置,所以如果有這樣的需求,需要使用Quartz。

配置任務

任務配置既可以使用Java配置,也可以使用XML配置。不管使用哪種方法,首先需要將要執行的方法所在的類配置為Spring Bean。例如下面就用XML配置注冊了兩個要執行的任務。

<bean id="simpleService" class="yitian.study.service.SimpleService"/>
    <bean id="expensiveTaskService"
          class="yitian.study.service.ExpensiveTaskService"/>

Java配置

定時任務

首先看看Java配置。我們需要在配置類上添加@EnableScheduling,如果需要異步的定時任務,還需要添加@Async。

@Configuration
@EnableAsync
@EnableScheduling
public class TaskConfiguration {
}

然后在要執行的方法上添加@Scheduled注解。@Scheduled注解有幾個參數,任務會在相應參數的時間下執行。cron參數指定Cron表達式;fixedDelay指定任務執行的間隔,單位是毫秒;initialDelay指定當程序啟動后多長時間開始執行第一次任務,單位是毫秒;zone指定任務執行時間所在的時區。下面的例子簡單的指定了每隔一秒重復執行一次任務。

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);
    }
}

異步任務

然后是異步任務,如果任務執行時間比較長的話,我們可以考慮使用異步的任務。當調用異步任務的時候,異步方法直接返回,異步任務會交由相應的任務執行器來執行。在Spring中標記異步方法很簡單,直接在方法上使用@Async注解。如果需要指定異步方法使用的執行器,可以向注解傳遞執行器的名稱。異步方法可以返回空值。

@Async("otherExecutor")
void doSomething(String s) {
    // this will be executed asynchronously by "otherExecutor"
}

但是如果異步方法想返回其他值的話,就必須使用Future。不過不僅是 java.util.concurrent.Future ,異步方法還可以返回Spring的 org.springframework.util.concurrent.ListenableFuture 和JDK8的 java.util.concurrent.CompletableFuture 類型。

@Async
Future<String> returnSomething(int i) {
    // this will be executed asynchronously
}

異步方法不僅可以用于定時任務中,在Spring的其他地方也可以使用。例如Spring Data JPA可以使用@Async編寫異步的查詢方法。

需要注意,異步方法沒有對應的XML配置,如果我們想讓方法是異步的,只能使用注解。當然也不是完全不行,不過就比較麻煩了,你需要使用 AsyncExecutionInterceptor 和AOP配合才能達到類似的效果。

如果需要處理異步方法的異常,我們需要實現一個 AsyncUncaughtExceptionHandler 。下面的異步異常處理器簡單的打印異常信息。

public class MyAsyncUncaughtExceptionHandler implements AsyncUncaughtExceptionHandler {
    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        ex.printStackTrace();
    }
}

然后通過實現 AsyncConfigurer 接口(Java配置方式)或者 task:annotation-driven (XML配置方式)的 exception-handler 元素來配置。

XML配置

Spring提供了task命名空間,讓配置定時任務非常簡單。

定時器

task:scheduler 會注冊一個 ThreadPoolTaskScheduler 定時器,它只有一個屬性線程池大小。默認是1,我們需要根據任務的數量指定一個合適的大小。

<task:scheduler id="threadPoolTaskScheduler"
                    pool-size="10"/>

執行器

task:executor 會注冊一個 ThreadPoolTaskExecutor 執行器,我們可以使用它的相關屬性來配置該執行器。默認情況下執行隊列是無限的,可能會導致JVM使用完所有內存。因此我們最好指定一個確定的數值。還有一個 rejection-policy 屬性,指定執行器隊列滿時的執行策略:默認是 AbortPolicy ,直接拋出異常;如果當系統忙時丟棄某些任務是可接受的,可以使用 DiscardPolicy 或 DiscardOldestPolicy 策略;當系統負載較重時還可以使用 CallerRunsPolicy ,它不會將任務交給執行器線程,而是讓調用者線程來執行該任務。最后一個就是 keep-alive 屬性,也就是超出線程池數量 線程完成任務之后的存活時間,單位是秒。

<task:executor id="threadPoolTaskExecutor"
                   pool-size="10"
                   queue-capacity="10"/>

執行任務

執行任務很簡單,使用 <task:scheduled-tasks> 指定要執行的Bean和方法即可。

<task:scheduled-tasks>
        <task:scheduled ref="simpleService" method="doService"
                        cron="*/1 * * * * *"/>
        <task:scheduled ref="expensiveTaskService" method="doService"
                        cron="*/2 * * * * *"/>
    </task:scheduled-tasks>

要設置定時的話,只需要指定相應的屬性即可。

<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 是一個定時任務的庫。Spring也提供了它的支持。Quartz的使用方法請查閱相應文檔。這里只簡單介紹一下。

Spring的Quartz集成在 spring-context-support 包中,它還需要Spring事務的支持。因此我們需要下面這樣的依賴聲明。

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'

定義任務

Quartz的任務需要繼承Quartz的Job接口。所以一個典型的任務可以寫成這樣。

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用來定義實現了Job接口的任務。如果需要添加更多信息,可以使用 jobDataAsMap 屬性設置。

<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

如果任務沒有實現Job接口,也可以執行,這時候需要使用MethodInvokingJobDetailFactoryBean。如果存在任務對象,使用 targetObject 屬性,如果有任務類,使用 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>

觸發器

有了任務,就可以定義觸發器了。觸發器有兩個: SimpleTriggerFactoryBean ,以指定的間隔重復執行任務; CronTriggerFactoryBean ,以給定的Cron表達式執行任務。Quartz的Cron表達式比Spring 的強大,它支持第幾個星期幾這樣的Cron表達式。

<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>

執行任務

有了觸發器,我們就可以執行任務了。注冊一個 SchedulerFactoryBean ,然后將觸發器的Bean引用傳入即可。

<bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="cronTrigger"/>
            <ref bean="simpleTrigger"/>
        </list>
    </property>
</bean>

 

來自:https://segmentfault.com/a/1190000008709730

 

 本文由用戶 yeats2008 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!