本来想用独立的quartz2.2,但是它没有和spring集成,自己维护事物和数据源总感觉不完美。quartz提供的原生线程池,虽然官方说是测试友好的,但是功能略显单一。不支持动态缩减,配置多少就是多少。而与Spring整合后,spring提供更好的线程实现,还有对jdbcStore的实现。我公司一直用spring2.5,从来不说升级。这样也无法使用quartz2。迫不得已只能用quartz的1版本。
由于使用Spring xml直接配置任务和触发器一大堆东西,过于繁琐。希望有一个模块化的调度器支持包,能够简单的配置一下任务类路径和执行时间就可以实现调度。同时不失去Spring提供的特性。即支持配置的方式初始化任务,也支持运行中动态加入新的任务或新的触发器。这样就有如下一个简单设计思路:
一,使用Spring的工厂org.springframework.scheduling.quartz.SchedulerFactoryBean来生成调度器实例。
例如作如下配置:
<!-- 设置调度 -->
<bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
<property name="taskExecutor" ref="executor" />
<property name="dataSource" ref="dataSource"></property>
<property name="transactionManager" ref="initJobTxManager"></property>
<property name="applicationContextSchedulerContextKey" value="context"></property>
</bean>
taskExecutor是线程池,其配置决定了同时能有多少个任务执行。
dataSource是数据源,Quartz需要访问数据库存储自己的触发器,任务描述等信息
transactionManager是Spring的事物管理器,仅在Spring初始化任务和触发器时进行事物控制。这个与任务执行时的事物无关。对于我的模块没什么意义。
applicationContextSchedulerContextKey这个key的存在,就是为了在任务实现类中可以拿到当前ApplicationContext对象
这个工厂能够设置的属性还有很多,包括JOB和triggle,都是与Quartz的调度器初始化相关,即使这里无法配置一些属性,使用quartz.properties也同样生效。但是Spring建议不要在quartz.properties中配置重复的属性。这里一般不需要quartz.properties文件。
本模块不会通过Spring初始化任何的Job或者Triggle。因为希望由自己的接口来初始化。
<bean id="scheduler" class="com.privatedoctor.scheduler.service.PrivateSchedulerQuartzImpl">
<property name="core" ref="schedulerFactory"/>
</bean>
具体接口如下所示,基于Spring的努力,我们几乎不用考虑底层的实现,把已经初始化好的schedulerFactory拿过来用就行。我们只考虑接口本身如何配置和动态加入任务。
public interface IPrivateScheduler extends InitializingBean,DisposableBean,Lifecycle{
/**
* 添加任务
* <p>
* 如果覆盖属性为false,不会执行添加;否则会删除已经存在的任务。
* </p>
* @param config
* @throws PrivateSchedulerException
*/
public void addJob(JobConfig config) throws PrivateSchedulerException;
/**
* 为已存在的任务新增触发时间<br/>
* 注意:一个任务理论上支持任意个触发时间,实际受到线程池或数据库连接数限制
* @param jobID
* @param cron
* @throws PrivateSchedulerException
*/
public void schedulerJob(JobKey jobID,String cron,Map<String, Object> dataMap) throws PrivateSchedulerException;
/**
* 删除任务
* @param jobID
* @throws PrivateSchedulerException
*/
public void removeJob(JobKey jobID) throws PrivateSchedulerException;
}
这里除了本身提供了三个方法外,还继承了Spring的三个接口InitializingBean,DisposableBean,Lifecycle
接口本身能够胜任动态添加任务的需要,但本接口及其简单,完全可以提供更多更方便的触发器类型和方法。
实现InitializingBean后,Spring在创建实例时且设置完所有依赖属性后,调用afterPropertiesSet方法。因此在此方法中可以读取配置文件,解析Job信息,再调用addJob做初始化添加的工作。
实现Lifecycle是考虑可能会有暂停Quartz的需要。Quartz也提供了暂停和重启的方法,Lifecycle的三个方法感觉非常适合用来这里。但Spring对Lifecycle的实现会做什么事。我还没有去研究!(新手有理,新手无罪)
和InitializingBean正好相反,Spring会在销毁实例时调用DisposableBean接口的destroy()方法,用来彻底关闭Quartz很合适。
三,初始化任务
afterPropertiesSet是一个很好的方法,Spring初始化调度器也是使用这个方法。本模块本着简单易用的原则,封装所有的Job和触发器类型,只需要在配置文件中声明任务执行类,执行时间等信息即可完成初始化。为简单起见,执行时间统一使用cron表达式。
示例配置:job.properties
#job.properties
#required
job1.name=FinishOrder
#optional
job1.group=order
#required class path of the job
job1.jobClass=com.***.**.serviceorder.scheduler.job.FinishOrderJob
#required exist cover
job1.existCover=true
#optional save to mysql
job1.durability=true
#optional data map
job1.dataMap.rid=id12321
job1.dataMap.pid=jfe2323
#optional cron expression
job1.cron=0 5 0 * * ?
读取过程:
@Override
public void afterPropertiesSet() throws Exception {
ClassPathResource resource = new ClassPathResource(FILE_JOB_PROPERTIES);
if (!resource.exists()) {
return;
}
Properties jobList = PropertiesLoaderUtils.loadProperties(resource);
List<JobConfig> groupJobSet = groupKey(jobList);
for (JobConfig config : groupJobSet) {
if (!isExist(config)) {
addJob(config);
continue;
}
if (config.isExistCover()) {
addJob(config);
}
}
if (_log.isDebugEnabled()) {
_log.debug("任务初始化完成————————————————————————————————");
}
}
因为自己定义的格式,解析这个配置文件也很简单,就不贴实现了。在上面的调度器接口方法中,同样出现了对象JobConfig,这里面就是封装了Quartz所支持的一个Job的所有配置。对属性说明如下:
/**
* 重启应用之后是否删除任务的相关信息,默认false;<span style="font-family: Arial, Helvetica, sans-serif;">这个属性的含义是别人说的,我表示怀疑。但还没有验证,也没搞太清楚,什么叫轻快模式</span>
*/
private boolean volatility;
/**
* 保留到数据库,默认false;注意即使是false,关联了触发器的Job都会保存在数据库,直到触发器执行完毕。
*/
private boolean durability;
/**
* 应用重启之后忽略过期任务,默认false;对于未过期的任务,重启后是一定会执行的。
*/
private boolean shouldRecover;
/**
* 覆盖已经存在的任务
*/
private boolean existCover;
/**
* 执行时间,cron表达式字符串
*/
private String cron;
/**
* 自定义数据
*/
private Map<String, Object> dataMap;
/**
* 包括name和group的任务标示
*/
private JobKey jobID;
/**
* 任务类路径
*/
private Class<? extends BaseJob> jobClass;
四,封装Job接口
为了实现模块化,封装Job也是必须的。Quartz的Job是接口类型,因为作为基础框架非常需要更大的灵活性,但是我的模块更多的在于使用方便,这样可以创建Job一个抽象实现作为Job基类。好处1:把类似jobDataMap、applicationContext这样的东西变成保护类型的成员变量,子类使用更方便。好处2:在基类中做一些统一的处理。比如异常处理。同样的,对于statefulJob也要提供一个抽象实现。
至此,简单易用的调度器就设计完成了。显然,过于简化。有待日后完善了。只是想借此记录一下Quartz的实践经验。
其它实践:
关于给任务实例注入依赖的问题。
由于Job实例的产生由Quartz底层控制,如何注入我们需要的服务对象就成了一个问题。
Quartz有自己的上下文对象,这个对象可以放置任何对象,但是JobDataMap中最好不要放置Java对象类型数据,因为JobDataMap中的数据是要持久化的。通常任务实例中最可能需要注入的就是applicationContext,有了它,什么任务都能完成了。有两种比较好的方式:
1,把对象类型数据添加到Quartz上下文对象中,任务实例通过JobExecutionContext即可获取。上面提到的applicationContextSchedulerContextKey就是基于这种方式。
2,Quartz允许我们实现一个Job工厂,自己掌控Job实例的创建,注入什么对象类型都不是问题了。
关于事物
之前没有考虑过线程和事物之间的关系,使用Quartz后,不可能不考虑了。事物应该是无法跨线程的。举两个场景。
1,在web服务器中,应该是先有一个线程启动,然后调用servlet,然后调用Spring,然后执行逻辑代码。事物一般在Spring启动之后。也就是说,这里的事物是在线程内部的开始,提交或回滚的。
2,在一个事物内部启动Quartz实例,然后添加任务,然后启动新线程,新线程一定时间后调用任务,然后执行逻辑代码。这里,线程在事物的内部。但是很明显,原先事物在启动新线程之后就返回提交了!不可能等新的线程执行完任务后再提交!
所以在实际的事物控制中应该弄清除,要控制的是什么。这里可以控制任务的添加,但是任务的执行一定是另一个事物。
在使用Junit单元测试时,常用到事物的自动回滚,这样可以保证单元测试以无污染数据库的方式运行。然而在单元测试Quartz时,就不可以使用事物了。因为Junit在测试方法开始时开启事物,而方法结束后才提交。因此另一个执行线程无法读取测试方法添加的Job。只要Junit开启事物,就永远没有Job能够执行。除非数据库已经存在别的Job。
异常处理:
任务实例执行抛异常,意味着本次任务失败,但并不会影响下一次任务调度。
JobExecutionException提供了几种异常处理的控制,可以实现重新执行,停止所有触发器执行等,具体可参考此类的javadoc文档
触发器脱靶
数据源限制,执行线程限制,暂停等情况下可能出现脱靶。
Quartz提供了几种处理方式,在创建触发器时指定。具体可参考此类的javadoc文档
相关资源下载
Spring2.5源码包和Quartz.1.6.6带全部:http://pan.baidu.com/s/1c0pVJaS