前言
在使用xxl-job的過程中,需要給每個執行器額外配置一個端
口(默認9999),這導致服務除了web服務端口,
還要額外多占用一個端口,多少有些不爽,有沒有可能xxl直接
復用spring-boot所占用的端口吶?
EmbedServer
要想知道是否可行,首先得清楚為什么xxl-job要獨占一個端口
實際上,每個要執行定時任務得微服務都是xxl-job的一個執行器,
執行器要與調度中心進行通訊:接受調度指令/上傳日志文件/心跳
等,因此在xxl-job-core包中,會在初始化時啟動一個EmbedServer
其內部開啟一個socket負責與調度中心通訊(主要是接受調度中心的指令),使用的網絡框架是netty
于是我們的服務往往呈現如下場景
那么問題來了,調度中心與執行器通訊使用的什么協議吶?看一下netty的handler
就可得出結論,我們最熟悉的:HTTP
思路
既然調度中心的調度指令是通過http協議傳輸過來的,從理論來講完全可以讓調度中心
的請求發送到spring-boot的端口上,接受請求后按原來的執行邏輯執行對應的代碼即可,
這樣EmbedServer
就可以刪除,netty
可以不用,最重要的是服務不會額外占用端口了
實現
spring接口
貼一下執行器接受請求處理的核心代碼(EmbedHttpServerHandler中):
switch(uri){
case"/beat": // 心跳
return executorBiz.beat();
case"/idleBeat": // 空閑心跳
IdleBeatParam idleBeatParam=GsonTool.fromJson(requestData,IdleBeatParam.class);
return executorBiz.idleBeat(idleBeatParam);
case"/run": // 執行任務
TriggerParam triggerParam=GsonTool.fromJson(requestData,TriggerParam.class);
return executorBiz.run(triggerParam);
case"/kill": // 終止任務
KillParam killParam=GsonTool.fromJson(requestData,KillParam.class);
return executorBiz.kill(killParam);
case"/log": // 獲取日志
LogParam logParam=GsonTool.fromJson(requestData,LogParam.class);
return executorBiz.log(logParam);
default:
return new ReturnT<String>(ReturnT.FAIL_CODE,"invalid request, uri-mapping("+uri+") not found.");
}
其實就是根據不同的接口uri做對應的處理,一共五個接口,spring實現這5個接口再簡單不過了,
直接用@RequestMapping
就可以了,但我采用的方式是使用spring的動態注冊接口工具RequestMappingHandlerMapping
代碼如下:
@Component
@Slf4j
public class JobServer {
/**
* 定義一個請求的前綴
*/
@Value("${job.executor.pre}")
private String pre;
private ExecutorBiz executorBiz;
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@PostConstruct
public void init() throws NoSuchMethodException {
// 初始化執行器
this.executorBiz = new ExecutorBizImpl();
// 處理器
final RequestHandler handler = new RequestHandler(this.executorBiz);
// 回調處理方法
final Method method =
RequestHandler.class.getDeclaredMethod("invoke", HttpServletRequest.class, String.class);
// 注冊路由和回調方法
this.requestMappingHandlerMapping.registerMapping(
RequestMappingInfo
.paths(this.pre + "/beat", this.pre + "/idleBeat", this.pre + "/run",
this.pre + "/kill", this.pre + "/log")
.methods(RequestMethod.POST).build(),
handler,
method);
}
@AllArgsConstructor
private class RequestHandler {
private ExecutorBiz executorBiz;
/**
* 客戶端接受中心調度請求處理
*/
@ResponseBody
public Object invoke(final HttpServletRequest request, @RequestBody final String body) throws Throwable {
String uri = request.getRequestURI();
uri = uri.replace(JobServer.this.pre, "");
final String requestData = body;
// services mapping
try {
switch (uri) {
case "/beat":
return this.executorBiz.beat();
case "/idleBeat":
final IdleBeatParam idleBeatParam = GsonTool.fromJson(requestData, IdleBeatParam.class);
return this.executorBiz.idleBeat(idleBeatParam);
case "/run":
final TriggerParam triggerParam = GsonTool.fromJson(requestData, TriggerParam.class);
return this.executorBiz.run(triggerParam);
case "/kill":
final KillParam killParam = GsonTool.fromJson(requestData, KillParam.class);
return this.executorBiz.kill(killParam);
case "/log":
final LogParam logParam = GsonTool.fromJson(requestData, LogParam.class);
return this.executorBiz.log(logParam);
default:
return new ReturnT<String>(ReturnT.FAIL_CODE,
"invalid request, uri-mapping(" + uri + ") not found.");
}
} catch (final Exception e) {
JobServer.log.error(e.getMessage(), e);
return new ReturnT<String>(ReturnT.FAIL_CODE, "request error:" + ThrowableUtil.toString(e));
}
}
}
}
此時spring就擁有了與原netty一樣功能的5個接口,我還加了一個前綴,畢竟例如"/run"的接口地址太寬泛
注冊地址
有了五個接口,下一步就是讓調度中心發出指令時走這五個接口即可,如何實現吶?
調度中心中的注冊地址是自動注冊的,就是執行器的ip+port,調度中心發送指令其實就是通過httpClient調用這個
地址再加上五個接口的uri,所以只要執行器注冊時候注冊新的地址(spring的端口),事情就完美解決了
xxl-job-core中啟動netty服務成功時才會去調度中心注冊地址:
由于現在不需要netty了,所以這段要刪掉,但要保留注冊邏輯,并注冊我們的新地址,所以不可避免的要
修改xxl-job-core的代碼
修改XxlJobExecutor
的start
方法
public void start()throws Exception{
// init logpath
JobFileAppender.initLogPath(this.logPath);
// init invoker, admin-client
this.initAdminBizList(this.adminAddresses,this.accessToken);
// init JobLogFileCleanThread
JobLogFileCleanThread.getInstance().start(this.logRetentionDays);
// init TriggerCallbackThread
TriggerCallbackThread.getInstance().start();
/** 這之下原來的代碼是initEmbedServer(address, ip, port, appname, accessToken),現在直接改為注冊 **/
// get ip
String ip=(this.ip!=null&&this.ip.trim().length()>0)?this.ip:IpUtil.getIp();
// generate address,這里的port就是spring的port,并加入前綴
String address=this.address;
if(this.address==null||this.address.trim().length()==0){
String ip_port_address=IpUtil.getIpPort(ip,this.port); // registry-address:default use address to registry , otherwise use ip:port if address is null
address="http://{ip_port}".replace("{ip_port}",ip_port_address);
}
// start registry,開始注冊
ExecutorRegistryThread.getInstance().start(this.appname,address+this.pre);
}
解除注冊
原XxlJobExecutor
的destroy
方法,負責在執行器關閉時關閉EmbedServer,由于現在EmbedServer
已刪除,所以只保留解除注冊和之后的邏輯即可
public void destroy(){
// stop registry 原stopEmbedServer()
ExecutorRegistryThread.getInstance().toStop();
// 其余保留
總結
到此就實現了xxl-job走spring的接口,不額外占用端口,好處顯而易見,但也有一點壞處:導致定時任務調度
共用了處理web請求的線程池,自行評估即可