項目實踐
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 的原因。
添加任務
執行器管理
調度日誌
運行報表
用戶