springboot整合quartz定时器实现定时任务详解

时间:2022-06-14 23:25:54

最近需要 做一个按照时间,定时初始化一些信息的功能,研究了一下quartz,也简单了解一下TimerTask,废话不多说。
quartz和TimerTask的区别:
timer是jdk自带的(可想而知,肯定是不怎么好用)。
Quartz可以通过cron表达式精确到特定时间执行,而TimerTask不能。Quartz拥有TimerTask所有的功能,而TimerTask则没有。
学习quartz需要知道的几个概念

下面的概念来自网上,有点长,没关系,不愿意看可以跳过,下面有我个人理解精简版

  1. Job:是一个接口,只有一个方法void execute(JobExecutionContext context),开发者实现该接口定义运行任务,JobExecutionContext类提供了调度上下文的各种信息。Job运行时的信息保存在JobDataMap实例中;

  2. JobDetail:Quartz在每次执行Job时,都重新创建一个Job实例,所以它不直接接受一个Job的实例,相反它接收一个Job实现类,以便运行时通过newInstance()的反射机制实例化Job。因此需要通过一个类来描述Job的实现类及其它相关的静态信息,如Job名字、描述、关联监听器等信息,JobDetail承担了这一角色。

  3. Trigger:是一个类,描述触发Job执行的时间触发规则。主要有SimpleTrigger和CronTrigger这两个子类。当仅需触发一次或者以固定时间间隔周期执行,SimpleTrigger是最适合的选择;而CronTrigger则可以通过Cron表达式定义出各种复杂时间规则的调度方案:如每早晨9:00执行,周一、周三、周五下午5:00执行等;
  4. Calendar:org.quartz.Calendar和java.util.Calendar不同,它是一些日历特定时间点的集合(可以简单地将org.quartz.Calendar看作java.util.Calendar的集合——java.util.Calendar代表一个日历时间点,无特殊说明后面的Calendar即指org.quartz.Calendar)。一个Trigger可以和多个Calendar关联,以便排除或包含某些时间点。假设,我们安排每周星期一早上10:00执行任务,但是如果碰到法定的节日,任务则不执行,这时就需要在Trigger触发机制的基础上使用Calendar进行定点排除。
  5. Scheduler:代表一个Quartz的独立运行容器,Trigger和JobDetail可以注册到Scheduler中,两者在Scheduler中拥有各自的组及名称,组及名称是Scheduler查找定位容器中某一对象的依据,Trigger的组及名称必须唯一,JobDetail的组和名称也必须唯一(但可以和Trigger的组和名称相同,因为它们是不同类型的)。Scheduler定义了多个接口方法,允许外部通过组及名称访问和控制容器中Trigger和JobDetail。

几个概念的理解– 精简版

  1. job:你就理解成一个工作,是要干什么的,比如你是一个洗碗的,你的工作就是用洗洁精把盘子洗干净。代码里面就是一个类,这个类就是这个任务要做什么事情。
  2. jobdetail:就是这个工作的细节,也是一个接口,其中包含了这个工作的job类是什么,还有就是这个任务的名称,分组(主要是考虑到任务可能是分组的吧,所以框架是这样设计的),然后可以将参数放进去。JobDetail jobDetail = JobBuilder.newJob(QuartzInitVopVosFactory.class)
    .withIdentity(job.getJobName(), job.getJobGroup()).build();
    jobDetail.getJobDataMap().put("job", job);
    这两句新建jobdetail
  3. Trigger:触发器,这里面主要放了任务执行的时间(这里就涉及到cron表达式,这里不累赘,网上很多,下面会贴一个date转cron的工具类)trigger = TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup()).withSchedule(scheduleBuilder).build();这句代码新建一个触发器
  4. Scheduler:这个就是老大了,相当于一个容易, scheduler.scheduleJob(jobDetail, trigger);这句代码将任务的参数以及触发时间等信息放进去,构建成一个任务,到时间了自动执行。

概念大致上了解了,下面直接上代码:
额,多讲几句,网上的demo都不全,不是差这,就是差那,坑爹啊。
环境说明:springboot+quartz+maven
需要导入两个jar

<!-- quartz定时器 -->
        <dependency> 
            <groupId>org.quartz-scheduler</groupId>
            <artifactId>quartz</artifactId>
            <version>2.2.1</version>
        </dependency>
         <dependency><!-- 该依赖必加,里面有sping对schedule的支持 -->  
            <groupId>org.springframework</groupId>  
            <artifactId>spring-context-support</artifactId>  
        </dependency> 

有关配置:
这里就不用xml配置了,使用java实现配置,两个文件:

QuartzConfigration

package com.aaa.util;  

import org.quartz.Scheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;  

@Configuration  
public class QuartzConfigration {  

    @Autowired  
    private JobFactory jobFactory; 

