原創:微信公眾號
碼農參上
,歡迎分享,轉載請保留出處。
哈嘍大家好啊,我是Hydra。
xxl-job是一款非常優秀的任務排程中介軟體,輕量級、使用簡單、支援分散式等優點,讓它廣泛應用在我們的專案中,解決了不少定時任務的排程問題。
我們都知道,在使用過程中需要先到xxl-job的任務排程中心頁面上,設定執行器executor和具體的任務job,這一過程如果專案中的定時任務數量不多還好說,如果任務多了的話還是挺費工夫的。
假設專案中有上百個這樣的定時任務,那麼每個任務都需要走一遍繫結jobHander
後端介面,填寫cron
表示式這個流程…
我就想問問,填多了誰能不迷糊?
於是出於功能優化(偷懶)這一動機,前幾天我萌生了一個想法,有沒有什麼方法能夠告別xxl-job的管理頁面,能夠讓我不再需要到頁面上去手動註冊執行器和任務,實現讓它們自動註冊到排程中心呢。
分析一下,其實我們要做的很簡單,只要在專案啟動時主動註冊executor
和各個jobHandler
到排程中心就可以了,流程如下:
有的小夥伴們可能要問了,我在頁面上建立執行器的時候,不是有一個選項叫做自動註冊嗎,為什麼我們這裡還要自己新增新執行器?
其實這裡有個誤區,這裡的自動註冊指的是會根據專案中設定的xxl.job.executor.appname
,將設定的機器地址自動註冊到這個執行器的地址列表中。但是如果你之前沒有手動建立過執行器,那麼是不會給你自動新增一個新執行器到排程中心的。
既然有了想法咱們就直接開幹,先到github上拉一份xxl-job的原始碼下來:
https://github.com/xuxueli/xxl-job/https://github.com/xuxueli/xxl-job/
整個專案匯入idea後,先看一下結構:
結合著檔案和程式碼,先梳理一下各個模組都是幹什麼的:
xxl-job-admin
:任務排程中心,啟動後就可以存取管理頁面,進行執行器和任務的註冊、以及任務呼叫等功能了xxl-job-core
:公共依賴,專案中使用到xxl-job時要引入的依賴包xxl-job-executor-samples
:執行範例,分別包含了springboot版本和不使用框架的版本為了弄清楚註冊和查詢executor
和jobHandler
呼叫的是哪些介面,我們先從頁面上去抓一個請求看看:
好了,這樣就能定位到xxl-job-admin
模組中/jobgroup/save
這個介面,接下來可以很容易地找到原始碼位置:
按照這個思路,可以找到下面這幾個關鍵介面:
/jobgroup/pageList
:執行器列表的條件查詢/jobgroup/save
:新增執行器/jobinfo/pageList
:任務列表的條件查詢/jobinfo/add
:新增任務但是如果直接呼叫這些介面,那麼就會發現它會跳轉到xxl-job-admin
的的登入頁面:
其實想想也明白,出於安全性考慮,排程中心的介面也不可能允許裸調的。那麼再回頭看一下剛才頁面上的請求就會發現,它在Headers
中新增了一條名為XXL_JOB_LOGIN_IDENTITY
的cookie
:
至於這條cookie
,則是在通過使用者名稱和密碼呼叫排程中心的/login
介面時返回的,在返回的response
可以直接拿到。只要儲存下來,並在之後每次請求時攜帶,就能夠正常存取其他介面了。
到這裡,我們需要的5個介面就基本準備齊了,接下來準備開始正式的改造工作。
我們改造的目的是實現一個starter
,以後只要引入這個starter
就能實現executor
和jobHandler
的自動註冊,要引入的關鍵依賴有下面兩個:
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
在呼叫排程中心的介面前,先把xxl-job-admin
模組中的XxlJobInfo
和XxlJobGroup
這兩個類拿到我們的starter專案中,用於接收介面呼叫的結果。
建立一個JobLoginService
,在呼叫業務介面前,需要通過登入介面獲取cookie
,並在獲取到cookie
後,快取到原生的Map
中。
private final Map<String,String> loginCookie=new HashMap<>();
public void login() {
String url=adminAddresses+"/login";
HttpResponse response = HttpRequest.post(url)
.form("userName",username)
.form("password",password)
.execute();
List<HttpCookie> cookies = response.getCookies();
Optional<HttpCookie> cookieOpt = cookies.stream()
.filter(cookie -> cookie.getName().equals("XXL_JOB_LOGIN_IDENTITY")).findFirst();
if (!cookieOpt.isPresent())
throw new RuntimeException("get xxl-job cookie error!");
String value = cookieOpt.get().getValue();
loginCookie.put("XXL_JOB_LOGIN_IDENTITY",value);
}
其他介面在呼叫時,直接從快取中獲取cookie
,如果快取中不存在則呼叫/login
介面,為了避免這一過程失敗,允許最多重試3次。
public String getCookie() {
for (int i = 0; i < 3; 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!");
}
建立一個JobGroupService
,根據appName
和執行器名稱title
查詢執行器列表:
public List<XxlJobGroup> getJobGroup() {
String url=adminAddresses+"/jobgroup/pageList";
HttpResponse response = HttpRequest.post(url)
.form("appname", appName)
.form("title", title)
.cookie(jobLoginService.getCookie())
.execute();
String body = response.body();
JSONArray array = JSONUtil.parse(body).getByPath("data", JSONArray.class);
List<XxlJobGroup> list = array.stream()
.map(o -> JSONUtil.toBean((JSONObject) o, XxlJobGroup.class))
.collect(Collectors.toList());
return list;
}
我們在後面要根據組態檔中的appName
和title
判斷當前執行器是否已經被註冊到排程中心過,如果已經註冊過那麼則跳過,而/jobgroup/pageList
介面是一個模糊查詢介面,所以在查詢列表的結果列表中,還需要再進行一次精確匹配。
public boolean preciselyCheck() {
List<XxlJobGroup> jobGroup = getJobGroup();
Optional<XxlJobGroup> has = jobGroup.stream()
.filter(xxlJobGroup -> xxlJobGroup.getAppname().equals(appName)
&& xxlJobGroup.getTitle().equals(title))
.findAny();
return has.isPresent();
}
註冊新executor
到排程中心:
public boolean autoRegisterGroup() {
String url=adminAddresses+"/jobgroup/save";
HttpResponse response = HttpRequest.post(url)
.form("appname", appName)
.form("title", title)
.cookie(jobLoginService.getCookie())
.execute();
Object code = JSONUtil.parse(response.body()).getByPath("code");
return code.equals(200);
}
建立一個JobInfoService
,根據執行器id
,jobHandler
名稱查詢任務列表,和上面一樣,也是模糊查詢:
public List<XxlJobInfo> getJobInfo(Integer jobGroupId,String executorHandler) {
String url=adminAddresses+"/jobinfo/pageList";
HttpResponse response = HttpRequest.post(url)
.form("jobGroup", jobGroupId)
.form("executorHandler", executorHandler)
.form("triggerStatus", -1)
.cookie(jobLoginService.getCookie())
.execute();
String body = response.body();
JSONArray array = JSONUtil.parse(body).getByPath("data", JSONArray.class);
List<XxlJobInfo> list = array.stream()
.map(o -> JSONUtil.toBean((JSONObject) o, XxlJobInfo.class))
.collect(Collectors.toList());
return list;
}
註冊一個新任務,最終返回建立的新任務的id
:
public Integer addJobInfo(XxlJobInfo xxlJobInfo) {
String url=adminAddresses+"/jobinfo/add";
Map<String, Object> paramMap = BeanUtil.beanToMap(xxlJobInfo);
HttpResponse response = HttpRequest.post(url)
.form(paramMap)
.cookie(jobLoginService.getCookie())
.execute();
JSON json = JSONUtil.parse(response.body());
Object code = json.getByPath("code");
if (code.equals(200)){
return Convert.toInt(json.getByPath("content"));
}
throw new RuntimeException("add jobInfo error!");
}
在建立任務時,必填欄位除了執行器和jobHandler
之外,還有任務描述、負責人、Cron表示式、排程型別、執行模式。在這裡,我們預設排程型別為CRON
、執行模式為BEAN
,另外的3個欄位的資訊需要使用者指定。
因此我們需要建立一個新註解@XxlRegister
,來配合原生的@XxlJob
註解進行使用,填寫這幾個欄位的資訊:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface XxlRegister {
String cron();
String jobDesc() default "default jobDesc";
String author() default "default Author";
int triggerStatus() default 0;
}
最後,額外新增了一個triggerStatus
屬性,表示任務的預設排程狀態,0為停止狀態,1為執行狀態。
基本準備工作做完後,下面實現自動註冊執行器和jobHandler
的核心程式碼。核心類實現ApplicationListener
介面,在接收到ApplicationReadyEvent
事件後開始執行自動註冊邏輯。
@Component
public class XxlJobAutoRegister implements ApplicationListener<ApplicationReadyEvent>,
ApplicationContextAware {
private static final Log log =LogFactory.get();
private ApplicationContext applicationContext;
@Autowired
private JobGroupService jobGroupService;
@Autowired
private JobInfoService jobInfoService;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
addJobGroup();//註冊執行器
addJobInfo();//註冊任務
}
}
自動註冊執行器的程式碼非常簡單,根據組態檔中的appName
和title
精確匹配檢視排程中心是否已有執行器被註冊過了,如果存在則跳過,不存在則新註冊一個:
private void addJobGroup() {
if (jobGroupService.preciselyCheck())
return;
if(jobGroupService.autoRegisterGroup())
log.info("auto register xxl-job group success!");
}
自動註冊任務的邏輯則相對複雜一些,需要完成:
applicationContext
拿到spring容器中的所有bean,再拿到這些bean中所有新增了@XxlJob
註解的方法@XxlRegister
註解,如果沒有則跳過,不進行自動註冊@XxlJob
和@XxlRegister
的方法,通過執行器id和jobHandler
的值判斷是否已經在排程中心註冊過了,如果已存在則跳過jobHandler
,呼叫介面註冊到排程中心具體程式碼如下:
private void addJobInfo() {
List<XxlJobGroup> jobGroups = jobGroupService.getJobGroup();
XxlJobGroup xxlJobGroup = jobGroups.get(0);
String[] beanDefinitionNames = applicationContext.getBeanNamesForType(Object.class, false, true);
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = applicationContext.getBean(beanDefinitionName);
Map<Method, XxlJob> annotatedMethods = MethodIntrospector.selectMethods(bean.getClass(),
new MethodIntrospector.MetadataLookup<XxlJob>() {
@Override
public XxlJob inspect(Method method) {
return AnnotatedElementUtils.findMergedAnnotation(method, XxlJob.class);
}
});
for (Map.Entry<Method, XxlJob> methodXxlJobEntry : annotatedMethods.entrySet()) {
Method executeMethod = methodXxlJobEntry.getKey();
XxlJob xxlJob = methodXxlJobEntry.getValue();
//自動註冊
if (executeMethod.isAnnotationPresent(XxlRegister.class)) {
XxlRegister xxlRegister = executeMethod.getAnnotation(XxlRegister.class);
List<XxlJobInfo> jobInfo = jobInfoService.getJobInfo(xxlJobGroup.getId(), xxlJob.value());
if (!jobInfo.isEmpty()){
//因為是模糊查詢,需要再判斷一次
Optional<XxlJobInfo> first = jobInfo.stream()
.filter(xxlJobInfo -> xxlJobInfo.getExecutorHandler().equals(xxlJob.value()))
.findFirst();
if (first.isPresent())
continue;
}
XxlJobInfo xxlJobInfo = createXxlJobInfo(xxlJobGroup, xxlJob, xxlRegister);
Integer jobInfoId = jobInfoService.addJobInfo(xxlJobInfo);
}
}
}
}
建立一個設定類,用於掃描bean
:
@Configuration
@ComponentScan(basePackages = "com.xxl.job.plus.executor")
public class XxlJobPlusConfig {
}
將它新增到META-INF/spring.factories
檔案:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxl.job.plus.executor.config.XxlJobPlusConfig
到這裡starter
的編寫就完成了,可以通過maven釋出jar包到本地或者私服:
mvn clean install/deploy
新建一個springboot專案,引入我們在上面打好的包:
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>xxljob-autoregister-spring-boot-starter</artifactId>
<version>0.0.1</version>
</dependency>
在application.properties
中設定xxl-job的資訊,首先是原生的設定內容:
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要求的新設定內容:
# admin使用者名稱
xxl.job.admin.username=admin
# admin 密碼
xxl.job.admin.password=123456
# 執行器名稱
xxl.job.executor.title=test-title
完成後在程式碼中設定一下XxlJobSpringExecutor
,然後在測試介面上新增原生@XxlJob
註解和我們自定義的@XxlRegister
註解:
@XxlJob(value = "testJob")
@XxlRegister(cron = "0 0 0 * * ? *",
author = "hydra",
jobDesc = "測試job")
public void testJob(){
System.out.println("#公眾號:碼農參上");
}
@XxlJob(value = "testJob222")
@XxlRegister(cron = "59 1-2 0 * * ?",
triggerStatus = 1)
public void testJob2(){
System.out.println("#作者:Hydra");
}
@XxlJob(value = "testJob444")
@XxlRegister(cron = "59 59 23 * * ?")
public void testJob4(){
System.out.println("hello xxl job");
}
啟動專案,可以看到執行器自動註冊成功:
再開啟排程中心的任務管理頁面,可以看到同時新增了兩個註解的任務也已經自動完成了註冊:
從頁面上手動執行任務進行測試,可以執行成功:
到這裡,starter的編寫和測試過程就算基本完成了,專案中引入後,以後也能省出更多的時間來摸魚學習了~
專案的完整程式碼已經傳到了我的github上,小夥伴們如果有需要的可以自行下載。公眾號【碼農參上】後臺回覆【xxl】獲取專案git地址,也歡迎來給我點個star支援一下~
那麼,這次的分享就到這裡,我是Hydra,我們下篇再見。
作者簡介,
碼農參上
,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術。歡迎新增好友,進一步交流。