Quartz源码分析(上)

文章目录

    • 1.一个想法
    • 2.Quartz总体架构
      • 2.1 Job和JobDetail
      • 2.2 Trigger
      • 2.3 JobStore
    • 3.上篇小结

1.一个想法

最近项目中用到了Quartz框架,作为定时任务的调度框架,无论是和Spring的完美融合还是直接使用Java代码来进行使用,可以说都非常的简单且稳定,某天突然想到如果没有Quartz框架,我们应该如何实现一个纯Java代码的定时任务调度框架呢?

  • 想法一:我立刻有了一个设计思路,需要一条单独的调度线程,来不断地轮询当前时间点和任务时间点,到达任务出发时间就开启一个线程,把需要交给的任务提交给那个线程。但是这样做可能会导致线程数量过多,不断轮旋线程一直进行循环占用CPU资源也是不怎么合理的设计。
  • 想法二:基于第一版的思路,单独的调度线程再等待下一次触发任务的时候进入一个休眠,线程休眠时间会根据下次触发的时间差来确定,任务提交到线程池,可以避免出现任务过多导致的线程暴增的现象。

于是我就忍不住去看了Quartz的源码(多捞啊…),然后发现Quartz的核心设计思路竟然和我第二版的想法差不多,那么废话不多数,亮出源码吧!

2.Quartz总体架构

先偷张图:
Quartz源码分析(上)_第1张图片
Quartz框架的三件套:

  1. Job:需要执行的任务。
  2. Trigger:触发器,可以指定特定的时间触发Job
  3. Scheduler(核心):统一调度器,Job和Trigger作为参数传入,调度器完成任务的触发和时间调度

可以说Quartz从任务的提交,到任务的定时执行,就是依靠这三件套来完成的,当然这里我假设你已经掌握了Quartz的基本用法,下面我们对这些组件一一分析。

2.1 Job和JobDetail

我们知道,在想要在Quartz中提交一个任务,我们需要有一个自己实现Job接口的类,只需要将定时触发的代码覆写在其execute方法中

// 实现Job接口的类
public class MyJob implements Job {

    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        System.out.println("this is a test job!");
    }
}

使用Quartz的时候,我们通过实现Job接口来完成我们需要的业务,execute方法是需要Job接口需要实现的唯一方法。

实际上,Quartz内部需要将Job包装的更为复杂,就像Spring会在内部包装生成Bean一样,Job最终会被包装成为一个JobDetailImpl对象。

JobDetailImplJobDetail接口的默认实现类,我们可以看其中一些属性。
Quartz源码分析(上)_第2张图片
JobDetailImpl对象是给Quartz框架使用的,就像SpringBean对象是给Spring框架使用的,额外在Job基础上进行封装。

关于JobDetail对象的生成,Quartz使用了建造者模式

可以看到JobBuilder类的结构,很明显这是一个Builder类。

Quartz源码分析(上)_第3张图片

public JobDetail build() {

    JobDetailImpl job = new JobDetailImpl();
    
    job.setJobClass(jobClass);
    job.setDescription(description);
    if(key == null)
        key = new JobKey(Key.createUniqueName(null), null);
    job.setKey(key); 
    job.setDurability(durability);
    job.setRequestsRecovery(shouldRecover);
    
    
    if(!jobDataMap.isEmpty())
        job.setJobDataMap(jobDataMap);
    
    return job;
}

**核心方法如上,Builder模式在安卓端比较常用,这里的好处就是,可以比较方便的生成一个JobDetail对象,交给Scheduler使用。**ps:关于建造者模式-菜鸟教程

2.2 Trigger

Tigger在Quartz作为一个触发器的角色,我们可以通过Trigger对象来实现任务调度的频率设置,时间设置等。

Trigger在Quartz中有两种常用的使用手段,一种是基于SimpleTrigger接口实现的简单触发器,这种触发器规则比较简单,一般都是间隔特定时间执行,还有一个字是基于CronTrigger接口实现的Cron表达式触发器,Cron表达式能实现更为强大的触发规则,用过Quartz的小伙伴一定会接触到Cron表达式(使用Linux肯定也接触过)。这里我们看更为简单的SimpleTrigger

Trigger实例和JobDetail实例一样,都是采用了Builder模式创造,真正的实现类是SimpleTriggerImpl类。

我们先来看一下SimpleTrigger类的继承关系图:
Quartz源码分析(上)_第4张图片
其实关于Trigger作用的说明,我想Quartz自带的JavaDoc应该比我表述的清楚:

