Quzrtz--动态调度实现

111 阅读10分钟

1.动态调度的实现

传统的Spring方式集成Quartz,由于任务信息全部配置在xml文件中,如果需要操作任务或者修改任务运行频率,只能重新编译、打包、部署、重启,如果有紧急问题需要处理,会浪费很多的时间。

有没有可以动态调度任务的方法?比如停止一个Job?启动一个Job?修改Job的触发频率? 读取配置文件、写入配置文件、重启Scheduler或重启应用明显是不可取的。

对于这种频繁变更并且需要实时生效的配置信息,我们可以放到哪里? ZK、Redis、DB tables。

并且,我们可以提供一个界面,实现对数据表的轻松操作。

1.1 配置管理

这里我们用最简单的数据库的实现。

问题1:建一张什么样的表?参考JobDetail的属性。

CREATE TABLE `sys_job` (
`id` INT ( 11 ) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`job_name` VARCHAR ( 512 ) NOT NULL COMMENT '任务名称',
`job_group` VARCHAR ( 512 ) NOT NULL COMMENT '任务组名',
`job_cron` VARCHAR ( 512 ) NOT NULL COMMENT '时间表达式',
`job_class_path` VARCHAR ( 1024 ) NOT NULL COMMENT '类路径,全类型',
`job_data_map` VARCHAR ( 1024 ) DEFAULT NULL COMMENT '传递map参数',
`job_status` INT ( 2 ) NOT NULL COMMENT '状态:1启用 0停用',
`job_describe` VARCHAR ( 1024 ) DEFAULT NULL COMMENT '任务功能描述',
PRIMARY KEY ( `id` ) 
) ENGINE = INNODB AUTO_INCREMENT = 25 DEFAULT CHARSET = utf8;


-- 插入3条数据,3个任务
-- 注意第三条,是一个发送邮件的任务,需要改成你自己的QQ和授权码。不知道什么是授权码的自己百度。
INSERT INTO `sys_job` (`id`, `job_name`, `job_group`, `job_cron`, `job_class_path`, `job_data_map`, `job_status`, `job_describe`) VALUES (22, 'test', 'test', '*/20 * * * * ?', 'com.msb.demo.task.TestTask1', NULL, 1, 'a job a');
INSERT INTO `sys_job` (`id`, `job_name`, `job_group`, `job_cron`, `job_class_path`, `job_data_map`, `job_status`, `job_describe`) VALUES (23, 'test2', 'test', '*/30 * * * * ?', 'com.msb.demo.task.TestTask2', NULL, 1, 'another job');
INSERT INTO `sys_job` (`id`, `job_name`, `job_group`, `job_cron`, `job_class_path`, `job_data_map`, `job_status`, `job_describe`) VALUES (24, 'test3', 'mail', '*/10 * * * * ?', 'com.msb.demo.task.TestTask3', '{"data":{"loginAccount":"改成你的QQ邮箱","loginAuthCode":"改成你的邮箱授权码","sender":"改成你的QQ邮箱","emailContent":"    你好,我是蒋介石的私生子,我在台湾有2000亿新台币冻结了。我现在在古交,又回不了台湾。所以没办法,只要你给我转1000块钱帮我解冻我的账号,我在台湾有我自己的部队。要是你今天帮了我,等我回到台湾给你留一个三军统帅的位置,另外再给你200亿人民币,我建行账号1111111111111111  刘超越。这是我女秘书的账号,打了钱通知我,我给你安排专机接你来台。","emailContentType":"text/html;charset=utf-8","emailSubject":"十万火急","recipients":"改成你要的收件人邮箱,可以有多个,英文逗号隔开"}}', 1, 'fdsafdfds');

1.2 数据操作与任务调度

操作数据表非常简单,SSM增删改查。

但是在修改了表的数据之后,怎么让调度器知道呢?

调度器的接口:Scheduler

在我们的需求中,我们需要做的事情:
1、 新增一个任务
2、 删除一个任务
3、 启动、停止一个任务
4、 修改任务的信息(包括调度规律)

因此可以把相关的操作封装到一个工具类中。com.msb.demo.util.SchedulerUtil


package com.msb.demo.util;

import com.alibaba.fastjson.JSONObject;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

public class SchedulerUtil {
    private static Logger logger = LoggerFactory.getLogger(SchedulerUtil.class);

