前言
定時器是我們專案中經常會用到的,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/5 * * * * ?', 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、碼雲: