Tuesday, May 11, 2010

Using Quartz

Quartz is an open source job scheduling library that can be used within a Java application.

Using quartz in a Java EE application

Prerequisistes
JRE
Servlet engine
RDBMS + JDBC driver for storing jobs in a database

Versions used
JRE 1.6.0_20
Apache Tomcat 6.0.20
MySQL 5.0.45
MySQL connector/J 5.1.10
Quartz 1.8.0

Steps
  • Download the package from the quartz website, http://www.quartz-scheduler.org/
  • Unzip the package to a temporary location e.g. C:\temp
  • Add the quartz tables to your application's database schema. The scripts with the quartz table schemas will be in c:\temp\quartz-1.8.0\docs\dbtables.
C:\> mysql -h localhost --database=mydb --user=dbuser --password=dbpassword
mysql> \.  c:\temp\quartz-1.8.0\docs\dbtables\tables_mysql_innodb.sql
mysql> quit
  • Create indexes on the quartz tables just created by running the following additional script
create index idx_qrtz_t_next_fire_time on qrtz_triggers(NEXT_FIRE_TIME);
create index idx_qrtz_t_state on qrtz_triggers(TRIGGER_STATE);
create index idx_qrtz_t_nf_st on qrtz_triggers(TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_ft_trig_name on qrtz_fired_triggers(TRIGGER_NAME);
create index idx_qrtz_ft_trig_group on qrtz_fired_triggers(TRIGGER_GROUP);
create index idx_qrtz_ft_trig_n_g on qrtz_fired_triggers(TRIGGER_NAME,TRIGGER_GROUP);
create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(INSTANCE_NAME);
create index idx_qrtz_ft_job_name on qrtz_fired_triggers(JOB_NAME);
create index idx_qrtz_ft_job_group on qrtz_fired_triggers(JOB_GROUP);
  • Copy the file c:\temp\quartz-1.8.0\quartz-all-1.8.0.jar to your application's web-inf\lib folder e.g. tomcat\webapps\myapp\web-inf\lib
  • Copy all the jar files in c:\temp\quartz-1.8.0\lib to tomcat\webapps\myapp\web-inf\lib
  • Create a file named quartz.properties in myapp\web-\classes with the following details
# quartz configuration

# jobstore
org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX

org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate

# datasource
org.quartz.jobStore.dataSource = anyString

org.quartz.dataSource.anyString.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.anyString.URL = jdbc:mysql://localhost/mydb
org.quartz.dataSource.anyString.user = dbuser
org.quartz.dataSource.anyString.password = dbpassword
org.quartz.dataSource.anyString.validationQuery=select 1

# thread pool
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5

# disable quartz version update check
org.quartz.scheduler.skipUpdateCheck=true
  • Create a file named log4j.xml in myapp\web-\classes with the following details
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">

<log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">

  <appender name="default" class="org.apache.log4j.ConsoleAppender">
    <param name="target" value="System.out"/>
    <layout class="org.apache.log4j.PatternLayout">
      <param name="ConversionPattern" value="[%p] %d{dd MMM yyyy HH:mm:ss.SSS} %t [%c]%n%m%n%n"/>
    </layout>
  </appender>
    
 <logger name="org.quartz">
   <level value="info" />     
 </logger>

  <root>
    <level value="warn" />
    <appender-ref ref="default" />
  </root>
  
</log4j:configuration>
  • Create a servlet class to be running the scheduler
package my.app;

import javax.servlet.*;
import javax.servlet.http.*;

import org.quartz.impl.StdSchedulerFactory;
import org.quartz.utils.*;

import org.quartz.*;

public class TestScheduler extends HttpServlet
{
Scheduler myscheduler;
public void init(ServletConfig config) throws ServletException
{
try
{
//start scheduler and run a test job
// Initiate a Schedule Factory
SchedulerFactory schedulerFactory = new StdSchedulerFactory();
        // Retrieve a scheduler from schedule factory
        myscheduler = schedulerFactory.getScheduler();
                        
        // Initiate JobDetail with job name, job group, and executable job class
        JobDetail job1 = new JobDetail("myjobDetail", "myjobDetailGroup", MyJob.class);
        // Initiate SimpleTrigger with its name and group name
        SimpleTrigger simpleTrigger = new SimpleTrigger("mysimpleTrigger", "mytriggerGroup");
        
//example setting int parameter for the job
job1.getJobDataMap().put("int-parameter-name",5);

//schedule the job and start the scheduler
myscheduler.scheduleJob(job1, simpleTrigger);
        
        // start the scheduler
        myscheduler.start();
}
catch(Exception e)
{
System.err.println(e);
}
}

public void destroy()
{
//shut down the scheduler
try
{
myscheduler.shutdown(true);
}
catch(Exception e)
      {
      System.err.println(e);
      }
}
}
  • Include the scheduler class in the application's web.xml file so that it runs on startup
 <servlet>
  <servlet-name>TestScheduler</servlet-name>
  <servlet-class>my.app.TestScheduler</servlet-class>
  <!-- Load this servlet at server startup time. Number not special. Just indicates the sequence of loading servlets -->
  <load-on-startup>3</load-on-startup>
 </servlet>
  • Create a class that will be doing the work. The job class. This class needs to implement the org.quartz.job interface
package my.app;

import org.quartz.*;

public class MyJob implements Job
{

//no-argument public constructor
public MyJob()
{
}

//execute method of job interface that does the work
public void execute (JobExecutionContext context) throws JobExecutionException
{
//do anything here.

//you can take parameters passed by the scheduler and use them e.g
JobDataMap dataMap=context.getMergedJobDataMap();
int myIntVariable;
myIntVariable=dataMap.getInt("int-parameter-name");

if (myIntVariable==1)
{ 
//do something
}
else
{
//do something else
}
}
}
  • Instead of creating your own class to start the scheduler, you can use one provided by quartz. Modify the web.xml to have the following
<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>3</load-on-startup>    
</servlet>
  • A default scheduler instance will now be automatically created and started when the application starts, and automatically shut down when the application is stopped.

  • To access this scheduler within the application e.g. In a jsp page, you can retrieve the scheduler instance from the servlet context. You can have the following
<%@ page import="org.quartz.*,org.quartz.impl.*,org.quartz.utils.*,org.quartz.ee.servlet.QuartzInitializerServlet" %>

<%
  StdSchedulerFactory factory = (StdSchedulerFactory) getServletConfig().getServletContext().getAttribute(QuartzInitializerServlet.QUARTZ_FACTORY_KEY);
  Scheduler scheduler=factory.getScheduler();
  
  // Initiate JobDetail with job name, job group, and executable job class
        JobDetail job1 = new JobDetail("myjobDetail", "myjobDetailGroup", MyJob.class);
        // Initiate SimpleTrigger that will fire immediately
        SimpleTrigger simpleTrigger = new SimpleTrigger("mysimpleTrigger", "mytriggerGroup");        
        job1.getJobDataMap().put("int-parameter-name",5);                
        
        scheduler.scheduleJob(job1, simpleTrigger);
  %>
  • You can create jobs and triggers according to the application's logic and user inteface components used and then schedule using the scheduler object.


Deleting jobs and triggers
You can't add a trigger or job if another one with a similar name and group exists. Use methods of the scheduler object to do the deletion. An exception is not raised if the job or trigger doesn't exist.
scheduler.deleteJob("job name","job group");
scheduler.unscheduleJob("trigger name","trigger group");

Checking if a cron expression is valid
If using a cron trigger, and the expression provided isn't valid, an exception will be raised when creating the trigger. To avoid this you can check whether the expression is valid before creating the trigger object. There's a static method in the CronExpression class for this.
if (CronExpression.isValidExpression(myCronString)){

Determining next run job run time
You can determine the next time a job will run using the trigger's getFireTimeAfter method
java.util.Date nextRunDate=myTrigger.getFireTimeAfter(new java.util.Date())

Or within the job implementation's execute method,
public void execute(JobExecutionContext context) throws JobExecutionException {

java.util.Date nextRunDate=context.getTrigger().getFireTimeAfter(new java.util.Date());

}

Setting job end date
You can set the date on which a job should start or end using the associated trigger's setStartTime and setEndTime methods. By default, a trigger's start time is the time the object is instantiated with no end date. One can set the end date without specifying the start date and vice versa. The end date can't be before the start date, else an exception will be thrown.

Setting quarz properties in code
Instead of having the quartz configuration properties residing in a properties file, you can create a scheduler instance with the properties defined from code. For instance if you don't want to have the database username/password in clear text in the properties file. You'll need to create a java.util.Properties object, populate it with all the relevant quartz properties and then pass the properties object to the StdSchedulerFactory constructor e.g.
import java.util.*;
import org.quartz.*;
import org.quartz.impl.*; 

props=new Properties();
props.setProperty("org.quartz.threadPool.threadCount","10");
//...set other properties

//create scheduler instance 
SchedulerFactory schedulerFactory = new StdSchedulerFactory(props);      
org.quartz.Scheduler scheduler = schedulerFactory.getScheduler();
scheduler.start(); 

//if doing this from a servlet that's loaded on startup, you can put the scheduler instance in the servlet context so that you can access it from anywhere within the application

//save scheduler in the servlet context, to make it accessible throughout the application
getServletConfig().getServletContext().setAttribute("myscheduler",scheduler);

//to access the scheduler elsewhere in the application e.g. to schedule new jobs
Scheduler scheduler=(Scheduler) getServletConfig().getServletContext().getAttribute("myscheduler");
scheduler.scheduleJob(someJobObject, someTriggerObject);

//make sure to call the scheduler's shutdown method in the servlet's destroy method
If creating the scheduler instance like this, you won't need the QuartzInitializerServlet entry in the web.xml file.