最近,有个项目有需要用到定时任务,所以做了一个动态管理定时任务的模块。本文将从项目背景、需求、选型、思路、具体实现等方面展开介绍。

        背景:有个支付类的项目,中间会产生一些中间态的订单,需要有个定时任务轮询确认订单状态。该类项目体量较小,单节点部署,客户比较多,需要简单快速的部署、维护。

        需求:定时任务能够通过表达式灵活指定执行计划,并支持动态启动、关闭、修改。定时任务模块最好和业务包在一个jar包内,部署简单。

        选型:说到定时任务,当下最火的当属xxl-job,本案为什么不采用xxl-job呢?不是因为它不够强大,是因为需要单独部署组件,并且需要建一系列相关的表,运维小哥哥不会弄或者嫌麻烦。基于上述原因,考虑采用Springboot自己的定时任务,常见的有两种实现方式,一种基于注解,如:

@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
public class SaticScheduleTask {
    
    @Scheduled(cron = "0/5 * * * * ?")
    private void configureTasks() {
        System.out.println("开始执行静态定时任务时间");
    }
}

        另一种基于接口,主要代码如:

@Configuration      //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling   // 2.开启定时任务
public class DynamicScheduleTask implements SchedulingConfigurer {

    /**
     * 执行定时任务.
     */
    @Override
    public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

        taskRegistrar.addTriggerTask(
                //1.添加任务内容(Runnable)
                () -> System.out.println("执行动态定时任务: " + LocalDateTime.now().toLocalTime()),
                //2.设置执行周期(Trigger)
                triggerContext -> {
                    String cron = "0/5 * * * * ?";//这个表达式可以写在配置文件里,也可以从数据库读取
                    //合法性校验
                    if (StringUtils.isEmpty(cron)) {
                        //这里写具体的业务代码
                    }
                    //返回执行周期(Date)
                    return new CronTrigger(cron).nextExecutionTime(triggerContext);
                }
        );
    }

}

        但是上述两种都不太灵活,不能动态的启动、关闭和修改。最后,选择通过ThreadPoolTaskScheduler来实现动态管理定时任务。

        思路:

        1、ThreadPoolTaskScheduler可以实现任务调度,支持基于cron表达式的任务,其提交任务接口如下:

public ScheduledFuture<?> schedule(Runnable task, Trigger trigger) {
        //...
    }

        那么,定时任务业务实现类实现Runnable接口,用cron构造CronTrigger,就可以完成任务提交。提交任务的时候将回调对象ScheduledFuture记录下来,后面可以用它的cancel方法实现任务的关闭。

        2、在数据库建一张任务表,主要有任务编号、任务名称、调度计划表达式、任务状态等字段。在项目启动后,自动查询任务表,加载任务状态为启动状态的任务。

        3、写一个前端管理页面,主要用于查询展示定时任务,新增、启动、关闭和修改定时任务。通过管理页面对任务进行启动、关闭等操作时,一方面修改数据库中的状态,另一方面也要同步做启动或者关闭任务操作。

        具体实现:

        核心代码:

@Component
public class DynamicTimedTask implements ApplicationRunner {

    @Autowired
    TimedTaskService timedTaskService;

    private static final Logger logger = LoggerFactory.getLogger(DynamicTimedTask.class);

    /**
     * @Author yrd
     * @Description 项目启动后自动加载数据库里状态为启动的定时任务
     * @Date 10:40 2021/12/20
     * @Param [args]
     * @return void
     **/
    @Override
    public void run(ApplicationArguments args) throws Exception {
        //查询数据库中定时任务信息
        List<Map> timedTasks = timedTaskService.queryAllTimedTask();
        for (Map timedTask : timedTasks) {
            String taskId = timedTask.getString("taskid");//任务编号
            String cron = timedTask.getString("cron");//表达式
            String status = timedTask.getString("status");//任务状态
            if ("1".equals(status)) {
                startTask(taskId, cron);
            }
        }
    }

    //接受任务的返回结果
    private ConcurrentHashMap<String, ScheduledFuture> futureMap = new ConcurrentHashMap<>();

    @Autowired
    private ThreadPoolTaskScheduler threadPoolTaskScheduler;

    //实例化一个线程池任务调度类,可以使用自定义的ThreadPoolTaskScheduler
    @Bean
    public ThreadPoolTaskScheduler threadPoolTaskScheduler() {
        ThreadPoolTaskScheduler schedulerPool = new ThreadPoolTaskScheduler();
        schedulerPool.setPoolSize(3);
        schedulerPool.setWaitForTasksToCompleteOnShutdown(true);
        schedulerPool.setAwaitTerminationSeconds(60);
        return schedulerPool;
    }

    /**
     * 启动定时任务
     * @return
     */
    public boolean startTask(String taskId, String cron) {
        boolean flag = false;
        ScheduledFuture future = futureMap.get(taskId);
        if (future != null) {
            if (future.isCancelled()) {
                logger.info("任务【{}】已存在但是关闭状态!!!", taskId);
            } else {
                logger.info("任务【{}】已存在且已启动!!!", taskId);
                return true;
            }
        }
        if (SpringBeanUtils.containsBean(taskId)) {
            future = threadPoolTaskScheduler.schedule((Runnable) SpringBeanUtils.getBean(taskId), new CronTrigger(cron));
            if (future != null){
                flag = true;
                futureMap.put(taskId, future);
                logger.info("任务【{}】启动成功!!!", taskId);
            }else {
                logger.info("任务【{}】启动失败!!!", taskId);
            }
        } else {
            logger.info("任务【{}】未实现!!!", taskId);
        }
        return flag;
    }

