Quartz之一:任務調度的動態處理

Quartz是一個完全由java編寫的功能豐富的開源作業調度庫,可以集成到幾乎任何Java應用程序中,小到獨立應用程序,大到大型的電子商務系統。Quartz可以用來創建執行數十,數百乃至數萬個作業的簡單或復雜的計劃;作業的任務被定義為標準的Java組件,它可以執行幾乎任何你可能編程的任務。而且Quartz Scheduler包含許多企業級功能,例如支持JTA事務和集群。

任務調度是很多系統都會用到的功能,比如需要定期執行調度生成報表、或者博客定時更新之類的,都可以靠Quartz來完成。而需求中經常會出現,需要對調度任務進行動態添加、關閉、啟動和刪除,在這里本文就對Spring+Quartz實現任務調度動態處理進行說明。

開篇就彩蛋:其實我已將Spring+Quartz動態任務調度封裝為框架工具,不想看文章解釋編碼可以直接轉到我的GitHub上的項目Libra

TL;DR

  • 簡單了解一下Quartz中的概念和運行原理
  • 根據原理解析如何實現動態任務調度
  • 在Spring中配置Quartz框架,并自定義Quartz作業工廠類
  • 利用自定義注解實現動態任務類,并實現自動注入
  • 實現Dubbo環境下注解的自動注入
  • 附加:Libra如何來使用

理解Quartz的原理

先來直接看Quartz官網提供的單程序中使用示例:

  SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
  Scheduler scheduler = schedFact.getScheduler();

  // define the job and tie it to our HelloJob class
  JobDetail job = newJob(HelloJob.class)
      .withIdentity("myJob", "group1")
      .build();

  // Trigger the job to run now, and then every 40 seconds
  Trigger trigger = newTrigger()
      .withIdentity("myTrigger", "group1")
      .startNow()
      .withSchedule(simpleSchedule()
          .withIntervalInSeconds(40)
          .repeatForever())
      .build();

  // Tell quartz to schedule the job using our trigger
  scheduler.scheduleJob(job, trigger);
  scheduler.start();

通過示例就可以展開了解一下Quartz中涉及到的幾個類概念:

  • SchedulerFactory:調度器工廠。這是一個接口,用于調度器的創建和管理。示例中使用的是Quartz中的默認實現。
  • Scheduler:任務調度器。它表示一個Quartz的獨立運行容器,里面注冊了多個觸發器(Trigger)和任務實例(JobDetail)。兩者分別通過各自的組(group)和名稱(name)作為容器中定位某一對象的唯一依據,因此組和名稱必須唯一(觸發器和任務實例的組和名稱可以相同,因為對象類型不同)。
  • Job:是一個接口,只有一個方法void execute(JobExecutionContext context),開發者實現該接口定義運行任務,JobExecutionContext類提供了調度上下文的各種信息。Job運行時的信息保存在JobDataMap實例中。
  • JobDetail:Job實例。Quartz在每次執行Job時,都重新創建一個Job實例,所以它不直接接受一個Job的實例,相反它接收一個Job實現類,以便運行時通過newInstance()的反射機制實例化Job。因此需要通過一個類來描述Job的實現類及其它相關的靜態信息,如Job名字、描述、關聯監聽器等信息,JobDetail承擔了這一角色。
  • Trigger:觸發器,描述觸發Job執行的時間觸發規則。
    • SimpleTrigger:當僅需觸發一次或者以固定時間間隔周期執行,SimpleTrigger是最適合的選擇。
    • CronTrigger:通過Cron表達式定義出各種復雜時間規則的調度方案:如每早晨9:00執行,周一、周三、周五下午5:00執行等。

我們可以簡單的理解到整個Quartz運行調度的流程如下:

  1. 通過觸發器工廠(SchedulerFactory的實現類)創建一個調度器(Scheduler);
  2. 創建一個任務實例(JobDetail),為它指定實現了Job接口的實現類(示例中的HelloWord.class),并指定唯一標識(Identity)組(示例中的“group1”)和名稱(示例中的“myJob”);
  3. 創建一個觸發器(Trigger),為它指定時間觸發規則(示例中的simpleSchedule()生成的SimpleTrigger),并指定唯一標識(Identity)組(示例中的“group1”)和名稱(示例中的“myTrigger”)
  4. 最后通過調度器(Scheduler)將任務實例(JobDetail)和觸發器(Trigger)綁定在一起,并通過start()方法開啟任務調度。

Tips:當然Quartz還涉及到線程池等其他內容,你可以通過“Quartz原理揭秘和源碼解讀”或“Quartz原理解析”對Quartz的原理有更深入的了解,本篇文章就不詳細展開了。

如何實現動態任務調度

了解了原理就可以開始構思如何實現動態任務調度了,其實從上面可以看出Quartz已經很好的支持了任務的動態創建、修改和刪除,而我們的重點是如何抽象它以及想達到什么樣的程度。

我們的需求是通過數據庫(關系型或非關系型)來記錄當前執行的任務處理,已經配置好的任務在系統啟動時應該自動運行起來。另外,可以在系統運行時添加新的任務、對任務進行啟動或停止、重新設置任務運行時間,還可以刪除任務。

這里在系統運行時可以添加就涉及到兩個不同的情況:

  1. 添加任務指定的實現類(實現了Quartz的Job接口)已經存在于項目中
  2. 添加任務指定的實現類(實現了Quartz的Job接口)不存在于項目中,需要讓系統動態加載jar包

