项目中有模块依赖到了Quartz来做定时任务。那天和大师研究了一下午,讲一个使用这个工具的一些收获。
首先,用的不是原先的Quartz,而是与spring整合的。需要用到Spring-Conte-Support-4.2.3.Release.jar,Quartz-2.2.2.jar。使用的方式如下
<pre name="code" class="html"><pre name="code" class="html"><pre name="code" class="html"><?xml version="1.0" encoding="utf-8"?>
<beans>
<!-- 定时清理 MessageRelation和hadsend Map 1 -->
<bean id="clearRelationJob" class="com.yicong.kisp.job.ClearRelationAndHadsendJob"/>
<!-- JobDetajil,基于MethodInvokingJobDetailFactoryBean调用普通Spring Bean 2 -->
<bean id="clearRelationJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean">
<property name="targetObject" ref="clearRelationJob"/>
<property name="targetMethod" value="doClear"/>
<!-- 同一任务在前一次执行未完成而Trigger时间又到时是否并发开始新的执行, 默认为true. -->
<property name="concurrent" value="false"/>
</bean>
<!-- Cron式Trigger定义 3 -->
<bean id="clearRelationJobTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean">
<property name="jobDetail" ref="clearRelationJobDetail"/>
<property name="misfireInstruction" value="2"/>
<!-- 全年、周2,4,6、00:01:01 -->
<property name="cronExpression" value="1 1 0 ? 1-12 2,4,6 *"/>
<!-- 延迟10秒启动 -->
<property name="startDelay" value="10000"/>
</bean>
<!-- 调度器 4 -->
<bean id="schedulerFactoryBean" lazy-init="false" autowire="no" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="triggers">
<list>
<ref bean="clearRelationJobTrigger"/>
</list>
</property>
</bean>
</beans>
使用xml文件进行配置,都是“四段式”的配置方法。
1是自己写的来做具体逻辑处理的类
2是JobDetail,MethodInvokingJobDetailFactoryBean是spring对Quartz的JobDetail的包装,在它里面,定义了一个来自org.quartz包的JobDetail。
public class MethodInvokingJobDetailFactoryBean extends ArgumentConvertingMethodInvoker
implements FactoryBean<JobDetail>, BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean {
private String name;
private boolean concurrent = true;
private String targetBeanName;
private String beanName;
private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
private BeanFactory beanFactory;
private JobDetail jobDetail;
......
spring中大量用到了FactoryBean,这个接口的说明:
Interface to be implemented by objects used within a BeanFactory
which are themselves factories. If a bean implements this interface, it is used as a factory for an object to expose, not directly as a bean instance that will be exposed itself.
NB: A bean that implements this interface cannot be used as a normal bean. A FactoryBean is defined in a bean style, but the object exposed for bean references (getObject()
is always the object that it creates.
大意是说一个bean实现了它,通常用来作为一个向外暴露对象的工厂,而非直接得到它本身,向外暴露的对象由泛型指定,通过覆盖的getObject()返回真正的对象。
This interface is heavily used within the framework itself, for example for the AOPorg.springframework.aop.framework.ProxyFactoryBean
or theorg.springframework.jndi.JndiObjectFactoryBean
. It can be used for application components as well; however, this is not common outside of infrastructure code.
本接口被框架本身大量使用,如果AOP‘动态代理方面。然而在基础代码外少见。
2中的targetObject、targetMethod指定了类与方法,还有一些属性配置未写出来。
当2中的JobDetail准备完了,就可以被3引用了。3用来配置一个基于Cron的触发器。也是用到了FactoryBean。Trigger的作用在于根据Cron中的设置,定时触发job,所以还有concurrent、misfireInstructiont等可以配置。
4中把Trigger注册到Scheduler中,Scheduler配置线程池的大小。一个Job运行时消耗一个线程。
就这么多。就开始就这么用,感觉也还可以,但是改了什么属性就要重启应用,显得不灵活方便。虽然Quartz也有基于数据库表的,但是配套的表有十多张,有些用不上也不能删。想着业务表才十多张,Quartz也要这么多,很不舒服。
最方便的做法就是:
1、把必须要的JobDetail和Trigger的属性移到一张表中,在应用启动后自动读取表数据,初始化job并运行。
2、在管理页面中可以控制Job的运行、停止状态。
That's all。
那就要进行相应的改造了。由于已经用了spring,不考虑用原先的quartz,那么能不能人这四段代码中抽取出来,因为以xml的形式,一个job的运行,只需要这四段代码。找到了源码包,先找开MethodInvokingJobDetailFactoryBean,关键代码如下:
@Override
@SuppressWarnings("unchecked")
public void afterPropertiesSet() throws ClassNotFoundException, NoSuchMethodException {
prepare();
// Use specific name if given, else fall back to bean name.
String name = (this.name != null ? this.name : this.beanName);
// Consider the concurrent flag to choose between stateful and stateless job.
Class<?> jobClass = (this.concurrent ? MethodInvokingJob.class : StatefulMethodInvokingJob.class);
// Build JobDetail instance.
JobDetailImpl jdi = new JobDetailImpl();
jdi.setName(name);
jdi.setGroup(this.group);
jdi.setJobClass((Class) jobClass);
jdi.setDurability(true);
jdi.getJobDataMap().put("methodInvoker", this);
this.jobDetail = jdi;
postProcessJobDetail(this.jobDetail);
}
afterPropertiesSet()实现的是InitializingBean中的方法。根据注释,Interface to be implemented by beans that need to react once all their properties have been set by a BeanFactory。实现了这个接口的类,当属性都设置完成,将执行一次。在这里一个JobDetail对象封装完成,postProcessJobDetail没有默认实现,空方法。
CronTriggerFactoryBean也是相同道理:
@Override
public void afterPropertiesSet() throws ParseException {
......
CronTriggerImpl cti = new CronTriggerImpl();
cti.setName(this.name);
cti.setGroup(this.group);
cti.setJobKey(this.jobDetail.getKey());
cti.setJobDataMap(this.jobDataMap);
cti.setStartTime(this.startTime);
cti.setCronExpression(this.cronExpression);
cti.setTimeZone(this.timeZone);
cti.setCalendarName(this.calendarName);
cti.setPriority(this.priority);
cti.setMisfireInstruction(this.misfireInstruction);
cti.setDescription(this.description);
this.cronTrigger = cti;
}
最后是SchedulerFactoryBean,代码略。
弄清了思路,基本上是这样子的:
@SuppressWarnings({ "rawtypes", "unchecked" })
public void startJob(Integer jobId, String jobName, String method,
String clazz, String cron, String startDelay, String triggerName,
Properties p) throws Exception
{
if (MessageContainer.quartzMap.containsKey(jobName)) {
System.out.println("已经有名为" + jobName + "的Job了");
}else {
/*
* JobDetail
*/
MethodInvokingJobDetailFactoryBean methodJD = new MethodInvokingJobDetailFactoryBean();
methodJD.setName(jobName);
/*
* //根据类名获取Class对象 Class c=Class.forName(clazz); //参数类型数组 Class[]
* parameterTypes={Integer.class}; //根据参数类型获取相应的构造函数
* java.lang.reflect.Constructor
* constructor=c.getConstructor(parameterTypes); //参数数组 Object[]
* parameters={jobId}; //根据获取的构造函数和参数,创建实例 Object
* o=constructor.newInstance(parameters);
*/
// 数据库中类路径是com.xxx.Test形式的字符串,通过反射获取一个实例。Test中可能使用了@autowire注入了其他对象,
// 所以必须要从spring中get出来,不然o里面注入的都是空的
Class c = Class.forName(clazz);
Object o = BeanHoldFactory.getApplicationContext().getBean(c);
// 这里通过setJobId方法向Test对象中传入了一个值
Method mth = c.getMethod("setJobId", Integer.class);
mth.invoke(o, jobId);
methodJD.setTargetObject(o);
methodJD.setTargetMethod(method);
methodJD.afterPropertiesSet();
JobDetail jd = methodJD.getObject();
/*
* Trigger
*/
CronTriggerFactoryBean crTiger = new CronTriggerFactoryBean();
crTiger.setCronExpression(cron);
crTiger.setName(triggerName);
crTiger.setStartDelay(Integer.valueOf(startDelay));
crTiger.setJobDetail(jd);
//crTiger.setMisfireInstruction(Integer.valueOf(misfire));
crTiger.afterPropertiesSet();
Trigger trigger = crTiger.getObject();
/*
* scheduler
*/
Properties p = new Properties();
p.setProperty("org.quartz.threadPool.threadCount", "1");
p.setProperty("org.quartz.scheduler.skipUpdateCheck", "true");
SchedulerFactoryBean scheduler = new SchedulerFactoryBean();
// setTriggers(... triggers)可一次性传入多个trigger
scheduler.setTriggers(trigger);
scheduler.setQuartzProperties(p);
scheduler.afterPropertiesSet();
MessageContainer.quartzMap.put(jobName, scheduler);
scheduler.start();
}
}
可以看出JobDetail,Trigger和Scheduler都是new出来的,一一对应,这是因为在scheduler只有setTrigger,如果在页面新加一个job,相同的代码执行一遍,每set一次,原有的trigger就会丢失,而且线程数不对。用一个HashMap来存储scheduler。这样每次add时,可以通过contains(jobName)来判断是不已经存在。
你可以使用ApplicationListener在spring容器完成时,先把表的数据读到内存中,然后再看有个job,就循环执行多少次。Scheduler也提供了destroy()来销毁整个调度器,这样注册在上面的所有trigger都会消失,线程终结,这是最彻底的方式。
我的表设计,可以参考一下。
总体来说,目前基本满足了要求。既不要一大堆表,又可以数据库配置,不重启应用。这也告诉我一件事,了解一下别人是怎么写代码的是挺有意思的。都是牛人。
如果大家有更好的方法,敬请赐教。