SpringMVC 集成 Quartz 框架 完成动态定时任务


前言

随着需求的多样化,我们在制作一些项目的时候会对一些任务进行定时执行的操作(例如,定时获取实时新闻、天气、疫情趋势…),为了更加灵活的对任务进行操作,这时候我们就需要引用到其他框架的配合,此文章主要介绍 Quartz 框架的使用和对任务的操作做简单介绍。


详情查看:https://www.w3cschool.cn/quartz_doc/quartz_doc-2put2clm.html

一、前期准备

1.设计数据库

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for sys_cron
-- ----------------------------
DROP TABLE IF EXISTS `sys_cron`;
CREATE TABLE `sys_cron` (
  `cron_id` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'id',
  `cron_class` varchar(255) DEFAULT NULL COMMENT '类名',
  `cron` varchar(255) DEFAULT NULL COMMENT 'cron表达式',
  `cron_param` varchar(255) DEFAULT NULL COMMENT '参数',
  `cron_remark` varchar(255) DEFAULT NULL COMMENT '描述',
  `cron_status` varchar(255) DEFAULT NULL COMMENT '启停标识(1:表示启用)',
  PRIMARY KEY (`cron_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.导入依赖

<!-- 导入Quartz -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.2</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.3.2</version>
</dependency>

3.所需实体类

public class SysCron implements Serializable {

    /**
     * id
     */
    private String cronId;

    /**
     * 类名
     */
    private String cronClass;

    /**
     * cron表达式
     */
    private String cron;

    /**
     * 参数
     */
    private String cronParam;

    /**
     * 描述
     */
    private String cronRemark;

    /**
     * 启停标识(1:表示启用)
     */
    private String cronStatus;

    public SysCron(){}
    
    public SysCron(String cronId, String cronClass, String cron) {
        this.cronId = cronId;
        this.cronClass = cronClass;
        this.cron = cron;
    }
}

4.创建任务工具类

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.stereotype.Component;

/**
 * 2022/6/21 20:45
 * 动态定时任务
 *
 * @author 
 */
@Slf4j
@Component
@EnableScheduling
public class TaskInfo {
    
}

5.测试类

public class T {

    public void m1(){
        System.out.println("这是一个 3s 测试任务 (*^▽^*)");
    }

    public void m2(){
        System.out.println("这是一个 5s 测试任务 (*^▽^*)==");
    }

    public void m3(){
        System.out.println("这是一个 7s 测试任务 (*^▽^*)=====");
    }
}

6.job 实现类

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import java.lang.reflect.Method;
/**
 * 2022/6/23 9:28
 *
 * @author 
 */
public class StartJob implements Job {
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        String clazzName =  (String) context.getTrigger().getJobDataMap().get("clazz");
        String methodName = (String) context.getTrigger().getJobDataMap().get("method");
        
        try {
            System.out.print(clazzName + " 任务启动 ... ");
            //反射调用方法
            Class<?> clazz = Class.forName(clazzName);
            Object instance = clazz.getDeclaredConstructor().newInstance();
            Method method = clazz.getDeclaredMethod(methodName);
            method.invoke(instance);
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

二、使用任务工具类操作任务

很多操作任务的方法都可以在 Scheduler 接口中找到对应的 API,按需自取即可

1.设置触发器方法

/**
 * 根据任务信息设置触发器并返回
 * @param cron
 * @return
 */
private static CronTrigger getCronTrigger(SysCron cron) {
    int index = cron.getCronClass().lastIndexOf(".");
    return TriggerBuilder.newTrigger()
            //设置触发器名称和分组(组名需要与后续的增删改操作对应,不设置会采用默认的分组名 DEFAULT_GROUP)
            .withIdentity(cron.getCronId(), "group1")
            //设置内容(作业数据)
            .usingJobData("clazz", cron.getCronClass().substring(0, index)) //设置需要调用方法所在类的全类名
            .usingJobData("method", cron.getCronClass().substring(index + 1)) //设置调用方法的方法名
            //设置定时执行规则
            //SimpleScheduleBuilder:以 单位秒(s) 为执行规则
            //CronScheduleBuilder:以 cron 表达式为执行规则
            .withSchedule(CronScheduleBuilder.cronSchedule(cron.getCron())).build();
}

2.添加执行任务

/**
 * 执行定时任务
 *
 * @param cronList 任务集合
 */
public static void timedTask(List<SysCron> cronList) {
    try {
        //创建一个 scheduler (调度器)
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        //创建一个 Trigger 集合
        /*
         * 同时执行多个任务时需要将要值行的任务封装成一个 触发器(Trigger) 集合,每个任务对应一个触发器
         * 触发器的名称可以作为该触发器在其 分组内 的唯一标识 (可以理解为 一个键)
         */
        Set<Trigger> triggerSet = cronList.stream()
                .map(TaskInfo::getCronTrigger).collect(Collectors.toSet());
        //创建一个job (StartJob 是一个 Job 的实现类,用来执行任务)
        JobDetail job = JobBuilder.newJob(StartJob.class)
                .withIdentity("myJob", "group1").build();
        //注册trigger并启动scheduler
        scheduler.scheduleJob(job, triggerSet, true);
        //启动任务(start() 方法执行后才会调用 触发器(Trigger))
        scheduler.start();
    } catch (SchedulerException se) {
        se.printStackTrace();
    }
}

3.修改任务

 /**
  * 修改任务
  *
  * @param cron 任务信息
  */
 public static void updateTask(SysCron cron) {
     try {
         Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
         //根据任务信息从调度器中获取对应触发器的键
         TriggerKey triggerKey = new TriggerKey(cron.getCronId(), "group1");
         //创建新的触发器
         CronTrigger newTrigger = getCronTrigger(cron);
         //使用给定键删除触发器,并存储新的给定键
         scheduler.rescheduleJob(triggerKey, newTrigger);
     } catch (Exception e) {
         e.printStackTrace();
     }
 }

4.结束任务

/**
 * 结束任务
 *
 * @param cronList
 */
public static void endTask(List<SysCron> cronList) {
    if (Objects.isNull(cronList) || cronList.size() < 1) return;
    try {
        //创建一个 scheduler
        Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
        final String GROUP_NAME = "group1";
        List<TriggerKey> triggerKeys = cronList.stream()
        		//创建触发器对象
                .map(cron -> new TriggerKey(cron.getCronId(), GROUP_NAME))
                //筛选出调度器中已存在的触发器
                .filter(triggerKey -> {
                    try {
                        return scheduler.checkExists(triggerKey);
                    } catch (SchedulerException e) {
                        e.printStackTrace();
                        return false;
                    }
                }).collect(Collectors.toList());
        //调用 API 批量结束任务
        boolean del = scheduler.unscheduleJobs(triggerKeys);
        //提示信息
        if (del) {
            System.err.println("任务结束");
        } else {
            System.err.println("结束任务执行失败!!!");
        }
        //获取任务分组中的 触发器 集合,若集合中触发器个数为0,表示已无需要执行的任务
        Set<TriggerKey> group1 = scheduler.getTriggerKeys(GroupMatcher.groupEquals("group1"));
        if (group1.size() <= 0) {
            //本轮任务执行完之后,结束所有任务,并清除所有调度数据 - 所有作业、触发器日历
            scheduler.shutdown(true);
        }
    } catch (SchedulerException se) {
        System.err.println("结束任务时出现异常 o(╥﹏╥)o");
        se.printStackTrace();
    }
}

5.测试

//测试方法
public static void main(String[] args) {
    //全类名一定要与你要执行的方法相对应
     SysCron sysCron1 = new SysCron("1","xxx.xxxx.T.m1", "0/3 * * * * ? ");
     SysCron sysCron2 = new SysCron("2","xxx.xxxx.T.m2", "0/5 * * * * ?");
     SysCron sysCron3 = new SysCron("3","xxx.xxxx.T.m3", "0/7 * * * * ?");
    
     List<SysCron> list = new ArrayList<>();
     list.add(sysCron1);
     list.add(sysCron2);
     list.add(sysCron3);
    
     timedTask(list);
 }

6.设置项目启动时自动执行已开启的任务

@Resource
private ISysCronService cronService;

/**
 * 项目启动默认执行的任务
 */
@PostConstruct
public void init() {
    //获取任务信息
    List<SysCron> cronList = cronService
            .list(new LambdaQueryWrapper<SysCron>().eq(SysCron::getCronStatus, "1"));
    
    timedTask(cronList);
}

解决 Quartz 无法调用 Spring 容器中的方法的 空指针 问题

原因:

Job 是在 quartz 的框架中实例化的,service 是在 spring 容器中创建出来的,所以 Job 实现类不受 spring 管理,在 quartz 使用 spring 容器中的方法时导致注入失败

解决:

由于本人找到的方法都未解决此问题,于是我采用了一个比较笨的方法:在 Quartz 调用容器方法前 从Web 应用程序上下文集中初始化 service。
这里是写了一个统一管理 service 的类,让所有控制器继承该类。

public class BaseController {

	{
    	WebApplicationContext applicationContext= ContextLoader.getCurrentWebApplicationContext();
    	if (Objects.nonNull(applicationContext)) {
        	this.newsService = applicationContext.getBean(ISysNewsService.class);
        	this.cronService = applicationContext.getBean(ISysCronService.class);
    	}
	}

	@Resource
    public ISysNewsService newsService;

    @Resource
    public ISysCronService cronService;
}

新手上路,不足之处,望多指教


版权声明:本文为m0_51985332原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。