代码地址:https://github.com/gwy572294624/schedule-demo
git clone https://github.com/gwy572294624/schedule-demo
springboot项目 ,执行sql后修改数据库连接地址即可运行部署测试。
动态配置定时器
实现逻辑(文章开始前需要了解)
文章开始前先思考实现动态定时器需要什么
- 既然是动态的那就需要有地方存储动态的定时器任务(执行周期,执行的类方法信息)那就使用到了数据库
- 定时器动态添加之后有些任务因为业务逻辑需要停用或者更改执行机制,动态定时器需要支持可以增,删,改,停用,启用
- 每次项目启动重新加载库中的定时器任务
- 在程序中触发定时器是需要重开线程去执行(线程池)
- 每次执行的逻辑与结果怎么查看(存储数据库,或者查看程序运行日志)
程序中实现就是
- 项目启动初始化加载动态定时器信息或者(给程序中加入定时任务)
- 到点触发通过线程执行业务逻辑
代码实现
项目结构
数据库表信息
# 定时任务表
CREATE TABLE `tb_schedule_job` (
`job_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务id',
`bean_name` varchar(200) NOT NULL DEFAULT '' COMMENT 'spring bean名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名',
`params` varchar(200) NOT NULL DEFAULT '' COMMENT '参数',
`cron_expression` varchar(100) NOT NULL DEFAULT '' COMMENT 'cron表达式',
`yn` enum('y','n') NOT NULL DEFAULT 'y',
`remark` varchar(255) NOT NULL DEFAULT '' COMMENT '备注',
`ctime` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间',
`utime` bigint(20) NOT NULL DEFAULT '0' COMMENT '修改时间',
PRIMARY KEY (`job_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='定时任务';
INSERT INTO `tb_schedule_job` VALUES (1, 'testDynamicTask', 'test', '这是一个参数', '1 * * * * ? ', 'y', '', 0, 0);
COMMIT;
# 定时任务执行日志表
CREATE TABLE `tb_schedule_job_log` (
`log_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '任务日志id',
`job_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '任务id',
`bean_name` varchar(200) NOT NULL DEFAULT '' COMMENT 'spring bean名称',
`method_name` varchar(100) NOT NULL DEFAULT '' COMMENT '方法名',
`params` varchar(200) NOT NULL DEFAULT '' COMMENT '参数',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '任务状态 0:成功 1:失败',
`error` varchar(200) NOT NULL DEFAULT '' COMMENT '失败信息',
`times` int(11) NOT NULL DEFAULT '0' COMMENT '耗时(单位:毫秒)',
`ctime` bigint(20) NOT NULL DEFAULT '0' COMMENT '创建时间',
`yn` enum('y','n') NOT NULL DEFAULT 'y',
`utime` bigint(20) NOT NULL DEFAULT '0' COMMENT '修改时间',
PRIMARY KEY (`log_id`),
KEY `job_id` (`job_id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8 COMMENT='定时任务日志';
pom
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--定时器配置jar-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.68</version>
</dependency>
主要工具类
该类为程序中定时器增删改查类(主类)
package com.schedule.util.quartz;
import com.schedule.dao.ScheduleJobDO;
import org.quartz.*;
/**
* 定时任务工具类
* 该类为程序中定时器增删改查类
*
*/
public class ScheduleUtils {
private final static String JOB_NAME = "TASK_";
/**
* 获取触发器key
*/
public static TriggerKey getTriggerKey(Long jobId) {
return TriggerKey.triggerKey(JOB_NAME + jobId);
}
/**
* 获取jobKey
*/
public static JobKey getJobKey(Long jobId) {
return JobKey.jobKey(JOB_NAME + jobId);
}
/**
* 获取表达式触发器
*/
public static CronTrigger getCronTrigger(Scheduler scheduler, Long jobId) throws SchedulerException {
return (CronTrigger) scheduler.getTrigger(getTriggerKey(jobId));
}
/**
* 创建定时任务
*/
public static void createScheduleJob(Scheduler scheduler, ScheduleJobDO scheduleJob) {
try {
//构建job信息
JobDetail jobDetail = JobBuilder.newJob(ScheduleJobUtils.class).withIdentity(getJobKey(scheduleJob.getJobId())).build();
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
//按新的cronExpression表达式构建一个新的trigger
CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(getTriggerKey(scheduleJob.getJobId())).withSchedule(scheduleBuilder).build();
//放入参数,运行时的方法可以获取
jobDetail.getJobDataMap().put("JOB_KEY", scheduleJob);
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 更新定时任务
*/
public static void updateScheduleJob(Scheduler scheduler, ScheduleJobDO scheduleJob) {
try {
TriggerKey triggerKey = getTriggerKey(scheduleJob.getJobId());
//表达式调度构建器
CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpression())
.withMisfireHandlingInstructionDoNothing();
CronTrigger trigger = getCronTrigger(scheduler, scheduleJob.getJobId());
//按新的cronExpression表达式重新构建trigger
trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).build();
//参数
trigger.getJobDataMap().put("JOB_KEY", scheduleJob);
scheduler.rescheduleJob(triggerKey, trigger);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 立即执行任务
*/
public static void run(Scheduler scheduler, ScheduleJobDO scheduleJob) {
try {
//参数
JobDataMap dataMap = new JobDataMap();
dataMap.put("JOB_KEY", scheduleJob);
scheduler.triggerJob(getJobKey(scheduleJob.getJobId()), dataMap);
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 暂停任务
*/
public static void pauseJob(Scheduler scheduler, Long jobId) {
try {
scheduler.pauseJob(getJobKey(jobId));
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 恢复任务
*/
public static void resumeJob(Scheduler scheduler, Long jobId) {
try {
scheduler.resumeJob(getJobKey(jobId));
} catch (SchedulerException e) {
e.printStackTrace();
}
}
/**
* 删除定时任务
*/
public static void deleteScheduleJob(Scheduler scheduler, Long jobId) {
try {
scheduler.deleteJob(getJobKey(jobId));
} catch (SchedulerException e) {
e.printStackTrace();
}
}
}
每次触发定时器时开启的方法(动态定时器触发类)
package com.schedule.util.quartz;
import com.schedule.config.ThreadPoolCache;
import com.schedule.dao.ScheduleJobDO;
import com.schedule.dao.ScheduleJobLogDO;
import com.schedule.mapper.ScheduleJobLogMapper;
import lombok.extern.slf4j.Slf4j;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;
import org.springframework.stereotype.Component;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/**
* 定时任务工具
* 该类为执行方法,每次触发定时器时执行的方法
*
*/
@Slf4j
@Component
public class ScheduleJobUtils extends QuartzJobBean {
@Autowired
private ScheduleJobLogMapper scheduleJobLogMapper;
@Override
protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
ScheduleJobDO scheduleJob = (ScheduleJobDO ) context.getMergedJobDataMap()
.get("JOB_KEY");
//获取spring bean
// ScheduleJobLogService scheduleJobLogService = (ScheduleJobLogService) SpringContextUtils.getBean("scheduleJobLogService");
long startTime = System.currentTimeMillis();
//数据库保存执行记录
ScheduleJobLogDO jobLog = new ScheduleJobLogDO(scheduleJob,startTime);
//任务开始时间
Byte zero = 0;
Byte one=1;
try {
//执行任务
log.info("任务准备执行,任务ID:" + scheduleJob.getJobId());
ScheduleRunnable task = new ScheduleRunnable(scheduleJob.getBeanName(),
scheduleJob.getMethodName(), scheduleJob.getParams());
Future<?> future = ThreadPoolCache.InitThreadPoolCache.executorService.submit(task);
future.get();
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
jobLog.setTimes((int)times);
//任务状态 0:成功 1:失败
jobLog.setStatus(zero);
log.info("任务执行完毕,任务ID:" + scheduleJob.getJobId() + " 总共耗时:" + times + "毫秒");
} catch (Exception e) {
log.error("任务执行失败,任务ID:" + scheduleJob.getJobId(), e);
//任务执行总时长
long times = System.currentTimeMillis() - startTime;
jobLog.setTimes((int)times);
//任务状态 0:成功 1:失败
jobLog.setStatus(one);
jobLog.setError(e.toString().substring( 0, 199));
}finally {
scheduleJobLogMapper.insert(jobLog);
}
}
}
该类为构建线程执行任务(构建动态线程执行任务)
package com.schedule.util.quartz;
import com.schedule.util.SpringContextUtil;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
/**
* 执行定时任务实现
* 该类为构建动态线程执行任务。
*
*/
public class ScheduleRunnable implements Runnable {
private Object target;
private Method method;
private String params;
public ScheduleRunnable(String beanName, String methodName, String params) throws NoSuchMethodException, SecurityException {
this.target = SpringContextUtil.getBean(beanName);
this.params = params;
if(!ObjectUtils.isEmpty(params)){
this.method = target.getClass().getDeclaredMethod(methodName, String.class);
}else{
this.method = target.getClass().getDeclaredMethod(methodName);
}
}
@Override
public void run() {
try {
ReflectionUtils.makeAccessible(method);
if(!ObjectUtils.isEmpty(params)){
method.invoke(target, params);
}else{
method.invoke(target);
}
}catch (Exception e) {
e.printStackTrace();
}
}
}
项目启动加载数据库中的定时方法到程序中
package com.schedule.config;
import com.schedule.dao.ScheduleJobDO;
import com.schedule.service.ScheduleJobService;
import com.schedule.util.quartz.ScheduleUtils;
import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import java.util.List;
/**
* @Author
* @Date 2021/2/5
* @Version 1.0
* @Remarks 项目启动将动态定时器加载到系统中
*/
@Component
public class DynamicTaskInit implements ApplicationRunner {
@Autowired
@Qualifier("scheduleJobServiceImpl")
private ScheduleJobService scheduleJobService;
@Autowired
private Scheduler scheduler;
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("加载动态定时器");
List<ScheduleJobDO> scheduleJobDOList = scheduleJobService.getAll();
if (!ObjectUtils.isEmpty(scheduleJobDOList)){
for (ScheduleJobDO thisJob:scheduleJobDOList) {
ScheduleUtils.createScheduleJob(scheduler, thisJob);
}
}
}
}
动态定时器执行的类(动态定时器触发执行的类,类名与方法名参数信息都为动态)(定时器触发的模版类信息)(业务逻辑)
Bean名称默认为首字母小写的类名 如此类bean名称为(testDynamicTask)
package com.schedule.timer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
/**
* @Author
* @Date 2021/2/5 下午3:31
* @Version 1.0
* @Remarks 动态定时器执行的类
*/
@Slf4j
@Component
public class TestDynamicTask {
//测试方法
public void test(String params){
log.info(params);
System.out.println(System.currentTimeMillis());
}
//测试方法2
public void test2(String params){
log.info(params);
System.out.println(System.currentTimeMillis());
}
}
测试执行
controller
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
@Qualifier("scheduleJobServiceImpl")
private ScheduleJobService scheduleJobService;
@RequestMapping("/getall")
public String getAll(String name){
return JSON.toJSONString(scheduleJobService.getAll());
}
@RequestMapping("/add")
public String addJob(String methodName,String params,String cronExpression){
ScheduleJobDO scheduleJobDO=new ScheduleJobDO(methodName,params,cronExpression);
return scheduleJobService.addJob(scheduleJobDO);
}
@RequestMapping("/update")
public String updateJob(Long jobId,String methodName,String params,String cronExpression){
ScheduleJobDO scheduleJobDO=new ScheduleJobDO(methodName,params,cronExpression);
scheduleJobDO.setJobId(jobId);
return scheduleJobService.updateJob(scheduleJobDO);
}
@RequestMapping("/delete")
public String deleteJob(Long jobId){
return scheduleJobService.deleteJob(jobId);
}
}
service
@Service
public class ScheduleJobServiceImpl implements ScheduleJobService {
@Autowired
private ScheduleJobMapper scheduleJobMapper;
@Autowired
private Scheduler scheduler;
@Override
public List<ScheduleJobDO> getAll(){
return scheduleJobMapper.select(new ScheduleJobDO());
}
@Override
public String addJob(ScheduleJobDO scheduleJobDO) {
scheduleJobMapper.insert(scheduleJobDO);
ScheduleUtils.createScheduleJob(scheduler,scheduleJobDO);
return "add执行成功 !!";
}
@Override
public String updateJob(ScheduleJobDO scheduleJobDO) {
scheduleJobMapper.updateByPK(scheduleJobDO);
ScheduleUtils.updateScheduleJob(scheduler,scheduleJobDO);
return "update执行成功 !!";
}
@Override
public String deleteJob(Long jobId) {
scheduleJobMapper.deleteByPK(jobId);
ScheduleUtils.deleteScheduleJob(scheduler,jobId);
return "delete执行成功 !!";
}
}
add发送参数
127.0.0.1:8080/test/add?methodName=test2¶ms=测试新增定时任务&cronExpression=0 0/2 * * * ? *
执行结果
update发送参数
127.0.0.1:8080/test/update?methodName=test2¶ms=测试修改定时任务,每秒执行&cronExpression=* * * * * ? * &jobId=2
执行结果
delete发送参数
127.0.0.1:8080/test/delete?jobId=2
执行结果
停用与启用对我这边业务没有交互,就不演示了,有需要的可以自己尝试,
扩展
执行线程建议使用cacheThreadPool,因为定时器触发频率很高会导致新创建的线程占用大量内存和cpu无法及时释放。
测试过程中查看控制台日志ScheduleJobUtils类重写的executeInternal方法,默认是10个线程,使用完后会复用(类似线程池)。重写方法中的submit方法执行为自己提供线程池,建议使用cacheThreadPool。
文章中使用的是cacheThreadPool可参考(com.schedule.config.ThreadPoolCache)。
感谢https://blog.csdn.net/belonghuang157405/article/details/85605203
以上 ,希望本文可以帮到你。