Spring實(shí)現(xiàn)定時(shí)任務(wù)的幾種方式總結(jié)
一.簡(jiǎn)介
Spring Task 是 Spring 框架提供的一種任務(wù)調(diào)度和異步處理的解決方案。可以按照約定的時(shí)間自動(dòng)執(zhí)行某個(gè)代碼邏輯它可以幫助開(kāi)發(fā)者在 Spring 應(yīng)用中輕松地實(shí)現(xiàn)定時(shí)任務(wù)、異步任務(wù)等功能,提高應(yīng)用的效率和可維護(hù)性。
二.實(shí)現(xiàn)
一.基于注解@Scheduled
1.通過(guò)@Scheduled注釋結(jié)合cron表達(dá)式實(shí)現(xiàn) cron表達(dá)式:
cron表達(dá)式是一種用于設(shè)置定時(shí)任務(wù)的語(yǔ)法規(guī)則。它由6個(gè)字段組成,分別表示秒、分、小 時(shí)、日期、月份和星期幾。每個(gè)字段都可以設(shè)置一個(gè)數(shù)字、一組數(shù)字(用逗號(hào)分隔)、一段數(shù)字范圍(用短橫線(xiàn)分隔)、通配符(表示任意值)或者特定的字符(如星期幾的英文縮寫(xiě))
語(yǔ)法規(guī)則:
Cron表達(dá)式的詳細(xì)用法 - 簡(jiǎn)書(shū)
示例:
0 0 0 * * ?:每天的零點(diǎn)整執(zhí)行任務(wù)。 0 0 */2 * * ?:每隔2小時(shí)執(zhí)行一次任務(wù)。 0 0 12 * * ?:每天中午12點(diǎn)執(zhí)行任務(wù)。 0 15 10 * * ?:每天上午10點(diǎn)15分執(zhí)行任務(wù)。 0 0 6,18 * * ?:每天的早上6點(diǎn)和晚上6點(diǎn)執(zhí)行任務(wù)。 0 0/30 8-18 * * ?:每天的上午8點(diǎn)到下午6點(diǎn)之間,每隔30分鐘執(zhí)行一次任務(wù)。 0 0 0 1 1 ?:每年的1月1日零點(diǎn)整執(zhí)行任務(wù)。 0 0 0 * * 2:每周的星期二零點(diǎn)整執(zhí)行任務(wù)。 0 0 0 ? * 6#3:每月的第三個(gè)星期六零點(diǎn)整執(zhí)行任務(wù)。 0 0 0 L * ?:每個(gè)月的最后一天零點(diǎn)整執(zhí)行任務(wù)。 ————————————————
結(jié)合@Scheduled:
@Scheduled(cron ="*/6 * * * * ?") public void sayHello() { System.out.println("hello"); }
輸出結(jié)果:
注:?jiǎn)?dòng)類(lèi)需要能掃描到定時(shí)任務(wù)類(lèi),否則定時(shí)任務(wù)啟動(dòng)不起來(lái)。
除了cron表達(dá)式外,還支持(感興趣可以進(jìn)一步了解)
1.fixedRate:控制方法執(zhí)行的間隔時(shí)間,是以上一次方法執(zhí)行完開(kāi)始算起,如上一次方法執(zhí)行阻塞住了,那么直到上一次執(zhí)行完,并間隔給定的時(shí)間后,執(zhí)行下一次。
2.initialDelay:initialDelay = 10000 表示在容器啟動(dòng)后,延遲10秒后再執(zhí)行一次定時(shí)器。
優(yōu)缺點(diǎn):
優(yōu):添加注解即可,使用方便。
缺:1.@Scheduled作用在方法上,方法不能有參數(shù)
2.@Scheduled注解只能在開(kāi)始就寫(xiě)好,無(wú)法動(dòng)態(tài)定義
3.spring支持的springtask的cron語(yǔ)句無(wú)法識(shí)別年份,也就是定時(shí)任務(wù)以固定頻率執(zhí)行,無(wú)法做到只執(zhí)行一次。
二.基于接口方式SchedulingConfigurer:
為了實(shí)現(xiàn)動(dòng)態(tài)定義定時(shí)任務(wù)
一.創(chuàng)建數(shù)據(jù)庫(kù)表和相應(yīng)字段存放cron語(yǔ)句
drop table if exists scheduled; create table scheduled ( cron_id varchar(30) NOT NULL primary key, cron_name varchar(30) NULL, cron varchar(30) NOT NULL ); insert into scheduled values ('1','定時(shí)器任務(wù)一','0/6 * * * * ?');
二.新增mapper類(lèi)獲取數(shù)據(jù)庫(kù)存放的cron表達(dá)式
@Select("select cron from cron_demo where cron_id=#{id}") public String getCronById(int id);
三.新建task類(lèi)執(zhí)行定時(shí)任務(wù)
public class TaskDemo implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addTriggerTask();//or: addCronTask... } private void process(){ System.out.println("cron執(zhí)行"); }//要執(zhí)行的邏輯 }
注意實(shí)現(xiàn)SchedulingConfigurer接口
用于添加定時(shí)任務(wù)的方法在這里很多很靈活,如addTriggerTask,addCronTask,并且方法重載也較多,建議查看源碼學(xué)習(xí)
這里介紹常用api:addTriggerTask,和addCronTask
addTriggerTask:
第一個(gè)方法實(shí)際是調(diào)用第二個(gè)方法
Runable task為要執(zhí)行的邏輯(想要定時(shí)實(shí)現(xiàn)的方法),Trigger trigger為使用某種方式封裝的cron語(yǔ)句,介紹一個(gè)簡(jiǎn)單易懂好用的實(shí)現(xiàn)類(lèi)---CronTrigger
expression為cron表達(dá)式,zonid為代表時(shí)區(qū)(不用管,會(huì)調(diào)用系統(tǒng)默認(rèn)時(shí)區(qū)),默認(rèn)使用第一個(gè)構(gòu)造方法即可
第二種:
CronTask是TriggerTask的子類(lèi),其成員可謂非常人性化,expression即為cron表達(dá)式,構(gòu)造方法再傳入runable執(zhí)行內(nèi)容即可
示例:
public class TaskDemo implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.addCronTask(this::process,cronMapper.getCronByid(1))) }//要執(zhí)行的邏輯 private void process(){ System.out.println("cron執(zhí)行"); } }
從上示例看出,當(dāng)用addTriggetTask時(shí),如果用Crontask,用法和addCronTask差不多,這兩個(gè)的底層都是調(diào)用了一個(gè)叫add的方法
擴(kuò)展:Runnable runnabke的寫(xiě)法
注:Runable是線(xiàn)程的知識(shí)點(diǎn),由于本人目前沒(méi)有學(xué)習(xí)java線(xiàn)程部分,無(wú)法講解其底層原理,只介紹在實(shí)現(xiàn)定時(shí)任務(wù)時(shí)的用法
Runable只是一個(gè)接口,內(nèi)部只有一個(gè)void的run方法
需要定義實(shí)現(xiàn)類(lèi),如下在納新大作業(yè)中的實(shí)現(xiàn):
private class TaskRunnable implements Runnable{ private final Cron cron; public TaskRunnable(Cron cron) { this.cron = cron; } @Override public void run() { //定義任務(wù)要做的事,即把visibility字段設(shè)為0表示可見(jiàn), // 同時(shí)把時(shí)間設(shè)為設(shè)定的發(fā)送時(shí)間 // (如果設(shè)為當(dāng)前時(shí)間由于定時(shí)任務(wù)管理器CronManageTask掃面時(shí)間間隔問(wèn)題會(huì)導(dǎo)致實(shí)際執(zhí)行時(shí)間與預(yù)期發(fā)送時(shí)間不一致) mailboxService.lambdaUpdate() .set(Email::getVisibility,(short)0) .set(Email::getSendTime,cron.getExecuteTime()) .eq(Email::getId,cron.getEmailId()) .update(); } }
這里的cron為從外傳入的參數(shù),可以通過(guò)構(gòu)造方法將cron傳入對(duì)象中,這就解決了@Secheduled無(wú)法傳遞參數(shù)的問(wèn)題,
示例:
@RequiredArgsConstructor public class TaskDemo implements SchedulingConfigurer { private final CronMapper cronMapper; @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { Runnable task =this::process; CronTask cronTask = new CronTask(task,cronMapper.getCronById(1)); taskRegistrar.addTriggerTask(cronTask); } private void process(){ System.out.println("cron執(zhí)行"); }//要執(zhí)行的邏輯 }
還有一個(gè)在查找博客時(shí)看到的示例,方法基本上一樣只不過(guò)使用了lambda表達(dá)式,但是匿名內(nèi)部類(lèi)我只了解一點(diǎn)點(diǎn),還請(qǐng)大佬help:
@Autowired protected CronMapper cronMapper; @Override public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) { scheduledTaskRegistrar.addTriggerTask(() -> process(), triggerContext -> { String cron = cronMapper.getCron(1); if (cron.isEmpty()) { System.out.println("cron is null"); } return new CronTrigger(cron).nextExecutionTime(triggerContext); }); } private void process() { System.out.println("基于接口定時(shí)任務(wù)"); }
三.基于ThreadPoolTaskScheduler輕量級(jí)多線(xiàn)程定時(shí)任務(wù)框架
上述基于接口的方法解決了基于注解無(wú)法實(shí)現(xiàn)的動(dòng)態(tài)定義cron表達(dá)式和方法傳入?yún)?shù)的問(wèn)題,但示例無(wú)法實(shí)現(xiàn)根據(jù)傳入的年份指定在某一年特定日期執(zhí)行定時(shí)任務(wù),下面介紹一種實(shí)現(xiàn)方式
一.簡(jiǎn)介:
springboot中有一個(gè)bean,ThreadPoolTaskScheduler,可以很方便的對(duì)重復(fù)執(zhí)行的任務(wù)進(jìn)行調(diào)度管理;相比于通過(guò)java自帶的周期性任務(wù)線(xiàn)程池
ScheduleThreadPoolExecutor,此bean對(duì)象支持根據(jù)cron表達(dá)式創(chuàng)建周期性任務(wù)。
當(dāng)然,ThreadPoolTaskScheduler其實(shí)底層使用也是java自帶的線(xiàn)程池。
二.常用api介紹
ThreadPoolTaskScheduler 內(nèi)部方法非常豐富,本文實(shí)現(xiàn)的是一種corn表達(dá)式,周期執(zhí)行
- schedule(Runnable task, Trigger trigger) corn表達(dá)式,周期執(zhí)行
- schedule(Runnable task, Date startTime) 定時(shí)執(zhí)行
- scheduleAtFixedRate(Runnable task, Date startTime, long period) 定時(shí)周期間隔時(shí)間執(zhí)行。間隔時(shí)間單位 TimeUnit.MILLISECONDS
scheduleAtFixedRate(Runnable task, long period) 間隔時(shí)間執(zhí)行。單位毫秒
三.上實(shí)戰(zhàn)
1.新建實(shí)現(xiàn)類(lèi)cron(隨便取的名)這里直接使用lambda注解
@Data @AllArgsConstructor @NoArgsConstructor @TableName("thread_cron") public class cron { private String title; private LocalDate startTime;//起始時(shí)間 private LocalDate deadTime;//結(jié)束時(shí)間 private LocalDateTime executeTime;//運(yùn)行時(shí)間 }
解釋?zhuān)簊tartTime為任務(wù)啟動(dòng)年份第一天,deadTime為任務(wù)啟動(dòng)年份最后一天(指定年份執(zhí)行,也可以根據(jù)需求調(diào)整),executeTime為任務(wù)執(zhí)行時(shí)間
2.創(chuàng)建對(duì)應(yīng)的service接口和實(shí)現(xiàn)類(lèi)
public interface ThreadService extends IService<Cron> { void startCron(Cron cron);//啟動(dòng)定時(shí)任務(wù) void stopCron(Cron cron);//停止定時(shí)任務(wù) void changeCron(Cron cron);//更新定時(shí)任務(wù) }
實(shí)現(xiàn)類(lèi)的具體邏輯:
1.每個(gè)任務(wù)有一個(gè)執(zhí)行期限,就是cron類(lèi)中的startTime和deadTime,這里一般存儲(chǔ)年份信息,將任務(wù)限定在某年執(zhí)行,在啟動(dòng)定時(shí)任務(wù)也就是調(diào)用startCron方法時(shí),需要判斷當(dāng)前時(shí)間是否在期限內(nèi)
2.同一任務(wù)可能被多次啟動(dòng),這顯然是多余的,因此需要將已經(jīng)啟動(dòng)過(guò)的定時(shí)任務(wù)放入一個(gè)集合中,在調(diào)用startCron時(shí)檢查當(dāng)前任務(wù)是否在集合中。執(zhí)行定時(shí)任務(wù)的方法是ThreadPoolTaskScheduler中的public ScheduledFuture schedule(Runnable task, Trigger trigger)這個(gè)方法,可以看到,方法參數(shù)在上面基于接口處講過(guò),方法返回值ScheduledFuture包含執(zhí)行的任務(wù)的詳細(xì)信息,停止任務(wù)也需要調(diào)用其中的boolean cancel(boolean mayInterruptIfRunning)方法,因此,可以用此類(lèi)型的集合來(lái)存放執(zhí)行中的定時(shí)任務(wù)
示例:
準(zhǔn)備:
private final ThreadPoolTaskScheduler threadPoolTaskScheduler; private final Map<Integer, ScheduledFuture<?>> futureMap = new HashMap<>(); @Bean public ThreadPoolTaskScheduler threadPoolTaskScheduler() { return new ThreadPoolTaskScheduler(); }
startCron:
public void startCron(Cron cron) { //1.判斷cron是否被執(zhí)行過(guò) if(futureMap.containsKey(cron.getId())){log.info("定時(shí)任務(wù)存在 id={}",cron.getId());return;} //2.判斷是否還沒(méi)過(guò)執(zhí)行時(shí)間 //在springTask中,cron表達(dá)式無(wú)法對(duì)年進(jìn)行定時(shí),故使用startTime和deadTime來(lái)限制定時(shí)任務(wù)要執(zhí)行的年份 if(LocalDate.now().isEqual(cron.getStartTime()) || LocalDate.now().isEqual(cron.getDeadTime()) || (LocalDate.now().isAfter(cron.getStartTime()) && LocalDate.now().isBefore(cron.getDeadTime()))){ //提取執(zhí)行時(shí)間 LocalDateTime executeTime = cron.getExecuteTime(); //組裝cron表達(dá)式 DateTimeFormatter cronFormatter = DateTimeFormatter.ofPattern("s m H d M"); String cronExp = cronFormatter.format(executeTime)+" ?"; //執(zhí)行scheduled任務(wù) ScheduledFuture<?> future = threadPoolTaskScheduler.schedule(new TaskRunnable(cron), new CronTrigger(cronExp)); //將future傳入futureMap集合表示任務(wù)啟動(dòng),避免任務(wù)重復(fù)啟動(dòng) futureMap.put(cron.getId(),future); //輸出日志 log.info("任務(wù)啟動(dòng),id:{},executeTime:{}",cron.getId(),cron.getExecuteTime()); } }
stopCron:
public void stopCron(Cron cron) { ScheduledFuture<?> future = futureMap.get(cron.getId()); if (future != null) { future.cancel(true); futureMap.remove(cron.getId()); log.info("任務(wù)停止,id:{}",cron.getId()); } }
changeCron:
public void changeCron(Cron cron) { startCron(cron); stopCron(cron); }
TaskRunnable類(lèi):
private class TaskRunnable implements Runnable{ private final Cron cron; public TaskRunnable(Cron cron) { this.cron = cron; } @Override public void run() { //定義任務(wù)要做的事 System.out.println("定時(shí)任務(wù)執(zhí)行,id:"+cron.getId()); } }
3.創(chuàng)建cronTaskManager類(lèi)
注:cronTaskManager類(lèi)上加注解@Compoment
上述解決了基于注解的三個(gè)問(wèn)題,但是還存在一個(gè)問(wèn)題,定時(shí)任務(wù)制定后被啟用需要保持服務(wù)器或應(yīng)用程序一直被啟動(dòng),如果關(guān)閉應(yīng)用程序,定時(shí)任務(wù)也將失效,因此需要一個(gè)類(lèi)來(lái)管理定時(shí)任務(wù),基本思路是:在應(yīng)用啟動(dòng)時(shí)每隔一段時(shí)間掃描一邊數(shù)據(jù)庫(kù)存放的定時(shí)任務(wù),將其啟動(dòng)或停止。
public class cronTaskManager { @Lazy private final ThreadService threadService; //每半個(gè)小時(shí)掃描一次 @Scheduled(cron = "0 0/30 * * * ?") public void cronManage() { log.info("定時(shí)任務(wù)啟動(dòng)"); List<Cron> list = threadService.list(); list.forEach(cron -> { if (LocalDate.now().isAfter(cron.getDeadTime())) { threadService.stopCron(cron); threadService.removeById(cron.getId()); log.info("任務(wù)過(guò)期刪除,id:{},executeTime:{}",cron.getId(),cron.getExecuteTime()); } else { log.info("嘗試啟動(dòng)任務(wù),id:{},executeTime:{}",cron.getId(),cron.getExecuteTime()); threadService.startCron(cron); } }); } }
啟動(dòng)應(yīng)用定時(shí)啟動(dòng)ronManager方法掃描數(shù)據(jù)庫(kù)存在的定時(shí)任務(wù),如果任務(wù)過(guò)期則刪除,否則嘗試啟動(dòng)。
以上就是Spring實(shí)現(xiàn)定時(shí)任務(wù)的幾種方式總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于Spring實(shí)現(xiàn)定時(shí)任務(wù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
springmvc處理模型數(shù)據(jù)ModelAndView過(guò)程詳解
這篇文章主要介紹了springmvc處理模型數(shù)據(jù)ModelAndView過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-01-01Springboot詳解整合SpringSecurity實(shí)現(xiàn)全過(guò)程
Spring Security基于Spring開(kāi)發(fā),項(xiàng)目中如果使用Springboot作為基礎(chǔ),配合Spring Security做權(quán)限更加方便,而Shiro需要和Spring進(jìn)行整合開(kāi)發(fā)。因此作為spring全家桶中的Spring Security在java領(lǐng)域很常用2022-07-07Springboot?如何使用BindingResult校驗(yàn)參數(shù)
這篇文章主要介紹了Springboot?如何使用BindingResult校驗(yàn)參數(shù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-01-01Spring Mybatis Mapper模糊查詢(xún)的幾種方法
在Spring結(jié)合Mybatis進(jìn)行開(kāi)發(fā)時(shí),實(shí)現(xiàn)模糊查詢(xún)是一個(gè)常見(jiàn)需求,在Mybatis中,LIKE查詢(xún)可以通過(guò)多種方式實(shí)現(xiàn),本文給大家介紹了Spring Mybatis Mapper模糊查詢(xún)的幾種方法,需要的朋友可以參考下2024-03-03Spring Boot Admin Server管理客戶(hù)端過(guò)程詳解
這篇文章主要介紹了Spring Boot Admin Server管理客戶(hù)端過(guò)程詳解,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-03-03SpringBoot面試突擊之過(guò)濾器和攔截器區(qū)別詳解
過(guò)濾器(Filter)和攔截器(Interceptor)都是基于?AOP(Aspect?Oriented?Programming,面向切面編程)思想實(shí)現(xiàn)的,用來(lái)解決項(xiàng)目中某一類(lèi)問(wèn)題的兩種“工具”,但二者有著明顯的差距,接下來(lái)我們一起來(lái)看2022-10-10