Most application logic happens in response to some form of user action, such as a button click or a form submission. However, in many applications certain processes must be invoked without user interaction, usually at a given interval. For example, you might have a process that cleans out temporary files once an hour or a process that creates a data export from a database and sends it to an external system once a day at midnight. Most nontrivial applications require some kind of scheduling support—if not directly related to business logic of the application then to support system housekeeping.
When you are building scheduled tasks for your application, creating a task that runs once an hour or once a day is fairly simple. But what about a task that runs at 3:00 p.m. every Monday, Wednesday, and Friday? This is a little more difficult to code, and it makes sense to use prebuilt scheduling solutions rather than attempt to create your own scheduling framework.
When talking about scheduling from a programming perspective, we tend to talk about three distinct concepts. A job is a unit of work that needs to be scheduled to run at a specific interval. A trigger is a condition, perhaps a fixed interval or a given piece of data, that causes a job to run. A schedule is a collection of triggers that govern the complete timeline of a job. Typically, you encapsulate a job by implementing an interface or extending a given base class. You define your triggers in whatever terms your scheduling framework supports. Some frameworks may support only basic interval-based triggers, whereas others, such as Quartz, provide much more flexible trigger schemes. In general, a job has only a single trigger in its schedule, and the terms "schedule" and "trigger" are often used interchangeably.
Scheduling support in Spring comes in two distinct forms: JDK Timer
-based and Quartz-based. The JDK Timer
-based approach provides scheduling capabilities on any version 1.3 or later JVM, and it does not need external dependencies beyond Spring. Timer
-based scheduling is quite primitive and provides limited flexibility when defining job schedules. However, Timer
support is included with Java and requires no external libraries, which might be beneficial if you are restricted by application size or corporate policy. Quartz-based scheduling is much more flexible and allows triggers to be defined in a much more real-world way, such as the earlier example of 3:00 p.m. every Monday, Wednesday, and Friday.
In this chapter, we explore both of the scheduling solutions included with Spring. In particular, this chapter discusses three core topics: scheduling with the JDK Timer
, Quartz-based scheduling, and job scheduling considerations.
We'll start our discussion by exploring Spring's support for JDK Timer
-based scheduling. This section introduces the different trigger types available with Timer
-based scheduling and looks at how you can schedule any arbitrary logic without needing to create additional Java code.
After that, we'll look at the comprehensive Quartz scheduling engine and how it is integrated into Spring. In particular, we examine Quartz support for cron expressions allowing highly complex schedules to be configured using a concise format. As with the JDK Timer
, you'll see how to schedule any logic without needing to encapsulate it.
Finally, we'll discuss the various points to consider when choosing a scheduling implementation and patterns to use when creating logic for scheduled execution.
The most basic scheduling support with Spring is based on the JDK java.util.Timer
class. When scheduling using Timer
, you are limited to simple interval-based trigger definitions, which makes Timer
-based scheduling suitable only for jobs that you need to execute just once at some given future time or that you need to execute at some fixed frequency.
Timer
-based scheduling offers you three types of triggers:
One-off: When you use a one-off trigger, job execution is scheduled for some given point in the future, defined as the number of milliseconds from a given date. After the job executes, it is not rescheduled for further execution. We have found that one-off triggers are great for scheduling jobs that need to be done once that you might forget to do yourself. For instance, if a web application has scheduled maintenance coming up in a week, we can schedule a task to switch on the In Maintenance page when the maintenance is due to begin.
Repeating and fixed-delay: When you use a fixed-delay trigger, you schedule the first execution of the job just like for a one-off trigger, but after that, the job is rescheduled to execute after a given interval. When you are using fixed-delay, the interval is relative to the actual execution time of the previous job. This means that the interval between successive executions is always approximately the same, even if executions occur "late" when compared to the original schedule. With this type of trigger, the interval you specify is the actual interval between subsequent executions. Use this approach when the interval between executions must be kept as constant as possible.
Repeating and fixed-rate: Fixed-rate triggers function in a similar way to fixed-delay triggers, but the next execution time is always calculated based on the initial scheduled execution time. This means that if a single execution is delayed, subsequent executions are not delayed as a result. With this type of trigger, the interval you specify is not necessarily the actual interval between subsequent executions. Use this approach when the actual execution time is important, rather than the interval between executions.
You may find it difficult to visualize the differences between fixed-delay and fixed-rate triggers, and unfortunately, creating an example that causes enough of a delay in execution to fully clarify the differences reliably is difficult. That said, here is a simple example that should help highlight the differences.
Consider a task that starts executing at 1:00 p.m. and has a specified interval of 30 minutes. The task runs fine until 4:30 p.m., when the system experiences a heavy load and a particularly nasty garbage collection; these cause the actual execution time to be a minute late—4:31 p.m. Now, with fixed-delay scheduling it is the actual interval that is important, that is to say, we want 30 minutes between each actual execution, so the next execution is scheduled for 5:01 p.m. rather than 5:00 p.m. With fixed-rate scheduling, the interval defines the intended interval—that is to say, we intend the job to execute every 30 minutes based on the start time, not on the time of the last job—so the job is scheduled for execution at 5:00 p.m.
Both of these trigger types are useful in different ways. In general, you use fixed-delay triggers for situations where you want the time between each execution to be as regular as possible or when you want to avoid the possibility of two executions happening too close together, which can happen with fixed-rate execution if a particular execution is delayed long enough. You use fixed-rate triggers for real-time-sensitive operations such as those that must execute every hour on the hour.
To create a job to use with the Timer
class, you simply extend the TimerTask
class and implement the run()
method to execute your job's logic. Listing 12-1 shows a simple TimerTask
implementation that writes "Hello, World" to stdout
.
Example 12.1. Creating a Basic TimerTask
package com.apress.prospring2.ch12.timer; import java.util.TimerTask; public class HelloWorldTask extends TimerTask { public void run() { System.out.println("Hello World!"); } }
Here, you can see that in the run()
method, we simply write the "Hello, World" message to stdout
. Each time a job is executed, Timer
invokes the TimerTask
's run()
method. The simplest possible trigger we can create for this job is a one-off trigger to start the job in 1 second; Listing 12-2 shows this.
Example 12.2. Using a One-Off Trigger with the HelloWorldTask
package com.apress.prospring2.ch12.timer; import java.util.Timer; public class OneOffScheduling { public static void main(String[] args) { Timer t = new Timer(); t.schedule(new HelloWorldTask(), 1000); } }
To schedule a job using a given trigger when you are using the JDK Timer
class, you must first create an instance of the Timer
class and then create the trigger using one of the schedule()
or scheduleAtFixedRate
methods. In Listing 12-2, we used the schedule()
method to schedule an instance of HelloWorldTask
to run after a delay of 1,000 milliseconds. If you run this example, after the initial delay of 1 second, you get the following message:
Hello World!
This kind of one-off trigger is fairly useless—how often are you going to need to schedule a one-off task to run an arbitrary period of time after application start-up? For this reason, you can also specify an absolute date when you create a one-off trigger. So if we want to create a job to remind us seven days before an important birthday, we can replace our call to Timer.schedule()
with something like this:
Calendar cal = Calendar.getInstance(); cal.set(2008, Calendar.NOVEMBER, 30); t.schedule(new HelloWorldTask(), cal.getTime());
In this example, you can see that we created an instance of Calendar
for the date November 30, 2008. Then, using the Calendar
instance, we scheduled the HelloWorldTask
to run. This is clearly more useful than the first example, because no matter what time the application starts, the job is always scheduled to run at the same time. The only drawback with this approach is that we will not be reminded about the birthday in 2009 or 2010 unless we explicitly add more triggers. By using a repeating trigger, we can get around this.
Both types of repeating trigger, fixed-delay and fixed-rate, are configured in the same way: you specify a starting point, using either a number of milliseconds relative to the call to schedule
or an absolute date, and you specify an interval in milliseconds to control when subsequent executions occur. Remember that "interval" is interpreted differently depending on whether you are using a fixed-delay or fixed-rate trigger.
We can schedule the HelloWorldTask
job to run every 3 seconds with a 1-second delay using the code shown in Listing 12-3.
Example 12.3. Scheduling a Repeating Task
package com.apress.prospring2.ch12.timer; import java.util.Timer; public class FixedDelayScheduling { public static void main(String[] args) throws Exception{ Timer t = new Timer(); t.schedule(new HelloWorldTask(), 1000, 3000); } }
If you run this application, you will see the first "Hello, World" message displayed after about 1 second, followed by further "Hello, World" messages every 3 seconds. To schedule this job using a fixed-rate trigger, simply replace the call to Timer.schedule()
with a call to Timer.scheduleAtFixedRate()
, as shown in Listing 12-4.
Example 12.4. Scheduling a Job Using a Fixed-Rate Trigger
package com.apress.prospring2.ch12.timer; import java.util.Timer; public class FixedRateScheduling { public static void main(String[] args) throws Exception { Timer t = new Timer(); t.scheduleAtFixedRate(new HelloWorldTask(), 1000, 1000); } }
As with the one-off trigger, you can start both fixed-delay and fixed-rate triggers using an absolute date. Using this approach, we can create a trigger for our birthday reminder example that runs on a given date and then repeats each year. This is shown in Listing 12-5.
Example 12.5. Scheduling Birthday Reminders
package com.apress.prospring2.ch12.timer; import java.util.Calendar;
import java.util.Timer; public class SimpleBirthdayReminderScheduling { private static final long MILLIS_IN_YEAR = 1000 * 60 * 60 * 24 * 365; public static void main(String[] args) { Timer t = new Timer(); Calendar cal = Calendar.getInstance(); cal.set(2008, Calendar.NOVEMBER, 30); t.schedule(new HelloWorldTask(), cal.getTime()); t.scheduleAtFixedRate(new HelloWorldTask(), cal.getTime(), MILLIS_IN_YEAR); } }
In this example, you can see that we calculate the number of milliseconds in a year, and using a Calendar
instance, we define a starting point of November 30 and define the interval to be one year. Now, every year on November 30, provided that this application is running and conveniently ignoring the existence of leap years, the "Hello, World" message is written to stdout
. Clearly, this is not a fully functional example, because there is no real notification mechanism, and each time we want to add a new birthday reminder, we need to change the code. In the next section, we create a more robust birthday reminder application using Spring's JDK Timer
support classes.
As you saw in the previous section, you can easily create and schedule jobs using the JDK Timer
and TimerTask
classes. That said, the approach we took in the previous examples has some problems. First, we created the TimerTask
instances within the application rather than using Spring. For the HelloWorldTask
, this is acceptable, because we did not need to configure the job at all. However, many jobs require some configuration data, so we should manage these using Spring to allow for easy configuration. Second, the trigger information is hard-coded into the application, meaning that any changes to the time a job is triggered require a change to the application code and a recompilation. Finally, scheduling new jobs or removing a job requires changes to the application code; ideally, we should be able to configure this externally. By using Spring's Timer
support classes, we can externalize all job and trigger configuration as well as hand over control of Timer
creation to Spring, thus allowing jobs and their triggers to be defined externally.
The core of Spring's Timer
support comes in the form of the ScheduledTimerTask
and TimerFactoryBean
classes. The ScheduledTimerTask
class acts as a wrapper around your TimerTask
implementations and allows you to define trigger information for the job. Using the TimerFactoryBean
, you can have Spring automatically create Timer
instances for a given list of ScheduledTimerTask
beans using the trigger configuration data when creating the trigger.
Using ScheduledTimerTask and TimerFactoryBean
Before we dive in and look at our new and improved birthday reminder application, we should first look at the basics of how ScheduledTimerTask
and TimerFactoryBean
work. For each scheduled job you want to create, you need to configure an instance of the job class and an instance of ScheduledTimerTask
containing the trigger details. You can share the same TimerTask
instance across many ScheduledTimerTask
instances if you want to create many triggers for the same job. Once you have these components configured, simply configure a TimerFactoryBean
and specify the list of ScheduledTimerTask
beans. Spring then creates an instance of Timer
and schedules all the jobs defined by the ScheduledTimerTask
beans using that Timer
class.
This might sound complex at first, but in reality, it is not. Listing 12-6 shows a simple configuration for scheduling the HelloWorldTask
to run every 3 seconds with a delay of 1 second before the first execution.
Example 12.6. Configuring Job Scheduling Using TimerFactoryBean
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns=http://www.springframework.org/schema/beans xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <bean id="job" class="com.apress.prospring2.ch12.timer.HelloWorldTask"/> <bean id="timerTask" class="org.springframework.scheduling.timer.ScheduledTimerTask"> <property name="delay" value="1000" /> <property name="period" value="3000" /> <property name="timerTask" ref="job" /> </bean> <bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean"> <property name="scheduledTimerTasks"> <list> <ref local="timerTask"/> </list> </property> </bean> </beans>
Here, you can see that we have configured a bean, job
, of type HelloWorldTask
, and using this bean, we have configured a bean of type ScheduledTimerTask
, setting the delay to 1,000 milliseconds and the period to 3,000 milliseconds. The final part of the configuration is the timerFactory
bean, which is passed a list of beans of type ScheduledTimerTask
. In this case, we have only one task to schedule, represented by the timerTask
bean. Be aware that when specifying trigger information using ScheduledTimerTask
, you can supply a delay only in milliseconds, not an initial date for start-up. We'll show you a way around this when we build the birthday reminder application in the next section.
With all of the scheduling and job definition information contained in the configuration, our sample application has very little to do. In fact, all we need to do is load the ApplicationContext
, and Spring creates the Timer
class and schedules the HelloWorldTask
as per the configuration file. This code is shown in Listing 12-7.
Example 12.7. The TimerFactoryBeanExample Class
package com.apress.prospring2.ch12.timer; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class TimerFactoryBeanExample {
public static void main(String[] args) throws Exception { ApplicationContext ctx = new FileSystemXmlApplicationContext( "./ch12/src/conf/timer-context.xml"); System.in.read(); } }
If you run this application, you will see that the message "Hello, World" is written to stdout
every 3 seconds after an initial delay of 1 second. As you can see from this example, it is very simple to configure job scheduling external to your application's code. Using this approach, it is much simpler to make changes to a job's schedules or to add new scheduled jobs and remove existing ones.
A More Comprehensive Birthday Reminder Application
In this section, we create a more complex birthday reminder application using Spring's Timer
support. With this example, we want to be able to schedule multiple reminder jobs, each with a specific configuration, to identify whose birthday the reminder indicates. We also want to be able to add and remove reminders without having to modify the application code.
To get started, we need to create a job to perform the actual reminder. Because we are going to create these jobs using Spring, we can allow all configuration data to be provided using DI. Listing 12-8 shows the BirthdayReminderTask
.
Example 12.8. The BirthdayReminderTask
package com.apress.prospring2.ch12.timer.bday; import java.util.TimerTask; public class BirthdayReminderTask extends TimerTask { private String who; public void setWho(String who) { this.who = who; } public void run() { System.out.println("Don't forget it is " + who + "'s birthday is 7 days"); } }
Notice here that we defined a property on the task, who
, that allows us to specify of whose birthday we're being reminded. In a real birthday reminder application, the reminder would no doubt be sent to e-mail or some similar medium. For now, however, you'll have to be content with reminder messages written to stdout
!
With this task complete, we are almost ready to move on to the configuration stage. However, as we pointed out earlier, you cannot specify the start time of a scheduled job using a date when you are using ScheduledTimerTask
. This is problematic for our sample application, because we do not want to have to specify reminder dates as a relative offset to the start-up time of the application. Thankfully, we can overcome this problem quite easily by extending the ScheduledTimerTask
class and overriding the getDelay()
method used by TimerFactoryBean
to determine what delay it should assign to a trigger. At the same time, we can also override the getPeriod()
method to return the number of milliseconds in a year so that you do not have to add that literal into configuration files. Listing 12-9 shows the code for our custom ScheduledTimerTask
, BirthdayScheduledTask
.
Example 12.9. Customizing ScheduledTimerTask
package com.apress.prospring2.ch12.timer.bday; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import org.springframework.scheduling.timer.ScheduledTimerTask; public class BirthdayScheduledTask extends ScheduledTimerTask { private static final long MILLIS_IN_YEAR = 1000 * 60 * 60 * 24 * 365; private DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); private Date startDate; public void setDate(String date) throws ParseException { startDate = dateFormat.parse(date); } public long getDelay() { Calendar now = Calendar.getInstance(); Calendar then = Calendar.getInstance(); then.setTime(startDate); return (then.getTimeInMillis() - now.getTimeInMillis()); } public long getPeriod() { return MILLIS_IN_YEAR; } }
In this example, you can see that we define a new property for the BirthdayScheduledTask
class, date
, which allows us to specify the start date as a date rather than a delay period. This property is of type String
, because we use an instance of SimpleDateFormat
configured with the pattern yyyy-MM-dd
to parse dates such as 2008-11-30
. We override the getPeriod()
method, which TimerFactoryBean
uses when it configures the interval for the trigger, to return the number of milliseconds in a year. Also notice that we override getDelay()
, and using the Calendar
class, we calculate the number of milliseconds between the current time and the specified start date. This value is then returned as the delay. With this complete, we can now complete the configuration for our sample application, as shown in Listing 12-10.
Example 12.10. Configuring the Birthday Reminder Application
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="mum" class="com.apress.prospring2.ch12.timer.bday.BirthdayScheduledTask"> <property name="date" value="2008-11-30" /> <property name="fixedRate" value="true" /> <property name="timerTask"> <bean class="com.apress.prospring2.ch12.timer.bday.BirthdayReminderTask"> <property name="who" value="Mum"> </bean> </property> </bean> <bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean"> <property name="scheduledTimerTasks"> <list> <ref local="mum"/> </list> </property> </bean> </beans>
This code should look familiar to you by now. Notice that we used our BirthdayScheduledTask
class in place of the ScheduledTimerTask
class, and instead of specifying a delay and a period, we have simply specified the date. Also, we rely on the overridden getDelay()
and getPeriod()
methods to provide the TimerFactoryBean
with the delay and period values. In addition, notice that we set the fixedRate
property of the BirthdayScheduledTask
bean to true
. This property is inherited from ScheduledTimerTask
; TimerFactoryBean
uses it to decide whether or not it should create a fixed-rate or fixed-delay trigger.
Scheduling Arbitrary Jobs
When scheduling jobs, you often need to schedule the execution of logic that already exists. If this is the case, you might not want to go through the trouble of creating a TimerTask
class just to wrap your logic. Thankfully, you don't have to. Using the MethodInvokingTimerTaskFactoryBean
, you can schedule the execution of any method on any given bean or a static method on a specific class; you can even provide method arguments if your logic method requires them.
As an example of this, consider the FooBean
shown in Listing 12-11.
Example 12.11. The FooBean Class
package com.apress.prospring2.ch12.timer; public class FooBean { public void someJob(String message) { System.out.println(message); } }
If we want to schedule the someJob()
method to run every 3 seconds with a given argument rather than create a TimerTask
just to do that, we can simply use the MethodInvokingTimerTaskFactoryBean
to create a TimerTask
for us. The configuration for this is shown in Listing 12-12.
Example 12.12. Using MethodInvokingTimerTaskFactoryBean
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns=http://www.springframework.org/schema/beans xmlns:xsi=http://www.w3.org/2001/XMLSchema-instance xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <bean id="target" class="com.apress.prospring2.ch12.timer.FooBean"/> <bean id="task" class="org.springframework.scheduling.timer. MethodInvokingTimerTaskFactoryBean"> <property name="targetObject" ref="target" /> <property name="targetMethod" value="someJob" /> <property name="arguments" value="Hello World!" /> </bean> <bean id="timerTask" class="org.springframework.scheduling.timer.ScheduledTimerTask"> <property name="delay" value="1000" /> <property name="period" value="3000" /> <property name="timerTask" ref="task" /> </bean> <bean id="timerFactory" class="org.springframework.scheduling.timer.TimerFactoryBean"> <property name="scheduledTimerTasks"> <list> <ref local="timerTask"/> </list> </property> </bean> </beans>
We can replace the definition of our own custom TimerTask
bean with a definition using the MethodInvokingTimerTaskFactoryBean
. To configure MethodInvokingTimerTaskFactoryBean
, we specify the target of the invocation as a reference to another bean, the method to execute, and the argument to use when executing. The TimerTask
supplied by MethodInvokingTimerTaskFactoryBean
is used in the normal way, wrapped in a ScheduledTimerTask
, and passed to the TimerFactoryBean
.
Listing 12-13 shows a simple driver program to test this out.
Example 12.13. Testing the MethodInvokingTimerTaskFactoryBean
package com.apress.prospring2.ch12.timer; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class MethodInvokerScheduling { public static void main(String[] args) throws Exception { ApplicationContext ctx = new FileSystemXmlApplicationContext( "./ch12/src/conf/timerMethodInvoker.xml"); System.in.read(); } }
Running this example gives you the now familiar timed appearance of "Hello, World" messages on your console. Clearly, using MethodInvokingTimerTaskFactoryBean
removes the need to create custom TimerTask
implementations that simply wrap the execution of a business method.
JDK Timer
-based scheduling provides support for an application's basic scheduling needs using a simple and easy-to-understand architecture. Although the trigger system for JDK Timer
is not extremely flexible, it does provide basic schemes that allow you to control simple scheduling. Using Spring's support classes for Timer
, you externalize a task scheduling configuration and make it easier to add and remove tasks from the scheduler without having to change any application code. Using MethodInvokingTimerTaskFactoryBean
, you avoid having to create TimerTask
implementations that do nothing more than invoke a business method, thus reducing the amount of code you need to write and maintain.
The main drawback of JDK Timer
scheduling comes when you need to support complex triggers such as a trigger to execute a job every Monday, Wednesday, and Friday at 3:00 p.m. In the next part of this chapter, we look at the Quartz scheduling engine, which provides much more comprehensive support for scheduling and, just like Timer
, is fully integrated into Spring.
The open source Quartz project is a dedicated job scheduling engine designed to be used in both Java EE and Java SE settings. Quartz provides a huge range of features such as persistent jobs, clustering, and distributed transactions, though we do not look at the clustering or distributed transaction features in this book—you can find out more about these online at www.opensymphony.com/quartz
. Spring's Quartz integration is similar to its Timer
integration in that it provides for declarative configuration of jobs, triggers, and schedules. In addition to this, Spring provides additional job persistence features that allow the scheduling of a Quartz job to take part in a Spring-managed transaction.
Quartz is an extremely powerful job scheduling engine, and we cannot hope to explain everything about it in this chapter. However, we do cover the main aspects of Quartz that are related to Spring, and we discuss how you can use Quartz from a Spring application. As with our Timer
discussion, we start by looking at Quartz separately from Spring, and then we look at integrating Quartz and Spring.
The core of Quartz is made up of two interfaces, Job
and Scheduler
, and two classes, JobDetail
and Trigger
. From their names, it should be apparent what Job
, Scheduler
, and Trigger
do, but the role of the JobDetail
class is not so clear. Unlike Timer
-based scheduling, tasks are not executed using a single instance of your job class; instead, Quartz creates instances as it needs them. You can use the JobDetail
class to encapsulate the job state and to pass information to a job between subsequent executions of a job. Timer
-based scheduling has no notion of a Trigger
class; Trigger
logic is encapsulated by the Timer
class itself. Quartz supports a pluggable architecture for triggers, which allows you to create your own implementations as you see fit. That said, you rarely create your own Trigger
implementations because Quartz provides the superpowerful CronTrigger
class out of the box, which allows you to use cron expressions (more on that shortly) to have fine-grained control over job execution.
Simple Job Scheduling
To create a job for use in Quartz, simply create a class that implements the Job
interface. The Job
interface defines a single method, execute()
, from which you call your business logic. Quartz passes an instance of JobExecutionContext
to the execute()
method, allowing you to access context data about the current execution. We'll look at this in more detail in the next section.
Listing 12-14 show a simple Job
implementation that writes "Hello, World" to stdout
.
Example 12.14. Creating a Simple Job
package com.apress.prospring2.ch12.quartz; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class HelloWorldJob implements Job { public void execute(JobExecutionContext context) throws JobExecutionException { System.out.println("Hello World!"); } }
To schedule this job to run, we first need to obtain a Scheduler
instance, then create a JobDetail
bean that contains information about the job, and finally create a Trigger
to govern job execution. The code for this is shown in Listing 12-15.
Example 12.15. Scheduling Jobs in Quartz
package com.apress.prospring2.ch12.quartz; import java.util.Date; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; public class HelloWorldScheduling { public static void main(String[] args) throws Exception { Scheduler scheduler = new StdSchedulerFactory().getScheduler(); scheduler.start(); JobDetail jobDetail = new JobDetail("helloWorldJob", Scheduler.DEFAULT_GROUP, HelloWorldJob.class); Trigger trigger = new SimpleTrigger("simpleTrigger", Scheduler.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 3000); scheduler.scheduleJob(jobDetail, trigger); } }
This code starts by obtaining an instance of Scheduler
using the StdSchedulerFactory
class. We are not going to look at this class in any detail here, but you can find out more information in the Quartz tutorial, which is available on the OpenSymphony web site. For now, it is enough to know that the StdSchedulerFactory.getScheduler()
class returns a Scheduler
instance that is ready to run. In Quartz, a Scheduler
can be started, stopped, and paused. If a Scheduler
has not been started or is paused, no triggers fire, so we start the Scheduler
using the start()
method.
Next, we create the JobDetail
instance of the job we are scheduling, passing in three arguments to the constructor. The first argument is the job name and refers to this job when using one of the Scheduler
interface's administration methods, such as pauseJob()
, which allows a particular job to be paused. The second argument is the group name, for which we are using the default. Group names can be used to refer to a group of jobs together, perhaps to pause them all using Scheduler.pauseJobGroup()
. You should note that job names are unique within a group. The third and final argument is the Class
that implements this particular job.
With the JobDetail
instance created, we now move on to create a Trigger
. In this example, we use the SimpleTrigger
class, which provides JDK Timer
-style trigger behavior. The first and second arguments passed to the SimpleTrigger
constructor are the trigger name and group name, respectively. Both of these arguments perform similar functions for a Trigger
as they do for a JobDetail
. Also note that trigger names must be unique within a group, as otherwise, an exception is raised. The third and fourth arguments, both of type Date
, are the start and end dates for this Trigger
. By specifying null
for the end date, we are saying there is no end date. The ability to specify an end date for a trigger is not available when you are using Timer
. The next argument is the repeat count, which allows you to specify the maximum number of times the Trigger
can fire. We use the constant REPEAT_INDEFINITELY
to allow the Trigger
to fire without a limit. The final argument is the interval between Trigger
firings and is defined in milliseconds. We have defined an interval of 3 seconds.
The final step in this example is to schedule the job with a call to Scheduler.schedule()
that passes in the JobDetail
instance and the Trigger
. If you run this application, you will see the familiar stream of "Hello, World" messages appearing gradually in your console.
Using JobDataMaps
In the previous example, all information for the job execution was contained in the job itself. However, you can pass state into the job using the JobDetail
or Trigger
class. Each instance of JobDetail
and Trigger
has an associated JobDataMap
instance, which implements Map
, and allows you to pass in job data in key/value pairs. Your jobs can modify data in the JobDataMap
to allow for the passing of data between subsequent executions of the job. However, there are some considerations related to job persistence when using this approach. We discuss these later in the "About Job Persistence" section.
Storing data about a Trigger
is useful when you have the same job scheduled with multiple Trigger
implementations and want to provide the job with different data on each independent triggering. Entries of this map are made available via the JobDataMap
on the JobExecutionContext
that can be retrieved via the getMergedJobDataMap
method. As the method name suggests, the JobDataMap
on the JobExecutionContext
is a merge of the JobDataMap
found on the JobDetail
and the JobDataMap
found on the Trigger
, whereas data stored in the Trigger
overrides data stored on the JobDetail
.
In Listing 12-16, you can see an example of a Job
that uses data contained in the merged JobDataMap
to perform its processing.
Example 12.16. Using the JobDataMap
package com.apress.prospring2.ch12.quartz; import java.util.Map; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; public class MessageJob implements Job { public void execute(JobExecutionContext context) throws JobExecutionException {
Map properties = context.getMergedJobDataMap(); System.out.println("Previous Fire Time: " + context.getPreviousFireTime()); System.out.println("Current Fire Time: " + context.getFireTime()); System.out.println("Next Fire Time: " + context.getNextFireTime()); System.out.println(properties.get("message")); System.out.println(properties.get("jobDetailMessage")); System.out.println(properties.get("triggerMessage")); System.out.println(""); } }
From the merged JobDataMap
, we are able to extract the Object
s that are keyed as message
, jobDetailMessage
, and triggerMessage
and write them to stdout
. Also notice that we are able to get information about the previous, current, and next execution of this job from the JobExecutionContext
.
In Listing 12-17, you can see an example of how you populate the JobDataMap
on JobDetail
with data when scheduling the Job
.
Example 12.17. Adding Data to the JobDetail JobDataMap
package com.apress.prospring2.ch12.quartz; import org.quartz.Scheduler; import org.quartz.SimpleTrigger; import org.quartz.JobDetail; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import java.util.Map; import java.util.Date; public class JobDetailMessageScheduling { public static void main(String[] args) throws Exception { Scheduler scheduler = new StdSchedulerFactory().getScheduler(); scheduler.start(); JobDetail jobDetail = new JobDetail("messageJob", Scheduler.DEFAULT_GROUP, MessageJob.class); Map map = jobDetail.getJobDataMap(); map.put("message", "This is a message from Quartz"); map.put("jobDetailMessage", "A jobDetail message"); Trigger trigger = new SimpleTrigger("simpleTrigger", Scheduler.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 3000); scheduler.scheduleJob(jobDetail, trigger); } }
You will recognize much of this code from the example in Listing 12-15, but notice that once the JobDetail
instance has been created here, we access the JobDataMap
and add two messages to it, keyed as message
and jobDetailMessage
. If you run this example and leave it running for a few iterations, you end up with output similar to this:
Previous Fire Time: null Current Fire Time: Tue Oct 23 11:02:19 BST 2007 Next Fire Time: Tue Oct 23 11:02:22 BST 2007 This is a message from Quartz A jobDetail message null Previous Fire Time: Tue Oct 23 11:02:19 BST 2007 Current Fire Time: Tue Oct 23 11:02:22 BST 2007 Next Fire Time: Tue Oct 23 11:02:25 BST 2007 This is a message from Quartz A jobDetail message null Previous Fire Time: Tue Oct 23 11:02:22 BST 2007 Current Fire Time: Tue Oct 23 11:02:25 BST 2007 Next Fire Time: Tue Oct 23 11:02:28 BST 2007 This is a message from Quartz A jobDetail message null
You can see that both messages contained in the JobDataMap
are written to stdout
after the information about the execution times of the previous, current, and next execution is displayed.
Listing 12-18 gives you an example of also providing data on the Trigger
and the effect that merging the two JobDataMap
instances has on the values.
Example 12.18. Using the JobDataMap on the Trigger
package com.apress.prospring2.ch12.quartz; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import java.util.Date; public class TriggerMessageScheduling { public static void main(String[] args) throws Exception { Scheduler scheduler = new StdSchedulerFactory().getScheduler(); scheduler.start(); JobDetail jobDetail = new JobDetail("triggerMessageJob", Scheduler.DEFAULT_GROUP, MessageJob.class); jobDetail.getJobDataMap().put("message", "This is a message from Quartz"); jobDetail.getJobDataMap().put("jobDetailMessage", "My job details data."); Trigger trigger = new SimpleTrigger("simpleTrigger", Scheduler.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 3000); trigger.getJobDataMap().put("message", "Message from Trigger"); trigger.getJobDataMap().put("triggerMessage", "Another trigger message.");
scheduler.scheduleJob(jobDetail, trigger); } }
As you can see, the JobDetail
is configured exactly as before. We simply add two messages to the Trigger
: a keyed message and a trigger message. Running this example produces output similar to the following:
Previous Fire Time: null Current Fire Time: Tue Oct 23 11:14:22 BST 2007 Next Fire Time: Tue Oct 23 11:14:25 BST 2007 Message from Trigger My job details data. Another trigger message. Previous Fire Time: Tue Oct 23 11:14:22 BST 2007 Current Fire Time: Tue Oct 23 11:14:25 BST 2007 Next Fire Time: Tue Oct 23 11:14:28 BST 2007 Message from Trigger My job details data. Another trigger message. Previous Fire Time: Tue Oct 23 11:14:25 BST 2007 Current Fire Time: Tue Oct 23 11:14:28 BST 2007 Next Fire Time: Tue Oct 23 11:14:31 BST 2007 Message from Trigger My job details data. Another trigger message.
Note that the value of the key message
is what we added to the Trigger JobDataMap
, not what we defined in the JobDetail
.
As you will see shortly, when using Spring to configure Quartz scheduling, you can create the JobDataMap
in your Spring configuration file, allowing you to externalize all Job
configuration completely.
Using the CronTrigger
In the previous examples, we used the SimpleTrigger
class, which provides trigger functionality very similar to that of the JDK Timer
class. However, Quartz excels in its support for complex trigger expressions using the CronTrigger
. CronTrigger
is based on the Unix cron daemon, a scheduling application that supports a simple, yet extremely powerful, trigger syntax. Using CronTrigger
, you can quickly and accurately define trigger expressions that would be extremely difficult or impossible to do with the SimpleTrigger
class. For instance, you can create a trigger that says, "Fire every 5 seconds of every minute, starting at the third second of the minute, but only between the hours of 2:00 and 5:00 p.m." Or it could say, "Fire on the last Friday of every month."
A CronTrigger
syntax expression, referred to as a cron expression, contains six required components and one optional component. A cron expression is written on a single line, and each component is separated from the next by a space. Only the last, or rightmost, component is optional. Table 12-1 describes the cron components in detail.
Table 12.1. Components of a cron Expression
Position | Meaning | Allowed Special Characters |
---|---|---|
1 | Seconds (0–59) | ,, |
2 | Minutes (0–59) | ,, |
3 | Hours (0–23) | ,, |
4 | Day of the month (1–31) | ,, |
5 | Month (either | ,, |
6 | Day of the week (either | ,, |
7 | Year (optional, 1970–2099), when empty, full range is assumed | ,, |
Each component accepts the typical range of values that you would expect, such as 0-59 for seconds and minutes and 1-31 for the day of the month. For the month and day of the week components, you can use numbers, such as 1–7 for day of the week, or text such as SUN-SAT
.
Each field also accepts a given set of special symbols, so placing an asterisk (*
) in the hours component means "every hour," and using an expression such as 6L
in the day of the week component means "last Friday of the month." Table 12-2 describes cron wildcards and special characters in detail.
Table 12.2. cron Expression Wildcards and Special Characters
Special Character | Description |
---|---|
| Any value. This special character can be used in any field to indicate that the value should not be checked. Therefore, our example cron expression will be fired on every day of the month, every month, and every day of the week between 1970 and 2099. |
| No specific value. This special character is usually used with other specific values to indicate that a value must be present but will not be checked. |
| Range. For example, |
| List separator. Allows you to specify a list of values, such as |
| Increments. This character specifies increments of a value. For example, |
| An abbreviation for "last." The meaning of |
|
|
| This value is allowed only for the day of the week field, and it specifies the nth day in a month. For example |
| The calendar value. This is allowed for the day of the month and day of the week fields. The values of days are calculated against a specified calendar. Specifying |
The last thing to bear in mind when writing cron expressions is daylight saving time. Changes because of daylight saving time may cause a trigger to fire twice in spring or to never fire in autumn.
There are many more permutations for cron expressions than we can discuss here; you can find a detailed description of cron syntax in the Javadoc for the CronTrigger
class.
Listing 12-19 shows an example of the CronTrigger
class in action.
Example 12.19. Using the CronTrigger Class
package com.apress.prospring2.ch12.quartz; import java.util.Map; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; public class CronTriggerExample { public static void main(String[] args) throws Exception { Scheduler scheduler = new StdSchedulerFactory().getScheduler(); scheduler.start(); JobDetail jobDetail = new JobDetail("messageJob", Scheduler.DEFAULT_GROUP, MessageJob.class); Map map = jobDetail.getJobDataMap(); map.put("message", "This is a message from Quartz"); String cronExpression = "3/5 * 14,15,16,17 * * ?"; Trigger trigger = new CronTrigger("cronTrigger", Scheduler.DEFAULT_GROUP, cronExpression); scheduler.scheduleJob(jobDetail, trigger); } }
Much of this code should look familiar to you; the only major difference here is that we use the cron expression. The actual creation of the CronTrigger
class is very similar to the creation of the SimpleTrigger
class in that you have a name and a group name. To help you understand the cron expression in the example, we'll break it down into components.
The first component, 3/5
, means every 5 seconds starting at the third second of the minute. The second component, *
, simply says every minute. The third component, 14, 15, 16, 17
, restricts this trigger to running between 2:00 and 5:59 p.m.—that is, when the time begins with 14, 15, 16, or 17. The next two components are both wildcards saying that this trigger can run in any month or any year. The final component uses the queston mark wildcard, ?
, to indicate that this trigger can run on any day of the week. This expression has the net effect of firing every 5 seconds, starting on the third second of the minute, but only between 2:00 and 5:59 p.m.
If you run this example, depending on the time of day, you see either a blank screen or the ever increasing list of "Hello, World" printouts. Try modifying the first component in the expression to change the frequency of the trigger or at which second in the minute the trigger starts. You should also try modifying other components to see what effects you get.
The CronTrigger
class is great for almost all trigger requirements. However, expressions can quickly become convoluted when you need to program exceptions to the rule. For instance, consider a process that checks a task list for a user every Monday, Wednesday, and Friday at 11:00 a.m. and 3:00 p.m. Now, consider what happens when you want to prevent this trigger from firing when the user is on vacation. Thankfully, Quartz provides support for this via the Calendar
interface. Using the Calendar
interface, you can accurately define a period that should either be explicitly included or explicitly excluded from a trigger's normal schedule. Quartz comes with six implementations of Calendar
, one of which is the HolidayCalendar
that stores a list of days to be excluded from a trigger's schedule. Listing 12-20 shows a modification of the previous example that uses a HolidayCalendar
to exclude December 25, 2007.
Example 12.20. Explicitly Excluding Dates with HolidayCalendar
package com.apress.prospring2.ch12.quartz; import java.util.Calendar; import java.util.Map; import org.quartz.CronTrigger; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.Trigger; import org.quartz.impl.StdSchedulerFactory; import org.quartz.impl.calendar.HolidayCalendar; public class CronWithCalendarExample { public static void main(String[] args) throws Exception { Scheduler scheduler = new StdSchedulerFactory().getScheduler(); scheduler.start(); // create a calendar to exclude a particular date Calendar cal = Calendar.getInstance(); cal.set(2007, Calendar.DECEMBER, 25); HolidayCalendar calendar = new HolidayCalendar(); calendar.addExcludedDate(cal.getTime()); // add to scheduler scheduler.addCalendar("xmasCalendar", calendar, true, false); JobDetail jobDetail = new JobDetail("messageJob", Scheduler.DEFAULT_GROUP, MessageJob.class);
Map map = jobDetail.getJobDataMap(); map.put("message", "This is a message from Quartz"); String cronExpression = "3/5 * 14,15,16,17 * * ?"; Trigger trigger = new CronTrigger("cronTrigger", Scheduler.DEFAULT_GROUP, cronExpression); trigger.setCalendarName("xmasCalendar"); scheduler.scheduleJob(jobDetail, trigger); } }
Here, you can see that we create an instance of HolidayCalendar
, and using the addExcludedDate()
method, we exclude December 25. With the Calendar
instance created, we add the Calendar
to the Scheduler
using the addCalendar()
method, giving it a name of xmasCalendar
. Later, before adding the CronTrigger
, we associate it with xmasCalendar
. Using this approach saves you from having to create complex cron expressions just to exclude a few arbitrary dates.
About Job Persistence
Quartz provides support for Job
persistence, allowing you to add jobs at runtime or make changes to existing jobs and persist these changes and additions for subsequent executions of the Job
. Central to this concept is the JobStore
interface, implementations of which are used by Quartz when it is performing persistence. By default, Quartz uses the RAMJobStore
implementation, which simply stores Job
instances in memory. Other available implementations are JobStoreCMT
and JobStoreTX
. Both of these classes persist job details using a configured DataSource
and support the creation and modification of jobs as part of a transaction. The JobStoreCMT
implementation is intended to be used in an application server environment and takes part in container-managed transactions. For stand-alone applications, you should use the JobStoreTX
implementation. Spring provides its own LocalDataSourceJobStore
implementation of JobStore
, which can take part in Spring-managed transactions. We will take a look at this implementation when we discuss Spring support for Quartz.
Earlier on, you saw how you can modify the contents of the JobDataMap
to pass information between different executions of the same Job
. However, if you try to run that example using a JobStore
implementation other than RAMJobStore
, you will be surprised to see that it doesn't work. The reason for this is that Quartz supports the notion of stateless and stateful jobs. When using the RAMJobStore
and modifying the JobDataMap
, you are actually modifying the store directly, so the type of Job
is unimportant, but this is not the case when you are using implementations other than RAMJobStore
. A stateless Job
only has the data in the JobDataMap
persisted when it is added to the Scheduler
, whereas stateful Job
s have their JobDataMap
persisted after every execution. To mark a Job
as stateful, implement the StatefulJob
interface instead of the Job
interface. StatefulJob
is a subinterface of Job
, so you do not need to implement Job
as well. You should also be aware that any data you place in the JobDataMap
when using Job
persistence must be serializable, because Quartz writes the JobDataMap
as a serialized blob to the database.
Spring's Quartz integration follows a similar pattern to the integration with Timer
in that it allows you to configure your job scheduling fully within the Spring configuration file. In addition to this, Spring provides further classes to integrate with the Quartz JobStore
, thus allowing you to configure Job
persistence in your configuration and for Job
modification to take part in Spring-managed transactions.
Scheduling a Job with Spring
As you would expect, much of the code you need to schedule a Quartz Job
using Spring goes into the Spring configuration file. Indeed, you only need to load the ApplicationContext
in your application for the configuration to take effect and for Spring to start the Scheduler
automatically.
In Listing 12-21, you can see the configuration code required to configure the MessageJob
class you saw in Listing 12-16 to run once every 3 seconds.
Example 12.21. Configuring Scheduling Declaratively
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <bean id="job" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="com.apress.prospring2.ch12.quartz.MessageJob"/> <property name="jobDataAsMap"> <map> <entry key="message" value="This is a message from the Spring config file!"/> </map> </property> </bean> <bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> <property name="jobDetail" ref="job"/> <property name="startDelay" value="1000"/> <property name="repeatInterval" value="3000"/> <property name="jobDataAsMap"> <map> <entry key="triggerMessage" value="Trigger message from the Spring config file!"/> </map> </property> </bean> <bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref local="trigger"/> </list> </property> </bean> </beans>
Here, you can see that we use the JobDetailBean
class, which extends the JobDetail
class, to configure the job data in a declarative manner. The JobDetailBean
provides more JavaBean-style properties that are accessible by Spring, and it also provides sensible defaults for properties that you usually have to specify yourself. For instance, notice that we did not specify a job name or a group name. By default, the JobDetailBean
uses the ID of the <bean>
tag as the job name and the default group of the Scheduler
as the group name. Notice that we are able to add data to the JobDataMap
property using the jobDataAsMap
property. The name of this property is not a typographical error— you can't add directly to the jobDataMap
property. It is of type JobDataMap
, and this type is not supported in Spring configuration files.
With the JobDetailBean
configured, the next step is to create a trigger. Spring offers two classes, SimpleTriggerBean
and CronTriggerBean
, that wrap the SimpleTrigger
and CronTrigger
classes, allowing you to configure them declaratively and to associate them with a JobDetailBean
—all within your configuration file. Notice that in Listing 12-21, we defined a starting delay of 1 second and a repeat interval of 3 seconds. By default, the SimpleTriggerBean
sets the repeat count to infinity.
The final piece of configuration you need is the SchedulerFactoryBean
. By default, the SchedulerFactoryBean
creates an instance of StdSchedulerFactory
, which, in turn, creates the Scheduler
implementation. You can override this behavior by setting the schedulerFactoryClass
property to the name of a class that implements SchedulerFactory
, which you wish to use in place of StdSchedulerFactory
. The only property that you need to configure scheduling is the triggers
property, which accepts a List
of TriggerBean
elements.
Because all of the job scheduling configuration is contained in the configuration, you need very little code to actually start the Scheduler
and execute the Job
instances. In fact, all you need to do is create the ApplicationContext
, as shown in Listing 12-22.
Example 12.22. Testing Declarative Quartz Configuration
package com.apress.prospring2.ch12.quartz.spring; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class SimpleSpringQuartzIntegrationExample { public static void main(String[] args) { ApplicationContext ctx = new FileSystemXmlApplicationContext( "./ch12/src/conf/quartz-simple.xml "); } }
As you can see, this class does nothing more than create an instance of ApplicationContext
using the configuration shown in Listing 12-21. If you run this application and leave it running for a few iterations, you end up with something like this:
Previous Fire Time: null Current Fire Time: Tue Oct 23 11:24:31 BST 2007 Next Fire Time: Tue Oct 23 11:24:34 BST 2007 This is a message from the Spring configuration file! null Trigger message from the Spring configuration file! Previous Fire Time: Tue Oct 23 11:24:31 BST 2007 Current Fire Time: Tue Oct 23 11:24:34 BST 2007 Next Fire Time: Tue Oct 23 11:24:37 BST 2007 This is a message from the Spring configuration file! null Trigger message from the Spring configuration file! Previous Fire Time: Tue Oct 23 11:24:34 BST 2007 Current Fire Time: Tue Oct 23 11:24:37 BST 2007 Next Fire Time: Tue Oct 23 11:24:40 BST 2007 This is a message from the Spring configuration file! null Trigger message from the Spring configuration file!
Notice that it is running just like it was for the previous MessageJob
example, but the messages displayed are the messages configured in the Spring configuration file.
Using Persistent Jobs
One of the great features of Quartz is its ability to create stateful, persistent jobs. This opens up some great functionality that is not available when you are using Timer
-based scheduling. With persistent jobs, when you can add jobs to Quartz at runtime, they will still be in your application after a restart. Plus, you can modify the JobDataMap
passed between executions of a Job
, and the changes will still be in effect after a restart.
In this example, we are going to schedule two jobs, one using Spring configuration mechanisms and one at runtime. We'll see how the Quartz persistence mechanism copes with changes to the JobDataMap
for these jobs and what happens in subsequent executions of the application.
To start with, you need to create a database in which Quartz can store the Job
information. In the Quartz distribution—we used version 1.6.0—you will find a selection of database scripts for a variety of different RDBMS flavors. For the example here, we use Oracle, but you should not encounter problems using a different database as long as Quartz has a database script for it. For version 1.6.0, try the docs/dbTables
subfolder of the Quartz distribution. Once you have located the script for your database, execute it against your database and verify that 12 tables, each with the prefix qrtz
, have been created.
Next, create your test Job
. Because we want to make changes to JobDataMap
during Job
execution, we need to flag to Quartz that it should treat this as a stateful Job
. We do this by implementing the StatefulJob
interface rather than the Job
interface. This is shown in Listing 12-23.
Example 12.23. Creating a Stateful Job
package com.apress.prospring2.ch12.quartz.spring; import java.util.Map; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.quartz.StatefulJob; public class PersistentJob implements StatefulJob { public void execute(JobExecutionContext context) throws JobExecutionException { Map map = context.getJobDetail().getJobDataMap(); System.out.println("[" + context.getJobDetail().getName() + "]" + map.get("message")); map.put("message", "Updated Message"); } }
The StatefulJob
interface does not declare additional methods for your class to implement; it is simply a marker telling Quartz that it should persist the JobDetail
after every execution. Here, you can see that we display the message that is stored in the JobDataMap
along with the name of the Job
.
The next steps are to configure the Job
in Spring and configure the Scheduler
with a DataSource
it can use for persistence, as shown in Listing 12-24.
Example 12.24. Configuring Quartz Persistence in Spring
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd"> <bean id="job" class="org.springframework.scheduling.quartz.JobDetailBean"> <property name="jobClass" value="com.apress.prospring2.ch12.quartz.spring.PersistentJob"/> <property name="jobDataAsMap"> <map> <entry key="message" value="Original Message"/> </map> </property> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.SingleConnectionDataSource"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@oracle.devcake.co.uk:1521:INTL"/> <property name="username" value="PROSPRING"/> <property name="password" value="x******6"/> </bean> <bean id="trigger" class="org.springframework.scheduling.quartz.SimpleTriggerBean"> <property name="jobDetail" ref="job"/> <property name="startDelay" value="1000"/> <property name="repeatInterval" value="3000"/> </bean> <bean id="schedulerFactory" class="org.springframework.scheduling.quartz.SchedulerFactoryBean"> <property name="triggers"> <list> <ref local="trigger"/> </list> </property> <property name="dataSource" ref="dataSource"/> </bean> </beans>
You will recognize much of this configuration code from Listing 12-21; the important part here is the dataSource
bean. In this code, we use the Spring class SingleConnectionDataSource
; this DataSource
implementation is handy for testing, but never use it in production (check the Javadoc for this class if you are unsure why). Also, remember, you need to modify the connection details in the configuration as appropriate for your environment. For more details on configuring other DataSource
s with Spring, see Chapter 8.
Using the configured dataSource
bean, we set the dataSource
property of the SchedulerFactoryBean
. By doing this, we instruct Spring to create a Scheduler
that is configured to persist Job
data using the given DataSource
. Internally, this is achieved using Spring's own JobStore
implementation, LocalDataSourceJobStore
.
With the configuration complete, all that remains is to load it in an application and add another Job
to the Scheduler
at runtime. Listing 12-25 shows the code for this.
Example 12.25. Testing Job Persistence
package com.apress.prospring2.ch12.quartz.spring; import java.util.Date; import org.quartz.JobDetail; import org.quartz.Scheduler; import org.quartz.SimpleTrigger; import org.quartz.Trigger; import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class SpringWithJobPersistence { public static void main(String[] args) throws Exception { ApplicationContext ctx = new FileSystemXmlApplicationContext( "./ch12/src/conf/quartzPersistent.xml"); // get the scheduler Scheduler scheduler = (Scheduler) ctx.getBean("schedulerFactory"); JobDetail job = scheduler.getJobDetail("otherJob", Scheduler.DEFAULT_GROUP); if (job == null) { // the job has not yet been created job = (JobDetail) ctx.getBean("job"); job.setName("otherJob"); job.getJobDataMap().put("message", "This is another message"); Trigger trigger = new SimpleTrigger("simpleTrigger", Scheduler.DEFAULT_GROUP, new Date(), null, SimpleTrigger.REPEAT_INDEFINITELY, 3000); scheduler.scheduleJob(job, trigger); } } }
This code requires little explanation. However, note that before we schedule the second job, we check to see if it already exists using the Scheduler.getJobDetail()
method. This prevents us from overwriting the Job
on subsequent runs of the application.
The first time you run this example, you get output something like this:
[otherJob]This is another message [job]Original Message [otherJob]Updated Message [job]Updated Message [otherJob]Updated Message [job]Updated Message
As you can see, the first time each Job
executes, the message displayed is the original message configured in the JobDataMap
when the Job
was scheduled. On subsequent executions, each Job
displays the updated message that was set during the previous execution. If you stop the application and restart it, you see something slightly different:
[otherJob]Updated Message [job]Updated Message [otherJob]Updated Message [job]Updated Message [otherJob]Updated Message [job]Updated Message
This time, you can see that, because the Job
data was persisted, you do not need to re-create the second Job
, and the JobDataMap
accurately reflects changes that were made during the last run of the application.
Scheduling Arbitrary Jobs with Quartz
Like the Timer
-based scheduling classes, Spring provides the ability to schedule the execution of arbitrary methods using Quartz. We won't go into detail on this, because it works in an almost identical manner to the Timer
approach. Instead of using MethodInvokingTimerTaskFactoryBean
, you use MethodInvokingJobDetailFactoryBean
, and instead of automatically creating TimerTask
implementations, you automatically create JobDetail
ones.
If you are going to be adding job scheduling to your application, you should bear in mind a few considerations when you choose a scheduler and a scheduling approach.
The first decision you have to make when adding scheduling to your application is which scheduler to use. This choice is actually quite easy. If you have only very simple scheduling requirements or are restricted in the external libraries that you can package with your application, you should use Timer
-based scheduling. Otherwise, use Quartz.
Even if you find that your requirements are simple, you might want to go with Quartz, especially if you have to create an explicit Job
implementation. If you use Quartz from the outset, if your requirements become more advanced, you can easily add persistence, transactions, or more complex triggers without having to change a TimerTask
to a Job
. In general, we have found that using Quartz for all of our scheduling allows us to become familiar with a single scheduling approach and saves our developers from having to worry about two different approaches when one provides everything they need.
A common approach that we see many developers take when adding scheduling to an application is to place business logic inside a Job
or TimerTask
. Generally, this is a bad idea. In many cases, you need to have scheduled tasks available for execution on demand, which requires the logic to be separate from the scheduling framework.
Also, you should not unnecessarily couple your business logic to a particular scheduler. We have found that a better approach is to keep business logic in separate classes and either create a simple wrapper around those classes that is specific to your scheduler or, preferably, use the appropriate MethodInvoker*FactoryBean
to create the wrapper for you.
So far in this chapter, we have discussed various ways of scheduling jobs to be executed at a specific point in time, at defined intervals, or using a combination of both times and intervals. Now, we are going to look at another way to schedule jobs in Spring that depend less on a specific time or interval than on immediate or event-triggered execution.
For example, think of a web server handling incoming requests. A simple approach for building such a server application would be to process each job in a new thread. Depending on the server you are building and its environment, this might work absolutely fine. But as the creation of a thread needs time and system resources, you might end up spending more time creating and destroying threads than executing jobs, not to mention that you might run out of system resources. To run stably, a server needs some way of managing how much can be done at the same time. The concept of thread pools and work queue offers just this.
The java.util.concurrent Package
One welcome addition to Java 5 was the java.util.concurrent
package based on Doug Lea's util.concurrent
package, a library offering efficient and well-tested tools to simplify the development of multithreaded applications. This package provides the Executor
interface, which defines only one method execute(Runnable command)
to execute Runnable
tasks. It abstracts the submission of tasks away from the details of how they are run. Implementations of this interface offer all sorts of execution policy: thread-per-task, thread pooling, or even synchronous execution just to name a few (you can find some implementations of these in the Javadoc for the Executor
interface).
To give you a little example of how this interface and its subinterface ExecutorService
can be used, we will first create a task to execute using a slightly amended version of our former HelloWorldTask
from Listing 12-1. We could just as well use the HelloWorldTask
straight away, as it extends TimerTask
that implements Runnable
, but we wouldn't be able to see the task scheduling differences between various Executor
implementations.
Example 12.26. HelloWorldCountDownTask
package com.apress.prospring2.ch12; public class HelloWorldCountDownTask implements Runnable { private String name; private int count = 4; public HelloWorldCountDownTask(String name) { this.name = name; } public void run() { while (count > 0) { count--; if (count == 0) { System.out.println(name + " says 'Hello World!"); } else { System.out.println(name + ": " + count); Thread.yield(); }
} } }
All this task does is print out a countdown from 3, calling the Thread.yield()
method afterward to pause this thread's executions and allow other threads to be executed. As a last statement, the task will say hello to the world and finish execution.
Next, as shown in Listing 12-27, we are going to use an ExecutorService
implementation to schedule and execute this task.
Example 12.27. Use ExecutorService to Schedule and Execute a Task
package com.apress.prospring2.ch12.executor; import com.apress.prospring2.ch12.HelloWorldCountDownTask; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ExecutorServiceExample { public static void main(String[] args) { ExecutorService service = Executors.newFixedThreadPool(2); service.execute(new HelloWorldCountDownTask("Anna")); service.execute(new HelloWorldCountDownTask("Beth")); service.execute(new HelloWorldCountDownTask("Charlie")); service.execute(new HelloWorldCountDownTask("Daniel")); service.shutdown(); } }
The java.util.concurrent.Executors
class provides convenient factory and utility methods for Executor
and ExecutorService
classes. We are using its newFixedThreadPool()
method to retrieve a ThreadPoolExecutor
with a fixed number of two threads in the pool. We then submit four tasks to execution and call the ExecutorService
's shutdown()
method to shut down the ExecutorService
after all tasks have been executed. After calling this method, no further tasks can be added to the service. Running this example will print out the following:
Anna: 3 Anna: 2 Beth: 3 Anna: 1 Anna says 'Hello World! Charlie: 3 Beth: 2 Charlie: 2 Beth: 1 Charlie: 1 Beth says 'Hello World! Daniel: 3 Charlie says 'Hello World! Daniel: 2 Daniel: 1 Daniel says 'Hello World!
Note that there are only two tasks being executed at the same time, and task Charlie
is only getting executed after task Anna
has finished. Try a different number of threads in the pool or a different Executor
implementation, and you will find the printout to be different.
Spring's TaskExecutor Abstraction
Since version 2.0, Spring has offered an abstraction to the previously discussed Java 5 Executor
framework. Identical to the java.util.concurrent.Executor
interface, the TaskExecutor
interface defines only the single method execute(Runnable command)
. Intended to be used internally in other Spring components such as asynchronous JMS and JCA environment support, it now lets you add thread pooling behavior to your own application without creating the need for Java 5.
Spring comes with a variety of TaskExecutor
implementations, which are described in Table 12-3.
Table 12.3. Springs TaskExecutor Implementations
Implementation | Description |
---|---|
| This implementation provides asynchronous threading with a new thread per invocation policy. It also allows setting a concurrency limit that will block further invocations. |
| When you choose this implementation task, execution happens synchronously in the calling thread. |
| This class implements Spring's |
| This is a subclass of Quartz's |
| Behaving similarly to the |
| This implementation uses a |
| This uses the CommonJ |
The differences in how the various implementations work is easiest to see in a small example. Listing 12-28 shows the configuration of three TaskExecutor
implementations in Spring: ThreadPoolTaskExecutor
, SyncTaskExecutor
, and TimerTaskExecutor
.
Example 12.28. task-executor-context.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"> <bean id="threadPoolTaskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor"> <property name="corePoolSize" value="5"/>
<property name="maxPoolSize" value="10"/> <property name="queueCapacity" value="25"/> </bean> <bean id="synchTaskExecutor" class="org.springframework.core.task.SyncTaskExecutor"/> <bean id="timerTaskExecutor" class="org.springframework.scheduling.timer.TimerTaskExecutor"> <property name="delay" value="3000"/> <property name="timer" ref="timer"/> </bean> <bean id="timer" class="java.util.Timer"/> </beans>
We can then use the defined beans to load them from the ApplicationContext
and use them in a TaskExecutorExample
, as shown in Listing 12-29.
Example 12.29. TaskExecutorExample
package com.apress.prospring2.ch12.taskexecutor; import org.springframework.core.task.TaskExecutor; import com.apress.prospring2.ch12.HelloWorldCountDownTask; public class TaskExecutorExample { private TaskExecutor taskExecutor; public TaskExecutorExample(TaskExecutor taskExecutor) { this.taskExecutor = taskExecutor; } public void executeTasks() { this.taskExecutor.execute(new HelloWorldCountDownTask("Anna")); this.taskExecutor.execute(new HelloWorldCountDownTask("Beth")); this.taskExecutor.execute(new HelloWorldCountDownTask("Charlie")); this.taskExecutor.execute(new HelloWorldCountDownTask("Daniel")); } }
As you see, we use the same four HelloWorldCountDownTask
instances as before. The generated output will highlight the different execution strategies. As expected, the SyncTaskExecutorExample
, shown in Listing 12-30, executes the tasks synchronously:
Example 12.30. SyncTaskExecutorExample
package com.apress.prospring2.ch12.taskexecutor; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.task.TaskExecutor; import java.io.IOException; public class SynchTaskExecutorExample {
public static void main(String[] args) throws IOException { ApplicationContext ctx = new ClassPathXmlApplicationContext( "com/apress/prospring2/ch12/taskexecutor/task-executor-context.xml"); TaskExecutor taskExecutor = (TaskExecutor)ctx.getBean("synchTaskExecutor", TaskExecutor.class); TaskExecutorExample example = new TaskExecutorExample(taskExecutor); example.executeTasks(); } }
Running this code will create output like what's shown here:
Anna: 3 Anna: 2 Anna: 1 Anna says 'Hello World! Beth: 3 Beth: 2 Beth: 1 Beth says 'Hello World! Charlie: 3 Charlie: 2 Charlie: 1 Charlie says 'Hello World! Daniel: 3 Daniel: 2 Daniel: 1 Daniel says 'Hello World!
If your application is running on Java 5 or higher, you can configure this abstraction to delegate to any of Java 5's implementations. Spring's ThreadPoolTaskExecutor
enables you to configure a JDK 1.5 ThreadPoolExecutor
through bean properties and exposes it as a Spring TaskExecutor
. Additionally, Spring provides the ConcurrentTaskExecutor
as an adapter class for other Java 5 Executor
implementations, making upgrading from Java 1.4 even easier.
The adapter class implements both the TaskExecutor
and Executor
interfaces. Because the primary interface is the TaskExecutor
, exception handling follows its contract. For example, when a task cannot be accepted for execution, a Spring TaskRejectedException
is thrown rather than a java.util.concurrent
.RejectedExecutionException
.
A further convenient feature of the TaskExecutor
interface is its wrapping of exceptions in runtime exceptions. When a task fails with an exception, the situation is usually considered fatal. Without the need or possibility to recover, the exception can go unchecked—your code stays more portable, and you can quite easily switch between TaskExecutor
implementations.
In this chapter, we showed you various mechanisms for scheduling jobs with Spring. We looked at the basic support offered when you use JDK Timer
and the more sophisticated support offered through Quartz. You saw how the different trigger types are used, and in particular, we explored the CronTrigger
in Quartz as a means of creating complex schedules that match real-world scenarios.
Job scheduling is an important part of enterprise applications, and Spring provides excellent support for adding scheduling to your own applications. In the next chapter, we are going to examine Spring's support for sending e-mail messages.
3.21.39.142