/**
 * A {@link Trigger} that is used to fire a Job
 * at a given moment in time, and optionally repeated at a specified interval.
 * 
 * `SimpleTrigger`类开头的这段注释很好的说明了`SimpleTrigger`的作用,fire这里意为执行。
 * /

SimpleTriggerImpl类里面有很多属性,包括Trigger名,属组,开始时间,执行次数,执行间隔等等很多属性,和JobDetailImpl类一样,这些属性都是给Quartz框架用的。

这里还要特别说明一个和Trigger紧密相关的类,就是ScheduleBuilder类,很明显这也是一个Builder模式的类,其实就是为了生成一个Trigger使用的时间表,然后作为参数传入TriggerBuilder类中,这里注释说的也很明白,ScheduleBuilder会被用于定义Trigger的触发时间。(当然CronTrigger又是另一套转换逻辑,需要把corn表达式转换成时间表)

Quartz源码分析(上)_第5张图片

2.3 JobStore

下面进入一个经典问题:我现在有了咖啡,有了牛奶,请问我怎么得到咖啡牛奶?

好吧只需要一个杯子。

就像现在我们有了Job有了Trigger怎么把他们建立对应关系并储存起来呢?答案就是JobStore类,JobStore是建立JobDetailTrigger关联的关键类,同时也存储JobDetailTigger的类。Quartz内部有两个实现类:

  • 存储在内存中,对应类RAMJobStore,这个存储类的特性是项目重启以后会清空数据。
  • 存储到数据库中,使用到了JDBC接口,对应JobStoreTX类和JobStoreCMT类,这两个类分别对应Quartz单机持久化部署和集群持久化部署的数据库操作实现。

这里我们分析RAMJobStore,这个类我们不需要手动创建,如果配置文件中不启用数据库配置的话,Quartz默认使用这个类。

在Quartz框架中,我们会使用scheduleJob方法提交JobTrigger,实现JobTrigger关联的关键步骤有两个

  1. Trigger实例设置JobKey,这里需要注意Quartz框架中每个Job都会有一个唯一的JobKey来标识
  2. 存储JobDetialTriggerJobStore中,Quartz中有两种方式存储,一种时存储到内存中,一种是使用JDBC存储到数据库中。

Quartz源码分析(上)_第6张图片
下面是RAMJobStore类的核心方法,storeJobAndTrigger源码:

public void storeJobAndTrigger(JobDetail newJob,
        OperableTrigger newTrigger) throws JobPersistenceException {
    storeJob(newJob, false);
    storeTrigger(newTrigger, false);
}

简单明了,先存Job再存Trigger,当然其实storeJobstoreTrigger方法才是真正的实现了存储对象方法。

下面问题来了, 如果让你设计一种存储JobTrigger在内存中的结构,同时让他具有关联性,你会选择Java中哪种容器实现?让我想起来大二我们课程设计实现一个SQL型数据库,我硬生生用几层嵌套HashMap创造了一种存储结构。当然这里RAMJobStore中就是采用了嵌套HashMap来实现JobTrigger的映射关系。

// 核心存储容器
protected Map<JobKey, List<TriggerWrapper>> triggersByJob = 
							new HashMap<JobKey, List<TriggerWrapper>>(1000);

没错,就是你想象的这么简单。JobKey之前我们已经说过了,是Quartz框架中全局唯一的(即使分布式部署Quartz也是全局唯一的,用到了UUID类),ListTrigger的列表,也就是说一个Job可以对应多个Trigger触发器。

我们再来看一下存储方法:

public void storeTrigger(OperableTrigger newTrigger,
        boolean replaceExisting) throws JobPersistenceException {
    TriggerWrapper tw = new TriggerWrapper((OperableTrigger)newTrigger.clone());

    synchronized (lock) {
		// …… 省略部分代码
        // 核心功能代码:add to triggers by job
        List<TriggerWrapper> jobList = triggersByJob.get(tw.jobKey);
        if(jobList == null) {
            jobList = new ArrayList<TriggerWrapper>(1);
            triggersByJob.put(tw.jobKey, jobList);
        }
        jobList.add(tw);
        // ……省略部分代码
    }
}

synchronized 关键字在这里是确保多线程环境下的数据安全,其实真的很简单,就是单纯的去存储数据,不过JobStore在我们之后要讲到的Scheduler核心代码里扮演着很重要的角色。

3.上篇小结

到现在为止,我们还没有分析到Quartz的核心工作代码,只是了解了JobTrigger的创建,包装和存储,当然这也是非常重要的,俗话说巧妇难为无米之炊,JobTrigger作为原材料也是Quartz必不可少的一环,下篇我们会去分析Scheduler源码,也就是Quartz的核心代码,其中涉及到的线程模型,设计思想,会进行一些分析。

关于我们的一个想法,Quartz是如何将它照进现实的,还请看下一篇。


参考资料:

  • https://blog.csdn.net/qq_29166327/article/details/80502310 – Quartz使用介绍
  • http://www.runoob.com/design-pattern/builder-pattern.html – Builder模式介绍
  • https://www.jianshu.com/p/f466e02e2e94 – 这个大佬也是Quartz的源码分析,质量很高很详细,早知道就一开始就看他写的了,惭愧。

你可能感兴趣的:(Quartz,源码,分析,设计模式,java多线程,中间件使用)