Quartz Job Scheduling Framework Building Open Source Enterprise Applications
Quartz Job Scheduling Framework Building Open Source Enterprise Applications
If your Java applications depend on tasks that must be performed at specific times or if your systems have
recurring maintenance jobs that could be automated, then you need Quartz: the first full-featured, open
source job scheduling framework.
Quartz Job Scheduling Framework reveals how to make the most of Quartz with virtually any Java EE or
Java SE application, from the smallest standalone program to the largest e-commerce application.
Best-selling author Chuck Cavaness shows developers and architects how to integrate Quartz with leading
open source Java frameworks, including Hibernate and Struts.
Using practical examples, Cavaness illuminates everything from basic job scheduling to the use of Quartz in
clustered environments and enterprise workflow applications. To jumpstart your own Quartz projects, he
also presents a full chapter of "cookbook" sample code. Coverage includes
• Using Listeners to receive callbacks from Quartz when key events occur
1
2
Copyright
Foreword
Preface
Acknowledgments
About the Author
Chapter 1.
Scheduling in the
Enterprise
What Is Job
Scheduling?
Why job
Scheduling Is
Important
Uses for Job
Schedulers in the
Enterprise
Uses for Job
Schedulers in
NonEnterprise
Job scheduling
Versus Workflow
What about
Alternative
Solutions?
Chapter 2. Getting
Started with Quartz
History of the
Quartz Framework
Downloading and
Installing Quartz
Building Quartz
from Source
Getting Help from
the Quartz
Community
Who's Using
Quartz?
2
3
Chapter 3. Hello,
Quartz
The "Hello,
World" Quartz
Project
Scheduling the
Quartz
ScanDirectoryJob
Scheduling a
Quartz Job
Declaratively
Packaging the
Quartz
Application
Chapter 4.
Scheduling Jobs
The Quartz
Scheduler
The Quartz
SchedulerFactory
Managing the
Scheduler
Managing Jobs
Volatility,
Durability, and
Recoverability
Quick Java Thread
Overview
Thread Usage in
Quartz
Understanding
Quartz Triggers
Chapter 5. Cron
Triggers and More
Quick Lesson in
Cron
Using the Quartz
CronTrigger
The Cron
Expression Format
Using Start and
End Dates with
CronTrigger
Using TriggerUtils
with the
CronTrigger
Using
CronTriggers in
the
JobInitializationPlugin
Cron Expressions
Cookbook
Creating a
Fire-Now Trigger
3
4
Chapter 6. JobStores
and Persistence
Job Storage
Job Storage in
Quartz
Using Memory to
Store Scheduler
Information
Using Persistent
JobStores
Using the
Database for Job
Storage
Creating the
Quartz Database
Structure
Using JobStoreTX
Configuring a
DataSource for
JobStoreTX
Running Quartz
with JobStoreTX
Using Memory to
Store Scheduler
Information
Using the
JobStoreCMT
Configuring
Datasources for
JobStoreCMT
Improving
Performance with
Persistent
JobStores
Creating New
JobStores
Chapter 7.
Implementing Quartz
Listeners
Listeners as
Extension Points
Implementing a
Listener
Listening for Job
Events
Listening for
Trigger Events
Listening for
Scheduler Events
Using the
FileScanListener
Implementing
Listeners in the
quartz_jobs.xml
4
5
File
Thread Use in
Listeners
Uses of the Quartz
Listeners
Chapter 8. Using
Quartz Plug-Ins
What Is a Plug-In?
Creating a Quartz
Plug-In
Registering Your
Plug-Ins
Using Multiple
Plug-Ins
Quartz Utility
Plug-Ins
Chapter 9. Using
Quartz Remotely
Why RMI with
Quartz?
Brief Overview of
Java RMI
Requirements of
RMI
Creating a Quartz
RMI Server
Using the RMI
Registry
Creating the RMI
Client
Testing the RMI
Server and Client
Chapter 10. Using
Quartz with J2EE
If I Have J2EE,
Why Do I Need
Quartz?
Using the J2EE
Container's
DataSource
Using Other J2EE
Resources
The EJB 2.1
Specification:
Finally Some
Light
Chapter 11.
Clustering Quartz
What Does
Clustering Mean
to Quartz?
How Clustering
Works in Quartz
Configuring
5
6
Quartz to Use
Clustering
Running the
Quartz Cluster
Nodes
Quartz Clustering
Cookbook
Chapter 12. Quartz
Cookbook
Working with the
Scheduler
Working with Jobs
Chapter 13. Quartz
and Web
Applications
Using Quartz
Within a Web
Application
Integrating Quartz
Using Quartz with
the Struts
Framework
The
QuartzInitializerServlet
to the Rescue
Using a
ServletContextListener
Introducing the
Quartz Web
Application
Chapter 14. Using
Quartz with
Workflow
What Is
Workflow?
Job Chaining in
Quartz
Quick Introduction
to OSWorkflow
Integration of
Quartz with
OSWorkflow
Creating a
Workflow Job
Conclusion
Appendix A. Quartz
Configuration
Reference
The Main Quartz
Properties
Configuring the
Quartz ThreadPool
Configuring
Quartz Listeners
6
7
Configuring a
TriggerListener
Configuring
Quartz Plug-Ins
Configuring
Quartz RMI
Settings
Configuring
JobStore Settings
Configuring the
JobStoreTX
JobStore
Configuring
JobStoreCMT
Configuring
Quartz
Datasources
Configuring a
Datasource Using
a Custom
ConnectionProvider
Index
Copyright
Many of the designations used by manufacturers and sellers to distinguish their products are claimed as
trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim,
the designations have been printed with initial capital letters or in all capitals.
The author and publisher have taken care in the preparation of this book, but make no expressed or implied
warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for
incidental or consequential damages in connection with or arising out of the use of the information or
programs contained herein.
The publisher offers excellent discounts on this book when ordered in quantity for bulk purchases or special
sales, which may include electronic versions and/or custom covers and content particular to your business,
training goals, marketing focus, and branding interests. For more information, please contact:
International Sales
international@pearsoned.com
7
8
Cavaness, Chuck.
Quartz job scheduling framework : building open source enterprise
applications / Chuck Cavaness.
p. cm.
1. Quartz (Electronic resource) 2. Production scheduling. 3. Open
source software. I. Title.
TS157.5.C38 2006
658.5'3dc22
2006007504
All rights reserved. Printed in the United States of America. This publication is protected by copyright, and
permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval
system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or
likewise. For information regarding permissions, write to:
Text printed in the United States on recycled paper at R.R. Donnelley in Crawfordsville, Indiana.
Dedication
This book is dedicated to the Quartz user community. I hope you find both the project and
this book helpful in your work life. Quartz is a great tool and can make your job easier. Learn
itlive itlove it. James, you should be proud of what you created.
Chuck Cavaness
Foreword
The event-driven programming paradigm has been popular for a number of decades, and although the term is
not among the current industry buzzwords, it is one of the most popular and useful ways in which a software
system can be built. Operating systems, word processors, Web applications, and games are all event driven:
The software waits until it receives notification of some event (such as a system interrupt, key stroke, or
mouse click) and then changes the control of program flow based on some interpretation of the event.
Quartz enables programs to take advantage of the event-driven paradigm without any human interacting with
the system. More specifically, it lets applications define events that should occur at certain trigger points in
time. Running a small subprogram (or job) based upon a date in time is commonly called job scheduling.
8
9
Job scheduling is found in nearly every class of software application and service, from large e-commerce
systems, to simple stand-alone GUI applications, to system-maintenance and monitoring tools, to even
gamesvirtually anywhere the event-driven paradigm is useful.
Indeed, before building Quartz, I found myself continually working on projects that needed scheduling
functionality. The Java Platform provided by Sun Microsystems offered reasonably good support for
threading but provided little else to cause events to occur within a system at certain times or on regular
intervals. Because of this, I found myself frequently creating components to provide rudimentary scheduling
capabilities. Some of the projects I worked on during the "dot-com" heydays enabled me to see some of the
scheduling functionality provided by various higher-level platforms, such as EAI tools, manufacturing
process-control systems, and even a few of the first J2EE application servers that included some basic
scheduling features. And, of course, I had been familiar with UNIX cron for a number of years.
Although scheduling wasn't the focus of any of my projects (I concentrated on a service needed by each), my
mind began to take interest in the problem of scheduling, and I found myself spending time thinking of
various designs for a Java-based scheduling service. I created a few proofs-of-concept and then, having seen
that that the open source community at that time had no offering in the way of a scheduler, in 2001 I
registered a project with the name of Quartz on SourceForge. I decided it was my turn to pay back the
community for other people's great work that I had been benefiting from freely.
I was quickly surprised by the number of people interested in using Quartz, as well as how many other open
source projects were including Quartz in their distribution. But most of all, I began to see that Quartz was
destined to become far more feature rich than I had ever planned it to be. Although it had been designed with
a fair amount of extensibility and customization in mind, several more hooks for providing plug-ins and the
like were addedand growing pains have become a bit evident in the code base. One of the most major changes
for the Quartz project came in 2004 when it joined the OpenSymphony family of projects and moved its
source code repository from SourceForge to java.net.
The community around Quartz has grown well, and I owe many thanks to dozens of people who have
contributed in the user forums, mailing lists, and, of course, the source code itself. I now know firsthand that
no open source project can be successful without a contributing user base. As you learn to use Quartz, please
consider contributing back to the open source community because you yourself are benefiting from the work
of others. Or, in the words of Benjamin Franklin, "Live usefully."
I have found job scheduling to be a field that is simple in concept but very challenging in certain areas of
implementation. Many ways exist for integrating and making use of Quartz with your stand-alone and J2EE
applications. I am regularly surprised by hearing about some way in which someone has put job scheduling to
work. I hope that this book will aid you in not only learning how to use Quartz, but also identifying the ways
in which you can put the(schedule-based) event-driven paradigm to use in the applications you build.
James House
Preface
I first came across Quartz in 2004 when I was looking for a job scheduler while designing a medical
claims-processing engine. I was initially skeptical about the framework because it seemed to so exactly fit
what I was looking for. Very quickly that skepticism turned into kidlike joy as I realized what I had stumbled
upon.
During the course of writing other books and articles and while doing the presentation circuit at various Java
User's Groups across the country, I had not once come across the Quartz framework. I probably had looked at
9
10
and researched hundredspossibly even thousandsof other open source frameworks. I was just dumbfounded by
how this framework could have escaped my attention and the detection of the user community.
Many things about the framework remind me of the early days with Apache Struts, including some of the
internal design approaches that the creator of Quartz chose, as well as the way in which the user community
has grown as word has spread. Although it's true that the number of users who need job scheduling will likely
never be as high as those who need a Web framework, Quartz offers much to be excited about.
Acknowledgments
It's important that I thank many people for helping me get this book finished. The book started fast and was
easy to write, but as most book projects (and most things in life) go, it was hard to keep the motivation all the
way through to the end.
I need to thank the editors for having so much patience with me. To the technical editors, James House and
Doug Barth, I appreciate your time and feedback to make the book as good as it can be. To Greg and Mary
Kate, I know that I was difficult to work with and worked more slowly than you wanted, but I do appreciate
the understanding and calmness you always presented to me.
It would be criminal to not personally thank James House for the immense amount of time that he has given to
the Quartz project and to the user community. Many of us think about creating stuff like Quartz, but people
like James make it happen and make it possible for people like me to write these books. Without him, none of
this would be possible. I hope that life rewards him tenfold.
Next, I would like to acknowledge the team of Quartz users and developers that provided advice and feedback
on the book's content and direction. Your help was very much appreciated. I only hope I've done justice to
your input and given something back to the community.
I would be remiss if I didn't thank the OpenSymphony folks as well. They are doing a great service to users
like me, and much of it goes unnoticed. Thanks for the help from you during this project.
And finally, I would be kicked out of my house and left homeless if I didn't thank my wonderful wife of 16
years and our three boys, Josuha, Benjamin, and Zachary. What a sacrifice they made to provide me with the
time that I needed to complete this. I love all of you and thank you for indulging my selfishness during this
time.
Chuck is a published author of five books on popular technologies, including books on Java J2EE and many
open source technologies such as Apache Struts. He has also written for JavaWorld, O'Reilly, OnJava, and
InformIt.com and has taught courses on programming and software development at Georgia Institute of
Technology.
10
11
Because not every job demands immediate execution, jobs can be scheduled to be executed in the future. For
example, a system administrator might have a job list that he follows every night:
Job scheduling is often referred to as running batch jobs or batch processing. These batch jobs typically run in
the background and normally don't involve much user interaction. The variety of tasks that are now
considered as potential batch jobs has grown significantly: It's not uncommon for larger organizations to run
hundreds of jobs per hour, every hour of every day. And as the size and complexity of organizations continues
to grow, so does the demand for batched jobs and a job scheduler.
11
12
Figure 1.1. The bigger, more complex, and more frequent the task is, the more value can be gained from
automation.
Humans are, well, human. We make mistakes far more frequently than computers do. By automating a series
of tasks into a job and then creating a schedule for when that job should be executed, we can eliminate much
of the chance for errors by simply removing humans from the equation.
Another advantage of job scheduling is scalability. We might be able to manually accommodate 10 or 20 jobs
per hour, but the higher we go in job count, the harder it is to keep up without introducing errors in the jobs.
With job scheduling, we are limited only by hardware resources.
So we can safely say that automation through job scheduling offers at least these three advantages over a
similar manual approach:
• Resource efficiency
• Fewer errors
• More scalability
Scenario "docText">Many web sites (both commercial and otherwise) allow users to register
an account that includes a username and password. For security reasons, it's a good idea to
expire the user's passwords regularlysay, every 90 days. You might create a job that runs
every night at midnight and sends an e-mail to a list of users when their passwords will
expire in three days. This is great use of a job scheduler. Figure 1.2 illustrates the password
reminder job.
12
13
Figure 1.2. The password expiration job sends e-mails nightly to remind users that their passwords will expire
soon.
Besides expiring passwords, Web sites can send other alerts and reminders(not to mention spam). A job
scheduler can be used for those in a similar way.
Many businesses need to integrate information with their vendors and clients. One method of performing this
integration is to exchange data files. Businesses can use real-time methods such as SOAP, but many don't
need to operate in real time and instead choose asynchronous methods such as File Transfer Protocol (ftp) to
drop and pick up files.
Picture a worker's compensation company that receives files every morning containing patient/accident
information. This company could hire someone to manually check the ftp site every morning for the file.
Alternatively, someone could write a job that executes every morning to scan the ftp site and insert the files
into the patient database. With a scheduled job, the employee no longer has to perform the manual checks and
can do something more valuable for the company. Figure 1.3 illustrates the file-transfer operation.
Figure 1.3. The filetransfer job checks the ftp site for patient files to process to the database.
Companies are driven on profit and loss, and it's important to have the latest revenue and margin data for
executives and the finance team to analyze. Pulling the data for sales reports can be very slow and resource
intensive because it usually involves searching thousands of records and joining many database tables. A
better approach involves running a job every night after the billing and invoicing is updated to create
temporary tables or views that then can be used to run reports against. Creating temp tables or views makes
the reports more dynamic, and users won't get frustrated while waiting for a report to generate. Some
reporting packages, such as Crystal Reports XI, include a job scheduler (see Figure 1.4).
Figure 1.4. The sales data report is executed to generate revenue and margin information for the sales team.
13
14
As another example, you might just want to query a database and send e-mails to mail recipients based on the
data.
This job can take advantage of a workflow in which each part of the job is a step in the workflow. This doesn't
mean that job scheduling without a workflow component is bad. It's just a very common next step. As long as
the jobscheduling framework can integrate with a thirdparty workflow solution as Quartz easily does, all is
good. More on Quartz and workflow comes in Chapter 14, "Using Quartz with Workflow."
14
15
selecting the execution time, storing job information in various places, and
using hooks for customization, just to name a few. Two classes in the Java
SDK don't make a job scheduler: The Java Timer has no way to organize jobs
and triggers, uses one thread per task instead of a pool of threads, and has
several other deficiencies that keep it from performing as a full job scheduler.
HomeGrown solution
As with the Timer and TimerTask classes just mentioned, it's easy to
underestimate the effort involved in creating a flexible and featurerich job
scheduler. Creating a job scheduler is not a trivial task, either. It requires
expertise in not only Java threads, but many other complicated topics as well.
Job scheduling is not something to build from scratch if it's not your specialty.
Commercial Solutions
Quite a few commercial job schedulers are available on the market. We make
no attempt in this book to review or evaluate these commercial solutions. Table
1.1 lists several of the more popular solutions and URLs where you can get
more information.
Quartz was created by James House, who envisioned the first conceptual pieces of the framework in 1998.
These included the concept of a queue of jobs and a pool of threads to process the jobs, although probably in
an unrecognizable form by most of today's Quartz users.
15
16
Over the next several years, House noted that he kept running into the same requirement: the need for a
flexible job-scheduling tool. His search for a cheap but feature-rich job-scheduling framework for Java led
him to several alternatives:
House's limited choices and interest in the problem prompted him to create an open source project for job
scheduling. In spring 2001, he created the project and registered it on SourceForge. The site is still viewable
today at http://sourceforge.net/projects/quartz, but it is no longer maintained there.
Since Quartz first was developed, numerous contributors and developers have added to the project. It's safe to
say, however, that the framework exists as it does today thanks to House and his interest in the job scheduling
problem. His determination to solve the problem in the public arena is commendable.
The download file is in a ZIP format, so you need a zip utility such as WinZip. You
can also use Java's jar command to unzip the distribution file:
The contents of the Quartz distribution are extracted to the current directory.
The extracted Quartz zip file contains several subdirectories. Table 2.1 describes
the directories that are part of the Quartz download.
16
17
Third-party libraries used
by the framework
src/java/org/quartz Source code for the
client-side (public) API of
the scheduler
src/java/org/quartz/core Source code for the server
(private) API of the
scheduler
src/java/org/quartz/simpl Simple implementations
provided by the
framework that have no
dependencies on
third-party products
src/java/org/quartz/impl Implementations of
support modules that
might have dependencies
on third-party products
src/java/org/quartz/utils Helper and utility
components used
throughout the framework
src/jboss Quartz features that are
specific for use with Jboss
src/oracle Quartz features that are
specific for use with
Oracle
src/weblogic Quartz features that are
specific for use with
WebLogic
If you are in a hurry to get Quartz working, the fastest way is to grab the prebuilt Quartz JAR, located in the
root directory of the download, and add it to the classpath of your application. You also need to grab the
dependent JARs Quartz requires. Table 2.2 lists the JAR files that are required for a basic Quartz installation.
Name
Location
Notes
Commons BeanUtils
<quartz-download>/ lib/optional
Commons Collections
<quartz-download>/ lib/cor
17
18
Yes.
Commons Digester
<quartz-download>/ lib/optional
Commons Logging
<quartz-download>/ lib/core
Yes.
As with the Quartz JAR, you should also put the dependent JARs in the classpath for your application.
Quartz includes a configuration file called quartz.properties that enables you to configure many aspects of
Quartz. A default quartz.properties file exists within the Quartz JAR, but if you need to modify any of the
defaults, you must include a copy of this file in your classpath.
The next chapter goes into detail about which settings are configurable from this file and how to do it. You
will most likely need to configure one or more of these settings, so you should copy the default
quartz.properties file to your classpath.
18
19
The Quartz CVS Repository
The CVS repository for the Quartz project, along with the other OpenSymphony projects, is hosted on
Java.net. To download anything from the CVS repository, you must have an account. You can view the source
tree anonymously from xwork.dev.java.net/source/browse/quartz, but you need an account to download the
entire tree.
After you have created your account, you can download the necessary files to build Quartz. To do this, open a
command prompt in a directory where you want to put the projects. The download creates two subdirectories
in the directory where you run the command: quartz and opensymphony.
and
cvs -d :pserver:[username]@cvs.dev.java.net:/
cvs checkout opensymphony
Substitute your account name (without the brackets) in the username field.
After you have downloaded the two modules, change the directory to the quartz module and type this:
ant -projecthelp
This gives you a list of targets with descriptions. The default target for the Quartz build file is jar; you can
just type ant from the command line, and it will build a deployable Quartz JAR.
19
20
The user community for Quartz is rather active. As with many other open source projects, the majority of
members simply anonymously view the lists and messages; fewer users post and answer questions. In the six
months that Quartz has been recording user's posts, the user forum has received more than 1,500 messages on
500 different topics and an amazing 25,000 people having viewed threads. This doesn't even include the
messages and views of the developer list. If you track these numbers over six months, you can actually see an
increasing trend. In terms of the number of downloads, Quartz is downloaded an average of 2,000 to 3,000 per
month. These numbers can go higher when a new version is released.
You can find the links to both the user and developer forum at http://forums.opensymphony.com, or you can
get to it by going to the Quartz home page at www.opensymphony.com/quartz. You are encouraged to sign up
for e-mail from the lists; when you feel comfortable, you can then join in and help the project in any way you
can.
The list of users and projects that are using Quartz is quite impressive.You can find the list on the Quartz Wiki
at http://wiki.opensymphony.com/display/QRTZ1/Quartz+Users. Probably what's most impressive is the
number of other well-known open source projects that include Quartz as part of their software: Among them
are JIRA, Spring, and several Jakarta projects.
There's no real way to gauge how many users are actively using Quartz in "real" projects. But judging from
the download numbers and some of the high-profile projects that are currently using Quartz, it looks like the
number of users is many thousandsand on the rise.
With those thoughts in mind, this chapter introduces a simple Quartz application that is representative of the
type of applications that you'll create with the Quartz framework. This example walks you through the steps
necessary to create and execute the sample application. When you're finished with this chapter, you should
20
21
have a solid foundation to build upon for the rest of the book.
This chapter shows you how to create a Quartz job that, when told to do so by our Quartz application, will
scan a specified directory and look for XML files. If it finds one or more XML files within the specified
directory, it will print some cursory information about the file. How is this useful and interesting, you say?
Hopefully, you can make the leap that after your job detects certain files in a directory, it can do many
interesting things with those files. You might want to ftp them to a remote host or e-mail them as attachments.
Perhaps the files are orders that were sent from a customer and need to be inserted into your database. Infinite
possibilities exist; we discuss some of the more interesting ones later in the book.
All attempts have been made to keep this new material straightforward and to cover only the essentials.
However, we also investigate some of the common configuration settings that affect the runtime behavior of a
Quartz application. It's expected that you know the Java language well; we spend very little time explaining
the standard aspects of it.
Finally, this chapter wraps up with a brief discussion of how to package the example application. Apache Ant
is our choice for building and packaging Java projects: Quartz applications are no exception.
The first step is to set up your development environment for this project. You can choose any development
tool or IDE that makes you feel warm and fuzzy; Quartz won't hold it against you. If you're part of the
growing mass of Java developers that have realized just how great Eclipse is, you'll be extra warm and fuzzy:
We use that IDE throughout the examples in the book.
If you haven't already done so, you can download Eclipse from http://eclipse.org. You can choose any of the
3.x. For Eclipse documentation, check www.eclipse.org/eclipse/temp0123.html; you should find everything
you need to help you get started with Eclipse.
For the examples throughout this book, we create an Eclipse Java project; each chapter contains a separate
source directory. Figure 3.1 illustrates the Quartz Java project in Eclipse.
21
22
You need to import several JARs into the project to build them successfully. To start, you need the Quartz
binary, which is named quartz-<version>.jar. Quartz also requires several third-party libraries to work;
the ones you need depend on which features of the framework you're using. For now, you should include the
Commons Logging, Commons Collections, Commons BeanUtils, and Commons Digester libraries, found in
the <QUARTZ_HOME>/lib/core and <QUARTZ_HOME>/lib/optional directories. Table 3.1 provides more
information on the dependent JARs for Quartz.
It's also a good idea to pull the Quartz source code into Eclipse. This does two things for you. First, it enables
you to set breakpoints and step from the code into Quartz source. Second, it helps you learn the framework
from the inside out. If you need to see how something is (or isn't) working, you have the actual code right
there at your fingertips. Try that with commercial software!
Every Quartz job must have a concrete Java class that implements the org.quartz.Job interface. The Quartz
job interface has a single method, execute(), that you must implement in your job. The execute() method
signature is shown here:
22
23
public void execute(JobExecutionContext context) throws JobExecutionException;
When the Quartz Scheduler determines that it's time to fire a job, it instantiates the job class and invokes the
execute() method. The Scheduler calls the execute() method with no expectations other than that you
throw an org.quartz.JobExecutionException if there's a problem with the job.
You can perform whatever business logic you need within the execute() method: For example, you might
instantiate and call a method on another class, send an e-mail, ftp a file, invoke a Web Service, call an EJB,
execute a workflow, or, in the case of our example, check to see if files exist in a particular directory.
Listing 3.1 shows our first Quartz job, which is designed to scan a directory for files and display details about
the files.
package org.cavaness.quartzbook.chapter3;
import java.io.File;
import java.util.Date;
import org.apache.commons.logging.Log;
import Org.apache.commons.logging.LogFactory;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
/**
* <p>
* A simple Quartz job that, once configured, will scan a
* directory and print out details about the files found
* in the directory.
* </p>
* Subdirectories will filtered out by the use of a
* <code>{@link FileExtensionFileFilter}</code>.
*
* @author Chuck Cavaness
* @see java.io.FileFilter
*/
public class ScanDirectoryJob implements Job {
static Log logger = LogFactory.getLog(ScanDirectoryJob.class);
23
24
if (dirName == null) {
throw new JobExecutionException( "Directory not configured" );
}
When Quartz calls the execute() method, it passes it an org.quartz.JobExecutionContext, which wraps
many things about the Quartz runtime environment and currently executing job. From the
JobExecutionContext, you can access information about the Scheduler, the job, the trigger information for
the job, and much, much more. In Listing 3.1, the JobExecutionContext is used to access the
org.quartz.JobDetail class. The JobDetail class holds the detailed information about a job, including the
name given to the job instance, the group that the job belongs to, whether the Job is persistent (volatility), and
many other interesting properties.
The JobDetail has a reference to org.quartz.JobDataMap. The JobDataMap holds user-defined properties
configured for the particular job. For example, in Listing 3.1, we get the name of the directory to scan from
the JobDataMap. We could have hard-coded the directory in the ScanDirectoryJob, but we would have had
a tough time reusing the job for other directories. In the later section "Scheduling a Quartz Job
Programmatically," you'll see exactly how the directory is configured in the JobDataMap.
The rest of the code in the execute() method is just standard Java: It gets the directory name and creates a
java.io.File object. It does a little bit of validation on the directory name to make sure it's a valid directory
and that it exists. It then calls the listFiles() method on the File object to retrieve the files from the
directory. java.io.FileFilter is created and passed in as an argument to the listFiles() method.
org.quartzbook.cavaness. FileExtensionFileFilter implements the java.io.FileFilter interface to
24
25
weed out directories and return only XML files. By default, the listFiles() method returns everything it
finds, whether it's a file or a subdirectory, so the list must be filtered because we are interested only in XML
files.
Note
The FileExtensionFileFilter is not part of the Quartz framework; it's a subclass of the
java.io.FileFilter, which is part of core Java. We have created the FileExtensionFileFilter as part of
our example to filter out everything but XML files. This is quite handyyou should think about building a set
of file filters for your application and reusing them for your various Quartz jobs.
package org.cavaness.quartzbook.chapter3;
import java.io.File;
import java.io.FileFilter;
/**
* A FileFilter that only passes Files of the specified extension.
* <p>
* Directories do not pass the filter.
*
* @author Chuck Cavaness
*/
public class FileExtensionFileFilter implements FileFilter {
/*
* Pass the File if it has the extension.
*/
public boolean accept(File file) {
// Lowercase the filename for easier comparison
String lCaseFilename = file.getName().toLowerCase();
The FileExtensionFileFilter is used to block any file that doesn't contain the string ".xml" as part of its
name. It also blocks any subdirectories, which would normally be returned as well from the listFiles()
method. Using file filters is a very convenient way of selectively choosing inputs to your Quartz jobs when
they involve files as input.
25
26
For other aspects of your Quartz application, you will have to make some decisions based on the
requirements and functionality you need from Quartz. The next section highlights when you must
use a declarative and a programmatic approach. Because much of the Java industry is moving
toward a declarative approach, this is our preference.
In the next section, we discuss how to configure the job for scheduling and how to run the
ScanDirectoryJob.
Before we talk about the ScanDirectoryJob specifically, let's discuss in general how to instantiate and run an
instance of the Quartz Scheduler. Listing 3.3 illustrates the basic steps necessary to create and start a Quartz
Scheduler instance.
26
27
Listing 3.3. Running a Simplified Quartz Scheduler
package org.cavaness.quartzbook.chapter3;
package org.cavaness.quartzbook.chapter3;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
try {
// Get a Scheduler instance from the Factory
scheduler = StdSchedulerFactory.getDefaultScheduler();
If you run the code in Listing 3.3 and you are logging the output, you should see something like the following
output:
INFO [main] (SimpleScheduler.java:30) - Scheduler started at Mon Sep 05 13:06:38 EDT 2005
27
28
log4j.rootLogger=error, stdout
This log4j.properties file will log only ERROR messages to stdout, but any INFO messages
coming from the package org.cavaness.quartzbook will appear. This is due to the last line in the
properties file.
Listing 3.3 shows just how simple it is to startup a Quartz scheduler. When the Scheduler is up and running,
you can do much with it and obtain a great deal of information from it. For example, you might need to
schedule a few jobs or change the execution times of jobs already scheduled. You might need to put the
Scheduler in stand-by mode and then later restart it so that it begins executing scheduled jobs again. While the
Scheduler is in stand-by, no jobs are executed, even if they are supposed to be based on their scheduled times.
Listing 3.4 shows how to put the Scheduler in stand-by mode and then unpause it so the Scheduler picks up
where it left off.
try {
if (!scheduler.isInStandbyMode()) {
// pause the scheduler
scheduler.standby();
}
The partial fragment in Listing 3.4 is just a quick illustration that when you have a reference to a Quartz
Scheduler, you can do some pretty interesting things with it. Of course, the Scheduler doesn't have to be in
stand-by mode for interesting things to happen. For example, you can schedule new jobs and unschedule
existing jobs, all with the Scheduler still running. We add to our repertoire of possibilities with a scheduler
throughout the book.
28
29
As simple as these examples seem to be, there is a catch. We haven't specified any jobs to be executed or
times for those jobs to execute. Although the Quartz Scheduler did start up and run in Listing 3.3, we didn't
specify any jobs to be executed. That's what we discuss next.
All jobs that you want Quartz to execute must be registered with the Scheduler. In most situations, this should
be done before the Scheduler is started. As promised earlier in this chapter, this is one area in which you get to
choose either a declarative or a programmatic approach. First we show you how to do it programmatically;
later in this chapter, we repeat this exercise with the declarative version.
For each job that is to be registered with the Scheduler, a JobDetail must be defined and associated with a
Scheduler instance. This is shown in Listing 3.5.
package org.cavaness.quartzbook.chapter3;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
try {
// Create a Scheduler and schedule the Job
Scheduler scheduler = example.createScheduler();
example.scheduleJob(scheduler);
/*
* return an instance of the Scheduler from the factory
*/
public Scheduler createScheduler() throws SchedulerException {
return StdSchedulerFactory.getDefaultScheduler();
}
29
30
The program in Listing 3.5 provides a good example of how to programmatically schedule a job. The code
first calls the createScheduler() method to obtain an instance of the Scheduler from the Scheduler factory.
When a Scheduler is obtained, it is passed into the schedulerJob(), which takes care of all the details of
associating a job with a Scheduler.
First, a JobDetail object is created for the job that we want to run. The arguments in the constructor for the
JobDetail include a name for the job, a logical group to assign the job to, and the fully qualified class that
implements the org.quartz.Job interface. We could have used several different versions of the JobDetail
constructor:
public JobDetail();
public JobDetail(String name, String group, Class
jobClass);
public JobDetail(String name, String group, Class jobClass,
boolean volatility, boolean durability, boolean
recover);
Note
A job should be uniquely identifiable by its name and group within a Scheduler instance. If you add two jobs
with the same name and group, an ObjectAlreadyExistsException will be thrown.
As stated earlier in this chapter, the JobDetail acts as a definition for a specific job. It contains properties for
the job instance and also can be accessed by the Job class at runtime. One of the most important uses of the
JobDetail is to hold the JobDataMap, which is used to store state/parameters for a job instance. In Listing 3.5,
the name of the directory to scan is stored in the JobDataMap in the scheduleJob() method.
30
31
Understanding and Using Quartz Triggers
Jobs are only part of the equation. Notice from Listing 3.5 that we don't set the execution date and times for
the job within the JobDetail object. This is done by using a Quartz trigger. As the name implies, triggers are
responsible for triggering a job to be executed. You create a trigger and associate it with a job when
registering the job with the Scheduler. Four types of Quartz triggers are available, but two main types are used
most often and, thus, are used for the next several chapters: SimpleTrigger and CronTrigger.
A SimpleTrigger is the simpler of the two and is used primarily to fire single event jobs. This trigger fires at
a given time, repeats for n number of times with a delay of m between each firing, and then quits.
CronTriggers are much more complicated and powerful. They are based on the common Gregorian calendar
and are used when you need to execute a job with a more complicated schedulefor example, every Monday,
Wednesday, and Friday at midnight during the months of April and September.
To make working with triggers easier, Quartz includes a utility class called org.quartz.TriggerUtils.
triggerUtils provides many convenience methods for simplifying the construction and configuration of
triggers. The examples throughout this chapter use the triggerUtils class; SimpleTrigger and
CronTrigger are used in later chapters.
As you can see from Listing 3.5, the triggerUtils method called makeSecondlyTrigger() is used to create
a trigger that fires every 10 seconds (triggerUtils actually produces an instance of SimpleTrigger in this
case, but our code doesn't care to know that). We must also give the trigger instance a name and tell it when to
start firing; in Listing 3.5, this starts immediately because setStartTime() is set to the current time.
Listing 3.5 illustrates how to register a single job with the Scheduler. If you have more than one job (and you
probably do), you will need to create a JobDetail for each job. Each one must be registered with the
Scheduler via the scheduleJob() method.
Note
Go back to Listing 3.1 and look at the code where the scan directory property is retrieved from the
JobDataMap. Based on Listing 3.5, you can see how it gets set.
If you are reusing a job class to run multiple instances of the same job, you need to create a JobDetail for
each one. For example, if you wanted to reuse the ScanDirectoryJob to check two different directories, you
would need to create and register two instances of the JobDetail class. Listing 3.6 shows how this could be
done.
package org.cavaness.quartzbook.chapter3;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
31
32
try {
// Create a Scheduler and schedule the Job
Scheduler scheduler = example.createScheduler();
/*
* return an instance of the Scheduler from the factory
*/
public Scheduler createScheduler() throws SchedulerException {
return StdSchedulerFactory.getDefaultScheduler();
}
trigger.setName(jobName + "-Trigger");
32
33
Listing 3.6 is very similar to the program from Listing 3.5, with a few small differences. The main difference
is that Listing 3.6 has been refactored to allow multiple calls to the schedulerJob() method. The settings for
things such as the job name and the scan interval are being passed in. So after the Scheduler instance is
obtained from the createScheduler() method, two jobs (of the same class) are scheduled using different
arguments.
If we execute the Listing_3_6 class, we should get output similar to the following:
INFO [main] (Listing_3_6.java:35) - Scheduler started at Mon Sep 05 15:12:15 EDT 2005
INFO [QuartzScheduler_Worker-0] ScanDirectory1 fired at Mon Sep 05 15:12:15 EDT 2005
INFO [QuartzScheduler_Worker-0] - c:\quartz-book\input\order-145765.xml - Size: 0
INFO [QuartzScheduler_Worker-0] - ScanDirectory2 fired at Mon Sep 05 15:12:15 EDT 2005
INFO [QuartzScheduler_Worker-0] - No XML files found in c:\quartz-book\input2
INFO [QuartzScheduler_Worker-1] - ScanDirectory1 fired at Mon Sep 05 15:12:25 EDT 2005
INFO [QuartzScheduler_Worker-1] - c:\quartz-book\input\order-145765.xml - Size: 0
INFO [QuartzScheduler_Worker-3] - ScanDirectory2 fired at Mon Sep 05 15:12:30 EDT 2005
INFO [QuartzScheduler_Worker-3] - No XML files found in c:\quartz-book\input2
Before we can discuss how to schedule your jobs declaratively, we need to talk about the quartz.properties
file. This properties file enables you to configure the runtime environment of Quartz and, more important, tell
Quartz to get the job and trigger information from an external resource, such as an XML file.
The quartz.properties file defines the runtime behavior of the Quartz application and contains many
properties that can be set to control how Quartz behaves. Only the basics are covered within this chapter; we
save the more advanced settings for later. We also do not go into detail about the valid values for each setting
33
34
at this point.
Let's look at a bare-bones quartz.properties file and discuss some of the settings. Listing 3.7 shows a
stripped-down version of the quartz.properties file.
Note
The Quartz framework sets defaults for almost all of these properties.
#===============================================================
#Configure Main Scheduler Properties
#===============================================================
org.quartz.scheduler.instanceName = QuartzScheduler
org.quartz.scheduler.instanceId = AUTO
#===============================================================
#Configure ThreadPool
#===============================================================
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
#===============================================================
#Configure JobStore
#===============================================================
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#===============================================================
#Configure Plugins
#===============================================================
org.quartz.plugin.jobInitializer.class =
org.quartz.plugins.xml.JobInitializationPlugin
org.quartz.plugin.jobInitializer.overWriteExistingJobs = true
org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.jobInitializer.validating=false
In the quartz.properties file in Listing 3.7, the properties are logically separated into four sections. The
properties don't have to be grouped or listed in any order. The lines with the # are comments.
Note
This discussion does not list every possible setting. Here we discuss only the basic settings that you need to be
familiar or that are necessary to get the declarative example working. These properties are discussed
throughout the book in the chapters that pertain to the relevant sections of the properties file.
34
35
Scheduler Properties
The first section contains two lines and sets the instanceName and instanceId for the Scheduler. The value
for the property org.quartz.scheduler.instanceName can be any string that you like. It's used to
distinguish a particular scheduler instance when multiple schedulers are being used. Multiple schedulers are
normally used within a clustered environment. (Quartz clustering is discussed in Chapter 11, "Clustering
Quartz.") For now, a string such as this one is fine:
org.quartz.scheduler.instanceName = QuartzScheduler
In fact, that is the default when none is provided in the properties file.
The second Scheduler property shown in Listing 3.7 is org.quartz.scheduler.instanceId. As with the
instanceName property, the instanceId property can be any string value you want. The value should be
unique across all Scheduler instances, especially within a cluster. You may use the value AUTO if you want the
value to be generated for you. The generated value will be NON_CLUSTERED if this is running in a nonclustered
Quartz environment. If you're using Quartz in a clustered environment, the value will be the hostname of the
machine plus the current date and time. For most situations, setting the value to AUTO is fine.
Threadpool Properties
The next section sets the necessary values for the threads that run in the background and do the heavy lifting
in Quartz. The tHReadCount property controls how many worker threads are created and available to process
jobs. In principal, the more jobs that you have to process, the more worker threads you'll need. The number
for the tHReadCount must be at least 1. Quartz imposes no maximum on the number of worker threads, but
setting this value to greater than 100 on most machines becomes quite unwieldy, especially if your jobs have
long-running logic in them. There is no default, so you must specify a value for this property.
The tHReadPriority property sets the priority that the worker threads run with. Threads with higher priorities
are typically given preference over threads with a lower priority. The minimum value for the tHReadPriority
is the constant java.lang.Thread.MAX_PRIORITY, which equates to 10. The minimum value is
java.lang.Thread.MIN_PRIORITY, which equals 1. The typical value for this property is
Thread.NORM_PRIORITY, which is 5. For most situations, you'll want to set this to the value of 5, which is the
default if the property isn't specified.
The final threadpool setting is for the property org.quartz.threadPool.class. The value for this class
should be a fully qualified name of the class that implements the org.quartz.spi.ThreadPool interface. The
threadpool that ships with Quartz is org.quartz.simpl.SimpleThreadPool, and it should meet the needs of
most users. This threadpool has simple behavior and is well tested. It provides a fixed-size pool of threads that
survive the lifetime of the Scheduler. You can create your own threadpool class, if you want. For example,
you might need to do this if you want a threadpool that grows and shrinks with demand. There is no default
specified, so you must provide a value for this property.
Jobstore Settings
The properties within the JobStore section describe how job and trigger information is stored during the
lifetime of the Scheduler instance. We haven't yet talked about the JobStore and its purpose; we save that for
later because it's not necessary for the current example. For now, all you need to know is that we are storing
Scheduler information in memory instead of a relational database.
35
36
Storing Scheduler information in memory is fast and the easiest to configure. When the Scheduler process is
halted, however, all job and trigger state is lost. Job storage in memory is accomplished by setting the
org.quartz.jobStore.class property to org.quartz.simpl.RAMJobStore, as we've done in Listing 3.7. If
we don't want to lose our Scheduler state when the JVM is halted, we could use a relational database to store
that information. This requires a different JobStore implementation that we discuss later. Chapters 5, "Cron
Triggers and More," and 6, "JobStores and Persistence," cover the various types of JobStores and when you
should use them.
Plug-In Settings
The final section in the simple quartz.properties file is the one that specifies any Quartz plug-ins that you
want to configure. A plug-in is used by many other open source frameworks, such as Struts from Apache (see
http://struts.apache.org).
The idea is to declaratively extend the framework by adding classes that implement the
org.quartz.spi.SchedulerPlugin interface. The SchedulerPlugin interface has three methods that are
called by the Scheduler.
Note
Plug-ins for Quartz are discussed in detail in Chapter 8, "Using Quartz Plug-Ins."
To declaratively configure the Scheduler information for our example, we will be using a plug-in called
org.quartz.plugins.xml.JobInitializationPlugin that comes with Quartz.
By default, this plug-in searches for a file called quartz_jobs.xml in the classpath and loads job and trigger
information from the XML file.
The next section discusses the quartz_jobs.xml file, which we informally refer to as the job definition file.
Note
By default, the JobInitializationPlugin looks for a file called quartz_jobs.xml on your classpath. You
can override this and force it to look for and use a file with a different name. To do this, you must set the
filename in the quartz.properties file that we discussed in the previous section. For now, we are going to
just rely on the default filename quartz_jobs.xml and show you how to modify the quartz.properties file
later in this chapter.
Listing 3.8 shows the job-definition XML file for the Scan Directory example. It configures job and trigger
information using a declarative approach exactly like the example from Listing 3.5.
36
37
Listing 3.8. The quartz_ jobs.xml file for the ScanDirectory Job
<quartz>
<job>
<job-detail>
<name>ScanDirectory</name>
<group>DEFAULT</group>
<description>
A job that scans a directory for files
</description>
<job-class>
org.cavaness.quartzbook.chapter3.ScanDirectoryJob
</job-class>
<volatility>false</volatility>
<durability>false</durability>
<recover>false</recover>
<job-data-map allows-transient-data="true">
<entry>
<key>SCAN_DIR</key>
<value>c:\quartz-book\input</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<simple>
<name>scanTrigger</name>
<group>DEFAULT</group>
<job-name>ScanDirectory</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-06-10 6:10:00 PM</start-time>
<!-- repeat indefinitely every 10 seconds -->
<repeat-count>-1</repeat-count>
<repeat-interval>10000</repeat-interval>
</simple>
</trigger>
</job>
</quartz>
The <job> element represents a job that you want to register with the Scheduler, just as we did earlier in the
chapter with the scheduleJob() method. You can see the <job-detail> and <trigger> elements, which we
programmatically passed into the schedulerJob() method in Listings 3.5. This is essentially what is
happening here, but in a declarative fashion. You can also see in Listing 3.8 that we set the SCAN_DIR property
into the JobDataMap, as we also did in the Listing 3.5 example.
The <trigger> element is also very intuitive: It simply sets up a SimpleTrigger with the same properties as
before. So Listing 3.8 is just a different (arguably, better) way of doing what we did in Listing 3.5. You
obviously can support multiple jobs as well. We did this in Listing 3.6 programmatically, and we can also
support it declaratively. Listing 3.9 shows the comparable version of Listing 3.6.
Listing 3.9. You Can Specify Multiple Jobs in the quartz_jobs.xml File
<quartz>
<job>
37
38
<job-detail>
<name>ScanDirectory1</name>
<group>DEFAULT</group>
<description>
A job that scans a directory for files
</description>
<job-class>
org.cavaness.quartzbook.chapter3.ScanDirectoryJob
</job-class>
<volatility>false</volatility>
<durability>false</durability>
<recover>false</recover>
<job-data-map allows-transient-data="true">
<entry>
<key>SCAN_DIR</key>
<value>c:\quartz-book\input1</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<simple>
<name>scanTrigger1</name>
<group>DEFAULT</group>
<job-name>ScanDirectory1</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-07-19 8:31:00 PM</start-time>
<!-- repeat indefinitely every 10 seconds -->
<repeat-count>-1</repeat-count>
<repeat-interval>10000</repeat-interval>
</simple>
</trigger>
</job>
<job>
<job-detail>
<name>ScanDirectory2</name>
<group>DEFAULT</group>
<description>
A job that scans a directory for files
</description>
<job-class>
org.cavaness.quartzbook.chapter3.ScanDirectoryJob
</job-class>
<volatility>false</volatility>
<durability>false</durability>
<recover>false</recover>
<job-data-map allows-transient-data="true">
<entry>
<key>SCAN_DIR</key>
<value>c:\quartz-book\input2</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<simple>
<name>scanTrigger2</name>
<group>DEFAULT</group>
<job-name>ScanDirectory2</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-06-10 6:10:00 PM</start-time>
<!-- repeat indefinitely every 15 seconds -->
<repeat-count>-1</repeat-count>
38
39
<repeat-interval>15000</repeat-interval>
</simple>
</trigger>
</job>
</quartz>
Earlier in this chapter, you were told that the JobInitializationPlugin looks for the file quartz_jobs.xml
to get the declarative job information. If you want to change that file, you need to modify the
quartz.properties file and tell the plug-in which file to load. For example, if you wanted Quartz to load job
information from an XML file called my_quartz_jobs.xml, you would have to give the plug-in that filename.
Listing 3.10 shows how this can be accomplished; we are only repeating the plug-in section here.
org.quartz.plugin.jobInitializer.class =
org.quartz.plugins.xml.JobInitializationPlugin
org.quartz.plugin.jobInitializer.fileName = my_quartz_jobs.xml
org.quartz.plugin.jobInitializer.overWriteExistingJobs = true
org.quartz.plugin.jobInitializer.validating = false
org.quartz.plugin.jobInitializer.overWriteExistingJobs = false
org.quartz.plugin.jobInitializer.failOnFileNotFound = true
In Listing 3.10, we add the property org.quartz.plugin.jobInitializer.fileName and set the value to the
name of our file. It must be available to the classloader, which means somewhere on the classpath.
When Quartz starts up and reads the quartz.properties file, it initializes the plug-in. It passes all of these
properties to the plug-in, and the plug-in gets notified to look for a different file.
Packaging the
Quartz Application
Let's finish off this early
chapter by briefly discussing
the process of packaging an
application that utilizes the
Quartz framework.
Quartz Third-Party
Dependencies
39
40
Underneath this directory,
you'll find several
subdirectories:
• <QUARTZ_HOME>\lib\core
• <QUARTZ_HOME>\lib\optional
• <QUARTZ_HOME>\lib\build
40
41
Yes, if using a
for job storage
quartz.jar Yes Core Quartz Framework
servlet.jar If using a http://java.sun.com/products/servlet/
Servlet
container, but it
should already
be present
log4j.jar Come on, who http://logging.apache.org/
is not using
this?
You also must include quartz.properties with your application. If you are deploying your application in an
exploded format, you should place the quartz.properties file in a directory that is loaded by the
classloader. (An "exploded" format is one that is not contained within a JAR, WAR, EAR, or other Java
archive, but that is laid on the file system in individual files.) For example, if you have a classes directory
(such as a Web application's WEB-INF/classes directory), store the quartz.properties file there. If you are
deploying in one of Java's archived formats, put the properties file in the root of the archive file. This same
rule applies when using the quartz_jobs.xml file.
Chapter 4 moves into the core of the Quartz framework. Arguably, this is the most important chapter for you
to read and understand. The Scheduler is the heart of the framework. This chapter focuses on how to use the
Scheduler to manage your jobs, how to create and associate triggers so the jobs will fire, and how to choose
the calendar to provide more flexibility for a given schedule.
"For a moment, nothing happened. Then, after a second or so, nothing continued to happen."
We don't attempt to cover every class and interface within the framework. Instead, we present a subset of the
components that will help you understand how Quartz does what it does. Figure 4.1 shows a compacted class
41
42
The Scheduler is the primary API of Quartz. For Quartz users, most of the interaction with the framework
takes place with the Scheduler.Clients interact with the Scheduler through the org.quartz.Scheduler
interface. The Scheduler implementation, in this case, is a proxy, and the calls are forwarded to an instance of
the QuartzScheduler. The QuartzScheduler is not visible to the client, and there's no direct interaction with
the instance.
The QuartzScheduler is at the root of the framework and is the engine that drives the entire framework. Not
all of the functionality is built directly into the QuartzScheduler, however. The framework has been
designed to be flexible and configurable so that many of the important functions are carried out by separate
components and subframeworks. This means that Quartz users can substitute their own versions of certain key
features for the default ones. Still, even though the QuartzScheduler delegates some of its responsibility, it is
still in charge of the overall process of scheduling.
Clients can interact with two types of Schedulers, shown in Figure 4.2. Both implement the
org.quartz.Scheduler interface.
42
43
As a Quartz user, you interact with the classes that implement the org.quartz.Scheduler interface. Before
you can invoke any of its API, you need to know how to create an instance of the Scheduler.
You can use two different types of the SchedulerFactory to create Schedulers (see Figure 4.3).
Figure 4.3. All Scheduler instances should be created by using the SchedulerFactory.
The DirectSchedulerFactory is designed for those who want absolute control over how a Scheduler
instance is manufactured. Listing 4.1 shows that the easiest way to use the DirectSchedulerFactory to
create a Scheduler instance.
43
44
try {
// Initialize the Scheduler Factory with 10 threads
factory.createVolatileScheduler(10);
When using the DirectSchedulerFactory, there are three basic steps. First, you must acquire an instance of
the factory using the static getInstance() method. When you have the factory instance, you need to initialize
it by calling one of the createXXX methods. For example, Listing 4.1 used the createVolatileScheduler()
method to tell the factory to initialize itself with ten worker threads (we talk more about worker threads later
in this chapter). The third and final step is to retrieve an instance of a Scheduler from the factory with the
getScheduler() method.
You can choose from several different createXXX() methods, depending on what type of Scheduler you want
to use and how you need it configured. Listing 4.1 used the createVolatileScheduler() method to create a
Scheduler. The createVolatileScheduler() method takes a single argument: the number of threads to
create. In Figure 4.2, you saw that there is also a RemoteScheduler class. You must use a different
createXXX() method to create a RemoteScheduler instance. Two versions are available:
RemoteSchedulers are discussed in Chapter 10, "Using Quartz with J2EE." If you just need a standard
Scheduler, you can use one of these versions:
44
45
public void createScheduler(ThreadPool threadPool, JobStore jobStore)
throws SchedulerException;
In the last chapter, we used a properties file to initialize the Scheduler. To create a Scheduler instance with
DirectSchedulerFactory, you must pass configuration parameters through one of the createXXX()
methods. In the next section, we discuss the StdSchedulerFactory, a version of SchedulerFactory that
relies on a set of properties to configure the Scheduler instead of passing them in as arguments to the
createXXX method. It thus avoids hard-coding Scheduler configuration settings within your code.
Listing 4.2 demonstrates the first approach, supplying the properties through a java.util.Properties
instance.
45
46
props.put(StdSchedulerFactory.PROP_THREAD_POOL_CLASS,
"org.quartz.simpl.SimpleThreadPool");
props.put("org.quartz.threadPool.threadCount", "10");
try {
Listing 4.2 provides a very simple example of using the StdSchedulerFactory to create an instance of a
Scheduler. The two properties passed into the factory in the example are the name of a class that implements
the org.quartz.spi.ThreadPool interface and the number of threads that the Scheduler should use to
process jobs. These two properties are required because no defaults are set for them (we talk more about these
shortly).
With the DirectSchedulerFactory from Listing 4.1, we called one of the createXXX() methods to initialize
the factory. With the StdSchedulerFactory, you use one of the available initialize() methods. After the
factory has been initialized, you can call the getScheduler() method to acquire the instance of the Scheduler.
Passing the properties to the factory using the java.util.Properties object is just one way of configuring
the SchedulerFactory. Hard-coding configuration properties is hardly recommended and should be avoided
when possible. If you needed to modify the number of threads in this example, you would need to change the
code and recompile.
Fortunately, the StdSchedulerFactory provides other ways to supply the necessary properties. The factory
can also be initialized by passing in the name of an external file that contains these configuration settings.
This alternate form of the initialize() method is shown here:
For the file and properties to be loaded successfully, the file must be available to the classloader. This means
that it should be in the classpath of your application. If you would rather use an java.io.InputStream to
load the file, you can use this alternate form of the initialize() method:
In Chapter 3, "Hello, Quartz," you saw an example that used an external file called quartz.properties to
load the settings for the SchedulerFactory. This external properties file is just a special use of the previous
46
47
method signatures. If you don't specify where to get the properties using one of the initialize() methods,
the StdSchedulerFactory attempts to load them from a file called quartz.properties. This is the behavior
you saw in Chapter 3.
If you use the no-argument version of the initialize() method, the StdSchedulerFactory performs the
following steps to attempt to load the properties for the factory:
Using the StdSchedulerFactory for creating a Scheduler instance is so common that the
StdSchedulerFactory includes a static convenience method called geTDefaultScheduler() that uses the
previous steps outlined to instantiate the factory. This is shown in Listing 4.3.
try {
// Create a default instance of the Scheduler
Scheduler scheduler =
StdSchedulerFactory.getDefaultScheduler();
logger.info("Scheduler starting up...");
scheduler.start();
47
48
The static getdefaultScheduler() method calls the empty constructor. If none of the initialize()
methods has been previously invoked, the no-argument initialize() method is called. This sets in motion
the load file sequence mentioned earlier. In the default case, the quartz.properties file is located, and the
properties are loaded from that file.
Scheduler Functionality
Most of the material in this chapter so far has focused on obtaining an instance of the Scheduler. So when you
have a Scheduler instance, what can you do with it? Well, to start, the examples have shown that you can call
the start() method on it. The Scheduler API includes approximately 65 different methods. We don't
enumerate all of them here, but you need to understand a little more about the Scheduler API.
Starting the Scheduler couldn't be more straightforward. When you have an instance of a Scheduler that has
been properly initialized and your jobs and triggers have been registered, simply call the start() method:
When the start() method is called, the Scheduler starts looking for jobs that need to be executed. You use
the start() method when you have a new Scheduler instance and when the Scheduler has been placed into
standby mode. After shutdown() has been called, you can no longer call start() on that Scheduler instance.
48
49
Standby Mode
Putting the Scheduler in standby mode causes the Scheduler to temporarily stop looking for jobs to execute.
As an example, suppose your Scheduler gets its job information from a database and you need to restart the
database. You could either restart Scheduler when the database is back online or just put the Scheduler in
standby mode. You can put a Scheduler in standby mode by calling the standby() method:
In standby mode, the Scheduler does not attempt to execute jobs because the thread that searches for jobs that
need executing is paused.
You can use two versions of the shutdown() method to stop the Scheduler:
The only difference between these two methods is that one version takes a Boolean argument that doesn't let
the Scheduler stop until all currently executing jobs have finished executing. The shutdown() method without
the argument is similar to calling shutdown(false).
Managing Jobs
Previous chapters took a cursory look at Quartz jobs, but now we go through a more formal discussion of
Quartz jobs and how to use them.
Quite simply, a Quartz job is a Java class that performs a task on your behalf. This task can be anything that
you can code in Java. Here are a few examples to make the point:
• Use JavaMail (or another Mail framework, such as Commons Net) to send e-mails
• Create a remote interface and invoke a method on an EJB
49
50
These examples are just a few; you surely can come up with your own. Anything that you can do in Java can
become a job.
The only requirement that Quartz puts on your Java class is that it must implement the org.quartz.Job
interface. Your job class can implement any other interfaces that it wants or extend any class that it needs, but
it or a superclass must implement the job interface. The job interface defines a single method:
When the Scheduler determines that it is time to run the job, the execute() method is called, and a
JobExecutionContext object is passed to the job. The only contractual obligation that Quartz puts on the
execute() method is that if there's a serious problem with the job, you must throw an
org.quartz.JobExecutionException.
JobExecutionContext
When the Scheduler calls a job, a JobExecutionContext is passed to the execute() method. The
JobExecutionContext is an object that gives the job access to the runtime environment of Quartz and the
details of the job itself. This is analogous to a Java Web application in which a servlet has access to the
ServletContext. From the JobExecutionContext, the job can access everything about its environment,
including the JobDetail and trigger that were registered with the Scheduler for the job. Listing 4.4 shows a
job called PrintInfoJob that prints some information about the job.
As you can see from Listing 4.4, Quartz jobs can be very basic. The PrintInfoJob gets the JobDetail object,
which is stored in the JobExecutionContext, and prints some basic details about the job. The JobDetail
class deserves a little more discussion.
50
51
JobDetail
You first saw the org.quartz.JobDetail class back in Chapter 3. A JobDetail instance is created for every
job that is scheduled with the Scheduler. The JobDetail serves as the definition for a job instance. Notice in
Listing 4.5 that the job isn't the object registered with the Scheduler; it's actually the JobDetail instance.
Listing 4.5. A JobDetail Is Registered with the Scheduler, Not the Job
try {
You can see in Listing 4.5 that the JobDetail gets added to the Scheduler, not the job. The job class is part of
the JobDetail but is not instantiated until the Scheduler is ready to execute it.
51
52
You can define state for a job using org.quartz.JobDataMap. The JobDataMap implements java.util.Map
through its superclass, org.quartz.utils.DirtyFlagMap. You can store key/value pairs within the
JobDataMap, and those data pairs can be passed along and accessed from within your job class. This is a
convenient way to pass configuration information to your job. Listing 4.6 illustrates this approach using a job
we created especially for this purpose, called PrintJobDataMapJob.
Listing 4.6. Use the JobDataMap to Pass Configuration Information to Your Job
try {
// Create a default instance of the Scheduler
scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
logger.info("Scheduler was started at " + new Date());
scheduler.scheduleJob(jobDetail, trigger);
52
53
In Listing 4.6, the information that we want to pass to the PrintJobDataMapJob is stored in the JobDataMap
within the JobDetail. Because the JobDataMap implements the java.util.Map interface, we store state there
using a key/value pair configuration. The JobDataMap includes niceties to make it easier to deal with object
conversion. Normally with maps, you have to explicitly convert from object to the known type. The
JobDataMap includes methods that do this on your behalf.
When the Scheduler eventually calls the job, the job can use the JobDetail to access and use the key/value
pairs from the JobDataMap. Listing 4.7 shows the PrintJobDataMapJob.
Listing 4.7. The Job Can Access the JobDataMap THRough the JobExecutionContext Object
logger.info("in PrintJobDataMapJob");
while (iter.hasNext()) {
Object key = iter.next();
Object value = jobDataMap.get(key);
When you obtain the JobDataMap, you can use its methods as you might any map instance. Normally, you
access the data within the JobDataMap using a predefined key of your choice. You can also iterate through the
map itself, as Listing 4.7 shows.
For jobs such as PrintJobDataMapJob, the properties within the JobDataMap become an informal contract
obligation between the client scheduling the job and the job itself. Job creators should document very
carefully which properties are required and which are optional. This helps ensure that jobs get reused by other
members of your team.
53
54
forward, this method should be considered a best practice for retrieving the JobDataMap
You learned from the previous section that information can be inserted into the JobDataMap and accessed by
your jobs. For every job execution however, a new instance of the JobDataMap is created with the values that
have been stored (for example, in a database) for the particular job. Therefore, there's no way to hold that
information between job invocationsthat is, unless you use a stateful job.
In the same way that stateful session beans (SFSB) in J2EE keep their state between calls, the Quartz
StatefulJob can hold its state between job executions. However, just like SFSBs, Quartz stateful jobs have
some downsides when compared with their stateless counterparts.
The Quartz framework offers the org.quartz.StatefulJob interface when you need to maintain state
between job executions. The StatefulJob interface extends the standard job interface and adds no methods
that you have to implement. You simply implement the StatefulJob interface using the same execute()
method as the job interface. If you have an existing job class, all you have to do is change the job interface to
org.quartz.StatefulJob.
Two key differences exist between a job and StatefulJob as they are used by the framework. First, the
JobDataMap is repersisted in the JobStore after each execution. This ensures that changes that you make to
the job data are kept for the next execution.
The other important difference between stateless and stateful jobs is that two or more stateful JobDetail
instances can't execute concurrently. Say that you have created and registered a stateful JobDetail with the
Scheduler. You also have set up two triggers that fire the job: one that fires every minute and another that fires
every five minutes. If the two triggers tried to fire the job at the same time, the framework would not allow
that to occur. The second trigger would be blocked until the first one completed.
This requirement has to do with the JobDataMap storage. Because the JobDataMap is stored along with the
JobDetail that defines the job instance, thread-safety issues must be taken into consideration. Only one
thread can run and update the JobDataMap storage at a time. Otherwise, the data would be erroneous because
the second trigger could try to execute the job before the first had a chance to update the storage. Even
stranger results could occur if the second execution completed before the first, which is possible, depending
on what your job does.
Because of these differences, you should use the StatefulJob carefully. When you need to prevent
concurrent executions of a job, the stateful job is your easiest bet. In the J2EE world, stateful has developed a
54
55
Job Volatility
A volatile job is one that is not persisted between program shutdowns. A job is set to be
volatile by calling the setVolatility(true) method on the JobDetail.
Job Durability
A durable job is one that should remain in the JobStore, even if there are no longer any
triggers that can fire for the job. Let's say that you set up a single-fire trigger that fires and is
therefore moved to the STATE_COMPLETE state. No more firing times exist, but the trigger was
scheduled to execute only once. The job that this trigger pointed to is now orphaned because it
no longer has any triggers associated with it.
If you set a job to be durable, it will not be removed from the JobStore when it becomes
orphaned. This keeps it available for further scheduling whenever your program decides to add
another trigger for it. If the setDurability(false) method was invoked on the JobDetail,
the job would be removed from the JobStore when all triggers have fired. The default value
for durability is false. Hence, the default behavior of jobs and triggers is to be automatically
removed from the JobStorethe trigger when it completes all its firing and the job when it has
no associated triggers.
Job Recoverability
When the Scheduler experiences an unexpected shutdown and a job is executing, a recoverable
job is re-executed when the Scheduler is restarted. The job starts executing again right from the
55
56
beginning. The Scheduler has no way of knowing where the job was during execution when
the program was stopped and, therefore, must start all over again.
By default, the value is set to false and the Scheduler does not try to recover the jobs.
You can remove a job that has been scheduled in several ways. One way is to remove all the
triggers associated with the job; if the job is durable, it will be removed from the Scheduler. An
easier way is just to remove the job directly. You can use the deleteJob() method to do this:
Interrupting Jobs
It's sometimes necessary to be able to interrupt a job, especially if it takes a long time to
execute. For example, suppose you have a job that takes an hour to run, and you realize five
minutes into the job that it's going to need to run again because of some uncontrollable error.
You might as well interrupt the job, fix the problem, and then rerun it.
You can call the interrupt() method on the Scheduler by providing the name of the job and
the group that was used when the job was scheduled:
The Scheduler will invoke the interrupt() method on your job. It's up to you and your job to
determine how to interrupt the job. Although there are several acceptable approaches, Listing
4.8 provides one of the more common. The Quartz framework can signal to a job that an
interrupt has been requested for it, but here you have control over the job and, therefore, are
responsible for the approach.
56
57
Listing 4.8. An InterruptableJob Can Be Used to Determine Whether interrupt() has Been
Called on the Scheduler
if (counter <= 0) {
jobFinished = true;
}
The Quartz framework provides several jobs that you can use within your applications with
little work. Table 4.1 lists the jobs and their usage.
57
58
changed.
org.quartz.jobs.FileScanListener A listener that
tells
FileScanJob
when a file
has been
modified.
org.quartz.jobs.NativeJob A job that is
used for
executing
native
programs
(such as .exe
files under
Windows).
org.quartz.jobs.NoOpJob A job that
doesn't do
anything but
that might be
useful for
testing
listeners.
Some users
even use this
just to cause a
listener to run.
org.quartz.jobs.ee.mail.SendMailJob A job that
sends an
e-mail using
the JavaMail
API.
org.quartz.jobs.ee.jmx.JMXInvokerJob A job that
invokes a
method on a
JMX bean.
org.quartz.jobs.ee.ejb.EJBInvokerJob A job that
invokes a
method on an
EJB.
58
59
Threads in Java
Threads allow a program to do many tasks simultaneouslyor, at least, it seems that the tasks are running
simultaneously. Excluding parallel processing for this discussion, only a single thread executes at any
particular moment, but the CPU gives each thread a little bit of time to run and then switches (through
time-slicing) back and forth between threads very quickly. It can give the appearance of multiple running
threads.
The Java language includes built-in support for threads with the Thread class. When a thread is told to run,
the thread's run() method is executed. Just because you create a thread instance and call the start() method
does not mean that the run() method will be executed immediately; the thread instance must wait until the
JVM tells it that it can run.
A thread can be in one of several possible states during its life-time. It can be in only one state at a time, and
these states are dictated by the JVM, not the operating system. The thread states are listed here:
• New
• Runnable
• Blocked
• Waiting
• Timed Waiting
• Terminated
When a new thread is created, it is assigned to the New state. No system resources are allocated to a thread in
this state. To move the thread to the Runnable state, the start() method must be called.
When the start() method is called on a new thread, the thread enters the Runnable state and is considered to
be running. This doesn't mean that the thread is actually executing instructions. Even though the state is set to
Runnable and the thread is scheduled to run, it might have to wait until other executing threads finish or are
temporarily suspended. The JVM decides which thread should get a chance to run next. The manner in which
the JVM determines the next thread depends on several factors, including the operating system, the particular
JVM, and thread priority, among others. Figure 4.4 shows the life cycle of a Java thread.
59
60
The Blocked and Waiting states are a little beyond the discussion for this book and tie into some topics that
we don't need to explore here. If you want more information on this topic, see the Java tutorial on threads at
the Sun site, http://java.sun.com/docs/books/tutorial/essential/threads/temp0123.html.
Each Java program is assigned a process. The program then breaks up its tasks into threads. Even when you
write a simple "Hello, World" program that consists of only a single main() method, the program still uses
threads, albeit just one.
Sometimes the term lightweight process is used to refer to a thread. This is in contrast to the term
heavyweight, which refers to an actual operating system process. If you are using the Windows operating
system, you can look at the Task Manager and see the heavyweight processes running, but you won't see
threads. Figure 4.5 illustrates how multiple threads operate within a Java application.
As you can see from Figure 4.5, threads don't usually run concurrently. Putting aside multiple CPUs and
parallel processing here, only one thread at a time gets attention from the CPU to execute. Each JVM and
operating system might handle this differently, however. Sometimes the thread might get to run to completion
and then another thread can then run. This is known as non-preemptive. Other times, a thread is interrupted so
that the others can do their thing. This is called preemptive. In general, threads must compete with one another
for the CPU, and this competition is often based on thread priority.
We have mentioned several times that it's up to the JVM to determine which Runnable thread should get the
next opportunity to run. The JVM supports a scheduling scheme known as fixed-priority scheduling that
schedules threads based on their priority, compared to other Runnable threads.
60
61
The priority is an integer value that gets assigned to a new thread based on the priority of the parent thread
that created it. The value ranges from Thread.MIN_PRIORITY, which equates to 1, up to
Thread.MAX_PRIORITY, which equates to 10. The constants are defined in the java.lang.Thread class.
The larger the integer value (up to the MAX_PRIORITY), the higher the priority the thread will have. Unless
otherwise specified, most of the threads created in a Java application run at Thread.NORMAL_PRIORITY, which
is right in the middle at a value of 5. You can modify the priority of the thread by calling the setPriority()
method.
In general, a running thread continues to run until one of several conditions occurs:
• The thread yields (possibly by calling the sleep() method, yield() method, or Object.wait()
method).
• The thread's run() method finishes.
• If the OS supports time slicing, its time is up.
• Another higher-priority thread becomes Runnable.
Daemon Threads
Daemon threads are sometimes referred to as service threads becomes they usually run at a very low priority
(in the background) and do the dull yet essential tasks. For example, the garbage collector in Java is an
example of a daemon thread. This thread runs in the background and looks for and reclaims memory that the
application no longer is using.
You can make a thread a daemon by setting true in the setDaemon() method. Otherwise, the thread will be a
user thread. You can make a thread a daemon thread only before the thread is started. Daemon threads are
special, in that the JVM exits if only daemon threads are running and no nondaemon threads are still alive.
THReadGroups in Java are implemented by the java.lang.ThreadGroup class and represent a group of
threads that can be acted upon as a single entity. Every Java thread is a member of a thread group. In a thread
group, you can stop and start all of the threads that are members of the group. You can set priorities and
perform other common thread functions. Thread groups are very useful for building multithreaded
applications such as Quartz.
When a Java application starts, it creates a THReadGroup called main. Unless specified, all threads that are
created become members of this group. When a thread is assigned to a specific THReadGroup, it can't be
changed.
Java ThreadPools
Java 1.5 introduced a new concept to the Java language called thread pools. At first glance, these might seem
similar to thread groups, but they actually are used for different purposes. Whereas multiple threads can
belong to the same ThreadGroup, groups don't share the qualities of a typical pooled resource. That is, thread
groups are only used for membership.
Thread pools are shared resources and are a managed collection of threads that can be used to perform tasks.
Thread pools (and resource pools, in general) provide several benefits over nonpooled resources. First and
foremost, resource pools provide performance improvements by reducing the overhead of object creation. If
you instantiate ten threads and use these threads repeatedly instead of creating a new one every time you need
61
62
one, you improve the performance of the application. This concept permeates Java and J2EE. Other benefits
include being able to limit the number of resources, which can help the stability and scalability of the
applicationa greatly desired feature, no matter what language or application this is.
When a Quartz Scheduler is first created through one of the factory methods, the factory configures several
important resources that the Scheduler will need throughout its lifetime. A few of those important resources
have to do with threads.
When a Quartz application is first launched, the main thread starts the Scheduler. The QuartzScheduler is
created and creates an instance of the org.quartz.core.QuartzSchedulerThread class. The
QuartzSchedulerThread contains the processing loop for determining when the next job should be triggered.
As the name implies, the QuartzSchedulerThread is a Java thread. It runs as a nondaemon thread with a
normal priority.
The main processing loop for the QuartzSchedulerThread method is illustrated here:
The QuartzSchedulerResources
When the factory creates the Scheduler instance, it also passes the Scheduler an instance of an
org.quartz.core.QuartzSchedulerResources. The QuartzSchedulerResources contains, among others
things, a ThreadPool instance that provides a pool of worker threads that are responsible for executing jobs.
62
63
In Quartz, a ThreadPool is represented by the org.quartz.spi.ThreadPool interface (because Quartz was
invented before JDK 1.5, it needed to create its own ThreadPool classto ensure backward compatibility,
Quartz still utilizes its THReadPool instead of Java's) and includes a concrete implementation called
org.quartz.simpl.SimpleThreadPool. The SimpleThreadPool has a fixed number of worker threads and
doesn't shrink or grow based on load. Figure 4.6 shows the sequence of steps that the framework undergoes
during startup as it relates to threads.
Quartz doesn't process your jobs on the main thread. If it did, it would severely reduce the scalability of the
application. Instead, Quartz delegates the thread-management responsibilities to a separate component. For
the generic Quartz setup, which most users employ, the SimpleThreadPool class handles thread management.
The SimpleThreadPool creates a number of WorkerThread instances that are responsible for processing a job
on a separate thread. The WorkerThread is an inner class defined within the SimpleThreadPool class and is
essentially a thread. The number of WorkerThreads that are created and the priority for these threads are
specified by the configuration settings in the quartz.properties file or are passed into the factory.
When the QuartzSchedulerThread asks the ThreadPool to run a JobRunShell instance, the ThreadPool
checks to see if a worker thread is available. If all the configured worker threads are busy, the ThreadPool
waits until one becomes available. When a worker thread is available and a JobRunShell is waiting to be
executed, the worker thread calls the run() method on the JobRunShell class.
63
64
Although WorkerThreads are indeed Java threads, the JobRunShell class implements Runnable as well. That
means that it can act as a thread and contains a run() method. As discussed earlier in the chapter, the purpose
of the JobRunShell is to call the execute() method on a job. It does this and also notifies the job and trigger
listeners, and it finishes by updating trigger information about the execution.
• org.quartz.SimpleTrigger
• org.quartz.CronTrigger
• org.quartz.NthIncludedDayTrigger
There is a fourth trigger, called UICronTrigger, but it has been deprecated in Quartz 1.5. It was
primarily used by the Quartz Web application and is not used within Quartz itself.
Appropriately named, the SimpleTrigger is the simplest Quartz trigger to set up and use. It's
designed to be used for jobs that need to start at a particular date/time and to repeat n number of
times with a possible delay between each execution. Listing 4.9 provides an example that uses a
SimpleTrigger.
JobDetail jobDetail =
new JobDetail("PrintInfoJob",
Scheduler.DEFAULT_GROUP,
PrintInfoJob.class);
/*
* Create a SimpleTrigger that starts immediately,
* with a null end date/time, repeats forever and has
* 1 minute (60000 ms) between each firing.
*/
64
65
Trigger trigger =
new SimpleTrigger("myTrigger",
Scheduler.DEFAULT_GROUP, new Date(), null,
SimpleTrigger.REPEAT_INDEFINITELY,
60000L);
scheduler.scheduleJob(jobDetail, trigger );
Several variations of the SimpleTrigger constructor exist. They range from the no-argument
version all the way to one that takes a full set of arguments. This code fragment shows a simple
constructor that takes just the name and group for the trigger:
This trigger will execute immediately and will not repeat. There's also a constructor that takes
several arguments and configures the trigger to fire at a particular time, repeat multiple times, and
delay between each firing.
The CronTrigger allows for a much more complex firing schedule. Whereas you might have to use
two or more SimpleTriggers to meet your firing needs, you might need just a single CronTrigger
instance.
As the name implies, the CronTrigger is based on UNIX cron-like expressions. For example, you
might have a job that needs to be executed every five minutes between 8:00 AM and 9:00 AM on
Monday and Friday. If you tried to implement this with a SimpleTrigger, you would probably end
up with several triggers for the job. However, you can use an expression like this to produce a
trigger that will fire on this schedule:
65
66
Because CronTriggers have so much flexibility built into them by the nature of the almost limitless
expressions that can be created, the next chapter focuses exclusively on everything you wanted to
know about CronTriggers and cron expressions. Chapter 5, "CronTriggers and More," also
presents a set of cookbook examples of how to create CronTriggers for specific firing schedules.
NthIncludedDayTrigger trigger =
new NthIncludedDayTrigger(
"MyTrigger", Scheduler.DEFAULT_GROUP);
trigger.setN(15);
trigger.setIntervalType(
NthIncludedDayTrigger.INTERVAL_TYPE_MONTHLY);
You are not forced to live with just a single trigger per job. If you need a more complex firing
schedule, you can create multiple triggers and assign them to the same job. The Scheduler
determines the proper execution schedule based on all the triggers for the job. Using multiple
triggers for the same JobDetail is shown in the following method fragment:
try {
// Create and start the scheduler
Scheduler scheduler =
StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
logger.info("Scheduler has been started");
JobDetail jobDetail =
new JobDetail("PrintInfoJob",
Scheduler.DEFAULT_GROUP,
PrintInfoJob.class);
66
67
logger.error(ex);
}
Don't confuse the Quartz Calendar object with the java.util.Calendar in the Java API. They are
two different components and are used for two different purposes. As you are probably aware,
Java's Calendar object is used for general-purpose Date and Time utilities; much of the functionality
that used to reside in Java's Date class now resides within the Calendar classes.
The Quartz Calendar, on the other hand, is used exclusively to block out sections of time so that
triggers are prevented from firing during those blocked-out periods. For example, let's assume that
you work for a financial institution such as a bank. It's very common for banks to have many "bank
holidays." Suppose you don't need (or want) the jobs to run on those days. You can accomplish this
in one of several ways:
• Let your jobs run anyway. (This might mess things up for the bank.)
• Manually stop your jobs during the holidays. (Someone would need to be there to do it.)
• Create multiple triggers that don't include these days. (This would be time-consuming to set
up and maintain.)
• Set up a bank holiday calendar that excludes these days. (Very easy to do!)
Although you could use each of these solutions to solve the problem, the Quartz Calendar is
specifically designed for this.
Quartz defines the org.quartz.Calendar interface that all Quartz Calendars must implement. It
contains several methods, but these are the two most important ones:
67
68
As a Quartz job creator and developer, you don't necessarily need to be that familiar with the
Calendar interface. It's mainly for situations for which the existing Calendars (those that come with
Quartz) are not sufficient. Out of the box, Quartz includes many Calendar implementations that
should fill your needs. Table 4.1 lists the Calendars that come with Quartz ready to use.
Table 4.1. Quartz Includes Many Calendar Types That Your Applications Can Use
Calendar Name Class
BaseCalendar org.quartz.impl.calend
WeeklyCalendar org.quartz.impl.calend
MonthlyCalendar org.quartz.impl.calend
AnnualCalendar org.quartz.impl.calend
HolidayCalendar org.quartz.impl.calend
To use a Quartz Calendar, you simply need to instantiate one, add your excluded dates to it, and then register
it with the Scheduler. Finally, associate the Calendar instance with each trigger instance that you want to use
the Calendar with.
Listing 4.10 shows an example of excluding bank holidays using the Quartz AnnualCalendar class.
68
69
Listing 4.10. Using the AnnualCalendar to Exclude Bank Holidays
scheduleJob(scheduler, PrintInfoJob.class);
cal.setDayExcluded(gCal, true);
/*
* Set up a trigger to start firing now, repeat forever
* and have (60000 ms) between each firing.
*/
Trigger trigger =
TriggerUtils.makeImmediateTrigger("myTrigger",
-1,60000);
JobDetail jobDetail =
new JobDetail(jobClass.getName(),
Scheduler.DEFAULT_GROUP, jobClass);
69
70
When you run the example from Listing 4.10, unless it's July 4, you should see the job execute. As an exercise
that's left for you to do, change the excluded date in the scheduleJob() method to the current date that you're
reading this. If you run the code again, you should see that the current date has been excluded and the next
firing time will be tomorrow.
This last section demonstrates how easy it is to create your own Calendar class. Suppose that you need a
Calendar to exclude certain minutes of the hour. For example, suppose you need to exclude the first five
minutes of every hour or the last 15 minutes of every hour. You can create a new Calendar to support this
functionality.
Listing 4.11 shows the HourlyCalendar that we can use to exclude sets of minutes from the hour.
Listing 4.11. The HourlyCalendar Can Exclude Certain Minutes from Every Hour
public HourlyCalendar() {
super();
}
public HourlyCalendar(Calendar baseCalendar) {
super(baseCalendar);
}
70
71
if (minute == excludedMin.intValue()) {
return true;
}
continue;
}
return false;
}
excludedMinutes.addAll(minutes);
}
if (isMinuteExcluded(minute))
return;
excludedMinutes.add(new Integer(minute));
}
if (super.isTimeIncluded(timeStamp) == false) {
return false;
}
return !(isMinuteExcluded(minute));
}
if (isMinuteExcluded(minute) == false)
return timeStamp; // return the
// original value
If you use the HourlyCalendar to schedule a job, all you need to do is set the minutes of the hour that you
want to exclude; the Calendar and the Scheduler do the rest. You can see the HourlyCalendar demonstrated
in Listing 4.12.
71
72
Listing 4.12. The HourlyCalendar Executes Based on Certain Excluded Minutes of the Hour
Trigger trigger =
TriggerUtils.makeImmediateTrigger("myTrigger",
-1, 10000);
JobDetail jobDetail =
new JobDetail(jobClass.getName(),
Scheduler.DEFAULT_GROUP, jobClass);
When you run Listing 4.12, you should see that the PrintInfoJob is not executed during the excluded
minutes. Change the minutes that are excluded using the setMinuteExcluded() method and see for yourself
how the new Calendar works.
72
73
The UNIX cron daemon wakes up every minute and examines the configuration files, which are called
crontabs. (Crontab comes from the phrase CRON TABle, which is a list of jobs and other instructions for the
cron daemon.) The daemon inspects the commands that are stored within the crontabs and determines whether
any tasks need to be executed.
You can think of the UNIX crontab as a combination of triggers and jobs because they list both the execution
schedule and the command (job) to be executed.
The crontab format contains six fieldsfive for the schedule and the sixth for the command to execute. (Quartz
cron expression has seven fields.) These are the five schedule fields:
• Minute (0059)
• Hour (0023)
• Day (131)
• Month (112)
• Weekday (06 or sunsat)
73
74
The UNIX cron format allows for a few special characters in the cron expression, such as the asterisk
character (*), which matches all values. Here's an example of a UNIX crontab:
This crontab entry prints the string "WAKE UP" to the UNIX device /dev/console every morning at 8 AM.
Figure 5.1 shows it in action.
Figure 5.1. The UNIX Cron executing the 0 8 * * * echo "WAKE UP" 2>$1 /dev/console expression
74
75
try {
// Create a default instance of the Scheduler
scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
logger.info("Scheduler was started at " + new Date());
// Create a CronTrigger
try {
// CronTrigger that fires @7:30am Mon - Fri
CronTrigger trigger = new
CronTrigger("CronTrigger", null,
"0 30 7 ? * MON-FRI");
scheduler.scheduleJob(jobDetail, trigger);
} catch (ParseException ex) {
logger.error("Error parsing cron expr", ex);
0 30 7 ? * MON-FRI
When interpreted by the Scheduler, this causes the trigger to fire at 7:30 AM Monday through Friday. Let's
look at the format of the cron expression for Quartz CronTriggers.
With UNIX cron, the job (or command) that is to be executed is stored
with the cron expression, in the sixth position. Quartz uses the cron
expression to store the firing schedule. The CronTrigger, which
references the cron expression, is associated with the job at schedule
time.
75
76
Another difference between the UNIX cron expression format and
Quartz is the number of supported fields in the expression. Where UNIX
gives five (minute, hour, day, month, and dayofweek), Quartz
provides seven. Table 5.1 lists the seven cron expression fields Quartz
supports.
The fields are separated by a space, just as with UNIX cron. Arguably, the simplest expression we could write
would look something like this:
* * * ? * *
This expression would fire the scheduled job every second, for every minute, for every hour of every day.
As with UNIX cron, Quartz cron expressions support special characters that can be used to create more
complicated execution schedules. However, Quartz supports many more than the standard UNIX cron
expression.
The * Character
Using the asterisk (*) in a field indicates that you want to include all legal values for that field. For example,
using this character in the month field means to fire the trigger for every month.
Example expression:
0 * 17 * * ?
Meaning: Fire the trigger every minute, every day starting at 5 PM until 5:59 PM. It stops at 5:59 PM because
the value 17 is in the hour field, and at 6 PM, the hour becomes 18 and doesn't agree with this trigger until the
next day at 5 PM.
Use the * character when you want the trigger to fire for every valid value of the field.
76
77
The ? Character
The question mark (?) character can be used only in the dayofmonth and dayofweek fields, but not at the
same time. You can think of the ? character as "I don't care what value is in this field." This is different from
the asterisk, which indicates every value for the field. The ? character says that no value was specified for this
field.
The reasons a value can't be specified for both fields are tough to explain and even tougher to understand.
Basically, if a value was specified for each, the meaning would become ambiguous: Consider if an expression
had the value 11 in a field for the day of the month and a value of WED in the field for the day of the week.
Should that trigger fire only on the 11th of the month if it falls on a Wednesday? Or should it fire on both the
11th and every Wednesday? The ambiguity is removed by not allowing a value in both fields at the same time.
Just remember that if you specify a value in one of the two fields, you must put a ? in the other.
Example expression:
0 10,44 14 ? 3 WED
Meaning: Fire at 2:10 PM and 2:44 PM every Wednesday in the month of March.
The , Character
The comma (,) character is used to specify a list of additional values within a given field. For example, using
the value 0,15,30,45 in the second field means to fire the trigger every 15 seconds.
Example expression:
0 0,15,30,45 * * * ?
The / Character
The slash (/) character is used to schedule increments. We just used the comma to increment every 15
minutes, but we could have also written it like 0/15.
Example expression:
0/15 0/30 * * * ?
Meaning: Fire the trigger every 15 seconds on the hour and half-hour.
You can't increment beyond the fields range. For example, you can't specify 30/20 in the second field and
expect the scheduler to fire correctly.
77
78
The Character
The hyphen (-) character is used to specify a range. For example, 3-8 in the hour field means "the hours 3, 4,
5, 6, 7, and 8." The fields will not wrap, so values such as 50-10 are not allowed.
Example expression:
0 45 3-8 ? * *
The L Character
The L character represents the last allowed value for the field. It is supported by the dayofmonth and
dayofweek fields only. When used in the dayofmonth field, it represents the last day of the month for the
value specified in the month field. For example, when the month field has JAN specified, using L in the
dayofmonth field would cause the trigger to fire on January 31. If SEP was specified as the month, then L
would mean to fire on September 30. In order words, it means to fire the trigger on the last day of whatever
month is specified.
The expression 0 0 8 L * ? means to fire the trigger at 8:00 AM the last day of every month. The *
character in the month field gives us the "every month" part.
When the L character is used in the dayofweek field, it indicates the last day of the week, which is Saturday
(or, numerically, 7). So if you needed to fire the trigger on the last Saturday of every month at 11:59 PM, you
could use the expression 0 59 23 ? * L.
When used in the dayofweek field, you can use a numerical value in conjunction with the L character to
represent the last X day of the month. For example, the expression 0 0 12 ? * 2L says to fire the trigger on
the last Monday of every month.
The W Character
The W character stands for weekday (MonFri) and can be used only in the dayofmonth field. It is used to
specify the weekday that is nearest to the given day. Most business processes are based on the work week, so
the W character can be very important. For example, a value of 15W in the dayofmonth field means "the nearest
weekday to the 15th of the month." If the 15th was on a Saturday, the trigger would fire on Friday the 14th
because it's closer to the 15th than Monday, which would be the 17th in this example. The W character can be
specified only when the day of the month is a single day, not a range or list of days.
78
79
The # Character
The # character can be used only in the dayofweek field. It's used to specify the nth XXX day of the month.
For example, if you specified the value 6#3 in the dayofweek field, it would mean the third Friday of the
month (6 = Friday and #3 means the third one in the month). Another example of 2#1 means the first Monday
of the month (2 = Monday and #1 means the first one of the month). Note that if you specify #5 and there is
no 5 of the given day of the week in the month, no firing will occur that month.
0 * 14-20 * * ?
the trigger would fire every minute from 2 PM to 7:59 PM, every day. As soon as you ran the CronTrigger
for this expression, if it was after 2 PM, it would start firing. It would do this every day indefinitely.
On the other hand, if you wanted this schedule to not start until the next day and to continue for only a couple
days, you could use the setStartTime() and setEndTime() methods on the CronTrigger to "time box" the
firings. Listing 5.2 illustrates an example that confines the CronTrigger to just a couple days.
Listing 5.2. You Can Use startTime and endTime with a CronTrigger
try {
// Create a CronTrigger
try {
// cron that fires every min from 2 8pm
79
80
CronTrigger trigger =
new CronTrigger("MyTrigger", null,
"0 * 14-20 * * ?");
scheduler.scheduleJob(jobDetail, trigger);
} catch (ParseException ex) {
logger.error("Couldn't parse cron expr", ex);
}
The example in Listing 5.2 uses the java.utl.Calendar to select a begin time and end time period for the
trigger. In the case of this example, the trigger will start firing the day after it's scheduled and will fire for only
two days after it starts to fire.
Use the startTime and endTime properties of the CronTrigger just as you would for SimpleTriggers.
For example, if you needed to execute a job every day at 5:30 PM, you could use the following code:
try {
80
81
TRiggerUtils makes it easier and more convenient to use triggers, without giving up too much of the
flexibility.
Just as with SimpleTriggers, you can specify CronTrigger expressions in the XML file, and the Quartz
Scheduler will use this information to schedule your jobs. This is extremely convenient if you want to declare
your job information externally to your application code. Listing 5.3 shows the quartz_jobs.xml file that is
used with the JobInitializationPlugin to load job information.
Listing 5.3. CronTriggers Can Be Specified in an XML File and Loaded with the JobInitializationPlugin
<quartz>
<job>
<job-detail>
<name>PrintInfoJob</name>
<group>DEFAULT</group>
<description>
A job that prints out some basic information.
</description>
<job-class>
org.cavaness.quartzbook.common.PrintInfoJob
</job-class>
</job-detail>
<trigger>
<cron>
<name>printJobInfoTrigger</name>
<group>DEFAULT</group>
<job-name>PrintInfoJob</job-name>
<job-group>DEFAULT</job-group>
The cron expression shown in Listing 5.3 is the same one from Listing 5.1. When Quartz loads the XML file,
it will schedule the PrintInfoJob (also listed in the XML file) to execute at 7:30 AM Monday through
Friday. More is said about the JobInitializationPlugin in Chapter 8.
81
82
Table 5.2. Cron Expressions for Schedules That Involve a Minute Frequency
Usage Expression
Fire every minute, starting at 5 PM and ending at 5:59 PM, every day 0 * 17 * *
?
Fire every 5 minutes, starting at 11 PM and ending at 11:55 PM, every day 0 0/5 23 *
* ?
Fire every 5 minutes, starting at 3 PM and ending at 2:55 PM, and fire
every 5 minutes starting at 6 PM and ending at 6:55 PM, every day 0 0/5 15,18
* * ?
Fire every minute, starting at 5 AM and ending at 5:05 AM, every day 0 0-5 5 * *
?
Usage
Expression
003**?
003?**
0 0 12 * * ?
0 15 10 * * ? 2005
82
83
Table 5.3. Cron Expressions for Schedules That Are Based on a Weekly and/or Monthly Frequency
Usage
Expression
0 15 10 15 * ?
0 15 10 L * ?
0 15 10 ? * 6L
Fire at 10:15 AM on every last Friday of every month during the years 2002, 2003, 2004, and 2005
0 15 10 ? * 6L 2002-2005
Fire at 12 PM (noon) every 5 days every month, starting on the first day of the month
0 0 12 1/5 * ?
0 11 11 11 11 ?
0 10,44 14 ? 3 WED
83
84
complete successfully, so you want to rerun the job immediately. In Quartz 1.5, several methods were added
to the triggerUtils class to make that easier. Listing 5.4 shows how to schedule a job to run only once, right
away.
Listing 5.4. You Can Use the TRiggerUtils to Execute an Immediate Job
try {
// Create a default instance of the Scheduler
scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
logger.info("Scheduler was started at " + new Date());
trigger.setName("FireOnceNowTrigger");
scheduler.scheduleJob(jobDetail, trigger);
} catch (SchedulerException ex) {
logger.error(ex);
}
}
}
In Listing 5.4, the makeImmediateTrigger() method of triggerUtils is used to execute an immediate job.
The first parameter is the number of times the trigger will fire. The second parameter is the interval between
the executions. The method signature is shown here for convenience:
The TRiggerUtils class offers many convenience methods for simplifying the use of triggers. Be sure to
check this utility class first to see if it has what you need. You'll see more examples of the triggerUtils
class throughout the book.
84
85
Job Storage
In the previous chapters, we didn't spend any time discussing where the job and trigger information for the
Scheduler was stored. You might have realized, however, that when you stopped the Scheduler, the
knowledge of which jobs had run and which ones had not was lost. In fact, all the information about the
running jobs was destroyed.
When the application was restarted, the trigger and job information was added back, and everything was fine
again. Suppose, however, that a job was scheduled for execution at 5 PM and the Scheduler was stopped five
minutes before that time, at 4:55 PM. What would happen if you restarted the Scheduler at 5:05 PM? Would
the Scheduler remember that it was supposed to fire the job at 5 PM? The answer is that it depends on which
type of JobStore you're using and how you have it configured.
By default, we've been using the memory storage mechanism in the examples from the past several chapters.
Both types are designed to serve the same purpose: to store job information. How they each go about it,
however, and what functionality they provide the Scheduler is very different.
Quartz provides an interface for all types of job storage. The interface located in the org.quartz.spi package
is called JobStore. All job storage mechanisms, regardless of where or how they store their information, must
implement this interface.
The JobStore interface has too many methods to list here, but the API for the JobStore interface can be
generalized into the following categories:
• Job-related API
85
86
• Trigger-related API
• Calendar-related API
• Scheduler-related API
Quartz users almost never access or see concrete classes that implement the JobStore interface; they are used
internally by the Quartz Scheduler to retrieve job and trigger information during runtime. It is a worthwhile
exercise, however, to familiarize yourself with each type so that you better understand the facilities these
provide on your behalf and to choose the right one for your Quartz application.
The memory job storage facility for Quartz is provided by a class called org.quartz.simpl.RAMJobStore,
which, as we said, implements the JobStore interface. The RAMJobStore is the out-of-the-box solution for
Quartz. By this, we mean that unless you change the configuration, this is the JobStore that will be used for
any Quartz application. Using this JobStore over the others brings several advantages.
First, the RAMJobStore is the easiest JobStore to configure: It's already configured for you. When you
download and install Quartz, it's configured to use the RAMJobStore as the storage mechanism. You can see
this in the default quartz.properties file, shown in Listing 6.1.
org.quartz.scheduler.instanceName = DefaultQuartzScheduler
org.quartz.scheduler.rmi.export = false
org.quartz.scheduler.rmi.proxy = false
org.quartz.scheduler.wrapJobExecutionInUserTransaction = false
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread = true
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
Listing 6.1 shows the default quartz.properties file that is included with the Quartz binary. When you don't
include a quartz.properties file of your own in your application, this is the properties file that gets used.
You can see from the last line in the default quartz.properties file that the RAMJobStore is the default
value for the configuration property named org.quartz.jobstore.class. Even if the
org.quartz.jobstore.class property is not set in quartz.properties, the RAMJobStore is the default
JobStore that is used. This is hardwired into the Scheduler factory initialization routine.
86
87
Another advantage to using the RAMJobStore is its speed. Because all the Scheduler information is stored in
the computer's memory, accessing this data is as fast as it gets. There are no out-of-process calls, no database
connectionsjust plain old simple memory access. It doesn't get any better.
You might remember from Chapter 4, "Scheduling Jobs," that jobs can be configured with a volatility
property. When the volatility property is set to false, the job will be persisted between application
shutdowns. This is not true when the RAMJobStore is being used; that behavior is explicitly reserved for
persistent JobStores.
Configuring your Quartz application to use the RAMJobStore is extremely easy. If you are using a custom
quartz.properties file, not the one that comes with the Quartz JAR file, add this line to your properties
file:
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
That's all you have to do to use the RAMJobStore. As we said, it doesn't get any easier.
If the purpose of the RAMJobStore is to store Scheduler information, how does that information get loaded
into memory in the first place? You can load job information in two ways. First, you can hardcode your jobs,
triggers, calendars, and listeners into the code itself. As Chapters 3, "Hello, Quartz," and 4, "Scheduling Jobs,"
pointed out, however, this is always a dangerous thing to do because maintenance can become a nightmare.
Any change, regardless of how trivial it is, requires a code change and recompile. Even if the change is a
change to the firing times, code has to be modified and recompiled. Not a big deal, you say? That might be
true for small, trivial applications, but this can become a real problem for larger numbers of jobs and triggers.
The second approach is to use the JobInitializationPlugin, which is discussed in detail in Chapter 8,
"Using Quartz Plug-Ins." This Quartz plug-in uses an XML file to load jobs, triggers, calendars, and
everything else that you need to load. The advantage to this approach is that a change requires a simple
change to the XML fileno code changes, no recompilation, just a text editor. Read Chapter 8 for more
information on Quartz plug-ins.
Disadvantage to RAMJobStore
"The RAMJobStore can't be all positive, right?" you ask. Well, that's true. We mentioned several of the
advantages of using a RAMJobStore. Now let's talk about the one big negative: Because a computer's memory
is volatile, when your Quartz application is stopped, it releases the memory back to the operating system. Of
course, along with everything else stored in that released memory is the scheduling information.
If your application requires Scheduler information to be maintained between application restarts, you need to
take a look at persistent JobStores.
87
88
As with RAMJobStore, persistent JobStores have both advantages and disadvantages. You should be careful
to understand the pros and cons before choosing a persistent JobStore. This section explains the differences
and the circumstances under which you would want to use a persistent JobStore.
Currently, Quartz provides two types of persistent JobStores, each one unique in its persistence mechanism.
All the persistent JobStores that ship with Quartz extend the
org.quartz.impl.jdbcjobstore.JobStoreSupport class.
The JobStoreSupport class is abstract and implements the JobStore interface, discussed earlier in the
chapter. It provides base functionality for all JDBC-based JobStores. Figure 6.1 shows the type hierarchy for
JobStores.
As Figure 6.1 illustrates, JobStoreSupport implements the JobStore interface and acts as a base class for the
two concrete persistent JobStores that Quartz offers.
88
89
Because the JobStoreSupport class is abstract, Quartz offers two different types of concrete persistent
JobStores, each one designed for a specific database environment and configuration:
• org.quartz.impl.jdbcjobstore.JobStoreTX
• org.quartz.impl.jdbcjobstore.JobStoreCMT
Both persistent JobStores are discussed shortly. For now, let's talk about the database that is required for
both versions.
Because the persistent JobStores in Quartz are dependent on JDBC, you must ensure that your platform
supports JDBC.
The persistent JobStores in Quartz are designed to be used with the following database platforms:
• Oracle
• MySQL
• MS SQL Server 2000
• HSQLDB
• PostgreSQL
• DB2
89
90
• Cloudscape/Derby
• Pointbase
• Informix
• Firebird
• Most other RDBMSs with a fully JDBC-compliant driver
The JobStoreCMT class is designed to be used when you want the application container to manage the
transactions for your JobStore and those transactions should participate within the transactional boundaries
that are managed by the container. This should be obvious from the "Container Managed Transactions
(CMT)" in the name.
90
91
Triggers stores as blobs (this is used when Quartz
users create their own custom trigger typeswith
JDBC, JobStore does not have specific
knowledge about how to store instances)
QRTZ_TRIGGER_LISTENERS Stores information about configured
triggerListeners
QRTZ_TRIGGERS Stores information about configured triggers
In Table 6.1, all tables start with the prefix QRTZ_. This is the default, but you can change it by providing an
alternate prefix in the quartz.properties file. Changing the prefix is necessary if you use multiple table sets
for different Scheduler instances. This would be the case if you need multiple nonclustered schedulers but
want to use a single database instance.
Quartz includes SQL scripts for all the supported database platforms. You can find the SQL scripts in the
<quartz home>/docs/dbTables directory, where <quartz_home> is the directory where you unzipped the
Quartz distribution.
About 18 different database platform scripts are available. This should cover just about any database that you
can come up with. If yours is not included, you can use one of the existing ones and modify it for your
database platform.
To install the required database tables, open the .sql file that is specifically designed for your database
platform and run the commands using your preferred query tool. With MS SQL Server, for example, you need
to run the commands in the file tables_sqlServer.sql using the Query Analyzer tool that comes with the
database. With certain databases (including MS SQL Server), you need to create an empty database before
running the commands. The SQL commands don't include creating the database. You should also pay careful
attention to any notes at the top of the SQL file. Usually, you must follow a few instructions before executing
the commands. For example, with the MS SQL Server SQL file, you need to modify this command at the top
of the file with the name that you gave the empty database when you created it:
USE [enter_db_name_here]
The SQL files create the necessary table structures and also put some basic constraints and indexes on the
tables. Later in this chapter, we talk about how you can improve performance by making some additional
changes to the structure.
Using JobStoreTX
The first persistent JobStore that we discuss is called JobStoreTX. The "TX" in
the name stands for "transaction." As we stated earlier, the JobStoreTX is
designed to be used in an environment in which you want Quartz to manage the
transactions. For example, if you are building a J2SE application and are not
using an application server such as WebLogic or JBoss, JobStoreTX would be the
right choice for a persistent JobStore.
91
92
In the last section, you saw how easy it is to configure the RAMJobStore. We
mentioned that one of the advantages to the RAMJobStore is its simple
configuration. We've already discussed what has to be done to the database to get
it ready; we now discuss what's needed to configure the Quartz application for a
JDBC JobStore.
To tell the Quartz runtime environment that you want to use a JobStore other
than the default RAMJobStore, you must configure several properties. It doesn't
matter what order you do them in, but they all must be done before you run the
application for the first time.
To inform the Scheduler that JobStoreTX should be used, you must add the
following line to the quartz.properties file:
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
Be sure to remove the RAMJobStore line (if present) when switching to the
JobStoreTX.
In the same way that the JDBC API relies on a JDBC driver designed specifically
for a database platform, Quartz relies on a DriverDelegate to communicate with
a given database. As the name implies, database calls from the Scheduler to the
JobStore are delegated to a preconfigured DriverDelegate instance. The
delegate is responsible for all communications with the JDBC driver and, thus,
the database.
Table 6.2. You Must Configure One of the DriverDelegates Classes for Your
Platform
Database Platform Quartz Delegate Class
Cloudscape/Derby org.quartz.impl.jdbcjobstore.Cloudsca
DB2 (version 6.x) org.quartz.impl.jdbcjobstore.DB2v6Del
DB2 (version 7.x) org.quartz.impl.jdbcjobstore.DB2v7Del
DB2 (version 8.x) org.quartz.impl.jdbcjobstore.DB2v8Del
HSQLDB org.quartz.impl.jdbcjobstore.PostgreS
MS SQL Server org.quartz.impl.jdbcjobstore.MSSQLDel
Pointbase org.quartz.impl.jdbcjobstore.Pointbas
92
93
PostgreSQL org.quartz.impl.jdbcjobstore.P
(WebLogic JDBC Driver) org.quartz.impl.jdbcjobstore.W
(WebLogic 8.1 with Oracle) org.quartz.impl.jdbcjobstore.o
Oracle org.quartz.impl.jdbcjobstore.o
After you determine which delegate you need based on your database platform, you need to add the following
to the quartz.properties file:
As an example, if you are using MS SQL Server as your database platform, you would need to add the
following to the properties file:
org.quartz.jobStore.driverDelegateClass=
org.quartz.impl.jdbcjobstore.MSSQLDelegate
Back when we first discussed the database tables for Quartz, we mentioned that all the tables had the prefix
QRTZ_ added to them. Under certain circumstances, you might need to create multiple sets of Quartz database
tables. In that situation, you would need to change the prefix for each set.
The prefixes for the table names are configured in the quartz.properties file using the property
org.quartz.jobStore.tablePrefix. To change the prefix, just set the property to a different value:
org.quartz.jobStore.tablePrefix = SCHEDULER2_
Make sure the table names all start with this prefix.
93
94
Table 6.3 shows the set of properties that can be used to tune the JobStoreTX.
Property
Default
org.quartz.jobStore.driverDelegateClass
Description: Driver delegates understand the particular dialects of various database systems.
org.quartz.jobStore.dataSource
Description: This is the name used in the DataSource configuration section of the quartz.properties file.
org.quartz.jobStore.tablePrefix
QRTZ_
Description: This is the prefix given to the set of database tables for this Scheduler. Schedulers can use
different tables from the same database if the prefixes are different.
org.quartz.jobStore.useProperties
False
Description: The "use properties" flag instructs the persistent JobStore that all values in JobDataMaps will be
Strings and, therefore, can be stored as name-value pairs instead of storing more complex objects in their
serialized form in the BLOB column. This is can be handy because you avoid the class-versioning issues that
can arise from serializing your non-String classes into a BLOB.
org.quartz.jobStore.misfireThreshold
60000
Description: The number of milliseconds the Scheduler will tolerate a trigger to pass its next-fire-time before
being considered misfired. The default value (if you don't make an entry of this property in your
configuration) is 60000 (60 seconds). This is not specific to JDBC-JobStore; it is also a parameter used by
RAMJobStore.
org.quartz.jobStore.isClustered
False
Description: Set this to true to turn on clustering features. This property must be set to true if you are having
multiple instances of Quartz use the same set of database tables.
org.quartz.jobStore.clusterCheckinInterval
15000
Description: Set the frequency (in milliseconds) at which this instance checks in with the other instances of
the cluster. This affects the quickness of detecting failed instances. It is used only when isClustered is set to
94
95
true.
org.quartz.jobStore.maxMisfiresToHandleAtATime
20
Description: This is the maximum number of misfired triggers the JobStore will handle in a given pass.
Handling many (more than a couple dozen) at once can cause the database tables to be locked long enough to
hamper the performance of firing other (not yet misfired) triggers.
org.quartz.jobStore.dontSetAutoCommitFalse
False
Description: Setting this parameter to true tells Quartz not to call setAutoCommit(false) on connections
obtained from the DataSource(s). This can be helpful in a few situations, such as if you have a driver that
complains if it is called when it is already off. This property defaults to false because most drivers require that
setAutoCommit(false) be called.
org.quartz.jobStore.selectWithLockSQL
Description: This must be a SQL string that selects a row in the LOCKS table and places a lock on the row. If it
is not set, the default is SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE, which works for
most databases. The {0} is replaced during runtime with the TABLE_PREFIX that you configured earlier.
org.quartz.jobStore.txIsolationLevelSerializable
False
Description: A value of true tells Quartz (when using JobStoreTX or CMT) to call
setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) on JDBC connections. This can be
helpful to prevent lock timeouts with some databases under high load and long-lasting transactions.
When using a JobStoreTX, you must specify the Datasource properties within the
quartz.properties file. This allows Quartz to create and manage the Datasource for you. Table
6.4 lists the Datasource properties that need to be configured when using a JobStoreTX.
95
96
Table 6.4. Available Properties for Configuring a Quartz Datasource
Property Required
org.quartz.dataSource.NAME.driver Yes
Description: Fully qualified name of your JDBC Driver class.
org.quartz.dataSource.NAME.URL Yes
Description: The connection URL (host, port, and so on) for connection to your database.
org.quartz.dataSource.NAME.user No
Description: The username to use when connecting to your database.
org.quartz.dataSource.NAME.password No
Description: The password to use when connecting to your database.
org.quartz.dataSource.NAME.maxConnections No
Description: The maximum number of connections that the DataSource can create in its pool of connections.
org.quartz.dataSource.NAME.validationQuery No
Description: An optional SQL query string that the DataSource can use to detect and replace failed/corrupt
connections. For example, an Oracle user might choose select table_name from user_tables, which is a
query that should never fail unless the connection is actually bad.
For each property listed in Table 6.4, you need to substitute a name of your choice for the NAME part of the
property. It doesn't matter what it is as long as it's the same value for all properties of the Datasource. This
name is used to uniquely identify the Datasource. If you need to configure multiple Datasources (which you
will do when using a JobStoreCMT), each Datasource should have a unique NAME value.
Listing 6.2 shows an example of a configured Datasource for the JobStoreTX that should be added to the
quartz.properties file.
org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz
org.quartz.dataSource.myDS.user = admin
org.quartz.dataSource.myDS.password = myPassword
org.quartz.dataSource.myDS.maxConnections = 10
After adding a Datasource section to the quartz.properties file like the one from Listing 6.2, you still
need to make it available to the Quartz JobStoreTX that has been configured. You do so by adding this
property to the properties file:
org.quartz.jobStore.dataSource = <DS_NAME>
The value for <DS_NAME> should match the name assigned in the Datasource configuration. Using the
Datasource example from Listing 6.2, you would add the following line to the quartz.properties file:
org.quartz.jobStore.dataSource = myDS
This value is then passed to the JobStoreSupport and becomes available to your JobStoreTX so that
connections can be retrieved and passed to the DriverDelegate instance.
96
97
Listing 6.3. Simple Startup Class Called from the Command Line to Start the Scheduler
When you use the SchedulerMain class from Listing 6.3 to test the JobStoreTX configuration, you should get
output similar to the following:
The log messages shown are using Log4j, so they might differ slightly from yours. A few things should be
obvious from the output. First, no triggers or jobs were found in the database. This is a very important and
sometimes confusing point. The database doesn't come loaded with any jobs or triggers: That's not possible
because it wouldn't know which ones to load for you. This is something that you'll have to do yourself. You
97
98
Earlier in the section , "Using Memory to Store Scheduler Information," we talked about how to load job and
trigger information into memory when using the RAMJobStore. So how are jobs and triggers loaded into the
database? Several methods exist for getting job information into the database:
We talked about the first two approaches back in the RAMJobStore section. When used with a JDBC
JobStore, the approach is not much different, with a few exceptions. First, you need to understand that when
using these two methods, the job information is in the database. Even if you stop the application, the
information remains in the database. Even if you took the JobInitializationPlugin out of your application,
the information would be in the database. In that sense, it is seeding the database with job information.
Chapter 8 covers the JobInitializationPlugin and Quartz plug-ins in general.
The last approach is probably the most interesting one. We haven't yet talked about the Quartz Web
application, but we do in Chapter 13, "Quartz and Web Applications." For now, you should know that the
Quartz Web application is a browser-based GUI that is designed to manage the Quartz Scheduler. It was
designed by Quartz users and presents a rather nice interface for adding jobs and triggers, starting and pausing
the Scheduler, and delivering other functionality.
The JobStoreCMT is designed to participate within the transactional boundaries of a container. This
means that the container creates a JTA transaction and makes it available to the JobStore. Quartz's
interaction with the JobStore stays within that transaction. If there are any problems, Quartz can
98
99
signal to the container that it wants the transaction to roll back by calling setRollbackOnly() on the
transaction.
As with the JobStoreTX and the RAMJobStore before that, the first step in using the JobStoreCMT is
to inform the Scheduler of your desire to use the JobStoreCMT. Similar to before, you do this by
setting the JobStore class property to the quartz.properties file:
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT
Be sure to remove the RAMJobStore line if it's present in the properties file.
You also need to choose the DriverDelegate as you did for the JobStoreTX. Quartz relies on a
DriverDelegate to communicate with a given database. The delegate is responsible for all
communications with the JDBC driver and, thus, the database.
Look back at Table 6.2 for a list of DriverDelegates and choose one based on your database
platform and environment. To add the MS SQL Server delegate to the quartz.properties file, add
the following line:
org.quartz.jobStore.driverDelegateClass= org.quartz.impl.jdbcjobstore.MSSQLDelegate
You can use several properties to help tune the JobStoreCMT. Table 6.5 shows the entire set.
99
100
configuration) is 60000 (60 seconds).
org.quartz.jobStore.isClustered False
Description: Set this to true to turn on clustering features. This property must be set to true if you are having
multiple instances of Quartz use the same set of database tables.
org.quartz.jobStore.clusterCheckinInterval 15000
Description: Set the frequency (in milliseconds) at which this instance checks in with the other instances of the
cluster. This affects the quickness of detecting failed instances.
org.quartz.jobStore.maxMisfiresToHandleAtATime 20
Description: This is the maximum number of misfired triggers the JobStore will handle in a given pass.
Handling many (more than a couple dozen) at once can cause the database tables to be locked long enough to
hamper the performance of firing other (not yet misfired) triggers.
org.quartz.jobStore.dontSetAutoCommitFalse False
Description: Setting this parameter to true tells Quartz not to call setAutoCommit(false) on connections
obtained from the DataSource(s). This can be helpful in a few situations, such as if you have a driver that
complains if it is called when it is already off. This property defaults to false because most drivers require that
setAutoCommit(false) be called.
org.quartz.jobStore.selectWithLockSQL SELECT *
FROM
{0}LOCKS
WHERE
LOCK_NAME
= ? FOR
UPDATE
Description: This must be a SQL string that selects a row in the LOCKS table and places a lock on the row. If it is
not set, the default is SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE, which works for most
databases. The {0} is replaced during runtime with the TABLE_PREFIX that you configured earlier.
org.quartz.jobStore.dontSetNonManagedTX ConnectionAutoCommitFalse False
Description: This is the same as the property org.quartz.jobStore.dontSetAutoCommitFalse, except that it
applies to the nonManagedTXDataSource.
org.quartz.jobStore.txIsolationLevelSerializable False
Description: A value of true tells Quartz (when using JobStoreTX or CMT) to call
setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE) on JDBC connections. Th s can be
helpful to prevent lock timeouts with some databases under high load and long-lasting transactions.
org.quartz.jobStore.txIsolationLevelReadCommitted False
Description: When set to true, this property tells Quartz to call
setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED) on the nonmanaged JDBC
connections. This can be helpful to prevent lock timeouts with some databases (such as DB2) under high load
and long-lasting transactions.
100
101
(such as the scheduling of the job) is performed using a JDBC connection that has its
transaction under the control of the container. This design allows user interactions with
Quartz to be part of their "larger" transaction, without JobStoreCMT having to use the
application server's transaction manager (for example, via UserTransaction) to create
and end transactions when it is doing its own internal work (such as the internal handling
misfired triggers). It would be an improvement (from a configuration point of view) if
JobStoreCMT did make use of UserTransaction because then only one datasource
would be needed. However, when compared with the other feature requests and needed
improvements, making this change has never become a top priority for the team, and
JobStoreCMT continues to require two datasources.
For the most part, we set up the nonmanaged DataSource in the same way that we did for
JobStoreTX, except that we add one line to specify that this is the nonManagedTXDataSource:
org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz
org.quartz.dataSource.myDS.user = admin
org.quartz.dataSource.myDS.password = myPassword
org.quartz.dataSource.myDS.maxConnections = 10
This sets up the nonmanaged Datasource and lets the JobStore know that the
nonManagedTXDataSource is called "myDS."
The second Datasource needs to be configured as a managed Datasource. This means that Quartz
uses a Datasource that the container has already created to interact with the database while
performing Scheduler functions. When Quartz grabs a connection from the Datasource, there
should already be a JTA transaction in progress for Quartz to schedule jobs and triggers. For
example, the code calling into Quartz could be within a method on a SessionBean that has its
transaction descriptor set to REQUIRED. Another approach would be for the client program to start a
transaction directly through the use of javax.transaction.UserTransaction.
As with the nonmanaged Datasource, the configuration for the container-managed Datasource
goes in the quartz.properties file. The following example illustrates how to set up the managed
Datasource:
org.quartz.dataSource.NAME.jndiURL=jdbc/quartzDS
org.quartz.dataSource.NAME.java.naming.factory.initial=
weblogic.jndi.WLInitialContextFactory
org.quartz.dataSource.NAME.java.naming.provider.url=t3://localhost:7001
org.quartz.dataSource.NAME.java.naming.security.principal=weblogic
org.quartz.dataSource.NAME.java.naming.security.credentials=weblogic
101
102
Table 6.6 lists the properties that are available for the managed Datasource.
Using the properties from Table 6.6, here's an example of configuring the managed Datasource in
quartz.properties.
org.quartz.dataSource.WL.jndiURL = OraDataSource
org.quartz.dataSource.WL.jndiAlwaysLookup = DB_JNDI_ALWAYS_LOOKUP
org.quartz.dataSource.WL.java.naming.factory.initial = weblogic.jndi.WLInitialContextFactory
org.quartz.dataSource.WL.java.naming.provider.url = t3://localhost:7001
org.quartz.dataSource.WL.java.naming.security.principal = weblogic
org.quartz.dataSource.WL.java.naming.security.credentials = weblogic
When using Quartz with persistent JobStores, the biggest area of concern has to be the interaction with the
relational database. Database I/O (just like file I/O) is usually not very fast. You can improve performance by
doing things such as tuning the SQL, adding indexes, and manipulating tables and columns. Because
performance concerns were already taken into account in the writing of Quartz, you don't want to dive right
into doing these things to Quartz without having an actual performance problem, trying to solve it through a
configuration setting, and trying everything possible not to manipulate source code. The good news is that
Quartz is open source and you have complete insight into what it's doing and how it's doing it. If you don't
like how it queries the database, it's your prerogative to fix it. Before you take this route, however, be sure to
check with the users and developers on the Quartz forum to see if others have had the problem and explore the
recommended suggestions.
One very easy (and very effective) way to improve performance is to make sure the tables have indexes
created on all the appropriate columns. Some of the database-creation scripts that ship with Quartz already
have the commands for creating indexes. If yours does not, you can simply refer to those defined at the
bottom of tables_oracle.sql and translate any syntax changes that are necessary for your RDBMs.
Whatever you do, if you make changes that improve performance, be sure to offer them back to the
community and the Quartz project.
102
103
So what happens if you need a totally different type of JobStore? You're going to need to create a new type.
This section discusses several approaches and offers ideas for how you can create a new JobStore when the
provided solutions just don't fill the need.
Regardless of whether they use a database, a file system, or even just memory, all JobStores must implement
the JobStore interface. New JobStores that you create are no exception. Looking back through this chapter,
you can see that RAMJobStore directly implements the JobStore interface, the JDBC JobStores subclass
JobStoreSupport, which itself implements the JobStore interface.
The JobStore interface has 40 methods that must be implemented by any JobStore implementation, yours
included. How you implement those methods depends entirely on the type of JobStore that you are building.
That doesn't mean that your JobStore will have only 40 methods; this is just the minimum that the interface
requires. These 40 methods represent the public contract between a JobStore and the Scheduler.
Let's pick one of the methods and talk about it for a second. Let's pick the JobStore interface method:
The Scheduler calls the JobStore's schedulerStarted() method to inform the JobStore that the Scheduler
has been started. If you look at the implementation in RAMJobStore, you can see that nothing is done inside
this method for this implementation:
However, if you look at the implementation for the two JDBC JobStores, you can see that some work has to
be carried out when the Scheduler is first started:
if (isClustered()) {
clusterManagementThread = new ClusterManager(this);
clusterManagementThread.initialize();
}
else {
try {
recoverJobs();
} catch (SchedulerException se) {
103
104
throw new SchedulerConfigException(
"Failure occurred during job recovery.", se);
}
}
misfireHandler = new MisfireHandler(this);
misfireHandler.initialize();
}
Each JobStore implementation will be unique, and the functionality that is carried out within the interface
method will vary. If you are serious about creating your own JobStore, you should review the source code for
RAMJobStore to fully understand the responsibilities a JobStore has. The RAMJobStore should be used as a
guide for any custom JobStore you need.
Quartz listeners are a type of extension point in which you, as a Quartz user, can extend the framework and
customize it to do new things. This customization takes place within the listener class implementations that
we show you how to build in this chapter.
Listeners are not the only extension points in the framework. Along with plug-ins and a few other
customization options, listeners provide a simple means of customizing the framework and making it do what
you need it to do. Because the extension points for listeners are supported through a publicized interface, you
don't have to worry about creating your own branch of the code that then is unsupportable.
Implementing a Listener
In the discussion that follows, the approach for implementing a listener is generally the same for all three
types. The steps can be generalized into the following:
2.
104
105
Implement all the methods of the listener interface with logic that is specific to your application.
The listener is a Java interface and must be implemented by a concrete class. The class doesn't have to be a
specialized class created just for this purpose; it can be any Java class that you want to receive the callback
methods on. In keeping with good program design, you should be careful to keep tight cohesion but loose
coupling. Seriously consider which class you decide to implement the listener with; this is important from an
overall design perspective.
Because listeners are ordinary Java interfaces, every method must be implemented in your listener
implementation class. If there are any methods in the listener interface that you are not interested in, you are
allowed to have an empty body for the method; however, you still must provide a valid method
implementation of it. For example, the following code snippet shows an empty method body for one of the
SchedulerListener methods:
To receive the callback methods, the Scheduler must be aware of the listener instance. Figure 7.1 illustrates
the process of registering a listener with the Scheduler and receiving the callbacks.
Figure 7.1. Listeners are registered with and receive callbacks from the Scheduler.
The JobListener and triggerListener can be registered as either a global or nonglobal listener. A global
listener is one that receives event notifications for all jobs/triggers. A nonglobal listener (or just a standard
105
106
listener) receives event notifications for only those jobs or triggers that have registered listeners on them.
Whether you register your listener as a global or nonglobal listener depends on your specific application
needs. We provide examples of both ways in the following sections. Another way to look at global and
nonglobal listeners comes from the creator of the Quartz framework. James House describes global and
nonglobal listeners this way:
Global listeners are active in the sense that they are eagerly seeking every possible event in
order to perform their task upon it. Generally, the work done by global listeners [is] not
specific to the particular job or trigger. Nonglobal listeners are generally passive in the sense
that they do nothing until a trigger fires or job executes that has specifically named that
listener. Hence, nonglobal listeners are more likely than global listeners to modify or add to
the work performed by the jobsomewhat like a decorator of the well-known decorator design
pattern.
The methods in the JobListener interface are fairly self-explanatory. However, we should make a few points
about them.
The getName() method returns a String that represents the name of the JobListener. For listeners that are
registered as global, the getName() method is primarily used for logging. However, for JobListeners that
are referenced by specific jobs, the listener name registered with the JobDetail must match the value returned
from the getName() method on the listener instance. This will be more obvious after you see a few examples.
The Scheduler calls this method when the JobDetail is about to be executed.
106
107
The jobExecutionVetoed() Method
The Scheduler calls this method when the JobDetail was about to be executed but a triggerListener
vetoed the execution.
package org.cavaness.quartzbook.chapter7;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.quartz.JobListener;
The JobListener in Listing 7.2 prints a log message, which, obviously, is a basic use of the Listener. The
logic that you implement is entirely up to you and your application needs. You might want to send an e-mail
when a job finishes successfully or schedule another job after one gets vetoedyou have the freedom to perform
just about any action from within the callback method.
Earlier we stated that JobListeners (and triggerListeners) can be either global or nonglobal. Notice that
we didn't have to know in advanced whether the JobListener in Listing 7.2 was a global or nonglobal
listener: We just implemented the interface and provided the listener methods. Listing 7.3 illustrates how to
use the SimpleJobListener from Listing 7.2 and register it as global JobListener.
107
108
Listing 7.3. Using the SimpleJobListener as a Global JobListener
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cavaness.quartzbook.common.PrintInfoJob;
import org.quartz.JobDetail;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
try {
example.startScheduler();
} catch (SchedulerException ex) {
logger.error(ex);
}
}
/*
* Set up a trigger to start firing now, with no end
* date/time, repeat forever and have
* 10 secs (10000 ms) between each firing.
*/
Trigger trigger = TriggerUtils.makeSecondlyTrigger(10);
trigger.setName("SimpleTrigger");
trigger.setStartTime(new Date());
scheduler.addGlobalJobListener(jobListener);
// Start the scheduler
scheduler.start();
logger.info("Scheduler was started at " + new Date());
}
}
108
109
The code in Listing 7.3 should be pretty straightforward by now. A JobDetail and trigger are created and
registered with a Scheduler instance, just as we've done many times before.
A SimpleJobListener from Listing 7.2 is instantiated and registered with the Scheduler as a global
JobListener by calling the addGlobalJobListener() method. Finally, the Scheduler is started.
Because we set up a single job (the PrintInfoJob), we get callbacks for only that JobDetail. However, if we
had scheduled another job, we would also see callback log messages for the second job because the listener
was configured as a global listener.
You can also use the SimpleJobListener from Listing 7.2 as a nonglobal JobListener. To do this, you
would only need to modify the code in the startScheduler() method from Listing 7.3. Listing 7.4 illustrates
the small change that would need to be made.
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cavaness.quartzbook.common.PrintInfoJob;
import org.quartz.JobDetail;
import org.quartz.JobListener;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
try {
example.startScheduler();
} catch (SchedulerException ex) {
logger.error(ex);
}
}
try {
// Create the scheduler and JobDetail
scheduler = StdSchedulerFactory.getDefaultScheduler();
JobDetail jobDetail =
new JobDetail("PrintInfoJob",
Scheduler.DEFAULT_GROUP,
PrintInfoJob.class);
/*
* Set up a trigger to start firing now, with no end
109
110
trigger.setName("SimpleTrigger");
trigger.setStartTime(new Date());
Listing 7.4 is very similar to the code from Listing 7.3. Because the JobListener is being registered as a
nonglobal listener, you have to call the addJobListener() method on the Scheduler instead of the
addGlobalJobListener() method. For a nonglobal JobListener, it should be added to the Scheduler before
any JobDetail that references it is added using the schedulerJob() or addJob() methods.
Next, the name of the JobListener is set on the JobDetail. Notice that the JobListener instance is not set;
just the name is. This is done by calling the addJobListener() method and passing in the name. The name
passed into the addJobListener() method must match the name returned from the listener getName()
method. If the Scheduler can't find a listener with that name, it throws a SchedulerException.
110
111
As with the previous JobListener, the getName() method on the TRiggerListener interface returns a
String that represents the name of the listener. For nonglobal TRiggerListeners, the name given in the
addTriggerListener() method should match the value returned from the listener's gettName() method.
The Scheduler calls this method when the trigger associated with the listener has fired and the execute()
method is about to be called on the job. In the case of a global triggerListener, this method is called for all
triggers.
The Scheduler calls this method when the trigger has fired and the job is about to be executed. The
triggerListener is given a chance to veto the execution of the job. If this method returns true, the job will
not be executed for this trigger firing.
The Scheduler calls this method when the trigger has misfired. As the JavaDocs for this method point out, you
should be careful about performing long-lasting logic in this method: Doing so could cause a domino effect
when there are many misfired triggers. You should keep the logic in this method to a minimum.
111
112
The TRiggerComplete() Method
The Scheduler calls this method when the trigger has fired and the job has finished executing. It doesn't mean
that the trigger will not fire againjust that the current firing of the trigger (and subsequent job execution) has
ended. The trigger might still have future firing times.
package org.cavaness.quartzbook.chapter7;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobExecutionContext;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
112
113
Just as with the JobListener in Listing 7.2, the triggerListener in Listing 7.6 is rudimentary. It merely
prints a log message when the Scheduler invokes the method. The code in Listing 7.7 tests the simple
TRiggerListener.
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cavaness.quartzbook.common.PrintInfoJob;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
scheduler.addGlobalTriggerListener(triggerListener);
/*
* Set up a trigger to start firing now, with no end
* date/time, repeat forever and have 10 secs
* (10000 ms) between each firing.
*/
Trigger trigger = TriggerUtils.makeSecondlyTrigger(10);
trigger.setName("SimpleTrigger");
trigger.setStartTime(new Date());
113
114
}
}
Listing 7.7 shows how to register the SimpleTriggerListener as a global triggerListener. It looks almost
identical to the code from Listing 7.3 that registered a global JobListener. You just need to call the
addGlobalTriggerListener() method and pass it the triggerListener instance.
To register a nonglobal triggerListener, you need to call the addTriggerListener() method and pass the
triggerListener instance. Then call the addTriggerListener() method on the trigger instance and pass the
name of the triggerListener.
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cavaness.quartzbook.common.PrintInfoJob;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerListener;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
try {
example.startScheduler();
} catch (SchedulerException ex) {
logger.error(ex);
}
}
114
115
scheduler.addTriggerListener( triggerListener );
/*
* Set up a trigger to start firing now, with no end
* date/time, repeat forever and have 10 secs
* (10000 ms) between each firing.
*/
Trigger trigger = TriggerUtils.makeSecondlyTrigger(10);
trigger.setName("SimpleTrigger");
trigger.setStartTime(new Date());
The same warning that we mentioned earlier for nonglobal JobListeners applies here: You should add the
TRiggerListener to the Scheduler before setting it on the trigger instance and storing the trigger.
As you can see from the methods listed in Listing 7.9, the SchedulerListener is notified when events occur
at the Scheduler level, whether it's the addition or removal of a job in the Scheduler or if the Scheduler
encounters a serious error. These events are more about the management of the Scheduler than a particular job
or trigger.
115
116
The Scheduler calls these methods when a new JobDetail is scheduled or unscheduled, respectively.
The Scheduler calls this method when a trigger has reached the state that it will never fire again. Unless the
job has been set up as durable, it will be removed from the Scheduler.
The Scheduler calls this method when a trigger or group of triggers has been paused. If it's a trigger group, the
triggerName parameter will be null.
The Scheduler calls this method when a trigger or group of triggers has been unpaused (or resumed). If it's a
trigger group, the triggerName parameter will be null.
The Scheduler calls this method when a JobDetail or a group of JobDetails has been paused.
The Scheduler calls this method when a job or a group of jobs has been unpaused (or resumed). If it's a job
group, the jobName parameter will be null.
The Scheduler calls this method when a serious error has occurred during the normal runtime of the
Scheduler. The type of error can vary, but a few examples are listed here:
The Scheduler calls this method to inform the SchedulerListener that the Scheduler is shutting down.
116
117
Listing 7.10. A Simplified SchedulerListener Implementation
package org.cavaness.quartzbook.chapter7;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.Trigger;
if (triggerName == null) {
// triggerGroup is being unscheduled
logger.info(triggerGroup + " is being unscheduled");
} else {
logger.info(triggerName + " is being unscheduled");
}
}
if (triggerName == null) {
// triggerGroup is being unscheduled
logger.info(triggerGroup + " is being paused");
} else {
logger.info(triggerName + " is being paused");
}
}
if (triggerName == null) {
// triggerGroup is being unscheduled
logger.info(triggerGroup + " is now resuming");
} else {
logger.info(triggerName + " is now resuming");
}
}
117
118
if (jobName == null) {
// triggerGroup is being unscheduled
logger.info(jobGroup + " is now resuming");
} else {
logger.info(jobName + " is now resuming");
}
}
As with the previous examples, the SimpleSchedulerListener in Listing 7.10 provides simple
implementations for the listener methods. Listing 7.11 uses the SimpleSchedulerListener class.
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.cavaness.quartzbook.common.PrintInfoJob;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.SchedulerListener;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
scheduler.addSchedulerListener(schedulerListener);
118
119
/*
* Set up a trigger to start firing now, with no end
* date/time, repeat forever and have 5 secs
* between each firing.
*/
Trigger trigger = TriggerUtils.makeSecondlyTrigger(5);
trigger.setName("SimpleTrigger");
trigger.setStartTime(new Date());
We changed some things in Listing 7.11 from the previous examples, to actually cause more of the
SchedulerListener methods to be called. In Listing 7.11, the Scheduler is created and started before the job
is registered. This is so the jobScheduled() method gets called when the job is scheduled. We also changed
the trigger to repeat two times instead of running indefinitely. This forces the triggerFinalized() method to
be called because there were no more firing times for the trigger. Other than these contrived conditions, using
the SchedulerListener is the same as using the job or trigger listeners.
As with the other types of Quartz listeners, you must create a concrete class that implements the
FileScanListener interface. Only one method must be implemented:
119
120
Listing 7.12. A Simple FileScanListener Implementation
package org.cavaness.quartzbook.chapter7;
import java.io.File;
import java.sql.Timestamp;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
Obviously, you would want to do something more interesting than just writing out a log message, but you get
the point with the simple example in Listing 7.12. We also need to schedule the FileScanJob that uses the
new type of listener. Listing 7.13 shows how to schedule the FileScanJob.
package org.cavaness.quartzbook.chapter7;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.jobs.FileScanJob;
try {
Scheduler scheduler = example.createScheduler();
example.scheduleJob(scheduler);
scheduler.start();
120
121
return StdSchedulerFactory.getDefaultScheduler();
}
scheduler.scheduleJob(jobDetail, trigger);
}
}
The program in Listing 7.13 is like almost every other Quartz application that needs to schedule a job. The
FileScanJob requires two parameters: the FILE_NAME of the file to watch and the name of the
FileScanListener (FILE_SCAN_LISTENER_NAME). The values for these two parameters are stored in the
JobDataMap so the FileScanJob can access them.
The only "gotcha" that you should watch out for is to make sure that you add an instance of the
FileScanListener to the SchedulerContext. This is done in Listing 7.13 and shown in the following code
fragment:
scheduler.getContext().put("SimpleFileScanListener",
new SimpleFileScanListener());
This is necessary because the FileScanJob gets a reference to the SchedulerContext and looks for a
FileScanListener using the name that was set in the JobDataMap:
jobDataMap.put(FileScanJob.FILE_SCAN_LISTENER_NAME,
"SimpleFileScanListener");
If you are confused, don't worry: Look at the source code for the org.quartz.jobs.FileScanJob class.
That's one of the best things about open source software.
121
122
Starting with Quartz 1.5, you are able to specify listeners in the job-definition file, otherwise known as the
quartz_jobs.xml file. Listing 7.14 shows an example of using a global listener.
Listing 7.14. Quartz Listeners Can Be Implemented with the quartz_jobs.xml File
<quartz>
<job-listener
class-name="org.cavaness.quartzbook.chapter7.SimpleJobListener"
name="SimpleJobListener">
</job-listener>
<job>
<job-detail>
<name>PrintInfoJob</name>
<group>DEFAULT</group>
<job-listener-ref>SimpleJobListener</job-listener-ref>
<job-class>
org.cavaness.quartzbook.common.PrintInfoJob
</job-class>
</job-detail>
<trigger>
<simple>
<name>printJobTrigger</name>
<group>DEFAULT</group>
<job-name> PrintInfoJob</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-09-13 6:10:00 PM</start-time>
<! repeat indefinitely every 10 seconds >
<repeat-count>-1</repeat-count>
<repeat-interval>10000</repeat-interval>
</simple>
</trigger>
</job>
</quartz>
You see in Listing 7.14 the additional <job-listener> element with the two required attributes:
<job-listener
class-name="org.cavaness.quartzbook.chapter7.SimpleJobListener"
name="SimpleJobListener">
The class-name property identifies the fully qualified name of the listener class. The name attribute assigns a
logical name to the listener used in the <job-detail> element.
The next step is to define a <job-listener-ref> element in the <job-detail> element in the same file for
each job that you want the listener on. The value of the element must match the name property of one of the
defined <job-listener> elements in the file.
122
123
After you have done that, make sure you've configured the Scheduler to use the JobInitializationPlugin
by setting the properties in the quartz.properties file. Quartz plug-ins are discussed in detail in the next
chapter. For now, just add the following lines to your quartz.properties file:
org.quartz.plugin.jobInitializer.class =
org.quartz.plugins.xml.JobInitializationPlugin
org.quartz.plugin.jobInitializer.overWriteExistingJobs = true
org.quartz.plugin.jobInitializer.failOnFileNotFound = true
org.quartz.plugin.jobInitializer.validating=false
Then name your XML file quartz_jobs.xml and put the file in your classpath.
The other tip is to make sure you have a no-arg constructor for your listener. Under certain
conditions, the Quartz framework won't complain, but when using this declarative approach, you
will get an error. It's better just to declare the no-arg constructor and always be safe.
123
124
The sequence of listener method calls is fixed. As Figure 7.2 shows, the same thread that calls the job's
execute() method is used to call the JobListener and triggerListener methods before and after the
execution. This is important to know if you are using any sort of third-party thread-management tools or have
decided to implement your own thread-pool management. It can also have a negative impact on performance
if you implement long-running logic in the listener methods. Because the thread that invokes the listener
methods is the same worker thread executing jobs, you should not implement listener methods that are
complicated and that take a long time to complete. Keep their execution times to a minimum.
These are only a few ideas. Quartz listeners provide a way for you to be notified programmatically when
specific events take place during the runtime of your Quartz application. What you choose to do with that
knowledge, if anything, is entirely up to you.
124
125
What Is a Plug-In?
If you have used other open source frameworks such as Apache Struts, you're already familiar with concept of
plug-ins and their use. Quite simply, a Quartz plug-in is a Java class that implements the
org.quartz.spi.SchedulerPlugin interface and is registered as a plug-in with the Scheduler. The plug-in
interface contains three methods and is shown in Listing 8.1.
The methods of the SchedulerPlugin are called during the initialization and startup of a Scheduler. The
Scheduler calls these methods on every plug-in that is registered. The following sections describe when each
method of the plug-in is called.
The initialize() method is called during the creation of the Scheduler. When the getScheduler() method
is called on the StdSchedulerFactory, the factory calls the initialize() method on all the registered
plug-ins.
Each plug-in is registered with a unique name. This given name and Scheduler instance is included in the call
to the initialize() method. You should take whatever action is necessary to initialize your plug-in. For
example, your plug-in might need to read and parse data from a file or database.
125
126
The Scheduler instance calls the start() method to let the plug-in know that it can perform any startup
actions it needs. For example, if you have jobs to schedule, this is the time to schedule them.
The shutdown() method is called to inform the plug-in that the Scheduler is shutting down. This is an
opportunity for the plug-in to clean up any open resources that are open. For example, database connections or
open files should be closed.
The JobInitializationPlugin
The Quartz framework includes a plug-in for loading job and trigger information from an XML file. The
plug-in is org.quartz.plugins.xml.JobInitializationPlugin and was discussed briefly back in Chapter
3, "Hello, Quartz." When you use this plug-in, the Quartz framework searches for a file called
quartz_jobs.xml and attempts to load job and trigger information from this file.
126
127
As Chapter 3 explained, this plug-in is very convenient when your application requirements don't involve
loading job information from a database. It's also very useful during development and testing because you can
quickly configure which jobs and triggers are to be fired. That is, arguably, it's easier to modify an XML file
than a set of database tables.
A nice extension to this idea of loading job and trigger information from an XML file would be to have a
directory where you can store job XML files, and, by using a plug-in, the Scheduler would load whatever job
files were present. This would enable you to conveniently add or remove jobs at Scheduler startup by simply
adding or removing them from the specified directory. In the rest of this chapter, we show you how to build
this plug-in.
We call this new plug-in the JobLoaderPlugin. Listing 8.2 shows the JobLoaderPlugin class.
Listing 8.2. A Quartz SchedulerPlugin that Loads Multiple Job Files from a Directory
package org.cavaness.quartzbook.chapter8;
import java.io.File;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Scheduler;
import org.quartz.SchedulerConfigException;
import org.quartz.SchedulerException;
import org.quartz.spi.SchedulerPlugin;
import org.quartz.xml.JobSchedulingDataProcessor;
127
128
public JobLoaderPlugin() {
}
this.pluginName = name;
this.scheduler = scheduler;
// Check directory
if (getJobsDirectory() == null
|| !(dir =
new File(getJobsDirectory())).exists()) {
throw new SchedulerConfigException(
"The jobs directory was missing "
+ jobsDirectory);
}
logger.info("Loading jobs from " + dir.getName());
JobSchedulingDataProcessor processor =
new JobSchedulingDataProcessor(
true, isValidateXML(), isValidateSchema());
128
129
try {
processor.processFileAndScheduleJobs(
fileName, scheduler, true);
The real work of the JobLoaderPlugin in Listing 8.2 is done in just two methods: initialize() and
start(). Both are required by the SchedulerPlugin interface. The rest of the methods are just setXXX and
getXXX methods to fulfill the JavaBean contract because private properties have been declared.
As you can see, the initialize() method, which is called by the Scheduler, calls the private loadJobs()
method. The loadJobs() method uses the jobsDirectory that was passed in from the quartz.properties
file to retrieve all XML files stored in that directory. The plug-in doesn't try to schedule the jobs yet because
the Scheduler isn't fully instantiated when the plug-in's initialize() method is called. The
JobLoaderPlugin simply holds on to an array of File objects, waiting for the start() method to be called.
We also hold on to the instance of the Scheduler so that we have access to it when the start() method is
called.
129
130
The JobLoaderPlugin start() Method
When the Scheduler calls the start() method in the JobLoaderPlugin, the start() method calls
processJobs(). The processJobs() method loops through the array of job files and loads each one into the
Scheduler instance.
When the processJobs() method is complete, all job files from the specified jobsDirectory should have
been loaded and scheduled.
To register your plug-in in the quartz.properties file, create a property in the quartz.properties file
using the following format:
org.quartz.plugin.<pluginName>.class=
<fully_qualified_class_name_of_plugin>
Quartz looks for all entries in the properties file that have the key:
org.quartz.plugin.<pluginName>.class
Then it attempts to create an instance of the class that is on the right side of the equals sign and assumes that
it's a plug-in. You name the plug-in by providing a unique name in the <pluginName> field.
Listing 8.3 shows a quartz.properties file for use with the JobLoaderPlugin.
#===============================================================
# Configure Main Scheduler Properties
#===============================================================
org.quartz.scheduler.instanceName = QuartzScheduler
org.quartz.scheduler.instanceId = AUTO
#===============================================================
# Configure ThreadPool
#===============================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
130
131
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
#===============================================================
# Configure JobStore
#===============================================================
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
#===============================================================
# Configure Plugins
#===============================================================
org.quartz.plugin.jobLoader.class =
org.cavaness.quartzbook.chapter8.JobLoaderPlugin
org.quartz.plugin.jobLoader.jobsDirectory =
c:\\quartz-book\\sample\\chapter8
You should have seen most of the settings in the quartz.properties file from Listing 8.3. The last section is
where the JobLoaderPlugin is registered.
During initialization and startup, the Quartz Scheduler loads the properties from the quartz.properties file.
You must follow a particular format when specifying plug-ins in the quartz.properties file. The format is
shown here:
Looking back at Listing 8.3, you can see that our JobLoaderPlugin has this format:
org.quartz.plugin.jobLoader.class=
org.cavaness.quartzbook.chapter8.JobLoaderPlugin
The name given to the plug-in is jobLoader and is arbitrary. We could have used any name, as long as it was
unique from any other registered plug-in. On the right side of the equals sign, you must specify the fully
qualified name of the plug-in class. This class must be in the classpath or available to the classloader.
Most plug-ins require configuration values and, keeping with good programming practices, we don't want to
have to hard-code these values into the plug-in class. Quartz provides a mechanism to pass parameter values
to your plug-in class by providing these parameter values in the quartz.properties file.
The Scheduler finds all other properties that match a format of this:
131
132
<plugin prefix>.<plugin name>.<property name>=<someValue>
It treats them as JavaBean properties on the plug-in class. From Listing 8.3, this means that this property is
turned into a setJobsDirectory() method call with the string value of
c:\\quartz-book\\sample\\chapter8 as a parameter for the method:
org.quartz.plugin.jobLoader.jobsDirectory=
c:\\quartz-book\\sample\\chapter8
You can have as many properties as you need to configure your plug-in.
The Quartz framework converts the property values to the type specified in the plug-in, assuming that it's a
primitive type. For example, you can specify properties of type int and expect Quartz to convert the String
from the quartz.properties file to an int. The framework, however, will not convert 1 to an integer class.
The JobLoaderPlugin looks for all XML files in the specified directory and assumes that each file is a valid
Quartz jobs file. By "valid," we mean that the XML file adheres to the latest job-scheduling XSD file, which
at the time of this writing is job_scheduling_data_1_5.xsd.
To make the JobLoaderPlugin more useful, we put each job, along with its job detail and trigger information,
in a single XML file. This enables us to add and remove complete jobs just by putting the file into the
directory or taking it out. This is very helpful in a development environment when you want to test only
certain jobs. A single job XML file is shown in Listing 8.4.
132
133
<quartz xmlns="http://www.opensymphony.com/quartz/JobSchedulingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opensymphony.com/quartz/JobSchedulingData
http://www.opensymphony.com/quartz/xml/job_scheduling_data_1_5.xsd"
version="1.5">
<job>
<job-detail>
<name>PrintInfoJob1</name>
<group>DEFAULT</group>
<job-class>
org.cavaness.quartzbook.chapter3.ScanDirectoryJob
</job-class>
<volatility>false</volatility>
<durability>false</durability>
<recover>false</recover>
<job-data-map allows-transient-data="true">
<entry>
<key>SCAN_DIR</key>
<value>c:\quartz-book\input1</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<simple>
<name>trigger1</name>
<group>DEFAULT</group>
<job-name>PrintInfoJob1</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-07-30T16:04:00</start-time>
</quartz>
The job file is Listing 8.4 contains all the information necessary for the JobLoaderPlugin to schedule the job.
This file also contains an entry for the JobDataMap, which is available to the job class at runtime. The
example in Listing 8.4 uses a configured SimpleTrigger to schedule an infinitely repeating trigger that fires
every 10 seconds. To further test the plug-in, we created a second job file, which differs from the first in some
small way. Listing 8.5 shows the second job file.
<quartz xmlns="http://www.opensymphony.com/quartz/JobSchedulingData"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opensymphony.com/quartz/JobSchedulingData
http://www.opensymphony.com/quartz/xml/job_scheduling_data_1_5.xsd"
version="1.5">
<job>
133
134
<job-detail>
<name>PrintInfoJob2</name>
<group>DEFAULT</group>
<job-class>
org.cavaness.quartzbook.chapter3.ScanDirectoryJob</job-class>
<volatility>false</volatility>
<durability>false</durability>
<recover>false</recover>
<job-data-map allows-transient-data="true">
<entry>
<key>SCAN_DIR</key>
<value>c:\quartz-book\input2</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<simple>
<name>trigger2</name>
<group>DEFAULT</group>
<job-name>PrintInfoJob2</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-07-30T16:04:00</start-time>
<!-- repeat indefinitely every 10 seconds -->
<repeat-count>-1</repeat-count>
<repeat-interval>60000</repeat-interval>
</simple>
</trigger>
</job>
</quartz>
The second job file in Listing 8.5 differs only slightly from the one in Listing 8.4. We've changed the
directory for the job, which is scanned and changed the trigger schedule. The point here is that you can have
multiple jobs in the jobs directory, and the JobLoaderPlugin will load them all and schedule them
individually with the Scheduler.
To get around this limitation, you can create a Quartz plug-in that acts as a parent plug-in and loads multiple
other plug-ins in a given order. Listing 8.6 shows what the ParentPlugin looks like.
Listing 8.6. The ParentPlugin Can Load Child Plug-Ins in a Specified Order
package org.cavaness.quartzbook.chapter8;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
134
135
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Scheduler;
import org.quartz.SchedulerConfigException;
import org.quartz.SchedulerException;
import org.quartz.spi.SchedulerPlugin;
/**
* Default no-arg Constructor
*
*/
public ParentPlugin() {
}
/**
* Pass the initialize call on to the child plug-ins.
*
* @throws SchedulerConfigException
* if there is an error initializing.
*/
this.pluginName = name;
this.scheduler = scheduler;
while (tokenizer.hasMoreElements()) {
String pluginClassname = tokenizer.nextToken();
try {
Class pluginClass =
Class.forName(pluginClassname);
childPlugin.initialize(pluginClassname,
scheduler);
135
136
The plug-in in Listing 8.6 doesn't do anything, as far as a plug-in goes, but it acts as a loader of the child
plug-ins. A child plug-in is any valid Quartz plug-in. It can be one of your own or one of those included with
the framework.
136
137
To configure the ParentPlugin in the quartz.properties file, just add the parent as you would any other
plug-in. That is, add the following line:
org.quartz.plugin.parentPlugin.class =
org.cavaness.quartzbook.chapter8.ParentPlugin
Then, to add the child plug-ins and the order you want them loaded, just specify a comma-separated list of
plug-ins:
As Listing 8.6 shows, the ParentPlugin tokenizes the comma-separated string and loads the plug-ins in the
order they were in the list. This might seem like a lot of complexity, but it gets the job done. A future version of
the Quartz framework might support a load order mechanism for plug-ins, but for now, the ParentPlugin
works fine.
JobInitializationPlugin
We've already talked several times about this plug-in. It loads job and trigger information from an XML file (by defaul
quartz_jobs.xml). You can configure the name of the file by setting the filename parameter in the quartz.propertie
for the plug-in. If you don't require a database to store your jobs or if you need the capability to quickly test specific job
plug-in is very helpful.
JobInitializationPluginMulitple
The files for the JobInitializationPluginMultiple are comma separated and specified in the quartz.properties f
nice feature for this plug-in is that it can periodically scan the files for changes and then reload the job information whe
change. It adds this behavior by implementing the org.quartz.jobs.FileScanListener. The scan interval (defined in
seconds) can be specified in the properties file.
LoggingJobHistoryPlugin
137
138
but provides a default message format. The format can specify which fields from the job are included in the message. You can
provide separate message formats for the following events:
For example, if you wanted to override the default message for when a job is about to be executed and all you cared to see in
the log message was the job name and time of execution, you could add this to the quartz.properties file:
You can include several data elements about the job in the log message. Table 8.1 lists the elements and their data types.
Table 8.1. The Elements That Can Be Used in the Job Message Format
Element # D
T
0 S
1 S
2 D
3 S
4 S
5 D
6 D
7 I
Each event can be separately configured in the quartz.properties file. The only downside to using this
plug-in is that if you have many jobs, the log files can fill up quite fast, and it almost becomes too much
information.
LoggingTriggerHistoryPlugin
This plug-in is equivalent to the job history plug-in but is used for trigger history information instead. You can
provide log message formats for the following trigger events:
• triggerCompleteMessage Logged when the trigger has finished all firings and will not fire again
• TRiggerFiredMessage Logged when a trigger fires
• triggerMisfiredMessage Logged after a trigger misfires
As with the LoggingJobHistoryPlugin, you can override the default message format in the properties file.
138
139
You can include several data elements about the job in the log message. Table 8.1 lists the elements and their
data types. Table 8.2 lists the elements that can be used in the trigger history log message.
Table 8.2. The Elements that Can Be Used in the Trigger Message Format
Element #
Data Type
Description
String
String
Date
Date
Date
String
String
139
140
Integer
Like the LoggingJobHistoryPlugin, this plug-in can log a lot of messages, especially if you have several
triggers firing often.
ShutdownHookPlugin
This plug-in catches the shutdown event of the JVM and forces the Scheduler to shut down. You might be
saying, "Why do I need to tell the Scheduler to shut down if the JVM is already shutting down?" The reason is
mainly so the Scheduler can perform a "clean" shutdown.
When the initialize() method is called on the plug-in, it adds a new java.lang.Thread to the JVM. When
the JVM gets a shutdown event, it is caused by one of two events:
• The program exits normally when the last nondaemon thread exits or when the System.exit()
method is invoked.
• The JVM is terminated in response to user input, such as the user pressing Ctrl+C, or a system-wide
event, such as a user logoff or system shutdown.
When the JVM gets the shutdown notification, it performs a callback to the shutdown thread and gives the
thread a chance to run. In the case of the ShutdownHookPlugin, the run() method calls the Scheduler and
tells it to shut down. Be default, the shutdown() call to the Scheduler includes a Boolean value of true, which
tells the Scheduler to perform a "clean" shutdown. This means that the Scheduler will wait for all executing
triggers before stopping.
You can tell the plug-in to not perform a clean shutdown by including that as a plug-in parameter in the
quartz.properties file.
Fortunately, several techniques (or mechanisms) enable you to do this. The Quartz framework nicely supports
one of those mechanisms, Remote Method Invocation (RMI). This chapter focuses on deploying Quartz as an
RMI server so that you can access the Scheduler from outside the JVM. Several benefits arise from being able
to do this, all of which are discussed in this chapter.
140
141
applications need a way to talk to the Scheduler from within their own address space or JVM. In this case,
using something such as RMI is the only way to solve this problem.
With RMI, objects that run in one address space (or JVM) are free to call objects running in other JVMs. This
expands the Quartz toolkit and makes the framework more beneficial.
RMI is a mechanism that enables an object in one address space to communicate with objects in a different
address space. These two address spaces can exist on the same machine or on entirely different ones. In
general, you can think of RMI as an object-oriented Remote Procedure Call (RPC) mechanism.
RMI is not specific to Java, but Java does have its own version: Java RMI, which is released and supported by
Sun (http://java.sun.com/products/jdk/rmi) as part of its Java SDK. Java RMI allows objects running in one
JVM to communicate with and invoke methods on objects running in a second. These two JVMs can be
running on the same machine or, more interestingly, on different ones. They might even be running on
different platforms. For example, one JVM might be running on a Windows box, while the second runs on
Linux. With Java RMI, it really doesn't matter. Figure 9.1 illustrates this scenario.
RMI is based on the principle of separation of responsibility. Interface definition is separated from the
implementation. This coincides nicely with the goal of RMI: to allow a client, which is primarily concerned
with the interface definition, to be separated (possibly physically) from the implementation of that interface
definition. In order words, the client has an interface on which it can invoke the service call, while the
implementation of that service lives on another machine. Figure 9.2 illustrates this separation within the Java
programming language.
141
142
Figure 9.2. Java supports separating the service interface from the implementation.
When the RMI client invokes a method on the object, it is really invoking a method on a proxy object located
in the client JVM. This proxy object knows how to communicate with the object located on the server. All
communication, including values being passed to the server object and returned from it, occurs through the
proxy object. This is shown in Figure 9.3.
Figure 9.3. All communication from the client to the server goes through the proxy object.
The RMI server is responsible for creating the server object (which the client invokes methods on) and
making the object available to remote clients. The RMI server runs in a standard JVM. The server objects are
also regular Java objects and can be invoked locally as well as remotely.
142
143
The RMI Client
The RMI client is a Java program that (typically) runs in a separate JVM from the server; it might be on the
same physical machine as the RMI or distributed on a separate machine. The client gets a reference to the
server object by looking it up in the RMI Registry.
The RMI Object Registry is used by both the RMI client and server. When the server wants to make an object
available to remote clients, it registers the object (along with a unique name) with the Registry. After a server
object has been registered, RMI clients and methods invoked on the object can look it up.
Requirements of RMI
RMI is a nice feature of the Java library. The capability to a call a method on an object from one JVM on a
second JVM located on a machine in another part of the world is really powerful. But for RMI to work,
several things must be in total alignment. Java is a very dynamic programming language. It supports the
capability to (at runtime) download new class files that are not defined within the JVM's classpath. Along
with this flexibility comes danger. You can imagine the bad things that could happen if your application were
to download someone else's classes without you having knowledge of what's inside.
When using RMI, code that doesn't yet exist in the client can be dynamically downloaded from the server
program. By default, the RMI application does not install a SecurityManager. This can be dangerous if an
unsuspecting client downloads malicious code. To help secure the application, the RMI server application
should install a special SecurityManager called RMISecurityManager.
The RMISecurityManager uses a Java security policy file to determine what permissions are granted to the
RMI application. By default, Java looks for a policy file in the java.home/lib/security directory. A
sample policy file entry is shown here:
grant {
permission java.security.AllPermission;
};
This policy file entry grants full access to everyone. You should start with this setting to get everything
working and then tighten your security policy.
143
144
Configuring the Quartz RMI Server
The first step is to modify the quartz.properties file that will be deployed with the Quartz RMI
server. When using RMI within Quartz, you must add several new properties. Table 9.1 includes
the complete list of RMI properties.
Table 9.1. The Properties That Are Necessary for the RMI Server
Property Default
org.quartz.scheduler.rmi.export false
Notes: This flag must be set to true if you want the Quartz Scheduler to be available as a RMI
object.
org.quartz.scheduler.rmi.registryHost localhost
Notes: This is the port on which the RMI Registry is listening (usually 1099).
org.quartz.scheduler.rmi.createRegistry never
Notes: This determines whether Quartz creates the RMI Registry. Use false or never if you
don't want Quartz to create a Registry. Use TRue or as_needed if you want Quartz to first attempt
to use an existing Registry and then fall back to creating one. Use always if you want Quartz to
create a Registry and then fall back to using an existing one. If a Registry is created, it will be
bound to a port number in the given registryPort and registryHost.
org.quartz.scheduler.rmi.serverPort -1
Notes: This is the port on which the Quartz Scheduler service will bind and listen for connections.
By default, the RMI service randomly selects a port as the Scheduler is bound to the RMI Registry.
All the properties in Table 9.1 need to be added to the quartz.properties file for the Quartz
RMI server. Although there are defaults for these properties, it's best to specify the values
explicitly to prevent any confusion. Listing 9.1 shows an example properties file that is used
with the Quartz RMI server.
Listing 9.1. An Example quartz.properties file for Use with the Quartz RMI Server
#==============================================================
# Configure Main Scheduler Properties
#==============================================================
org.quartz.scheduler.instanceName = RMIScheduler
#==============================================================
# Configure RMI Properties
#==============================================================
org.quartz.scheduler.rmi.export = true
org.quartz.scheduler.rmi.registryHost = localhost
org.quartz.scheduler.rmi.registryPort = 1099
org.quartz.scheduler.rmi.serverPort = 0
org.quartz.scheduler.rmi.createRegistry = true
#==============================================================
# Configure ThreadPool
#==============================================================
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
144
145
org.quartz.threadPool.threadCount = 10
org.quartz.threadPool.threadPriority = 5
#==============================================================
# Configure JobStore
#==============================================================
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
You'll recognize most of the settings in Listing 9.1 from the various quartz.properties files that you've
seen in previous chapters. The only new properties are the ones added from Table 9.1.
To run the Quartz RMI server, you need to create a startup class that obtains a Scheduler from the factory and
starts running the Scheduler. This is true of Quartz with or without RMI. Because we are using RMI in this
example, however, a few new steps must be carried out.
First, to make things less confusing, we rename our quartz.properties file server.properties and tell
the Quartz RMI server to load that file instead of the default quartz.properties file. Changing the name
makes things easier when trying to debug problems. This way, we can be sure that Quartz is loading the
correct settings file.
The second change is that we load a new SecurityManager so that all the necessary permissions will be
granted to the RMI server. We discussed the RMISecurityManager earlier in the chapter.
Other than these changes, the startup class shown in Listing 9.2 should look familiar.
Listing 9.2. The QuartzRMIServer Can Be Used to Start the Quartz RMI Server
package org.cavaness.quartzbook.chapter9;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.Scheduler;
import org.quartz.SchedulerFactory;
import org.quartz.impl.StdSchedulerFactory;
145
146
Scheduler scheduler =
StdSchedulerFactory.getDefaultScheduler();
/*
* Due to the server.properties file, our Scheduler will
* be exported to RMI Registry automatically.
*/
scheduler.start();
System.out.println("\n");
System.out.println(
"The scheduler will run until you type \"exit\"");
while (true) {
System.out.print("Type 'exit' to shutdown server: ");
if ("exit".equals(rdr.readLine())) {
break;
}
}
In Listing 9.2, after the RMISecurityManager has been set, a Scheduler is retrieved from the factory and the
start() method is called. The server is designed to be run from the console so after the Scheduler is started,
it runs until the user types exit from the console. The Scheduler then is shut down and stops serving
remote clients.
Other than using the RMISecurityManager, notice that we didn't have to do anything special in the code with
the Quartz Scheduler to use it as a remote Scheduler. That was all taken care of in the server.properties
file. When the Scheduler is created, if the properties file tells it to, the Scheduler exports itself to the RMI
Registry and makes it available to be called remotely.
146
147
If you do start the Registry from the command line, make sure the port that you start it on is the same as the
port specified in the properties file. When starting from the command line, you change to the
<JAVA_HOME>/bin directory and type this:
rmiregistry <port>
If you don't specify a port number, the default 1099 is used. This happens to be the default that Quartz uses as
well.
If you don't want to run the Registry from the command line, Quartz will start it automatically if you have the
correct value for the property org.quartz.scheduler.rmi.createRegistry. From Table 9.1, the property
has several possible values:
• false (never)
• true (as_needed)
• always
If you want Quartz to start the Registry, set the property to true or always.
As with the server in Table 9.1, Table 9.2 lists all of the properties that must be set for the Quartz
RMI client. Two of these properties must be set for both the server and the client.
Table 9.2. The Quartz RMI Properties Necessary for the Client
Property Default
org.quartz.scheduler.rmi.registryHost localhost
Notes: This is the host at which the RMI Registry is running.
org.quartz.scheduler.rmi.registryPort 1099
Notes: This is the port on which the RMI Registry is listening (usually 1099).
org.quartz.scheduler.rmi.proxy false
Notes: If you want to connect to a remotely served scheduler, set the org.quartz.scheduler.rmi.proxy flag
to true. You must also then specify a host and port for the RMI Registry process.
For the client to locate the server object, it needs to know where the RMI Registry is running so that it can
look up the object. The org.quartz.scheduler.rmi.registryHost and
org.quartz.scheduler.rmi.registryPort should be the host and port of where the RMI Registry is
running. If you configured the Quartz RMI server to start the Registry automatically, the RMI Registry is
running on the same box as the RMI server.
Because you want the client to contact the remote Scheduler to schedule jobs, you need to set the property
147
148
org.quartz.scheduler.rmi.proxy to true.
Listing 9.3 shows an example quartz.properties file that you can include with the RMI client to
communicate with the server.
Listing 9.3. An Example quartz.properties File for Use with a Quartz RMI Client
#=============================================================
# Configure Main Scheduler Properties
#=============================================================
org.quartz.scheduler.instanceName = RMIScheduler
#org.quartz.scheduler.instanceId = AUTO
#==============================================================
#Configure RMI Properties
#==============================================================
org.quartz.scheduler.rmi.registryHost=localhost
org.quartz.scheduler.rmi.registryPort=1099
org.quartz.scheduler.rmi.proxy= true
Other than the three RMI properties, you've seen many example quartz.properties file like the one in
Listed 9.3.
When you have the properties file for the client configured, you need to build a client Java class that will get a
handle to the remote Scheduler and do something with it. There's not much to creating this class, but just as
with the server class, we changed the quartz.properties file to be called client.properties and told the
client to load the properties from this file. Again, this is done just to help us remember where the properties
are coming from and to avoid any confusion. Other than this change, you've seen the code in Listing 9.4 many
times before throughout the examples in previous chapters.
Listing 9.4. An Example Quartz RMI Client That Schedules a Job with a Remote Scheduler
package org.cavaness.quartzbook.chapter9;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.CronTrigger;
import org.quartz.JobDataMap;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
148
149
import org.quartz.impl.StdSchedulerFactory;
An interesting observation that seems almost magical is that we didn't have to tell the factory that we wanted a
remote Scheduler. The factory determined this from the client.properties file that we told it to load.
Specifically, setting this RMI property caused the factory to create a remote Scheduler:
org.quartz.scheduler.rmi.proxy = true
The following is a fragment of code from the StdSchedulerFactory that determines that the client wants to
connect to a remote Scheduler:
if (rmiProxy) {
if (autoId)
schedInstId = DEFAULT_INSTANCE_ID;
String uid =
QuartzSchedulerResources.getUniqueIdentifier(
schedName, schedInstId);
149
150
RemoteScheduler remoteScheduler =
new RemoteScheduler(schedCtxt, uid, rmiHost, rmiPort);
schedRep.bind(remoteScheduler);
return remoteScheduler;
}
The fragment takes place in the instantiate() method on the StdSchedulerFactory. In this fragment, the
factory checks to see if the rmiProxy was set to TRue, which it is for the client. If it is true, a new instance
of RemoteScheduler is instantiated and returned. This is why our client didn't have to do anything special.
The Scheduler instance that is returned to our client is really an instance of RemoteScheduler, but
RemoteScheduler implements the Scheduler interface, so the client code is none the wiser.
To run the Quartz RMI server, just run the QuartzRMIServer class as you would any other Java class. As we
mentioned earlier, be sure to include all the required JARs in the classpath, as well as the server.properties
file. The easiest way to accomplish this is to create a batch file (or shell script) with all of this in it. Listing 9.4
shows a sample batch file that you can use to start the server.
Listing 9.4. A Sample startserver.bat File Used to Start the Quartz RMI Server
java org.cavaness.quartzbook.chapter9.QuartzRMIServer
You will need to include the necessary jars in the classpath for the command in Listing 9.4 to work correctly.
These include quartz.jar, commons-logging.jar, commons-logging-api.jar, commons-collections3.1.jar,
beanutils.jar, commons-beanutils-bean-collectons.jar, commons-beanutils-core.jar.
When the server is running, you can then run the RMI client. The client can be run in the same way as the
server, by creating a batch file or shell script. When you start the client, if all goes well, you should not only
see the output from the client console that a remote job has been scheduled, but you should also see a message
printed in the server console that it received the remote job to schedule and run.
Whereas the RMI client is designed to schedule its job and exit, the Quartz RMI server is designed to continue
to run until you type exit. The server should write that it received a remote job to schedule and then go about
150
151
its business of waiting for more clients to connect. This means that you can run the client multiple times, and
this is exactly what you would want from a design like this.
You can just type exit when you're finished running the examples.
What's Up Next?
This chapter introduced a new way to interact with the Quartz Scheduler that wasn't possible before. Using
Quartz with RMI enables you to build applications for which you can separate the components and distribute
them across multiple machines. This is a powerful concept because it offers better scalability without
additional development work.
The next chapter takes this concept to the next level by showing how it's possible to use Quartz within a J2EE
application server. Interestingly, Java application servers all use a form of RMI as well to support remote
method invocation.
Many business processes require asynchronous job scheduling. For example, Web sites usually
need to send e-mails to registered users to alert them to new features or the latest specials.
Medical claimprocessing companies typically need to download medical data at night and do
something with that data. A company that sells some type of product might have reports
generated each night that show sales and commission information. All of these scenarios could
benefit from a timer service that executes asynchronous jobs.
The Java/J2EE community has produced several attempts at solving the timer problem. Early on,
vendors added propriety solutions within their J2EE servers. (For this chapter, the terms J2EE
151
152
server and J2EE container are used interchangeably.) For example, the WebLogic product had
some custom extensions, as did the IBM J2EE server. Unfortunately, they were not exactly
compatible for moving components from one to another. Later, these vendors and others tried to
develop a common set of timer components.
Starting with Java 1.3, Sun added the java.util.Timer and java.util.TimerTask classes to
help add basic Timer functionality to the core language. Although the Timer and TimerTask can
work for simple requirements, there is much more to true job scheduling than can be solved by
two concrete classes. Hopefully, that's a point that you already understand.
Two basic strategies exist for architecting and deploying Quartz with J2EE. With one strategy,
you can design Quartz to work outside the J2EE container as a standard J2SE client. As you'll see
shortly, this is the simplest approach. The second strategy is to deploy Quartz as a J2EE client
that resides within the J2EE container. In this scenario, the J2EE client is a Web Archive (WAR)
file and is deployed like any other Web application. The strategy you choose depends on your
exact needs. Each comes with a set of pros and cons.
If you just need to invoke services on Enterprise JavaBeans (EJBs) or put messages inside a JMS
queue or topic, the easiest way to configure Quartz with J2EE is to run Quartz outside the J2EE
container as a stand-alone J2SE application. It then would function like any other Java
application that lives outside the container but needs to call methods on distributed components
within the container.
We've essentially been practicing this approach in the previous chapters, minus the part about
calling EJBs. You can create a Quartz application that contains the Quartz libraries and
job-scheduling information and connects to the J2EE server through the home and remote
interfaces. You can then invoke methods on EJBs like any other distributed client. You can also
create and insert JMS messages and have them processed by message-driven beans (MDB)
running within the container. This approach is shown in Figure 10.1.
Figure 10.1. Quartz can work with J2EE as a stand-alone J2SE client.
This approach works nicely if you have existing J2EE components that you want Quartz to
interoperate with, and you don't want to or can't make any changes to the server. To implement
this approach, you just need to build a Quartz application, as we've done in previous chapters,
and use the EJBInvokerJob that ships with Quartz. We discuss the EJBInvokerJob shortly.
152
153
Deploying Quartz Within the J2EE Server
Quartz can also be deployed directly within the container to do away with the external Quartz
application. This is commonly referred to as using a J2EE client. You might choose this approach
over the previous one for several reasons. One reason is that there's only one application to
maintain, compared with two in the other approach. If the external Quartz client shuts down, the
job-scheduling information is temporarily lost, and the business owners will not be thrilled. In
other words, it's one failure point versus two. Another reason for deploying Quartz within the
container is to have Quartz use some of the other resources that the container offers, such as mail
sessions, data sources, and other resource adapters. If you are using the J2EE server in a clustered
environment, it also makes sense to deploy Quartz within the container to make the clustering
easier and more manageable. Figure 10.2 illustrates how Quartz can be deployed with the J2EE
application.
When Quartz is deployed within a J2EE container, you must understand and deal with some
complications. Before we get into those, let's talk about how to deploy Quartz as a J2SE client
and access the container from outside the container.
By far the easiest and simplest way to use Quartz with J2EE is to deploy it outside the container.
What makes this approach easier is that you don't have to deal with many of the issues that will
surface as Quartz attempts to create threads and execute within the container. It's also easier
because deploying applications into a J2EE container can be frustrating, even with all of the latest
tools and technologies such as XDoclet and administrative consoles.
153
154
Listing 10.1. An Example Stateless Session Bean
import java.rmi.RemoteException;
import javax.ejb.EJBException;
import javax.ejb.SessionBean;
import javax.ejb.SessionContext;
public TestBean() {
super();
}
With this EJB deployed and ready in your J2EE application server of choice, you can use the
EJBInvokerJob to invoke one of the helloWorld() methods available to remote clients.
You set up the EJBInvokerJob just as you would for any other job. Listing 10.2 shows an
example of using the EJBInvokerJob to invoke the helloWorld() on the SLSB.
package org.cavaness.quartzbook.chapter10;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
import org.quartz.jobs.ee.ejb.EJBInvokerJob;
try {
// Create a Scheduler and schedule the Job
Scheduler scheduler = example.createScheduler();
example.scheduleJob(scheduler);
154
155
scheduler.start();
loadJobDataMap(jobDetail);
scheduler.scheduleJob(jobDetail, trigger);
}
/*
* Configure the EJB parameters in the JobDataMap
*/
public JobDetail loadJobDataMap(JobDetail jobDetail) {
jobDetail.getJobDataMap().put(EJBInvokerJob.EJB_JNDI_NAME_KEY,
"ejb/HelloWorldSession");
jobDetail.getJobDataMap().put(EJBInvokerJob.EJB_METHOD_KEY,
"helloWorld");
jobDetail.getJobDataMap().put(EJBInvokerJob.PROVIDER_URL,
"t3://localhost:7001");
jobDetail.getJobDataMap().put(
EJBInvokerJob.INITIAL_CONTEXT_FACTORY,
"weblogic.jndi.WLInitialContextFactory");
return jobDetail;
/*
* return an instance of the Scheduler from the factory
*/
public Scheduler createScheduler() throws SchedulerException {
return StdSchedulerFactory.getDefaultScheduler();
}
}
As you can see from Listing 10.2, the EJBInvokerJob is configured like any other job. A
JobDetail and trigger are created and registered with the Scheduler. Several JobDataMap
parameters can be used for the job to function properly with various J2EE containers. Table 10.1
lists the JobDataMap parameters that the job supports.
155
156
The parameters you add to the JobDataMap depend on which J2EE server you're using and what
its requirements are. For example, if you're using BEA WebLogic, you would need to specify at
least the ones from Listing 10.1, obviously substituting values for your specific environment. If
you were using WebSphere, most of the values would be different.
When we set up and run Listing 10.2 within our external Quartz application, every 10 seconds the
helloWorld() method on the EJB is invoked. This approach is nice because we don't have to
worry about deploying the Quartz application within the J2EE container. It enforces a separation
of job information from business processing logic.
Table 10.1. The EJBInvokerJob Uses Several Parameters, Depending on Your Specific J2EE Server
Static Constant String Value
EJB_JNDI_NAME_KEY ejb
Notes: JNDI name of the bean's home interface
PROVIDER_URL java.naming.provider.url
Notes: Vendor-specific URL that specifies where the server can be found
INITIAL_CONTEXT_FACTORY java.naming.factory.init
Notes: Vendor-specific context factory that is used to look up resources
EJB_METHOD_KEY method
Notes: Name of the method to invoke on the EJB
EJB_ARGS_KEY args
Notes: Object[] of the args to pass to the method (optional, if left out, there are no arguments)
EJB_ARG_TYPES_KEY argType
Notes: Class[] of the args to pass to the method (optionalif left out, the types will be derived by calling getClass() on each of t
arguments)
PRINCIPAL java.naming.security.pri
Notes: The principal (user) to be used for the EJB method call
CREDENTIALS java.naming.security.cre
Notes: The credentials to be used for the EJB method call
In the example in Listing 10.2, the helloWorld() method that was invoked on the EJB didn't defined any
parameters. The EJBInvokedJob class enables you to pass arguments to an EJB method by specifying them
using the EJB_ARGS_KEY and EJB_ARG_TYPES_KEY parameters shown in Table 10.1.
Listing 10.3 shows another simple example that passes an argument to a different version of helloWorld()
EJB running on the Apache Geronimo J2EE server.
Listing 10.3 is very similar to Listing 10.2, except that it includes the parameters EJB_ARGS_KEY and
EJB_ARG_TYPES_KEY. Also, because it's running against the Geronimo J2EE application server, it needed to
add the arguments for PRINCIPAL and CREDENTIALS.
package org.cavaness.quartzbook.chapter10;
import java.util.Date;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
156
157
import org.quartz.jobs.ee.ejb.EJBInvokerJob;
try {
trigger.setName("helloWorldTrigger");
// Start the trigger firing from now
trigger.setStartTime(new Date());
/*
* Configure the EJB parameters in the JobDataMap
*/
public JobDetail loadJobDataMap(JobDetail jobDetail) {
jobDetail.getJobDataMap().put(
EJBInvokerJob.EJB_JNDI_NAME_KEY, "ejb/Test");
jobDetail.getJobDataMap().put(EJBInvokerJob.EJB_METHOD_KEY,
"helloWorld");
157
158
jobDetail.getJobDataMap().put(
EJBInvokerJob.PROVIDER_URL, "127.0.0.1:4201");
jobDetail.getJobDataMap().put(
EJBInvokerJob.INITIAL_CONTEXT_FACTORY,
"org.openejb.client.RemoteInitialContextFactory");
jobDetail.getJobDataMap().put(
EJBInvokerJob.PRINCIPAL, "system");
jobDetail.getJobDataMap().put(
EJBInvokerJob.CREDENTIALS, "manager");
return jobDetail;
}
/*
* return an instance of the Scheduler from the factory
*/
public Scheduler createScheduler() throws SchedulerException {
return StdSchedulerFactory.getDefaultScheduler();
}
}
Because Quartz needs to get a reference to the home and remote interfaces for the EJB, you need to deploy
some J2EE client JARs with your external Quartz application. The JARs you need to add depend on which
J2EE container you're using. If you're using WebLogic, for example, you'll probably just put the
weblogic.jar with the Quartz application. For Geronimo, several are involved. Check with the server
documentation to be sure.
Running Quartz as a J2EE client is a little more involved than running Quartz as an external J2SE application.
This is mostly because deploying applications within the container is somewhat more complicated. In
addition, the J2EE specification puts some constraints on components within the container. One of the biggest
guidelines that the specification gives involves who and what can create Java threads. Because it's the
container's responsibility to manage all resources, it can't allow just anything or anyone to create threads. If it
did, it would have a harder time managing the environment and keeping things stable. Quartz creates its own
worker threads, so you need to follow some steps to make sure things work properly.
158
159
Assume that a stateless session bean such as the one from Listing 10.1 is already deployed in the container.
The easiest way to deploy Quartz within the container is to build a WAR file that contains all the necessary
files and then use the admin tools, or Eclipse, to deploy the Web application within the container.
The Web application directory structure is just like that of any other Web application. You need to add the
following files to it:
Because you are building a Web application, you need to add the requisite web.xml deployment descriptor.
Listing 10.4 shows the web.xml for our client application that will be installed within the container.
Listing 10.4. The web.xml for the Quartz J2EE Client Application
<web-app>
<servlet>
<servlet-name>QuartzServlet</servlet-name>
<servlet-class>
org.quartz.ee.servlet.QuartzInitializerServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>QuartzServlet</servlet-name>
<url-pattern>/servlet/QuartzServlet</url-pattern>
</servlet-mapping>
</web-app>
The Quartz framework includes a Java servlet called QuartzInitializerServlet that, when invoked,
initializes the Quartz Scheduler and loads job information. In Listing 10.4, we've set the <load-on-startup>
parameter to have a value of 1 so the servlet will be loaded and initialized when the container is started. By
using the servlet to start the Quartz Scheduler, we avoid the issues of thread permission because the container
will allow servlets to create user threads.
Next, you need to put the standard quartz.properties file into the WEB-INF/classes directory of the Web
application. There's nothing special about this version of the properties file; it's essentially what we did in past
chapters. However, here we use the JobInitializationPlugin (this was shown in Chapter 8, "Using Quartz
Plug-Ins," and is designed to load job information from an XML file). By default, the plug-in looks for a file
called quartz_jobs.xml and loads the jobs found in the file. As Chapter 8 described, using this particular
159
160
plug-in keeps you from having to write job-loading code and be forced to recompile when changes occur. The
quartz_jobs.xml file for this example is shown in Listing 10.5.
<quartz>
<job>
<job-detail>
<name>HelloWorldJob</name>
<group>DEFAULT</group>
<job-class>org.quartz.jobs.ee.ejb.EJBInvokerJob</job-class>
<volatility>false</volatility>
<durability>false</durability>
<recover>false</recover>
<job-data-map allows-transient-data="true">
<entry>
<key>ejb</key>
<value>ejb/Test</value>
</entry>
<entry>
<key>java.naming.factory.initial</key>
<value>org.openejb.client.RemoteInitialContextFactory</value>
</entry>
<entry>
<key>java.naming.provider.url</key>
<value>127.0.0.1:4201</value>
</entry>
<entry>
<key>method</key>
<value>helloWorld</value>
</entry>
<entry>
<key>java.naming.security.principal</key>
<value>system</value>
</entry>
<entry>
<key>java.naming.security.credentials</key>
<value>manager</value>
</entry>
</job-data-map>
</job-detail>
<?docpage num="212"?> <trigger>
<simple>
<name>helloWorldTrigger</name>
<group>DEFAULT</group>
<job-name>HelloWorldJob</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-06-10 6:10:00 PM</start-time>
<!- repeat indefinitely every 10 seconds >
<repeat-count>-1</repeat-count>
<repeat-interval>10000</repeat-interval>
</simple>
</trigger>
</job>
</quartz>
You can see from Listing 10.5 that we are still using the EJBInvoker Job by specifying it within the
quartz_jobs.xml file.
160
161
org.quartz.plugin.jobInitializer.class =
org.quartz.plugins.xml.JobInitializationPlugin
After all these files have been configured, you can build the WAR file and deploy it within your container.
When the container starts up, the servlet is loaded and initialized and starts the Scheduler. The Scheduler uses
the JobInitializerPlugin to load job information from the quartz_jobs.xml file. From that point, the
EJBInvokerJob invokes the helloWorld() method on the EJB.
When packaging the J2EE Client application, you need to package the J2EE client JARs necessary for your
particular server. Each one is different, so check the documentation to be sure. You also need all the Quartz
libraries that are required when building a stand-alone Quartz application.
If you are using a J2EE container and one of the two types of JDBC JobStores, then you will want to use the
container's DataSource as well. Refer back to Chapter 6 for how to set up the quartz.properties file when
using JDBC JobStores within a J2EE container.
161
162
initialContext.lookup(urlToMailSession);
This is exactly the same behavior that some users require of their Quartz applications. Users
want to build and set up Quartz applications so that when a job absolutely needs to be
executed, it gets executed. As the popularity of your Quartz application grows and an
increasing demand is placed on it, a cluster of Quartz applications will provide better peace of
mind that you'll be able to handle that demand and ensure that all goes as planned. And you
get all of this with very little effort to set up and maintain.
162
163
The Benefits to Clustering Quartz Applications
Clustering Quartz applications provides two key benefits over nonclustered environments:
• High availability
• Scalability
High Availability
A highly available application is one that can service clients for a high percentage of the time.
In some cases, this might mean 24 hours a day, 7 days a week. For other applications, it
might just mean "most of the time." Availability is usually expressed as a percentage between
0 and 100 percent. An application might fail often but still achieve high availability. On the
other hand, an application might go down once but then might stay down for a long time, for
low availability. What counts is not how many times the application goes down, but the total
amount of downtime. Obviously, as developers, we hope our applications never fail. But this
does happen, and you must be prepared for it.
The level of availability for hardware and software is sometimes referred to as levels of nine.
Levels of nine indicate the number of nine digits in the percentage of availability. For
example, 99.999 is said to have five levels of nine because there are five digits. Table 11.1
shows the approximate percentage of downtime for a particular level.
Looking at Table 11.1, you might come to the conclusion that four levels of nine (about an hour of downtime
per year) is an awesome amount of availabilityand, in general, that's true. However, if the application was a
Quartz application designed to send out invoices and it was down for five minutes every day for 12 straight
days when the invoices were supposed to go out, the business would lose a lot of revenue, and you would
probably be looking for a new job (and I'm not talking about the Quartz kind of job, either). It's not just about
the amount of downtimeit's also when that downtime strikes.
Part of what makes high availability possible is the concept of failover. Failover ensures that even if a system
failure occurs, other redundant components or services can handle the requests and insulate the clients (or
jobs) from the failures. The capability to fail over from a failed component or service to another functioning
one increases the availability of the application. The switch or failover should be transparent.
Scalability
Scalability means having the capability to dynamically add new resources such as hardware to the application
environment, to increase the capacity of the application. In a scalable application, achieving this increase in
capacity does not involve changing code or the design.
163
164
Achieving scalability is not done with magic. An application must be designed properly from the beginning;
supporting extra capacity usually takes administrative effort in adding the new hardware (such as memory) or
starting more instances of the application.
Load Balancing
As part of achieving good scalability, the capability to distribute work across the nodes in the cluster is very
important. Spreading out work ensures that each node in the cluster is footing its share of the workload.
Imagine if all the work was being given to one node in the cluster while the other nodes remained idle. If this
pattern continued, eventually the overworked node would not be able to handle the increased work, and this
would result in a failure.
In the best scenario, work is spread evenly across all instances in the cluster. Several different algorithms can
be used to distribute the work, including random, round-robin, and weighted round-robin, just to name a few.
Currently, Quartz provides a minimal load-balancing capability using a random algorithm. Each Scheduler
instance in the cluster attempts to fire scheduled triggers as fast as the Scheduler permits. The Scheduler
instances compete (using database locks) for the right to execute a job by firing its trigger. When a trigger for
a job has been fired, no other Scheduler will attempt to fire that particular trigger until the next scheduled
firing time. This mechanism works better than you might infer from its simplicity. This is because the
Scheduler that is "most busy" will be the one least likely to find the next job to fire. Hence, it's possible to
achieve something near to a true balancing of the load.
Figure 11.1 shows that each node communicates directly with the database and has no knowledge of others
outside the database.
164
165
Figure 11.1. Each node in a Quartz cluster is aware of the other instances only via the database.
The Quartz Scheduler itself is not cluster-aware, but the JDBC JobStore configured for the Scheduler is.
When the Quartz Scheduler is started, it calls the schedulerStarted() method on the JobStore, which, as
the name implies, tells the JobStore that the Scheduler has been started. The schedulerStarted() method is
implemented in the JobStoreSupport class.
The JobStoreSupport class uses a property setting from the quartz.properties file (discussed shortly) to
determine whether the Scheduler instance is participating in a cluster. If a cluster is configured, a new instance
of the class ClusterManager is created, initialized, and started. The ClusterManager is an inner class within
the JobStoreSupport class. The ClusterManager class, which extends java.lang.Thread, runs periodically
and performs a check-in function for the Scheduler instance. When the clusterCheckin() method is called,
the JobStoreSupport updates the database table SCHEDULER_STATE for the Scheduler instance. The Scheduler
also checks to see if any of the other cluster nodes have failed. The check-in occurs periodically based on a
configuration property (discussed shortly).
When a Scheduler instance performs the check-in routine, it looks to see if there are other Scheduler instances
that didn't check in when they were supposed to. It does this by inspecting the SCHEDULER_STATE table and
looking for schedulers that have a value in the LAST_CHECK_TIME column that is older than the property
org.quartz.jobStore.clusterCheckinInterval (discussed in the next section). If one or more nodes
haven't checked in, the running Scheduler assumes that the other instance(s) have failed.
165
166
clocks. See the section "Quartz Clustering Cookbook," later in this chapter for details on how to
do this.
When a Scheduler instance fails while it's executing a job, it's possible to get the job re-executed by another,
working Scheduler. For this to happen, the job's recoverable property, configured in the JobDetail object,
must be set to true.
If the recoverable property is set to false (the default), when a Scheduler fails while running a job, it won't be
re-executed; instead, it will be fired by a different Scheduler instance upon the trigger's fire time, if any. How
quickly a failed Scheduler instance is detected depends on the check-in interval of each Scheduler. This is
discussed in the next section.
Just as when running Quartz in a nonclustered environment, each Quartz application requires a
quartz.properties file. As mentioned in Chapter 3, "Hello, Quartz," if you don't specify one, the default
quartz.properties file (within the quartz.jar file) gets used. It's best to always specify one because you
will eventually need to modify one or more of the settings.
When using the clustered features of Quartz, you need to modify the quartz.properties file for each node.
Listing 11.1 shows an example quartz.properties file that is used for a clustered instance. The properties
are discussed after the listing.
#==============================================================
#Configure Main Scheduler Properties
#==============================================================
org.quartz.scheduler.instanceName = TestScheduler1
org.quartz.scheduler.instanceId = instance_one
166
167
#==============================================================
#Configure ThreadPool
#==============================================================
org.quartz.threadPool.class = org.quartz.simpl.Simple ThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 5
#==============================================================
#Configure JobStore
#==============================================================
org.quartz.jobStore.misfireThreshold = 60000
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass =
org.quartz.impl.jdbcjobstore.MSSQLDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.isClustered = true
org.quartz.jobStore.clusterCheckinInterval = 20000
#==============================================================
#Non-Managed Configure Datasource
#==============================================================
org.quartz.dataSource.myDS.driver = net.sourceforge.jtds.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:jtds:sqlserver://localhost:1433/quartz
org.quartz.dataSource.myDS.user = admin
org.quartz.dataSource.myDS.password = admin
org.quartz.dataSource.myDS.maxConnections = 10
• org.quartz.scheduler.instanceName
• org.quartz.scheduler.instanceId
These properties are used in several places within the JDBC JobStore and database to uniquely identify the
instance.
Some other serious clustering issues were introduced in Quartz 1.5.1. If you need to cluster
Quartz, you probably should avoid that version.
167
168
Configuring the JobStore Section
To use clustering with Quartz, you are required to use either JobStoreTX or JobStoreCMT as the Scheduler's
JobStore. Chapter 6, "JobStores and Persistence," detailed how to set up and use one of the two provided
JDBC JobStores. From Listing 11.1, you can see that the same settings from Chapter 6 are shown, with two
additional properties:
• org.quartz.jobStore.isClustered
• org.quartz.jobStore.clusterCheckinInterval
By setting the value of the org.quartz.jobStore.is Clustered property to true, you are telling the
Scheduler instance that it is participating in a cluster. This property is used throughout the scheduling
framework to modify the default behavior when operating in a clustered environment.
The smaller the clusterCheckinInterval is, the more often the Scheduler nodes check for failed Scheduler
instances. The default value is set to 15000 (or 15 seconds).
Because you must use a JDBC JobStore (either JobStoreTX or JobStoreCMT) for clustering, you also
need to configure a nonmanaged data source (by "nonmanaged," we mean that the data source is not managed
by an application server). Look at Listing 11.1 for an example of setting up a nonmanaged data source for the
JobStoreTX. Look at Chapter 6 for a list of available properties and their allowed values.
As with all Quartz applications that use a database for Job storage, you must load job information into the
database. We talked about the various methods of accomplishing this in Chapter 6.
One of the ways is to write a stand-alone Quartz application that is configured to use the database via one of
the JDBC JobStores, creates a Scheduler instance, and schedules all of the job and trigger information. You
might give this application the capability to clear the job information from the database by passing an
argument into the command line. The problem with this approach is that maintaining the database becomes
very cumbersome.
Another approach would be to use a query tool that comes with your specific RDBMS and load the
information yourself manually. This makes updates and deletion quite easy, but it can be problematic loading
the data for the first time unless you know what you're doing; we highly discourage this.
Some find using the Quartz Web Application, discussed in Chapter 13," Quartz and Web Applications," to be
a convenient method. Managing job information is easy and can even be done by nontechnical resources. See
Chapter 13 for information on the Quartz Web Application and, if you choose, for information on integrating
Quartz into your own GUI application.
168
169
Because Quartz uses a random load-balancing algorithm, you will see jobs being executed by different
instances in a random manner. No pattern or predetermined node executes a particular job or execution.
The next section discusses some of the more common problems and tasks when dealing with Quartz in a
clustered environment.
Currently, no way exists to assign (pin) a job to a specific node in the cluster. If you need this behavior, you
can create a nonclustered Quartz application running in parallel with a clustered node and use either a separate
set of database tables or an XML file along with the JobInitializationPlugin.
Don't point a nonclustered instance to the same set of database tables being used by a cluster. You will get
very erratic results.
As with the previous answer, there is currently no way to run an instance of a job on every node in a cluster.
The best option is to use a nonclustered instance in parallel with each node in the cluster and either use a
separate set of database tables or use an XML file along with the JobInitializationPlugin and the
RAMJobStore.
Quartz doesn't really care if you run nodes on the same machine or on different ones. When clustering is done
on separate machines, it is commonly referred to as horizontal clustering. When nodes are run from the same
machine, it is referred to as vertical clustering. With vertical clustering, there is a single point of failure. This
is bad news for highly available applications because if the machine crashes, all nodes are effectively stopped.
When you are running a Quartz cluster on separate machines, the clocks should be synchronized to prevent
weird and unpredictable behavior. We have already mentioned that if the clocks are not in synch, that
Scheduler instances will become confused about the state of other nodes. There are several easy ways to
ensure the clocks are in synch, and there should be no reason not to do this.
169
170
The easiest way to synch the computers' clocks is to use one of the Internet Time Servers (ITS). For
information on how to set up your clock based on one of these internationally accepted standards, see
http://tf.nist.gov/service/its.htm.
Currently there is no easy way to get a list of all jobs executing in the cluster without going to the database
directly. If you ask a Scheduler instance, you will get only a list of jobs executing on that instance. You could
write some JDBC code that hits the database and retrieves the information from the proper table. Of course,
this goes outside of Quartz, but it will solve the problem. Another approach is to use Quartz's RMI features to
connect to each node in turn, querying it for its currently executing jobs.
Nothing prevents you from using clustered and nonclustered Quartz applications in the same environment.
The only caution is not to mix these two environments in the same database. That is, the nonclustered
environment should not go against the same set of database tables as the clustered applications; you will get
erratic results, and both clustered and nonclustered Jobs could encounter problems.
If you do run a nonclustered Quartz application in parallel with a clustered node, try using the
JobInitializationPlugin (along with an XML file) and the RAMJobStore. This will make your life much
easier.
You can still use job and trigger listeners in a clustered environment. The only confusion comes in when you
try to understand which Scheduler instance will receive the callback method.
The easiest way to remember this is that the Listener will be notified in the Scheduler instance where the job
or trigger is executed. Because a job and trigger are executed on only a single node, the Listener is notified on
that node.
170
171
You can start the Quartz Scheduler in several ways, but the easiest is to use one of the two SchedulerFactory
implementations. In particular, the org.quartz.impl.StdSchedulerFactory is easy to use and performs all
the work of setting up the Schedulerall you need to do is use the static geTDefaultScheduler() method, as
Listing 12.1 demonstrates.
try {
// Get a Scheduler instance from the Factory
scheduler = StdSchedulerFactory.getDefaultScheduler();
When you have an instance of the Scheduler from the factory, you can start the Scheduler and add any
necessary jobs and triggers. You can add jobs and triggers either before or after you start the Scheduler.
The Quartz framework supports multiple configuration files, which enables you to create different versions of
the Scheduler. As an example, one version of the configuration file might set up the Scheduler as a single
instance using the RAMJobStore, whereas a different configuration file might configure the Scheduler to be
part of a cluster and use one of the JDBCJobStores.
To specify a configuration file other than the default, you can use the initialize() method on the
StdSchedulerFactory and specify the name of the configuration file as an argument. Listing 12.2 illustrates
an example of this.
try {
StdSchedulerFactory factory =
new StdSchedulerFactory();
factory.initialize("myquartz.properties");
scheduler = factory.getScheduler();
scheduler.start();
logger.info("Scheduler started at " + new Date());
171
172
Stopping a Scheduler
The Scheduler API includes two versions of the shutdown() method. One takes a Boolean argument, and one
takes no arguments. The Boolean parameter tells the Scheduler to wait until executing jobs are finished.
Using the no-argument version is the same as passing false to the alternative method. If you don't care about
stopping any currently executing jobs, just call this:
scheduler.shutdown();
scheduler.shutdown(false);
On the other hand, if you want the executing jobs to finish before stopping the Scheduler, pass true to the
shutdown method:
scheduler.shutdown(true);
To temporarily halt the firing of any triggers, you can call the standby() method on the Scheduler. The
Scheduler and its resources are not destroyed, and the Scheduler can be restarted at any time. Listing 12.3
shows an example of using the standby() method.
172
173
try {
// Get a Scheduler instance from the factory
scheduler = StdSchedulerFactory.getDefaultScheduler();
When a Scheduler is put in standby mode, scheduled triggers will not fire. When the Scheduler is restarted
using the start() method, all triggers that should have fired are processed based on the misfire settings.
Creating a new job class is simple. Just create a class that implements the org.quartz.Job interface. This
interface requires that you implement the execute() method, which is called when the Scheduler determines
that the job should execute.
Listing 12.4 demonstrates a simple job that checks a mail server for new mail messages for a specific user.
When the Scheduler executes this job, the execute() method is called, and the code connects to a mail server
and gets any mail messages. This job simply prints who the message is from and the subject of the mail
message.
Listing 12.4. A Quartz Job That Checks a Mail Server for Mail Messages
package org.cavaness.quartzbook.chapter12;
import java.security.NoSuchProviderException;
import java.util.Properties;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.Session;
import javax.mail.Store;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
173
174
// Default Constructor
public CheckEmailJob() {
super();
}
// Get session
Session session = null;
try {
// Get folder
Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_ONLY);
// Get directory
Message message[] = folder.getMessages();
int numOfMsgs = message.length;
if (numOfMsgs > 0) {
for (int i = 0, n = numOfMsgs; i < n; i++) {
System.out.println("(" + i + " of " + numOfMsgs + "): "
+ message[i].getFrom()[0] + "\t"
+ message[i].getSubject());
}
} else {
System.out.println("No Messages for user");
}
// Close connection
folder.close(false);
store.close();
} catch (NoSuchProviderException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
CheckEmailJob job = new CheckEmailJob();
job.checkMail();
}
}
174
175
Most of Listing 12.4 involves using the JavaMail API to access the mail server. In terms of implementing a
new Quartz job class, very little has to be done. Essentially, you implement the job interface and the
execute() method, and it's ready to be scheduled. This is shown in the next example.
As the previous example demonstrated, it's pretty straight-forward to create a Quartz job. Fortunately,
configuring a job with the Scheduler isn't much more difficult. Listing 12.5 shows an example of scheduling
the CheckEmailJob from the previous listing.
package org.cavaness.quartzbook.chapter12;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
try {
// Get a Scheduler instance from the Factory
scheduler = StdSchedulerFactory.getDefaultScheduler();
175
176
trigger.setName("emailJobTrigger");
The code in Listing 12.5 obtains the Scheduler from the StdSchedulerFactory and starts it. It then creates a
JobDetail for the CheckEmailJob and creates a trigger for the job to fire every hour, starting immediately.
The org.quartz.TriggerUtils class is convenient and contains many useful methods. One of the most
useful methods is the one that can schedule a fire-once immediate trigger. Listing 12.6 demonstrates how to
fire the CheckEmailJob only once.
package org.cavaness.quartzbook.chapter12;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
try {
// Get a Scheduler instance from the Factory
scheduler = StdSchedulerFactory.getDefaultScheduler();
176
177
Listing 12.6 uses the static makeImmediateTrigger() method on the TRiggerUtils clas and passes 0 for the
repeatCount and 0 for the repeatInterval so that the trigger fires only once.
Quartz provides the flexibility to modify jobs that are already scheduled. It supports this by allowing the
JobDetail to be replaced with a modified JobDetail. To show an example of this, let's update our
CheckEmailJob class from Listing 12.4. The version that was shown in Listing 12.4 hard-coded the mail
properties within the job class. It would be better if those properties were passed in so they could be changed
at will; let's change the CheckEmailJob to allow for that. Listing 12.7 shows an updated version of that job.
package org.cavaness.quartzbook.chapter12;
import java.util.Properties;
import javax.mail.Folder;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.NoSuchProviderException;
import javax.mail.Session;
import javax.mail.Store;
import org.quartz.Job;
import org.quartz.JobDataMap;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
public CheckEmailJob() {
super();
}
public void execute(JobExecutionContext context) throws JobExecutionException {
loadMailProperties(context.getJobDetail().getJobDataMap());
checkMail();
}
177
178
if (map.getString(HOST_KEY) != null) {
mailHost = map.getString(HOST_KEY);
}
if (map.getString(USERNAME_KEY) != null) {
username = map.getString(USERNAME_KEY);
}
if (map.getString(PASSWORD_KEY) != null) {
password = map.getString(PASSWORD_KEY);
}
}
protected void checkMail() {
// Get session
Session session = null;
try {
// Get system properties
Properties props = System.getProperties();
// Get folder
Folder folder = store.getFolder("INBOX");
folder.open(Folder.READ_ONLY);
// Get directory
Message message[] = folder.getMessages();
int numOfMsgs = message.length;
if (numOfMsgs > 0) {
for (int i = 0, n = numOfMsgs; i < n; i++) {
System.out.println("(" + i + " of " + numOfMsgs + "): "
+ message[i].getFrom()[0] + "\t"
+ message[i].getSubject());
}
} else {
System.out.println("No Messages for user");
}
// Close connection
folder.close(false);
store.close();
} catch (NoSuchProviderException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (MessagingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public static void main(String[] args) {
CheckEmailJob job = new CheckEmailJob();
job.checkMail();
}
}
The main difference between the CheckEmailJob from Listing 12.7 and the version in 12.4 is the
loadMailProperties() method. This method is called when the job is first executed and checks the
JobDataMap to see if the mail properties were set within the map. If so, those are used. If not, the defaults
within the job class are used.
178
179
Listing 12.8 shows how the properties can be set up within the JobDataMap and passed to the job. This listing
also shows how you can change the job by replacing the JobDetail with a modified instance.
package org.cavaness.quartzbook.chapter12;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
public class Listing_12_8 {
static Log logger = LogFactory.getLog(Listing_12_8.class);
try {
// Get a Scheduler instance from the Factory
scheduler = StdSchedulerFactory.getDefaultScheduler();
The code in Listing 12.8 shows two things. First, it shows how you can pass mail properties to the job class
through the JobDataMap. Second, it illustrates how you can use the addJob() method to update the
179
180
JobDetail of an already scheduled job. The addJob() method takes a Boolean argument that tells the
Scheduler whether to replace the scheduled JobDetail with the one being passed in. The job name and group
must match the one within the Scheduler for it to be replaced with the new one. Typically, your code would
retrieve the existing job, modify the contents of its JobDataMap, and then resave it.
You might also need to update an existing trigger for a job. You can replace a trigger with a different one as
long as it's for the same job. You can replace a trigger by using the rescheduleJob() method on the
Scheduler:
If you were building a GUI for Quartz, you might need to list the jobs registered with the Scheduler. Listing
12.9 presents an approach for doing just that.
package org.cavaness.quartzbook.chapter12;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.JobDetail;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerUtils;
import org.quartz.impl.StdSchedulerFactory;
try {
// Get a Scheduler instance from the Factory
scheduler = StdSchedulerFactory.getDefaultScheduler();
180
181
}
}
Listing 12.9 registers a single job, the CheckEmailJob from Listing 12.7, and then demonstrates how to loop
through the JobGroups and list the jobs within each group. In a GUI, this list could be presented in a list box
or a drop-down list.
You can also list the triggers in a manner similar to that in Listing 12.9 The code would look very similar, but
with triggers instead.
If you need to list the triggers of a single job, you can use the gettriggersOfJob() method found on the
Scheduler. This method returns a TRigger[] of the triggers associated with the job.
181
182
You might want to integrate Quartz within a Web application for several reasons. A few of the more obvious
ones are listed here:
The primary use of Quartz within a Web app is, of course, to allow easier scheduling and maintenance of jobs
through a GUI interface. Other secondary reasons include better management of running and scheduled jobs,
as well as quicker notification when things go wrong. In general, the same reasons that you would want to put
a GUI around any software application can be generalized for applications using Quartz: to make it easier to
use the application.
Integrating Quartz
Fortunately, two things make it easy to integrate Quartz into a Web application. First, the list of third-party
libraries the Quartz framework requires is pretty straightforward. Most of the third-party dependencies already
are included in any Java Web application, especially ones built with open source frameworks such as Apache
Struts. When deploying Quartz within a Web application, Quartz requires the following third-party libraries:
• Commons BeanUtils
• Commons Collections
• Commons Logging
• Commons Digester
If you've built Java Web applications before, you've seen all these listed here. A few other JARs might be
necessary, depending on your exact deployment of Quartz. For example, if Quartz stores its job information in
a database, the Standard JDBC APIs library (jdbc2_0-stdext.jar) is required, along with possibly the Java
Transaction API (jta.jar).
You might also need some optional libraries, depending on the totality of your requirements. For example, if
your application needs to send e-mails, you'll need the activation and JavaMail libraries. But this is true
182
183
whether you are deploying Quartz within a Web application or just as a stand-alone application.
Over the past several years, the Servlet and JSP specifications have improved and allowed for better
portability between tool vendors. This has had a calming effect on the Java development community and
allows Web developers to focus on the "real" business needs and not on what has to be done to get the
application to deploy and run.
As in any other Java Web application, the Servlet specification instructs that all JARs (third-party or
otherwise) must be placed into WEB-INF/lib. Therefore, one of the first steps is to put the quartz.jar file
and its dependent JARs into the WEB-INF/lib directory.
The other thing to keep in mind is that it's extremely important (and sometimes confusing) where
you install libraries. Fortunately, Web container vendors are starting to adhere to specifications
more closely, and developers are becoming more educated. For Web applications, you almost
always want to install any third-party library (specific to your application) into the WEB-INF/lib
directory. Issues with XML parsers and encryption packages such as Sun Java Secure Socket
Extensions (JSSE) still pop up, but these are becoming rare as commercial and open source
vendors update their releases.
It's entirely up to you which Java Web application framework you choose to integrate with Quartz. So many
frameworks are available that it can be quite overwhelming. To say that one particular framework is better
than another is very subjective because a lot has to do with your requirements and skill set. However, a few
Web frameworks have proven themselves over time. One example is the Apache Struts framework (formerly
known as Jakarta Struts). For the purpose of this section, we will use the Struts framework to demonstrate
how to integrate with Quartz.
183
184
Because Quartz does not directly depend on the Struts framework, you don't have to worry about the version
of Struts that you use. Just grab the latest version that's available. You should realize, however, that the Struts
and Quartz frameworks share some third-party dependencies. In fact, the required libraries we listed earlier
for Quartz are all required by the Struts framework. Just be careful about mixing up different versions, as the
note in the last section warned.
After you download Struts, you can create your directory structure and install the necessary files. For this
example, we're going to create a fictitious Web application called Job Management Console. Because this is
just a pretend application, we won't be building it to completion. Instead, we use it to explain several key
points of integration with Quartz and leave the rest for you to explore. For now, assume that the boss has
given us a task to build a GUI around our Job Scheduling framework (which, of course, would be based on
Quartz). Figures 13.1 and 13.2 show the login and main screen of the Job Management Console application.
Figure 13.1. The login screen for our Job Management Console application.
When the user presses the Login button, the application should take the user to the Dashboard screen, which is
shown in Figure 13.2.
Figure 13.2 shows the Dashboard of the console, where all users are taken after they log in.
184
185
The Dashboard page shown in Figure 13.2 is pretty simple but is good enough for our purposes.
Figure 13.2. The Job Management Console Dashboard is the main screen for users.
The project structure for the Job Management Console is very standard among Java Web applications, and
most Java Web developers will recognize the purpose of most of the directories shown. The tags directory
underneath WEB-INF will house the .tld files, used by applications to reference the custom tag libraries. The
Struts framework provides several tag libraries that can be used to make JSP development easier; the .tld
directories hold the descriptor files for those tags. Figure 13.3 shows the project structure for the application.
Figure 13.3. The project structure for the Job Management Console.
185
186
When Quartz is used from the command line, a Java class is used to create a SchedulerFactory and
instantiate a Scheduler instance. Because Quartz will now be running within a Web application, you don't
have easy access to the main() method because the application is started by the container, and main() is
buried in code, possibly even behind an actual executable. Fortunately, the solution is easy: All you have to do
is ensure that when the container first starts the Web application, you have some code that performs the
factory-creation logic. That is to say, when the container first loads the web application, you need to create a
SchedulerFactory and start the Scheduler.
186
187
When the container loads the QuartzInitializerServlet, the servlet's init() method is called. The servlet
reads several initialization parameters, creates an instance of the StdSchedulerFactory class, and initializes
the Scheduler using the specified (or default) Quartz properties file.
After the factory is created, the init() method determines whether the Scheduler should be started
immediately or whether the application should decide when to start it. Listing 3.1 shows the init() method
of the QuartzInitializerServlet.
StdSchedulerFactory factory;
try {
if (shutdownPref != null)
performShutdown = Boolean.valueOf(shutdownPref).booleanValue();
// get Properties
if (configFile != null) {
factory = new StdSchedulerFactory(configFile);
} else {
factory = new StdSchedulerFactory();
}
/*
* If the "start-scheduler-on-load" init-parameter is not specified,
* the
* scheduler will be started. This is to maintain backwards
* compatability.
*/
if (startOnLoad == null ||
(Boolean.valueOf(startOnLoad).booleanValue())) {
// Start now
scheduler = factory.getScheduler();
scheduler.start();
log("Scheduler has been started...");
} else {
log("Scheduler has not been started. Use scheduler.start()");
}
log(
"Storing the Quartz Scheduler Factory in the servlet context at key: " +
QUARTZ_FACTORY_KEY);
cfg.getServletContext().setAttribute(QUARTZ_FACTORY_KEY, factory);
} catch (Exception e) {
log("Quartz Scheduler failed to initialize: " +
e.toString());
throw new ServletException(e);
187
188
}
}
The QuartzInitializerServlet is part of the Quartz JAR file. As long as you have the quartz.jar file in
the WEB-INF/lib of the Web application, the servlet is available for you to use within your application.
The Java Servlet specification specifies that every Web application must contain a Web deployment
descriptor. The descriptor (web.xml) contains the following types of information:
• Initialization parameters
• Session configuration
• Servlet/JSP definitions
• Servlet/JSP mappings
• MIME type mappings
• Welcome file list
• Error pages
• Security
Because the QuartzInitializerServlet is a Java servlet, it must be configured within the deployment
descriptor for the container to load it. Listing 13.2 illustrates how to set up the QuartzInitializerServlet
within the web.xml file.
<web-app>
<servlet>
<servlet-name>QuartzInitializer</servlet-name>
<display-name>Quartz Initializer Servlet</display-name>
<servlet-class>
org.quartz.ee.servlet.QuartzInitializerServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>config-file</param-name>
<param-value>/some/path/my_quartz.properties</param-value>
</init-param>
<init-param>
<param-name>shutdown-on-unload</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>start-scheduler-on-load</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
</web-app>
188
189
The config-file parameter is used to specify a path and a filename for the Quartz properties file. The
StdSchedulerFactory uses this file to configure the Scheduler instance. This parameter is optional; if it is
not specified, the default quartz.properties file is used. The easiest way to use this parameter (assuming
that you want to provide your own properties file) is to put your properties file in the WEB-INF/classes
directory and specify the init-param as follows:
<init-param>
<param-name>config-file</param-name>
<param-value>/my_quartz.properties</param-value>
</init-param>
The shutdown-on-unload parameter is used to cause the scheduler.shutdown() method to be called when
the container unloads the servlet. A container unloads the servlet when it is shutting down and, in some
conditions, when it is being reloaded in a hot-deploy environment. This parameter is optional and defaults to
true.
The start-scheduler-on-load parameter is used to tell the servlet to call the start() method on the
Scheduler instance. If it is not started, the Scheduler will need to be started by the application at a later time,
and no jobs will run until the start() method is called. The parameter is optional and defaults to true if it is
not specified. This parameter was added in release 1.5 and might not be present in earlier versions.
Starting with Quartz 1.5, the QuartzInitializerServlet will automatically store the StdSchedulerFactory
instance in the ServletContext at a predetermined key.
189
190
You can see this from the end of the init() method in Listing 13.1. After the factory is stored within the
ServletContext, there are many ways to gain access to it. The easiest way, especially if you are using the
Struts framework, is to use the request object. Listing 13.3 shows a Struts Action class called
StartSchedulerAction. When this action is invoked (presumably with a URL such as
/startscheduler.do), the SchedulerFactory is retrieved, and the method getScheduler() can be called.
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.cavaness.jobconsole.web.QuartzFactoryServlet;
import org.cavaness.jobconsole.web.WebConstants;
import org.quartz.Scheduler;
import org.quartz.impl.StdSchedulerFactory;
When the Scheduler is retrieved from the SchedulerFactory, you can use the methods on the Scheduler
instance as normal. In Listing 13.3, the Scheduler is started using the start() method that you've become
accustomed to.
The benefit of storing the StdSchedulerFactory in the ServletContext is that it prevents you from having
to create it repeatedly. In fact, you can make it even easier to access the Scheduler by putting all the logic of
accessing the ServletContext and creating the Scheduler in a utility class. Listing 13.4 shows a class called
ActionUtil that makes it easier to get a reference to the Scheduler object.
190
191
Listing 13.4. The ActionUtil Class Is Convenient for Accessing the Scheduler
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
ServletContext ctx =
request.getSession().getServletContext();
StdSchedulerFactory factory =
(StdSchedulerFactory) ctx.getAttribute(
QuartzFactoryServlet.QUARTZ_FACTORY_KEY);
return factory.getScheduler();
}
}
You can use the ActionUtil getScheduler() method to conveniently retrieve the Scheduler instance. The
following code fragment shows the StartSchedulerAction class from Listing 13.3 using the ActionUtil
getScheduler() method:
}
}
The ActionUtil shown in Listing 13.4 is not part of the Quartz framework yet, but it's possible that it will be
added in the future. You can add the same thing in your application as needed.
Using a ServletContextListener
It's worth mentioning that you can configure and integrate Quartz within a Web application in another way.
Starting with version 2.3 of the Servlet API, you can create listeners that get callbacks from the servlet
container at certain times during the life cycle of the container. One of the listener interfaces is called
javax.servlet.ServletContextListener and contains two methods:
191
192
public void contextInitialized(ServletContextEvent sce);
public void contextDestroyed(ServletContextEvent sce );
The container calls these two methods when the container is started and shut down, respectively. It's possible
to initialize the Quartz Scheduler in the contextInitialized() method and shut it down from the
contextDestroyed() method. Listing 13.5 illustrates this.
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.quartz.SchedulerException;
import org.quartz.impl.StdSchedulerFactory;
/**
* Called when the container is shutting down.
*/
public void contextDestroyed(ServletContextEvent sce) {
try {
factory.getDefaultScheduler().shutdown();
} catch (SchedulerException ex) {
logger.error("Error stopping Quartz", ex);
}
/**
* Called when the container is first started.
*/
public void contextInitialized(ServletContextEvent sce) {
ctx = sce.getServletContext();
try {
factory.getScheduler().start();
ctx.setAttribute(QUARTZ_FACTORY_KEY, factory);
192
193
As we did for the QuartzInitializerServlet, we need to add some configuration information to the Web
deployment descriptor (web.xml) for the listener. For our listener, we need to add a <listener> element to
the deployment descriptor. It is shown in this snippet:
<web-app>
<listener>
<listener-class>
org.cavaness.jobconsole.web.QuartzServletContextListener
</listener-class>
</listener>
<!Other deployment descriptor info not shown >
</web-app>
A ServletContextListener has been added to the Quartz framework because of requests from the user
community. The code from Listing 13.5 should be considered an example in case you need to develop your
own listener.
Lately there has been an increased call for the application to be updated and supported, and it's starting to
draw new developers who are willing to work on it and keep it up-to-date. The application is known as the
Quartz Web application.
The upper-left side of the main screen in the Quartz Web application presents a list of features that are
available (see Figure 13.4).
193
194
Figure 13.5 shows the Scheduler Control screen, which enables you to start, stop, and pause the Scheduler.
194
195
Downloading and Building the Quartz Web Application
The Quartz Web application is part of the Quartz source repository on OpenSymphony. It used to be housed
on SourceForge, but it has recently moved over to its new home. The old site was located at
http://sourceforge.net/projects/quartz; the new site (including the Quartz Web application) can be found at
www.opensymphony.com/quartz/cvs.action.
Currently, there is no binary release of the Web application, and the standard Quartz download does not
include the Web application, although the maintainers of the application have said they will have a build set
up early 2006. For now, you need to download the Quartz source tree and build the Web application using the
Ant build file located in the quartz/webapp directory.
To download Quartz from CVS, you need an account on the java.net site because anonymous access is not
permitted. From a command line where you want to download the source tree to, type this:
After you have downloaded the Quartz and OpenSymphony source, you can build Quartz from the quartz
directory, but you also can build the Web application from the webapp directory underneath quartz. Change
directories to webapp and type ant.
When you have successfully built the Web application, there should be a quartz.war file created in the
webapp/dist directory. Deploy this Web Archive (WAR) file into a container of your choice.
Some Quartz users have reported issues with deploying the Quartz Web application as a WAR
file into WebLogic 8.1 and particular versions of WebSphere. If you're using Tomcat, you
generally won't have many problems, but you might face some challenges with other containers.
If you do encounter some trouble, try deploying the application in an exploded format instead of
as a WAR.
195
196
Now that the WAR file is deployed, fire it up in your Web browser. For Tomcat, the default URL is
http://localhost:8080/quartz/. From the home page, you have options to manage the Scheduler, jobs, triggers,
and logging.
All the menu options on the home page are secured. A new security filter was added in the latest release to
mimic container-managed security. This means that you can call HttpServletRequest.getRemoteUser()
and HttpRequest.isUserInRole() just like you can with container-managed security. Users are defined in
the SecurityFilter-config.xml file. By default, it contains one user, quartz, with the password quartz. You
can add your users to the SecurityFilter-config.xml file or simply extend
org.securityfilter.realm.SecurityRealmInterface to provide a Security Realm to suit your needs.
Start by selecting the Scheduler Control menu item and, when prompted, sign in as user quartz. A test
Scheduler is preconfigured with the Quartz Web application, and it is your only choice on this page. Take a
look inside quartz\webapp\config\resources to see how the Scheduler is configured. There you'll find the
now familiar quartz.properties file. The name in the dropdown list on the Scheduler Controller page is the
org.quartz.scheduler.instanceName property from quartz.properties.
Click the Job Definitions menu next. There you see three jobs: NativeJob, NoJob, and SendMailJob. If you
looked around in the \webapp\config\resources folder, you've already guessed where jobs are defined:
JobDefinitions.xml. This is loaded by the ContextLoaderServlet when the Quartz Web application starts.
You can edit the JobDefinitions.xml file or provide your own job-definition files by changing the
ContextLoaderServlet <init-param> in the web.xml file.
If you're ready to run a job, you can select the Create link to the left of NativeJob on the Job Definitions
page. You want to pick a native application that makes it very plain to see that your job has run. If you are on
the Windows platform, you can use Notepad.exe. Start by giving the job a namepossibly Native Job Test.
The name must be unique within the Scheduler. Next, type notepad in the command parameter and click
Save. You should now see the Job Detail properties you just created.
Click the List Jobs menu item to see all the JobDetails on the Scheduler. Click the Execute link to the left of
your JobDetail. A Notepad window pops opennot very practical, but you can imagine the possibilities. Have
you been wondering as you read this book how you could leverage Quartz for non-Java jobs? Now you have
your answer.
Now would be a good time to explore the Logging menu. Click Logging and scroll to the bottom of the
listing. There you will find an entry for the job you just executed, along with other activity that has been
occurring since you started your Scheduler.
Clicking the List all Triggers menu option shows you triggers that the Job Initializer loaded. Notice that there
is no option to add a trigger from the Quartz Web application. Triggers must be declared within the job.xml
files, which are also in the resources folder. The list of job.xml files is declared in the Job Initializer section
of the quartz.properties file.
That's all there is to the Quartz Web application. All that is left to do is add your own jobs and triggers.
196
197
What Is Workflow?
The Web is full of definitions and examples of what one person or group thinks workflow is. Some define
workflow as "the automation of the back office." Others use the phrase, "business processing modeling," and
charge a lot of money for consulting fees explaining it to you. For the purposes of this chapter, we use the
following definition of workflow:
As we get further into the chapter, this definition will become clearer.
If you're asking, "What does workflow have to work Quartz?" the answer is, "Quite a bit." Even for simple
tasks such as performing automated builds or just sending e-mails, workflow has a place. Quartz supports
some basic approaches to chaining jobs together. This chapter discusses those and shows how to integrate
Quartz with a popular open source workflow solution.
For clarity, we should make the distinction that job chaining is one Quartz job either conditionally or
unconditionally scheduling another job when the first one finishes. Using the Quartz framework alone to
accomplish this is laden with problems and limitations. It's worth going through the exercise, however, so that
you can fully understand those limitations.
We're going to make some of you mad with this statement, but as you'll learn from this material, job chaining
in Quartz is not workflow. It might have some resemblance to workflow, and it might smell like workflow or
feel like workflow, but it's definitely not workflow as you'll soon come to know it. You can think of it as the
197
198
"lazy man's workflow"sort of workflow on a shoestring budget. In all seriousness, workflow systems such as
OSWorkflow offer much more functionality than you'll get out of Quartz job chaining. This is not a knock on
Quartz: Quartz was designed for job scheduling, and it does that very well. Workflow frameworks such as
OSWorkflow are designed to do workflow. Both are great tools.
Quartz takes two main approaches to job chaining. One uses Quartz listeners; the other uses the JobDataMap.
The first approach to job chaining is to use Quartz listeners. This is done by creating either a JobListener or
a TRiggerListener that, when notified by the Scheduler, schedules the next job for execution. The method
jobWasExecuted() on the JobListener or the triggerComplete() method on the triggerListener can be
used as the location to "chain" the next job. Let's suppose that you have a job called ImportantJob that
performs some important logic for your business. You create it like any other Quartz job that you created so
far. Listing 14.1 shows the outline of the job that represents some important job your Quartz application needs
to perform.
Listing 14.1. ImportantJob Represents a Job That You Might Need to Perform for Your Business
Notice that there's nothing special about the job in Listing 14.1. Let's further suppose that you needed to chain
a second job to the completion of the ImportantJob shown in Listing 14.1. You could choose any job, but
let's make it one that prints some details about the job that ran before it. Call it PrintJobResultJob (see
Listing 14.2).
Listing 14.2. PrintJobResultJob Prints Information About the Chained Job That Ran Before It
The PrintJobResultJob is designed to look in its JobDataMap and see if a JobResult object is present. The
class JobResult is not part of the Quartz framework, but you can easily create it to represent the result of a
job execution. In many instances, creating something like a JobResult class can be helpful. Listing 14.3
198
199
public JobResult(){
startedTime = System.currentTimeMillis();
}
if (success) {
buf.append("and was successful. ");
} else {
buf.append("but was NOT successful. ");
}
return buf.toString();
}
}
The JobResult class in Listing 14.3 contains several pieces of information about the result of a job execution:
the time it started, the time it finished, and a flag that indicates whether the execution was successful. You can
obviously put whatever fields you need in your version; this is just a simple example.
The next step in this job-chaining example is to create the listener class to perform the actual chaining. For
this example, we're going to use a JobListener, but a triggerListener would work as well. Listing 14.5
shows the job-chaining JobListener.
public JobChainListener() {
super();
}
199
200
try {
// Create the chained JobDetail
JobDetail jobDetail =
new JobDetail("ChainedJob", null,
nextJobClass);
logger.info(nextJobClass.getName() +
" has been scheduled executed");
As you can see from Listing 14.5, the work of chaining the next job is done in the jobWasExecuted()
method. As you learned in Chapter 7, "Implementing Quartz Listeners," the Scheduler calls the
jobWasExecuted() method when a job is finished executing. This makes the perfect method to chain the jobs
together. In the jobWasExecuted() method from Listing 14.5, several things are going on.
200
201
First, a new JobDetail and trigger are created for the chained job. Then the JobResult is retrieved from the
JobDataMap of the first job, and the finishedTime and success fields are set. For the chained (next) job to
have access to the JobResult object, it is loaded into the JobDataMap of the chained job.
The last part of Listing 14.5 schedules the new job with the Scheduler. Because the trigger for the new job
was set to fire right away, the PrintJobResultJob job will execute immediately. Looking back at Listing
14.2, you can see that when the execute() method on the PrintJobResultJob class is called, it retrieves the
JobResult object and calls the toString() method. Again, this is a very simple example to show you how to
chain jobs. Your jobs would obviously have more complicated logic than those shown here. The job chaining,
however, would work the same.
So the good news is that it's not that difficult to use a listener class to chain jobs. The bad news is that there
are some serious design issues with this approach. First, although we were smart enough to pass in the name
of the next job to the listener class, there's a fairly tight coupling in this code. The listener needs to be told
about the chained job at creation, so the listener becomes tightly coupled to a specific chained job for the life
cycle of the listener. You would need to set up a listener for each chained joband what about when you need
to chain more than two jobs? Things can get out of hand very quickly.
Another approach for chaining jobs is to use the JobDataMap to store the next job to execute. In the earlier
listener example, we used the JobDataMap to store and pass the JobResult, but this new approach gets rid of
the listener and uses the JobDataMap to do it all.
Because we've ridden ourselves of the listener class, this means that the job itself must handle the chaining of
the next job. The behavior can be abstracted to a base job class, if you want. The example here doesn't do that,
however, to keep it as simple as possible.
Listing 14.5 shows the new ImportantJob class. After it completes, the next job in the chain gets scheduled.
You might even decide to schedule the next job based on some flag or condition of the last execution. For
example, if some flag were set to true, you would execute Job A; if the flag were set to false, you would
201
202
execute Job B.
The next job in the chain is stored in the JobDataMap using a key of your choice. In this example, we used the
value NEXT_JOB. One of the problems with this approach is that something must store the next job in the
JobDataMap before the job execution.
Listing 14.5. Job Chaining Can Also Be Done Using the JobDataMap
try {
Class jobClass = Class.forName(nextJob);
scheduleJob(jobClass, context.getScheduler());
} catch (Exception ex) {
logger.error("error scheduling chained job", ex);
}
}
}
202
203
After the name of the job is retrieved from the JobDataMap, a JobDetail and trigger are created, and the job
is scheduled. As with the listener example from before, the code that schedules the first job needs to add the
initial chained job to the JobDataMap for all this to work. This can be done when the job is first added to the
Scheduler and the Scheduler is started. Listing 14.6 shows this.
Listing 14.6. The First Chained Job Needs to Be Configured When the First Job Is Scheduled
try {
// Create and start the Scheduler
Scheduler scheduler =
StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
JobDetail jobDetail =
new JobDetail("ImportantJob", null,
ImportantJob.class);
scheduler.scheduleJob(jobDetail, trigger);
The code in Listing 14.6 is used to schedule the first job and, at the same time, set up the next job in the chain.
If there were a third job, it would have to be set up in the JobDataMap for the second job. You can obviously
see how unwieldy this approach becomes when you have more than two jobs in a chain. OSWorkflow helps
with this and a whole mess of other problems.
If you are specifying your job information in the quartz_jobs.xml file and are using the
JobInitializationPlugin to load that information, this approach might not be so bad. That's because you
can specify the job chain very easily in the XML file. For example, consider the quartz_jobs.xml in Listing
14.7.
203
204
Listing 14.7. Job Chaining Is a Little More Manageable When Using the JobInitializationPlugin
<quartz>
<job>
<job-detail>
<name>ImportantJob</name>
<job-class>
org.cavaness.quartzbook.chapter14.ImportantJob
</job-class>
<job-data-map allows-transient-data="true">
<entry>
<key>NEXT_JOB</key>
<value>org.cavaness.quartzbook.chapter14.ChainedJob</value>
</entry>
</job-data-map>
</job-detail>
<trigger>
<simple>
<name>FireOnceTrigger</name>
<group>DEFAULT</group>
<job-name>ImportantJob</job-name>
<job-group>DEFAULT</job-group>
<start-time>2005-07-19 8:31:00 PM</start-time>
<repeat-count>0</repeat-count>
</simple>
</trigger>
</job>
</quartz>
The result of the job information in Listing 14.7 is identical to that of the previous example, only better. If you
needed to change which job gets chained to the ImportantJob, you would only need to change the XML file.
In the previous job-chaining examples, code would have had to be changed and recompiled.
OSWorkflow operates on the principle of a finite state machine. A workflow consists of a set of states, with a
start state and one or more end states. To move from one state to another, a transition needs to occur. There
might be multiple transitions out of a particular statein fact, you can have multiple transitions out of a state at
the same time. The transition that is chosen depends on the environment, the inputs to the state, and some
conditional information that we discuss later.
204
205
One of the key components of OSWorkflow is the workflow descriptor, which is sometimes called the
workflow definition; we use the two terms interchangeably here. The workflow descriptor defines all the
aspects of a particular workflow. The descriptor is implemented as an XML file, and the framework includes a
DTD for validation. Much of your time working with OSWorkflow will involve understanding the layout and
rules for the descriptor file. You can view the DTD for version 2.7 at
www.opensymphony.com/osworkflow/workflow_2_7.dtd.
OSWorkflow Concepts
To help the discussion, we'll create a fictitious workflow and use it throughout the remainder of this chapter.
When created, this workflow will scan a directory looking for files. In the medical field, these could be
electronic orders from customers or patient information.
If any files are present when we check, the workflow process reads them and stores the information in a
database. The last step in the workflow generates an e-mail with a count of records that were inserted into the
database. You can imagine that if we were running a worldwide business, we would expect that files could be
dropped at any time of the day or night. That's why Quartz is so perfect for the scheduling portion.
We want to build a system using Quartz and OSWorkflow that will check for these files periodically and,
when one or more is received, process the file(s) through a specific workflow. Figure 14.1 shows the
workflow that we want to use.
205
206
As you can see from the workflow in Figure 14.1, several steps make up the workflow itself. It's a good idea
to develop a set of generic steps that can be reused in many different workflows. Let's look at a quick series of
definitions and explanations for the example workflow.
<workflow>
<initial-actions>
...
</initial-actions>
<steps>
...
</steps>
</workflow>
Workflow Step
A workflow consists of a number of states or steps. Each step is given a name of your choosing. In
OSWorkflow, the transition from one step to another is a result of an action. The following XML fragment
illustrates a step:
<results>
...
</results>
</action>
206
207
</actions>
</step>
Workflow Action
Simply put, the goal of any workflow engine is to progress the workflow from the start to the finish. That
means we need a way to transition the workflow from one step to another. In OSWorkflow, actions are used
to determine which transition path is chosen and, therefore, which step is transitioned to. A single step in the
workflow might have multiple paths (or transitions) that it can perform based on the result of the action.
Several factors can be used to help determine which path the workflow takes, including external events and
input from users. Workflow actions use conditions and functions to determine the action result and, therefore,
which transition to take. Each action must have at least one unconditional result and might have zero or more
conditional results. This helps ensure that a workflow will transition out of the step, even if none of the
conditional results causes a transition. In general, an action will result in a single transition out of a step. As
we discuss later, this is not entirely true because you can cause a split to occur where multiple transitions out
of a step take place. Eventually, these multiple transitions must join back into a single path. The following
XML fragment provides an example of a workflow action element:
Special types of actions called initial actions are used to start a workflow by specifying the first step to
execute. Here's an example of an initial-actions element:
<initial-actions>
<action id="1" name="Start Workflow">
<results>
<unconditional-result old-status="Finished"
status="Queued" step="1" />
</results>
</action>
</initial-actions>
Action Result
An action result tells the workflow what tasks to do next. OSWorkflow provides both conditional and
unconditional results. A result can have multiple condition elements that evaluate to TRue or false. The first
condition within the result element that evaluates to TRue is executed and determines the next step.
Unconditional results will execute if it's the only one present or if none of the conditional ones resulted in a
true value.
Three types of supported workflow result elements exist, regardless of whether they are conditional or
unconditional:
207
208
• Split into two or more steps
• Join multiple steps back into a single step
Workflow Functions
In OSWorkflow, most of its power and flexibility comes from the use of functions and conditions. Functions
are logic that can be performed during a transition from one step to another; they are where most of the work
takes place, especially for our Quartz + OSWorkflow integration approach. OSWorkflow includes functions
to call EJB methods, functions that use the Java Message Service (JMS), functions that send e-mails, and
many more. The function support that we are interested in is the one that can call an ordinary Java class.
After creating the FunctionProvider class, we set up the function in the workflow like this:
function type="class">
<arg name="class.name">
org.cavaness.quartzbook.chapter14.ReadFileFunction
</arg>
</function>
When the workflow gets to the step and action with this function definition, it calls our Java class and invokes
the function's execute() method. This is really powerful because you can easily integrate new and legacy
systems into your workflow.
Auto Actions
If you are building workflows with steps that you want to automatically transition without user input, you can
use the auto property on an action to force the action to occur. This means that the workflow, once started,
will run through its steps automatically and will not wait for an external event to cause it to transition.
We don't use the GUI designer here, but it's worth noting that the OSWorkflow team has built a graphical tool
to create and edit workflow definitions. The tool is a rich client that can be launched using Sun's Java Web
Start technology. Figure 14.2 shows a screenshot of the GUI tool with the example workflow loaded.
208
209
Figure 14.2. The OSWorkflow Designer is a nice tool for creating and modifying workflows.
Earlier in this chapter, when we talked about chaining jobs, each job represented a separate task. Job X
executed and performed a task, and then called on Job Y to perform a somewhat related but separate task.
There must have been some dependency between the two tasks, or you wouldn't have chained them together.
When adding workflow to this process, those individual jobs turn into steps within the workflow, and you
need to create only a single job. When notified by the Scheduler, that job starts the workflow and then waits
for the workflow to complete. This has some serious implications. The good news is tha by using
OSWorkflow, you end up with fewer Quartz jobs because what used to be jobs are now steps (actually
functions). The bad news is that if you have lots of jobs already created, it might take some work to convert
jobs into OSWorkflow functions.
209
210
You can download the full distribution of OSWorkflow from its home on the OpenSymphony site at
www.opensymphony.com/osworkflow. Grab the OSWorkflow binary from the distribution's root directory
and also the third-party libraries within the <OSWORKFLOW_DISTRIBUTION>/lib/core directory. Drop these
binaries into the lib directory of your project. This should be the same directory where the Quartz binary is
already located.
You must create a couple configuration files and place them in your classes directory. The first
configuration file you need to create is called osworkflow.xml. This file is loaded at OSWorkflow startup
and configures the runtime environment. The file for our example is shown in Listing 14.8.
Listing 14.8. The osworkflow.xml File Is Used to Configure the Runtime Environment for OSWorkflow
<osworkflow>
<persistence
class="com.opensymphony.workflow.spi.memory.MemoryWorkflowStore"/>
<factory class="com.opensymphony.workflow.loader.XMLWorkflowFactory">
<property name="resource" value="workflows.xml" />
</factory>
</osworkflow>
If you review the OSWorkflow documentation, you will find that you can choose from different types of
persistence stores and workflow factories. The ones used in Listing 14.8 are the simplest and work fine for our
examples.
The workflow factory class configured in Listing 14.8 is called the XMLWorkflowFactory, and it includes a
property called resource. The XMLWorkflowFactory is used to load a resource file that contains all the
available workflows. In this case, the resource property has a value of workflows.xml. You are allowed to
have as many different workflows as you want. Each workflow will reside in a separate XML file, but you
need some way of specifying the list of workflows that are available to the OSWorkflow engine. Because we
have specified the factory as an XMLWorkflowFactory, the framework looks in the workflows.xml file to get
the list of available workflows to load. Listing 14.9 shows the workflows.xml file for our example.
Listing 14.9. The workflows.xml File Defines the List of Workflows Available to the Application
<workflows>
<workflow
name="data-import"
type="resource"
location="data-import-workflow.xml"/>
</workflows>
Listing 14.9 lists only one workflow: data-import, which will be used to reference when we start the
workflow. The actual workflow definition is stored in the file data-import-workflow.xml. Listing 14.10
shows the data-import workflow.
210
211
<steps>
<step id="1" name="Read Files">
<actions>
<action id="2" name="Get the files" auto="true">
<pre-functions>
<function type="class">
<arg name="class.name">
org.cavaness.quartzbook.chapter14.ReadFileFunction
</arg>
</function>
</pre-functions>
<results>
<unconditional-result old-status="Finished"
status="Underway" step="2" />
</results>
</action>
</actions>
</step>
<results>
<unconditional-result old-status="Finished"
status="Underway" step="3" />
</results>
</action>
</actions>
</step>
The workflow definition in Listing 14.10 contains two steps: "read files" and "send e-mail notification. When
the workflow is started, the initial-actions section is executed, and it calls step 1. When step 1 is entered,
the execute() method in org.cavaness.quartzbook.chapter14.ReadFileFunction is called. Listing
14.11 shows this function.
211
212
Listing 14.11. The Workflow Engine Calls the ReadFileFunction During step 1
When the execute() method of the ReadFileFunction completes, the workflow transitions to step 2. In step
2, the org.cavaness.quartzbook.chapter14.SendEmailFunction is called and given a chance to execute.
The SendEmailFunction is shown in Listing 14.12.
We've obviously left out the implementation for our functions; it wouldn't have added anything to the
discussion. We assume that you know how to send an e-mail using JavaMail.
212
213
Listing 14.13. The Quartz WorkflowJob Is Designed to Call an OSWorkflowJob
/**
* Called by the scheduler to execute a workflow
*/
public void execute(JobExecutionContext context)
throws JobExecutionException {
JobDataMap jobDataMap = context.getJobDataMap();
long workflowId =
workflow.initialize(workflowName, 1, workflowInputs);
workflow.doAction(workflowId, 1, workflowInputs);
}
}
The workflow is actually started in the executeWorkflow() method in Listing 14.13. A new instance of a
workflow object is created. It's initialized with the name of the workflow that was read from the JobDataMap.
The initialize() and doAction() methods on the workflow instance take a java.util.Map as the third
argument. The values in the Map are passed to each function in the workflow through the TRansientVars
parameter. If you look back at Listing 14.11, you can see how the SCAN_DIR is being pulled from the
transientVars. This data was originally in the JobDataMap.
In this example, we take the JobDataMap from the Quartz job and pass the values into the workflow. This is
one of the ways in which integration between the two frameworks is easy and straight-forward.
Finally, Listing 14.14 shows the Scheduler code that is used to schedule the WorkflowJob and store the
workflow name and SCAN_DIR into the JobDataMap.
213
214
Listing 14.14. The WorkflowJob Is Scheduled as Normal, but the Workflow Name Must Be Stored in the JobDataMap
try {
// Create and start the Scheduler
Scheduler scheduler =
StdSchedulerFactory.getDefaultScheduler();
scheduler.start();
JobDetail jobDetail =
new JobDetail("WorkflowJob", null,
WorkflowJob.class);
// Store the scan directory and workflow name
JobDataMap dataMap = jobDetail.getJobDataMap();
dataMap.put("SCAN_DIR", "c:\\quartz-book\\input");
dataMap.put("WORKFLOW_NAME", "data-import");
You should see no surprises in Listing 14.14. The only thing of interest is that we store the workflow name in
the JobDataMap. As with the job-chaining example earlier in this chapter, if you wanted to use the
JobInitializationPlugin, you could easily specify the name of the workflow within the file.
Conclusion
What have you learned in this chapter? First, job chaining can be done with the Quartz framework. You can
use either of the presented approaches, but the approach without the listener implementation probably will
give you fewer headaches. The other lesson I hope you take away is that job chaining is not workflow. It
might seem like workflow, and you might not be convinced of this yet, but go back through the examples and
try to build one of your own. Be sure to read the OSWorkflow documentation and find out about the features
that we didn't mention. You should see that workflow is much more than Job chaining.
Finally, you should have learned that using OSWorkflow with Quartz is really pretty easy. All it takes is a few
binaries, a few configuration files, and some workflow functions. Tie all that together with a workflow
definition file, and off you go. Before you know it, you will have built a library of reusable functions and a
bunch of workflows to run your business. Pretty soon, you'll have a nice little application that you can be
proud of.
214
215
org.quartz.scheduler.instanceName
Each Quartz Scheduler must be identified by a given name. The name serves as a mechanism for client code
to distinguish Schedulers when multiple instances are used within the same program. If you are using the
clustering features, you must use the same name for every instance in the cluster that is "logically" the same
Scheduler.
org.quartz.scheduler.instanceId
Each Quartz Scheduler must be assigned a unique ID. The value can be any string but must be unique for all
schedulers. You may use the value AUTO as the instanceId if you want the ID to be generated for you.
Starting with version 1.5.1, you can customize how instance IDs are automatically generated. See the
instanceIDGenerator.class property, which is described next.
215
216
org.quartz.scheduler.instanceIdGenerator.class
Starting with version 1.5.1, this property enables you to customize how instanceIDs are generated. This
property can be used only if the property org.quartz.scheduler.instance-Id is set to AUTO. Defaults to
org.quartz.simpl.SimpleInstanceIdGenerator, which generates an instance ID based on host name and
time stamp.
org.quartz.scheduler.threadName
This can be any String that is a valid name for a Java thread. If this property is not specified, the thread will
receive the Scheduler's name (org.quartz.scheduler.instanceName) plus the appended string
'_QuartzSchedulerThread'.
org.quartz.scheduler.idleWaitTime
This property sets the amount of time in milliseconds that the Scheduler will wait before it requeries for
available triggers when the Scheduler is otherwise idle. Normally, you should not have to tune this parameter,
unless you're using XA transactions and are having problems with delayed firings of triggers that should fire
immediately.
org.quartz.scheduler.dbFailureRetryInterval
This property sets the amount of time in milliseconds that the Scheduler will wait between retries when it has
detected a loss of connectivity within the JobStore (such as to the database). This parameter is not used when
using RamJobStore.
org.quartz.scheduler.classLoadHelper.class
org.quartz.context.key.SOME_KEY
This property is used to add a name-value pair that will be placed into the "Scheduler context" as strings. (see
Scheduler.getContext()). So for example, the setting org.quartz.context.key.MyEmail =
myemail@somehostcom would perform the equivalent of scheduler.getContext().put("MyEmail","
myemail@somehost.com")
org.quartz.scheduler.userTransactionURL
This should be set to the JNDI URL where Quartz can locate the application server's UserTransaction
manager. The default value (if not specified) is java:comp/UserTransaction, which works for almost all
application servers. Websphere users might need to set this property to jta/usertransaction. This is used
only if Quartz is configured to use JobStoreCMT, and
org.quartz.scheduler.wrapJobExecutionInUserTransaction is set to true.
216
217
org.quartz.scheduler.wrapJobExecutionInUserTransaction
Set this property to true if you want Quartz to start a UserTransaction before calling execute on your job.
The transaction will commit after the job's execute method completes and after the JobDataMap is updated (if
it is a StatefulJob). The default value is false.
org.quartz.scheduler.jobFactory.class
This is the class name of the JobFactory to use. The default is org.quartz.simpl.SimpleJobFactory. You
can also try the value org.quartz.simpl.PropertySettingJobFactory. A job factory is responsible for
producing instances of job classes. The SimpleJobFactory class calls newInstance() on the job class. The
PropertySettingJobFactory also calls newInstance() but reflectively sets the job's bean properties using
the contents of the JobDataMap.
org.quartz.threadPool.class
This is used to specify the class name of the THReadPool implementation you want to use. The ThreadPool
that ships with Quartz is org.quartz.simpl.SimpleThreadPool and should be sufficient in most situations.
It has very simple behavior and is very well tested. It provides a fixed-size pool of threads that "live" the
lifetime of the Scheduler.
org.quartz.threadPool.threadCount
This specifies the number of threads that are available for concurrent execution of jobs. It can be set to a
positive integer between 1 and 100. Values greater than 100 are allowed but might be impractical. If you have
only a few jobs that fire a few times a day, one thread is plenty. If you have tens of thousands of jobs, with
many firing every minute, you probably want a thread count more like 50 or 100.
217
218
org.quartz.threadPool.threadPriority
This is used to specify the priority that the worker threads run at. The value can be any integer between 1 and
10. The default is 5, which equates to THRead.NORM_PRIORITY.
org.quartz.threadPool.makeThreadsDaemons
Set this to true to have the threads in the pool created as daemon threads. The default is false.
org.quartz.threadPool.threadsInheritGroupOfInitializingThread
Set this to true if you want new threads to inherit the group of its parent thread. The default is true.
org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread
Set this to true if you want new threads to inherit the Classloader of the parent creating thread. The default
is false.
Configuring a JobListener
To configure a JobListener, you must specify the class that implements the listener interface:
org.quartz.jobListener.NAME.class = com.foo.SomeListenerClass
NAME can be any name that you want to provide for the listener but should match the value returned from the
getName() call to the class. You can provide properties for the listener that will be set reflectively:
org.quartz.jobListener.NAME.propName = propValue
org.quartz.jobListener.NAME.prop2Name = prop2Value
Configuring a TRiggerListener
The process of configuring a triggerListener is very similar to that of a JobListener. In fact, aside from
switching jobListener for TRiggerListener, it's the same.
218
219
org.quartz.triggerListener.NAME.class =
com.foo.SomeListenerClass
org.quartz.triggerListener.NAME.propName = propValue
org.quartz.triggerListener.NAME.prop2Name = prop2Value
For more information on Quartz Listeners, see Chapter 7, "Implementing Quartz Listeners."
org.quartz.plugin.NAME.class = com.foo.MyPluginClass
Here, NAME is a name that you assign to the plug-in. You can pass parameters to a plug-in instance by
providing configuration lines like these:
org.quartz.plugin.NAME.propName = propValue
org.quartz.plugin.NAME.prop2Name = prop2Value
The NAME for the parameters must match the NAME assigned to the plug-in. For more information on Quartz
plug-ins, see Chapter 8, "Using Quartz Plug-Ins."
219
220
org.quartz.scheduler.rmi.createRegistry No String never
org.quartz.scheduler.rmi.serverPort No Integer Random
org.quartz.scheduler.rmi.proxy No Boolean False
org.quartz.scheduler.rmi.export
Set this to TRue if you want the Quartz Scheduler to export itself via RMI as a server.
org.quartz.scheduler.rmi.registryHost
This is the host at which the RMI Registry can be found. The default is localhost.
org.quartz.scheduler.rmi.registryPort
This is the port on which the RMI Registry is listening. The default is 1099.
org.quartz.scheduler.rmi.createRegistry
Set this property according to how you want Quartz to create an RMI Registry. Use false or never if you
don't want Quartz to create a registry (for example, if you already have an external registry running). Use
true or as_needed if you want Quartz to first attempt to use an existing registry and then fall back to creating
one. Use always if you want Quartz to attempt to create a registry and then fall back to using an existing one.
If a registry is created, it will be bound to a port number in the given
org.quartz.scheduler.rmi.registryPort property, and org.quartz.rmi.registryHost should be
localhost.
org.quartz.scheduler.rmi.serverPort
This indicates the port on which the Quartz Scheduler service will bind and listen for connections. By default,
the RMI service randomly selects a port as the scheduler is bound to the RMI registry.
org.quartz.scheduler.rmi.proxy
Set this property to true if you want to connect to a remotely served Scheduler. You must also then specify a
host and port for the RMI Registry process, which is typically localhost port 1099. Do not specify true for
both org.quartz.scheduler.rmi.export and org.quartz.scheduler.rmi.proxy in the same config file; if
you do, the export option will be ignored. A value of false for both export and proxy properties is, of
course valid, if you're not using Quartz via RMI.
For more information on using Quartz with RMI, see Chapter 10, "Using Quartz with J2EE."
220
221
You configure the JobStore by providing a fully qualified class that implements the JobStore interface. For
example, the following tells a Quartz application to use the RAMJobStore:
org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
• JobStoreTX
• JobStoreCMT
You can select the JobStoreTX class by setting the class name like this:
org.quartz.jobStore.class =
org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass
Quartz can use most of the popular database platforms by using a delegate. These are the allowed values for
the org.quartz.jobStore.driverDelegateClass property:
• org.quartz.impl.jdbcjobstore.StdJDBCDelegate
• org.quartz.impl.jdbcjobstore.MSSQLDelegate
• org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
• org.quartz.impl.jdbcjobstore.WebLogicDelegate (for WebLogic drivers)
221
222
• org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
• org.quartz.impl.jdbcjobstore.oracle.WebLogicOracleDelegate
• org.quartz.impl.jdbcjobstore.oracle.weblogic.WebLogicOracleDelegate
• org.quartz.impl.jdbcjobstore.CloudscapeDelegate
• org.quartz.impl.jdbcjobstore.DB2v6Delegate
• org.quartz.impl.jdbcjobstore.DB2v7Delegate
• org.quartz.impl.jdbcjobstore.HSQLDBDelegate
• org.quartz.impl.jdbcjobstore.PointbaseDelegate
org.quartz.jobStore.dataSource
The value for this property must be the name of one the DataSources defined in the DataSource
configuration section later in this appendix.
org.quartz.jobStore.tablePrefix
The table prefix property is a string equal to the prefix given to Quartz's tables that were created in your
database. You can have multiple sets of Quartz tables within the same database if they use different table
prefixes.
org.quartz.jobStore.useProperties
The "use properties" setting instructs the JDBC JobStore that all values in JobDataMaps will be Strings and,
therefore, can be stored as name-value pairs instead of storing more complex objects in their serialized form in
the BLOB column. This can be helpful because you avoid the class-versioning issues that can arise from
serializing your non-String classes into a BLOB.
org.quartz.jobStore.misfireThreshold
Set this property to the number of milliseconds the Scheduler will tolerate a trigger to pass its next-fire time
before it is considered misfired. The default value is 60000 (60 seconds).
org.quartz.jobStore.isClustered
Set this to true to turn on clustering features. This property must be set to TRue if you are using multiple
instances of Quartz and the same set of database tables.
org.quartz.jobStore.clusterCheckinInterval
Set the frequency (in milliseconds) at which this instance checks in with the other instances of the cluster.
This value affects the quickness of detecting failed instances.
org.quartz.jobStore.maxMisfiresToHandleAtATime
This is the maximum number of misfired triggers the JobStore will handle in a given pass. Handling many
(more than a couple dozen) at once can cause the database tables to be locked long enough to hamper the
performance of firing other (not yet misfired) triggers.
222
223
org.quartz.jobStore.dontSetAutoCommitFalse
Setting this parameter to TRue tells Quartz not to call setAutoCommit(false) on connections obtained from
the DataSource(s). This can be helpful in a few situations, such as if you have a driver that complains if it is
called when it is already off. This property defaults to false because most drivers require
setAutoCommit(false) to be called.
org.quartz.jobStore.selectWithLockSQL
This must be a SQL string that selects a row in the LOCKS table and places a lock on the row. If it is not set, the
default is SELECT * FROM {0}LOCKS WHERE LOCK_NAME = ? FOR UPDATE, which works for most databases.
The {0} is replaced during runtime with the TABLE_PREFIX that you configured earlier.
org.quartz.jobStore.txIsolationLevelSerializable
Configuring JobStoreCMT
The JobStoreCMT provides an alternate JobStore that can work with a relational
database. You can choose the JobStoreCMT by selecting it with the
org.quartz.jobStore.class property:
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreCMT
When using the JobStoreCMT, a few additional properties can be set. These are
shown in Table A.5.
org.quartz.jobStore.nonManagedTXDataSource
JobStoreCMT requires a (second) datasource that contains connections that will not be part of
container-managed transactions. The value of this property must be the name of one of the datasources
defined in the configuration properties file. This datasource must contain non-CMT connectionsin other
words, connections for which it is legal for Quartz to directly call commit() and rollback() on.
223
224
org.quartz.jobStore.dontSetNonManagedTXConnectionAutoCommitFalse
org.quartz.jobStore.txIsolationLevelReadCommitted
For more information on JDBC JobStores, see Chapter 6, "JobStores and Persistence."
Each datasource that you define must be given a name, and the properties you
define for each must contain that name. The dataSource's NAME can be anything
you want; it has no meaning and simply identifies the datasource when it is
assigned to the JDBC JobStore.
224
225
org.quartz.dataSource.NAME.driver
This must be the Java class name of the JDBC driver for your database.
org.quartz.dataSource.NAME.URL
This is the connection URL (host, port, and so on) for connection to your database.
org.quartz.dataSource.NAME.user
org.quartz.dataSource.NAME.password
org.quartz.dataSource.NAME.maxConnections
This is the maximum number of connections that the DataSource can create in its pool of connections.
org.quartz.dataSource.NAME.validationQuery
This is an optional SQL query string that the DataSource can use to detect and replace failed/corrupt
connections. For example, an Oracle user might choose select table_name from user_tables, which is a
query that should never fail unless the connection is actually bad.
When you're using the Datasource defined in an application server, the properties in Table A.7 are available.
Table A.7. The Properties for Configuring Quartz to Use a Datasource from an Application Server
Name
Required
Type
Default Value
org.quartz.dataSource.NAME.jndiURL
Yes
String
null
org.quartz.dataSource.NAME.java.naming.factory.initial
No
String
225
226
null
org.quartz.dataSource.NAME.java.naming.provider.url
No
String
null
org.quartz.dataSource.NAME.java.naming.security.principal
No
String
null
org.quartz.dataSource.NAME.java.naming.security.credentials
No
String
null
org.quartz.dataSource.NAME.jndiURL
This is the JNDI URL for a DataSource that is managed by your application server.
org.quartz.dataSource.NAME.java.naming.factory.initial
This is the (optional) class name of the JNDI InitialContextFactory that you want to use.
org.quartz.dataSource.NAME.java.naming.provider.url
org.quartz.dataSource.NAME.java.naming.security.principal
This is the (optional) user principal for connecting to the JNDI context.
org.quartz.dataSource.NAME.java.naming.security.credentials
These are the (optional) user credentials for connecting to the JNDI context.
226
227
org.quartz.dataSource.myConnProvider.connectionProvider.class = com.foo.MyConnectionProvider
After instantiating the class, Quartz can automatically set configuration properties on the instance, bean style.
org.quartz.dataSource.myConnProvider.someStringProperty = someValue
org.quartz.dataSource.myConnProvider.someIntProperty = 5
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
227
228
actions (OSWorkflow)
ActionUtil class, getScheduler( ) method
addGlobalJobListener( ) method
addGlobalTriggerListener( ) method
addJob( ) method
addJobListener( ) method
addTriggerListener( ) method
administrative functions, Scheduler
creating and starting the Scheduler
standby mode
stopping a Scheduler
alerts, sending (job scheduling scenario)
AnnualCalendar class, excluding bank holidays (code listing)
application container, persistent job storage
applications
jobs versus
packaging
sample [See sample Quartz application.]
assignment of jobs to specific instances, clustering
asterisk (*), cron expressions
asynchronous job scheduling (timer service)
auto actions (OSWorkflow)
AUTO feature, clustering
availability, Quartz clustering
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Calendar
creating calendars
228
229
parameters
usage scenarios
CheckEmailJob
CheckForInterruptJob code listing
class diagram (Quartz)
client class (Quartz RMI)
clients
Java RMI applications
Quartz RMI
configuration
testing
cluster nodes, running
clusterCheckin( ) method
clusterCheckinInterval property (JobStores)
clustering capabilities
benefits
high availability
load balancing
scalability
configuration
JobStore DataSource
JobStores
loading database with Scheduler information
quartz.properties file
detection of failed Scheduler nodes
how it works
Quartz Scheduler on startup
recovery of jobs from failed instances
running Quartz cluster nodes
troubleshooting
applying global listeners
assignment of jobs to specific instances
retrieving a list of executing jobs from cluster
running clustered and nonclustered instances together
running jobs on every node in cluster
running nodes on separate machines
time synch services
CMT (Container Managed Transactions) 2nd
column names (database tables)
comma (,), cron expressions
commercial job schedulers, list of
concepts (OSWorkflow)
action results
actions
auto actions
functions
steps
config-file initialization parameter (QuartzInitializerServlet)
configuration
JobStoreCMT
DataSources
DriverDelegate class
JobStoreTX
database table prefixes
229
230
Datasources
DriverDelegate class
setting JobStore property
ParentPlugin
Quartz applications, reference
Quartz for clustering
JobStore DataSource
JobStores
loading database with Scheduler information
quartz.properties file
Quartz in Eclipse
Quartz RMI clients
Quartz RMI servers
Quartz Web application
quartz.properties file
RAMJobStore class
Web deployment descriptor
ConnectionProvider, configuration of DataSource
Container Managed Transactions (CMT) 2nd
contextDestroyed( ) method
contextInitialized( ) method
createRegistry property (Quartz RMI Registry)
createVolatileScheduler( ) method
cron [See also CronTriggers.]
daily cron expressions
minute cron expressions
monthly cron expressions
Quartz format
UNIX format
weekly cron expressions
crontabs
CronTriggers 2nd 3rd [See also cron, daily cron expressions.]
JobInitializationPlugin and
start and end times
TriggerUtils class with
custom ConnectionProvider, configuration of DataSource
CVS repository, downloading from 2nd
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
daemon threads
daily cron expressions
Dashboard screen (Web application Job Management Console)
data-import workflow
database table prefixes, configuration
database tables, column names
databases, job storage
230
231
DataSources
configuration 2nd
custom ConnectionProvider
for JobStoreTX
managed DataSources
nonmanaged DataSources
of JobStores
J2EE container
sample code listing
declarative approach
implementation of listeners
programmatic approach versus
scheduling jobs
deleteJob( ) method
dependencies, third-party libraries
deployment models, Quartz/J2EE
J2EE container DataSource
J2EE resources
J2EE servers 2nd
running as J2SE client 2nd
detection, failed Scheduler nodes
directory structure
Quartz
Struts framework
Web applications
DirectSchedulerFactory
disabling info log messages
downloading
Eclipse
OSWorkflow
Quartz
Quartz Web application
DriverDelegate class, configuration 2nd
durable jobs
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
231
232
enterprise
Enterprise JavaBeans (EJB), Timer service
errors
JobExecutionException
SchedulerConfigException
SchedulerException
examples [See sample Quartz application.]
execute( ) method 2nd
PrintJobResultJob class
Quartz job interface
executeWorkflow( ) method
executing jobs immediately
execution time, job instances
exploded format
extension points
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
file filters
file transfers (job scheduling scenario)
FileExtensionFileFilter
FileScanJob, scheduling
FileScanListener interface
implementation
scheduling the FileScanJob
fileUpdated( ) method (FileScanListener interface)
filters
fire-now triggers
fire-once immediate triggers
fixed-priority scheduling (threads)
functions (OSWorkflow)
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
232
233
TriggerListener interface
getScheduler( ) method 2nd
getTriggersOfJob( ) method
global listeners
clustered environments
nonglobal listeners versus
GUI designer, workflow
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
implementation
FileScanListener interface
JobListener interface code listing
JobStores interface
listeners
creating the listener class
methods
registering listener
SchedulerListener interface
TriggerListener interface
info log messages, disabling
init( ) method
233
234
initialization of Quartz within Web applications
initialize( ) method
JobLoaderPlugin class
SchedulerPlugin interface
StdSchedulerFactory
installation
JDBC-persistent JobStore tables
OSWorkflow
Quartz
Quartz libraries, integration of Quartz with Web applications
Quartz Web application
instance IDs, AUTO feature when clustering
instanceName property, matching clients and servers
instances
creating Scheduler instances (SchedulerFactory)
execution time
RemoteScheduler
instantiate( ) method (StdSchedulerFactory)
integration
Quartz and Web applications
accessing the SchedulerFactory and Scheduler
configuration of Web deployment descriptor
installation of Quartz libraries
QuartzInitializerServlet
selection of Web application framework
ServletContextListener
structure of Web applications
Struts framework
Quartz with OSWorkflow
downloading and installing OSWorkflow
Internet Time Servers (ITS)
interrupt( ) method
InterruptableJob interface
interrupting jobs
introspection, converting parameter values in quartz.properties file
isClustered property (JobStores)
ITS (Internet Time Servers)
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
234
235
running as J2SE client 2nd
servers, deploying Quartz within 2nd
J2SE clients, running Quartz as
EJBInvokerJob 2nd
Jakarta Struts [See directory structure, Struts framework.]
JAR files
installing Quartz
quartz.properties file
Web applications
Java
properties files
RMI
clients
Object Registry
requirements
servers
threads
daemon threads
lifecycle
priorities
processes versus
run( ) method
start( ) method
ThreadGroups
ThreadPools
Timer class
TimerTask class
Java 2 Platform, Enterprise Edition [See J2EE (Java 2 Platform, Enterprise Edition).]
JDBC JobStores 2nd
JDBC, persistent JobStores dependency on
job chaining
JobDataMap
JobInitializationPlugin
listeners
workflow versus
job instances, execution time
Job Management Console project structure, Struts framework
job scheduling 2nd 3rd [See also CronTriggers.]
advantages of
declarative approach
defined 2nd
DirectSchedulerFactory code listing
programmatic approach
Quartz clustering capabilities
benefits
configuration
detection of failed Scheduler nodes
how it works
Quartz Scheduler on startup
recovery of jobs from failed instances
running Quartz cluster nodes
troubleshooting
QuartzScheduler
DirectSchedulerFactory
235
236
hierarchy
StdSchedulerFactory
Scheduler API
job management
managing the Scheduler
running
SchedulerFactory
SimpleTriggers code listing
solutions compared
triggers
usage scenarios
workflow versus
JobDataMap object
accessing through the JobExecutionContext object code listing
availability on triggers
job chaining
jobs management
parameters
passing configuration information to jobs code listing
stateful jobs
JobDetail class
code listing
jobs management
JobExecutionContext class
accessing the JobDataMap through code listing
jobs management
JobExecutionException error
jobExecutionVetoed( ) method (JobListener interface)
JobInitializationPlugin 2nd 3rd
changing the XML file
CronTriggers and
job chaining
modifying quartz.properties file for
quartz_jobs.xml file
JobInitializationPluginMultiple
JobListener interface
implementation code listing
methods
registering nonglobal JobListeners
JobLoaderPlugin class 2nd
creating job file for
JobRunShell class, run( ) method
jobs [See also job chaining; job instances; ; job scheduling.]
applications versus
creating job class 2nd
executing immediately
firing one at a time
interruption
listing jobs in Scheduler
listing triggers in Scheduler
management
JobDataMap object
JobDetail class
JobExecutionContext class
236
237
org.quartz.Job interface
stateful versus stateless jobs
parameters, hard-coding versus passing
Quartz framework
removing from Scheduler
replacing an existing scheduled job
runtime behavior
durability
recoverability
volatility
stateful jobs
storage
creating new JobStores
JobStore interface
memory (nonpersistent) storage
persistent storage
updating an existing trigger
jobScheduled( ) method (SchedulerListener interface)
jobsPaused( ) method (SchedulerListener interface)
jobsResumed( ) method (SchedulerListener interface)
JobStore interface
configuration
JobStoreCMT
JobStoreTX
JobStore settings (quartz.properties file)
JobStoreCMT class 2nd 3rd
configuration 2nd
DataSources
DriverDelegate class
JobStores class
configuration of Quartz for clustering
creating new JobStores
JDBC
persistent JobStores
application container
JDBC support
JobStoreCMT
JobStoreSupport class
JobStoreTX
loading jobs and triggers into database
performance improvement
required tables
stand-alone environment
JobStoreSupport class
JobStoreTX 2nd
configuration 2nd
database table prefixes
Datasources
DriverDelegate class
setting JobStore property
running Quartz with
jobToBeExecuted( ) method (JobListener interface)
jobUnScheduled( ) method (SchedulerListener interface)
jobWasExecuted( ) method 2nd
237
238
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
238
239
defining list of workflows available with workflows.xml file
DirectSchedulerFactory
EJBInvokerJob sample application
example ScanDirectoryJob
excluding bank holidays with AnnualCalendar class
excluding minutes from every hour using HourlyCalendar class
FileExtensionFileFilter in ScanDirectoryJob
FileScanListener interface implementation
fire-once trigger for the CheckEmailJob
HourlyCalendar class execution
implementation of Quartz listeners with quartz_jobs.xml file
ImportantJob
init( ) method of the QuartzInitializerServlet class
initializing Quartz with ServletContextListener
job chaining
with JobDataMap
with JobInitializationPlugin
JobDetail class
JobListener interface
implementation
methods
JobLoaderPlugin class
JobResult representation of a job execution
listing jobs in Scheduler
modifying quartz.properties file for JobInitializationPlugin
modifying web.xml file for QuartzInitializerServlet
multiple jobs in quartz_jobs.xml file
nonglobal JobListener that performs job chaining
nonglobal TriggerListeners
osworkflow.xml file used to configure runtime environment for OSWorkflow
ParentPlugin loading child plug-ins in order
passing configuration information to job using JobDataMap
PrintInfoJob
PrintJobResultJob prints information about chained job
putting Scheduler in stand-by mode
Quartz DataSource
Quartz job that checks a mail server for mail
quartz.properties file
Clustered instance
JobLoaderPlugin
Quartz RMI clients
RMI server
quartz_jobs.xml file
J2EE client
ScanDirectoryJob
ReadFileFunction
running a simplified Quartz Scheduler
running multiple instances of ScanDirectoryJob
sample InterruptableJob
sample Quartz RMI client
SchedulerListener interface
implementation
methods
SchedulerPlugin interface implementation
239
240
scheduling jobs
CheckEmailJob
FileScanJob
programmatically
with SimpleTriggers
second job XML file
SendEmailFunction
SimpleJobListener
as a global JobListener
as a nonglobal JobListener
SimpleSchedulerListener class
SimpleTriggerListener as a global TriggerListener
single job XML file
standby mode of Scheduler
starting Quartz RMI server with QuartzRMIServer
starting Scheduler using different Quartz configuration file
starting the default Scheduler
startserver.bat file used to start RMI server
startTime and endTime with CronTrigger
startup class called to start Scheduler
stateless session bean
TriggerListener interface
implementation
methods
TriggerUtils to execute immediate jobs
updated CheckEmailJob that allows properties to be passed in
updating a scheduled job
web.xml for Quartz J2EE client applications
WorkflowJob
load balancing, clustering capabilities
loading jobs
RAMJobStore class
Scheduler
loadJobs( ) method
loadMailProperties( ) method
log message formats, trigger events
logging info log messages, disabling
LoggingJobHistoryPlugin
LoggingTriggerHistoryPlugin
login screen (Web application Job Management Console)
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
makeImmediateTrigger( ) method
managed DataSources, configuration
management
jobs
240
241
JobDataMap object
JobDetail class
JobExecutionContext class
org.quartz.Job interface
stateful versus stateless jobs
Scheduler
shutdown( ) method
standby( ) method
start( ) method
memory (nonpersistent) storage, RAMJobStore class
configuration
disadvantages
job volatility property
loading jobs
methods
addGlobalJobListener( )
addGlobalTriggerListener( )
addJob( )
addJobListener( )
addTriggerListener( )
clusterCheckin( )
contextDestroyed( )
contextInitialized( )
createVolatileScheduler( )
deleteJob( )
execute( ) 2nd
executeWorkflow( )
fileUpdated( )
getDefaultScheduler( ) 2nd
getInstance( )
getMergedJobDataMap( )
getName( ) 2nd
getScheduler( ) 2nd
getTriggersOfJob( )
helloWorld( )
init( )
initialize( )
JobLoaderPlugin class
SchedulerPlugin interface
StdSchedulerFactory
instantiate( )
interrupt( )
jobExecutionVetoed( )
JobListener interface
jobScheduled( )
jobsPaused( )
jobsResumed( )
jobToBeExecuted( )
jobUnScheduled( )
jobWasExecuted( ) 2nd
listeners
loadJobs( )
loadMailProperties( )
makeImmediateTrigger( )
241
242
processFileAndScheduleJobs( )
processJobs( )
rescheduleJob( )
run( ) 2nd
schedulerError( )
SchedulerListener interface
SchedulerPlugin interface
schedulerShutdown( )
schedulerStarted( ) 2nd
setDaemon( )
setDurability( )
setJobsDirectory( )
setMinuteExcluded( )
setName( )
setPriority( )
setRollbackOnly( )
setVolatility(true)
setXXX( )
shutdown( )
managing the Scheduler
Scheduler
SchedulerPlugin interface
standby( ) 2nd
start( )
JobLoaderPlugin class
managing the Scheduler
SchedulerPlugin interface
threads
toString( )
triggerComplete( ) 2nd
triggerFinalized( )
triggerFired( )
TriggerListener interface
triggerMisfired( )
triggersPaused( )
triggersResumed( )
vetoJobExecution( )
minute cron expressions
modifying quartz.properties file for JobInitializationPlugin
monthly cron expressions
multiple instances of ScanDirectoryJob
multiple jobs (calendars)
multiple plug-ins
multiple triggers
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
242
243
New state (threads)
non-preemptive running of threads
nonclustered instances, running with clustered instances
nonglobal listeners
global listeners versus
registering
nonglobal TriggerListeners
code listing
registering
nonmanaged DataSources, configuration
nonpersistent (memory) storage, RAMJobStore class
configuration
disadvantages
job volatility property
loading jobs
NthIncludedDayTriggers
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
243
244
packaging applications
parameters
Calendar interface
EJBInvokerJob
JobDataMap
passing to plug-ins
QuartzInitializerServlet
config-file
shutdown-on-unload
start-scheduler-on-load
ParentPlugin, configuration
passing parameters (jobs), hard-coding versus
pausing a Scheduler, standby( ) method
performance improvement, persistent JobStores
persistent JobStores
application container
JDBC support
JobStoreCMT
configuration
JobStoreSupport class
JobStoreTX
configuration
running Quartz with
loading jobs and triggers into database
performance improvement
required tables
installation
stand-alone environment
plug-in settings (quartz.properties file)
plug-ins
configuration
defined
JobInitializationPlugin
job chaining
multiple plug-ins
configuration of ParentPlugin
registering
JobLoaderPlugin job file
passing parameters to
specifying properties
SchedulerPlugin interface
creating plug-ins
methods
utility plug-ins
JobInitializationPlugin
JobInitializationPluginMultiple
LoggingJobHistoryPlugin
LoggingTriggerHistoryPlugin
ShutdownHookPlugin
pound (#), cron expressions
preemptive running of threads
PrintInfoJob code listing
PrintJobResultJob class, execute( ) method
priorities, Java threads
244
245
processes, Java threads versus
processFileAndScheduleJobs( ) method
processJobs( ) method
programmatic approach
declarative approach versus
scheduling jobs
projects [See sample Quartz application.]
properties [See also quartz.properties file.]
DataSource configuration
JobStoreCMT configuration 2nd
JobStoreTX configuration 2nd
managed DataSources
plug-ins
Quartz DataSources configuration
Quartz RMI clients
Quartz RMI settings configuration
RMI servers
Scheduler
ThreadPool configuration
properties files
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Quartz
building from source code
configuring in Eclipse
cron format
directory structure
downloading
history of
installing
integration (OSWorkflow)
J2EE deployment models
J2EE container DataSource
J2EE resources
J2EE servers 2nd
running as J2SE client 2nd
job chaining
JobDataMap
JobInitializationPlugin
listeners
workflow versus
sample application [See sample Quartz application.]
user community
Web applications
accessing the SchedulerFactory and Scheduler
configuration of Web deployment descriptor
245
246
integration
loading jobs into database
Quartz Web application
QuartzInitializerServlet
ServletContextListener
Struts framework
Quartz applications [See applications.]
Quartz Calendar
creating calendars
parameters
usage scenarios
Quartz JAR files, quartz.properties file
Quartz jobs [See jobs, applications versus.]
Quartz listeners [See listeners.]
Quartz plug-ins [See plug-ins.]
Quartz RMI
client class
clients
Registry
server startup class
servers
testing RMI clients and servers
Quartz Scheduler [See Scheduler.]
Quartz triggers
CronTriggers
multiple triggers
NthIncludedDayTriggers
SimpleTriggers
quartz.properties file 2nd
code listing
configuring 2nd
creating a Scheduler
modifying for JobInitializationPlugin
packaging applications
quartz_jobs.xml file
CronTriggers and
implementing listeners
QuartzInitializerListener class
QuartzInitializerServlet 2nd
parameters
config-file
shutdown-on-unload
start-scheduler-on-load
QuartzSchedulerResources class
QuartzSchedulerThread class
Query Analyzer tool
question mark (?), cron expressions
246
247
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
247
248
Runnable state (threads)
running
Quartz cluster nodes
Quartz Web application
Scheduler
runtime behavior (jobs)
durability
recoverability
volatility
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
248
249
loading jobs
managing the Scheduler
shutdown( ) method
standby( ) method
start( ) method
programmatic approach
properties
removing jobs from
running
SchedulerFactory
stand-by mode
startup class code listing
startup in a cluster
StdSchedulerFactory
threads
QuartzSchedulerResources class
QuartzSchedulerThread class
WorkerThreads
triggers
Scheduler Control screen (Quartz Web application)
scheduler properties (quartz.properties file)
SchedulerConfigException
schedulerError( ) method
SchedulerException error
SchedulerFactory 2nd
accessing with QuartzInitializerServlet
SchedulerListener interface
implementation
methods
SchedulerMain class
SchedulerPlugin interface
creating plug-ins
JobInitializationPlugin
JobLoaderPlugin class 2nd
methods
schedulerShutdown( ) method
schedulerStarted( ) method 2nd
scheduling jobs [See job scheduling.]
screenshots, Quartz Web application
SendEmailFunction
SendMailJob
serialization, EJBInvokerJob
server startup class (Quartz RMI)
servers
Java RMI applications
Quartz RMI
configuration
testing
ServletContextListener, integration of Quartz and Web applications
servlets, QuartzInitializerServlet
setDaemon( ) method
setDurability( ) method
setJobsDirectory( ) method
setMinuteExcluded( ) method
249
250
setName( ) method
setPriority( ) method
setRollbackOnly( ) method
setVolatility(true) method
setXXX( ) methods, plug-in properties
SFSBs (stateful session beans)
shutdown( ) method
managing the Scheduler
Scheduler
SchedulerPlugin interface
shutdown-on-unload initialization parameter (QuartzInitializerServlet)
ShutdownHookPlugin
SimpleSchedulerListener class
SimpleThreadPool class
SimpleTriggers 2nd
scheduling jobs code listing
slash (/), cron expressions
SLSB (Stateless Session Bean)
source code, building Quartz from
special characters, Quartz cron format
SQL files, installation of required database tables
SQL tool, loading jobs
stand-alone environments, persistent job storage
stand-by mode (Scheduler) 2nd
standby( ) method 2nd
start times, CronTrigger
start( ) method
JobLoaderPlugin class
managing the Scheduler
SchedulerPlugin interface
threads
start-scheduler-on-load initialization parameter (QuartzInitializerServlet)
starting Scheduler
startup class, Quartz RMI servers
stateful jobs
JobDataMap object
stateless jobs versus
stateful session beans (SFSBs)
StatefulJob interface
stateless jobs, stateful jobs versus
Stateless Session Bean (SLSB)
StdSchedulerFactory 2nd 3rd
creating using properties Instance code listing
initialize( ) method
steps (OSWorkflow)
stopping Scheduler
storage of jobs
creating new JobStores
JobStore interface
memory (nonpersistent) storage
RAMJobStore class
persistent storage
application container
JDBC support
250
251
JobStoreCMT
JobStoreSupport class
JobStoreTX
loading jobs and triggers into database
performance improvement
required tables
stand-alone environment
structure of Web applications
Struts framework, integration of Quartz with Web applications
directory structure
intialization
Job Management Console project structure
synchronization, cluster nodes 2nd
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
251
252
NthIncludedDayTriggers
SimpleTriggers
trigger events, log message formats
triggerComplete( ) method 2nd
triggerFinalized( ) method
triggerFired( ) method
TriggerListener interface
configuration
implementation
methods
registering nonglobal TriggerListeners
triggerMisfired( ) method
triggers 2nd
CronTriggers 2nd
JobInitializationPlugin and
start and end times
TriggerUtils class with
JobDataMap object
listing triggers in Scheduler
multiple triggers
NthIncludedDayTriggers
Quartz jobs, updating
SimpleTriggers
TriggerUtils class, fire-now triggers
triggersPaused( ) method
triggersResumed( ) method
TriggerUtils class
fire-now triggers
firing jobs one at a time
with CronTriggers
troubleshooting Quartz clustering
applying global listeners
assignment of jobs to specific instances
retrieving a list of executing jobs from cluster
running clustered and nonclustered instances together
running jobs on every node in cluster
running nodes on separate machines
time synch services
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
UICronTriggers
UNIX cron format
Quartz cron format versus
unsynchronized clocks, running cluster nodes 2nd
usage scenarios
Quartz calendars
252
253
Quartz listeners
user community for Quartz
utility plug-ins
JobInitializationPlugin
JobInitializationPluginMultiple
LoggingJobHistoryPlugin
LoggingTriggerHistoryPlugin
ShutdownHookPlugin
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
Index
[SYMBOL] [A] [B] [C] [D] [E] [F] [G] [H] [I] [J] [L] [M] [N] [O] [P] [Q] [R] [S] [T] [U] [V] [W] [X]
253
254
intialization
Job Management Console project structure
WEB-INF/classes directory
Web Archive (WAR) files
Web deployment descriptor, configuration
WEB-INF/classes directory (Web applications)
weekly cron expressions
Wescott, Jeffrey
WorkerThreads
workflow
concepts
action results
actions
auto actions
functions
steps
creating a workflow job
defined
GUI designer
job chaining versus
job scheduling versus
Quartz integration
downloading and installing OSWorkflow
workflow descriptor file
workflow descriptor file
WorkflowJob
254