如何使用 SpringBoot 实现动态增删启停定时任务?

简介: 我是小假 期待与你的下一次相遇 ~

在SpringBoot项目中,可以通过@EnableScheduling注解和@Scheduled注解实现定时任务,也可以通过SchedulingConfigurer接口来实现定时任务。但是这两种方式不能动态添加、删除、启动、停止任务。

要实现动态增删启停定时任务功能,比较广泛的做法是集成Quartz框架。但是在满足项目需求的情况下,尽量少的依赖其它框架,避免项目过于臃肿和复杂。

查看spring-context这个jar包中org.springframework.scheduling.ScheduledTaskRegistrar这个类的源代码,发现可以通过改造这个类就能实现动态增删启停定时任务功能。

添加执行定时任务的线程池配置类

  1. @Configuration  
  2. public class SchedulingConfig {  
  3.    @Bean  
  4.    public TaskScheduler taskScheduler() {  
  5.        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();  
  6.        // 定时任务执行线程池核心线程数  
  7.        taskScheduler.setPoolSize(4);  
  8.        taskScheduler.setRemoveOnCancelPolicy(true);  
  9.        taskScheduler.setThreadNamePrefix("TaskSchedulerThreadPool-");  
  10.        return taskScheduler;  
  11.    }  
  12. }

添加ScheduledFuture的包装类。ScheduledFutureScheduledExecutorService定时任务线程池的执行结果。

  1. public final class ScheduledTask {  
  2.    volatile ScheduledFuture<?> future;  
  3.    /**  
  4.     * 取消定时任务  
  5.     */  
  6.    public void cancel() {  
  7.        ScheduledFuture<?> future = this.future;  
  8.        if (future != null) {  
  9.            future.cancel(true);  
  10.        }  
  11.    }  
  12. }

添加Runnable接口实现类,被定时任务线程池调用,用来执行指定bean里面的方法。

  1. public class SchedulingRunnable implements Runnable {  
  2.    private static final Logger logger = LoggerFactory.getLogger(SchedulingRunnable.class);  
  3.    private String beanName;  
  4.    private String methodName;  
  5.    private String params;  
  6.    public SchedulingRunnable(String beanName, String methodName) {  
  7.        this(beanName, methodName, null);  
  8.    }  
  9.    public SchedulingRunnable(String beanName, String methodName, String params) {  
  10.        this.beanName = beanName;  
  11.        this.methodName = methodName;  
  12.        this.params = params;  
  13.    }  
  14.    @Override  
  15.    public void run() {  
  16.        logger.info("定时任务开始执行 - bean:{},方法:{},参数:{}", beanName, methodName, params);  
  17.        long startTime = System.currentTimeMillis();  
  18.        try {  
  19.            Object target = SpringContextUtils.getBean(beanName);  
  20.            Method method = null;  
  21.            if (StringUtils.isNotEmpty(params)) {  
  22.                method = target.getClass().getDeclaredMethod(methodName, String.class);  
  23.            } else {  
  24.                method = target.getClass().getDeclaredMethod(methodName);  
  25.            }  
  26.            ReflectionUtils.makeAccessible(method);  
  27.            if (StringUtils.isNotEmpty(params)) {  
  28.                method.invoke(target, params);  
  29.            } else {  
  30.                method.invoke(target);  
  31.            }  
  32.        } catch (Exception ex) {  
  33.            logger.error(String.format("定时任务执行异常 - bean:%s,方法:%s,参数:%s ", beanName, methodName, params), ex);  
  34.        }  
  35.        long times = System.currentTimeMillis() - startTime;  
  36.        logger.info("定时任务执行结束 - bean:{},方法:{},参数:{},耗时:{} 毫秒", beanName, methodName, params, times);  
  37.    }  
  38.    @Override  
  39.    public boolean equals(Object o) {  
  40.        if (this == o) return true;  
  41.        if (o == null || getClass() != o.getClass()) return false;  
  42.        SchedulingRunnable that = (SchedulingRunnable) o;  
  43.        if (params == null) {  
  44.            return beanName.equals(that.beanName) &&  
  45.                    methodName.equals(that.methodName) &&  
  46.                    that.params == null;  
  47.        }  
  48.        return beanName.equals(that.beanName) &&  
  49.                methodName.equals(that.methodName) &&  
  50.                params.equals(that.params);  
  51.    }  
  52.    @Override  
  53.    public int hashCode() {  
  54.        if (params == null) {  
  55.            return Objects.hash(beanName, methodName);  
  56.        }  
  57.        return Objects.hash(beanName, methodName, params);  
  58.    }  
  59. }

