SpringBoot+ThreadPoolTaskScheduler,定时任务还可以这么玩~

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

6219ad1a8f208a79322cbcaf7fc67e64.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:juejin.cn/post/
7242105983135514685

1aefa68b4e1a3797d71723b855eec62f.jpeg


最近做了一个需求:将定时任务保存到数据库中,并在页面上实现定时任务的开关,以及更新定时任务时间后重新创建定时任务。

于是想到了SpringBoot中自带的ThreadPoolTaskScheduler

在SpringBoot中提供的ThreadPoolTaskScheduler这个类,该类提供了一个schedule(Runnable task, Trigger trigger)的方法可以实现定时任务的创建,该方法是通过管理线程来实现。

schedule(Runnable task, Trigger trigger)源码:

public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
        ScheduledExecutorService executor = getScheduledExecutor();
        try {
                ErrorHandler errorHandler = this.errorHandler;
                if (errorHandler == null) {
                        errorHandler = TaskUtils.getDefaultErrorHandler(true);
                }
                return new ReschedulingRunnable(task, trigger, executor, errorHandler).schedule();
        }
        catch (RejectedExecutionException ex) {
                throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
        }
}

上述源码中,首先会获取定时任务执行服务,即:ScheduledExecutorService executor = getScheduledExecutor(); ,然后创建重排线程类,并调用schedule() 方法来创建ScheduledFuture<?>

ScheduledFuture<?> 中提供了cancel(boolean mayInterruptIfRunning) 来实现定时任务的删除。通过这两个方法,我们可以实现上述的需求。

废话不多说,代码撸起。

1 代码实现

1.1 定时任务线程配置

/**
 * @author: jiangjs
 * @description: 
 * @date: 2023/2/17 9:51
 **/
@Configuration
public class SchedulingTaskConfig {
    @Bean(name = "taskSchedulerPool")
    public ThreadPoolTaskScheduler threadPoolTaskScheduler(){
        ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
        //设置线程池大小
        taskScheduler.setPoolSize(60);
        //线程名称前缀
        taskScheduler.setThreadNamePrefix("task-pool-");
        //设置终止时等待最大时间,单位s,即在关闭时,需等待其他任务完成执行
        taskScheduler.setAwaitTerminationSeconds(3000);
        //设置关机时是否等待计划任务完成,不中断正在运行的任务并执行队列中的所有任务,默认false
        taskScheduler.setWaitForTasksToCompleteOnShutdown(true);
        return taskScheduler;
    }
}

1.2 获取类工具

/**
 * @author: jiangjs
 * @description: 上下文获取类
 * @date: 2023/1/30 10:28
 **/
@Component
public class SpringContextUtils implements ApplicationContextAware {
    private static ApplicationContext context;
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextUtils.context = applicationContext;
    }

    public static Object getBean(String name){
        return context.getBean(name);
    }
}

通过ApplicationContext中getBean通过类名来获取对应的类。

1.3 创建定时任务线程类

由于ThreadPoolTaskScheduler是基于线程来创建定时任务的,因此我们封装一个类来实现Runnable,其主要作用是获取数据参数,绑定定时任务类及定时任务方法,然后在通过反射拿到方法进行执行。参数则定义成泛型,便于直接传递,定时任务方法获取后不需要再次转换。

import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Objects;
/**
 * @author: jiangjs
 * @description: 实现定时任务线程
 * @date: 2023/2/16 15:31
 **/
@Slf4j
public class SchedulingTaskRunnable<T> implements Runnable {
    /**
     * 其他参数
     */
    private final T t;
    /**
     * 定时任务类
     */
    private final String clazz;

    /**
     * 定时任务方法
     */
    private final String methodName;

    SchedulingTaskRunnable(T t,String clazz,String methodName){
        this.t = t;
        this.clazz = clazz;
        this.methodName = methodName;
    }