    /**
     * 新增定时任务
     * @param jobClassName 类路径
     * @param jobName 任务名称
     * @param jobGroupName 组别
     * @param cronExpression Cron表达式
     * @param jobDataMap 需要传递的参数
     * @throws Exception
     */
    public static void addJob(String jobClassName,String jobName, String jobGroupName, String cronExpression,String jobDataMap) throws Exception {
       // 通过SchedulerFactory获取一个调度器实例
       SchedulerFactory sf = new StdSchedulerFactory();
       Scheduler scheduler = sf.getScheduler();
       // 启动调度器
       scheduler.start();
       // 构建job信息
       JobDetail jobDetail = JobBuilder.newJob(getClass(jobClassName).getClass())
             .withIdentity(jobName, jobGroupName).build();
       // JobDataMap用于传递任务运行时的参数,比如定时发送邮件,可以用json形式存储收件人等等信息
       if (StringUtils.isNotEmpty(jobDataMap)) {
          JSONObject jb = JSONObject.parseObject(jobDataMap);
          Map<String, Object> dataMap =(Map<String, Object>) jb.get("data");
          for (Map.Entry<String, Object> m:dataMap.entrySet()) {
             jobDetail.getJobDataMap().put(m.getKey(),m.getValue());
          }
       }
       // 表达式调度构建器(即任务执行的时间)
       CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
       // 按新的cronExpression表达式构建一个新的trigger
       CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobName, jobGroupName)
             .withSchedule(scheduleBuilder).startNow().build();
       try {
          scheduler.scheduleJob(jobDetail, trigger);
       } catch (SchedulerException e) {
          logger.info("创建定时任务失败" + e);
          throw new Exception("创建定时任务失败");
       }
    }
    
    /**
     * 停用一个定时任务
     * @param jobName 任务名称
     * @param jobGroupName 组别
     * @throws Exception
     */
    public static void jobPause(String jobName, String jobGroupName) throws Exception {
       // 通过SchedulerFactory获取一个调度器实例
       SchedulerFactory sf = new StdSchedulerFactory();
       Scheduler scheduler = sf.getScheduler();
       scheduler.pauseJob(JobKey.jobKey(jobName, jobGroupName));
    }
    
    /**
     * 启用一个定时任务
     * @param jobName 任务名称
     * @param jobGroupName 组别
     * @throws Exception
     */
    public static void jobresume(String jobName, String jobGroupName) throws Exception {
       // 通过SchedulerFactory获取一个调度器实例
       SchedulerFactory sf = new StdSchedulerFactory();
       Scheduler scheduler = sf.getScheduler();
       scheduler.resumeJob(JobKey.jobKey(jobName, jobGroupName));
    }
    
    /**
     * 删除一个定时任务
     * @param jobName 任务名称
     * @param jobGroupName 组别
     * @throws Exception
     */
    public static void jobdelete(String jobName, String jobGroupName) throws Exception {
       // 通过SchedulerFactory获取一个调度器实例
       SchedulerFactory sf = new StdSchedulerFactory();
       Scheduler scheduler = sf.getScheduler();
       //停止触发
       scheduler.pauseTrigger(TriggerKey.triggerKey(jobName, jobGroupName));
       //取消发布
       scheduler.unscheduleJob(TriggerKey.triggerKey(jobName, jobGroupName));
       scheduler.deleteJob(JobKey.jobKey(jobName, jobGroupName));
    }
    
    /**
     * 更新定时任务表达式
     * @param jobName 任务名称
     * @param jobGroupName 组别
     * @param cronExpression Cron表达式
     * @throws Exception
     */
    public static void jobReschedule(String jobName, String jobGroupName, String cronExpression) throws Exception {
       try {
          SchedulerFactory schedulerFactory = new StdSchedulerFactory();
          Scheduler scheduler = schedulerFactory.getScheduler();
          TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
          // 表达式调度构建器
          CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(cronExpression);
          CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey);
          // 按新的cronExpression表达式重新构建trigger
          trigger = trigger.getTriggerBuilder().withIdentity(triggerKey).withSchedule(scheduleBuilder).startNow().build();
          // 按新的trigger重新设置job执行
          scheduler.rescheduleJob(triggerKey, trigger);
       } catch (SchedulerException e) {
          System.out.println("更新定时任务失败" + e);
          throw new Exception("更新定时任务失败");
       }
    }
    
    /**
     * 检查Job是否存在
     * @throws Exception
     */
    public static Boolean isResume(String jobName, String jobGroupName) throws Exception {
       SchedulerFactory sf = new StdSchedulerFactory();
       Scheduler scheduler = sf.getScheduler();
       TriggerKey triggerKey = TriggerKey.triggerKey(jobName, jobGroupName);
       Boolean state = scheduler.checkExists(triggerKey);
         
       return state;
    }

    /**
     * 暂停所有任务
     * @throws Exception
     */
    public static void pauseAlljob() throws Exception {
       SchedulerFactory sf = new StdSchedulerFactory();
       Scheduler scheduler = sf.getScheduler();
       scheduler.pauseAll();
    }

    /**
     * 唤醒所有任务
     * @throws Exception
     */
    public static void resumeAlljob() throws Exception {
       SchedulerFactory sf = new StdSchedulerFactory();
       Scheduler sched = sf.getScheduler();
       sched.resumeAll();
    }

    /**
     * 获取Job实例
     * @param classname
     * @return
     * @throws Exception
     */
    public static BaseJob getClass(String classname) throws Exception {
       try {
          Class<?> c = Class.forName(classname);
          return (BaseJob) c.newInstance();
       } catch (Exception e) {
          throw new Exception("类["+classname+"]不存在!");
       }
       
    }
}

