前言

好久没更新博客了,最近上班做了点小东西,总结复盘一下

参考资料:

SpringBoot 设置动态定时任务,千万别再写死了~ (qq.com)

3千字带你搞懂XXL-JOB任务调度平台-阿里云开发者社区 (aliyun.com)

一、定时任务

1. 引入依赖

创建Springboot应用,引入相应依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

在spring-boot-starter-web中排除spring-boot-starter-logging是为了不使用springboot默认的日志实现logback,而是引入log4j2的日志实现

引入lombok是为了使用@Data、@RequiredArgsConstructor等注解

2. 代码实现

在启动类上添加注解@EnableScheduling

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo;  

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class DttNoticeApplication {
public static void main(String[] args) {
SpringApplication.run(DttNoticeApplication.class, args);
}
}

配置文件指定运行的端口:

1
2
server:
port: 8080

编写实现定时任务的类,用@Scheduled修饰执行定时任务的方法,并用@Component将该类注册为Bean

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.example.demo;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

@Component
public class Task{
// cron表达式常用于定时任务,此处表示每10秒执行一次
@Scheduled(cron="0/10 * * * * ?")
public void scheduledTask(){
// ...
}
}

二、动态定时任务

定时任务执行时间的配置文件,位于resources/task-config.ini:

1
printTime.cron=0/10 * * * * ?

编写实现定时任务的类,利用@PropertySource指定获取的配置文件并用@Value注入到相应成员中,并用@Component将该类注册为Bean

实现SchedulingConfigurer接口,重载configureTasks函数,

其中,configureTasks函数接收一个ScheduledTaskRegistrar类型的参数,调用该对象的addTriggerTask,接收一个Runnable对象用于执行任务,以及一个Trigger对象用于计算下一次执行任务的时间

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
package com.example.demo.task;  

import lombok.Data;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.PropertySource;
import org.springframework.scheduling.Trigger;
import org.springframework.scheduling.TriggerContext;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.scheduling.support.PeriodicTrigger;
import org.springframework.stereotype.Component;


@Data
@Slf4j
@Component
@RequiredArgsConstructor
@PropertySource("classpath:task-config.ini")
public class ScheduleTask implements SchedulingConfigurer {

// private Long timer = 100 * 1000L;

@Value("${printTime.cron}")
private String cron;

@Override
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
// 间隔触发的任务
taskRegistrar.addTriggerTask(new Runnable() {
@Override
public void run() {
// ...
}
}, new Trigger() {
@Override
public Date nextExecutionTime(TriggerContext triggerContext) {
// 使用CronTrigger触发器,可动态修改cron表达式来操作循环规则
CronTrigger cronTrigger = new CronTrigger(cron);
Date nextExecutionTime = cronTrigger.nextExecutionTime(triggerContext);
return nextExecutionTime;

// 使用PerodicTrigger触发器,修改timer变量指定操作间隔,单位为毫秒
// PeriodicTrigger periodicTrigger = new PeriodicTrigger(timer);
// Date nextExecutionTime = periodicTrigger.nextExecutionTime(triggerContext);
// return nextExecutionTime;
}
});
}
}

注意到这里使用了@Data注解,是为了能够直接调用成员变量的setter更改cron表达式(或timer)的值

编写Controller提供修改定时任务执行时间的接口:

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
package com.example.demo.controller;  

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import com.szhg.dttnotice.task.ScheduleTask;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/notice")
public class NoticeController {

private final ScheduleTask scheduleTask;

@GetMapping("/updateCron")
public String updateCron(String cron) {
log.info("new cron :{}", cron);
scheduleTask.setCron(cron);
return "执行任务的表达式修改为: " + cron ;
}

// @GetMapping("/updateTimer")
// public String updateTimer(Long timer) {
// log.info("new timer :{}", timer);
// scheduleTask.setTimer(timer * 1000);
// return "执行任务的时间间隔修改为" + timer + "s";
// }
}

三、分布式定时任务

