SpringBoot 結合 XXL-JOB 實現定時任務(2.3.1)

項目實踐

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
[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 配置類

@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="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
@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、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<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
@Slf4j
public 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://service.mail.qq.com/cgi-bin/help?subtype=1&&id=28&&no=1001256

原因:xxl-job-admin 項目的 application.properties 文件中關於 spring.mail.password 的配置不對,可能有人配置了自己郵箱的登錄密碼。

解決方案:

總結

通過對比 Quartz 和 XXL-JOB 的使用,可以發現後者更易上手,代碼侵入不嚴重,且具備可視化界面。這就是推薦新手使用 XXL-JOB 的原因。

 

添加任務

執行器管理

調度日誌

運行報表

用戶

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章