1. 程式人生 > 程式設計 >springboot如何配置定時任務

springboot如何配置定時任務

概述

在Java環境下建立定時任務有多種方式:

  • 使用while迴圈配合 Thread.sleep(),雖然稍嫌粗陋但也勉強可用
  • 使用 Timer和 TimerTask
  • 使用 ScheduledExecutorService
  • 定時任務框架,如Quartz

在SpringBoot下執行定時任務無非也就這幾種方式(主要還是後兩種)。只不過SpringBoot做了許多底層的工作,我們只需要做些簡單的配置就行了。

通過註解實現定時任務

在SpringBoot中僅通過註解就可以實現常用的定時任務。步驟就兩步:

在啟動類中新增 @EnableScheduling註解

@EnableScheduling
@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication.run(MyApplication.class,args);
}

}

在目標方法中新增 @Scheduled註解,同時在 @Scheduled註解中新增觸發定時任務的元資料。

@Scheduled(fixedRate = 1000)
public void job() {
System.out.println(Thread.currentThread().getId() + " ----- job1 ----- " + System.currentTimeMillis());
}

注意: 目標方法需要沒有任何引數,並且返回型別為 void 。

這裡的定時任務元資料是“fixRate=1000”,意思是固定間隔每1000毫秒即執行一次該任務。

再來看幾個 @Schedule註解的引數:

  • fixedRate:設定定時任務執行的時間間隔,該值為當前任務啟動時間與下次任務啟動時間之差;
  • fixedDelay:設定定時任務執行的時間間隔,該值為當前任務結束時間與下次任務啟動時間之差;
  • cron:通過cron表示式來設定定時任務啟動時間,在Cron Generator網站可以直接生成cron表示式。

這樣建立的定時任務存在一個問題:如存在多個定時任務,這些任務會同步執行,也就是說所有的定時任務都是在一個執行緒中執行。

再添幾個定時任務來執行下看看:

@Scheduled(fixedRate = 1000)
public void job1() {
System.out.println(Thread.currentThread().getId() + " ----- job1 ----- " + System.currentTimeMillis());
}

@Scheduled(fixedRate = 1000)
public void job2() {
System.out.println(Thread.currentThread().getId() + " ----- job2 ----- " + System.currentTimeMillis());
}

@Scheduled(fixedRate = 1000)
public void job3() {
System.out.println(Thread.currentThread().getId() + " ----- job3 ----- " + System.currentTimeMillis());
}

程式碼中一共建立了三個定時任務,每個定時任務的執行間隔都是1000毫秒,在任務體中輸出了執行任務的執行緒ID和執行時間。

看下執行結果:

20 ----- job3 ----- 1573120568263
20 ----- job1 ----- 1573120568263
20 ----- job2 ----- 1573120568263
20 ----- job3 ----- 1573120569264
20 ----- job1 ----- 1573120569264
20 ----- job2 ----- 1573120569264
20 ----- job3 ----- 1573120570263
20 ----- job1 ----- 1573120570263
20 ----- job2 ----- 1573120570263

可以看到這三個定時任務的執行有如下的特點:

  • 所有的定時任務每次都是在同一個執行緒上執行;
  • 雖然未必是job1第一個開始執行,但是每批任務的執行次序是固定的——這是由fixRate引數決定的

這樣的定時任務已經能夠覆蓋絕大部分的使用場景了,但是它的缺點也很明顯:前面的任務執行時間過長必然會影響之後的任務的執行。為了解決這個問題,我們需要非同步執行定時任務。接下來的部分我們將主要著眼於如何實現非同步執行定時任務。

通過@Async註解實現非同步定時任務

最常用的方式是使用 @Async註解來實現非同步執行定時任務。啟用 @Async註解的步驟如下:

在啟動類中新增 @EnableAsync註解:

@EnableAsync
@EnableScheduling
@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication.run(MyApplication.class,args);
}

}

在定時任務方法上新增 @Async註解

@Async
@Scheduled(fixedRate = 1000)
public void job1() {
System.out.println(Thread.currentThread().getId() + " ----- job1 ----- " + System.currentTimeMillis());
}

我們為前面的三個定時任務都加上 @Async註解再執行看看:

25 ----- job1 ----- 1573121781415
24 ----- job3 ----- 1573121781415
26 ----- job2 ----- 1573121781415
30 ----- job3 ----- 1573121782298
31 ----- job1 ----- 1573121782299
32 ----- job2 ----- 1573121782299
25 ----- job2 ----- 1573121783304
35 ----- job3 ----- 1573121783306
36 ----- job1 ----- 1573121783306

通過輸出資訊可以看到每個定時任務都在不同的執行緒上執行,彼此的執行次序和執行時間也互不影響,說明配置為非同步執行已經成功。

通過配置實現非同步定時任務

現在我們有必要稍稍深入瞭解下springboot定時任務的執行機制了。

springboot的定時任務主要涉及到兩個介面: TaskScheduler和 TaskExecutor。在springboot的預設定時任務實現中,這兩個介面的實現類是 ThreadPoolTaskScheduler和 ThreadPoolTaskExecutor。

ThreadPoolTaskScheduler負責實現任務的定時執行機制,而 ThreadPoolTaskExecutor則負責實現任務的非同步執行機制。二者中, ThreadPoolTaskScheduler執行棧更偏底層一些。

儘管在職責上有些區別,但是兩者在底層上都是依賴java的執行緒池機制實現的: ThreadPoolTaskScheduler依賴的底層執行緒池是 ScheduledExecutorService,springboot預設為其提供的coreSize是1,所以預設的定時任務都是在一個執行緒中執行; ThreadPoolTaskExecutor依賴的底層執行緒池是 ThreadPoolExecutor,springboot預設為其提供的corePoolSize是8。

說到這裡應該清楚了:我們可以不新增 @Async註解,僅通過調整 ThreadPoolTaskScheduler依賴的執行緒池的coreSize也能實現多執行緒非同步執行;同樣的,即使添加了 @Async註解,將 ThreadPoolTaskExecutor依賴的執行緒池的corePoolSize設定為1,那定時任務還是隻能在一個執行緒上同步執行。看下springboot的相關配置項:

spring:
task:
scheduling:
pool:
size: 1
execution:
pool:
core-size: 2

其中spring.task.scheduling是 ThreadPoolTaskScheduler的執行緒池配置項,spring.task.execution是 ThreadPoolExecutor的執行緒池配置項。

再稍稍擴充套件下: @Async註解的value屬性就是用來指明使用的 TaskExecutor例項的。預設值是空字串,表示使用的是springboot自啟動的 TaskExecutor例項。如有需要,也可以使用自定義的 TaskExecutor例項,如下:

/**
 * 配置執行緒池
 * @return
 */
@Bean(name = "scheduledPoolTaskExecutor")
public ThreadPoolTaskExecutor getAsyncThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(20);
taskExecutor.setMaxPoolSize(200);
taskExecutor.setQueueCapacity(25);
taskExecutor.setKeepAliveSeconds(200);
taskExecutor.setThreadNamePrefix("my-task-executor-");
// 執行緒池對拒絕任務(無執行緒可用)的處理策略,目前只支援AbortPolicy、CallerRunsPolicy;預設為後者
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//排程器shutdown被呼叫時等待當前被排程的任務完成
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
//等待時長
taskExecutor.setAwaitTerminationSeconds(60);
taskExecutor.initialize();
return taskExecutor;
}

此外,還有一種做法是通過提供自定義的 TaskScheduler Bean例項來實現非同步執行。要提供提供自定義的 TaskScheduler 例項,可以直接通過 @Bean註解宣告建立,也可以在 SchedulingConfigurer介面中配置。這些在後面我們會提到。

呼叫SpringBoot介面實現定時任務

有時候會需要將定時任務的定時元資料寫在資料庫或其他配置中心以便統一維護。這種情況就不是通過註解能夠搞定的了,此時我們需要使用springboot定時任務一些元件來自行程式設計實現。常用的元件包括 TaskScheduler、 Triger介面和 SchedulingConfigurer介面。

注意:因為我們用到了springboot的定時任務元件,所以仍然需要在啟動類上新增 @EnableScheduling註解。

Trigger介面

