在6月份整理过一份xxl-job初级部署开发,当时的项目只是将xxl-job-admin进行docker部署,在其后台管理进行执行器和任务添加,并在Springboot工程中实现了定时任务对应的回调函数的开发,但这样定时任务的增删改查无法在项目的后台管理界面上统一展现,不太方便。所以在项目上线后,为了方便统一管理维护,本周花了两天时间对xxl-job-admin的任务管理功能进行了集成。
一、开发思路概述
1、集成方案选择
经过小半天的百度之后,发现两种实现方案。
方案一是直接将xxl-job整个开源代码拉到本地,变成项目的一个模块,然后直接使用xxl-job相关的API,或再进行一层封装,这样可以直接在API上使用注解@PermissionLimit,让内部调用这些API不需要进行认证。
方案二是重写xxl-job执行器和任务管理,包括管理页面及相关API,通过这些API嵌套调用xxl-job-admin的接口。
这两种方案简单比较如下:
方案名称 | 优点 | 缺点 |
xxl-job源码引入并修改 | 后端代码复用度高,工作量小,质量优 | 侵入性高,功能冗余多,安全性较差,捆绑性强 |
重写执行器/任务管理,对接xxl-job | 灵活性高,应对开源版本迭代或更换定时器方案方便,安全性较高 | 工作量较大,API版本迭代后需重新适配 |
从项目总体把控的角度来看,最终选择了方案二,即在工程中增加定时任务管理模块,简化一些xxl-job特有的信息字段,通过xxl-job的接口API功能调用完成定时任务管理功能。
2、技术方案概述
在确定最终技术路线时,将执行器Executor相关的功能略去,即这部分的增删改查仍由xxl-job的管理界面完成,纳入项目部署镜像的初始化脚本中,这样在运维部署完成xxl-job的docker引擎后,内部就已经加入了执行器相关信息,在项目后台管理界面中,执行器是不可见的,只能进行定时任务的增删改查、启用/停用、运行等操作。
这样的技术方案是综合考虑开发与运维的分工协作角度的结果,具体的实现与分工见下表:
工作内容 | 开发 | 运维 |
xxl-job部署(docker或独立服务器) | 核对容器配置信息 | 负责镜像下载、容器配置和打包、最终镜像部署 |
执行器增删改查 | 提供执行器所需配置,纳入项目开发配置文件 | 形成数据库初始化脚本随容器同时部署 |
定时任务增删改查、启停、运行 | 应用中通过代码实现,在后台管理页面中进行配置 | 无需处理 |
二、详细开发步骤
0、环境信息
Springboot:2.7.0
xxl-job docker image:xuxueli/xxl-job-admin:2.3.1
MySQL:8.0.31
hutool:5.7.22
1、xxl-job登录并保存cookie
登录接口API访问url为<ip:port>/xxl-job-admin/login,post请求,参数有userName和password两个,传参方式为x-www-form-urlencoded,参考代码见下:
final static private String CONSTANTS_COOKIE_LOGINID = "XXL_JOB_LOGIN_IDENTITY";
@Value("${xxl-job.admin.addresses}")
private String adminAddress;
private void loginXxlJob() {
String urlLogin = adminAddress + "/login";
try {
// login and get the cookie of response
HttpResponse httpResponse = HttpRequest.post(urlLogin).
form("userName", userName).
form("password", password).
execute();
HttpCookie httpCookie = httpResponse.getCookie(CONSTANTS_COOKIE_LOGINID);
if (Objects.nonNull(httpCookie)) {
tokenLoginId = httpCookie.getValue();
}
} catch(Exception e) {
logger.error("xxl-job-admin登录错误", e);
}
}
2、接口及参数验证
使用了APIFox接口工具验证所有接口及参数,以便后续开发时参考。
(1)定时任务获取列表,/xxl-job-admin/jobinfo/pageList
(2)定时任务新增,/xxl-job-admin/jobinfo/add
(3)定时任务更新,/xxl-job-admin/jobinfo/update
(4)定时任务启用,/xxl-job-admin/jobinfo/start
(5)定时任务停用,/xxl-job-admin/jobinfo/stop
(6)定时任务删除,/xxl-job-admin/jobinfo/remove
(7)定时任务运行,/xxl-job-admin/jobinfo/trigger
3、数据模型设计
考虑定时任务模型设计的灵活性,没有完全按照xxl-job的数据表模型,而是大幅简化后重新设计表t_schedule_job,按照设计规范实现controller、model、service、mapper等。
@Data
@TableName("t_schedule_job")
public class ScheduleJob implements Serializable {
/**
* 任务Id
*/
@TableId
private Long jobId;
/**
* bean名称
*/
private String beanName;
/**
* 方法名称
*/
private String methodName;
/**
* 方法参数
*/
private String params;
/**
* cron表达式
*/
private String cronExpression;
/**
* 任务状态
*/
private short status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
在ScheduleJob的ServiceImp中,去实现与xxl-job-admin的各类接口,具体实现逻辑是:
(1)列表,在调用ScheduleJob的列表之前,先登录xxl-job-admin并取得cookie,获取定时任务列表,对本地任务表进行更新,然后再返回本地任务表的列表信息;
(2)单任务详细信息,无需调用xxl-job-admin接口,因任务信息已经在上一个接口中包含,所以直接返回本地任务表的单任务详细信息;
(3)新增任务,先调用xxl-job-admin的任务新增接口,成功返回后,再进行本地任务表新增,如果失败,也不执行本地任务表新增,返回错误信息给前端;
(4)修改任务,先调用xxl-job-admin的任务更新接口,成功返回后,再进行本地任务表修改,如果失败,也不执行本地任务表修改,返回错误信息给前端;
(5)启用任务,先调用xxl-job-admin的任务启用接口,成功返回后,再进行本地任务表启用,如果失败,也不执行本地任务表启用,返回错误信息给前端;
(6)停用任务,先调用xxl-job-admin的任务停用接口,成功返回后,再进行本地任务表停用,如果失败,也不执行本地任务表停用,返回错误信息给前端;
(7)删除任务,先调用xxl-job-admin的任务删除接口,成功返回后,再进行本地任务表删除,如果失败,也不执行本地任务表删除,返回错误信息给前端;
(8)执行任务,先调用xxl-job-admin的任务运行接口,成功返回后,返回成功信息给前端,如果失败,则返回错误信息给前端。
4、典型接口开发
下面以任务列表接口为例,提供参考代码示例,其他接口相对更加简单,不再一一展示。
// in serviceImpl class
public List<ScheduleJob> getXxlJobList() {
String urlPageList = adminAddress + "/jobinfo/pageList";
try {
// retry 3 times to login
for (int i = 0; i < 3; i++) {
if (tokenLoginId == "") {
// not login, call login api
loginXxlJob();
} else {
break;
}
}
// call pagelist api of jobinfo
HttpResponse httpResponse = HttpRequest.post(urlPageList)
.form("jobGroup", jobGroup)
.form("triggerStatus", -1)
.form("jobDesc")
.form("executorHandler")
.form("author")
.form("start", 0)
.form("length", 50)
.cookie(CONSTANTS_COOKIE_LOGINID + "=" + tokenLoginId)
.execute();
String strRes = httpResponse.body();
JSONArray arrayJob = JSONUtil.parse(strRes).getByPath("data", JSONArray.class);
// init a schedulejob array with blank field
JSONArray arrayScheduleJob = new JSONArray();
// change the value of array
for (int j = 0; j < arrayJob.size(); j++) {
JSONObject jsonJob = arrayJob.getJSONObject(j);
if (!jsonJob.getStr("jobGroup").equals(CONSTANTS_APP_NAME)) {
// not the right app
continue;
}
String jobId = jsonJob.getStr("id");
String beanName = "orderTask";
String methodName = jsonJob.getStr("executorHandler");
String params = jsonJob.getStr("executorParam");
String cronExpression = jsonJob.getStr("scheduleConf");
String status = jsonJob.getStr("triggerStatus");
String remark = jsonJob.getStr("jobDesc");
String createTime = jsonJob.getStr("addTime");
String strScheduleJob = String.format("{'jobId':'%s', 'beanName':'%s', 'methodName':'%s', 'params':'%s', 'cronExpression':'%s', 'status':'%s', 'remark':'%s', 'createTime':'%s'}",
jobId, beanName, methodName, params, cronExpression, status, remark, createTime);
arrayScheduleJob.add(new JSONObject(strScheduleJob));
}
List<ScheduleJob> listScheduleJob = JSONUtil.parseArray(arrayScheduleJob).toList(ScheduleJob.class);
return listScheduleJob;
} catch (Exception e) {
logger.error("xxl-job-admin任务列表获取错误", e);
return null;
}
}
// in controller class
public ServerResponseEntity<IPage<ScheduleJob>> page(ScheduleJob scheduleJob,PageParam<ScheduleJob> page) {
List<ScheduleJob> listScheduleJob = scheduleJobService.getXxlJobList();
if (listScheduleJob != null) {
// success getting list of scheduled jobs, update the database
scheduleJobService.saveOrUpdateBatch(listScheduleJob);
}
IPage<ScheduleJob> scheduleJobIPage = scheduleJobService.page(page, new LambdaQueryWrapper<ScheduleJob>());
return scheduleJobIPage;
}
5、后续优化
(1)因登录后返回的cookie并没有注明有效期,测试场景下可以使用类变量进行保存,但真实环境下可简单粗暴地在每次接口调用时都重新进行登录,复杂一些则根据返回信息(如过期则会302返回登录页)进行重新登录。
(2)如果要达到定时任务的真正灵活接入各种分布式任务方案,需要对ScheduleJob类进行进一步抽象,而不能直接使用诸如getXxlJobList命名的方法,待下次有空再安排吧。
本文暂时没有评论,来添加一个吧(●'◡'●)