1.3 前端界面

image.png

1.4 容器启动与Service注入

容器启动

因为任务没有定义在ApplicationContext.xml中,而是放到了数据库中,Spring Boot启动时,怎么读取任务信息? 或者,怎么在Spring启动完成的时候做一些事情? 创建一个类,实现CommandLineRunner接口,实现run方法。 从表中查出状态是1的任务,然后构建。

package com.msb.demo.config;

import com.alibaba.fastjson.JSONObject;
import com.msb.demo.entity.SysJob;
import com.msb.demo.service.ISysJobService;
import com.msb.demo.util.BaseJob;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 这个类用于启动SpringBoot时,加载作业。run方法会自动执行。
 *
 * 另外可以使用 ApplicationRunner
 *
 */
@Component
public class InitStartSchedule implements CommandLineRunner {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISysJobService sysJobService;
    @Autowired
    private MyJobFactory myJobFactory;

    @Override
    public void run(String... args) throws Exception {
        /**
         * 用于程序启动时加载定时任务,并执行已启动的定时任务(只会执行一次,在程序启动完执行)
         */

        //查询job状态为启用的
        HashMap<String,String> map = new HashMap<String,String>();
        map.put("jobStatus", "1");
        List<SysJob> jobList= sysJobService.querySysJobList(map);
        if( null == jobList || jobList.size() ==0){
            logger.info("系统启动,没有需要执行的任务... ...");
        }
        // 通过SchedulerFactory获取一个调度器实例
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler scheduler = sf.getScheduler();
        // 如果不设置JobFactory,Service注入到Job会报空指针
        scheduler.setJobFactory(myJobFactory);
        // 启动调度器
        scheduler.start();

        for (SysJob sysJob:jobList) {
            String jobClassName=sysJob.getJobName();
            String jobGroupName=sysJob.getJobGroup();
            //构建job信息
            JobDetail jobDetail = JobBuilder.newJob(getClass(sysJob.getJobClassPath()).getClass()).withIdentity(jobClassName, jobGroupName).build();
            if (StringUtils.isNotEmpty(sysJob.getJobDataMap())) {
                JSONObject jb = JSONObject.parseObject(sysJob.getJobDataMap());
                Map<String, Object> dataMap = (Map<String, Object>)jb.get("data");
                for (Map.Entry<String, Object> m:dataMap.entrySet()) {
                    jobDetail.getJobDataMap().put(m.getKey(),m.getValue());
                }
            }
            //表达式调度构建器(即任务执行的时间)
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(sysJob.getJobCron());
            //按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName)
                    .withSchedule(scheduleBuilder).startNow().build();
            // 任务不存在的时候才添加
            if( !scheduler.checkExists(jobDetail.getKey()) ){
                try {
                    scheduler.scheduleJob(jobDetail, trigger);
                } catch (SchedulerException e) {
                    logger.info("\n创建定时任务失败"+e);
                    throw new Exception("创建定时任务失败");
                }
            }
        }
    }