添加定时任务注册类,用来增加、删除定时任务。

  1. @Component  
  2. public class CronTaskRegistrar implements DisposableBean {  
  3.    private final Map<Runnable, ScheduledTask> scheduledTasks = new ConcurrentHashMap<>(16);  
  4.    @Autowired  
  5.    private TaskScheduler taskScheduler;  
  6.    public TaskScheduler getScheduler() {  
  7.        return this.taskScheduler;  
  8.    }  
  9.    public void addCronTask(Runnable task, String cronExpression) {  
  10.        addCronTask(new CronTask(task, cronExpression));  
  11.    }  
  12.    public void addCronTask(CronTask cronTask) {  
  13.        if (cronTask != null) {  
  14.            Runnable task = cronTask.getRunnable();  
  15.            if (this.scheduledTasks.containsKey(task)) {  
  16.                removeCronTask(task);  
  17.            }  
  18.            this.scheduledTasks.put(task, scheduleCronTask(cronTask));  
  19.        }  
  20.    }  
  21.    public void removeCronTask(Runnable task) {  
  22.        ScheduledTask scheduledTask = this.scheduledTasks.remove(task);  
  23.        if (scheduledTask != null)  
  24.            scheduledTask.cancel();  
  25.    }  
  26.    public ScheduledTask scheduleCronTask(CronTask cronTask) {  
  27.        ScheduledTask scheduledTask = new ScheduledTask();  
  28.        scheduledTask.future = this.taskScheduler.schedule(cronTask.getRunnable(), cronTask.getTrigger());  
  29.        return scheduledTask;  
  30.    }  
  31.    @Override  
  32.    public void destroy() {  
  33.        for (ScheduledTask task : this.scheduledTasks.values()) {  
  34.            task.cancel();  
  35.        }  
  36.        this.scheduledTasks.clear();  
  37.    }  
  38. }

添加定时任务示例类

  1. @Component("demoTask")  
  2. public class DemoTask {  
  3.    public void taskWithParams(String params) {  
  4.        System.out.println("执行有参示例任务:" + params);  
  5.    }  
  6.    public void taskNoParams() {  
  7.        System.out.println("执行无参示例任务");  
  8.    }  
  9. }

定时任务数据库表设计

