Job Scheduling in Java
Pages: 1, 2, 3, 4

Beyond the Ordinary

So far, we saw how we can schedule tasks in an application, and for simple requirements that is quite enough. But for more advanced users and complex requirements, a lot more features are needed to support fully useful scheduling. In such cases, there are two common solutions that one could choose. The first is to build your own scheduler with the desired functionality; the second is to locate a project that can fulfill your specific needs. The second solution is more appropriate in most cases, because you can save time and resources and won't have to duplicate someone else's effort.



This brings us to Quartz, an open source job-scheduling system with significant advantages over the simple Timer API.

The first advantage is persistence. If your jobs are "static," as in our introductory example, then maybe you don't need to persist jobs. But we often find tasks need to be "dynamically" triggered when a certain condition is met, and you have to be sure that those tasks won't be lost between system restarts (or crashes). Quartz offers both non-persistent and persistent jobs, in which state is saved in a database, so you can be sure that those jobs won't be lost. Persistent jobs introduce additional performance overhead in the system, so you have to use them pragmatically.

The Timer API also lacks methods to simplify setting the desired execution time. As we saw in the example above, the most you can do is to set a start date and a repeat interval. Surely, everyone who has used the Unix cron tool would like to see similar configuration possibilities within their scheduler. Quartz defines org.quartz.CronTrigger, which lets you set a desired firing date in a flexible manner.

Developers often need one more feature: managing and organizing jobs and tasks by their names. In Quartz, you can get jobs by their names or presence in a group, which can be a real help in environments with a large number of scheduled jobs and triggers.

Now, let's implement our report generation example using Quartz and explain basic features of the library.

package net.nighttale.scheduling;

import org.quartz.*;

public class QuartzReport implements Job {

  public void execute(JobExecutionContext cntxt)
    throws JobExecutionException {
      System.out.println(
        "Generating report - " +
cntxt.getJobDetail().getJobDataMap().get("type")
      );
      //TODO Generate report
  }

  public static void main(String[] args) {
    try {
      SchedulerFactory schedFact 
       new org.quartz.impl.StdSchedulerFactory();
      Scheduler sched  schedFact.getScheduler();
      sched.start();
      JobDetail jobDetail 
        new JobDetail(
          "Income Report",
          "Report Generation",
          QuartzReport.class
        );
      jobDetail.getJobDataMap().put(
                                "type",
                                "FULL"
                               );
      CronTrigger trigger  new CronTrigger(
        "Income Report",
        "Report Generation"
      );
      trigger.setCronExpression(
        "0 0 12 ? * SUN"
      );
      sched.scheduleJob(jobDetail, trigger);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

Quartz defines two basic abstractions, Jobs and Triggers. A Job is the abstraction of the work that should be done, and Trigger represents time when the action should occur.

Job is an interface, so all we have to do is to make our class implement the org.quartz.Job interface (or org.quartz.StatefulJob, as we will see in a moment) and override the execute() method. In the example, we saw how one can pass parameters to the Job through the jobDataMap attribute, which is a modified implementation of java.util.Map. Deciding whether you are going to implement stateful or non-stateful jobs is a matter of deciding whether you want to change these parameters during execution. If you implement Job, all of the parameters are saved at the moment the job is scheduled for the first time, and all changes made later are discarded. If you change the StatefulJob's parameters in the execute() method, this new value will be passed when the job triggers another time. One important implication to consider: StatefulJobs can't be executed concurrently, because the parameters might change during the execution.

There are two basic kinds of Triggers: SimpleTrigger and CronTrigger. SimpleTrigger provides basically the same functionality you get from the Timer API. It should be used if the Job should be triggered once, followed possibly by repeats at a specific interval. You can specify start date, end date, repeat count, and repeat interval for this kind of trigger.

In the example above, we used CronTrigger because of its flexibility to schedule jobs on a more realistic basis. CronTriggers allow us to express schedules such as "every weekday at 7:00 p.m." or "every five minutes on Saturday and Sunday." We will not go further into explaining cron expressions in this article, so you are advised to find all of the details about its possibilities in the class' Javadoc.

In order to run the above example, you'll need a file named quartz.properties in the classpath, with basic Quartz configuration. If you want to use a different name for the file, you should pass it as a parameter to the StdSchedulerFactory constructor. Here is an excerpt of the file with minimal properties:

#
# Configure Main Scheduler Properties 
#

org.quartz.scheduler.instanceName = TestScheduler
org.quartz.scheduler.instanceId = one

#
# Configure ThreadPool 
#

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount =  5
org.quartz.threadPool.threadPriority = 4

#
# Configure JobStore 
#

org.quartz.jobStore.misfireThreshold = 5000

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore

Another advantage of Quartz versus the standard Timer API is its usage of a thread pool. Quartz uses this thread pool to get threads for job execution. The size you choose for the thread pool affects the number of jobs that can be executed concurrently. If the job needs to be fired and there is no free thread in the pool, it will sleep until a thread becomes available. How many threads to use in the system is a tough decision, and it is best to determine it experimentally. The default value is five, which is quite enough if you are not dealing with thousands of Jobs. There is one thread pool implementation in Quartz itself, but you are not limited on its usage.

Now we come to the JobStores. JobStores keep all the data about Jobs and Triggers. So, this is where we need to decide if we are going to keep our Jobs persistent or not. In the example, we used org.quartz.simpl.RAMJobStore, which means that all of the data will be kept in memory and thus will not be persistent. As a result, if the application crashes, all the data about scheduled Jobs will be lost. In some situations, this is the desired behavior, but when you want to make the data persistent, you should configure your application to use org.quartz.simpl.JDBCJobStoreTX (or org.quartz.simpl.JDBCJobStoreCMP). JDBCJobStoreTX requires some more configuration parameters that will be explained by example.

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_

#
# Configure Datasources 
#

org.quartz.dataSource.myDS.driver = org.postgresql.Driver
org.quartz.dataSource.myDS.URL = jdbc:postgresql:dev
org.quartz.dataSource.myDS.user = dejanb
org.quartz.dataSource.myDS.password =
org.quartz.dataSource.myDS.maxConnections  5

In order to successfully use a relational database with Quartz, first we need to create the tables it needs. You can use any available database server that has an appropriate JDBC driver. In the docs/dbTables folder, you'll find initialization scripts to generate the necessary tables.

After that, we should declare the delegate class that adapts standard SQL queries to the specific RDBMS SQL dialect. In our example, we have chosen PostgreSQL as the database server so the appropriate org.quartz.impl.jdbcjobstore.PostgreSQLDelegate class is submitted. For information what delegate to use with your specific server, you should consult the Quartz documentation.

The tablePrefix parameter defines what prefix is used for Quartz tables in the database (the default is QRTZ_). This way, you can distinguish these tables from the rest of the database.

For every JDBC store, we need to define datasource to be used. This is part of the common JDBC configuration and will not be further explained here.

The beauty of Quartz is that after these configuration changes are made, our report-generation example would persist the data in the database without changing a single line of code.

Pages: 1, 2, 3, 4

Next Pagearrow