    public static BaseJob getClass(String classname) throws Exception
    {
        Class<?>  c= Class.forName(classname);
        return (BaseJob)c.newInstance();
    }
}

Service类注入到Job中

Spring Bean如何注入到实现了Job接口的类中?

package com.msb.demo.task;

import com.alibaba.fastjson.JSONObject;
import com.msb.demo.entity.SysJob;
import com.msb.demo.service.ISysJobService;
import com.msb.demo.util.BaseJob;
import com.msb.demo.util.MailUtil;
import org.apache.commons.lang3.StringUtils;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.apache.commons.*;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;


import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.List;


public class TestTask3 implements BaseJob {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISysJobService sysJobService;

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        logger.info("开始执行任务3... ...");
        HashMap<String,String> map = new HashMap<String,String>();
        map.put("jobGroup", "mail");
        map.put("jobStatus", "1");
        List<SysJob> jobList= sysJobService.querySysJobList(map);

        if( null == jobList || jobList.size() ==0){
            logger.info("没有状态为可用的发送邮件任务... ...");
        }

        for (SysJob sysJob:jobList) {
            String jobClassName=sysJob.getJobName();
            String jobGroupName=sysJob.getJobGroup();
            String jobData  = sysJob.getJobDataMap();
            if (StringUtils.isNotEmpty(jobData)) {
                JSONObject jd = JSONObject.parseObject(sysJob.getJobDataMap());
                JSONObject data = jd.getJSONObject("data");
                String loginAccount = data.getString("loginAccount");
                String loginAuthCode = data.getString("loginAuthCode");
                String sender = data.getString("sender");
                String recipientsStr = data.getString("recipients");
                String[] recipients = recipientsStr.split(",");
                String emailSubject = data.getString("emailSubject");
                String emailContent = data.getString("emailContent");
                String emailContentType = data.getString("emailContentType");

                Date date = new Date();
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                emailSubject = emailSubject + sdf.format(date) ;
                logger.info("开始发送邮件... ...");
                MailUtil.sendEmail(loginAccount,loginAuthCode,sender,recipients,emailSubject,emailContent,emailContentType);
            }else {
                logger.info("JobDataMap为空,没有发送邮件的相关信息... ...");
            }

        }
    }
}

现在这个类当中,注入了SysJobService。但是我们知道TestTask3 是没有被Spring接管的。所以这个肯定是有问题的

如果没有任何配置,注入会报空指针异常。

原因: 因为定时任务Job对象的实例化过程是在Quartz中进行的,而Service Bean是由Spring容器管理的,Quartz察觉不到Service Bean的存在,所以无法将Service Bean装配到Job对象中。

分析:

Quartz集成到Spring中,用到SchedulerFactoryBean,其实现了InitializingBean方法,在唯一的方法afterPropertiesSet()在Bean的属性初始化后调用。

调度器用AdaptableJobFactory对Job对象进行实例化。所以,如果我们可以把这个JobFactory指定为我们自定义的工厂的话,就可以在Job实例化完成之后,把Job纳入到Spring容器中管理。

解决这个问题的步骤:

1、定义一个AdaptableJobFactory,实现JobFactory接口,实现接口定义的newJob方法,在这里面返回Job实例

package com.msb.demo.config;

import org.quartz.Job;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.spi.JobFactory;
import org.quartz.spi.TriggerFiredBundle;
import org.springframework.scheduling.quartz.DelegatingJob;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Method;

/**
 *
 * 将Spring的对象注入到Quartz Job 1
 */
public class AdaptableJobFactory implements JobFactory {
	@Override
	public Job newJob(TriggerFiredBundle bundle, Scheduler arg1) throws SchedulerException {
		 return newJob(bundle);
	}

	 public Job newJob(TriggerFiredBundle bundle) throws SchedulerException {
	        try {
	        	// 返回Job实例
	            Object jobObject = createJobInstance(bundle);
	            return adaptJob(jobObject);
	        }
	        catch (Exception ex) {
	            throw new SchedulerException("Job instantiation failed", ex);
	        }
	    }

