SpringBoot系列——動態定時任務

qch發表於2021-08-09

  前言

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

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

  碼雲:https://gitee.com/huanzi-qch/springBoot

相關文章