而對于實現類的處理也產生了兩種方案:

  • 方案一:業務相關的實現類直接實現Job接口
  • 方案二:編寫統一的實現類實現Job接口,在統一實現類中使用反射指定到某個類方法執行

這里基于脫離任務狀態區分更好解耦方便拓展等方面考慮最終選擇了方案二。

Tips:在后面可以看到,方案二更加靈活。

補充一下Quartz中的任務狀態:

無狀態Job:默認情況下都為此類型,可以并發執行。

有狀態Job(StatefulJob):同一個實例(JobDetail)不能同時運行。在Quartz舊版本中實現StatefulJob接口,新版已經廢棄了此接口,使用注解(@DisallowConcurrentExecution)實現。

這里使用方案二會創建一個無狀態和一個有狀態的統一任務實現類,但真正的業務任務類可以是一個。

確定好方案,便整理一下我們的具體實現思路如下:

  1. 實現兩個統一的任務實現類(有狀態和無狀態),通過JobDataMap傳遞配置數據,根據數據通過反射運行指定的真正實現類中的方法。
  2. 因為要傳遞配置數據,則單獨創建一個配置Bean用于傳遞。
  3. 而在解決如何收集業務實現類以提供選擇的問題上,決定使用自定義注解+Spring提供的注解掃描方式。
  4. 要實現統一的任務實現類到真正的任務實現類的數據傳遞,并實現Spring環境下的自動注入。
  5. 系統啟動時,需要一個初始化方法訪問數據并生成配置項,將配置好的任務全部添加調度。
  6. 提供任務增、刪、改、查以及啟動停止等方法工具,便于數據接口對任務進行處理。

不過,首先我們想要將Quartz和Spring配置在一起。

Spring結合Quartz的配置

添加依賴

Spring和Quartz運行在各自的容器中,需要一定的配置才能將二者融合在一起。Spring對Quartz的支持放在context-support包中,因此項目除了引入Quartz還需引入spring-context-support,要是Maven項目需要在pom.xml中添加如下內容:

<!-- quartz -->
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.3.0</version>
</dependency>

<!-- Spring -->
...
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
    <version>5.0.0.RELEASE</version>
</dependency>

Tips:版本看你自己,我這里是最新的5.0.0

添加配置

Spring的配置使用xml或者Configuration方式都可以。都是要設置以下幾個內容:

  • 載入Quartz的屬性配置文件quartz.properties
  • 設置BeanSchedulerFactoryBean將Spring和Quartz連接起來
  • 添加一個我們自定義的一個工廠類,這個類可以將Spring的上下文環境傳導到Quartz中

我們先添加這個自定義工廠類,內容為:

/**
 * Custom JobFactory, so that Job can be added to the Spring Autowired (Autowired)
 *
 * @author ProteanBear
 */
public class AutowiringSpringBeanJobFactory extends AdaptableJobFactory
{
    /**
     * Spring Context
     */
    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    /**
     * Create a Job instance, add Spring injection
     *
     * @param bundle A simple class (structure) used for returning execution-time data from the JobStore to the QuartzSchedulerThread
     * @return Job instance
     * @throws Exception throw from createJobInstance
     */
    @Override
    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception
    {
        Object jobInstance=super.createJobInstance(bundle);
        //Manually execute Spring injection
        capableBeanFactory.autowireBean(jobInstance);
        return jobInstance;
    }
}

這個類繼承自AdaptableJobFactory(一個由Spring提供的工廠類),在構建它時它存在于Spring上下文中,我們自動注入Spring提供的AutowireCapableBeanFactory,通過它就可以使用代碼在我們創建Job實例時進行自動注入處理。

這樣就可以開始添加配置文件(xml)或者Configuration類,二者選擇一個就好。

spring-quartz.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans-4.0.xsd">

    <!-- Load the configuration -->
    <bean id="quartzProperties" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
        <property name="locations">
            <list>
                <value>classpath*:quartz.properties</value>
            </list>
        </property>
        <property name="fileEncoding" value="UTF-8"></property>
    </bean>

    <!-- With a custom JobFactory, you can inject a Spring-related job into your job -->
    <bean id="jobFactory" class="com.github.proteanbear.libra.framework.AutowiringSpringBeanJobFactory">
    </bean>

    <!-- Task scheduling factory -->
    <bean id="schedulerFactoryBean" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
        <property name="jobFactory" ref="jobFactory"/>
        <property name="overwriteExistingJobs" value="true"/>
        <property name="quartzProperties" ref="quartzProperties"/>
        <!-- After the application is started, start the task by 5 seconds delay -->
        <property name="startupDelay" value="5"/>
        <!-- Configure the spring context through the applicationContextSchedulerContextKey property -->
        <property name="applicationContextSchedulerContextKey">
            <value>applicationContext</value>
        </property>
    </bean>
</beans>
Configuration
/**
 * Spring integrated timing task framework quartz configuration
 *
 * @author ProteanBear
 */
