编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

Step by Step之xxl-job任务管理集成开发

wxchong 2024-06-11 09:58:23 开源技术 18 ℃ 0 评论

在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命名的方法,待下次有空再安排吧。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表