補習系列(9)-springboot 定時器,你用對了嗎
目錄
- 簡介
- 一、應用啟動任務
- 二、JDK 自帶調度線程池
- 三、@Scheduled
- 定制 @Scheduled 線程池
- 四、@Async
- 定制 @Async 線程池
- 小結
簡介
大多數的應用程序都離不開定時器,通常在程序啟動時、運行期間會需要執行一些特殊的處理任務。
比如資源初始化、數據統計等等,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篇" ,如果覺得老司機的文章還不賴,請多多分享轉發^-^
補習系列(9)-springboot 定時器,你用對了嗎