	    // 通过反射的方式创建实例
	    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
	        Method getJobDetail = bundle.getClass().getMethod("getJobDetail");
	        Object jobDetail = ReflectionUtils.invokeMethod(getJobDetail, bundle);
	        Method getJobClass = jobDetail.getClass().getMethod("getJobClass");
	        Class jobClass = (Class) ReflectionUtils.invokeMethod(getJobClass, jobDetail);
	        return jobClass.newInstance();
	    }

	    protected Job adaptJob(Object jobObject) throws Exception {
	        if (jobObject instanceof Job) {
	            return (Job) jobObject;
	        }
	        else if (jobObject instanceof Runnable) {
	            return new DelegatingJob((Runnable) jobObject);
	        }
	        else {
	            throw new IllegalArgumentException("Unable to execute job class [" + jobObject.getClass().getName() +
	                    "]: only [org.quartz.Job] and [java.lang.Runnable] supported.");
	        }
	    }
}

image.png

2、定义一个MyJobFactory,继承AdaptableJobFactory。 使用Spring的AutowireCapableBeanFactory,把Job实例注入到容器中。

@Component
public class MyJobFactory extends AdaptableJobFactory {
  @Autowired
  private AutowireCapableBeanFactory capableBeanFactory;
​
  protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
    Object jobInstance = super.createJobInstance(bundle);
    capableBeanFactory.autowireBean(jobInstance);
​
    return jobInstance;
 }
}

3、指定Scheduler的JobFactory为自定义的JobFactory。 com.msb.demo.config.InitStartSchedule中: scheduler.setJobFactory(myJobFactory);

package com.msb.demo.config;

import com.alibaba.fastjson.JSONObject;
import com.msb.demo.entity.SysJob;
import com.msb.demo.service.ISysJobService;
import com.msb.demo.util.BaseJob;
import org.apache.commons.lang3.StringUtils;
import org.quartz.*;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * 这个类用于启动SpringBoot时,加载作业。run方法会自动执行。
 *
 * 另外可以使用 ApplicationRunner
 *
 */
@Component
public class InitStartSchedule implements CommandLineRunner {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISysJobService sysJobService;
    @Autowired
    private MyJobFactory myJobFactory;

    @Override
    public void run(String... args) throws Exception {
        /**
         * 用于程序启动时加载定时任务,并执行已启动的定时任务(只会执行一次,在程序启动完执行)
         */

        //查询job状态为启用的
        HashMap<String,String> map = new HashMap<String,String>();
        map.put("jobStatus", "1");
        List<SysJob> jobList= sysJobService.querySysJobList(map);
        if( null == jobList || jobList.size() ==0){
            logger.info("系统启动,没有需要执行的任务... ...");
        }
        // 通过SchedulerFactory获取一个调度器实例
        SchedulerFactory sf = new StdSchedulerFactory();
        Scheduler scheduler = sf.getScheduler();
        // 如果不设置JobFactory,Service注入到Job会报空指针
        scheduler.setJobFactory(myJobFactory);
        // 启动调度器
        scheduler.start();

        for (SysJob sysJob:jobList) {
            String jobClassName=sysJob.getJobName();
            String jobGroupName=sysJob.getJobGroup();
            //构建job信息
            JobDetail jobDetail = JobBuilder.newJob(getClass(sysJob.getJobClassPath()).getClass()).withIdentity(jobClassName, jobGroupName).build();
            if (StringUtils.isNotEmpty(sysJob.getJobDataMap())) {
                JSONObject jb = JSONObject.parseObject(sysJob.getJobDataMap());
                Map<String, Object> dataMap = (Map<String, Object>)jb.get("data");
                for (Map.Entry<String, Object> m:dataMap.entrySet()) {
                    jobDetail.getJobDataMap().put(m.getKey(),m.getValue());
                }
            }
            //表达式调度构建器(即任务执行的时间)
            CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(sysJob.getJobCron());
            //按新的cronExpression表达式构建一个新的trigger
            CronTrigger trigger = TriggerBuilder.newTrigger().withIdentity(jobClassName, jobGroupName)
                    .withSchedule(scheduleBuilder).startNow().build();
            // 任务不存在的时候才添加
            if( !scheduler.checkExists(jobDetail.getKey()) ){
                try {
                    scheduler.scheduleJob(jobDetail, trigger);
                } catch (SchedulerException e) {
                    logger.info("\n创建定时任务失败"+e);
                    throw new Exception("创建定时任务失败");
                }
            }
        }
    }

    public static BaseJob getClass(String classname) throws Exception
    {
        Class<?>  c= Class.forName(classname);
        return (BaseJob)c.newInstance();
    }
}

实现动态调度的关键点在于,将Quartz需要用到的类加载到Spring容器中。