定时 之 Quartz
Table Of Contents
What Is Quartz?- About This Tutorial
- Using Quartz
- Jobs And Triggers
- More About Jobs & JobDetails
- More About Triggers
- SimpleTriggers
- CronTriggers
- TriggerTriggers & JobListeners
- SchedulerListeners
- JobStores
- Configuration, Resource Usage and StdSchedulerFactory
- Advanced (Enterprise) Features
- Miscellaneous Features
- Summary
- Changes and Additions to This Document
What is Quartz?
Quartz is fault-tolerant, and can persist ('remember') your scheduled jobs between system restarts.
What is Quartz - From a Software Component View?
Quartz is distributed as a small java library (.jar file) that contains all of the core Quartz functionality. The main interface (API) to this functionality is the Scheduler interface. It provides simple operations such as scheduling/unscheduling jobs, starting/stopping/pausing the scheduler.
If you wish to schedule your own software components for execution they must implement the simple Job interface, which contains the method execute(). If you wish to have components notified when a scheduled fire-time arrives, then the components should implement either the TriggerListener or JobListener interface.
The main Quartz 'process' can be started and ran within your own application, as a stand-alone application (with an RMI interface), or within a J2EE app. server to be used as a resource by your J2EE components.
About This Tutorial
This tutorial is meant to give you a quick - yet useful - primer on the basic capabilities of Quartz, and how to use Quartz within your own software project. It may take you approximately one hour to read this document end-to-end.
Aside from teaching you how to use Quartz, this document also aims to help you answer the question: "is Quartz the right solution for my project's scheduling needs?"
Although there are Java programming examples throughout this document, you should find it fairly easy to follow, as long as you have rudimentary understanding of how software is written.
When you're ready to actually use Quartz, the JavaDOC that is provided with it should serve as your reference manual. This will help you learn more of the caveats of particular components, and provide you with more examples and instructions.
Help!
Please provide feedback as to this document's: usefulness, completeness, grammer errors, points of confusion, etc. This can be done via the Quartz user forum or mailing list that can be found at the project's development site.
Using Quartz
Before you can use the scheduler, it needs to be instantiated (who'd have guessed?). To do this, you use a SchedulerFactory. Some users of Quartz may keep an instance of a factory serialized in a JNDI store, others may find it just as easy (or easier) to instantiate and use a factory instance directly (such as in the example below).
Once a scheduler is instantiated, it can be started, paused, and shutdown. Note that once a scheduler is shutdown, it cannot be restarted without being re-instantiated. Triggers do not fire (jobs do not execute) until the scheduler has been started, nor while it is in the paused state.
Here's a quick snippet of code, that instantiates and starts a scheduler, and schedules a job for execution:
SchedulerFactory schedFact = new org.quartz.impl.StdSchedulerFactory();
Scheduler sched = schedFact.getScheduler();
sched.start();
JobDetail jobDetail = new JobDetail("myJob",
sched.DEFAULT_GROUP,
DumbJob.class);
SimpleTrigger trigger = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
new Date(),
null,
0,
0L);
sched.scheduleJob(jobDetail, trigger);
As you can see, working with quartz is rather simple. We'll now spend a little time talking about Jobs and Triggers, so that you can more fully understand this example.
Jobs & Triggers
As mentioned previously, you can make Java component executable by the scheduler simply by making it implement the Job interface. Here is the interface:
package org.quartz;
public interface Job {
public void execute(JobExecutionContext context)
throws JobExecutionException;
}
In case you couldn't guess, when the Job's trigger fires (more on that in a moment), the execute(..) method is invoked by the scheduler. The JobExecutionContext object that is passed to this method provides the job instance with information about its "run-time" environment - a handle to the Scheduler that executed it, a handle to the Trigger that triggered the execution, the job's JobDetail object, and a few other items.
The JobDetail object is created by the Quartz client (your program) at the time the Job is added to the scheduler. It contains various property settings for the Job, as well as a JobDataMap, which can be used to store state information for a given instance of your job class.
Trigger objects are used to trigger the execution (or 'firing') of jobs. When you wish to schedule a job, you instantiate a trigger and 'tune' its properties to provide the scheduling you wish to have. There are currently two types of triggers, SimpleTrigger and CronTrigger.
SimpleTrigger is handy if you need 'one-shot' execution (just single execution of a job at a given moment in time), or if you need to fire a job at a given time, and have it repeat N times, with a delay of T between executions. CronTrigger is useful if you wish to have triggering based on calendar-like schedules - such as "every Friday, at noon" or "at 10:15 on the 10th day of every month."Why Jobs AND Triggers? Many job schedulers do not have separate notions of jobs and triggers. Some define a 'job' as simply an execution time (or schedule) along with some small job identifier. Others are much like the union of Quartz's job and trigger objects. While developing Quartz, we decided that it made sense to create a separation between the schedule and the work to be performed on that schedule. This has (in our opinion) many benefits.
For example, Jobs can be created and stored in the job scheduler independent of a trigger, and many triggers can be associated with the same job. Another benefit of this loose-coupling is the ability to configure jobs that remain in the scheduler after their associated triggers have expired, so that that it can be rescheduled later, without having to re-define it. It also allows you to modify or replace a trigger without having to re-define its associated job.
Identifiers
Jobs and Triggers are given identifying names as they are registered with the Quartz scheduler. Jobs and triggers can also be placed into 'groups' which can be useful for organizing your jobs and triggers into categories for later maintenance. The name of a job or trigger must be unique within its group - or in other words, the true identifier of a job or trigger is its name + group.
Now that you have a general idea about what Jobs and Triggers are, let's talk about them in some more detail.
More About Jobs & JobDetails
As you've seen, Jobs are rather easy to implement. There are just a few more things that you need to understand about the nature of jobs, about the execute(..) method of the Job interface, and about JobDetails.
While a class that you implement is the actual "job", Quartz needs to be informed about various attributes that you may wish the job to have. This is done via the JobDetail class, which was mentioned briefly in the previous section. Software 'archaeologists' may be interested to know that in an older incarnation of Quartz, the implementation of the functionality of JobDetail was imposed upon the implementor of each Job class by having all of JobDetail's 'getter' methods on the Job interface itself. This forced a cumbersome job of re-implementing virtually identical code on every Job class - which was really dumb... thus we created the JobDetail class.
Let's take a moment now to discuss a bit about the 'nature' of Jobs and the life-cycle of job instances within Quartz. First lets take a look back at some of that snippet of code we saw earlier:
JobDetail jobDetail = new JobDetail("myJob", // job name
sched.DEFAULT_GROUP, // job group
DumbJob.class); // the java class to execute
SimpleTrigger trigger = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
new Date(),
null,
0,
0L);
sched.scheduleJob(jobDetail, trigger);
Now consider the job class "DumbJob" defined as such:
public class DumbJob implements Job {
public DumbJob() {
}
public void execute(JobExecutionContext context)
throws JobExecutionException
{
System.err.println("DumbJob is executing.");
}
}
Notice that we 'feed' the scheduler a JobDetail instance, and that it refers to the job to be executed by simply providing the job's class. Each (and every) time the scheduler executes the job, it creates a new instance of the class before calling its execute(..) method. One of the ramifications of this behavior is the fact that jobs must have a no-arguement constructor. Another ramification is that it does not make sense to have data-members defined on the job class - as their values would be 'cleared' every time the job executes.
You may now be wanting to ask "how can I provide properties/configuration for a Job instance?" and "how can I keep track of a job's state between executions?" The answer to these questions are the same: the key is the JobDataMap, which is part of the JobDetail object.
The JobDataMap can be used to hold any number of (serializable) objects which you wish to have made available to the job instance when it executes. JobDataMap is an implementation of the Java Map interface, and has some added convenience methods for storing and retreiving data of primitive types.
Here's some quick snippets of putting data into the JobDataMap prior to adding the job to the scheduler:
jobDetail.getJobDataMap().put("jobSays", "Hello World!");
jobDetail.getJobDataMap().put("myFloatValue", 3.141f);
jobDetail.getJobDataMap().put("myStateData", new ArrayList());
Here's a quick example of getting data from the JobDataMap during the job's execution:
public class DumbJob implements Job {
public DumbJob() {
}
public void execute(JobExecutionContext context)
throws JobExecutionException
{
String instName = context.getJobDetail().getName();
String instGroup = context.getJobDetail().getGroup();
JobDataMap dataMap = context.getJobDetail().getJobDataMap();
String jobSays = dataMap.getString("jobSays");
float myFloatValue = dataMap.getFloat("myFloatValue");
ArrayList state = (ArrayList)dataMap.get("myStateData");
state.add(new Date());
System.err.println("Instance " + instName + " of DumbJob says: " + jobSays);
}
}
If you use a persistent JobStore (discussed in the JobStore section of this tutorial) you should use some care in deciding what you place in the JobDataMap, because the object in it will be serialized, and they therefore become prone to class-versioning problems. Obviously standard Java types should be very safe, but beyond that, anytime someone changes the definition of a class for which you have serialized instances, care has to be taken not to break compatibility. Further information on this topic can be found in this Java Developer Connection Tech Tip: Serialization In The Real World. Optionally, you can put JDBC-JobStore and JobDataMap into a mode where only primitives and strings can be stored in the map, thus eliminating any possibility of later serialization problems.
Stateful vs. Non-Stateful Jobs
Now, some additional notes about a job's state data (aka JobDataMap): A Job instance can be defined as "stateful" or "non-stateful". Non-stateful jobs only have their JobDataMap stored at the time they are added to the scheduler. This means that any changes made to the contents of the job data map during execution of the job will be lost, and will not seen by the job the next time it executes. You have probably guessed, a stateful job is just the opposite - its JobDataMap is re-stored after every execution of the job. One side-effect of making a job stateful is that it cannot be executed concurrently. Or in other words: if a job is stateful, and a trigger attempts to 'fire' the job while it is already executing, the trigger will block (wait) until the previous execution completes.
You 'mark' a Job as stateful by having it implement the StatefulJob interface, rather than the Job interface.
One final point on this topic that may or may not be obvious by now: You can create a single job class, and store many 'instances' of it within the scheduler by creating multiple instances of JobDetails - each with its own set of properties and JobDataMap - and adding them all to the scheduler.
Other Attributes Of Jobs
Here's a quick summary of the other properties which can be defined for a job instance via the JobDetail object:
- Durability - if a job is non-durable, it is automatically deleted from the scheduler once there are no longer any active triggers associated with it.
- Volatility - if a job is volatile, it is not persisted between re-starts of the Quartz scheduler.
- RequestsRecovery - if a job "requests recovery", and it is executing during the time of a 'hard shutdown' of the scheduler (i.e. the process it is running within crashes, or the machine is shut off), then it is re-executed when the scheduler is started again. In this case, the JobExecutionContext.isRecovering() method will return true.
- JobListeners - a job can have a set of zero or more JobListeners associated with it. When the job executes, the listeners are notified. More discussion on JobListeners can be found in the section of this document that is dedicated to the topic of TriggerListeners & JobListeners.
The Job.execute(..) Method
Finally, we need to inform you of a few details of the Job.execute(..) method. The only type of exception (including RuntimeExceptions) that you are allowed to throw from the execute method is the JobExecutionException. Because of this, you should generally wrap the entire contents of the execute method with a 'try-catch' block. You should also spend some time looking at the documentation for the JobExecutionException, as your job can use it to provide the scheduler various directives as to how you want the exception to be handled.
More About Triggers
Like jobs, triggers are relatively easy to work with, but do contain a variety of customizable options that you need to be aware of and understand before you can make full use of Quartz. Also, as noted earlier, there are different types of triggers, that you can select to meet different scheduling needs.
Calendars
Quartz Calendar objects (not java.util.Calendar objects) can be associated with triggers at the time the trigger is stored in the scheduler. Calendars are useful for excluding blocks of time from the the trigger's firing schedule. For instance, you could create a trigger that fires a job every weekday at 9:30 am, but then add a Calendar that excludes all of the business's holidays.
Calendar's can be any serializable objects that implement the Calendar interface, which looks like this:
package org.quartz;
public interface Calendar {
public boolean isTimeIncluded(long timeStamp);
public long getNextIncludedTime(long timeStamp);
}
Notice that the parameters to these methods are of the long type. As you may guess, they are timestamps in millisecond format. This means that calendars can 'block out' sections of time as narrow as a millisecond. Most likely, you'll be interested in 'blocking-out' entire days. As a convenience, Quartz includes the class org.quartz.impl.HolidayCalendar, which does just that.
Calendars must be instantiated and registered with the scheduler via the addCalendar(..) method. If you use HolidayCalendar, after instantiating it, you should use its addExcludedDate(Date date) method in order to populate it with the days you wish to have excluded from scheduling. The same calendar instance can be used with multiple triggers such as this:
HolidayCalendar cal = new HolidayCalendar();
cal.addExcludedDate( someDate );
sched.addCalendar("myHolidays", cal, false);
SimpleTrigger trigger = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
new Date(),
null,
SimpleTrigger.REPEAT_INDEFINITELY,
60L * 1000L);
trigger.setCalendarName("myHolidays");
// .. schedule job with trigger
SimpleTrigger trigger2 = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
new Date(),
null,
5,
5L * 24L * 60L * 60L * 1000L);
trigger2.setCalendarName("myHolidays");
// .. schedule job with trigger2
The details of the values passed in the SimpleTrigger constructors will be explained in the next section. For now, just believe that the code above creates two triggers: one that will repeat every 60 seconds forever, and one that will repeat five times with a five day interval between firings. However, any of the firings that would have occurred during the period excluded by the calendar will be skipped.
Misfire Instructions
Another important property of a Trigger is its "misfire instruction". A misfire occurs if a persistent trigger "misses" its firing time because of the scheduler being shutdown. The different trigger types have different misfire instructions available to them. By default they use a 'smart policy' instruction - which has dynamic behavior based on trigger type and configuration. When the scheduler starts, it searches for any persistent triggers that have misfired, and it then updates each of them based on their individually configured misfire instructions. When you start using Quartz in your own projects, you should make yourself familiar with the misfire instructions that are defined on the given trigger types, and explained in their JavaDOC. More specific information about misfire instructions will be given under each trigger type's section of this document. The misfire instruction for a given trigger instance can be configured using the setMisfireInstruction(..) method.
TriggerUtils
The TriggerUtils class (in the org.quartz.helpers package) contains conveniences to help you create triggers and dates without having to monkey around with java.util.Calendar objects. Use this class to easily make triggers that fire every minute, hour, day, week, month, etc. Also use this class to generate dates that are rounded to the nearest second, minute or hour - this can be very useful for setting trigger start-times.
TriggerListeners
Finally, triggers may have registered listeners, just as jobs may. Objects implementing the TriggerListener interface will receive notifications as a trigger is fired.
Now we'll spend a little time talking about the individual trigger types...
More About SimpleTrigger
SimpleTrigger should meet your needs if you need to have a job execute exactly once at a specific moment in time, or at a specific moment in time followed by repeats at a specific interval.
With this description, you may not find it surprising to find that the properties of a SimpleTrigger include: a start-time, and end-time, a repeat count, and a repeat interval. All of these properties are exactly what you'd expect them to be, with only a couple special notes related to the end-time property.
The repeat count can be zero, a positive integer, or the constant value SimpleTrigger.REPEAT_INDEFINITELY. The repeat interval property must be zero, or a positive long value, and represents a number of milliseconds. Note that a repeat interval of zero will cause 'repeat count' firings of the trigger to happen concurrently (or as close to concurrently as the scheduler can manage).
If you're not already familiar with the java.util.Calendar class, you may find it helpful for computing your trigger fire-times, depending on the start-time (or end-time) that you're trying to create.
The end-time property (if it is specified) over-rides the repeat count property. This can be useful if you wish to create a trigger such as one that fires every 10 seconds until a given moment in time -- rather than having to compute the number of times it would repeat between the start-time and the end-time, you can simply specify the end-time and then use a repeat count of REPEAT_INDEFINITELY (you could even specify a repeat count of some huge number that is sure to be more than the number of times the trigger will actually fire before the end-time arrives).
SimpleTrigger has a few different constructors, but we'll examine this one, and use it in the few examples that follow:
public SimpleTrigger(String name, String group, Date startTime,
Date endTime, int repeatCount, long repeatInterval)
SimpleTrigger Example 1 - Create a trigger that fires exactly once, ten seconds from now:
long startTime = System.currentTimeMillis() + 10000L;
SimpleTrigger trigger = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
new Date(startTime),
null,
0,
0L);
SimpleTrigger Example 2 - Create a trigger that fires immediately, then repeats every 60 seconds, forever:
SimpleTrigger trigger = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
new Date(),
null,
SimpleTrigger.REPEAT_INDEFINITELY,
60L * 1000L);
SimpleTrigger Example 3 - Create a trigger that fires immediately, then repeats every 10 seconds until 40 seconds from now:
long endTime = System.currentTimeMillis() + 40000L;
SimpleTrigger trigger = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
new Date(),
new Date(endTime),
SimpleTrigger.REPEAT_INDEFINITELY,
10L * 1000L);
SimpleTrigger Example 4 - Create a trigger that fires on March 17 of the year 2002 at precisely 10:30 am, and repeats 5 times (for a total of 6 firings) - with a 30 second delay between each firing:
java.util.Calendar cal = new java.util.GregorianCalendar(2002, cal.MARCH, 17);
cal.set(cal.HOUR, 10);
cal.set(cal.MINUTE, 30);
cal.set(cal.SECOND, 0);
cal.set(cal.MILLISECOND, 0);
Data startTime = cal.getTime()
SimpleTrigger trigger = new SimpleTrigger("myTrigger",
sched.DEFAULT_GROUP,
startTime,
null,
5,
30L * 1000L);
Spend some time looking at the other constructors (and property setters) available on SimpleTrigger, so that you can use the one most convenient to what you want to accomplish.
SimpleTrigger Misfire Instructions
SimpleTrigger has several instructions that can be used to inform Quartz what it should do when a misfire occurs. (Misfire situations were introduced in the More About Triggers section of this tutorial). These instructions are defined as constants on SimpleTrigger itself (including JavaDOC describing their behavior). The instructions include:
MISFIRE_INSTRUCTION_FIRE_NOW
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_EXISTING_REPEAT_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT
MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_EXISTING_COUNT
You should recall from the earlier discussion of mis-fire instructions that all triggers have the
Trigger.MISFIRE_INSTRUCTION_SMART_POLICYinstruction available for use, and this instruction is also the default for all trigger types.
If the 'smart policy' instruction is used, SimpleTrigger dynamically chooses between its various MISFIRE instructions, based on the configuration and state of the given SimpleTrigger instance. The JavaDOC for the SimpleTrigger.updateAfterMisfire() method explains the exact details of this dynamic behavior.
More About CronTrigger
CronTriggers are often more useful than SimpleTrigger, if you need a job-firing schedule that recurs based on calendar-like notions, rather than on the exactly specified intervals of SimpleTrigger.
With CronTrigger, you can specify firing-schedules such as "every Friday at noon", or "every weekday and 9:30 am", or even "every 5 minutes between 9:00 am and 10:00 am on every Monday, Wednesday and Friday".
Cron Expressions
Cron-Expressions are used to configure instances of CronTrigger. Cron-Expressions are strings that are actually made up of six sub-expressions, that describe individual details of the schedule. These sub-expression are separated with white-space, and represent:
- Seconds
- Minutes
- Hours
- Day-of-Month
- Month
- Day-of-Week
An example of a complete cron-expression is the string
"0 0 12 ? * WED"
- which means "every Wednesday at
12:00 pm".
Individual sub-expressions can contain ranges and/or lists. For example, the day of week field in the previous (which reads "WED") example could be replaces with "MON-FRI", "MON, WED, FRI", or even "MON-WED,SAT".
Wild-cards (the '*'
character) can be used to say
"every" possible value of this field. Therefore the '*'
character in the "Month" field of the previous example simply means
"every month". A '*'
in the Day-Of-Week field would
obviously mean "every day of the week".
All of the fields have a set of valid values that can be specified. These
values should be fairly obvious - such as the numbers 0 to 59 for seconds and
minutes, and the values 0 to 23 for hours. Day-of-Month can be any value 0-31,
but you need to be careful about how many days are in a given month! Months
can be specified as values between 0 and 11, or by using the strings
JAN, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, OCT, NOV and
DEC.
Days-of-Week can be specified as vaules between 1 and 7
(1 = Sunday) or by using the strings SUN, MON, TUE, WED,
THU, FRI and SAT
.
The '/'
character can be used to specify
increments to values. For example, if you put '0/15'
in the Minutes field, it means 'every 15 minutes, starting at minute zero'. If
you used '3/20'
in the Minutes field, it would mean
'every 20 minutes during the hour, starting at minute three' - or in other words
it is the same as specifying '3,23,43'
in the
Minutes field.
The '?'
character is allowed for the day-of-month
and day-of-week fields. It is used to specify "no specific value". This is
useful when you need to specify something in one of the two fields, but not the
other. See the examples below (and CronTrigger JavaDOC) for clarification.
The 'L'
character is allowed for the day-of-month
and day-of-week fields. This character is short-hand for "last", but it has
different meaning in each of the two fields. For example, the value "L" in the
day-of-month field means "the last day of the month" - day 31 for January,
day 28 for February on non-leap years. If used in the day-of-week field by
itself, it simply means "7" or "SAT". But if used in the day-of-week field
after another value, it means "the last xxx day of the month" - for example
"6L" or "FRIL" both mean "the last friday of the month". When using the 'L'
option, it is important not to specify lists, or ranges of values, as you'll
get confusing results.
Here are a few more examples of expressions and their meanings - you can find even more in the JavaDOC for CronTrigger
CronTrigger Example 1 - an expression to create a trigger that simply fires every 5 minutes
"0 0/5 * * * ?"
CronTrigger Example 2 - an expression to create a trigger that fires every 5 minutes, at 10 seconds after the minute (i.e. 10:00:10 am, 10:05:10 am, etc.).
"10 0/5 * * * ?"
CronTrigger Example 3 - an expression to create a trigger that fires at 10:30, 11:30, 12:30, and 13:30, on every Wednesday and Friday.
"0 30 10-13 ? * WED,FRI"
CronTrigger Example 4 - an expression to create a trigger that fires every half hour between the hours of 8 am and 10 am on the 5th and 20th of every month. Note that the trigger will NOT fire at 10:00 am, just at 8:00, 8:30, 9:00 and 9:30
"0 0/30 8-9 5,20 * ?"
Note that some scheduling requirements are too complicated to express with a single trigger - such as "every 5 minutes between 9:00 am and 10:00 am, and every 20 minutes between 1:00 pm and 10:00 pm". The solution in this scenario is to simply create two triggers, and register both of them to run the same job.
TriggerListeners and JobListeners
Listeners are objects that you create to perform actions based on events occuring within the scheduler. As you can probably guess, TriggerListeners receive events related to triggers, and JobListeners receive events related to jobs.
Trigger-related events include: trigger firings, trigger mis-firings (discussed in the "Triggers" section of this document), and trigger completions (the jobs fired off by the trigger is finished).
Job-related events include: a notification that the job is about to be executed, and a notification when the job has completed execution.
To create a listener, simply create an object the implements either the org.quartz.TriggerListener and/or org.quartz.JobListener interface. Listeners are then registered with the scheduler during run time, and must be given a name (or rather, they must advertise their own name via their getName() method. Listeners can be registered as either "global" or "non-global". Global listeners receive events for ALL triggers/jobs, and non-global listeners receive events only for the specific triggers/jobs that explicitely name the listener in their getTriggerListenerNames() or getJobListenerNames() properties.
scheduler.addGlobalJobListener(myJobListener);or
scheduler.addJobListener(myJobListener);
Listeners are not used by most users of Quartz, but are handy when application requirements create the need for the notification of events, without the Job itself explicitly notifying the application.
SchedulerListeners
SchedulerListeners are much like TriggerListeners and JobListeners, except they receive notification of events within the Scheduler itself - not necessarily events related to a specific trigger or job.
Scheduler-related events include: the addition of a job/trigger, the removal of a job/trigger, a serious error within the scheduler, notification of the scheduler being shutdown, and others.
SchedulerListeners are created and registered in much the same way as the other listener types, except there is no distinction between global and non-global listeners. SchedulerListeners can be virtually any object that implements the org.quartz.SchedulerListener interface.
JobStores
JobStore's are responsible for keeping track of all the "work data" that you give to the scheduler: jobs, triggers, calendars, etc. Selecting the appropriate JobStore for your Quartz scheduler instance is an important step. Luckily, the choice should be a very easy one once you understand the differences between them. You declare which JobStore your scheduler should use (and it's configuration settings) in the properties file (or object) that you provide to the SchedulerFactory that you use to produce your scheduler instance.
Note: Never use a JobStore instance directly in your code. For some reason many people attempt to do this. The JobStore is for behind-the-scenes use of Quartz itself. You have to tell Quartz (through configuration) which JobStore to use, but then you should only work with the Scheduler interface in your code.
RAMJobStore
RAMJobStore is the simplest JobStore to use, it is also the most performant (in terms of CPU time). RAMJobStore gets its name in the obvious way: it keeps all of its data in RAM. This is why it's lightning-fast, and also why it's so simple to configure. The drawback is that when your application ends (or crashes) all of the scheduling information is lost - this means RAMJobStore cannot honor the setting of "non-volatility" on jobs and triggers. For some applications this is acceptable - or even the desired behavior, but for other applications, this may be disasterous.
To use RAMJobStore (and assuming you're using StdSchedulerFactory) simply specify the class name org.quartz.simpl.RAMJobStore as the JobStore class property that you use to configure quartz:
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStoreThere are no other settings you need to worry about.
JDBCJobStore
JDBCJobStore is also aptly named - it keeps all of its data in a database via JDBC. Because of this it is a bit more complicated to configure than RAMJobStore, and it also is not as fast. However, the performance draw-back is not terribly bad, especially if you build the database tables with indexes on the primary keys. On a 800MHz Windoz box running a Quartz application utilizing Oracle running on a not-so-new Solaris box, the time to retreive and update a firing trigger and its associated job has been measured at about 15 milliseconds.
JDBCJobStore works with nearly any database, it has been used widely with Oracle, MySQL, MS SQLServer2000, HSQLDB, PostreSQL and DB2. To use JDBCJobStore, you must first create a set of database tables for Quartz to use. You can find table-creation SQL scripts in the "docs/dbTables" directory of the Quartz distribution. If there is not already a script for your database type, just look at one of the existing ones, and modify it in any way necessary for your DB. One thing to note is that in these scripts, all the the tables start with the prefix "QRTZ_" (such as the tables "QRTZ_TRIGGERS", and "QRTZ_JOB_DETAIL"). This prefix can actually be anything you'd like, as long as you inform JDBCJobStore what the prefix is (in your Quartz properties). Using different prefixes may be useful for creating multiple sets of tables, for multiple scheduler instances, within the same database.
Once you've got the tables created, you have one more major decision to make before configuring and firing up JDBCJobStore. You need to decide what type of transactions your application needs. If you don't need to tie your scheduling commands (such as adding and removing triggers) to other transactions, then you can let Quartz manage the transaction by using JobStoreTX as your JobStore (this is the most common selection).
If you need Quartz to work along with other transactions (i.e. within a J2EE application server), then you should use JobStoreCMT - in which case Quartz will let the app server container manage the transactions.
The last piece of the puzzle is setting up a DataSource from which JDBCJobStore can get connections to your database. DataSources are defined in your Quartz properties using one of a few different approaches. One approach is to have Quartz create and manage the DataSource itself - by providing all of the connection information for the database. Another approach is to have Quartz use a DataSource that is managed by an application server that Quartz is running inside of - by providing JDBCJobStore the JNDI name of the DataSource. For details on the properties, consult the example config files in the "docs/config" folder.
To use JDBCJobStore (and assuming you're using StdSchedulerFactory) you first need to set the JobStore class property of your Quartz configuration to be either org.quartz.impl.jdbcjobstore.JobStoreTX or org.quartz.impl.jdbcjobstore.JobStoreCMT - depending on the selection you made based on the explanations in the above few paragraphs.
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
Next, you need to select a DriverDelegate for the JobStore to use. The DriverDelegate is responsible for doing any JDBC work that may be needed for your specific database. StdJDBCDelegate is a delegate that uses "vanilla" JDBC code (and SQL statements) to do its work. If there isn't another delegate made specifically for your database, try using this delgate - we've only made database-specific delegates for databases that we've found problems using StdJDBCDelegate with (which seems to be most ;-). Other delegates can be found in the "org.quartz.impl.jdbcjobstore" package, or in its sub-packages. Other delegates include DB2v6Delegate (for DB2 version 6 and earlier), HSQLDBDelegate (for HSQLDB), MSSQLDelegate (for microsoft SQLServer 2000), PostgreSQLDelegate (for PostgreSQL 7.x), WeblogicDelegate (for using JDBC drivers made by Weblogic), and OracleDelegate (for using Oracle 8i and 9i).
Once you've selected your delegate, set its class name as the delegate for JDBCJobStore to use.
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
Next, you need to inform the JobStore what table prefix (discussed above) you are using.
org.quartz.jobStore.tablePrefix = QRTZ_
And finally, you need to set which DataSource should be used by the JobStore. The named DataSource must also be defined in your Quartz properties. In this case, we're specifying that Quartz should use the DataSource name "myDS" (that is defined elsewhere in the configuration properties).
org.quartz.jobStore.dataSource = myDS
Note: If your Scheduler is very busy (i.e. nearly always executing the same number of jobs as the size of the thread pool, then you should probably set the number of connections in the DataSource to be the about the size of the thread pool + 1.
Note: The "org.quartz.jobStore.useProperties" config parameter can be set to "true" (defaults to false) in order to instruct JDBCJobStore that all values in JobDataMaps will be Strings, and therefore can be stored as name-value pairs, rather than storing more complex objects in their serialized form in the BLOB column. This is much safer in the long term, as you avoid the class versioning issues that there are with serializing your non-String classes into a BLOB.
Configuration, Resource Usage and StdSchedulerFactory
Quartz is architected in modular way, and therefore to get it running, several components need to be "snapped" together. Fortunately, some helpers exist for making this happen.
Components that need to be configured before Quartz can do its thing:
- ThreadPool
- JobStore
- DataSources (if necessary)
- The Scheduler itself
The ThreadPool provides a set of Threads for Quartz to use when executing Jobs. The more threads in the pool, the greater number of Jobs that can run concurrently. However, too many threads may bog-down your system. Most Quartz users find that 5 or so threads are plenty- because they have fewer than 100 jobs at any given time, the jobs are not generally scheduled to run at the same time, and the jobs are short-lived (complete quickly). Other users find that they need 10, 15, 50 or even 100 threads - because they have tens-of-thousands of triggers with various schedules - which end up having an average of between 10 and 100 jobs trying to execute at any given moment. Finding the right size for your scheduler's pool is completely dependent on what you're using the scheduler for. There are no real rules, other than to keep the number of threads as small as possible (for the sake of your machine's resources) - but make sure you have enough for your Jobs to fire on time. Note that if a trigger's time to fire arrives, and there isn't an available thread, Quartz will block (pause) until a thread comes available, then the Job will execute - some number of milliseconds later than it should have. This may even cause the tread to misfire - if there is no available thread for the duration of the scheduler's configured "misfire threshold".
A ThreadPool interface is defined in the org.quartz.spi package, and you can create a ThreadPool implementation in any way you like. Quartz ships with a simple (but very satisfactory) thread pool named org.quartz.simpl.SimpleThreadPool. This ThreadPool simply maintains a fixed set of threads in its pool - never grows, never shrinks. But it is otherwise quite robust and is very well tested - as nearly everyone using Quartz uses this pool.
The JobStore and DataSrouces were discussed in the "JobStores" section of this document. Worth noting here, is the fact that all JobStores implement the org.quartz.spi.JobStore interface - and that if one of the bundled JobStores does not fit your needs, then you can make your own.
Finally, you need to create your Scheduler instance. The Scheduler itself needs to be given a name, told its RMI settings, and handed instances of a JobStore and ThreadPool. The RMI settings include whether the Scheduler should create itself as an RMI server object (make itself available to remote connections), what host and port to use, etc.. StdSchedulerFactory (discussed below) can also produce Scheduler instances that are actually proxies (RMI stubs) to Schedulers created in remote processes.
StdSchedulerFactory
StdSchedulerFactory is an implementation of the org.quartz.SchedulerFactory interface. It uses a set of properties (java.util.Properties) to create and initialize a Quartz Scheduler. The properties are generally stored in and loaded from a file, but can also be created by your program and handed directly to the factory. Simply calling getScheduler() on the factory will produce the scheduler, initialize it (and its ThreadPool, JobStore and DataSources), and return a handle to its public interface.
There are some sample configurations (including full descriptions of the properties) in the "docs/config" directory of the Quartz distribution.
DirectSchedulerFactory
DirectSchedulerFactory is another SchedulerFactory implementation. It is useful to those wishing to create their Scheduler instance in a more programatic way. Its use is generally discouraged for the following reasons: (1) it requires the user to have a greater understanding of what they're doing, and (2) it does not allow for declaritive configuration - or in other words, you end up hard-coding all of the scheduler's settings.
Logging
Quartz uses the org.apache.commons.logging framework for all of its logging needs. Quartz does not produce much logging information - generally just some information during initialization, and then only messages about serious problems while Jobs are executing. In order to "tune" the logging settings (such as the amount of output, and where the output goes), you need to understand the Jakarta Commons Logging framework, which is beyond the scope of this document.
Advanced (Enterprise) Features
Clustering
Clustering currently only works with the JDBC-Jobstore (JobStoreTX or JobStoreCMT). Features include job fail-over (if the JobDetail's "request recovery" flag is set to true) and load-balancing.
Enable clustering by setting the "org.quartz.jobStore.isClustered" property to "true". Each instance in the cluster should use the same copy of the quartz.properties file. Exceptions of this would be to use properties files that are identical, with the following allowable exceptions: Different thread pool size, and different value for the "org.quartz.scheduler.instanceId" property. Each node in the cluster MUST have a unique instanceId, which is easily done (without needing different properties files) by placing "AUTO" as the value of this property.
Special Notes:
- Never run clustering on separate machines, unless their clocks are synchronized using some form of time-sync service (daemon) that runs very regularly (the clocks must be within a second of each other). See http://www.boulder.nist.gov/timefreq/service/its.htm if you are unfamiliar with how to do this.
- Never fire-up a non-clustered instance against the same set of tables that any other instance is running against. You will get serious data corruption, and eratic behavior.
JTA Transactions
As explained in the "JobStores" section of this document, JobStoreCMT allows Quartz scheduling operations to be performed within larger JTA transactions.
Jobs can also execute within a JTA transaction (UserTransaction) by setting the "org.quartz.scheduler.wrapJobExecutionInUserTransaction" property to "true". With this option set, a a JTA transaction will begin() just before the Job's execute method is called, and commit() just after the call to execute terminates.
Miscellaneous Features
Plug-Ins
Quartz provides an interface (org.quartz.spi.SchedulerPlugin) for plugging-in additional functionality.
Plugins that ship with Quartz to provide various utililty capabilities can be found documented in the org.quartz.plugins package.
Jobs
Quartz also provides a number of utility Jobs that you can use in your application for doing things like sending e-mails and invoking EJBs. These out-of-the-box Jobs can be found documented in the org.quartz.jobs package.
Summary
By now you should have a fairly good understanding of what Quartz is and what you can do with it. If you're still rather confused, please contact the project developer's via the project development site and let us know how this document can be improved.
What Next?
If you haven't already looked at the example programs in the 'examples/' directory of the distribution, you'll probably want to do that now. After you've done that, start playing around with Quartz yourself and read the JavaDOC of the main 'client-side' components - which are found in the "org.quartz" package.
PLEASE! - feel free to contribute to Quartz in any way that interests you. Your feedback is always welcome - many of the current features and styles of Quartz usage have come directly from users. There's always plenty of development work to be done, contact the project leaders if you are interested.