SpringBoot结合XXL-JOB实现定时任务
Posted dovienson
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot结合XXL-JOB实现定时任务相关的知识,希望对你有一定的参考价值。
《从零打造项目》系列文章
工具
ORM框架选型
- SpringBoot项目基础设施搭建
- SpringBoot集成Mybatis项目实操
- SpringBoot集成Mybatis Plus项目实操
- SpringBoot集成Spring Data JPA项目实操
数据库变更管理
定时任务框架
缓存
安全框架
开发规范
前言
上篇文章我们介绍了 Quartz 的使用,当时实现了两个简单的需求,不过最后我们总结的时候也提到 Quartz 有不少缺点,代码侵入太严重,所以本篇将介绍 xxl-job 这个定时任务框架。
Quartz的不足
Quartz 的不足:Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:
- 通过调用API的方式操作任务,不人性化。
- 需要持久化业务的 QuartzJobBean 到底层数据表中,系统侵入性相当严重。
- 调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。
Xxl-job介绍
官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动,执行,中止以及包含了日志记录与查询和任务状态监控。
更多详细介绍推荐阅读官方文档。
项目实践
Spring Boot集成XXL-JOB
Spring Boot 集成 XXL-JOB 主要分为以下两步:
- 配置运行调度中心(xxl-job-admin)
- 配置运行执行器项目
xxl-job-admin 可以从源码仓库中下载代码,代码地址有两个:
- GitHub:github.com/xuxueli/xxl…
- Gitee:gitee.com/xuxueli0323…
下载完之后,在 doc/db
目录下有数据库脚本 tables_xxl_job.sql
,执行下脚本初始化调度数据库 xxl_job
,如下图所示:
配置调度中心
将下载的源码解压,用 IDEA 打开,我们需要修改一下 xxl-job-admin 中的一些配置。(我这里下载的是最新版 2.3.1)
1、修改 application.properties,主要是配置一下 datasource 以及 email,其他不需要改变。
### xxl-job, datasource
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
### xxl-job, email
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=1739468244@qq.com
spring.mail.from=1739468244@qq.com
# 此处不是邮箱登录密码,而是开启SMTP服务后的授权码
spring.mail.password=xxxxx
2、修改 logback.xml,配置日志输出路径,我是在解压的 xxl-job-2.3.1 项目包中新建了一个 logs 文件夹。
<property name="log.path" value="/Users/xxx/xxl-job-2.3.1/logs/xxl-job-admin.log"/>
然后启动项目,正常启动后,访问地址为:http://localhost:8080/xxl-job-admin,默认的账户为 admin,密码为 123456,访问后台管理系统后台。
这样就表示调度中心已经搞定了,下一步就是创建执行器项目。
创建执行器项目
本项目与 Quartz 项目用的业务表和业务逻辑都一样,所以引入的依赖会比较多。
环境配置
1、引入依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.3</version>
<relativePath/>
</parent>
<properties>
<java.version>1.8</java.version>
<fastjson.version>1.2.73</fastjson.version>
<hutool.version>5.5.1</hutool.version>
<mysql.version>8.0.19</mysql.version>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.20</org.projectlombok.version>
<druid.version>1.1.18</druid.version>
<springdoc.version>1.6.9</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>$mysql.version</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>$druid.version</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>$org.mapstruct.version</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>$org.mapstruct.version</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>$hutool.version</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>$springdoc.version</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
2、application.yml 配置文件
server:
port: 9090
# xxl-job
xxl:
job:
admin:
addresses: http://127.0.0.1:8080/xxl-job-admin # 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
executor:
appname: hresh-job-executor # 执行器 AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
ip: # 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
port: 6666 # ### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
logpath: /Users/xxx/xxl-job-2.3.1/logs/xxl-job # 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
logretentiondays: 30 # 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
accessToken: default_token # 执行器通讯TOKEN [选填]:非空时启用;
spring:
application:
name: xxl-job-practice
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/xxl_job?serverTimezone=Hongkong&characterEncoding=utf-8&useSSL=false
username: root
password: root
mybatis:
mapper-locations: classpath:mapper/*Mapper.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
lazy-loading-enabled: true
上述 xxl-job 的 logpath 配置与调度中心的输出日志用的是同一个目录,accessToken 也与调度中心的 xxl.job.accessToken 一致。
核心类
1、xxl-job 配置类
@Configuration
public class XxlJobConfig
@Value("$xxl.job.admin.addresses")
private String adminAddresses;
@Value("$xxl.job.executor.appname")
private String appName;
@Value("$xxl.job.executor.ip")
private String ip;
@Value("$xxl.job.executor.port")
private int port;
@Value("$xxl.job.accessToken")
private String accessToken;
@Value("$xxl.job.executor.logpath")
private String logPath;
@Value("$xxl.job.executor.logretentiondays")
private int logRetentionDays;
@Bean
public XxlJobSpringExecutor xxlJobExecutor()
// 创建 XxlJobSpringExecutor 执行器
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appName);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
// 返回
return xxlJobSpringExecutor;
2、xxl-job 工具类
@Component
@RequiredArgsConstructor
public class XxlUtil
@Value("$xxl.job.admin.addresses")
private String xxlJobAdminAddress;
private final RestTemplate restTemplate;
// 请求Url
private static final String ADD_INFO_URL = "/jobinfo/addJob";
private static final String REMOVE_INFO_URL = "/jobinfo/removeJob";
private static final String GET_GROUP_ID = "/jobgroup/loadByAppName";
/**
* 添加任务
*
* @param xxlJobInfo
* @param appName
* @return
*/
public String addJob(XxlJobInfo xxlJobInfo, String appName)
Map<String, Object> params = new HashMap<>();
params.put("appName", appName);
String json = JSONUtil.toJsonStr(params);
String result = doPost(xxlJobAdminAddress + GET_GROUP_ID, json);
JSONObject jsonObject = JSON.parseObject(result);
Map<String, Object> map = (Map<String, Object>) jsonObject.get("content");
Integer groupId = (Integer) map.get("id");
xxlJobInfo.setJobGroup(groupId);
String xxlJobInfoJson = JSONUtil.toJsonStr(xxlJobInfo);
return doPost(xxlJobAdminAddress + ADD_INFO_URL, xxlJobInfoJson);
// 删除job
public String removeJob(long jobId)
MultiValueMap<String, String> map = new LinkedMultiValueMap<String, String>();
map.add("id", String.valueOf(jobId));
return doPostWithFormData(xxlJobAdminAddress + REMOVE_INFO_URL, map);
/**
* 远程调用
*
* @param url
* @param json
*/
private String doPost(String url, String json)
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> entity = new HttpEntity<>(json, headers);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, entity, String.class);
return responseEntity.getBody();
private String doPostWithFormData(String url, MultiValueMap<String, String> map)
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, entity, String.class);
return responseEntity.getBody();
此处我们利用 RestTemplate 来远程调用 xxl-job-admin 中的服务,从而实现动态创建定时任务,而不是局限于通过 UI 界面来创建任务。
这里我们用到三个接口,都需要我们在 xxl-job-admin 中手动添加,这样在调用接口时,就不需要登录验证了,这就要求在定义接口时加上一个 PermissionLimit
并设置 limit 为 false,那么这样就不用去登录就可以调用接口。
3、修改 JobGroupController,新增 loadByAppName 方法
@RequestMapping("/loadByAppName")
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT<XxlJobGroup> loadByAppName(@RequestBody Map<String, Object> map)
XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map);
return jobGroup != null ? new ReturnT<XxlJobGroup>(jobGroup)
: new ReturnT<XxlJobGroup>(ReturnT.FAIL_CODE, null);
XxlJobGroupDao 文件以及对应的 xml 文件
XxlJobGroup loadByAppName(Map<String, Object> map);
<select id=【基建—xxl-job定时任务平台】
SpringBoot定时任务 - 开箱即用分布式任务框架xxl-job
SpringBoot定时任务 - Spring自带的定时任务是如何实现的?有何注意点?