@Configuration
public class LibraQuartzConfiguration
{
    /**
     * Load the configuration
     *
     * @return the configuration properties
     * @throws IOException The properties set is error.
     */
    @Bean
    public Properties quartzProperties() throws IOException
    {
        PropertiesFactoryBean propertiesFactoryBean=new PropertiesFactoryBean();
        propertiesFactoryBean.setLocation(new ClassPathResource("quartz.properties"));
        propertiesFactoryBean.afterPropertiesSet();
        return propertiesFactoryBean.getObject();
    }

    /**
     * With a custom JobFactory, you can inject a Spring-related job into your job.
     *
     * @param applicationContext the application context
     * @return the job factory
     */
    @Bean
    public JobFactory jobFactory(ApplicationContext applicationContext)
    {
        return new AutowiringSpringBeanJobFactory();
    }

    /**
     * Task scheduling factory
     *
     * @param jobFactory the job factory
     * @return the scheduler factory
     * @throws IOException The properties set is error.
     */
    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory)
            throws IOException
    {
        SchedulerFactoryBean schedulerFactoryBean=new SchedulerFactoryBean();

        //configuration
        schedulerFactoryBean.setOverwriteExistingJobs(true);
        //After the application is started, start the task by 5 seconds delay
        schedulerFactoryBean.setStartupDelay(5);
        //Job factory
        schedulerFactoryBean.setJobFactory(jobFactory);
        //load properties
        schedulerFactoryBean.setQuartzProperties(quartzProperties());
        //Configure the spring context through the applicationContextSchedulerContextKey property
        schedulerFactoryBean.setApplicationContextSchedulerContextKey("applicationContext");

        return schedulerFactoryBean;
    }
}
quartz.properties
# Quartz配置
# 設置org.quartz.scheduler.skipUpdateCheck的屬性為true來跳過更新檢查
org.quartz.scheduler.skipUpdateCheck=true
# 線程池配置
org.quartz.threadPool.threadCount=8

Tips:屬性配置我這里只配置了兩項,欲知詳情請參見【官方文檔】。

使用xml的話,最后不要忘記載入配置文件,也有兩種方式:

  • 在web.xml中,載入Spring配置(不是SpringMVC的地方)的地方,即<context-param>下的<param-value>中增加一項classpath*:spring-quartz.xml,當然具體位置看你放的位置。
  • 在Spring的配置xml中(如application-context.xml)添加<import resource="classpath*:spring-quartz.xml"/>

自定義注解與自動注入

Spring+Quartz就配置好了,下面才開始真正的編碼開發。

創建兩個統一的任務實現類

分別為有狀態的QuartzJobDispatcherDisallow和無狀態的QuartzJobDispatcher,但其實二者主要是注解的區別,因此添加一個抽象父類AbstractQuartzJobDispatcher來統一反射調用方法。

/**
 * Central task super class to implement general configuration information parsing
 * and execution methods
 *
 * @author ProteanBear
 */
public abstract class AbstractQuartzJobDispatcher
{
    /**
     * Get the log object
     *
     * @return the log object
     */
    abstract protected Logger getLogger();

    /**
     * Get Spring injection factory class
     *
     * @return Spring injection factory class
     */
    abstract protected AutowireCapableBeanFactory getCapableBeanFactory();

    /**
     * Invoke the specified method by reflection
     *
     * @param jobTaskBean The job config
     * @param jobDataMap the job data
     */
    protected void invokeJobMethod(JobTaskBean jobTaskBean,JobDataMap jobDataMap)
    {
        ...
    }

    /**
     * Calculate the running time
     *
     * @param startTime The start time
     * @param endTime   The end time
     * @return The running time description
     */
    protected String calculateRunTime(Date startTime,Date endTime)
    {
        ...
    }
}

Tips:兩個抽象方法有子類實現,getLogger()可以讓子類提供在日志中可以區分任務是否有狀態;getCapableBeanFactory()將自動注入放入子類中,并獲取后繼續手動傳遞到真正反射指向的實現類中。

QuartzJobDispatcher繼承抽象父類AbstractQuartzJobDispatcher實現接口JobQuartzJobDispatcherDisallow內容一樣只是多了@DisallowConcurrentExecution注解),并實現execute方法。

/**
 * Generic stateless task,
 * which is responsible for the method of dispatching execution
 * through configuration information
 *
 * @author ProteanBear
 */
public class QuartzJobDispatcher extends AbstractQuartzJobDispatcher implements Job
{
    /**
     * Log
     */
    private static final Logger logger=LoggerFactory.getLogger(QuartzJobDispatcher.class);

    /**
     * Spring injection factory class
     */
    @Autowired
    private AutowireCapableBeanFactory capableBeanFactory;

    /**
     * Get the log object
     *
     * @return the log object
     */
    @Override
    protected Logger getLogger(){return logger;}

    /**
     * Get Spring injection factory class
     *
     * @return Spring injection factory class
     */
    @Override
    protected AutowireCapableBeanFactory getCapableBeanFactory(){return capableBeanFactory;}

    /**
     * Actuator
     *
     * @param context the job execution context
     * @throws JobExecutionException the exception
     */
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException
    {
        ...
    }
}

方法里我們需要從JobDataMap中獲取傳遞的數據,這個數據描述了實際要執行(即反射調用)的類的方法。

創建Bean描述任務類

JobTaskBean
屬性 類型 說明
key String 任務標識
title String 任務顯示名稱
group String 任務組名
description String 任務描述
taskClass Class 任務對應的實現類
methodList List<Method> 任務實現類中需要執行的方法
fieldSetMethodMap Map<String,Method> 任務實現類屬性設置方法,用于傳遞數據
concurrent boolean 任務是有狀態還是無狀態