在分布式的架构中,我们需要一个支持集群、支持监控、支持告警等等功能的解决方案,那么上述方法就比较麻烦了。

主流的分布式任务调度平台包括elastic-job、xxl-job、quartz等

本文重点介绍xxl-job

首先从源码仓库地址将代码拉到本地:xuxueli/xxl-job: A distributed task scheduling framework.(分布式任务调度平台XXL-JOB) (github.com)

1. 运行调度中心

从根路径下找到doc/db/tables_xxl_job.sql,在数据库中新建Schema,执行该sql脚本

DataGrip的示例如下:

image.png

回看项目的根路径下有哪些模块:

  1. xxl-job-admin:任务调度的管理平台,跑起来后可在浏览器中访问
  2. xxl-job-core:项目的公共依赖
  3. xxl-job-executor-samples:执行器(也就是需要执行的任务)的示例

找到admin项目下的application.properties文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
### 调度中心JDBC链接
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=password
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
### 报警邮箱
spring.mail.host=smtp.qq.com
spring.mail.port=25
spring.mail.username=xxx@qq.com
spring.mail.password=xxx
spring.mail.properties.mail.smtp.auth=true
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
spring.mail.properties.mail.smtp.socketFactory.class=javax.net.ssl.SSLSocketFactory
### 调度中心通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=default_token
### 调度中心国际化配置 [必填]: 默认为 "zh_CN"/中文简体, 可选范围为 "zh_CN"/中文简体, "zh_TC"/中文繁体 and "en"/英文;
xxl.job.i18n=zh_CN
## 调度线程池最大线程配置【必填】
xxl.job.triggerpool.fast.max=200
xxl.job.triggerpool.slow.max=100
### 调度中心日志表数据保存天数 [必填]:过期日志自动清理;限制大于等于7时生效,否则, 如-1,关闭自动清理功能;
xxl.job.logretentiondays=10

datasource配置连接到我们刚才创建的数据库
mail配置报警邮箱
accessToken(重要)配置后,执行的任务也需要配置相同的accessToken

运行启动类(或者打成jar包运行)后,可在浏览器中访问到管理平台

2. 注册定时任务

新建一个Springboot项目,并添加依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency><!-- 官网的demo是2.2.1,中央maven仓库还没有,所以就用2.2.0 -->
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.2.0</version>
</dependency>
</dependencies>

配置application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# web port
server.port=8081
# log config
logging.config=classpath:logback.xml
spring.application.name=xxljob-demo
### 调度中心部署跟地址 [选填]:如调度中心集群部署存在多个地址则用逗号分隔。执行器将会使用该地址进行"执行器心跳注册"和"任务结果回调";为空则关闭自动注册;
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
### 执行器通讯TOKEN [选填]:非空时启用;
xxl.job.accessToken=default_token
### 执行器AppName [选填]:执行器心跳注册分组依据;为空则关闭自动注册
xxl.job.executor.appname=xxl-job-demo
### 执行器注册 [选填]:优先使用该配置作为注册地址,为空时使用内嵌服务 ”IP:PORT“ 作为注册地址。从而更灵活的支持容器类型执行器动态IP和动态映射端口问题。
xxl.job.executor.address=
### 执行器IP [选填]:默认为空表示自动获取IP,多网卡时可手动设置指定IP,该IP不会绑定Host仅作为通讯实用;地址信息用于 "执行器注册" 和 "调度中心请求并触发任务";
xxl.job.executor.ip=127.0.0.1
### 执行器端口号 [选填]:小于等于0则自动获取;默认端口为9999,单机部署多个执行器时,注意要配置不同执行器端口;
xxl.job.executor.port=9999
### 执行器运行日志文件存储磁盘路径 [选填] :需要对该路径拥有读写权限;为空则使用默认路径;
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
### 执行器日志文件保存天数 [选填] : 过期日志自动清理, 限制值大于等于3时生效; 否则, 如-1, 关闭自动清理功能;
xxl.job.executor.logretentiondays=10

