• Springboot定时任务热部署设计及实现


    1. 需求来源

    业务上需要将定时器的执行周期进行配置(新增、编辑、删除),并支持热部署(直接生效,无需重启)

    2. 设计

    2.1 表结构(postgresql)

    1. drop table if exists t_scheduler;
    2. create table t_scheduler (
    3. id int4 not null,
    4. task_name varchar(32) not null,
    5. cron varchar(32) not null,
    6. delete int2 not null default 0,
    7. constraint t_scheduler_pkey primary key ("id")
    8. );
    9. comment on column t_scheduler.id is '任务Id';
    10. comment on column t_scheduler.task_name is '任务名称';
    11. comment on column t_scheduler.cron is '任务执行周期';
    12. comment on column t_scheduler.delete is '任务是否删除0未删除1已删除';
    1. -- 任务数据
    2. insert into t_scheduler values(1, 'SyncUser', '0/10 * * * * ?', 0);
    3. insert into t_scheduler values(2, 'WeatherTask', '0/10 * * * * ?', 0);

    2.2 程序设计

    主配置类

    里面包含一个定时任务,扫描上述任务配置表,检查有没有更新

    任务接口(该接口继承Runnable)

    任务定义接口,自定义的任务,均实现该接口

    任务对象工厂

    用来通过配置的任务名,反射获取任务对象

    任务Dao

    包含任务查询等

    3. 代码

    3.1 主配置类

    1. package com.hz.basepro.schedule.start;
    2. import com.hz.basepro.bean.ScheduleTask;
    3. import com.hz.basepro.schedule.dao.businessmapper.ScheduleMapper;
    4. import com.hz.basepro.schedule.task.TaskHelper;
    5. import org.slf4j.Logger;
    6. import org.slf4j.LoggerFactory;
    7. import org.springframework.beans.factory.annotation.Autowired;
    8. import org.springframework.scheduling.annotation.EnableScheduling;
    9. import org.springframework.scheduling.annotation.Scheduled;
    10. import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
    11. import org.springframework.scheduling.support.CronTrigger;
    12. import org.springframework.stereotype.Component;
    13. import java.util.HashMap;
    14. import java.util.Iterator;
    15. import java.util.List;
    16. import java.util.Map;
    17. import java.util.stream.Collectors;
    18. @Component
    19. @EnableScheduling
    20. public class ScheduleTaskConfig {
    21. private static final Logger LOGGER = LoggerFactory.getLogger(ScheduleTaskConfig.class);
    22. @Autowired
    23. private ScheduleMapper scheduleMapper;
    24. /**
    25. * 任务Map
    26. *
    27. */
    28. private Map<Integer, ScheduleTask> taskMap = new HashMap<>();
    29. /**
    30. * 任务对应的定时器Map
    31. */
    32. private Map<Integer, ThreadPoolTaskScheduler> schedulerMap = new HashMap<>();
    33. @Scheduled(initialDelay = 0, fixedRate = 1 * 1000)
    34. public void checkTask() {
    35. List<ScheduleTask> scheduleTaskList = scheduleMapper.getAllScheduleTask();
    36. for (ScheduleTask scheduleTask : scheduleTaskList) {
    37. if (taskMap.containsKey(scheduleTask.getId())) {
    38. ScheduleTask oldTask = taskMap.get(scheduleTask.getId());
    39. // cron发生改变 1. 移除现有的任务 2. 重新生成新的任务
    40. if (!scheduleTask.getCron().equals(oldTask.getCron())) {
    41. removeTask(oldTask.getId());
    42. addTask(scheduleTask);
    43. }
    44. taskMap.remove(scheduleTask.getId());
    45. } else {
    46. // cron未改变,添加新任务
    47. addTask(scheduleTask);
    48. }
    49. taskMap.put(scheduleTask.getId(), scheduleTask);
    50. }
    51. // 移除已经删除的任务
    52. List<Integer> taskIds = scheduleTaskList.stream().map(task -> task.getId()).collect(Collectors.toList());
    53. for(Iterator<Map.Entry<Integer, ScheduleTask>> it = taskMap.entrySet().iterator();it.hasNext();) {
    54. Map.Entry<Integer, ScheduleTask> entry = it.next();
    55. Integer oldTaskId = entry.getKey();
    56. if (!taskIds.contains(oldTaskId)) {
    57. // 1. 移除任务
    58. removeTask(oldTaskId);
    59. // 2. map移除任务
    60. it.remove();
    61. }
    62. }
    63. }
    64. /**
    65. * 添加定时任务
    66. *
    67. * @param task
    68. */
    69. public void addTask(ScheduleTask task) {
    70. ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
    71. scheduler.setThreadNamePrefix(task.getTaskName());
    72. scheduler.setPoolSize(1);
    73. scheduler.initialize();
    74. scheduler.schedule(TaskHelper.getTask(task.getTaskName()), new CronTrigger(task.getCron()));
    75. schedulerMap.put(task.getId(), scheduler);
    76. LOGGER.info("定时器创建:{}.", task.getId());
    77. }
    78. /**
    79. * 移除定时任务
    80. *
    81. * @param taskId
    82. */
    83. public void removeTask(Integer taskId) {
    84. try {
    85. ThreadPoolTaskScheduler scheduler = schedulerMap.get(taskId);
    86. if (scheduler == null) {
    87. LOGGER.info("定时器不存在:{}.", taskId);
    88. } else {
    89. try {
    90. scheduler.shutdown();
    91. } catch (Exception e) {
    92. LOGGER.error("线程池关闭失败, taskId: {}.", taskId, e);
    93. try {
    94. scheduler.shutdown();
    95. } catch (Exception ex) {
    96. LOGGER.error("线程池再次关闭失败, taskId: {}.", taskId, e);
    97. }
    98. }
    99. }
    100. } finally {
    101. schedulerMap.remove(taskId);
    102. }
    103. LOGGER.info("定时器销毁:{}.", taskId);
    104. }
    105. }

    3.2 任务对象工厂TaskHelper

    1. package com.hz.basepro.schedule.task;
    2. import com.hz.basepro.bean.CommonException;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. public class TaskHelper {
    6. private static final Logger LOGGER = LoggerFactory.getLogger(TaskHelper.class);
    7. private static final String basePackage = "com.hz.basepro.schedule.task.impl.";
    8. public static Task getTask(String taskName) {
    9. try {
    10. Class<?> clazz = Class.forName(basePackage + taskName);
    11. Task task = (Task)clazz.newInstance();
    12. return task;
    13. } catch (Exception e) {
    14. throw new CommonException("找不到对应的任务:" + taskName, e);
    15. }
    16. }
    17. }

    3.3 任务对象

    1. package com.hz.basepro.schedule.task;
    2. public interface Task extends Runnable {
    3. }
    1. package com.hz.basepro.schedule.task.impl;
    2. import com.hz.basepro.schedule.task.Task;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import java.text.SimpleDateFormat;
    6. import java.util.Date;
    7. public class SyncUser implements Task {
    8. private static final Logger LOGGER = LoggerFactory.getLogger(SyncUser.class);
    9. private static final ThreadLocal<SimpleDateFormat> FORMAT_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    10. @Override
    11. public void run() {
    12. SimpleDateFormat sdf = FORMAT_LOCAL.get();
    13. LOGGER.info("同步用户任务进行中:{}", sdf.format(new Date()));
    14. }
    15. }
    1. package com.hz.basepro.schedule.task.impl;
    2. import com.hz.basepro.schedule.task.Task;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import java.text.SimpleDateFormat;
    6. import java.util.Date;
    7. public class WeatherTask implements Task {
    8. private static final Logger LOGGER = LoggerFactory.getLogger(WeatherTask.class);
    9. private static final ThreadLocal<SimpleDateFormat> FORMAT_LOCAL = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
    10. @Override
    11. public void run() {
    12. SimpleDateFormat sdf = FORMAT_LOCAL.get();
    13. LOGGER.info("天气任务进行中:{}", sdf.format(new Date()));
    14. }
    15. }

    3.4 ScheduleMapper

    1. package com.hz.basepro.schedule.dao.businessmapper;
    2. import com.hz.basepro.bean.ScheduleTask;
    3. import java.util.List;
    4. public interface ScheduleMapper {
    5. /**
    6. * 获取所有任务
    7. *
    8. * @return
    9. */
    10. List<ScheduleTask> getAllScheduleTask();
    11. }

    3.5 查询sql

    1. <select id="getAllScheduleTask" resultType="com.hz.basepro.bean.ScheduleTask">
    2. select id, task_name, cron from t_scheduler where delete = 0
    3. </select>

    3.6涉及的Bean

    1. package com.hz.basepro.bean;
    2. public class ScheduleTask {
    3. private int id;
    4. private String taskName;
    5. private String cron;
    6. public int getId() {
    7. return id;
    8. }
    9. public void setId(int id) {
    10. this.id = id;
    11. }
    12. public String getTaskName() {
    13. return taskName;
    14. }
    15. public void setTaskName(String taskName) {
    16. this.taskName = taskName;
    17. }
    18. public String getCron() {
    19. return cron;
    20. }
    21. public void setCron(String cron) {
    22. this.cron = cron;
    23. }
    24. @Override
    25. public String toString() {
    26. return "SchedulerTask{" +
    27. "id=" + id +
    28. ", taskName='" + taskName + '\'' +
    29. ", cron='" + cron + '\'' +
    30. '}';
    31. }
    32. }

    4. 执行结果

    4.1 初始执行结果

    1. 2022-06-29 14:54:23,281 INFO [scheduling-1] (ScheduleTaskConfig.java:99)- 定时器创建:2.
    2. 2022-06-29 14:54:23,283 INFO [scheduling-1] (ScheduleTaskConfig.java:99)- 定时器创建:1.
    3. 2022-06-29 14:54:30,001 INFO [WeatherTask1] (WeatherTask.java:19)- 天气任务进行中:2022-06-29 14:54:30
    4. 2022-06-29 14:54:30,001 INFO [SyncUser1] (SyncUser.java:20)- 同步用户任务进行中:2022-06-29 14:54:30
    5. 2022-06-29 14:54:40,000 INFO [SyncUser1] (SyncUser.java:20)- 同步用户任务进行中:2022-06-29 14:54:40
    6. 2022-06-29 14:54:40,000 INFO [WeatherTask1] (WeatherTask.java:19)- 天气任务进行中:2022-06-29 14:54:40
    7. 2022-06-29 14:54:50,001 INFO [SyncUser1] (SyncUser.java:20)- 同步用户任务进行中:2022-06-29 14:54:50

    4.2 修改任务执行周期

    update t_scheduler set cron = '0/5 * * * * ?' where id = 1;

    4.3 修改后的执行日志

    1. 2022-06-29 14:56:22,629 INFO [scheduling-1] (ScheduleTaskConfig.java:125)- 定时器销毁:1.
    2. 2022-06-29 14:56:22,631 INFO [scheduling-1] (ScheduleTaskConfig.java:99)- 定时器创建:1.
    3. 2022-06-29 14:56:25,001 INFO [SyncUser1] (SyncUser.java:20)- 同步用户任务进行中:2022-06-29 14:56:25
    4. 2022-06-29 14:56:30,000 INFO [WeatherTask1] (WeatherTask.java:19)- 天气任务进行中:2022-06-29 14:56:30
    5. 2022-06-29 14:56:30,000 INFO [SyncUser1] (SyncUser.java:20)- 同步用户任务进行中:2022-06-29 14:56:30
    6. 2022-06-29 14:56:35,001 INFO [SyncUser1] (SyncUser.java:20)- 同步用户任务进行中:2022-06-29 14:56:35
    7. 2022-06-29 14:56:40,001 INFO [SyncUser1] (SyncUser.java:20)- 同步用户任务进行中:2022-06-29 14:56:40
    8. 2022-06-29 14:56:40,001 INFO [WeatherTask1] (WeatherTask.java:19)- 天气任务进行中:2022-06-29 14:56:40

    4.4 结论

           符合期望结果,修改的周期的任务按照期望5s执行一次。未修改的任务仍然是10s执行一次。

  • 相关阅读:
    信息安全服务CCRC认证申报的完整流程
    【AXI4 verilog】手把手带你撸AXI代码(四、AXI4接口的RAM设计)
    uniapp 常见的问题以及解决办法
    Spring IOC源码:registerBeanPostProcessors 详解
    《数据结构、算法与应用C++语言描述》-栈的应用-开关盒布线问题
    Oracle/PLSQL: Least Function
    JavaScript中事件及其详解
    shiro会话管理
    一个快速切换一个底层实现的思路分享
    数组的存储和压缩
  • 原文地址:https://blog.csdn.net/pp_lan/article/details/125521213