兩個任務類(有狀態和無狀態)中獲取數據并調用抽象父類中的反射執行方法:

public void execute(JobExecutionContext context) throws JobExecutionException
{
    //Get recorded task configuration information
    JobTaskBean jobTaskBean=(JobTaskBean)context.getMergedJobDataMap().get(LibraKey.CONFIG.toString());

    //Execute the method specified by the configuration information
    invokeJobMethod(jobTaskBean,context.getMergedJobDataMap());
}

利用反射調用真正的實現類中的方法

invokeJobMethod方法主要是兩部分,第一通過反射將JobDataMap中同名稱的數據(通過fieldSetMethodMap)傳遞到指定的實現類(taskClass)中;第二是通過反射調用methodList中指定的方法:

protected void invokeJobMethod(JobTaskBean jobTaskBean,JobDataMap jobDataMap)
{
    //Job class
    Class jobClass=null;
    //Job method
    Object job=null;

    try
    {
        //Load config
        String name=jobTaskBean.getTitle();
        getLogger().info("Ready run job task for:"+jobTaskBean.toString());

        //Get the specified class and object
        jobClass=jobTaskBean.getTaskClass();
        job=jobClass.newInstance();
        if(job==null)
        {
            throw new Exception("Task【"+name+"】Task initialization error,dead start !");
        }
        //Spring autowire
        getCapableBeanFactory().autowireBean(job);

        //Pass the data
        Method setMethod=null;
        Object data=null;
        for(String field : jobTaskBean.getFieldSetMethodMap().keySet())
        {
            //Get the data
            data=jobDataMap.get(field);
            if(data==null) continue;
            setMethod=jobTaskBean.getFieldSetMethodMap().get(field);

            //Set the data
            try
            {
                setMethod.invoke(job,data);
            }
            catch(Exception ex)
            {
                ex.printStackTrace();
                getLogger().error(ex.getMessage());
            }
        }

        //Traverse execution of all annotation methods
        Method method=null;
        String methodName=null;
        for(int i=0, size=jobTaskBean.getMethodList().size();i<size;i++)
        {
            method=jobTaskBean.getMethodList().get(i);
            methodName=method.getName();

            //Invoke method
            getLogger().info("Start invoke job \""+name+"\"'s method:"+methodName);
            Date startTime=new Date();
            method.invoke(job);
            getLogger().info("Invoke method "+methodName+" of job "+name+" success.Use time "+calculateRunTime(
                startTime,new Date()));
        }
    }
    catch(Exception ex)
    {
        getLogger().error(ex.getMessage(),ex);
    }
}

通過自定義注解+Spring注解掃描,獲取任務實現類

上面可以看到反射其實很簡單,關鍵是我們怎么掃描到系統中的我們編寫實現類呢?

這里就用到了Spring提供的自定義注解掃描機制。首先添加一個自定義注解來標注我們的任務實現類,就叫做JobTask。并在為JobTask注解指定Spring的@Component,這樣Spring啟動時就會掃描到這個注解,而我們就可以通過Spring的上下文applicationContextgetBeansWithAnnotation方法獲取到標注了@JobTask注解的所有類。

我們再創建兩個自定義注解(JobTaskExecuteJobTaskData)分別用來標注要執行的任務方法和需要傳遞進來的數據屬性。這樣在系統啟動時運行初始化方法獲取到Spring上下文中標注了@JobTask注解的所有類,并遍歷它的屬性和方法查找自定義注解,最終生成JobTaskBean任務類描述并緩存在HashMap中。

