0


SpringBoot结合XXL-JOB实现定时任务

《从零打造项目》系列文章

工具

  • 比MyBatis Generator更强大的代码生成器

ORM框架选型

  • SpringBoot项目基础设施搭建
  • SpringBoot集成Mybatis项目实操
  • SpringBoot集成Mybatis Plus项目实操
  • SpringBoot集成Spring Data JPA项目实操

数据库变更管理

  • 数据库变更管理:Liquibase or Flyway
  • SpringBoot结合Liquibase实现数据库变更管理

定时任务框架

  • Java定时任务技术分析
  • SpringBoot结合Quartz实现定时任务
  • SpringBoot结合XXL-JOB实现定时任务

缓存

  • Spring Security结合Redis实现缓存功能

安全框架

  • Java应用程序安全框架
  • Spring Security系列文章
  • Spring Security结合JWT实现认证与授权

开发规范

  • 后端必知:遵循Google Java规范并引入checkstyle检查

前言

上篇文章我们介绍了 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 主要分为以下两步:

  1. 配置运行调度中心(xxl-job-admin)
  2. 配置运行执行器项目

xxl-job-admin 可以从源码仓库中下载代码,代码地址有两个:

  1. GitHub:github.com/xuxueli/xxl…
  2. Gitee:gitee.com/xuxueli0323…

image_d5911b2b.png

下载完之后,在

doc/db

目录下有数据库脚本

tables_xxl_job.sql

,执行下脚本初始化调度数据库

xxl_job

,如下图所示:

image_5db1afcc.png

配置调度中心

将下载的源码解压,用 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
[email protected]
[email protected]
# 此处不是邮箱登录密码,而是开启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 配置类

@ConfigurationpublicclassXxlJobConfig{@Value("${xxl.job.admin.addresses}")privateString adminAddresses;@Value("${xxl.job.executor.appname}")privateString appName;@Value("${xxl.job.executor.ip}")privateString ip;@Value("${xxl.job.executor.port}")privateint port;@Value("${xxl.job.accessToken}")privateString accessToken;@Value("${xxl.job.executor.logpath}")privateString logPath;@Value("${xxl.job.executor.logretentiondays}")privateint logRetentionDays;@BeanpublicXxlJobSpringExecutorxxlJobExecutor(){// 创建 XxlJobSpringExecutor 执行器XxlJobSpringExecutor xxlJobSpringExecutor =newXxlJobSpringExecutor();
    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@RequiredArgsConstructorpublicclassXxlUtil{@Value("${xxl.job.admin.addresses}")privateString xxlJobAdminAddress;privatefinalRestTemplate restTemplate;// 请求UrlprivatestaticfinalStringADD_INFO_URL="/jobinfo/addJob";privatestaticfinalStringREMOVE_INFO_URL="/jobinfo/removeJob";privatestaticfinalStringGET_GROUP_ID="/jobgroup/loadByAppName";/**
   * 添加任务
   *
   * @param xxlJobInfo
   * @param appName
   * @return
   */publicStringaddJob(XxlJobInfo xxlJobInfo,String appName){Map<String,Object> params =newHashMap<>();
    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);returndoPost(xxlJobAdminAddress +ADD_INFO_URL, xxlJobInfoJson);}// 删除jobpublicStringremoveJob(long jobId){MultiValueMap<String,String> map =newLinkedMultiValueMap<String,String>();
    map.add("id",String.valueOf(jobId));returndoPostWithFormData(xxlJobAdminAddress +REMOVE_INFO_URL, map);}/**
   * 远程调用
   *
   * @param url
   * @param json
   */privateStringdoPost(String url,String json){HttpHeaders headers =newHttpHeaders();
    headers.setContentType(MediaType.APPLICATION_JSON);HttpEntity<String> entity =newHttpEntity<>(json, headers);ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, entity,String.class);return responseEntity.getBody();}privateStringdoPostWithFormData(String url,MultiValueMap<String,String> map){HttpHeaders headers =newHttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);HttpEntity<MultiValueMap<String,String>> entity =newHttpEntity<>(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)publicReturnT<XxlJobGroup>loadByAppName(@RequestBodyMap<String,Object> map){XxlJobGroup jobGroup = xxlJobGroupDao.loadByAppName(map);return jobGroup !=null?newReturnT<XxlJobGroup>(jobGroup):newReturnT<XxlJobGroup>(ReturnT.FAIL_CODE,null);}

XxlJobGroupDao 文件以及对应的 xml 文件

XxlJobGrouploadByAppName(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)publicReturnT<String>addJob(@RequestBodyXxlJobInfo jobInfo){return xxlJobService.add(jobInfo);}@RequestMapping("/removeJob")@ResponseBody@PermissionLimit(limit =false)publicReturnT<String>removeJob(String id){return xxlJobService.remove(Integer.parseInt(id));}

addJob 方法与 JobInfoController 文件中的 add 方法具体逻辑是一样的,只是换个接口名。

@RequestMapping("/add")@ResponseBodypublicReturnT<String>add(XxlJobInfo jobInfo){return xxlJobService.add(jobInfo);}

至此,关于调度中心的修改就结束了。

5、XxlService 创建任务

@Service@Slf4j@RequiredArgsConstructorpublicclassXxlService{privatefinalXxlUtil xxlUtil;@Value("${xxl.job.executor.appname}")privateString appName;publicvoidaddJob(XxlJobInfo xxlJobInfo){
    xxlUtil.addJob(xxlJobInfo, appName);long triggerNextTime = xxlJobInfo.getTriggerNextTime();
    log.info("任务已添加,将在{}开始执行任务",DateUtils.formatDate(triggerNextTime));}}