添加定时任务实体类

  1. public class SysJobPO {  
  2.    /**  
  3.     * 任务ID  
  4.     */  
  5.    private Integer jobId;  
  6.    /**  
  7.     * bean名称  
  8.     */  
  9.    private String beanName;  
  10.    /**  
  11.     * 方法名称  
  12.     */  
  13.    private String methodName;  
  14.    /**  
  15.     * 方法参数  
  16.     */  
  17.    private String methodParams;  
  18.    /**  
  19.     * cron表达式  
  20.     */  
  21.    private String cronExpression;  
  22.    /**  
  23.     * 状态(1正常 0暂停)  
  24.     */  
  25.    private Integer jobStatus;  
  26.    /**  
  27.     * 备注  
  28.     */  
  29.    private String remark;  
  30.    /**  
  31.     * 创建时间  
  32.     */  
  33.    private Date createTime;  
  34.    /**  
  35.     * 更新时间  
  36.     */  
  37.    private Date updateTime;  
  38.    public Integer getJobId() {  
  39.        return jobId;  
  40.    }  
  41.    public void setJobId(Integer jobId) {  
  42.        this.jobId = jobId;  
  43.    }  
  44.    public String getBeanName() {  
  45.        return beanName;  
  46.    }  
  47.    public void setBeanName(String beanName) {  
  48.        this.beanName = beanName;  
  49.    }  
  50.    public String getMethodName() {  
  51.        return methodName;  
  52.    }  
  53.    public void setMethodName(String methodName) {  
  54.        this.methodName = methodName;  
  55.    }  
  56.    public String getMethodParams() {  
  57.        return methodParams;  
  58.    }  
  59.    public void setMethodParams(String methodParams) {  
  60.        this.methodParams = methodParams;  
  61.    }  
  62.    public String getCronExpression() {  
  63.        return cronExpression;  
  64.    }  
  65.    public void setCronExpression(String cronExpression) {  
  66.        this.cronExpression = cronExpression;  
  67.    }  
  68.    public Integer getJobStatus() {  
  69.        return jobStatus;  
  70.    }  
  71.    public void setJobStatus(Integer jobStatus) {  
  72.        this.jobStatus = jobStatus;  
  73.    }  
  74.    public String getRemark() {  
  75.        return remark;  
  76.    }  
  77.    public void setRemark(String remark) {  
  78.        this.remark = remark;  
  79.    }  
  80.    public Date getCreateTime() {  
  81.        return createTime;  
  82.    }  
  83.    public void setCreateTime(Date createTime) {  
  84.        this.createTime = createTime;  
  85.    }  
  86.    public Date getUpdateTime() {  
  87.        return updateTime;  
  88.    }  
  89.    public void setUpdateTime(Date updateTime) {  
  90.        this.updateTime = updateTime;  
  91.    }  
  92. }

新增定时任务

  1. boolean success = sysJobRepository.addSysJob(sysJob);  
  2. if (!success)  
  3.    return OperationResUtils.fail("新增失败");  
  4. else {  
  5.    if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {  
  6.        SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());  
  7.        cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());  
  8.    }  
  9. }  
  10. return OperationResUtils.success();

修改定时任务,先移除原来的任务,再启动新任务

  1. boolean success = sysJobRepository.editSysJob(sysJob);  
  2. if (!success)  
  3.    return OperationResUtils.fail("编辑失败");  
  4. else {  
  5.    //先移除再添加  
  6.    if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {  
  7.        SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());  
  8.        cronTaskRegistrar.removeCronTask(task);  
  9.    }  
  10.    if (sysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {  
  11.        SchedulingRunnable task = new SchedulingRunnable(sysJob.getBeanName(), sysJob.getMethodName(), sysJob.getMethodParams());  
  12.        cronTaskRegistrar.addCronTask(task, sysJob.getCronExpression());  
  13.    }  
  14. }  
  15. return OperationResUtils.success();

删除定时任务

  1. boolean success = sysJobRepository.deleteSysJobById(req.getJobId());  
  2. if (!success)  
  3.    return OperationResUtils.fail("删除失败");  
  4. else{  
  5.    if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {  
  6.        SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());  
  7.        cronTaskRegistrar.removeCronTask(task);  
  8.    }  
  9. }  
  10. return OperationResUtils.success();

定时任务启动/停止状态切换

  1. if (existedSysJob.getJobStatus().equals(SysJobStatus.NORMAL.ordinal())) {  
  2.    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());  
  3.    cronTaskRegistrar.addCronTask(task, existedSysJob.getCronExpression());  
  4. } else {  
  5.    SchedulingRunnable task = new SchedulingRunnable(existedSysJob.getBeanName(), existedSysJob.getMethodName(), existedSysJob.getMethodParams());  
  6.    cronTaskRegistrar.removeCronTask(task);  
  7. }

