摘要: 原创出处 blog.csdn.net/pipidog008/article/details/128869522 「dovienson」欢迎转载,保留摘要,谢谢!
前言
上篇文章我们介绍了 Quartz 的使用,当时实现了两个简单的需求,不过最后我们总结的时候也提到 Quartz 有不少缺点,代码侵入太严重,所以本篇将介绍 xxl-job 这个定时任务框架。
Quartz的不足
Quartz 的不足:Quartz 作为开源任务调度中的佼佼者,是任务调度的首选。但是在集群环境中,Quartz采用API的方式对任务进行管理,这样存在以下问题:
通过调用API的方式操作任务,不人性化。
需要持久化业务的 QuartzJobBean 到底层数据表中,系统侵入性相当严重。
调度逻辑和QuartzJobBean耦合在同一个项目中,这将导致一个问题,在调度任务数量逐渐增多,同时调度任务逻辑逐渐加重的情况下,此时调度系统的性能将大大受限于业务。
Xxl-job介绍
官方说明:XXL-JOB 是一个轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线,开箱即用。
通俗来讲:XXL-JOB 是一个任务调度框架,通过引入 XXL-JOB 相关的依赖,按照相关格式撰写代码后,可在其可视化界面进行任务的启动,执行,中止以及包含了日志记录与查询和任务状态监控。
更多详细介绍推荐阅读官方文档。
https://www.xuxueli.com/xxl-job/
项目实践
Spring Boot集成XXL-JOB Spring Boot 集成 XXL-JOB 主要分为以下两步:
配置运行调度中心(xxl-job-admin)
配置运行执行器项目
xxl-job-admin 可以从源码仓库中下载代码,代码地址:
https://gitee.com/xuxueli0323/xxl-job
下载完之后,在 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: admin: addresses: http://127.0.0.1:8080/xxl-job-admin executor: appname: hresh-job-executor ip: port: 6666 logpath: /Users/xxx/xxl-job-2.3.1/logs/xxl-job logretentiondays: 30 accessToken: default_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 = 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; 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" ; 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); } 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); } 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="loadByAppName" parameterType="java.util.HashMap" resultMap="XxlJobGroup" > SELECT <include refid="Base_Column_List" /> FROM xxl_job_group AS t WHERE t.app_name = #{appName} </select>
4、修改 JobInfoController
,增加 addJob
方法和 removeJob
方法
@RequestMapping ("/addJob" ) @ResponseBody @PermissionLimit (limit = false ) public ReturnT<String> 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)); }
addJob 方法与 JobInfoController
文件中的 add 方法具体逻辑是一样的,只是换个接口名。
@RequestMapping ("/add" ) @ResponseBody public ReturnT<String> add (XxlJobInfo jobInfo) { return xxlJobService.add(jobInfo); }
至此,关于调度中心的修改就结束了。
5、XxlService 创建任务
@Service @Slf 4j@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、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。
@Service @RequiredArgsConstructor @Slf 4jpublic class UserService { private final UserMapper userMapper; private final UserStruct userStruct; private final WeatherService weatherService; private final XxlService xxlService; @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<User> users = userMapper.queryAll(); log.info("执行发送天气通知给用户的任务。。。" ); WeatherInfo weatherInfo = weatherService.getWeather(WeatherConstant.WU_HAN); for (User user : users) { log.info(user.getUsername() + "----" + weatherInfo.toString()); } } }
2、WeatherService,获取天气温度等信息,这里就不贴代码了。
3、UserController,只有一个用户注册方法
@RestController @RequiredArgsConstructor public class UserController { private final UserService userService; @PostMapping ("/register" ) public Result<Object> register (@RequestBody UserRequest userRequest) { userService.register(userRequest); return Result.ok(); } }
任务处理器
这里演示两种任务处理器,一种是用于处理 UI 页面创建的任务,另一种是处理代码创建的任务。
1、DemoHandler,仅用作演示,没什么实际含义。
@RequiredArgsConstructor @Slf 4jpublic class DemoHandler extends IJobHandler { @XxlJob (value = "demoHandler" ) @Override public void execute () throws Exception { log.info("自动任务" + this .getClass().getSimpleName() + "执行" ); } }
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); } }
在最新版本的 xxl-job 中,任务核心类 “IJobHandler” 的 “execute” 方法取消出入参设计。改为通过 “XxlJobHelper.getJobParam
” 获取任务参数并替代方法入参,通过 “XxlJobHelper.handleSuccess/handleFail
” 设置任务结果并替代方法出参,示例代码如下
@XxlJob ("demoJobHandler" )public void execute () { String param = XxlJobHelper.getJobParam(); XxlJobHelper.handleSuccess(); }
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、首先在执行器管理页面,点击新增按钮,弹出新增框。输入AppName (与application.yml中配置的appname保持一致),名称,注册方式默认自动注册,点击保存。
2、新增任务
控制台输出:
com.msdn.time.handler.DemoHandler : 自动任务DemoHandler执行
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); }
重启项目后,比如说明再创建一个名为 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:
原因:xxl-job-admin
项目的 application.properties
文件中关于 spring.mail.password
的配置不对,可能有人配置了自己邮箱的登录密码。
解决方案:
总结
通过对比 Quartz 和 XXL-JOB 的使用,可以发现后者更易上手,代码侵入不严重,且具备可视化界面。这就是推荐新手使用 XXL-JOB 的原因。