背景
在日常開發過程中,使用定時任務去執行一些業務邏輯是很常見的一種場景。比如定時發送短信,郵件,電商系統的定時自動收貨、定時上下架功能等等。
一般實現定時任務有以下幾種方案:
JDK自帶
- JDK自帶的Timer:這是java自帶的java.util.Timer類,這個類允許你調度一個java.util.TimerTask任務。使用這種方式可以讓你的程序按照某一個頻度執行,一般用的較少。
- JDK1.5+ 新增的ScheduledExecutorService:是基於線程池設計的定時任務類,每個調度任務都會分配到線程池中的一個線程去執行,也就是說,任務是併發執行,互不影響。
第三方框架
使用 Quartz、elastic-job、xxl-job 等開源第三方定時任務框架,適合分佈式項目應用。不過配置起來稍顯複雜,不太易上手。
Spring Task
Spring3.0以後自帶的task,可以將它看成一個輕量級的Quartz,而且使用起來比Quartz簡單許多。使用 Spring 提供的一個註解 @Schedule即可,開發簡單,使用比較方便。
本文主要向大家介紹使用註解和ScheduledExecutorService來實現定時任務。
使用
註解實現
正式開始之前,我們先看一下通過註解實現定時任務的方法。
1.啓動類添加 @EnableScheduling 註解
2.在被spring管理的類的方法上添加 @Scheduled 註解
@Scheduled(cron = "0/1 * * * * *") public void task01() throws InterruptedException { log.info("task01,當前時間{},線程名稱{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), Thread.currentThread().getName()); TimeUnit.SECONDS.sleep(10); } @Scheduled(cron = "0/1 * * * * *") public void task02() { log.info("task02,當前時間{},線程名稱{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), Thread.currentThread().getName()); } @Scheduled(cron = "0/1 * * * * *") public void task03() { log.info("task03,當前時間{},線程名稱{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), Thread.currentThread().getName()); }
啓動項目之後,控制檯輸出以下內容:
2021-07-28 16:32:25.020 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:32:25,線程名稱scheduling-1 2021-07-28 16:32:25.021 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:32:25,線程名稱scheduling-1 2021-07-28 16:32:25.021 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task01,當前時間2021-07-28 16:32:25,線程名稱scheduling-1 2021-07-28 16:32:35.025 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:32:35,線程名稱scheduling-1 2021-07-28 16:32:35.026 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:32:35,線程名稱scheduling-1 2021-07-28 16:32:36.001 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:32:36,線程名稱scheduling-1 2021-07-28 16:32:36.002 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task01,當前時間2021-07-28 16:32:36,線程名稱scheduling-1 2021-07-28 16:32:46.004 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:32:46,線程名稱scheduling-1 2021-07-28 16:32:46.005 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:32:46,線程名稱scheduling-1 2021-07-28 16:32:47.001 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:32:47,線程名稱scheduling-1 2021-07-28 16:32:47.002 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:32:47,線程名稱scheduling-1 2021-07-28 16:32:47.003 INFO 27964 --- [ scheduling-1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task01,當前時間2021-07-28 16:32:47,線程名稱scheduling-1
可以看出,在這種情況下,所有的定時任務都是在同一個線程(線程名稱scheduling-1)下執行的,並且只有在前一個定時任務執行完畢之後纔會執行其他的定時任務。
這時,如果我們配置一個類型爲 TaskScheduler 類型的bean,然後重啓程序,可看到這時控制檯的內容是這樣的
配置Bean
@Bean public ThreadPoolTaskScheduler threadPoolTaskScheduler(){ ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); //設置線程池大小 threadPoolTaskScheduler.setPoolSize(10); //設置線程名稱前綴 threadPoolTaskScheduler.setThreadNamePrefix("schedule-task"); return threadPoolTaskScheduler; }
控制檯
2021-07-28 16:38:38.028 INFO 24956 --- [ schedule-task3] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:38:38,線程名稱schedule-task3 2021-07-28 16:38:38.028 INFO 24956 --- [ schedule-task2] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:38:38,線程名稱schedule-task2 2021-07-28 16:38:38.028 INFO 24956 --- [ schedule-task1] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task01,當前時間2021-07-28 16:38:38,線程名稱schedule-task1 2021-07-28 16:38:39.002 INFO 24956 --- [ schedule-task6] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:38:39,線程名稱schedule-task6 2021-07-28 16:38:39.002 INFO 24956 --- [ schedule-task5] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:38:39,線程名稱schedule-task5 2021-07-28 16:38:40.002 INFO 24956 --- [ schedule-task2] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:38:40,線程名稱schedule-task2 2021-07-28 16:38:40.002 INFO 24956 --- [ schedule-task3] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:38:40,線程名稱schedule-task3 2021-07-28 16:38:41.001 INFO 24956 --- [ schedule-task6] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:38:41,線程名稱schedule-task6 2021-07-28 16:38:41.001 INFO 24956 --- [ schedule-task4] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:38:41,線程名稱schedule-task4 2021-07-28 16:38:42.001 INFO 24956 --- [ schedule-task5] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:38:42,線程名稱schedule-task5 2021-07-28 16:38:42.001 INFO 24956 --- [schedule-task10] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task02,當前時間2021-07-28 16:38:42,線程名稱schedule-task10 2021-07-28 16:38:43.001 INFO 24956 --- [ schedule-task2] c.c.c.i.l.s.i.RequirementDtoServiceImpl : task03,當前時間2021-07-28 16:38:43,線程名稱schedule-task2
通過結果我們可以看出,這時定時任務是多線程執行的,不同任務也不用等待其他任務執行完畢之後才能執行。
其實這個類就是我們下面要介紹的通過 ThreadPoolTaskScheduler 線程池來控制定時任務。下面我們來看一下詳細的用法。
線程池實現
1.啓動類添加 @EnableScheduling 註解
2.創建定時任務類
ScheduleController.java
/** * <p> * 接口控制定時任務開始和停止 * </p> * * @className ScheduleController * @author Sue * @date 2021/7/28 **/ @RestController @RequestMapping("/task") public class ScheduleController { ScheduleTaskService scheduleTaskService; public ScheduleController(ScheduleTaskService scheduleTaskService) { this.scheduleTaskService = scheduleTaskService; } @PostMapping("/startCron") public R startCron() { scheduleTaskService.startCorn(); return R.ok("定時任務啓動成功!"); } @PostMapping("/stopCron") public R stopCron() { scheduleTaskService.stopCorn(); return R.ok("定時任務關閉成功!"); } }
ScheduleTaskService.java
/** * <p> * 定時任務 * </p> * * @className ThreadPoolTaskSchedulerService * @author Sue * @create 2021/7/21 **/ public interface ScheduleTaskService { /** * <p> * 開始定時任務 * </p> * * @return boolean * @author Sue * @date 2021/7/21 */ boolean startCorn(); /** * <p> * 關閉定時任務 * </p> * * @return boolean * @author Sue * @date 2021/7/21 */ boolean stopCorn(); }
ScheduleTaskServiceImpl.java
/** * <p> * 定時任務 * </p> * * @className ThreadPoolTaskSchedulerServiceImpl * @author Sue * @create 2021/7/21 **/ @Slf4j @Service public class ScheduleTaskServiceImpl implements ScheduleTaskService { private ScheduledFuture<?> future; ThreadPoolTaskScheduler threadPoolTaskScheduler; public ScheduleTaskServiceImpl(ThreadPoolTaskScheduler threadPoolTaskScheduler) { this.threadPoolTaskScheduler = threadPoolTaskScheduler; } @Override public boolean startCorn() { if (future != null) { future.cancel(true); log.info("定時任務已停止"); } //每10秒執行一次,一般情況下這裏可以通過讀取外部配置來實現動態的修改定時任務的執行時間 String cornConfig = "0/10 * * * * *"; future = threadPoolTaskScheduler.schedule(new ScheduledTaskRunnable(), new CronTrigger(cornConfig)); log.info("定時任務開啓"); return false; } @Override public boolean stopCorn() { if (future != null) { future.cancel(true); log.info("定時任務已停止"); } return false; } static class ScheduledTaskRunnable implements Runnable { @Override public void run() { //需要執行的業務邏輯 log.info("定時任務開始執行,每10秒執行一次,當前時間{}", LocalDateTimeUtil.format(LocalDateTimeUtil.now(), DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); } } }
3.項目啓動後,調用接口,控制定時任務的啓動和停止
測試
調用startCorn接口,可以看出定時任務成功開啓
調用stopCorn接口,停止定時任務
補充
corn表達式的使用
cron 表達式是一個字符串,該字符串由 6 個空格分爲 7 個域,每一個域代表一個時間含義。 通常定義 “年” 的部分可以省略,實際常用的 Cron 表達式由前 6 部分組成。格式如下:
[秒] [分] [時] [日] [月] [周] [年]
Seconds Minutes Hours Day-of-Month Month Day-of-Week Year (optional field)
需要說明的是,Cron 表達式中,“周” 是從週日開始計算的。“周” 域上的 1 表示的是週日,7 表示週六。
通配符說明
- * 表示所有值. 例如:在分的字段上設置 “*”,表示每一分鐘都會觸發
- ? 表示不指定值。使用的場景爲不需要關心當前設置這個字段的值。例如:要在每月的10號觸發一個操作,但不關心是周幾,所以需要周位置的那個字段設置爲"?" 具體設置爲 0 0 0 10 * ?
- - 表示區間。例如 在小時上設置 “10-12”,表示 10,11,12點都會觸發。
- , 表示指定多個值,例如在周字段上設置 “MON,WED,FRI” 表示週一,週三和週五觸發
- / 用於遞增觸發。如在秒上面設置"5/15" 表示從5秒開始,每增15秒觸發(5,20,35,50)。 在月字段上設置’1/3’所示每月1號開始,每隔三天觸發一次。
- L 表示最後的意思。在日字段設置上,表示當月的最後一天(依據當前月份,如果是二月還會依據是否是潤年[leap]), 在周字段上表示星期六,相當於"7"或"SAT"。如果在"L"前加上數字,則表示該數據的最後一個。例如在周字段上設置"6L"這樣的格式,則表示“本月最後一個星期五"
- W 表示離指定日期的最近那個工作日(週一至週五). 例如在日字段上設置"15W",表示離每月15號最近的那個工作日觸發。如果15號正好是週六,則找最近的週五(14號)觸發, 如果15號是周未,則找最近的下週一(16號)觸發.如果15號正好在工作日(週一至週五),則就在該天觸發。如果指定格式爲 “1W”,它則表示每月1號往後最近的工作日觸發。如果1號正是週六,則將在3號下週一觸發。(注,“W"前只能設置具體的數字,不允許區間”-").
- # 序號(表示每月的第幾個周幾),例如在周字段上設置"6#3"表示在每月的第三個週六.注意如果指定"#5",正好第五週沒有周六,則不會觸發該配置(用在母親節和父親節再合適不過了) ;
提示:
'L’和 'W’可以組合使用。如果在日字段上設置"LW",則表示在本月的最後一個工作日觸發;
周字段的設置,若使用英文字母是不區分大小寫的,即MON 與mon相同;
例子
- "0 */1 * * * ?" 每隔 1 分鐘執行一次
- "0 24,30 * * * ?" 在24分,30分執行一次
- "0 0 10,14,16 * * ?" 每天上午10點,下午2點,4點
- "0 0/30 9-17 * * ?" 朝九晚五工作時間內每半小時
- "0 0 12 ? * WED" 表示每個星期三中午12點
- "0 0 12 * * ?" 每天中午12點觸發
- "0 15 10 ? * *" 每天上午10:15觸發
- "0 15 10 * * ?" 每天上午10:15觸發
- "0 15 10 * * ? *" 每天上午10:15觸發
- "0 15 10 * * ? 2005" 2005年的每天上午10:15觸發
- "0 * 14 * * ?" 在每天下午2點到下午2:59期間的每1分鐘觸發
- "0 0/5 14 * * ?" 在每天下午2點到下午2:55期間的每5分鐘觸發
- "0 0/5 14,18 * * ?" 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
- "0 0-5 14 * * ?" 在每天下午2點到下午2:05期間的每1分鐘觸發
- "0 10,44 14 ? 3 WED" 每年三月的星期三的下午2:10和2:44觸發
- "0 15 10 ? * MON-FRI" 週一至週五的上午10:15觸發
- "0 15 10 15 * ?" 每月15日上午10:15觸發
- "0 15 10 L * ?" 每月最後一日的上午10:15觸發
- "0 15 10 ? * 6L" 每月的最後一個星期五上午10:15觸發
- "0 15 10 ? * 6L 2002-2005" 2002年至2005年的每月的最後一個星期五上午10:15觸發
- "0 15 10 ? * 6#3" 每月的第三個星期五上午10:15觸發