Spring与Quartz集成看起来简单,其实还有很多小的细节解决起来比较耗时,本文作为备忘,摘要代码以供参考。
环境:Spring 3.2,Quartz 2.2.x
数据库:mysql 5.5+
一、普通定时器,非Cluster环境:即没有使用Quartz Cluster,每个Spring实例中的定时器都会同时运行,这在分布式环境中并不会带来太多额外的问题,如果你的定时器中有操作数据库的情况,请注意多个定时器同时运行带来的并发问题。
1、spring-quartz.xml
<bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="jobFactory"> <bean class="org.springframework.scheduling.quartz.SpringBeanJobFactory"/> </property> <property name="autoStartup" value="true"/> <property name="triggers"> <list> <ref bean="testTrigger" /> </list> </property> </bean> <bean id="testTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="cronExpression" value="0/10 * * * * ?" /> <property name="jobDetail" ref="testJobDetail" /> </bean> <bean id="testJobDetail" class="org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean"> <property name="targetObject" ref="testJob"/> <property name="targetMethod" value="execute"/> <property name="concurrent" value="false"/> </bean> <bean id="testJob" class="com.test.support.service.job.TestJob"/>
2、TestJob.java
public class TestJob{ private StudentManager studentManager; public void setStudentManager(StudentManager studentManager) { this.studentManager = studentManager; } public void execute() { System.out.println("test" + studentManager); } }
通过上述方式,我们可以看到TestJob无需实现任何额外的接口,只需要借助“MethodInvokingJobDetailFactoryBean”即可实现Job中依赖注入其他Spring Bean,因为此时testJob已经被声明为Spring bean。除此之外,其他的方式均需要额外的操作,才能将spring bean注入到Job中。
二、Cluster方式:基于JDBC方式,将Quartz job、trigger都序列化到数据库中,这种方式的好处也非常明显,当部署多个spring实例时,集群中任何时候“Scheduler”获取lock,并负责管理所有的job和trigger。Cluster模式下,有很多小细节难以解决,而且与spring的集成并不是非常良好,比如jobData必须实现Serializable接口、Job实例无法透明的注入spring bean等。
首先到Quartz的官网,下载quartz的源代码,并找到相应的数据库sql脚本,并执行,最好为quartz相关的表单独创建一个database。执行SQL脚本的主要作用是生成Cluster所需要的table,这一步quartz并不会在运行时为我们完成。(可能脚本有书写错误,比如mysql的script需要修改一处拼写错误)
1、spring-quartz.xml
<bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="dataSource" ref="quartzDataSource" /> <property name="quartzProperties"> <props> <prop key="org.quartz.scheduler.instanceName">quartz-cluster</prop> <prop key="org.quartz.scheduler.instanceId">AUTO</prop> <!-- 线程池配置 --> <prop key="org.quartz.threadPool.class">org.quartz.simpl.SimpleThreadPool</prop> <prop key="org.quartz.threadPool.threadCount">6</prop> <prop key="org.quartz.threadPool.threadPriority">5</prop> <!-- JobStore 配置 --> <prop key="org.quartz.jobStore.class">org.quartz.impl.jdbcjobstore.JobStoreTX</prop> <!-- 集群配置 --> <prop key="org.quartz.jobStore.isClustered">true</prop> <prop key="org.quartz.jobStore.clusterCheckinInterval">20000</prop> <prop key="org.quartz.jobStore.maxMisfiresToHandleAtATime">1</prop> <prop key="org.quartz.jobStore.misfireThreshold">120000</prop> <prop key="org.quartz.jobStore.tablePrefix">QRTZ_</prop> </props> </property> <property name="schedulerName" value="clusterScheduler" /> <property name="startupDelay" value="15" /> <property name="applicationContextSchedulerContextKey" value="applicationContext" /> </property> <property name="overwriteExistingJobs" value="true" /> <property name="autoStartup" value="true" /> <property name="triggers"> <list> <ref bean="testTrigger" /> </list> </property> </bean> <bean id="testTrigger" class="org.springframework.scheduling.quartz.CronTriggerFactoryBean"> <property name="cronExpression" value="0/4 * * * * ?" /> <property name="jobDetail" ref="testJobDetail" /> </bean> <bean id="testJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean"> <property name="jobClass" value="com.test.support.service.job.TestJob" /> <property name="durability" value="true" /> </bean> <bean id="quartzDataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"></property> <property name="url" value="jdbc:mysql://192.168.1.121:3306/quartz?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> <property name="maxActive" value="6"></property> <property name="maxIdle" value="1"></property> <property name="minIdle" value="1"></property> <property name="maxWait" value="60000"></property> <property name="defaultAutoCommit" value="true"></property> </bean>
2、TestJob.java
public class TestJob extends QuartzJobBean{ private StudentManager studentManager; public void executeInternal(JobExecutionContext context) throws JobExecutionException { //因为spring无法注入bean,所以需要手动从ApplicationContext中获取 setting(context); System.out.println("test" + studentManager); } protected void setting(JobExecutionContext context) { try { //key来自spring-quartz.xml中“applicationContextSchedulerContextKey”的值 ApplicationContext applicationContext = (ApplicationContext) context.getScheduler().getContext().get("applicationContext"); studentManager = (StudentManager) applicationContext.getBean("studentManager"); } catch (Exception e) { throw new RuntimeException(e); } } }
目前spring尚没有内置的机制来解决自动注入的问题,所以job中的spring bean需要引入一些外部的机制。上述办法就是其中一个。此外还有一种方式:继承SpringBeanJobFactory。
1)spring-quartz.xml
.... <property name="applicationContextSchedulerContextKey" value="applicationContext" /> <property name="jobFactory"> <bean class="com.test.support.service.AutowireSpringBeanJobFactory" /> </property> ....
<context:annotation-config /> <context:component-scan base-package="com.test.support.service.job" />
2)AutowireSpringBeanJobFactory.java
public class AutowireSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware { private transient AutowireCapableBeanFactory beanFactory; public void setApplicationContext(final ApplicationContext context) { beanFactory = context.getAutowireCapableBeanFactory(); } @Override protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception { final Object job = super.createJobInstance(bundle); beanFactory.autowireBean(job); return job; } }
3)TestJob.java
@Service public class TestJob extends QuartzJobBean{ @Autowired private StudentManager studentManager; public void executeInternal(JobExecutionContext context) throws JobExecutionException { System.out.println("test" + studentManager); } }
通过继承SpringBeanJobFactory可以实现springBean的自动注入,前提是必须使用“Autowired”注释,不过开发者一定有办法让注释和setter方式同时支持。本人不太喜欢这种方式,感觉和spring耦合过度了,或许不久的将来,spring会提供内部机制解决这些问题。还有一个很严重的问题,如果基于cluster,那么trigger的class信息将会被序列化到DB中,此后即使从quartz Scheduler配置文件中移除了某个trigger,那么这个trigger仍然会触发,除非删除trigger相应的job类(或者从db中删除trigger的记录),这个问题实在是难以想象,解决这个问题需要重写spring + Quartz的很多集成类,实在是有些麻烦,希望quartz cluster机制能够更加完善。