基于quartz1.66+spring2.5的一个模块化设计

本来想用独立的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






  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值