业务代码

1、UserService,包括用户注册,给用户发送欢迎消息,以及发送天气温度通知。

@Service@RequiredArgsConstructor@Slf4jpublicclassUserService{privatefinalUserMapper userMapper;privatefinalUserStruct userStruct;privatefinalWeatherService weatherService;privatefinalXxlService xxlService;/**
   * 假设有这样一个业务需求,每当有新用户注册,则1分钟后会给用户发送欢迎通知.
   *
   * @param userRequest 用户请求体
   */@Transactionalpublicvoidregister(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);}publicvoidsayHelloToUser(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);}publicvoidpushWeatherNotification(){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@RequiredArgsConstructorpublicclassUserController{privatefinalUserService userService;@PostMapping("/register")publicResult<Object>register(@RequestBodyUserRequest userRequest){
    userService.register(userRequest);returnResult.ok();}}

任务处理器

这里演示两种任务处理器,一种是用于处理 UI 页面创建的任务,另一种是处理代码创建的任务。

1、DemoHandler,仅用作演示,没什么实际含义。

@RequiredArgsConstructor@Slf4jpublicclassDemoHandlerextendsIJobHandler{@XxlJob(value ="demoHandler")@Overridepublicvoidexecute()throwsException{
    log.info("自动任务"+this.getClass().getSimpleName()+"执行");}}

2、SayHelloHandler,用户注册后再 xxl-job 上创建一个任务,到时间后就调用该处理器。

@Component@RequiredArgsConstructorpublicclassSayHelloHandler{privatefinalUserService userService;@XxlJob(value ="sayHelloHandler")publicvoidexecute(){String param =XxlJobHelper.getJobParam();
    userService.sayHelloToUser(param);}}

在最新版本的 xxl-job 中,任务核心类 “IJobHandler” 的 “execute” 方法取消出入参设计。改为通过 “XxlJobHelper.getJobParam” 获取任务参数并替代方法入参,通过 “XxlJobHelper.handleSuccess/handleFail” 设置任务结果并替代方法出参,示例代码如下

@XxlJob("demoJobHandler")publicvoidexecute(){String param =XxlJobHelper.getJobParam();// 获取参数XxlJobHelper.handleSuccess();// 设置任务结果}

3、WeatherNotificationHandler,每天定时发送天气通知

@Component@RequiredArgsConstructorpublicclassWeatherNotificationHandlerextendsIJobHandler{privatefinalUserService userService;@XxlJob(value ="weatherNotificationHandler")@Overridepublicvoidexecute()throwsException{
    userService.pushWeatherNotification();}}

测试

1、首先在执行器管理页面,点击新增按钮,弹出新增框。输入AppName (与application.yml中配置的appname保持一致),名称,注册方式默认自动注册,点击保存。

image_3ded6957.png

2、新增任务

image_670b2e5a.png

image_a6993373.png

控制台输出:

com.msdn.time.handler.DemoHandler: 自动任务DemoHandler执行

2、利用 postman 来注册用户

image_799b2504.png

去 UI 任务管理页面,可以看到代码创建的任务。

image_83c8f5d6.png

1分钟后,控制台输出如下:

image_a088100a.png

3、在 UI 任务管理页面手动新增任务,用来发送天气通知。

image_c85caebc.png

点击执行一次,控制台输出如下:

image_df0334c3.png

实际应用中,对于手动创建的任务,直接点击启动就可以了。

这里还有一个问题,如果每次有新用户注册,都会创建一个定时任务,而且只执行一次,那么任务列表到时候就会有很多脏数据,所以我们在执行完发送欢迎通知后,就要删除。所以我们需要修改一下 SayHelloHandler

@XxlJob(value ="sayHelloHandler")publicvoidexecute(){String param =XxlJobHelper.getJobParam();
    userService.sayHelloToUser(param);long jobId =XxlJobHelper.getJobId();
    xxlUtil.removeJob(jobId);}

重启项目后,比如说明再创建一个名为 hresh2 的用户,然后任务列表就会新增一个任务。

image_d8b98e40.png

等控制台输出 sayHello 后,可以发现任务列表中任务 ID 为 20的记录被删除掉了。

问题

控制台输出邮件注册错误

11:01:48.740 logback [RMITCPConnection(1)-127.0.0.1]WARNo.s.b.a.mail.MailHealthIndicator-Mail health check failed
javax.mail.AuthenticationFailedException:535LoginFail. Please enter your authorization code tologin.More information in http://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256

原因:xxl-job-admin 项目的 application.properties 文件中关于 spring.mail.password 的配置不对,可能有人配置了自己邮箱的登录密码。

解决方案:

image_cfd1ee17.png

image_78147bb0.png

总结

通过对比 Quartz 和 XXL-JOB 的使用,可以发现后者更易上手,代码侵入不严重,且具备可视化界面。这就是推荐新手使用 XXL-JOB 的原因。

感兴趣的朋友可以去我的 Github 下载相关代码,如果对你有所帮助,不妨 Star 一下,谢谢大家支持!

参考文献

XXL-JOB动态创建任务详解篇2

Spring Boot 集成 XXL-JOB 任务调度平台


本文转载自: https://blog.csdn.net/pipidog008/article/details/128869522
版权归原作者 dovienson 所有, 如有侵权,请联系我们删除。

“SpringBoot结合XXL-JOB实现定时任务”的评论:

还没有评论