Configuring Quartz 2 with Spring in clustered mode

Run Quartz jobs to fire only once per cluster, not once per server, while still providing beans from the Spring managed context and using the latest version of Quartz..

Jeff Sheets

Aligning the stars to configure Quartz 2.1.7 to work with Spring 3.1.3 in a cluster was surprisingly complicated. The main idea is to run jobs to fire only once per cluster, not once per server, while still providing beans from the Spring managed context and using the latest version of Quartz. The documentation consists essentially of a number of blog posts and stackoverflow answers. So here is one final and (hopefully) more comprehensive summary of the process.

For the TL;DR version, just see the full github gist.

In Quartz.properties we’ll want to set useProperties=true so that data persisted to the DB is in String form instead of Serialized Java objects. But unfortunately the Spring 3.1.x CronTriggerFactoryBean sets a jobDetails property as a Java object, so Quartz will complain that the data is not a String. We’ll need to create our own PersistableCronTriggerFactoryBean to get around this issue (similar to this blog post and forum discussion).

/**
 * Needed to set Quartz useProperties=true when using Spring classes,
 * because Spring sets an object reference on JobDataMap that is not a String
 *
 * @see http://site.trimplement.com/using-spring-and-quartz-with-jobstore-properties/
 * @see http://forum.springsource.org/showthread.php?130984-Quartz-error-IOException
 */
public class PersistableCronTriggerFactoryBean extends CronTriggerFactoryBean {
    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();

        //Remove the JobDetail element
        getJobDataMap().remove(JobDetailAwareTrigger.JOB_DETAIL_KEY);
    }
}
# Using Spring datasource in quartzJobsConfig.xml
# Spring uses LocalDataSourceJobStore extension of JobStoreCMT
org.quartz.jobStore.useProperties=true
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = true

# Change this to match your DB vendor
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.MSSQLDelegate

# Needed to manage cluster instances
org.quartz.scheduler.instanceId=AUTO
org.quartz.scheduler.instanceName=MY_JOB_SCHEDULER

org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true

Additionally, in our Spring config the SchedulerFactoryBean will need to set both the triggers and the jobDetails objects. We also setup the scheduler to use Spring’s dataSource and transactionManager. And notice that durability=true must be set on each JobDetailFactoryBean.

<!-- truncated pieces of applicationContext.xml -->

<bean id="firstJobDetail" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
  <property name="jobClass" value="com.sheetsj.quartz.job.FirstJob"/>
  <property name="durability" value="true"/>
</bean>
<bean id="firstTrigger" class="com.sheetsj.quartz.PersistableCronTriggerFactoryBean">
  <property name="jobDetail" ref="firstJobDetail" />
  <!-- run every morning at 5:00 AM -->
  <property name="cronExpression" value="0 0 5 * * ?" />
</bean>

<bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">

 <property name="configLocation" value="classpath:quartz.properties"/>
 <property name="dataSource" value="dataSource"/>
 <property name="transactionManager" value="transactionManager"/>

 <!-- This name is persisted as SCHED_NAME in db. for local testing could change to unique name
      to avoid collision with dev server -->
 <property name="schedulerName" value="quartzScheduler"/>

 <!-- Will update database cron triggers to what is in this jobs file on each deploy.
      Replaces all previous trigger and job data that was in the database. YMMV  -->
  <property name="overwriteExistingJobs" value="true"/>

 <property name="autoStartup" value="true"/>
 <property name="applicationContextSchedulerContextKey" value="applicationContext"/>
 <property name="jobFactory">
   <bean class="com.sheetsj.quartz.AutowiringSpringBeanJobFactory"/>
  </property>

 <!-- NOTE: Must add both the jobDetail and trigger to the scheduler! -->
 <property name="jobDetails">
  <list>
    <ref bean="firstJobDetail" />
  </list>
 </property>
 <property name="triggers">
  <list>
   <ref bean="firstTrigger"/>
  </list>
 </property>
</bean>

By default you cannot use Autowired capabilities in the Quartz Jobs, but this can be easily setup with a AutowiringSpringBeanJobFactory.

/**
 * Autowire Quartz Jobs with Spring context dependencies
 * @see http://stackoverflow.com/questions/6990767/inject-bean-reference-into-a-quartz-job-in-spring/15211030#15211030
 */
public final class AutowiringSpringBeanJobFactory 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;
    }
}

You’ll also notice that we cannot use MethodInvokingJobDetailFactoryBean because it is not serializable, so we need to create our own Job class that extends QuartzJobBean. If your services are secured by Acegi or Spring Security, you will also need to register an authenticated quartzUser object with the security context.

@Component
@Scope(value = BeanDefinition.SCOPE_PROTOTYPE)
public class FirstJob extends QuartzJobBean {
    @Autowired
    private FirstService firstService;

    @Override
    protected void executeInternal(JobExecutionContext context) {
        //Quartz jobs have not been authenticated with acegi or spring security
        //so you may have to setup a user before calling your service methods
        //I used SecurityContextHolder.getContext().setAuthentication(quartzUser)
        //on an older version of acegi

        firstService.updateSomethingInTheDatabase();
    }
}

And finally, we’ll want to test that the trigger’s Cron expression actually fires when we want it to. Here is an example test case that pulls the cronExpression from configuration and tests that it fires correctly on 2 consecutive days:

/**
 * Verifies that the Cron Trigger Time Strings for the jobs are setup correctly
 */
@ContextConfiguration(locations = {"classpath:/applicationContext-test.xml"})
public class QuartzCronTriggerTest extends AbstractTransactionalJUnit4SpringContextTests {

    @Autowired
    private SchedulerFactoryBean quartzScheduler;

    @Test
    public void testFirstTrigger() throws SchedulerException {
        Trigger firstTrigger = quartzScheduler.getScheduler().getTrigger(new TriggerKey("firstTrigger"));

        //Must use tomorrow for testing because jobs have startTime of now
        DateTime tomorrow = new DateMidnight().toDateTime().plusDays(1);

        //Test first
        Date next = firstTrigger.getFireTimeAfter(tomorrow.toDate());
        DateTime expected = tomorrow.plusHours(5);
        assertThat(next, is(expected.toDate()));

        //Test the next day
        next = firstTrigger.getFireTimeAfter(next);
        expected = expected.plusDays(1);
        assertThat(next, is(expected.toDate()));
    }
}

Hopefully this helps others in configuring an enterprise-ready Quartz + Spring application to run jobs in a clustered server environment.

Share this Post

Related Blog Posts

JVM

Getting Groovy with Spring and WebSockets

June 25th, 2013

A demonstration of leveraging Project Tyrus with Groovy and Spring to build a WebSocket-driven application.

Object Partners
JVM

A Tale From the Grails trenches: Nasty 1.3.X Bug

June 11th, 2013

Grails 1.3.x has a nasty bug if you call the render method in a controller with the wrong signature

Object Partners
JVM

Redefining the Service Layer with Groovy Categories

June 5th, 2013

Demonstrating the use of Groovy Categories to redefine a traditional service layer implementation.

Object Partners

About the author

Jeff Sheets

Chief Software Technologist

Jeff has developed Java, Groovy, Grails, and Javascript web apps for industries as varied as Defense, Energy, Weather, Insurance, and Telecom. He is a co-organizer of the Omaha Java Users Group. Jeff has worked on Grails projects since the Grails 1.3.x days, and has experience with production Groovy code as well as Spock tests and Gradle builds. His latest focus has been on AngularJS and Spring Boot applications using JHipster. Jeff also enjoys volunteering at local CoderDojo events to teach programming to our next generation.