Search This Blog

Saturday 9 February 2013

Working with Quartz and Spring

In an earlier post we saw how Spring allows us to get scheduling done using Java's Timer Implementation. The timer schedulers has one shortcoming- There is no way to specify the time of the day when the task is to be run.An alternative and more powerful scheduler is the Quartz Scheduler.
My first step was to define some code that needed to be scheduled.
public class MessageDisplayBean extends QuartzJobBean {

    private String userName;
    private MessageAgent messageAgent;
    
    @Override
    protected void executeInternal(JobExecutionContext arg0)
            throws JobExecutionException {
        Date sysDate = new Date();
        DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");
        String time = dateFormat.format(sysDate);
        System.out.println("Hi "+ userName + ", displaying message at " 
            + time + " message is : " + messageAgent.getMessage());
    }
    //setter getters...
}
The class simply displays a random message to the user. The MessageAgent class is from our previous example.
Our class extends a QuartzJobBean. The class provides an implementation of the Job interface(the one that we would extend when working directly with Quartz). The next step is to configure the job for running as a task.
<!-- This includes the job to execute -->
<bean id="messageDisplayJob" class="org.springframework.scheduling.quartz.JobDetailBean">
    <property name="jobClass" value="com.old_style.MessageDisplayBean" />
    <property name="jobDataAsMap">
        <map>
                     <entry key="userName" value="Robin"/>
            <entry key="messageAgent">
                <bean class ="com.old_style.MessageAgent"/>
            </entry>            
        </map>
    </property>
</bean>

<!-- This is the trigger for the job. It tells Scheduler when to execute 
    a job -->
<bean id="messageTrigger" class="org.springframework.scheduling.quartz.CronTriggerBean">
    <property name="jobDetail" ref="messageDisplayJob" />
    <property name="cronExpression" value="*/2 * * * * ?" />
</bean>
  1. The first bean is the configuration for the job. Our job bean is of type JobDetailBean
    1. The actual implementation class is specified using the jobClass property. 
    2. The jobDataAsMap property is a list of the dependencies that need to be wired in our job implementation. 
  2. The second bean is a CronTriggerBean.This is used to set the trigger for our job. The details of the cron expression is available here.
  3. In our previous post we used ScheduledTimerTask If we are working directly with Quartz then the code for trigger would be :
    JobDetail job = new JobDetail();
    job.setName("messageDisplayJob");
    job.setJobClass(MessageDisplayBean.class);
    
    In case of Spring we have the CronTriggerBean which is a bean style wrapper for the CronTrigger class. 
  4. Also while directly working with the Quartz Scheduler we associate the trigger and the job using the scheduler API, here the job is associated with the trigger using the jobDetail property. 
  5. If we do not want Cron expression but would like something simple like our previous Timer based scheduler we can use the SimpleTrigger:
    <bean id="simpleTrigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean">
        <property name="jobDetail" ref="displayMessageJob"/>
        <property name="repeatInterval" value="1000"/> <!-- in milliseconds -->
        <property name="startDelay" value="5000"/> <!-- in milliseconds -->
    </bean> 
The last part of the configuration is the scheduler:
<bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean">
    <property name="triggers">
        <list>
            <ref bean="messageTrigger" />
        </list>
    </property>
</bean>
The bean is a FatoryBean which returns a Scheduler instance. The triggers are simply added here as a list. Spring will take care of starting the scheduler and running the jobs on the basis of their triggers. The SchedulerFactory is the one responsible for starting the scheduler. If we were working with Quartz directly then this code would be written by us:
JobDetail job = new JobDetail();
job.setName("messageDisplayJob");
job.setJobClass(MessageDisplayBean.class);

CronTrigger trigger = new CronTrigger();
trigger.setName("trigger");
trigger.setCronExpression("0/2 * * * * ?");

//schedule it
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start();
scheduler.scheduleJob(job, trigger);
With Spring all we need to do is load the Spring Container.
public static void main(String[] args) {
    ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring-quartz.xml");
}
Initial attempt to run the code resulted in an exception:
Exception in thread "main" org.springframework.beans.factory.BeanCreationExcepti
on: Error creating bean with name 'org.springframework.scheduling.quartz.Schedul
erFactoryBean#0' defined in class path resource [spring-quartz.xml]:
Instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: 
org/springframework/transaction/TransactionException
To fix this, I had to add Spring tx jar to the classpath. The logs now indicate that the message was generated successfully:
254  [main] DEBUG org.springframework.beans.factory.xml.BeanDefinitionParserDele
gate  - Neither XML 'id' nor 'name' specified - using generated bean name [org.s
pringframework.scheduling.quartz.SchedulerFactoryBean#0]
...
484  [main] DEBUG org.springframework.context.support.DefaultLifecycleProcessor 
 - Starting bean 'org.springframework.scheduling.quartz.SchedulerFactoryBean#0' 
of type [class org.springframework.scheduling.quartz.SchedulerFactoryBean]
484  [main] INFO  org.springframework.scheduling.quartz.SchedulerFactoryBean  - 
Starting Quartz Scheduler now
...
505  [org.springframework.scheduling.quartz.SchedulerFactoryBean#0_Worker-1] DEB
UG org.springframework.beans.CachedIntrospectionResults  - Caching PropertyDescr
iptors for class [com.old_style.MessageDisplayBean]
505  [org.springframework.scheduling.quartz.SchedulerFactoryBean#0_Worker-1] DEB
UG org.springframework.beans.CachedIntrospectionResults  - Found bean property '
messageAgent' of type [com.old_style.MessageAgent]
505  [org.springframework.scheduling.quartz.SchedulerFactoryBean#0_Worker-1] DEB
UG org.springframework.beans.CachedIntrospectionResults  - Found bean property '
userName' of type [java.lang.String]
Hi Robin, displaying message at 15:33:54 message is : What the heck !!
Hi Robin, displaying message at 15:33:56 message is : This rocks
Hi Robin, displaying message at 15:33:58 message is : What the heck !!
Hi Robin, displaying message at 15:34:00 message is : This rocks
Hi Robin, displaying message at 15:34:02 message is : Great job

Just one change - The Spring docs advise to not use the JobDetailBean directly anymore. Instead they recommend :
NOTE: This convenience subclass does not work against Quartz 2.0. Use Quartz 2.0's 
native JobDetailImpl class or the new Quartz 2.0 builder API instead. Alternatively, switch to 
Spring's JobDetailFactoryBean which largely is a drop-in replacement for this class 
and its properties and consistently works against Quartz 1.x as well as Quartz 2.0/2.1.
So I updated my XML configuration ( works in Spring 3.1.1 and beyond):
<bean id="messageDisplayJob" class="org.springframework.scheduling.quartz.JobDetailFactoryBean">
    <property name="jobClass" value="com.old_style.MessageDisplayBean" />
    <property name="jobDataAsMap">
        <map>
            <entry key="userName" value="Robin"/>
            <entry key="messageAgent">
                <bean class ="com.old_style.MessageAgent"/>
            </entry>            
        </map>
    </property>
</bean>

2 comments:

  1. we can use org.springframework.scheduling.quartz.CronTriggerFactoryBean instead of CronTriggerBean

    ReplyDelete
  2. This is not working for me as it throws an error as below:
    Exception encountered during context initialization
    Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.scheduling.quartz.SchedulerFactoryBean#0' defined in file [C:\Users\praveen.joshi\git\quartzTest\src\main\resources\applicationContext.xml]: Invocation of init method failed; nested exception is org.quartz.SchedulerException: Jobs added with no trigger must be durable.

    ReplyDelete