前言

  定时器是我们项目中经常会用到的,SpringBoot使用@Scheduled注解可以快速启用一个简单的定时器(详情请看我们之前的博客《SpringBoot系列——定时器》),然而这种方式的定时器缺乏灵活性,如果需要对定时器进行调整,需要重启项目才生效,本文记录SpringBoot如何灵活配置动态定时任务

  代码编写

  首先先建表,重要字段:唯一表id、Runnable任务类、Cron表达式,其他的都是一些额外补充字段

DROP TABLE IF EXISTS `tb_task`;
CREATE TABLE `tb_task` (
`task_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '定时任务id',
`task_name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定时任务名称',
`task_desc` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定时任务描述',
`task_exp` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定时任务Cron表达式',
`task_status` int(1) NULL DEFAULT NULL COMMENT '定时任务状态,0停用 1启用',
`task_class` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '定时任务的Runnable任务类完整路径',
`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`task_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '动态定时任务表' ROW_FORMAT = Compact; INSERT INTO `tb_task` VALUES ('1', 'task1', '测试动态定时任务1', '0/5 * * * * ?', 0, 'cn.huanzi.qch.springboottimer.task.MyRunnable1', '2021-08-06 17:39:23', '2021-08-06 17:39:25');
INSERT INTO `tb_task` VALUES ('2', 'task2', '测试动态定时任务2', '0/2 * * * * ?', 0, 'cn.huanzi.qch.springboottimer.task.MyRunnable2', '2021-08-06 17:39:23', '2021-08-06 17:39:25');

  项目引入jpa、数据库驱动,用于数据库操作

        <!--添加springdata-jpa依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency> <!--添加MySQL驱动依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

  数据库相关配置文件

spring:
datasource: #数据库相关
url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
mvc:
date-format: yyyy-MM-dd HH:mm:ss #mvc接收参数时对日期进行格式化 jackson:
date-format: yyyy-MM-dd HH:mm:ss #jackson对响应回去的日期参数进行格式化
time-zone: GMT+8
jpa:
show-sql: true

  entity实体与数据表映射,以及与之对应的repository

/**
* 动态定时任务表
* 重要属性:唯一表id、Runnable任务类、Cron表达式,
* 其他的都是一些额外补充说明属性
*/
@Entity
@Table(name = "tb_task")
@Data
public class TbTask {
@Id
private String taskId;//定时任务id
private String taskName;//定时任务名称
private String taskDesc;//定时任务描述
private String taskExp;//定时任务Cron表达式
private Integer taskStatus;//定时任务状态,0停用 1启用
private String taskClass;//定时任务的Runnable任务类完整路径
private Date updateTime;//更新时间
private Date createTime;//创建时间
}
/**
* TbTask动态定时任务Repository
*/
@Repository
public interface TbTaskRepository extends JpaRepository<TbTask,String>, JpaSpecificationExecutor<TbTask> {
}

  测试动态定时器的配置类,主要作用:初始化线程池任务调度、读取/更新数据库任务、启动/停止定时器等

/**
* 测试定时器2-动态定时器
*/
@Slf4j
@Component
public class TestScheduler2 { //数据库的任务
public static ConcurrentHashMap<String, TbTask> tasks = new ConcurrentHashMap<>(10); //正在运行的任务
public static ConcurrentHashMap<String,ScheduledFuture> runTasks = new ConcurrentHashMap<>(10); //线程池任务调度
private ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler(); @Autowired
private TbTaskRepository tbTaskRepository; /**
* 初始化线程池任务调度
*/
@Autowired
public TestScheduler2(){
this.threadPoolTaskScheduler.setPoolSize(10);
this.threadPoolTaskScheduler.setThreadNamePrefix("task-thread-");
this.threadPoolTaskScheduler.setWaitForTasksToCompleteOnShutdown(true);
this.threadPoolTaskScheduler.initialize();
} /**
* 获取所有数据库里的定时任务
*/
private void getAllTbTask(){
//查询所有,并put到tasks
TestScheduler2.tasks.clear();
List<TbTask> list = tbTaskRepository.findAll();
list.forEach((task)-> TestScheduler2.tasks.put(task.getTaskId(),task));
} /**
* 根据定时任务id,启动定时任务
*/
void start(String taskId){
try {
//如果为空,重新获取
if(TestScheduler2.tasks.size() <= 0){
this.getAllTbTask();
}
TbTask tbTask = TestScheduler2.tasks.get(taskId); //获取并实例化Runnable任务类
Class<?> clazz = Class.forName(tbTask.getTaskClass());
Runnable runnable = (Runnable)clazz.newInstance(); //Cron表达式
CronTrigger cron = new CronTrigger(tbTask.getTaskExp()); //执行,并put到runTasks
TestScheduler2.runTasks.put(taskId, Objects.requireNonNull(this.threadPoolTaskScheduler.schedule(runnable, cron))); this.updateTaskStatus(taskId,1); log.info("{},任务启动!",taskId);
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
log.error("{},任务启动失败...",taskId);
e.printStackTrace();
} } /**
* 根据定时任务id,停止定时任务
*/
void stop(String taskId){
TestScheduler2.runTasks.get(taskId).cancel(true); TestScheduler2.runTasks.remove(taskId); this.updateTaskStatus(taskId,0); log.info("{},任务停止...",taskId);
} /**
* 更新数据库动态定时任务状态
*/
private void updateTaskStatus(String taskId,int status){
TbTask task = tbTaskRepository.getOne(taskId);
task.setTaskStatus(status);
task.setUpdateTime(new Date());
tbTaskRepository.save(task);
}
}

  

  接下来就是编写测试接口、测试Runnable类(3个Runnable类,这里就不贴那么多了,就贴个MyRunnable1)

/**
* Runnable任务类1
*/
@Slf4j
public class MyRunnable1 implements Runnable {
@Override
public void run() {
log.info("MyRunnable1 {}",new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
}

  Controller接口

/**
* 动态定时任务Controller测试
*/
@RestController
@RequestMapping("/tbTask/")
public class TbTaskController { @Autowired
private TestScheduler2 testScheduler2; @Autowired
private TbTaskRepository tbTaskRepository; /**
* 启动一个动态定时任务
* http://localhost:10085/tbTask/start/2
*/
@RequestMapping("start/{taskId}")
public String start(@PathVariable("taskId") String taskId){
testScheduler2.start(taskId);
return "操作成功";
} /**
* 停止一个动态定时任务
* http://localhost:10085/tbTask/stop/2
*/
@RequestMapping("stop/{taskId}")
public String stop(@PathVariable("taskId") String taskId){
testScheduler2.stop(taskId);
return "操作成功";
} /**
* 更新一个动态定时任务
* http://localhost:10085/tbTask/save?taskId=2&taskExp=0/2 * * * * ?&taskClass=cn.huanzi.qch.springboottimer.task.MyRunnable3
*/
@RequestMapping("save")
public String save(TbTask task) throws IllegalAccessException {
//先更新表数据
TbTask tbTask = tbTaskRepository.getOne(task.getTaskId()); //null值忽略
List<String> ignoreProperties = new ArrayList<>(7); //反射获取Class的属性(Field表示类中的成员变量)
for (Field field : task.getClass().getDeclaredFields()) {
//获取授权
field.setAccessible(true);
//属性名称
String fieldName = field.getName();
//属性的值
Object fieldValue = field.get(task); //找出值为空的属性,我们复制的时候不进行赋值
if(null == fieldValue){
ignoreProperties.add(fieldName);
}
} //org.springframework.beans BeanUtils.copyProperties(A,B):A中的值付给B
BeanUtils.copyProperties(task, tbTask,ignoreProperties.toArray(new String[0]));
tbTaskRepository.save(tbTask);
TestScheduler2.tasks.clear(); //停止旧任务
testScheduler2.stop(tbTask.getTaskId()); //重新启动
testScheduler2.start(tbTask.getTaskId());
return "操作成功";
}
}

  效果演示

  启动

  启动一个定时任务,http://localhost:10085/tbTask/start/2

  可以看到,id为2的定时任务已经被启动,corn表达式为5秒执行一次,runnable任务为MyRunnable2

  修改

  修改一个定时任务,http://localhost:10085/tbTask/save?taskId=2&taskExp=0/2 * * * * ?&taskClass=cn.huanzi.qch.springboottimer.task.MyRunnable3

  调用修改后,数据库信息被修改,id为2的旧任务被停止重新启用新任务,corn表达式为2秒执行一次,runnable任务类为MyRunnable3

  停止

  停止一个定时任务,http://localhost:10085/tbTask/stop/2

  id为2的定时任务被停止

  后记

  可以看到,配置动态定时任务后,可以方便、实时的对定时任务进行修改、调整,再也不用重启项目啦

  SpringBoot配置动态定时任务暂时先记录到这,后续再进行补充

  代码开源

  代码已经开源、托管到我的GitHub、码云:

  GitHub:https://github.com/huanzi-qch/springBoot

  码云:https://gitee.com/huanzi-qch/springBoot

SpringBoot系列——动态定时任务的更多相关文章

  1. 【记录】【springboot】动态定时任务ScheduledFuture,可添加、修改、删除

    这里只演示添加和删除任务的,因为修改就是删除任务再添加而已. 方便演示,任务就是每3秒打印 1.没有任务 后台 2.添加一个任务 3.再添加一个任务 4.删除一个任务 5.再添加一个任务 6.代码 运 ...

  2. springboot动态定时任务

    SpringBoot设置动态定时任务 一.说明 1.在我们日常的开发中,很多时候,定时任务都不是写死的,而是写到数据库中,从而实现定时任务的动态配置. 2.定时任务执行时间可根据数据库中某个设置字段动 ...

  3. SpringBoot中并发定时任务的实现、动态定时任务的实现(看这一篇就够了)

    原创不易,如需转载,请注明出处https://www.cnblogs.com/baixianlong/p/10659045.html,否则将追究法律责任!!! 一.在JAVA开发领域,目前可以通过以下 ...

  4. springboot和quartz整合实现动态定时任务(持久化单节点)

    Quartz是一个完全由java编写的开源作业调度框架,为在Java应用程序中进行作业调度提供了简单却强大的机制,它支持定时任务持久化到数据库,从而避免了重启服务器时任务丢失,支持分布式多节点,大大的 ...

  5. SpringBoot系列——Spring-Data-JPA

    前言 jpa是ORM映射框架,更多详情,请戳:apring-data-jpa官网:http://spring.io/projects/spring-data-jpa,以及一篇优秀的博客:https:/ ...

  6. SpringBoot系列之profles配置多环境(篇二)

    SpringBoot系列之profles配置多环境(篇二) 继续上篇博客SpringBoot系列之profles配置多环境(篇一)之后,继续写一篇博客进行补充 写Spring项目时,在测试环境是一套数 ...

  7. SpringBoot系列(九)单,多文件上传的正确姿势

    SpringBoot系列(九)分分钟解决文件上传 往期推荐 SpringBoot系列(一)idea新建Springboot项目 SpringBoot系列(二)入门知识 springBoot系列(三)配 ...

  8. SpringBoot系列之从入门到精通系列教程

    对应SpringBoot系列博客专栏,例子代码,本博客不定时更新 Spring框架:作为JavaEE框架领域的一款重要的开源框架,在企业应用开发中有着很重要的作用,同时Spring框架及其子框架很多, ...

  9. C#进阶系列——动态Lamada(二:优化)

    前言:前几天写了一篇动态Lamada的文章C#进阶系列——动态Lamada,受园友xiao99的启发,今天打算来重新优化下这个动态Lamada的工具类.在此做个笔记,以免以后忘了. 一.原理分析 上篇 ...

随机推荐

  1. Centos7 unzip文件名中文乱码

    Centos7 unzip文件名中文乱码 前言 今天在批量处理windos文件时为了方便操作,将windos下面的文件夹打成zip包上传至centos7中解压处理,发现解压后中文文件名变成了乱码,如下 ...

  2. 高性能的Redis之对象底层实现原理详解

    对象 在前面的数个章节里, 我们陆续介绍了 Redis 用到的所有主要数据结构, 比如简单动态字符串(SDS).双端链表.字典.压缩列表.整数集合, 等等. Redis 并没有直接使用这些数据结构来实 ...

  3. Go语言中slice作为参数传递时遇到的一些“坑”

    前言 相信看到这个题目,可能大家都觉得是一个老生常谈的月经topic了.一直以来其实把握一个"值传递"基本上就能理解各种情况了,不过最近遇到了更深一点的"小坑" ...

  4. .net core AES加密解密及RSA 签名验签

    引用 using Org.BouncyCastle.Crypto.Parameters; using Org.BouncyCastle.Security; using System; using Sy ...

  5. Linux中date的用法

    一.命令格式:date [参数]... [+格式]二.命令功能:date 可以用来显示或设定系统的日期与时间.三.命令格式:%H 小时(以00-23来表示). %I 小时(以01-12来表示). %K ...

  6. 2300+字!在不同系统上安装Docker!看这一篇文章就够了

    辰哥准备出一期在Docker跑Python项目的技术文,比如在Docker跑Django或者Flask的网站.跑爬虫程序等等. 在Docker跑Python程序的时候不会太过于细去讲解Docker的基 ...

  7. ActiveMq 之JMS 看这一篇就够了

    什么是JMS MQ 全称:Java MessageService 中文:Java 消息服务. JMS 是 Java 的一套 API 标准,最初的目的是为了使应用程序能够访问现有的 MOM 系 统(MO ...

  8. 并发王者课-铂金10:能工巧匠-ThreadLocal如何为线程打造私有数据空间

    欢迎来到<并发王者课>,本文是该系列文章中的第23篇,铂金中的第10篇. 说起ThreadLocal,相信你对它的名字一定不陌生.在并发编程中,它有着较高的出场率,并且也是面试中的高频面试 ...

  9. Leetcode547 朋友圈解题报告 (DFS

    题目描述: 班上有 N 名学生.其中有些人是朋友,有些则不是.他们的友谊具有是传递性.如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友.所谓的朋友圈,是指所有朋 ...

  10. Robotframework学习笔记之一Common Resource导入的Library库显示红色(导入失败)

    第一次使用Robotframework,所以也遇到了很多的坑,导入项目后 ,一些自带的库显示红色,导入失败!(ps:自带的库也显示红色) Ride日志如下(Tools--view ride log): ...