Spring与Quartz Cluster备忘

    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&amp;characterEncoding=UTF-8&amp;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机制能够更加完善。

你可能感兴趣的:(cluster)