Trigger介面主要用來設定定時元資料。要通過程式實現定時任務就不能不用到這個介面。這個介面有兩個實現類:

  • PeriodicTrigger用來配置固定時長的定時元資料
  • CronTrigger用來配置cron表示式定時元資料

使用TaskScheduler介面

TaskScheduler介面前面我們提過,這個介面需要配合 Trigger介面一起使用來實現定時任務,看個例子:

@Autowired
private TaskScheduler taskScheduler;

public void job() {
int fixRate = 10;
taskScheduler.schedule(() -> System.out.println("job4 ----- " + System.currentTimeMillis()),new PeriodicTrigger(fixRate,TimeUnit.SECONDS));
}

在上面的程式碼裡,我們使用 @Autowired註解獲取了springbootr容器裡預設的 TaskScheduler例項,然後通過 PeriodicTrigger設定了定時元資料,定時任務的任務體則是一個 Runable介面的實現(在這裡只是輸出一行資訊)。

因為預設的 TaskScheduler例項的執行緒池coreSize是1,所以如有多個併發任務,這些任務的執行仍然是同步的。要調整為非同步可以在配置檔案中配置,也可以通過提供一個自定義的 TaskScheduler例項來設定:

@Bean("taskScheduler") 
public TaskScheduler taskExecutor() {
ThreadPoolTaskScheduler executor = new ThreadPoolTaskScheduler();
executor.setPoolSize(20);
executor.setThreadNamePrefix("my-task-scheduler");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
//排程器shutdown被呼叫時等待當前被排程的任務完成
executor.setWaitForTasksToCompleteOnShutdown(true);
//等待時長
executor.setAwaitTerminationSeconds(60);
return executor;
}

使用SchedulingConfigurer介面

SchedulingConfigurer介面的主要用處是註冊基於 Trigger介面自定義實現的定時任務。

在實現 SchedulingConfigurer介面後,通常還需要使用 @Configuration註解(當然啟動類上的 @EnableScheduling註解也不能少)來宣告它實現類。

這個介面唯一的一個方法就是configureTasks,字面意思是配置定時任務。這個方法最重要的引數是一個 ScheduledTaskRegistrar定時任務註冊類例項,該類有8個方法,允許我們以不同的方式註冊定時任務。

簡單做了個實現:

@Configuration
public class MyTaskConfigurer implements SchedulingConfigurer {

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

taskRegistrar
.addCronTask(
() -> System.out.println(Thread.currentThread().getId() + " --- job5 ----- " + System.currentTimeMillis()),"0/1 * * * * ?"
);

taskRegistrar
.addFixedDelayTask(
() -> System.out.println(Thread.currentThread().getId() + " --- job6 ----- " + System.currentTimeMillis()),1000
);

taskRegistrar
.addFixedRateTask(
() -> System.out.println(Thread.currentThread().getId() + " --- job7 ----- " + System.currentTimeMillis()),1000
);
}
}

這裡我們只使用了三種註冊任務的方法,分別嘗試註冊了fixDelay、fixRate以及cron觸發的定時任務。

springboot會自動啟動註冊的定時任務。看下執行結果:

22 --- job7 ----- 1573613616349
22 --- job6 ----- 1573613616350
22 --- job5 ----- 1573613617001
22 --- job7 ----- 1573613617352
22 --- job6 ----- 1573613617353
22 --- job5 ----- 1573613618065
22 --- job7 ----- 1573613618350
22 --- job6 ----- 1573613618355
22 --- job5 ----- 1573613619002

在執行結果中可以看到這裡的任務也是在單一執行緒同步執行的。要設定為非同步執行也簡單,因為 SchedulingConfigurer介面的另一個作用就是為定時任務提供自定義的 TaskScheduler例項。來看下:

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
scheduler.setThreadNamePrefix("my-task-scheduler");
scheduler.setPoolSize(10);
scheduler.initialize();
taskRegistrar.setTaskScheduler(scheduler);
}

在這裡,我將之前註冊的定時任務去掉了,目的是想驗證下這裡的配置是否對註解實現的定時任務有效。經檢驗是可行的。當然對在configureTasks方法中配置的定時任務肯定也是有效的。我就不一一貼結果了。

另外,需要注意:如 SchedulingConfigurer介面例項已經注入,將無法再獲取到springboot預設提供的 TaskScheduler介面例項。