    @Override
    public void run() {
        Object bean = SpringContextUtils.getBean(clazz);
        Method method;
        try{
            method = Objects.nonNull(t) ? bean.getClass().getDeclaredMethod(methodName,t.getClass()) : bean.getClass().getDeclaredMethod(methodName);
            ReflectionUtils.makeAccessible(method);
            if (Objects.nonNull(t)){
                method.invoke(bean,t);
            } else {
                method.invoke(bean);
            }
        }catch (Exception e){
            log.error("获取方法信息报错:{}",e.getMessage());
        }
    }
}

1.4 封装管理定时任务工具

该工具主要实现定时任务的创建和删除方法,在创建定时任务时要先调用删除方法,确保当前任务是唯一的,因此在更新定时任务时间时,只需要调用创建方法即可。

其中定义了一个 ConcurrentHashMap<String, ScheduledFuture<?>> ,主要作用是为了管理定时任务,通过自定义的任务名称或Id,获取到ScheduledFuture<?>来进行相关操作,例如:调用关闭方法。

import lombok.extern.slf4j.Slf4j;
 import org.apache.poi.ss.formula.functions.T;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
 import org.springframework.scheduling.support.CronTrigger;
 import org.springframework.stereotype.Component;

 import java.util.Objects;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ScheduledFuture;

 /**
  * @author: jiangjs
  * @description: 定时任务管理方法
  * @date: 2023/2/16 15:48
  **/
 @Component
 @Slf4j
 public class SchedulingTaskManage {

     /**
      * 将任务放入map便于管理
      */
     private final ConcurrentHashMap<String, ScheduledFuture<?>> cache = new ConcurrentHashMap<>();

     @Resource(name = "taskSchedulerPool")
     private ThreadPoolTaskScheduler threadPoolTaskScheduler;

     /**
      * 删除定时任务
      * @param key 自定义定时任务名称
      */
     public void stopSchedulingTask(String key){
         if (Objects.isNull(cache.get(key))){
             log.info(String.format(".......当前key【%s】没有定时任务......",key));
             return;
         }
         ScheduledFuture<?> future = cache.get(key);
         if (Objects.nonNull(future)){
             //关闭当前定时任务
             future.cancel(Boolean.TRUE);
             cache.remove(key);
             log.info(String.format("删除【%s】对应定时任务成功",key));
         }
     }

     /**
      * 创建定时任务
      * @param key 任务key
      * @param runnable 当前线程
      * @param cron 定时任务cron
      */
     public void createSchedulingTask(String key,SchedulingTaskRunnable<T> runnable, String cron){
         ScheduledFuture<?> schedule = taskScheduler.schedule(runnable, new CronTrigger(cron));
         assert schedule != null;
         cache.put(key,schedule);
         log.info(String.format("【%s】创建定时任务成功",key));
     }
 }

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

2 测试

创建的线程类与工具已经封装完成,接下来我们来进行下测试。

先创建定时任务表:

create table t_schedule_task(
    id int auto_increment primary key not null comment '主键Id',
    task_clazz varchar(200) not null comment '定时任务类',
    task_method varchar(200) not null comment '定时任务执行方法',
    cron varchar(200) not null comment '定时任务时间',
    status smallint not null default 0 comment '定时任务状态,0:开启,1:关闭'
) ENGINE=InnoDB AUTO_INCREMENT=10000 DEFAULT CHARSET=utf8 comment '定时任务管理表';

2.1 创建定时任务执行类

/**
* @author: jiangjs
* @description:
* @date: 2023/2/16 16:24
**/
@Slf4j
@Component(value = "testSchedulingTask")
public class TestSchedulingTask {
    public void taskMethod(UserInfo obj){
        log.info(String.format("调用了当前定时任务:输出参数:参数1:%s,参数2:%s",obj.getUserName(),obj.getPassword()));
    }
}

简单获取用户信息,进行显示。

2.2 创建实现方法