注意这里的accessToken要与前面admin配置的accessToken保持一致

在resources目录下,配置日志输出logback.xml:

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
<?xml version="1.0" encoding="UTF-8"?>  
<configuration debug="false" scan="true" scanPeriod="1 seconds">

<contextName>logback</contextName>
<property name="log.path" value="/data/applogs/xxl-job/xxl-job-executor-sample-springboot.log"/>

<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %contextName [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>

<appender name="file" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}.%d{yyyy-MM-dd}.zip</fileNamePattern>
</rollingPolicy>
<encoder>
<pattern>%date %level [%thread] %logger{36} [%file : %line] %msg%n
</pattern>
</encoder>
</appender>

<root level="info">
<appender-ref ref="console"/>
<appender-ref ref="file"/>
</root>

</configuration>

编写一个配置类,实例化一个XxlJobSpringExecutor类的Bean:

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
package com.example.xxljobdemo.config;  

import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class XxlJobConfig {
private Logger logger = LoggerFactory.getLogger(XxlJobConfig.class);
@Value("${xxl.job.admin.addresses}")
private String adminAddresses;
@Value("${xxl.job.accessToken}")
private String accessToken;
@Value("${xxl.job.executor.appname}")
private String appname;
@Value("${xxl.job.executor.address}")
private String address;
@Value("${xxl.job.executor.ip}")
private String ip;
@Value("${xxl.job.executor.port}")
private int port;
@Value("${xxl.job.executor.logpath}")
private String logPath;
@Value("${xxl.job.executor.logretentiondays}")
private int logRetentionDays;

@Bean
public XxlJobSpringExecutor xxlJobExecutor() {
logger.info(">>>>>>>>>>> xxl-job config init.");
XxlJobSpringExecutor xxlJobSpringExecutor = new XxlJobSpringExecutor();
xxlJobSpringExecutor.setAdminAddresses(adminAddresses);
xxlJobSpringExecutor.setAppname(appname);
xxlJobSpringExecutor.setAddress(address);
xxlJobSpringExecutor.setIp(ip);
xxlJobSpringExecutor.setPort(port);
xxlJobSpringExecutor.setAccessToken(accessToken);
xxlJobSpringExecutor.setLogPath(logPath);
xxlJobSpringExecutor.setLogRetentionDays(logRetentionDays);
return xxlJobSpringExecutor;
}
}

在管理平台注册执行器:

image.png

AppName为配置文件中的xxl.job.executor.appname
手动录入才需要填写机器地址一栏
IP和端口号分别为配置文件中的:xxl.job.executor.ip 和 xxl.job.executor.port

编写一个任务类,使用Bean模式,也就是在任务对应的方法上添加@XxlJob注解,自定义JobHandler的名称

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.xxljobdemo.jobhandler;  

import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.context.XxlJobHelper;
import com.xxl.job.core.handler.annotation.XxlJob;
import org.springframework.stereotype.Component;

@Component
public class XxlJobDemoHandler {
/**
* Bean模式,一个方法为一个任务
* 1、在Spring Bean实例中,开发Job方法,方式格式要求为 "public ReturnT<String> execute(String param)"
* 2、为Job方法添加注解 "@XxlJob(value="自定义jobhandler名称", init = "JobHandler初始化方法", destroy = "JobHandler销毁方法")",注解value值对应的是调度中心新建任务的JobHandler属性的值。
* 3、执行日志:需要通过 "XxlJobLogger.log" 打印执行日志;
*/
@XxlJob("demoJobHandler")
public ReturnT<String> demoJobHandler(String param) throws Exception {
XxlJobHelper.log("java, Hello World~~~");
XxlJobHelper.log("param:" + param);
return ReturnT.SUCCESS;
}
}

在管理平台新建任务:

image.png

JobHandler为上述代码@XxlJob注解中的值

随后运行该Springboot应用,可在管理平台执行一次或启动任务

image.png