簡介
大多數的應用程序都離不開定時器,通常在程序啟動時、運行期間會需要執行一些特殊的處理任務。
比如資源初始化、數據統計等等,SpringBoot 作為一個靈活的框架,有許多方式可以實現定時器或異步任務。
我總結了下,大致有以下幾種:
-
- 使用 JDK 的 TimerTask
-
- 使用 JDK 自帶調度線程池
-
- 使用 Quartz 調度框架
-
- 使用 @Scheduled 、@Async 注解
其中第一種使用 TimerTask 的方法已經不建議使用,原因是在系統時間跳變時TimerTask存在掛死的風險。
第三種使用 Quartz 調度框架可以實現非常強大的定時器功能,包括分布式調度定時器等等。
考慮作為大多數場景使用的方式,下面的篇幅將主要介紹 第二、第四種。
一、應用啟動任務
在 SpringBoot 應用程序啟動時,可以通過以下兩個接口實現初始化任務:
- CommandLineRunner
- ApplicationRunner
兩者的區別不大,唯一的不同在於:
CommandLineRunner 接收一組字符串形式的進程命令啟動參數;
ApplicationRunner 接收一個經過解析封裝的參數體對象。
詳細的對比看下代碼:
public class CommandLines {
private static final Logger logger = LoggerFactory.getLogger(CommandLines.class);
@Component
@Order(1)
public static class CommandLineAppStartupRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
logger.info(
"[CommandLineRunner]Application started with command-line arguments: {} .To kill this application, press Ctrl + C.",
Arrays.toString(args));
}
}
@Component
@Order(2)
public static class AppStartupRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
logger.info("[ApplicationRunner]Your application started with option names : {}", args.getOptionNames());
}
}
}
二、JDK 自帶調度線程池
為了實現定時調度,需要用到 ScheduledThreadpoolExecutor
初始化一個線程池的代碼如下:
/**
* 構造調度線程池
*
* @param corePoolSize
* @param poolName
* @return
*/
public static ScheduledThreadPoolExecutor newSchedulingPool(int corePoolSize, String poolName) {
ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(corePoolSize);
// 設置變量
if (!StringUtils.isEmpty(poolName)) {
threadPoolExecutor.setThreadFactory(new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread tr = new Thread(r, poolName + r.hashCode());
return tr;
}
});
}
return threadPoolExecutor;
}
可以將 corePoolSize 指定為大於1,以實現定時任務的並發執行。
為了在 SpringBoot 項目中使用,我們利用一個CommandLineRunner來實現:
@Component
@Order(1)
public class ExecutorTimer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(ExecutorTimer.class);
private ScheduledExecutorService schedulePool;
@Override
public void run(String... args) throws Exception {
logger.info("start executor tasks");
schedulePool = ThreadPools.newSchedulingPool(2);
schedulePool.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
logger.info("run on every minute");
}
}, 5, 60, TimeUnit.SECONDS);
}
}
schedulePool.scheduleWithFixedDelay 指定了調度任務以固定的頻率執行。
三、@Scheduled
@Scheduled 是 Spring3.0 提供的一種基於注解實現調度任務的方式。
在使用之前,需要通過 @EnableScheduling 注解啟用該功能。
代碼如下:
/**
* 利用@Scheduled注解實現定時器
*
* @author atp
*
*/
@Component
public class ScheduleTimer {
private static final Logger logger = LoggerFactory.getLogger(ScheduleTimer.class);
/**
* 每10s
*/
@Scheduled(initialDelay = 5000, fixedDelay = 10000)
public void onFixDelay() {
logger.info("schedule job on every 10 seconds");
}
/**
* 每分鍾的0秒執行
*/
@Scheduled(cron = "0 * * * * *")
public void onCron() {
logger.info("schedule job on every minute(0 second)");
}
/**
* 啟用定時器配置
*
* @author atp
*
*/
@Configuration
@EnableScheduling
public static class ScheduleConfig {
}
}
說明
上述代碼中展示了兩種定時器的使用方式:
第一種方式
指定初始延遲(initialDelay)、固定延遲(fixedDelay);
第二種方式
通過 cron 表達式定義
這與 unix/linux 系統 crontab 的定義類似,可以實現非常靈活的定制。
一些 cron 表達式的樣例:
表達式 | 說明 |
---|---|
0 0 * * * * | 每天的第一個小時 |
*/10 * * * * * | 每10秒鍾 |
0 0 8-10 * * * | 每天的8,9,10點鍾整點 |
0 * 6,19 * * * | 每天的6點和19點每分鍾 |
0 0/30 8-10 * * * | 每天8:00, 8:30, 9:00, 9:30 10:00 |
0 0 9-17 * * MON-FRI | 工作日的9點到17點 |
0 0 0 25 12 ? | 每年的聖誕夜午夜 |
定制 @Scheduled 線程池
默認情況下,@Scheduled 注解的任務是由一個單線程的線程池進行調度的。
這樣會導致應用內的定時任務只能串行執行。
為了實現定時任務並發,或是更細致的定制,
可以使用 SchedulingConfigurer 接口。
代碼如下:
@Configuration
@EnableScheduling
public class ScheduleConfig implements SchedulingConfigurer {
@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
taskRegistrar.setScheduler(taskExecutor());
}
@Bean(destroyMethod="shutdown")
public Executor taskExecutor() {
//線程池大小
return Executors.newScheduledThreadPool(50);
}
}
四、@Async
@Async 注解的意義在於將 Bean方法的執行方式改為異步方式。
比如 在前端請求處理時,能通過異步執行提前返回結果。
類似的,該注解需要配合 @EnableAsync 注解使用。
代碼如下:
@Configuration
@EnableAsync
public static class ScheduleConfig {
}
使用 @Async 實現模擬任務
@Component
public class AsyncTimer implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(AsyncTimer.class);
@Autowired
private AsyncTask task;
@Override
public void run(String... args) throws Exception {
long t1 = System.currentTimeMillis();
task.doAsyncWork();
long t2 = System.currentTimeMillis();
logger.info("async timer execute in {} ms", t2 - t1);
}
@Component
public static class AsyncTask {
private static final Logger logger = LoggerFactory.getLogger(AsyncTask.class);
@Async
public void doAsyncWork() {
long t1 = System.currentTimeMillis();
try {
Thread.sleep((long) (Math.random() * 5000));
} catch (InterruptedException e) {
}
long t2 = System.currentTimeMillis();
logger.info("async task execute in {} ms", t2 - t1);
}
}
示例代碼中,AsyncTask 等待一段隨機時間后結束。
而 AsyncTimer 執行了 task.doAsyncWork,將提前返回。
執行結果如下:
- async timer execute in 2 ms
- async task execute in 3154 ms
這里需要注意一點,異步的實現,其實是通過 Spring 的 AOP 能力實現的。
對於 AsyncTask 內部方法間的調用卻無法達到效果。
定制 @Async 線程池
對於 @Async 線程池的定制需使用 AsyncConfigurer接口。
代碼如下:
@Configuration
@EnableAsync
public static class ScheduleConfig implements AsyncConfigurer {
@Bean
public ThreadPoolTaskScheduler taskScheduler() {
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
//線程池大小
scheduler.setPoolSize(60);
scheduler.setThreadNamePrefix("AsyncTask-");
scheduler.setAwaitTerminationSeconds(60);
scheduler.setWaitForTasksToCompleteOnShutdown(true);
return scheduler;
}
@Override
public Executor getAsyncExecutor() {
return taskScheduler();
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
}
小結
定時異步任務是應用程序通用的訴求,本文收集了幾種常見的實現方法。
作為 SpringBoot 應用來說,使用注解是最為便捷的。
在這里我們對 @Scheduled、@Async 幾個常用的注解進行了說明,
並提供定制其線程池的方法,希望對讀者能有一定幫助。
歡迎繼續關注"美碼師的補習系列-springboot篇" ,如果覺得老司機的文章還不賴,請多多分享轉發-