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運行調度的流程如下:
- 通過觸發器工廠(SchedulerFactory的實現類)創建一個調度器(Scheduler);
- 創建一個任務實例(JobDetail),為它指定實現了Job接口的實現類(示例中的HelloWord.class),并指定唯一標識(Identity)組(示例中的“group1”)和名稱(示例中的“myJob”);
- 創建一個觸發器(Trigger),為它指定時間觸發規則(示例中的
simpleSchedule()
生成的SimpleTrigger),并指定唯一標識(Identity)組(示例中的“group1”)和名稱(示例中的“myTrigger”) - 最后通過調度器(Scheduler)將任務實例(JobDetail)和觸發器(Trigger)綁定在一起,并通過
start()
方法開啟任務調度。
Tips:當然Quartz還涉及到線程池等其他內容,你可以通過“Quartz原理揭秘和源碼解讀”或“Quartz原理解析”對Quartz的原理有更深入的了解,本篇文章就不詳細展開了。
如何實現動態任務調度
了解了原理就可以開始構思如何實現動態任務調度了,其實從上面可以看出Quartz已經很好的支持了任務的動態創建、修改和刪除,而我們的重點是如何抽象它以及想達到什么樣的程度。
我們的需求是通過數據庫(關系型或非關系型)來記錄當前執行的任務處理,已經配置好的任務在系統啟動時應該自動運行起來。另外,可以在系統運行時添加新的任務、對任務進行啟動或停止、重新設置任務運行時間,還可以刪除任務。
這里在系統運行時可以添加就涉及到兩個不同的情況:
- 添加任務指定的實現類(實現了Quartz的
Job
接口)已經存在于項目中 - 添加任務指定的實現類(實現了Quartz的
Job
接口)不存在于項目中,需要讓系統動態加載jar包
而對于實現類的處理也產生了兩種方案:
-
方案一:業務相關的實現類直接實現
Job
接口 -
方案二:編寫統一的實現類實現
Job
接口,在統一實現類中使用反射指定到某個類方法執行
這里基于脫離任務狀態區分、更好解耦及方便拓展等方面考慮最終選擇了方案二。
Tips:在后面可以看到,方案二更加靈活。
補充一下Quartz中的任務狀態:
無狀態Job:默認情況下都為此類型,可以并發執行。
有狀態Job(StatefulJob):同一個實例(JobDetail)不能同時運行。在Quartz舊版本中實現StatefulJob接口,新版已經廢棄了此接口,使用注解(
@DisallowConcurrentExecution
)實現。這里使用方案二會創建一個無狀態和一個有狀態的統一任務實現類,但真正的業務任務類可以是一個。
確定好方案,便整理一下我們的具體實現思路如下:
- 實現兩個統一的任務實現類(有狀態和無狀態),通過JobDataMap傳遞配置數據,根據數據通過反射運行指定的真正實現類中的方法。
- 因為要傳遞配置數據,則單獨創建一個配置Bean用于傳遞。
- 而在解決如何收集業務實現類以提供選擇的問題上,決定使用自定義注解+Spring提供的注解掃描方式。
- 要實現統一的任務實現類到真正的任務實現類的數據傳遞,并實現Spring環境下的自動注入。
- 系統啟動時,需要一個初始化方法訪問數據并生成配置項,將配置好的任務全部添加調度。
- 提供任務增、刪、改、查以及啟動停止等方法工具,便于數據接口對任務進行處理。
不過,首先我們想要將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
- 設置Bean
SchedulerFactoryBean
將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
實現接口Job
(QuartzJobDispatcherDisallow
內容一樣只是多了@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的上下文applicationContext
的getBeansWithAnnotation
方法獲取到標注了@JobTask
注解的所有類。
我們再創建兩個自定義注解(JobTaskExecute
和JobTaskData
)分別用來標注要執行的任務方法和需要傳遞進來的數據屬性。這樣在系統啟動時運行初始化方法獲取到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中的resumeJob
和deleteJob
都是必須任務代碼執行完成后才會暫停和刪除。
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
- java/com/github/proteanbear/test
- pom.xml:Maven項目配置
- src/main
-
libra-test-dubbo-common:測試項目,Dubbo服務接口定義;僅libra-dubbo分支。
- src/main/java/com/github/proteanbear/test
- DubboHelloService:Dubbo測試服務接口定義
- pom.xml:Maven項目配置
- src/main/java/com/github/proteanbear/test
-
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
- java/com/github/proteanbear/test
- pom.xml:Maven項目配置
- src/main
-
libra-test-jar-scan:測試項目,用于生成通用的服務類以及任務實現類的Jar包;其他測試項目使用
ScanUtils
掃描工具動態加載Jar包內容并設置任務。- src/main/java/com/github/proteanbear/test
- HelloService:Spring的Service,“Hello Libra!”
- TestLongTimeRunTask.java:任務實現類
- pom.xml:Maven項目配置
- src/main/java/com/github/proteanbear/test
-
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
- java/com/github/proteanbear/test
- pom.xml:Maven項目配置
- src/main
-
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
- java/com/github/proteanbear/test
- pom.xml:Maven項目配置
- src/main
-
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
- java/com/github/proteanbear/test
- pom.xml:Maven項目配置
- src/main
-
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:字符串處理工具,全靜態方法
- configuration
- resources
- spring-quartz.xml:Spring+Quartz的xml配置
- java/com/github/proteanbear/libra
- pom.xml:Maven項目配置
- src/main
關于兩種配置:
- 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的動態任務調度就到這里了,這里主要還是集中于單機環境下,再有一章計劃是探討一下分布式任務調度。
文章有些長,如果是從頭看到尾,那在這里真心感謝您的支持!