添加实现了CommandLineRunner接口的SysJobRunner类,当SpringBoot项目启动完成后,加载数据库里状态为正常的定时任务。

  1. @Service  
  2. public class SysJobRunner implements CommandLineRunner {  
  3.    private static final Logger logger = LoggerFactory.getLogger(SysJobRunner.class);  
  4.    @Autowired  
  5.    private ISysJobRepository sysJobRepository;  
  6.    @Autowired  
  7.    private CronTaskRegistrar cronTaskRegistrar;  
  8.    @Override  
  9.    public void run(String... args) {  
  10.        // 初始加载数据库里状态为正常的定时任务  
  11.        List<SysJobPO> jobList = sysJobRepository.getSysJobListByStatus(SysJobStatus.NORMAL.ordinal());  
  12.        if (CollectionUtils.isNotEmpty(jobList)) {  
  13.            for (SysJobPO job : jobList) {  
  14.                SchedulingRunnable task = new SchedulingRunnable(job.getBeanName(), job.getMethodName(), job.getMethodParams());  
  15.                cronTaskRegistrar.addCronTask(task, job.getCronExpression());  
  16.            }  
  17.            logger.info("定时任务已加载完毕...");  
  18.        }  
  19.    }  
  20. }

工具类SpringContextUtils,用来从Spring容器里获取bean

  1. @Component  
  2. public class SpringContextUtils implements ApplicationContextAware {  
  3.    private static ApplicationContext applicationContext;  
  4.    @Override  
  5.    public void setApplicationContext(ApplicationContext applicationContext)  
  6.            throws BeansException {  
  7.        SpringContextUtils.applicationContext = applicationContext;  
  8.    }  
  9.    public static Object getBean(String name) {  
  10.        return applicationContext.getBean(name);  
  11.    }  
  12.    public static <T> T getBean(Class<T> requiredType) {  
  13.        return applicationContext.getBean(requiredType);  
  14.    }  
  15.    public static <T> T getBean(String name, Class<T> requiredType) {  
  16.        return applicationContext.getBean(name, requiredType);  
  17.    }  
  18.    public static boolean containsBean(String name) {  
  19.        return applicationContext.containsBean(name);  
  20.    }  
  21.    public static boolean isSingleton(String name) {  
  22.        return applicationContext.isSingleton(name);  
  23.    }  
  24.    public static Class<? extends Object> getType(String name) {  
  25.        return applicationContext.getType(name);  
  26.    }  
  27. }


相关文章
|
1月前
|
Java Spring
使用 Spring Boot 多个定时任务阻塞问题的解决方案
我是小假 期待与你的下一次相遇 ~
|
1月前
|
缓存 监控 Java
说一说 SpringCloud Gateway 堆外内存溢出排查
我是小假 期待与你的下一次相遇 ~
125 5
|
26天前
|
安全 Java 数据库连接
让我们讲解一下 Map 集合遍历的方式
我是小假 期待与你的下一次相遇 ~
78 43
|
1月前
|
XML 数据可视化 Java
|
1月前
|
缓存 Java 索引
|
1月前
|
缓存 Java Maven
说一说 Maven 依赖下载失败的问题总结分析
我是小假 期待与你的下一次相遇 ~
177 1
|
1月前
|
Java API Nacos
OpenFeign与Nacos结合使用时获取服务提供者的真实IP地址的方法
最终,当服务调用一次次执行,数据一次次精准传递时,这个寻找真实IP地址的宝藏狩猎,就顺利完成了。这不单单是原创性的解决方案,更是创意性地结合了现代微服务技术,和你一起编织了这场寻宝之旅的冒险故事。
107 25
|
1月前
|
Ubuntu 机器人 开发者
Docker环境下的ROS Noetic:Ubuntu 20.04 系统下的解决方案
这就是在Docker环境下安装ROS Noetic在Ubuntu 20.04系统的一种简单方法,希望能对你有所帮助。
183 16
|
1月前
|
SQL JSON Java
|
1月前
|
Java API Nacos