前言

  定時器是我們專案中經常會用到的,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