    @Bean  
    public SchedulerFactoryBean schedulerFactoryBean() {  
        SchedulerFactoryBean schedulerFactoryBean = new SchedulerFactoryBean();  
        schedulerFactoryBean.setJobFactory(jobFactory);  
        // 用于quartz集群,QuartzScheduler 启动时更新己存在的Job
        schedulerFactoryBean.setOverwriteExistingJobs(true); 
        schedulerFactoryBean.setStartupDelay(1);  
        return schedulerFactoryBean;  
    }  

    @Bean  
    public Scheduler scheduler() {  
        return schedulerFactoryBean().getScheduler();  
    } 

}  

上面这个是主要配置,通过SchedulerFactoryBean,生成Scheduler。

JobFactory

package com.aaa.util;

import org.quartz.spi.TriggerFiredBundle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.scheduling.quartz.AdaptableJobFactory;
import org.springframework.stereotype.Component;

@Component  
public class JobFactory extends AdaptableJobFactory {       
    @Autowired    
    private AutowireCapableBeanFactory capableBeanFactory;    

    @Override    
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {    
        // 调用父类的方法 
        Object jobInstance = super.createJobInstance(bundle);    
        // 进行注入 
        capableBeanFactory.autowireBean(jobInstance);    
        return jobInstance;    
    }    
} 

这个配置主要用于第一个配置,在第一个配置中需要注入这个文件,这个文件主要是注入AutowireCapableBeanFactory 这个类,不然你在实现类(就是一个继承job的类,下面会贴)中是没办法注入其他service或者其他东西。

QuartzInitVopVosFactory

package com.aaa.util;

/** * 初始化运营信息接口调用 * @author wxy * @date 2018年6月4日 下午5:27:12 */
@DisallowConcurrentExecution
public class QuartzInitVopVosFactory implements Job{

    @Autowired
    private RedisTemplate<String, ?> redisTemplate;

    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
                //这里写job代码,就是这个任务,具体要实现什么,就在这里写
                Shift  jobBean = (Shift) jobExecutionContext.getMergedJobDataMap().get("job");
                //上面这句比较坑,必须用getMergedJobDataMap,不然获取的是一个list<map>对象。不好解析,
                //所有的参数以及其他信息都在JobExecutionContext
                //顺带提一句,如果你没有JobFactory 这个类,在这里是没办法注入任何类的。
                //shift是实体类,


        }
    }
package com.aaa.service.impl;

import java.util.Date;
import java.util.List;

/** * 定时器相关实现 * @author wxy * @date 2018年6月6日 下午5:37:43 */
@Service
public class QuartzServiceImpl implements QuartzService {

    @Autowired
    private Scheduler scheduler;

    @Autowired 
    private ShiftMapper shiftMapper;

    @Scheduled(fixedRate = 5000) // 每隔5s查库,并根据查询结果决定是否重新设置定时任务 
    public void initVopVos(){
        //这里获取任务信息数据
        List<Shift> jobList = shiftMapper.getTodayJob();  //从数据库中获取所以今天需要执行的任务

        try {
            for (Shift job : jobList) { 
                TriggerKey triggerKey = TriggerKey.triggerKey(job.getJobName(), job.getJobGroup());

                //获取trigger,即在spring配置文件中定义的 bean id="myTrigger"
                CronTrigger trigger = (CronTrigger) scheduler.getTrigger(triggerKey); 

                //不存在,创建一个
                if (null == trigger) {
                    JobDetail jobDetail = JobBuilder.newJob(QuartzInitVopVosFactory.class) 
                        .withIdentity(job.getJobName(), job.getJobGroup()).build();
                    jobDetail.getJobDataMap().put("job", job);

                    //表达式调度构建器
                    CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(CronDateUtils.getCron(job
                            .getDatetime()));

                    //按新的cronExpression表达式构建一个新的trigger
                    trigger = TriggerBuilder.newTrigger().withIdentity(job.getJobName(), job.getJobGroup()).withSchedule(scheduleBuilder).build();

                    scheduler.scheduleJob(jobDetail, trigger);
                } else {
                    // Trigger已存在,那么更新相应的定时设置
                    //表达式调度构建器,我这里数据库中存的执行时间是一个日期,这里讲date转成cron才能执行
                    CronScheduleBuilder scheduleBuilder = CronScheduleBuilder.cronSchedule(CronDateUtils.getCron(job
                            .getDatetime()));

                    //按新的cronExpression表达式重新构建trigger
                    trigger = trigger.getTriggerBuilder().startAt(new Date()).withIdentity(triggerKey)
                        .withSchedule(scheduleBuilder).build();
                       //scheduler.rescheduleJob如果服务器当前时间与你的表达式配置的执行时间差在两小时以内时,
                       //动态修改就会出现立即执行的情况。所以这里设置执行时间从当前时间开始

                    JobDataMap jobDataMap = trigger.getJobDataMap();//重新获取JobDataMap,并且更新参数
                    jobDataMap.put("job", job); 

                    //按新的trigger重新设置job执行
                    scheduler.rescheduleJob(triggerKey, trigger);
                }
            }
        } catch (SchedulerException e) {
            System.out.println("initVopVos Error"); 
        }
    }
}

