SpringBoot如何實(shí)現(xiàn)定時(shí)任務(wù),寫得太好了 !
寫在前面
SpringBoot創(chuàng)建定時(shí)任務(wù)的方式很簡單,主要有兩種方式:一、基于注解的方式(@Scheduled)二、數(shù)據(jù)庫動(dòng)態(tài)配置。實(shí)際開發(fā)中,第一種需要在代碼中寫死表達(dá)式,如果修改起來,又得重啟會顯示很麻煩;所以我們往往會采取第二種方式,可以直接從數(shù)據(jù)庫中讀取定時(shí)任務(wù)的指定執(zhí)行時(shí)間,無需重啟。
?
下面就來介紹下這兩種方式吧
一、基于注解(@Scheduled)
基于注解是一種靜態(tài)的方式,只需要幾行代碼就可以搞定了
添加一個(gè)配置類
1?@Configuration??//標(biāo)記配置類
?2?@EnableScheduling???//開啟定時(shí)任務(wù)
?3?public?class?MyScheduleConfig?{
?4?
?5?????//添加定時(shí)任務(wù)
?6?????@Scheduled(cron?=?"0/5?*?*?*?*??")
?7?????private?void?myTasks()?{
?8?????????System.out.println("執(zhí)行定時(shí)任務(wù)?"?+?LocalDateTime.now());
?9?????}
10?}
上面代碼的cron表達(dá)式表示每5秒執(zhí)行一次,可以通過這個(gè)網(wǎng)站(https://qqe2.com/cron)去生成要的cron表達(dá)式
啟動(dòng)應(yīng)用,控制臺看效果

?
這個(gè)方式的確很簡單方便,但前面介紹也說到了,有個(gè)缺點(diǎn)就是當(dāng)我們需要去修改定時(shí)任務(wù)的執(zhí)行周期或者停止的時(shí)候,我們需要到代碼層去修改,重啟。
?
二、數(shù)據(jù)庫動(dòng)態(tài)配置
這里使用MySQL數(shù)據(jù)庫
1、表數(shù)據(jù)添加,資源配置
1.1 添加表
CREATE?TABLE?`scheduled_job`?(
??`job_id`?int(11)?NOT?NULL?AUTO_INCREMENT?COMMENT?'主鍵id',
??`job_key`?varchar(128)?NOT?NULL?COMMENT?'定時(shí)任務(wù)完整類名',
??`cron_expression`?varchar(20)?NOT?NULL?COMMENT?'cron表達(dá)式',
??`task_explain`?varchar(50)?NOT?NULL?DEFAULT?''?COMMENT?'任務(wù)描述',
??`status`?tinyint(4)?NOT?NULL?DEFAULT?'1'?COMMENT?'狀態(tài),1:正常;-1:停用',
??PRIMARY?KEY?(`job_id`),
??UNIQUE?KEY?`job_key`?(`job_key`),
??UNIQUE?KEY?`cron_key_unique_idx`?(`job_key`)
)?ENGINE=InnoDB?AUTO_INCREMENT=1?DEFAULT?CHARSET=utf8?COLLATE=utf8_bin?COMMENT='定時(shí)任務(wù)表';
1.2 插入兩條數(shù)據(jù),job_key根據(jù)是完整的類名

?
1.3 引入依賴?
????????????org.springframework.boot
????????????spring-boot-starter-web
????????
????????
????????
????????????mysql
????????????mysql-connector-java
????????????5.1.49
????????????runtime
????????
????????
????????
????????????com.baomidou
????????????mybatis-plus-boot-starter
????????????3.3.1.tmp
????????
????????
????????
????????????org.projectlombok
????????????lombok
????????????1.18.20
????????????provided
????????
?
1.4 配置application.yml
spring:
??datasource:
????url:?jdbc:mysql://127.0.0.1:3306/test?userUnicode=true&characterEncoding=UTF8&useSSL=false
????username:?root
????password:?123
????driver-class-name:?com.mysql.jdbc.Driver
server:
??servlet:
????context-path:?/demo
??port:?8888
?
2、瘋狂貼代碼
2.1 創(chuàng)建定時(shí)任務(wù)線程池
@Configuration
@Slf4j
public?class?ScheduledConfig?{
????@Bean
????public?ThreadPoolTaskScheduler?threadPoolTaskScheduler()?{
????????log.info("創(chuàng)建定時(shí)任務(wù)調(diào)度線程池?start");
????????ThreadPoolTaskScheduler?threadPoolTaskScheduler?=?new?ThreadPoolTaskScheduler();
????????threadPoolTaskScheduler.setPoolSize(20);
????????threadPoolTaskScheduler.setThreadNamePrefix("taskExecutor-");
????????threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
????????threadPoolTaskScheduler.setAwaitTerminationSeconds(60);
????????log.info("創(chuàng)建定時(shí)任務(wù)調(diào)度線程池?end");
????????return?threadPoolTaskScheduler;
????}
}
?
2.2 項(xiàng)目啟動(dòng)時(shí)初始化定時(shí)任務(wù)
@Slf4j
@Component
public?class?ScheduledTaskRunner?implements?ApplicationRunner?{
????@Autowired
????private?ScheduledTaskService?scheduledTaskService;
????@Override
????public?void?run(ApplicationArguments?args)?throws?Exception?{
????????log.info("----初始化定時(shí)任務(wù)開始----");
????????scheduledTaskService.initTask();
????????log.info("----初始化定時(shí)任務(wù)完成----");
????}
}
?
2.3 定時(shí)任務(wù)公共接口
public?interface?ScheduledOfTask?extends?Runnable{
????void?execute();
????@Override
????default?void?run()?{
????????execute();
????}
}
?
2.4 創(chuàng)建兩個(gè)定時(shí)任務(wù)實(shí)現(xiàn)類
@Component
@Slf4j
public?class?TaskJob1?implements?ScheduledOfTask{
????@Override
????public?void?execute()?{
????????log.info("執(zhí)行任務(wù)1?"+?LocalDateTime.now());
????}
}
?
@Component
@Slf4j
public?class?TaskJob2?implements?ScheduledOfTask{
????@Override
????public?void?execute()?{
????????log.info("執(zhí)行任務(wù)2?"+?LocalDateTime.now());
????}
}
?
2.5 定時(shí)任務(wù)管理接口
public?interface?ScheduledTaskService{
????Boolean?start(ScheduledJob?scheduledJob);
????Boolean?stop(String?jobKey);
????Boolean?restart(ScheduledJob?scheduledJob);
????void?initTask();
}
?
2.6 定時(shí)任務(wù)管理實(shí)現(xiàn)類
@Slf4j
@Service
public?class?ScheduledTaskServiceImpl?implements?ScheduledTaskService?{
????/**
?????*?可重入鎖
?????*/
????private?ReentrantLock?lock?=?new?ReentrantLock();
????/**
?????*?定時(shí)任務(wù)線程池
?????*/
????@Autowired
????private?ThreadPoolTaskScheduler?threadPoolTaskScheduler;
????/**
?????*?啟動(dòng)狀態(tài)的定時(shí)任務(wù)集合
?????*/
????public?Map?scheduledFutureMap?=?new?ConcurrentHashMap<>();
????@Autowired
????private?ScheduledJobService?scheduledJobService;
????@Override
????public?Boolean?start(ScheduledJob?scheduledJob)?{
????????String?jobKey?=?scheduledJob.getJobKey();
????????log.info("啟動(dòng)定時(shí)任務(wù)"+jobKey);
????????//添加鎖放一個(gè)線程啟動(dòng),防止多人啟動(dòng)多次
????????lock.lock();
????????log.info("加鎖完成");
????????try?{
????????????if(this.isStart(jobKey)){
????????????????log.info("當(dāng)前任務(wù)在啟動(dòng)狀態(tài)中");
????????????????return?false;
????????????}
????????????//任務(wù)啟動(dòng)
????????????this.doStartTask(scheduledJob);
????????}?finally?{
????????????lock.unlock();
????????????log.info("解鎖完畢");
????????}
????????return?true;
????}
????/**
?????*?任務(wù)是否已經(jīng)啟動(dòng)
?????*/
????private?Boolean?isStart(String?taskKey)?{
????????//校驗(yàn)是否已經(jīng)啟動(dòng)
????????if?(scheduledFutureMap.containsKey(taskKey))?{
????????????if?(!scheduledFutureMap.get(taskKey).isCancelled())?{
????????????????return?true;
????????????}
????????}
????????return?false;
????}
????@Override
????public?Boolean?stop(String?jobKey)?{
????????log.info("停止任務(wù)?"+jobKey);
????????boolean?flag?=?scheduledFutureMap.containsKey(jobKey);
????????log.info("當(dāng)前實(shí)例是否存在?"+flag);
????????if(flag){
????????????ScheduledFuture?scheduledFuture?=?scheduledFutureMap.get(jobKey);
????????????scheduledFuture.cancel(true);
????????????scheduledFutureMap.remove(jobKey);
????????}
????????return?flag;
????}
????@Override
????public?Boolean?restart(ScheduledJob?scheduledJob)?{
????????log.info("重啟定時(shí)任務(wù)"+scheduledJob.getJobKey());
????????//停止
????????this.stop(scheduledJob.getJobKey());
????????return?this.start(scheduledJob);
????}
????/**
?????*?執(zhí)行啟動(dòng)任務(wù)
?????*/
????public?void?doStartTask(ScheduledJob?sj){
????????log.info(sj.getJobKey());
????????if(sj.getStatus().intValue()?!=?1)
????????????return;
????????Class>?clazz;
????????ScheduledOfTask?task;
????????try?{
????????????clazz?=?Class.forName(sj.getJobKey());
????????????task?=?(ScheduledOfTask)?SpringContextUtil.getBean(clazz);
????????}?catch?(ClassNotFoundException?e)?{
????????????throw?new?IllegalArgumentException("spring_scheduled_cron表數(shù)據(jù)"?+?sj.getJobKey()?+?"有誤",?e);
????????}
????????Assert.isAssignable(ScheduledOfTask.class,?task.getClass(),?"定時(shí)任務(wù)類必須實(shí)現(xiàn)ScheduledOfTask接口");
????????ScheduledFuture?scheduledFuture?=?threadPoolTaskScheduler.schedule(task,(triggerContext?->?new?CronTrigger(sj.getCronExpression()).nextExecutionTime(triggerContext)));
????????scheduledFutureMap.put(sj.getJobKey(),scheduledFuture);
????}
????@Override
????public?void?initTask()?{
????????List?list?=?scheduledJobService.list();
????????for?(ScheduledJob?sj?:?list)?{
????????????if(sj.getStatus().intValue()?==?-1)?//未啟用
????????????????continue;
????????????doStartTask(sj);
????????}
????}
}
?
2.7上面用到的獲取Bean的工具類SpringContextUtil
@Component
public?class?SpringContextUtil?implements?ApplicationContextAware?{
????private?static?ApplicationContext?applicationContext?=?null;
????@Override
????public?void?setApplicationContext(ApplicationContext?applicationContext)?throws?BeansException?{
????????if(SpringContextUtil.applicationContext?==?null){
????????????SpringContextUtil.applicationContext??=?applicationContext;
????????}
????}
????public?static?ApplicationContext?getApplicationContext()?{
????????return?applicationContext;
????}
????public?static?Object?getBean(String?name){
????????return?getApplicationContext().getBean(name);
????}
????public?static??T?getBean(Class?clazz){
????????return?getApplicationContext().getBean(clazz);
????}
????public?static??T?getBean(String?name,Class?clazz){
????????return?getApplicationContext().getBean(name,?clazz);
????}
}
?
2.8?表操作對應(yīng)的一些類
Pojo
@Data
@TableName("scheduled_job")
public?class?ScheduledJob?{
????@TableId(value?=?"job_id",type?=?IdType.AUTO)
????private?Integer?jobId;
????private?String?jobKey;
????private?String?cronExpression;
????private?String?taskExplain;
????private?Integer?status;
}
?ScheduledJobMapper
1?public?interface?ScheduledJobMapper?extends?BaseMapper?{
2?}
?
ScheduledJobService
public?interface?ScheduledJobService?extends?IService?{
????/**
?????*?修改定時(shí)任務(wù),并重新啟動(dòng)
?????*?@param?scheduledJob
?????*?@return
?????*/
????boolean?updateOne(ScheduledJob?scheduledJob);
}
@Service
@Slf4j
public?class?ScheduledJobServiceImpl?extends?ServiceImpl?implements?ScheduledJobService{
????@Autowired
????private?ScheduledTaskService?scheduledTaskService;
????@Override
????public?boolean?updateOne(ScheduledJob?scheduledJob)?{
????????if(updateById(scheduledJob))
????????????scheduledTaskService.restart(getById(scheduledJob.getJobId()));
????????return?true;
????}
}
?
2.9?修改定時(shí)任務(wù)的接口
@RestController
@RequestMapping("/job")
public?class?ScheduledJobController?{
????@Autowired
????private?ScheduledJobService?scheduledJobService;
????@PostMapping(value?=?"/update")
????public?CallBackResult?update(HttpServletRequest?request,?ScheduledJob?scheduledJob){
?????????if(scheduledJobService.updateOne(scheduledJob))
?????????????return?new?CallBackResult(true,"修改成功");
?????????return?new?CallBackResult(false,"修改失敗");
????}
}
?
3、測試結(jié)果
3.1 啟動(dòng)項(xiàng)目,看下定時(shí)任務(wù)的執(zhí)行結(jié)果,控制臺輸出結(jié)果

?
?
我們可以看到任務(wù)1是每5秒執(zhí)行一次,任務(wù)2是12秒執(zhí)行一次
?
3.2 修改任務(wù)1的cron參數(shù)或者狀態(tài)
?
?3.2.1 修改cron,執(zhí)行周期改為20秒執(zhí)行一次,狀態(tài)不變



?
再看控制臺輸出結(jié)果,任務(wù)2沒變化,任務(wù)1由5秒一次變成了20秒一次了

?
?3.2.1 修改狀態(tài)
?
?
?
?
?再看控制臺輸出結(jié)果,任務(wù)2沒變化,任務(wù)1已經(jīng)不再執(zhí)行了

?
最后
第二種方式支持通過接口的方式去改動(dòng),并且不需要重啟,當(dāng)然啦,也可以直接在數(shù)據(jù)庫中添加或修改數(shù)據(jù)后重啟項(xiàng)目,配置更加靈活一點(diǎn)。
如果是一個(gè)固定的需求,執(zhí)行周期一定不會變的了,推薦還是第一種寫法,畢竟簡單嘛。
? 作者?|??Jae1995
來源 |??cnblogs.com/jae-tech/p/15402078.html