初始化及遍歷查找注解生成任務類描述(來自JobTaskUtils
/**
 * Initialization
 */
private void init()
{
    logger.info("Start init job task!");

    if(this.applicationContext==null)
    {
        logger.error("ApplicationContext is null!");
        return;
    }

    //Initialization
    jobTaskMap=(jobTaskMap==null)?(new HashMap<>(20)):jobTaskMap;

    //Gets all the classes in the container with @JobTask annotations
    Map<String,Object> jobTaskBeanMap=this.applicationContext.getBeansWithAnnotation(JobTask.class);
    //Traverse all classes to generate task description records
    loadJobTaskBeans(jobTaskBeanMap);

    logger.info("Init job task success!");
}

/**
 * Load jobTaskBeans from map by the annotation
 *
 * @param jobTaskBeanMap the class map
 */
public void loadJobTaskBeans(Map<String,Object> jobTaskBeanMap)
{
    //Traverse all classes to generate task description records
    JobTask jobTaskAnnotation=null;
    JobTaskData jobTaskData=null;
    JobTaskExecute executeAnnotation=null;
    String key="";
    Method[] curMethods=null;
    Field[] curFields=null;
    Collection collection=jobTaskBeanMap.values();
    logger.info("Get class map at JobTask annotation by Spring,size is "+collection.size()+"!");
    for(Object object : collection)
    {
        //Get the class
        Class curClass=(object instanceof Class)
            ?((Class)object)
            :object.getClass();
        //Get annotation
        jobTaskAnnotation=(JobTask)curClass.getAnnotation(JobTask.class);
        if(jobTaskAnnotation==null) continue;

        //Get method annotation
        curMethods=curClass.getDeclaredMethods();
        List<Method> methodList=new ArrayList<>();
        for(int i=0, length=curMethods.length;i<length;i++)
        {
            Method curMethod=curMethods[i];
            executeAnnotation=curMethod.getAnnotation(JobTaskExecute.class);
            if(executeAnnotation!=null)
            {
                methodList.add(curMethod);
            }
        }
        //No method of operation, directly skip
        if(methodList.isEmpty()) continue;

        //Get field annotation @JobTaskData
        curFields=curClass.getDeclaredFields();
        Map<String,Method> fieldSetMethodMap=new HashMap<>();
        for(int i=0, length=curFields.length;i<length;i++)
        {
            Field curField=curFields[i];
            jobTaskData=curField.getAnnotation(JobTaskData.class);
            if(jobTaskData==null) continue;

            //Saved name
            String name=(StringUtils.isBlank(jobTaskData.value())?curField.getName():jobTaskData.value());
            //Get Field set method name
            String setMethodName="set"+curField.getName().substring(0,1).toUpperCase()+curField.getName()
                .substring(1);

            //Get set method
            Method setMethod=null;
            try
            {
                setMethod=curClass.getMethod(setMethodName,curField.getType());
            }
            catch(NoSuchMethodException e)
            {
                e.printStackTrace();
                logger.error(e.getMessage());
                continue;
            }
            if(setMethod==null) continue;

            //Put into map
            fieldSetMethodMap.put(name,setMethod);
        }

        //Generate a key
        key=("".equals(jobTaskAnnotation.value().trim()))?getDefaultTaskKey(curClass):jobTaskAnnotation.value();

        //Create a task description
        JobTaskBean jobTaskBean=new JobTaskBean(key,jobTaskAnnotation);
        //Set the current class
        jobTaskBean.setTaskClass(curClass);
        //Set all running methods
        jobTaskBean.setMethodList(methodList);
        //Set all field set method
        jobTaskBean.setFieldSetMethodMap(fieldSetMethodMap);
        //Set is jar class url
        jobTaskBean.setJarClassUrl(jarClassUrl(curClass));

        //Record
        jobTaskMap.put(key,jobTaskBean);

        logger.info("Record job task("+key+") for content("+jobTaskBean.toString()+")!");
    }
}

Tips:這個工具類(JobTaskUtils)實現了Spring的ApplicationContextAware這樣可以方便獲得ApplicationContext中的所有bean;并且這個類使用@Component注冊為Spring組件,使用@Scope標注為單例。

創建工具類操作任務

接下來我們創建一個工具類,將對任務的操作代碼封裝起來。可以通過它設置任務啟動、暫停、刪除以及立即運行。在此之前先要增加一個Bean,用來描述任務的配置屬性:

TaskConfigBean
屬性 類型 說明
taskId String 作為任務的唯一標識,默認使用Java的UUID生成
taskKey String 任務實現類標識,即@JobTask注解中的value
taskStatus int 任務狀態,0為禁用、1為啟用;為0時會刪除指定的任務
taskCron String Cron表達式,此字段不為空時使用Cron觸發器,為空時為使用間隔觸發器
jobDataMap JobDataMap 記錄要傳遞到任務實現類中的全部數據
taskInterval Integer taskCron為空時有效,指定間隔觸發器的間隔時間
taskIntervalType IntervalType taskCron為空時有效,指定間隔類型
taskIntervalRepeat Integer taskCron為空時有效,指定間隔重復頻率

Tips:IntervalType是個枚舉類型,包括SECOND(秒),MINUTE(分),HOUR(小時)。

然后Bean里需要實現一個生成當前任務時間配置的方法,即根據當前的設置生成一個時間生成器(Quartz里的ScheduleBuilder),具體內容如下:

public ScheduleBuilder scheduleBuilder() throws SchedulerException
{
    //Cron
    if(StringUtils.isNotBlank(taskCron))
    {
        return CronScheduleBuilder.cronSchedule(taskCron);
    }
    //Simple
    ScheduleBuilder result=null;
    if(taskInterval==null)
    {
        throw new SchedulerException("Timing settings must not be null!");
    }
    switch(taskIntervalType)
    {
        case SECOND:
            result=SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInSeconds(taskInterval)
                    .withRepeatCount(taskIntervalRepeat);
            break;
        case MINUTE:
            result=SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInMinutes(taskInterval)
                    .withRepeatCount(taskIntervalRepeat);
            break;
        case HOUR:
            result=SimpleScheduleBuilder.simpleSchedule()
                    .withIntervalInHours(taskInterval)
                    .withRepeatCount(taskIntervalRepeat);
    }
    return result;
}

然后就可以編寫工具類中的設置任務方法了:

ScheduleJobUtils.java
public final void set(TaskConfigBean jobConfig) throws SchedulerException
{
    if(jobConfig==null)
    {
        throw new SchedulerException("Object jobConfig is null.");
    }

    //Get the corresponding task class record
    JobTaskBean jobTaskBean=jobTaskUtils.getJobTask(jobConfig.getTaskKey());
    if(jobTaskBean==null) return;

    //Read the parameters
    //Name for the task key + configuration task id
    String name=key(jobConfig.getTaskId(),jobConfig.getTaskKey());
    String group=jobTaskBean.getGroup();
    Integer status=jobConfig.getTaskStatus();
    boolean concurrent=jobTaskBean.isConcurrent();

    //Build the task
    Scheduler scheduler=schedulerFactoryBean.getScheduler();
    TriggerKey triggerKey=TriggerKey.triggerKey(name,group);
    Trigger trigger=scheduler.getTrigger(triggerKey);

    //The task already exists, then delete the task first
    if(trigger!=null)
    {
        logger.info("Delete job task(name:"+name+",group:"+group+")");
        scheduler.deleteJob(JobKey.jobKey(name,group));
    }

    //Create a task
    Class jobClass=concurrent?QuartzJobDispatcherDisallow.class:QuartzJobDispatcher.class;
    JobDetail jobDetail=JobBuilder.newJob(jobClass)
            .withIdentity(name,group).build();
    //Set the transmission data
    jobDetail.getJobDataMap().put(LibraKey.CONFIG.toString(),jobTaskBean);
    jobDetail.getJobDataMap().putAll(jobConfig.getJobDataMap());

    //Create a timer
    trigger=TriggerBuilder.newTrigger().withIdentity(name,group)
            .withSchedule(jobConfig.scheduleBuilder()).build();

    //Add tasks to the schedule
    scheduler.scheduleJob(jobDetail,trigger);
    logger.info("Add job task(name:"+name+",group:"+group+")");

    //Task is disabled, pause the job
    if(status==0) pauseJob(jobConfig.getTaskId(),jobConfig.getTaskKey());
}

private final String key(String taskId,String taskKey)
{
    return taskKey+"_"+taskId;
}

private final JobKey jobKey(String taskId,String taskKey) throws SchedulerException
{
    //Get the task properties
    JobTaskBean jobTaskBean=jobTaskUtils.getJobTask(taskKey);
    if(jobTaskBean==null)
    {
        throw new SchedulerException("No task class for key:"+taskKey);
    }

    return JobKey.jobKey(key(taskId,taskKey),jobTaskBean.getGroup()+"");
}

private final TriggerKey triggerKey(String taskId,String taskKey) throws SchedulerException
{
    //Get the task properties
    JobTaskBean jobTaskBean=jobTaskUtils.getJobTask(taskKey);
    if(jobTaskBean==null)
    {
        throw new SchedulerException("No task class for key:"+taskKey);
    }

    return TriggerKey.triggerKey(key(taskId,taskKey),jobTaskBean.getGroup()+"");
}

Tips:這里只展示了設置方法,以及Key生成相關的私有方法,諸如啟動、暫停、刪除等操作不過是生成Key,然后調用Quartz提供的方法處理就好了。

這樣動態任務的框架封裝完成,你可以自己設計數據庫(關系型或者Key-value都可以),在Spring啟動時讀取數據然后生成TaskConfigBean初始化任務;并且為數據操作提供增刪改查時調用ScheduleJobUtils對實際運行的任務進行處理。

Tips:啟動時初始化可以使用Spring的@PostConstruct注解。

Tips:注意ScheduleJobUtils調用的Quartz中的resumeJobdeleteJob都是必須任務代碼執行完成后才會暫停和刪除。

Dubbo環境下的自動注入

前文看到Spring提供了非常方便的方法為Bean手動實現自動注入,只需注入AutowireCapableBeanFactory然后執行:

capableBeanFactory.autowireBean(job);

但是筆者在工作中使用時遇到了項目使用了Dubbo框架,也就是我們的任務調度服務中實現的任務處理類需要使用@Reference注解調用遠程服務,所以需要解決的是這類服務類自動注入的問題。查詢后發現Dubbo并未提供類似的方法(我們那時是Dubbo2,不知道Dubbo3會不會有)。那如何處理呢?!眾所周知Dubbo是使用的RPC協議,并且是使用Zookeeper作為注冊中心的由阿里提供的開源框架……所以說重點在于……開源啊,直接找源碼來看看Dubbo自己是怎么注入的呢(吐!那說其他的一堆有啥用!)。經過一番努力和整理后,在統一任務類的父類(AbstractQuartzJobDispatcher)中添加如下注入方法:

/**
 * Instantiate Dubbo service annotation @Reference.
 *
 * @param bean the bean
 */
private void referenceBean(Object bean)
{
    //Set by the set method
    Method[] methods=bean.getClass().getMethods();
    for(Method method : methods)
    {
        String name=method.getName();
        if(name.length()>3 && name.startsWith("set")
                && method.getParameterTypes().length==1
                && Modifier.isPublic(method.getModifiers())
                && !Modifier.isStatic(method.getModifiers()))
        {
            try
            {
                Reference reference=method.getAnnotation(Reference.class);
                if(reference!=null)
                {
                    Object value=refer(reference,method.getParameterTypes()[0]);
                    if(value!=null)
                    {
                        method.invoke(bean,new Object[]{});
                    }
                }
            }
            catch(Throwable e)
            {
                getLogger().error("Failed to init remote service reference at method "+name+" in class "+bean
                        .getClass().getName()+", cause: "+e.getMessage(),e);
            }
        }
    }

    //Through the property settings
    Field[] fields=bean.getClass().getDeclaredFields();
    for(Field field : fields)
    {
        try
        {
            if(!field.isAccessible())
            {
                field.setAccessible(true);
            }

            Reference reference=field.getAnnotation(Reference.class);
            if(reference!=null)
            {
                //Refer method interested can see for themselves, involving zk and netty
                Object value=refer(reference,field.getType());
                if(value!=null)
                {
                    field.set(bean,value);
                }
            }
        }
        catch(Throwable e)
        {
            getLogger().error(
                    "Failed to init remote service reference at filed "+field.getName()+" in class "+bean.getClass()
                            .getName()+", cause: "+e.getMessage(),e);
        }
    }
}

/**
 * Instantiate the corresponding Dubbo service object.
 *
 * @param reference The annotation of Reference
 * @param referenceClass The class of Reference
 * @return
 */
private Object refer(Reference reference,Class<?> referenceClass)
{
    //Get the interface name
    String interfaceName;
    if(!"".equals(reference.interfaceName()))
    {
        interfaceName=reference.interfaceName();
    }
    else if(!void.class.equals(reference.interfaceClass()))
    {
        interfaceName=reference.interfaceClass().getName();
    }
    else if(referenceClass.isInterface())
    {
        interfaceName=referenceClass.getName();
    }
    else
    {
        throw new IllegalStateException(
                "The @Reference undefined interfaceClass or interfaceName, and the property type "
                        +referenceClass.getName()+" is not a interface.");
    }

    //Get service object
    String key=reference.group()+"/"+interfaceName+":"+reference.version();
    ReferenceBean<?> referenceConfig=referenceConfigs.get(key);
    //Configuration does not exist, find service
    if(referenceConfig==null)
    {
        referenceConfig=new ReferenceBean<Object>(reference);
        if(void.class.equals(reference.interfaceClass())
                && "".equals(reference.interfaceName())
                && referenceClass.isInterface())
        {
            referenceConfig.setInterface(referenceClass);
        }

        ApplicationContext applicationContext=getApplicationContext();
        if(applicationContext!=null)
        {
            referenceConfig.setApplicationContext(applicationContext);

            //registry
            if(reference.registry()!=null && reference.registry().length>0)
            {
                List<RegistryConfig> registryConfigs=new ArrayList<RegistryConfig>();
                for(String registryId : reference.registry())
                {
                    if(registryId!=null && registryId.length()>0)
                    {
                        registryConfigs
                                .add(applicationContext.getBean(registryId,RegistryConfig.class));
                    }
                }
                referenceConfig.setRegistries(registryConfigs);
            }

            //consumer
            if(reference.consumer()!=null && reference.consumer().length()>0)
            {
                referenceConfig.setConsumer(applicationContext.getBean(reference.consumer(),ConsumerConfig.class));
            }

            //monitor
            if(reference.monitor()!=null && reference.monitor().length()>0)
            {
                referenceConfig.setMonitor(
                        (MonitorConfig)applicationContext.getBean(reference.monitor(),MonitorConfig.class));
            }

            //application
            if(reference.application()!=null && reference.application().length()>0)
            {
                referenceConfig.setApplication((ApplicationConfig)applicationContext
                        .getBean(reference.application(),ApplicationConfig.class));
            }

            //module
            if(reference.module()!=null && reference.module().length()>0)
            {
                referenceConfig.setModule(applicationContext.getBean(reference.module(),ModuleConfig.class));
            }

            //consumer
            if(reference.consumer()!=null && reference.consumer().length()>0)
            {
                referenceConfig.setConsumer(
                        (ConsumerConfig)applicationContext.getBean(reference.consumer(),ConsumerConfig.class));
            }

            try
            {
                referenceConfig.afterPropertiesSet();
            }
            catch(RuntimeException e)
            {
                throw e;
            }
            catch(Exception e)
            {
                throw new IllegalStateException(e.getMessage(),e);
            }
        }

        //Configuration
        referenceConfigs.putIfAbsent(key,referenceConfig);
        referenceConfig=referenceConfigs.get(key);
    }
    return referenceConfig.get();
}

在反射方法(invokeJobMethod)中調用:

//Spring autowire
getCapableBeanFactory().autowireBean(job);
//Dubbo service injection
referenceBean(job);

經過如上的封裝后,等于是在Quartz的基礎上搭建了一層與具體業務脫離的動態任務處理層,它可以使用在任何有此需求的項目(因為基于Quartz所以對大型的分布式項目支持就一般)中,前面也說過我已經將它開源出來,取名為Libra。

關于Libra

Libra:這里的取的意思是天秤座,沒啥深層含義就是取個名字。

開源項目地址Github-ProteanBear-Libra

項目最新版本v1.1.1

關于項目分支develop(開發)、master(主干)、libra-dubbo(帶Dubbo支持)

關于項目結構

  • libra-test-dubbo-client:測試項目,Dubbo服務消費者;僅libra-dubbo分支。
    • src/main
      • java/com/github/proteanbear/test
        • TaskService.java:任務初始化
        • TestLongTimeRunTask.java:任務實現類
      • resources
        • application-context.xml:Spring配置
        • dubbo-consumer.xml:Dubbo服務消費者配置
        • log4j.properties:日志配置
        • quartz.properties:Quartz配置
      • webapp/WEB-INF
        • web.xml
    • pom.xml:Maven項目配置
  • libra-test-dubbo-common:測試項目,Dubbo服務接口定義;僅libra-dubbo分支。
    • src/main/java/com/github/proteanbear/test
      • DubboHelloService:Dubbo測試服務接口定義
    • pom.xml:Maven項目配置
  • libra-test-dubbo-service:測試項目,Dubbo服務提供者;僅libra-dubbo分支。
    • src/main
      • java/com/github/proteanbear/test
        • DubboHelloServiceImpl:Dubbo測試服務接口實現
      • resources
        • application-context.xml:Spring配置
        • log4j.properties:日志配置
        • spring-dubbo.xml:Dubbo服務提供者配置
      • webapp/WEB-INF
        • web.xml
    • pom.xml:Maven項目配置
  • libra-test-jar-scan:測試項目,用于生成通用的服務類以及任務實現類的Jar包;其他測試項目使用ScanUtils掃描工具動態加載Jar包內容并設置任務。
    • src/main/java/com/github/proteanbear/test
      • HelloService:Spring的Service,“Hello Libra!”
      • TestLongTimeRunTask.java:任務實現類
    • pom.xml:Maven項目配置
  • libra-test-jar-xml:測試項目,使用jar包載入依賴,使用xml配置。
    • src/main
      • java/com/github/proteanbear/test
        • TaskService:任務初始化
      • resources
        • application-context.xml:Spring配置
        • log4j.properties:日志配置
        • quartz.properties:Quartz配置
      • webapp/WEB-INF
        • web.xml
    • pom.xml:Maven項目配置
  • libra-test-maven-configuration:測試項目,使用Maven載入項目依賴,使用Configuration配置。
    • src/main
      • java/com/github/proteanbear/test
        • TaskService:任務初始化
      • resources
        • application-context.xml:Spring配置
        • log4j.properties:日志配置
        • quartz.properties:Quartz配置
      • webapp/WEB-INF
        • web.xml
    • pom.xml:Maven項目配置
  • libra-test-maven-xml:測試項目,使用Maven載入項目依賴,使用xml配置。
    • src/main
      • java/com/github/proteanbear/test
        • TaskService:任務初始化
      • resources
        • application-context.xml:Spring配置
        • log4j.properties:日志配置
        • quartz.properties:Quartz配置
      • webapp/WEB-INF
        • web.xml
    • pom.xml:Maven項目配置
  • libra:框架項目
    • src/main
      • java/com/github/proteanbear/libra
        • configuration
          • LibraQuartzConfiguration:Spring+Quartz的Configuration配置
        • framework
          • AbstractQuartzJobDispatcher:統一任務類的抽象父類,實現反射方法
          • AutowiringSpringBeanJobFactory:自定義Spring中的任務生成工廠
          • JobTask:自定義注解,標注任務實現類
          • JobTaskBean:任務實現類信息描述
          • JobTaskData:自定義注解,標注任務實現類數據傳輸屬性
          • JobTaskExecute:自定義注解,標注任務執行方法
          • LibraKey:枚舉,指定項目中的通用常量
          • QuartzJobDispatcher:統一任務類,無狀態
          • QuartzJobDispatcherDisallow:統一任務類型,有狀態
          • TaskConfigBean:任務配置描述
        • utils
          • JobTaskUtils:任務實現類管理工具,Spring組件、單例
          • ScanUtils:文件夾及Jar包動態掃描工具,Spring組件、單例
          • ScheduleJobUtils:任務管理工具,Spring組件
          • StringUtils:字符串處理工具,全靜態方法
      • resources
        • spring-quartz.xml:Spring+Quartz的xml配置
    • pom.xml:Maven項目配置

關于兩種配置

  • Spring+Quartz的xml配置引入,Spring配置xml中添加:
<import resource="classpath*:spring-quartz.xml"/>
  • Spring+Quartz的Configuration配置引入,Spring配置xml中添加:
<context:annotation-config />

Tips:注意一定要在使用項目中添加quartz.proterties文件。

Tips:SpringBoot默認支持Configuration方式。

關于動態載入

在v1.1.0版本中加入了ScanUtils支持Jar包的動態載入,使用scanUtils.scan(jarFile)就可以動態載入Jar包中的內容。

測試項目中libra-test-jar-scan就是用于生成Jar包的項目,測試項目libra-test-maven-xml中配置了Maven依賴libra-test-jar-scan所以可以直接調用項目libra-test-jar-scan中的任務實現類;而測試項目libra-test-maven-configuration未在Maven配置中依賴此項目,但是在初始化方法中通過在使用ScanUtils動態加載了Jar包,同樣可以調用項目libra-test-jar-scan中的任務實現類。

Tips:在測試要自己去生成libra-test-jar-scan項目的Jar包,并在掃描前注意一下文件位置是否正確。

Quartz的動態任務調度就到這里了,這里主要還是集中于單機環境下,再有一章計劃是探討一下分布式任務調度。

文章有些長,如果是從頭看到尾,那在這里真心感謝您的支持!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,947評論 18 139
  • Spring Boot 參考指南 介紹 轉載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,958評論 6 342
  • 朋友說她們辦公室有位女同事同齡人心機很重套路特別多,人前一套人后一套,明明坑了人還表現出一副在幫別人的樣子。仗著做...
    森林_木閱讀 385評論 1 3
  • 媽媽前幾天去了東北,給姥姥上墳,留下我和我爸兩人在家。媽媽不在家,活就變成了我和老爸的。可爸爸不舍得我干會...
    向日葵223344閱讀 220評論 0 0