前言
在使用xxl-job的過程中,需要給每個(gè)執(zhí)行器額外配置一個(gè)端
口(默認(rèn)9999),這導(dǎo)致服務(wù)除了web服務(wù)端口,
還要額外多占用一個(gè)端口,多少有些不爽,有沒有可能xxl直接
復(fù)用spring-boot所占用的端口吶?
EmbedServer
要想知道是否可行,首先得清楚為什么xxl-job要獨(dú)占一個(gè)端口
實(shí)際上,每個(gè)要執(zhí)行定時(shí)任務(wù)得微服務(wù)都是xxl-job的一個(gè)執(zhí)行器,
執(zhí)行器要與調(diào)度中心進(jìn)行通訊:接受調(diào)度指令/上傳日志文件/心跳
等,因此在xxl-job-core包中,會在初始化時(shí)啟動一個(gè)EmbedServer
其內(nèi)部開啟一個(gè)socket負(fù)責(zé)與調(diào)度中心通訊(主要是接受調(diào)度中心的指令),使用的網(wǎng)絡(luò)框架是netty
于是我們的服務(wù)往往呈現(xiàn)如下場景
那么問題來了,調(diào)度中心與執(zhí)行器通訊使用的什么協(xié)議吶?看一下netty的handler
就可得出結(jié)論,我們最熟悉的:HTTP
思路
既然調(diào)度中心的調(diào)度指令是通過http協(xié)議傳輸過來的,從理論來講完全可以讓調(diào)度中心
的請求發(fā)送到spring-boot的端口上,接受請求后按原來的執(zhí)行邏輯執(zhí)行對應(yīng)的代碼即可,
這樣EmbedServer
就可以刪除,netty
可以不用,最重要的是服務(wù)不會額外占用端口了
實(shí)現(xiàn)
spring接口
貼一下執(zhí)行器接受請求處理的核心代碼(EmbedHttpServerHandler中):
switch(uri){
case"/beat": // 心跳
return executorBiz.beat();
case"/idleBeat": // 空閑心跳
IdleBeatParam idleBeatParam=GsonTool.fromJson(requestData,IdleBeatParam.class);
return executorBiz.idleBeat(idleBeatParam);
case"/run": // 執(zhí)行任務(wù)
TriggerParam triggerParam=GsonTool.fromJson(requestData,TriggerParam.class);
return executorBiz.run(triggerParam);
case"/kill": // 終止任務(wù)
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.");
}
其實(shí)就是根據(jù)不同的接口uri做對應(yīng)的處理,一共五個(gè)接口,spring實(shí)現(xiàn)這5個(gè)接口再簡單不過了,
直接用@RequestMapping
就可以了,但我采用的方式是使用spring的動態(tài)注冊接口工具RequestMappingHandlerMapping
代碼如下:
@Component
@Slf4j
public class JobServer {
/**
* 定義一個(gè)請求的前綴
*/
@Value("${job.executor.pre}")
private String pre;
private ExecutorBiz executorBiz;
@Autowired
private RequestMappingHandlerMapping requestMappingHandlerMapping;
@PostConstruct
public void init() throws NoSuchMethodException {
// 初始化執(zhí)行器
this.executorBiz = new ExecutorBizImpl();
// 處理器
final RequestHandler handler = new RequestHandler(this.executorBiz);
// 回調(diào)處理方法
final Method method =
RequestHandler.class.getDeclaredMethod("invoke", HttpServletRequest.class, String.class);
// 注冊路由和回調(diào)方法
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;
/**
* 客戶端接受中心調(diào)度請求處理
*/
@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));
}
}
}
}
此時(shí)spring就擁有了與原netty一樣功能的5個(gè)接口,我還加了一個(gè)前綴,畢竟例如"/run"的接口地址太寬泛
注冊地址
有了五個(gè)接口,下一步就是讓調(diào)度中心發(fā)出指令時(shí)走這五個(gè)接口即可,如何實(shí)現(xiàn)吶?
調(diào)度中心中的注冊地址是自動注冊的,就是執(zhí)行器的ip+port,調(diào)度中心發(fā)送指令其實(shí)就是通過httpClient調(diào)用這個(gè)
地址再加上五個(gè)接口的uri,所以只要執(zhí)行器注冊時(shí)候注冊新的地址(spring的端口),事情就完美解決了
xxl-job-core中啟動netty服務(wù)成功時(shí)才會去調(diào)度中心注冊地址:
由于現(xiàn)在不需要netty了,所以這段要?jiǎng)h掉,但要保留注冊邏輯,并注冊我們的新地址,所以不可避免的要
修改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),現(xiàn)在直接改為注冊 **/
// 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
方法,負(fù)責(zé)在執(zhí)行器關(guān)閉時(shí)關(guān)閉EmbedServer,由于現(xiàn)在EmbedServer
已刪除,所以只保留解除注冊和之后的邏輯即可
public void destroy(){
// stop registry 原stopEmbedServer()
ExecutorRegistryThread.getInstance().toStop();
// 其余保留
總結(jié)
到此就實(shí)現(xiàn)了xxl-job走spring的接口,不額外占用端口,好處顯而易見,但也有一點(diǎn)壞處:導(dǎo)致定時(shí)任務(wù)調(diào)度
共用了處理web請求的線程池,自行評估即可