通過Quartz實現定時任務

Quartz是一個非常強大的定時任務管理框架。短短的一篇文章未必能介紹清楚Quartz的全部用法。所以這裡只是簡單地演示下如何在springboot中是如何使用Quartz的。更多的用法建議優先參考Quartz官方文件。

在spring-boot-web 2.0及之後的版本,已經自動集成了quartz,如果不使用spring-boot-web或使用較早的版本的話我們還需要加一些依賴:

<!-- quartz -->
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
</dependency>
<!-- spring整合quartz -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
<!-- SchedulerFactoryBean依賴了tx包中的PlatformTransactionManager類,因為quartz的分散式功能是基於資料庫完成的 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>

新增完成這些依賴後,springboot服務在啟動時也會自啟動內部的quartz。事實上springboot已經為我們準備好了幾乎全部的quartz的配置。我們要做的只是把自定義的任務填進去。

首先我們需要建立一個Job例項,來實現Job的具體行為。

@Component
public class MyQuartzJob extends QuartzJobBean {

@Override
protected void executeInternal(JobExecutionContext context) {
JobDataMap map = context.getMergedJobDataMap();
// 從作業上下文中取出Key
String key = map.getString("key");
System.out.println(Thread.currentThread().getId() + " -- job8 ---------------------->>>>" + key);
}

}

QuartzJobBean是Spring提供的Quartz Job抽象類。在實現這個類的時候我們可以獲取注入到spring中的其他Bean。

配置Job

@Configuration
public class QuartzConfig implements InitializingBean {

@Autowired
private SchedulerFactoryBean schedulerFactoryBean;


@Override
public void afterPropertiesSet() throws Exception {
config();
}


private void config() throws SchedulerException {
Scheduler scheduler = schedulerFactoryBean.getScheduler();

JobDetail jobDetail = buildJobDetail();
Trigger trigger = buildJobTrigger(jobDetail);
scheduler.scheduleJob(jobDetail,trigger);
}


private JobDetail buildJobDetail() {
// 用來儲存互動資訊
JobDataMap dataMap = new JobDataMap();
dataMap.put("key","zhyea.com");

return JobBuilder.newJob(MyQuartzJob.class)
.withIdentity(UUID.randomUUID().toString(),"chobit-job")
.usingJobData(dataMap)
.build();
}


private Trigger buildJobTrigger(JobDetail jobDetail) {
return TriggerBuilder.newTrigger()
.forJob(jobDetail)
.withIdentity(jobDetail.getKey().getName(),"chobit-trigger")
.withSchedule(CronScheduleBuilder.cronSchedule("0/1 * * * * ?"))
.build();
}
}

在建立 QuartzConfig類的時候實現了 InitializingBean介面,目的是在 QuartzConfig例項及依賴類都完成注入後可以立即執行配置組裝操作。

這裡面有幾個關鍵介面需要說明下:

  • SchedulerFactoryBean,Quartz Scheduler工廠類,springboot自動化配置實現;
  • Scheduer,負責Quartz Job排程,可從工廠類例項獲取;
  • JobDetail,執行Quartz Job封裝;
  • Trigger,完成Quartz Job啟動。

還可以在配置檔案中新增Quartz的配置:

spring:
quartz:
startupDelay: 180000 #這裡是毫秒值

這裡配置了讓Quartz預設延遲啟動3分鐘。

看下執行結果:

30 -- job8 ---------------------->>>>zhyea.com
31 -- job8 ---------------------->>>>zhyea.com
32 -- job8 ---------------------->>>>zhyea.com
33 -- job8 ---------------------->>>>zhyea.com
34 -- job8 ---------------------->>>>zhyea.com
...

好了,就這些內容了。前面用到的程式都上傳到了GITHUB,有需要可以參考下。

參考文件

Spring Task Execution and Scheduling
Scheduling Tasks
SpringBoot Quartz Scheduler
Spring Boot Quartz Scheduler Example: Building an Email Scheduling app
Quartz Scheduler Tutorials

以上就是springboot如何配置定時任務的詳細內容,更多關於springboot配置定時任務的資料請關注我們其它相關文章!