前言
好久没更新博客了,最近上班做了点小东西,总结复盘一下
参考资料:
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); } }
|
配置文件指定运行的端口:
编写实现定时任务的类,用@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{ @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 {
@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 cronTrigger = new CronTrigger(cron); Date nextExecutionTime = cronTrigger.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 ; }
}
|
三、分布式定时任务
在分布式的架构中,我们需要一个支持集群、支持监控、支持告警等等功能的解决方案,那么上述方法就比较麻烦了。
主流的分布式任务调度平台包括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的示例如下:
回看项目的根路径下有哪些模块:
- xxl-job-admin:任务调度的管理平台,跑起来后可在浏览器中访问
- xxl-job-core:项目的公共依赖
- 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
| 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
xxl.job.accessToken=default_token
xxl.job.i18n=zh_CN
xxl.job.triggerpool.fast.max=200 xxl.job.triggerpool.slow.max=100
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> <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
| server.port=8081
logging.config=classpath:logback.xml spring.application.name=xxljob-demo
xxl.job.admin.addresses=http://127.0.0.1:8080/xxl-job-admin
xxl.job.accessToken=default_token
xxl.job.executor.appname=xxl-job-demo
xxl.job.executor.address=
xxl.job.executor.ip=127.0.0.1
xxl.job.executor.port=9999
xxl.job.executor.logpath=/data/applogs/xxl-job/jobhandler
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; } }
|
在管理平台注册执行器:
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 {
@XxlJob("demoJobHandler") public ReturnT<String> demoJobHandler(String param) throws Exception { XxlJobHelper.log("java, Hello World~~~"); XxlJobHelper.log("param:" + param); return ReturnT.SUCCESS; } }
|
在管理平台新建任务:
JobHandler为上述代码@XxlJob注解中的值
随后运行该Springboot应用,可在管理平台执行一次或启动任务