    /**
     * 停止定时任务
     * @return
     */
    public boolean stopTask(String taskId) {
        boolean flag = false;
        ScheduledFuture future = futureMap.get(taskId);
        if (future == null) {
            logger.info("任务【{}】不在任务队列中!!!", taskId);
            flag = true;
        } else {
            if (future.isCancelled()) {
                logger.info("任务【{}】已经是关闭状态!!!", taskId);
                flag = true;
            } else {
                boolean cancel = future.cancel(true);
                if (cancel){
                    flag = true;
                    logger.info("任务【{}】关闭成功!!!", taskId);
                }else {
                    logger.info("任务【{}】关闭失败!!!", taskId);
                }
            }
        }
        return flag;
    }
}

        说明:上述代码用到了通过bean的ID从应用上下文中获取bean实例,具体实现方法网上有很多介绍,本文不再详述。因此,具体任务的业务实现类bean的ID需要与任务ID一致,上述代码中startTask方法也对次作了判断。如图:

threadpool shutdown 阻塞_定时任务

threadpool shutdown 阻塞_java_02

 

         任务管理页面对应的controller类:

@Controller
@RequestMapping("/timedTask")
public class TimedTaskManageController {

    @Autowired
    TimedTaskService timedTaskService;

    /**
     * @Author yrd
     * @Description 进入定时任务管理页面
     * @Date 10:37 2021/12/20
     * @Param []
     * @return java.lang.String
     **/
    @RequestMapping("/timedTaskManagePage")
    public String timedTaskManagePage() throws AppException {
        return "/timedTask/timedTaskManage";
    }

    /**
     * @Author yrd
     * @Description 查询定时任务
     * @Date 10:38 2021/12/20
     * @Param [para]
     * @return java.util.List
     **/
    @RequestMapping("/queryTimedTask")
    @ResponseBody
    public List queryTimedTask(@RequestBody DataObject para) throws Exception {
        return timedTaskService.queryTimedTask(para);
    }

    /**
     * @Author yrd
     * @Description 进入新增定时任务页面
     * @Date 10:38 2021/12/20
     * @Param []
     * @return java.lang.String
     **/
    @RequestMapping("/addTimedTaskPage")
    public String addTimedTaskPage() throws Exception {
        return "/timedTask/addTimedTask";
    }

    /**
     * @Author yrd
     * @Description 根据任务编号检查定时任务
     * @Date 10:38 2021/12/20
     * @Param [taskId]
     * @return java.lang.String
     **/
    @RequestMapping("/checkTimedTask")
    @ResponseBody
    public String checkTimedTask(@RequestParam("taskId") String taskId) throws Exception {
        Map map = timedTaskService.queryTimedTaskById(taskId);
        JSONObject object = new JSONObject();
        if (map.isEmpty()) {
            if (SpringBeanUtils.containsBean(taskId)) {
                object.put("success", "true");
                object.put("msg", "");
            } else {
                object.put("success", "false");
                object.put("msg", "该任务编号对应的任务未实现!");
            }
        } else {
            object.put("success", "false");
            object.put("msg", "任务编号重复!");
        }
        return object.toString();
    }

    /**
     * @Author yrd
     * @Description 新增定时任务
     * @Date 10:39 2021/12/20
     * @Param [request]
     * @return java.lang.String
     **/
    @RequestMapping("/addTimedTask")
    @ResponseBody
    public String addTimedTask(HttpServletRequest request) throws Exception {
        Map param = new HashMap();
        param.put("taskId", request.getParameter("taskId").toString());
        param.put("taskName", request.getParameter("taskName").toString());
        param.put("cron", request.getParameter("cron").toString());
        return timedTaskService.addTimedTask(param);
    }

    /**
     * @Author yrd
     * @Description 进入修改定时任务页面
     * @Date 10:39 2021/12/20
     * @Param [taskId, model]
     * @return java.lang.String
     **/
    @RequestMapping("/editTimedTaskPage")
    public String editTimedTaskPage(@RequestParam("taskId") String taskId , Model model) throws Exception {
        Map map = timedTaskService.queryTimedTaskById(taskId);
        model.addAttribute("param", map);
        return "/timedTask/editTimedTask";
    }

    /**
     * @Author yrd
     * @Description 启动定时任务
     * @Date 10:39 2021/12/20
     * @Param [taskId]
     * @return java.lang.String
     **/
    @RequestMapping("/startTask")
    @ResponseBody
    public String startTask(@RequestParam("taskId") String taskId) throws Exception {
        return timedTaskService.startTask(taskId);
    }

    /**
     * @Author yrd
     * @Description 关闭定时任务
     * @Date 10:39 2021/12/20
     * @Param [taskId]
     * @return java.lang.String
     **/
    @RequestMapping("/stopTask")
    @ResponseBody
    public String stopTask(@RequestParam("taskId") String taskId) throws Exception {
        return timedTaskService.stopTask(taskId);
    }

    /**
     * @Author yrd
     * @Description 修改定时任务
     * @Date 10:39 2021/12/20
     * @Param [request]
     * @return java.lang.String
     **/
    @RequestMapping("/editTimedTask")
    @ResponseBody
    public String editTimedTask(HttpServletRequest request) throws Exception {
        Map param = new HashMap();
        param.put("taskId", request.getParameter("taskId").toString());
        param.put("taskName", request.getParameter("taskName").toString());
        param.put("cron", request.getParameter("cron").toString());
        return timedTaskService.editTimedTask(param);
    }
}

        前端、service及dao层的对应代码本文不再详述,以上功能亲测可以使用,并已运用到实际项目当中去。