/**
 * @author: jiangjs
 * @description:
 * @date: 2023/1/12 10:53
 **/
@Service
public class ScheduledTaskManageServiceImpl implements ScheduledTaskManageService {
    @Autowired
    private SchedulingTaskManage taskManage;
    @Resource
    private ScheduleTaskMapper scheduleTaskMapper;

    @Override
    public ResultUtil<?> addTask(ScheduleTask task) {
        UserInfo userInfo = new UserInfo();
        userInfo.setUserName("张三");
        userInfo.setPassword("121212121212");
        String cron = task.getCron();
        boolean validExpression = CronExpression.isValidExpression(cron);
        if (!validExpression){
            return ResultUtil.error("无效的cron格式,请重新填写");
        }
        scheduleTaskMapper.insert(task);
        SchedulingTaskRunnable<UserInfo> taskRunnable = new SchedulingTaskRunnable<>(userInfo, task.getTaskClazz(), task.getTaskMethod());
        taskManage.createSchedulingTask(String.valueOf(task.getId()), taskRunnable,task.getCron());
        return ResultUtil.success();
    }

    @Override
    public ResultUtil<?> deleteTask(Integer id) {
        scheduleTaskMapper.deleteById(id);
        taskManage.stopSchedulingTask(String.valueOf(id));
        return ResultUtil.success();
    }

    @Override
    public ResultUtil<?> editTask(ScheduleTask task) {
        scheduleTaskMapper.updateById(task);
        UserInfo userInfo = new UserInfo();
        userInfo.setUserName("张三");
        userInfo.setPassword("33333333");
        SchedulingTaskRunnable<UserInfo> taskRunnable = new SchedulingTaskRunnable<>(userInfo, task.getTaskClazz(), task.getTaskMethod());
        taskManage.createSchedulingTask(String.valueOf(task.getId()), taskRunnable,task.getCron());
        return ResultUtil.success();
    }
}

上述代码中ScheduleTaskMapper是继承Mybatis-plus中的BaseMapper实现单表操作,小伙伴们可自行实现。

2.3 执行结果

我们创建了三个方法,分别是:addTaskeditTaskdeleteTask来实现定时任务的删除,添加,具体实现参考上面代码。看看执行结果:

创建定时任务:

提交参数:

c9773c61d8d9a7c29a32a79c4a8302b2.png
  • taskClazz: 对应测试类 @Component(value = "testSchedulingTask")中的value值

  • taskMethod: 测试内中的执行方法。如:TestSchedulingTask中的taskMethod方法

  • cron: 定时任务的cron格式时间

abdf448507f7ee994a60c8c2f1b3fb2a.jpeg

从执行结果动态图可以看到,任务每隔5s种就会获取一次用户信息。

删除定时任务:

c53d753c1e8a298c3151280f7326b3ad.png

删除Id为1000的数据库任务,同时也是删除key为1000的定时任务

45745f1cb4412858451a793c86f3845f.png

任务被删除后,即使过了5s依然没有任务在执行。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

3 总结

ThreadPoolTaskScheduler实现定时任务主要是通过对线程的管理来进行操作,添加任务时即创建一个线程,删除时即将该线程删除。因此在创建定时任务只需要创建线程就可以,在创建线程时,通过反射来获取对应的方法及传递参数。

上述就是使用SprngBoot中的ThreadPoolTaskScheduler来实现定时任务,我们只要使用前端连接相应的接口就可以实现管理人员管理定时任务的功能。

  • 源码地址:https://github.com/lovejiashn/schedule_task.git


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

8aea51ce85c1b351901f8bd4fc303a16.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

5b45cfb66d1c85cb373da0a7d1eebcda.png

5e481d30d9841ddcc772b25ee4a622c3.pngeb58d344cacb71137bda18b8781f07c6.png5e7c6e6f80fa3e9e4438cd87afcbe0e1.png6e83be867d6f8598c5c0ef76e94e7f7f.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值