上面这段代码注释已经很清楚了,这里不做过多累赘,springboot启动以后,就会每隔五秒查一次数据库,如果有新任务就会新建一个scheduleJob,如果有修改,就会执行rescheduleJob。

package com.aaa.util;
import java.text.ParseException;  
import java.text.SimpleDateFormat;  
import java.util.Date;  

/** * 该类提供Quartz的cron表达式与Date之间的转换 * @author wxy * @date 2018年6月8日 上午10:21:04 */
public class CronDateUtils{  
    private static final String CRON_DATE_FORMAT = "ss mm HH dd MM ? yyyy";  

    /*** * * @param date 时间 * @return cron类型的日期 */  
    public static String getCron(final Date  date){  
        SimpleDateFormat sdf = new SimpleDateFormat(CRON_DATE_FORMAT);  
        String formatTimeStr = "";  
        if (date != null) {  
            formatTimeStr = sdf.format(date);  
        }  
        return formatTimeStr;  
    }  

    /*** * * @param cron Quartz cron的类型的日期 * @return Date日期 */  

    public static Date getDate(final String cron) {  


        if(cron == null) {  
            return null;  
        }  

        SimpleDateFormat sdf = new SimpleDateFormat(CRON_DATE_FORMAT);  
        Date date = null;  
        try {  
            date = sdf.parse(cron);  
        } catch (ParseException e) {  
            return null;// 此处缺少异常处理,自己根据需要添加 
        }  
        return date;  
    }  
} 

最后一个工具类,可以将date转cron表达式,可以将cron表达式转为date。

上面的代码可以实现:每隔五秒,查一次数据库,并将任务加入任务表,到时间执行,并且如果任务参数,或者执行时间有更改,会自动更新到任务中。

最后讲一下我遇到的几个问题(也是网上的demo没有解决的问题):
网上的demo说的很乱,我这里总结一下,当然肯定还有其他的解决方案。

  1. 数据库中cron的时间修改以后,每个五秒扫描一次数据库,然后任务定的是每天某个时间执行一次,但是执行很多次,原因:scheduler.rescheduleJob,如果服务器当前时间与你的表达式配置的执行时间差在两小时以内时,动态修改就会出现立即执行的情况。所以这里设置执行时间从当前时间开始trigger = trigger.getTriggerBuilder().startAt(new Date()).withIdentity(triggerKey),主要startAt
  2. jobExecutionContext.getJobDetail().getJobDataMap()获取的是一个listmap,应该使用jobExecutionContext.getMergedJobDataMap().getString(“params”);
  3. quartz的job无法注入spring对象,主要原因上面说过了,需要新增一个JobFactory ,并且注入AutowireCapableBeanFactory 。然后在通过JobFactory 新建Scheduler。
  4. 在数据库中参数修改的时候,发现代码中读取的并没有同步更新,原因在于Trigger已存在,这里重新构建scheduler的时候,需要再次执行JobDataMap jobDataMap = trigger.getJobDataMap();//重新获取JobDataMap,并且更新参数
    jobDataMap.put("job", job);
  5. quartz在springboot项目有时候会执行两次?这个原因目前不明,网上有说原因的,我这边偶尔出现,所以暂时不研究。
  6. 大家可能会觉得QuartzServiceImpl 这里面有一个JobBuilder.newJob(QuartzInitVopVosFactory.class) 这里面有一个QuartzInitVopVosFactory参数,写的太死了,大家可以利用反射,通过参数来调用这个方法。由于我这里参数太多,所以就没有用这种方式,给大家一个反射的参考例子:
public static void invokMethod(Operating operating) {
        Object object = null;
        Class clazz = null;
        try {
            clazz = Class.forName("com.business.controller.OperatingController");  
            object = clazz.newInstance();
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (object == null) {
            //log.error("任务名称 = [" + scheduleJob.getJobName() + "]---------------未启动成功,请检查是否配置正确!!!");
            System.out.println("任务名称 =---------------未启动成功,请检查是否配置正确!!!");
            return;
        }
        clazz = object.getClass(); 
        Method method = null;
        try {
            method = clazz.getMethod("init", Operating.class);
        } catch (NoSuchMethodException e) {
            //log.error("任务名称 = [" + scheduleJob.getJobName() + "]---------------未启动成功,方法名设置错误!!!");
            System.out.println("任务名称 =---------------未启动成功,方法名设置错误!!!");
        } catch (SecurityException e) {
            e.printStackTrace();
        }
        if (method != null) {
            try {
                method.invoke(object, operating);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
        System.out.println("任务名称 ----------启动成功");
    }

剩下的自己去研究吧。
如果有什么写的不对的地方,欢迎大家指出。