14 Sep 04

Quartz Quick Start

When I need to turn commanded automation into scheduled automation, I generally reach for cron. I’ve gotten used to its syntax and other peculiarities over the years, so cron is a tool I’m comfortable wielding. But recently I’ve been writing an automated monitoring program for a client requiring a scheduler that can live inside of their Java application and run on heterogeneous platforms. Java ports of cron are available, but I needed something with a bit more horsepower and flexibility.

Quartz is an open source job scheduling system written in Java that can be embedded within a Java/J2EE application or run as a stand-alone application. And yet those qualifications alone didn’t convince me to add the Quartz JAR file to my project. (I’m fairly picky about what libraries I’ll introduce into a project.) What did convince me to use Quartz is it makes the simple things easy and the complex things possible.

For now, I just need to do simple things: schedule recurring jobs. And before I can do the complex things, I learn by doing the simple things first. So I thought I’d share how I got started using Quartz as a way of introducing it to you. As a result, you’ll have another automation tool within reach.

Download and Install

Start by downloading Quartz.

Installation is a simple matter of adding quartz.jar and commons-logging.jar to your classpath (ok, so it’s two JAR files). You’ll find various other JAR files in the Quartz distribution, but thankfully those two JAR files are all you need to get started.

Create A Job

Next, you need to write some Java code to create a job and put it on a schedule.

Say, for example, you’re developing a build scheduler that runs Ant build files at regular intervals. (The world certainly doesn’t need another Java build scheduler with great continuous integration tools such as CruiseControl already there for the taking, but it’s an example that’s hopefully near and dear to your heart.)

Somewhere in guts of your build scheduler you create a job:

  JobDetail jobDetail =
      new JobDetail("buildJob", "buildGroup", BuildJob.class);

The JobDetail instance represents a schedulable job. The first two parameters uniquely identify the job by its name and group. The third parameter refers to a Java class that implements the Job interface. We’ll actually write the BuildJob class a bit later. But first, notice that the JobDetail refers to a class, which means you can’t call any instance methods to set the state of a Job instance. And because the scheduler will create a new instance of BuildJob each time the job is executed, the job’s instance variables are cleared between executions.

That appears to be a problem because you want your build scheduler to run a specified Ant build file and target. Therefore, when an instance of your BuildJob class is created it will need to be supplied two variables: the build file and target. Quartz has a mechanism for shuttling data to jobs when they’re run. All you need to do is put the data in the JobDataMap associated with the JobDetail instance:

  jobDetail.getJobDataMap().put("buildfile", "build.xml");
  jobDetail.getJobDataMap().put("target", "test");

The JobDataMap behaves like a Java Map with a key-value pair. We’ll see the data in the JobDataMap appear on the other side when we write the BuildJob class.

Tune A Trigger

Next, define when the job should run. Let’s say you want the Ant build file to run every 60 seconds indefinitely. To do that, create a trigger with the schedule properties:

  Date startTime = new Date();    // immediately
  Date endTime = null;            // forever
  long repeatInterval = 60000L;   // 60 seconds
  int repeatCount = SimpleTrigger.REPEAT_INDEFINITELY;

  SimpleTrigger trigger =
    new SimpleTrigger("buildTrigger", Scheduler.DEFAULT_GROUP,
                      startTime, endTime,
                      repeatCount, repeatInterval);

The SimpleTrigger is good for simple interval-based schedules. It runs the job at a specific moment in time and optionally repeats running it on an interval. Alternatively, you can use the CronTrigger to run a job on a calendar-like schedule. (Now you know another reason I like Quartz.) For example, to run the Ant build at 2:30 am, Monday through Friday, use:

  CronTrigger trigger =
    new CronTrigger("buildTrigger", Scheduler.DEFAULT_GROUP,
                    "0 30 2 ? * MON-FRI");

If you’re not familiar with cron syntax, that funny string supplied as the last parameter is a cron expression with the following form:

  Seconds Minutes Hours DayOfMonth Month DayOfWeek

Cron expressions give you a lot of scheduling flexibility. For a full range of examples, refer to the CronTrigger JavaDoc.

Schedule The Job

Until now we have not associated the job with its trigger. Indeed, jobs and triggers are decoupled. One benefit of this design is a job can be assigned to many triggers. Now we’re ready to schedule the job.

First, create a scheduler:

  SchedulerFactory factory = new StdSchedulerFactory();
  Scheduler scheduler = factory.getScheduler();

Then schedule the job by associating the job and its trigger:

  scheduler.scheduleJob(jobDetail, trigger);

The schedule is set, but jobs won’t actually fire until the scheduler has been started:

  scheduler.start();

(Jobs can also be scheduled while the scheduler is running.)

At some later time, make sure to gracefully shut down the scheduler:

  boolean waitForJobsToComplete = true;
  scheduler.shutdown(waitForJobsToComplete);

Write A Job Class

Before running the scheduler, we need to write a class that implements the Job interface. This brings us back to the BuildJob class.

You put the Java code that gets executed when the job runs in the method called execute(). Here’s an example implementation of the BuildJob class:

  public class BuildJob implements org.quartz.Job {

    public BuildJob() { }

    public void execute(JobExecutionContext context)
        throws JobExecutionException {

        JobDataMap map = context.getJobDetail().getJobDataMap();
        String buildFile = map.getString("buildfile");
        String target = map.getString("target");

        try {

            AntRunner runner = new AntRunner();
            runner.run(buildFile, target);

        } catch(Exception e) {
            throw new JobExecutionException(e);
        }
    }
  }

Notice that a JobExecutionContext instance is passed to the execute() method when the job is run. From the JobExecutionContext instance you’re able to get the JobDataMap instance containing the data defined when the job was scheduled—the Ant build file and the target.

The BuildJob simply delegates to the AntRunner class—a custom class I wrote that runs the Ant build file and target. By making Job classes as thin as possible—simply delegating the real work to a class not dependent on Quartz—the job execution code is easier to test and potentially reuse elsewhere in the system.

Notice also that the only checked exception the execute() method is allowed to throw is a JobExecutionException. Thus, any unhandled exceptions (checked or unchecked) raised during job execution should be caught and rethrown as a JobExceptionException.

Advanced Scheduling

We’ve barely tapped into the potential of Quartz. Thankfully, although it lets you do complex things, you can get started quickly and then wade into advanced features if you need them. If you want to dig deeper, the tutorial goes into more detail without overwhelming you with options.

Have fun!