xxl-job是一款非常優(yōu)秀的任務(wù)調(diào)度中間件,輕量級、使用簡單、支持分布式等優(yōu)點(diǎn),讓它廣泛應(yīng)用在我們的項(xiàng)目中,解決了不少定時任務(wù)的調(diào)度問題。
我們都知道,在使用過程中需要先到xxl-job的任務(wù)調(diào)度中心頁面上,配置執(zhí)行器executor 和具體的任務(wù)job ,這一過程如果項(xiàng)目中的定時任務(wù)數(shù)量不多還好說,如果任務(wù)多了的話還是挺費(fèi)工夫的。
假設(shè)項(xiàng)目中有上百個這樣的定時任務(wù),那么每個任務(wù)都需要走一遍綁定jobHander后端接口,填寫cron表達(dá)式這個流程…
我就想問問,填多了誰能不迷糊?
于是出于功能優(yōu)化(偷懶 )這一動機(jī),前幾天我萌生了一個想法,有沒有什么方法能夠告別xxl-job的管理頁面,能夠讓我不再需要到頁面上去手動注冊執(zhí)行器和任務(wù),實(shí)現(xiàn)讓它們自動注冊到調(diào)度中心呢。
分析
分析一下,其實(shí)我們要做的很簡單,只要在項(xiàng)目啟動時主動注冊executor和各個jobHandler到調(diào)度中心就可以了,流程如下:
有的小伙伴們可能要問了,我在頁面上創(chuàng)建執(zhí)行器 的時候,不是有一個選項(xiàng)叫做自動注冊 嗎,為什么我們這里還要自己添加新執(zhí)行器?
其實(shí)這里有個誤區(qū),這里的自動注冊指的是會根據(jù)項(xiàng)目中配置的xxl.job.executor.appname,將配置的機(jī)器地址自動注冊到這個執(zhí)行器的地址列表中。但是如果你之前沒有手動創(chuàng)建過執(zhí)行器,那么是不會給你自動添加一個新執(zhí)行器到調(diào)度中心的。
既然有了想法咱們就直接開干,先到github上拉一份xxl-job的源碼下來
整個項(xiàng)目導(dǎo)入idea后,先看一下結(jié)構(gòu):
結(jié)合著文檔和代碼,先梳理一下各個模塊都是干什么的:
xxl-job-admin:任務(wù)調(diào)度中心,啟動后就可以訪問管理頁面,進(jìn)行執(zhí)行器和任務(wù)的注冊、以及任務(wù)調(diào)用等功能了
xxl-job-core:公共依賴,項(xiàng)目中使用到xxl-job時要引入的依賴包
xxl-job-executor-samples:執(zhí)行示例,分別包含了springboot版本和不使用框架的版本
為了弄清楚注冊和查詢executor和jobHandler調(diào)用的是哪些接口,我們先從頁面上去抓一個請求看看:
好了,這樣就能定位到xxl-job-admin模塊中/jobgroup/save這個接口,接下來可以很容易地找到源碼位置:
按照這個思路,可以找到下面這幾個關(guān)鍵接口:
/jobgroup/pageList:執(zhí)行器列表的條件查詢
/jobgroup/save:添加執(zhí)行器
/jobinfo/pageList:任務(wù)列表的條件查詢
/jobinfo/add:添加任務(wù)
但是如果直接調(diào)用這些接口,那么就會發(fā)現(xiàn)它會跳轉(zhuǎn)到xxl-job-admin的的登錄頁面:
其實(shí)想想也明白,出于安全性考慮,調(diào)度中心的接口也不可能允許裸調(diào)的。那么再回頭看一下剛才頁面上的請求就會發(fā)現(xiàn),它在Headers中添加了一條名為XXL_JOB_LOGIN_IDENTITY的cookie:
至于這條cookie,則是在通過用戶名和密碼調(diào)用調(diào)度中心的/login接口時返回的,在返回的response可以直接拿到。只要保存下來,并在之后每次請求時攜帶,就能夠正常訪問其他接口了。
到這里,我們需要的5個接口就基本準(zhǔn)備齊了,接下來準(zhǔn)備開始正式的改造工作。
基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
改造
我們改造的目的是實(shí)現(xiàn)一個starter,以后只要引入這個starter就能實(shí)現(xiàn)executor和jobHandler的自動注冊,要引入的關(guān)鍵依賴有下面兩個:
com.xuxueli xxl-job-core 2.3.0 org.springframework.boot spring-boot-autoconfigure
1、接口調(diào)用
在調(diào)用調(diào)度中心的接口前,先把xxl-job-admin模塊中的XxlJobInfo和XxlJobGroup這兩個類拿到我們的starter項(xiàng)目中,用于接收接口調(diào)用的結(jié)果。
登錄接口
創(chuàng)建一個JobLoginService,在調(diào)用業(yè)務(wù)接口前,需要通過登錄接口獲取cookie,并在獲取到cookie后,緩存到本地的Map中。
privatefinalMaploginCookie=newHashMap<>(); publicvoidlogin(){ Stringurl=adminAddresses+"/login"; HttpResponseresponse=HttpRequest.post(url) .form("userName",username) .form("password",password) .execute(); List cookies=response.getCookies(); Optional cookieOpt=cookies.stream() .filter(cookie->cookie.getName().equals("XXL_JOB_LOGIN_IDENTITY")).findFirst(); if(!cookieOpt.isPresent()) thrownewRuntimeException("getxxl-jobcookieerror!"); Stringvalue=cookieOpt.get().getValue(); loginCookie.put("XXL_JOB_LOGIN_IDENTITY",value); }
其他接口在調(diào)用時,直接從緩存中獲取cookie,如果緩存中不存在則調(diào)用/login接口,為了避免這一過程失敗,允許最多重試3次。
publicStringgetCookie(){ for(inti=0;i3;?i++)?{ ????????String?cookieStr?=?loginCookie.get("XXL_JOB_LOGIN_IDENTITY"); ????????if?(cookieStr?!=null)?{ ????????????return?"XXL_JOB_LOGIN_IDENTITY="+cookieStr; ????????} ????????login(); ????} ????throw?new?RuntimeException("get?xxl-job?cookie?error!"); }
執(zhí)行器接口
創(chuàng)建一個JobGroupService,根據(jù)appName和執(zhí)行器名稱title查詢執(zhí)行器列表:
publicListgetJobGroup(){ Stringurl=adminAddresses+"/jobgroup/pageList"; HttpResponseresponse=HttpRequest.post(url) .form("appname",appName) .form("title",title) .cookie(jobLoginService.getCookie()) .execute(); Stringbody=response.body(); JSONArrayarray=JSONUtil.parse(body).getByPath("data",JSONArray.class); List list=array.stream() .map(o->JSONUtil.toBean((JSONObject)o,XxlJobGroup.class)) .collect(Collectors.toList()); returnlist; }
我們在后面要根據(jù)配置文件中的appName和title判斷當(dāng)前執(zhí)行器是否已經(jīng)被注冊到調(diào)度中心過,如果已經(jīng)注冊過那么則跳過,而/jobgroup/pageList接口是一個模糊查詢接口,所以在查詢列表的結(jié)果列表中,還需要再進(jìn)行一次精確匹配。
publicbooleanpreciselyCheck(){ ListjobGroup=getJobGroup(); Optional has=jobGroup.stream() .filter(xxlJobGroup->xxlJobGroup.getAppname().equals(appName) &&xxlJobGroup.getTitle().equals(title)) .findAny(); returnhas.isPresent(); }
注冊新executor到調(diào)度中心:
publicbooleanautoRegisterGroup(){ Stringurl=adminAddresses+"/jobgroup/save"; HttpResponseresponse=HttpRequest.post(url) .form("appname",appName) .form("title",title) .cookie(jobLoginService.getCookie()) .execute(); Objectcode=JSONUtil.parse(response.body()).getByPath("code"); returncode.equals(200); }
任務(wù)接口
創(chuàng)建一個JobInfoService,根據(jù)執(zhí)行器id,jobHandler名稱查詢?nèi)蝿?wù)列表,和上面一樣,也是模糊查詢:
publicListgetJobInfo(IntegerjobGroupId,StringexecutorHandler){ Stringurl=adminAddresses+"/jobinfo/pageList"; HttpResponseresponse=HttpRequest.post(url) .form("jobGroup",jobGroupId) .form("executorHandler",executorHandler) .form("triggerStatus",-1) .cookie(jobLoginService.getCookie()) .execute(); Stringbody=response.body(); JSONArrayarray=JSONUtil.parse(body).getByPath("data",JSONArray.class); List list=array.stream() .map(o->JSONUtil.toBean((JSONObject)o,XxlJobInfo.class)) .collect(Collectors.toList()); returnlist; }
注冊一個新任務(wù),最終返回創(chuàng)建的新任務(wù)的id:
publicIntegeraddJobInfo(XxlJobInfoxxlJobInfo){ Stringurl=adminAddresses+"/jobinfo/add"; MapparamMap=BeanUtil.beanToMap(xxlJobInfo); HttpResponseresponse=HttpRequest.post(url) .form(paramMap) .cookie(jobLoginService.getCookie()) .execute(); JSONjson=JSONUtil.parse(response.body()); Objectcode=json.getByPath("code"); if(code.equals(200)){ returnConvert.toInt(json.getByPath("content")); } thrownewRuntimeException("addjobInfoerror!"); }
2、創(chuàng)建新注解
在創(chuàng)建任務(wù)時,必填字段除了執(zhí)行器和jobHandler之外,還有任務(wù)描述 、負(fù)責(zé)人 、Cron表達(dá)式 、調(diào)度類型 、運(yùn)行模式 。在這里,我們默認(rèn)調(diào)度類型為CRON、運(yùn)行模式為BEAN,另外的3個字段的信息需要用戶指定。
因此我們需要創(chuàng)建一個新注解@XxlRegister,來配合原生的@XxlJob注解進(jìn)行使用,填寫這幾個字段的信息:
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public@interfaceXxlRegister{ Stringcron(); StringjobDesc()default"defaultjobDesc"; Stringauthor()default"defaultAuthor"; inttriggerStatus()default0; }
最后,額外添加了一個triggerStatus屬性,表示任務(wù)的默認(rèn)調(diào)度狀態(tài),0為停止?fàn)顟B(tài),1為運(yùn)行狀態(tài)。
3、自動注冊核心
基本準(zhǔn)備工作做完后,下面實(shí)現(xiàn)自動注冊執(zhí)行器和jobHandler的核心代碼。核心類實(shí)現(xiàn)ApplicationListener接口,在接收到ApplicationReadyEvent事件后開始執(zhí)行自動注冊邏輯。
@Component publicclassXxlJobAutoRegisterimplementsApplicationListener, ApplicationContextAware{ privatestaticfinalLoglog=LogFactory.get(); privateApplicationContextapplicationContext; @Autowired privateJobGroupServicejobGroupService; @Autowired privateJobInfoServicejobInfoService; @Override publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{ this.applicationContext=applicationContext; } @Override publicvoidonApplicationEvent(ApplicationReadyEventevent){ addJobGroup();//注冊執(zhí)行器 addJobInfo();//注冊任務(wù) } }
自動注冊執(zhí)行器的代碼非常簡單,根據(jù)配置文件中的appName和title精確匹配查看調(diào)度中心是否已有執(zhí)行器被注冊過了,如果存在則跳過,不存在則新注冊一個:
privatevoidaddJobGroup(){ if(jobGroupService.preciselyCheck()) return; if(jobGroupService.autoRegisterGroup()) log.info("autoregisterxxl-jobgroupsuccess!"); }
自動注冊任務(wù)的邏輯則相對復(fù)雜一些,需要完成:
通過applicationContext拿到spring容器中的所有bean,再拿到這些bean中所有添加了@XxlJob注解的方法
對上面獲取到的方法進(jìn)行檢查,是否添加了我們自定義的@XxlRegister注解,如果沒有則跳過,不進(jìn)行自動注冊
對同時添加了@XxlJob和@XxlRegister的方法,通過執(zhí)行器id和jobHandler的值判斷是否已經(jīng)在調(diào)度中心注冊過了,如果已存在則跳過
對于滿足注解條件且沒有注冊過的jobHandler,調(diào)用接口注冊到調(diào)度中心
具體代碼如下:
privatevoidaddJobInfo(){ ListjobGroups=jobGroupService.getJobGroup(); XxlJobGroupxxlJobGroup=jobGroups.get(0); String[]beanDefinitionNames=applicationContext.getBeanNamesForType(Object.class,false,true); for(StringbeanDefinitionName:beanDefinitionNames){ Objectbean=applicationContext.getBean(beanDefinitionName); Map annotatedMethods=MethodIntrospector.selectMethods(bean.getClass(), newMethodIntrospector.MetadataLookup (){ @Override publicXxlJobinspect(Methodmethod){ returnAnnotatedElementUtils.findMergedAnnotation(method,XxlJob.class); } }); for(Map.Entry methodXxlJobEntry:annotatedMethods.entrySet()){ MethodexecuteMethod=methodXxlJobEntry.getKey(); XxlJobxxlJob=methodXxlJobEntry.getValue(); //自動注冊 if(executeMethod.isAnnotationPresent(XxlRegister.class)){ XxlRegisterxxlRegister=executeMethod.getAnnotation(XxlRegister.class); List jobInfo=jobInfoService.getJobInfo(xxlJobGroup.getId(),xxlJob.value()); if(!jobInfo.isEmpty()){ //因?yàn)槭悄:樵儯枰倥袛嘁淮?Optional first=jobInfo.stream() .filter(xxlJobInfo->xxlJobInfo.getExecutorHandler().equals(xxlJob.value())) .findFirst(); if(first.isPresent()) continue; } XxlJobInfoxxlJobInfo=createXxlJobInfo(xxlJobGroup,xxlJob,xxlRegister); IntegerjobInfoId=jobInfoService.addJobInfo(xxlJobInfo); } } } }
4、自動裝配
創(chuàng)建一個配置類,用于掃描bean:
@Configuration @ComponentScan(basePackages="com.xxl.job.plus.executor") publicclassXxlJobPlusConfig{ }
將它添加到META-INF/spring.factories文件:
org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.xxl.job.plus.executor.config.XxlJobPlusConfig
到這里starter的編寫就完成了,可以通過maven發(fā)布jar包到本地或者私服:
mvncleaninstall/deploy
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺管理系統(tǒng) + 用戶小程序,支持 RBAC 動態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能
項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud
視頻教程:https://doc.iocoder.cn/video/
測試
新建一個springboot項(xiàng)目,引入我們在上面打好的包:
com.cn.hydra xxljob-autoregister-spring-boot-starter 0.0.1
在application.properties中配置xxl-job的信息,首先是原生的配置內(nèi)容:
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin xxl.job.accessToken=default_token xxl.job.executor.appname=xxl-job-executor-test xxl.job.executor.address= xxl.job.executor.ip=127.0.0.1 xxl.job.executor.port=9999 xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler xxl.job.executor.logretentiondays=30
此外還要額外添加我們自己的starter要求的新配置內(nèi)容:
#admin用戶名 xxl.job.admin.username=admin #admin密碼 xxl.job.admin.password=123456 #執(zhí)行器名稱 xxl.job.executor.title=test-title
完成后在代碼中配置一下XxlJobSpringExecutor,然后在測試接口上添加原生@XxlJob注解和我們自定義的@XxlRegister注解:
@XxlJob(value="testJob") @XxlRegister(cron="000**?*", author="hydra", jobDesc="測試job") publicvoidtestJob(){ System.out.println("#碼農(nóng)參上"); } @XxlJob(value="testJob222") @XxlRegister(cron="591-20**?", triggerStatus=1) publicvoidtestJob2(){ System.out.println("#作者:Hydra"); } @XxlJob(value="testJob444") @XxlRegister(cron="595923**?") publicvoidtestJob4(){ System.out.println("helloxxljob"); }
啟動項(xiàng)目,可以看到執(zhí)行器自動注冊成功:
再打開調(diào)度中心的任務(wù)管理頁面,可以看到同時添加了兩個注解的任務(wù)也已經(jīng)自動完成了注冊:
從頁面上手動執(zhí)行任務(wù)進(jìn)行測試,可以執(zhí)行成功:
到這里,starter的編寫和測試過程就算基本完成了,項(xiàng)目中引入后,以后也能省出更多的時間來摸魚學(xué)習(xí)了~
審核編輯:劉清
-
Micron
+關(guān)注
關(guān)注
0文章
30瀏覽量
56261 -
執(zhí)行器
+關(guān)注
關(guān)注
5文章
378瀏覽量
19434 -
RBAC
+關(guān)注
關(guān)注
0文章
44瀏覽量
9996
原文標(biāo)題:魔改xxl-job,徹底告別手動配置任務(wù)!
文章出處:【微信號:芋道源碼,微信公眾號:芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
簡單的任務(wù)調(diào)度代碼
Linux系統(tǒng)定時任務(wù)Crond
運(yùn)行調(diào)度中心后訪問出現(xiàn)500錯誤怎么解決
UCOSIII的任務(wù)管理與任務(wù)調(diào)度和切換簡述
OPC 實(shí)時任務(wù)系統(tǒng)動態(tài)調(diào)度算法的研究與設(shè)計(jì)The Stud
實(shí)時任務(wù)雙容錯調(diào)度算法
移動終端最優(yōu)節(jié)能任務(wù)調(diào)度
Python定時任務(wù)的實(shí)現(xiàn)方式
什么是定時任務(wù) xxl-job架構(gòu)設(shè)計(jì)方案
xxl-job驚艷的設(shè)計(jì),怎能叫人不愛
分布式定時調(diào)度:xxl-job最佳實(shí)踐方法
![分布式<b class='flag-5'>定時調(diào)度</b>:<b class='flag-5'>xxl-job</b>最佳實(shí)踐方法](https://file1.elecfans.com/web2/M00/B3/AA/wKgZomVn_MKAQa4rAAAsKeshSm4521.png)
基于Flexus X加速M(fèi)ySQL鏡像搭建XXL-JOB任務(wù)調(diào)度平臺
![基于Flexus X加速M(fèi)ySQL鏡像搭建<b class='flag-5'>XXL-JOB</b><b class='flag-5'>任務(wù)</b><b class='flag-5'>調(diào)度</b>平臺](https://file1.elecfans.com//web3/M00/03/CA/wKgZPGdsEluAAvNUAAFBJ9ndVVs789.png)
評論