前言
上篇文章我们介绍了 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2、修改 logback.xml,配置日志输出路径,我是在解压的 xxl-job-2.3.1 项目包中新建了一个 logs 文件夹。
这样就表示调度中心已经搞定了,下一步就是创建执行器项目。
创建执行器项目
本项目与 Quartz 项目用的业务表和业务逻辑都一样,所以引入的依赖会比较多。
环境配置
1、引入依赖:
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
上述 xxl-job 的 logpath 配置与调度中心的输出日志用的是同一个目录,accessToken 也与调度中心的 xxl.job.accessToken 一致。
核心类
1、xxl-job 配置类
@Configuration
public class XxlJobConfig {
@Value(“
x
x
l
.
j
o
b
.
a
d
m
i
n
.
a
d
d
r
e
s
s
e
s
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
a
d
m
i
n
A
d
d
r
e
s
s
e
s
;
@
V
a
l
u
e
(
"
{xxl.job.admin.addresses}") private String adminAddresses; @Value("
xxl.job.admin.addresses")privateStringadminAddresses;@Value("{xxl.job.executor.appname}”)
private String appName;
@Value(“
x
x
l
.
j
o
b
.
e
x
e
c
u
t
o
r
.
i
p
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
i
p
;
@
V
a
l
u
e
(
"
{xxl.job.executor.ip}") private String ip; @Value("
xxl.job.executor.ip")privateStringip;@Value("{xxl.job.executor.port}”)
private int port;
@Value(“
x
x
l
.
j
o
b
.
a
c
c
e
s
s
T
o
k
e
n
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
a
c
c
e
s
s
T
o
k
e
n
;
@
V
a
l
u
e
(
"
{xxl.job.accessToken}") private String accessToken; @Value("
xxl.job.accessToken")privateStringaccessToken;@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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 entity = new HttpEntity<>(json, headers);
ResponseEntity 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 responseEntity = restTemplate.postForEntity(url, entity, String.class);
return responseEntity.getBody();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
此处我们利用 RestTemplate 来远程调用 xxl-job-admin 中的服务,从而实现动态创建定时任务,而不是局限于通过 UI 界面来创建任务。
这里我们用到三个接口,都需要我们在 xxl-job-admin 中手动添加,这样在调用接口时,就不需要登录验证了,这就要求在定义接口时加上一个 PermissionLimit并设置 limit 为 false,那么这样就不用去登录就可以调用接口。
3、修改 JobGroupController,新增 loadByAppName 方法
@RequestMapping(“/loadByAppName”)
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT loadByAppName(@RequestBody Map<String, Object> map) {
XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map);
return jobGroup != null ? new ReturnT(jobGroup)
: new ReturnT(ReturnT.FAIL_CODE, null);
}
1
2
3
4
5
6
7
8
XxlJobGroupDao 文件以及对应的 xml 文件
XxlJobGroup loadByAppName(Map<String, Object> map);
1
SELECT
FROM xxl_job_group AS t
WHERE t.app_name = #{appName}
1
2
3
4
5
6
4、修改 JobInfoController,增加 addJob 方法和 removeJob 方法
@RequestMapping(“/addJob”)
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT addJob(@RequestBody XxlJobInfo jobInfo) {
return xxlJobService.add(jobInfo);
}
@RequestMapping("/removeJob")
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT<String> removeJob(String id) {
return xxlJobService.remove(Integer.parseInt(id));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
addJob 方法与 JobInfoController 文件中的 add 方法具体逻辑是一样的,只是换个接口名。
@RequestMapping(“/add”)
@ResponseBody
public ReturnT add(XxlJobInfo jobInfo) {
return xxlJobService.add(jobInfo);
}
1
2
3
4
5
至此,关于调度中心的修改就结束了。
5、XxlService 创建任务
@Service
@Slf4j
@RequiredArgsConstructor
public class XxlService {
private final XxlUtil xxlUtil;
@Value(“${xxl.job.executor.appname}”)
private String appName;
public void addJob(XxlJobInfo xxlJobInfo) {
xxlUtil.addJob(xxlJobInfo, appName);
long triggerNextTime = xxlJobInfo.getTriggerNextTime();
log.info(“任务已添加,将在{}开始执行任务”, DateUtils.formatDate(triggerNextTime));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
业务代码
1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserMapper userMapper;
private final UserStruct userStruct;
private final WeatherService weatherService;
private final XxlService xxlService;
/**
- 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知.
- @param userRequest 用户请求体
*/
@Transactional
public void register(UserRequest userRequest) {
if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) ||
isBlank(userRequest.getPassword())) {
BusinessException.fail(“账号或密码为空!”);
}
User user = userStruct.toUser(userRequest);
userMapper.insert(user);
LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L);
XxlJobInfo xxlJobInfo = XxlJobInfo.builder().jobDesc("定时给用户发送通知").author("hresh")
.scheduleType("CRON").scheduleConf(DateUtils.getCron(scheduleTime)).glueType("BEAN")
.glueType("BEAN")
.executorHandler("sayHelloHandler")
.executorParam(user.getUsername())
.misfireStrategy("DO_NOTHING")
.executorRouteStrategy("FIRST")
.triggerNextTime(DateUtils.toEpochMilli(scheduleTime))
.executorBlockStrategy("SERIAL_EXECUTION").triggerStatus(1).build();
xxlService.addJob(xxlJobInfo);
}
public void sayHelloToUser(String username) {
if (StrUtil.isBlank(username)) {
log.error(“用户名为空”);
}
User user = userMapper.selectByUserName(username);
String message = “Welcome to Java,I am hresh.”;
log.info(user.getUsername() + " , hello, " + message);
}
public void pushWeatherNotification() {
List users = userMapper.queryAll();
log.info(“执行发送天气通知给用户的任务。。。”);
WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN);
for (User user : users) {
log.info(user.getUsername() + “----” + weatherInfo.toString());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2、WeatherService,获取天气温度等信息,这里就不贴代码了。
3、UserController,只有一个用户注册方法
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping(“/register”)
public Result register(@RequestBody UserRequest userRequest) {
userService.register(userRequest);
return Result.ok();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
任务处理器
这里演示两种任务处理器,一种是用于处理 UI 页面创建的任务,另一种是处理代码创建的任务。
1、DemoHandler,仅用作演示,没什么实际含义。
@RequiredArgsConstructor
@Slf4j
public class DemoHandler extends IJobHandler {
@XxlJob(value = “demoHandler”)
@Override
public void execute() throws Exception {
log.info(“自动任务” + this.getClass().getSimpleName() + “执行”);
}
}
1
2
3
4
5
6
7
8
9
10
2、SayHelloHandler,用户注册后再 xxl-job 上创建一个任务,到时间后就调用该处理器。
@Component
@RequiredArgsConstructor
public class SayHelloHandler {
private final UserService userService;
@XxlJob(value = “sayHelloHandler”)
public void execute() {
String param = XxlJobHelper.getJobParam();
userService.sayHelloToUser(param);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
在最新版本的 xxl-job 中,任务核心类 “IJobHandler” 的 “execute” 方法取消出入参设计。改为通过 “XxlJobHelper.getJobParam” 获取任务参数并替代方法入参,通过 “XxlJobHelper.handleSuccess/handleFail” 设置任务结果并替代方法出参,示例代码如下
@XxlJob(“demoJobHandler”)
public void execute() {
String param = XxlJobHelper.getJobParam(); // 获取参数
XxlJobHelper.handleSuccess(); // 设置任务结果
}
1
2
3
4
5
3、WeatherNotificationHandler,每天定时发送天气通知
@Component
@RequiredArgsConstructor
public class WeatherNotificationHandler extends IJobHandler {
private final UserService userService;
@XxlJob(value = “weatherNotificationHandler”)
@Override
public void execute() throws Exception {
userService.pushWeatherNotification();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
测试
1、首先在执行器管理页面,点击新增按钮,弹出新增框。输入AppName (与application.yml中配置的appname保持一致),名称,注册方式默认自动注册,点击保存。
2、新增任务
控制台输出:
com.msdn.time.handler.DemoHandler : 自动任务DemoHandler执行
1
2、利用 postman 来注册用户
去 UI 任务管理页面,可以看到代码创建的任务。
1分钟后,控制台输出如下:
3、在 UI 任务管理页面手动新增任务,用来发送天气通知。
点击执行一次,控制台输出如下:
实际应用中,对于手动创建的任务,直接点击启动就可以了。
这里还有一个问题,如果每次有新用户注册,都会创建一个定时任务,而且只执行一次,那么任务列表到时候就会有很多脏数据,所以我们在执行完发送欢迎通知后,就要删除。所以我们需要修改一下 SayHelloHandler
@XxlJob(value = “sayHelloHandler”)
public void execute() {
String param = XxlJobHelper.getJobParam();
userService.sayHelloToUser(param);
long jobId = XxlJobHelper.getJobId();
xxlUtil.removeJob(jobId);
}
1
2
3
4
5
6
7
8
重启项目后,比如说明再创建一个名为 hresh2 的用户,然后任务列表就会新增一个任务。
等控制台输出 sayHello 后,可以发现任务列表中任务 ID 为 20的记录被删除掉了。
问题
控制台输出邮件注册错误
11:01:48.740 logback [RMI TCP Connection(1)-127.0.0.1] WARN o.s.b.a.mail.MailHealthIndicator - Mail health check failed
javax.mail.AuthenticationFailedException: 535 Login Fail. Please enter your authorization code to login. More information in http://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256
1
2
原因:xxl-job-admin 项目的 application.properties 文件中关于 spring.mail.password 的配置不对,可能有人配置了自己邮箱的登录密码。
解决方案:
总结
通过对比 Quartz 和 XXL-JOB 的使用,可以发现后者更易上手,代码侵入不严重,且具备可视化界面。这就是推荐新手使用 XXL-JOB 的原因。
感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!
————————————————
版权声明:本文为CSDN博主「dovienson」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/pipidog008/article/details/128869522前言
上篇文章我们介绍了 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2、修改 logback.xml,配置日志输出路径,我是在解压的 xxl-job-2.3.1 项目包中新建了一个 logs 文件夹。
这样就表示调度中心已经搞定了,下一步就是创建执行器项目。
创建执行器项目
本项目与 Quartz 项目用的业务表和业务逻辑都一样,所以引入的依赖会比较多。
环境配置
1、引入依赖:
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
上述 xxl-job 的 logpath 配置与调度中心的输出日志用的是同一个目录,accessToken 也与调度中心的 xxl.job.accessToken 一致。
核心类
1、xxl-job 配置类
@Configuration
public class XxlJobConfig {
@Value(“
x
x
l
.
j
o
b
.
a
d
m
i
n
.
a
d
d
r
e
s
s
e
s
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
a
d
m
i
n
A
d
d
r
e
s
s
e
s
;
@
V
a
l
u
e
(
"
{xxl.job.admin.addresses}") private String adminAddresses; @Value("
xxl.job.admin.addresses")privateStringadminAddresses;@Value("{xxl.job.executor.appname}”)
private String appName;
@Value(“
x
x
l
.
j
o
b
.
e
x
e
c
u
t
o
r
.
i
p
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
i
p
;
@
V
a
l
u
e
(
"
{xxl.job.executor.ip}") private String ip; @Value("
xxl.job.executor.ip")privateStringip;@Value("{xxl.job.executor.port}”)
private int port;
@Value(“
x
x
l
.
j
o
b
.
a
c
c
e
s
s
T
o
k
e
n
"
)
p
r
i
v
a
t
e
S
t
r
i
n
g
a
c
c
e
s
s
T
o
k
e
n
;
@
V
a
l
u
e
(
"
{xxl.job.accessToken}") private String accessToken; @Value("
xxl.job.accessToken")privateStringaccessToken;@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;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 entity = new HttpEntity<>(json, headers);
ResponseEntity 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 responseEntity = restTemplate.postForEntity(url, entity, String.class);
return responseEntity.getBody();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
此处我们利用 RestTemplate 来远程调用 xxl-job-admin 中的服务,从而实现动态创建定时任务,而不是局限于通过 UI 界面来创建任务。
这里我们用到三个接口,都需要我们在 xxl-job-admin 中手动添加,这样在调用接口时,就不需要登录验证了,这就要求在定义接口时加上一个 PermissionLimit并设置 limit 为 false,那么这样就不用去登录就可以调用接口。
3、修改 JobGroupController,新增 loadByAppName 方法
@RequestMapping(“/loadByAppName”)
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT loadByAppName(@RequestBody Map<String, Object> map) {
XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map);
return jobGroup != null ? new ReturnT(jobGroup)
: new ReturnT(ReturnT.FAIL_CODE, null);
}
1
2
3
4
5
6
7
8
XxlJobGroupDao 文件以及对应的 xml 文件
XxlJobGroup loadByAppName(Map<String, Object> map);
1
SELECT
FROM xxl_job_group AS t
WHERE t.app_name = #{appName}
1
2
3
4
5
6
4、修改 JobInfoController,增加 addJob 方法和 removeJob 方法
@RequestMapping(“/addJob”)
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT addJob(@RequestBody XxlJobInfo jobInfo) {
return xxlJobService.add(jobInfo);
}
@RequestMapping("/removeJob")
@ResponseBody
@PermissionLimit(limit = false)
public ReturnT<String> removeJob(String id) {
return xxlJobService.remove(Integer.parseInt(id));
}
1
2
3
4
5
6
7
8
9
10
11
12
13
addJob 方法与 JobInfoController 文件中的 add 方法具体逻辑是一样的,只是换个接口名。
@RequestMapping(“/add”)
@ResponseBody
public ReturnT add(XxlJobInfo jobInfo) {
return xxlJobService.add(jobInfo);
}
1
2
3
4
5
至此,关于调度中心的修改就结束了。
5、XxlService 创建任务
@Service
@Slf4j
@RequiredArgsConstructor
public class XxlService {
private final XxlUtil xxlUtil;
@Value(“${xxl.job.executor.appname}”)
private String appName;
public void addJob(XxlJobInfo xxlJobInfo) {
xxlUtil.addJob(xxlJobInfo, appName);
long triggerNextTime = xxlJobInfo.getTriggerNextTime();
log.info(“任务已添加,将在{}开始执行任务”, DateUtils.formatDate(triggerNextTime));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
业务代码
1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。
@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {
private final UserMapper userMapper;
private final UserStruct userStruct;
private final WeatherService weatherService;
private final XxlService xxlService;
/**
- 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知.
- @param userRequest 用户请求体
*/
@Transactional
public void register(UserRequest userRequest) {
if (Objects.isNull(userRequest) || isBlank(userRequest.getUsername()) ||
isBlank(userRequest.getPassword())) {
BusinessException.fail(“账号或密码为空!”);
}
User user = userStruct.toUser(userRequest);
userMapper.insert(user);
LocalDateTime scheduleTime = LocalDateTime.now().plusMinutes(1L);
XxlJobInfo xxlJobInfo = XxlJobInfo.builder().jobDesc("定时给用户发送通知").author("hresh")
.scheduleType("CRON").scheduleConf(DateUtils.getCron(scheduleTime)).glueType("BEAN")
.glueType("BEAN")
.executorHandler("sayHelloHandler")
.executorParam(user.getUsername())
.misfireStrategy("DO_NOTHING")
.executorRouteStrategy("FIRST")
.triggerNextTime(DateUtils.toEpochMilli(scheduleTime))
.executorBlockStrategy("SERIAL_EXECUTION").triggerStatus(1).build();
xxlService.addJob(xxlJobInfo);
}
public void sayHelloToUser(String username) {
if (StrUtil.isBlank(username)) {
log.error(“用户名为空”);
}
User user = userMapper.selectByUserName(username);
String message = “Welcome to Java,I am hresh.”;
log.info(user.getUsername() + " , hello, " + message);
}
public void pushWeatherNotification() {
List users = userMapper.queryAll();
log.info(“执行发送天气通知给用户的任务。。。”);
WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN);
for (User user : users) {
log.info(user.getUsername() + “----” + weatherInfo.toString());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
2、WeatherService,获取天气温度等信息,这里就不贴代码了。
3、UserController,只有一个用户注册方法
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping(“/register”)
public Result register(@RequestBody UserRequest userRequest) {
userService.register(userRequest);
return Result.ok();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
任务处理器
这里演示两种任务处理器,一种是用于处理 UI 页面创建的任务,另一种是处理代码创建的任务。
1、DemoHandler,仅用作演示,没什么实际含义。
@RequiredArgsConstructor
@Slf4j
public class DemoHandler extends IJobHandler {
@XxlJob(value = “demoHandler”)
@Override
public void execute() throws Exception {
log.info(“自动任务” + this.getClass().getSimpleName() + “执行”);
}
}
1
2
3
4
5
6
7
8
9
10
2、SayHelloHandler,用户注册后再 xxl-job 上创建一个任务,到时间后就调用该处理器。
@Component
@RequiredArgsConstructor
public class SayHelloHandler {
private final UserService userService;
@XxlJob(value = “sayHelloHandler”)
public void execute() {
String param = XxlJobHelper.getJobParam();
userService.sayHelloToUser(param);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
在最新版本的 xxl-job 中,任务核心类 “IJobHandler” 的 “execute” 方法取消出入参设计。改为通过 “XxlJobHelper.getJobParam” 获取任务参数并替代方法入参,通过 “XxlJobHelper.handleSuccess/handleFail” 设置任务结果并替代方法出参,示例代码如下
@XxlJob(“demoJobHandler”)
public void execute() {
String param = XxlJobHelper.getJobParam(); // 获取参数
XxlJobHelper.handleSuccess(); // 设置任务结果
}
1
2
3
4
5
3、WeatherNotificationHandler,每天定时发送天气通知
@Component
@RequiredArgsConstructor
public class WeatherNotificationHandler extends IJobHandler {
private final UserService userService;
@XxlJob(value = “weatherNotificationHandler”)
@Override
public void execute() throws Exception {
userService.pushWeatherNotification();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
测试
1、首先在执行器管理页面,点击新增按钮,弹出新增框。输入AppName (与application.yml中配置的appname保持一致),名称,注册方式默认自动注册,点击保存。
2、新增任务
控制台输出:
com.msdn.time.handler.DemoHandler : 自动任务DemoHandler执行
1
2、利用 postman 来注册用户
去 UI 任务管理页面,可以看到代码创建的任务。
1分钟后,控制台输出如下:
3、在 UI 任务管理页面手动新增任务,用来发送天气通知。
点击执行一次,控制台输出如下:
实际应用中,对于手动创建的任务,直接点击启动就可以了。
这里还有一个问题,如果每次有新用户注册,都会创建一个定时任务,而且只执行一次,那么任务列表到时候就会有很多脏数据,所以我们在执行完发送欢迎通知后,就要删除。所以我们需要修改一下 SayHelloHandler
@XxlJob(value = “sayHelloHandler”)
public void execute() {
String param = XxlJobHelper.getJobParam();
userService.sayHelloToUser(param);
long jobId = XxlJobHelper.getJobId();
xxlUtil.removeJob(jobId);
}
1
2
3
4
5
6
7
8
重启项目后,比如说明再创建一个名为 hresh2 的用户,然后任务列表就会新增一个任务。
等控制台输出 sayHello 后,可以发现任务列表中任务 ID 为 20的记录被删除掉了。
问题
控制台输出邮件注册错误
11:01:48.740 logback [RMI TCP Connection(1)-127.0.0.1] WARN o.s.b.a.mail.MailHealthIndicator - Mail health check failed
javax.mail.AuthenticationFailedException: 535 Login Fail. Please enter your authorization code to login. More information in http://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256
1
2
原因:xxl-job-admin 项目的 application.properties 文件中关于 spring.mail.password 的配置不对,可能有人配置了自己邮箱的登录密码。
解决方案:
总结
通过对比 Quartz 和 XXL-JOB 的使用,可以发现后者更易上手,代码侵入不严重,且具备可视化界面。这就是推荐新手使用 XXL-JOB 的原因。
感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!
————————————————
版权声明:本文为CSDN博主「dovienson」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/pipidog008/article/details/128869522