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.

40 comments:

  1. hello This post is really helful too me Thank You Very Much.
    I had tried it on my own application wat happens is wen the quartz protion is executed it will terminate tomcat.
    So i will be grateful to you if any help will be provided.

    ReplyDelete
  2. I see you've also posted your question on stackoverflow. Perhaps you could post there the bit of code you're using and if there are any other errors reported in the tomcat log files. I don't really have any experience with eclipse so I'm thinking stackoverflow would generate better answers.

    ReplyDelete
  3. This comment has been removed by the author.

    ReplyDelete
  4. This comment has been removed by the author.

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. This comment has been removed by the author.

    ReplyDelete
  8. This comment has been removed by the author.

    ReplyDelete
  9. srry its my mistake.I had solved the issue.The code contains system.exit(0);thats y this happens.But thanks 4 your help.

    ReplyDelete
  10. Quartz Schedular is limited for windows only ??? I think so if u have ne idea please clear my doubt.

    ReplyDelete
  11. No. Quartz is java based so will work on any platform with java available.

    ReplyDelete
  12. how could i initialize the servlet to run all the time coz servlet contains the code of scheduling and i want it to run whether the project is running or not.please help me.had done googling but not able to find much.

    ReplyDelete
  13. I had tried to use the load-on-startup but it is not working for me.I am writing the scheduling code in init method of servlet so that it can be run on start-up of project.But it is not working please help me.

    ReplyDelete
  14. If i need to do somthing like this : "i am using tomcat and want to schedule a task of sending mails from tomcat.how can i use quartz to do this?" Is it possible?

    ReplyDelete
  15. 1. The code can only run when tomcat and your project(web app) is running.

    2. For load-on-startup not working, which servlet class have you specified to load?

    3. Are you first able to schedule a simple task e.g. System.out.println("test").

    ReplyDelete
  16. This comment has been removed by the author.

    ReplyDelete
  17. Thanks for your answer and yes I had scheduled task, and there is a problem I m using corn trigger and I want to update the trigger expression according to user input time.

    Trigger[] triggers = sche.getTriggersOfJob(name,group);
    for (Trigger trigger : triggers) {
    if (trigger instanceof CronTrigger) {
    CronTrigger cronTrigger = (CronTrigger) trigger;
    cronExpr = cronTrigger.getCronExpression();
    }
    }

    cronExpr is giving null it is not going into the for loop.

    ReplyDelete
  18. I can't tell you why no triggers are returned. Maybe something to do with the quartz database or configuration. Hopefully you can google and get a solution.

    ReplyDelete
  19. Had used the same db table and in configuration file m not able to understand one thing plz help me.
    "org.quartz.jobStore.dataSource = anyString"

    what should i keep in the place of anyString?
    I think the value of one of the fields mentioned below?????

    "org.quartz.dataSource.myDS.driver = oracle.jdbc.driver.OracleDriver
    org.quartz.dataSource.myDS.URL = jdbc:oracle:thin:@10.0.1.23:1521:demodb
    org.quartz.dataSource.myDS.user = myUser
    org.quartz.dataSource.myDS.password = myPassword
    org.quartz.dataSource.myDS.maxConnections = 30"

    ReplyDelete
  20. I had used the quartz.properties file of this tutorial i need to mention any other parameter??????Thanks for reply.

    ReplyDelete
  21. For your example, "anyString" should be "myDS"

    ReplyDelete
  22. Then please help me from today morning i am trying this but not able to figure out any thing.All settings seems to be right then it is not going into the for loop.

    ReplyDelete
  23. Have you read the quartz documentation/tutorial on the quartz website? That will help with understanding.

    Does the trigger record exist in the quartz table in your demodb?

    ReplyDelete
  24. yes.Thanks I am done with it.I used myDS and it worked.

    ReplyDelete
  25. Hello.I need to get cron expression from the database or from the properties file while starting the job.But m not able to get it.It is giving error.

    "validateJarFile(C:\Program Files\Apache Software Foundation\Tomcat 5.5\webapps\\WEB-INF\lib\servlet.jar) - jar not loaded. See Servlet Spec 2.3, section 9.7.2. Offending class: javax/servlet/Servlet.class"

    So any solution for this?
    Actually I need to set time by user at that specific time the task will run.but the problem is that if we restart the tomcat i need to start the job again that is in db.how could i be able to do this?

    ReplyDelete
  26. what I basically need is the job is stored in quartz database I need to run the same job if tomcat restarts.

    ReplyDelete
  27. By default, jobs that don't run at their given time e.g. if tomcat is not running, will run when the application next restarts. Look at the quartz documentation on misfire instructions.

    ReplyDelete
  28. I know about the things you told me.But i am confused.
    Let me explain you what i am confused about with taking example that is mentioned above.

    See we are providing a servlet at tomcat start up in which we are providing the job details

    Ex.
    JobDetail job1 = new JobDetail("myjobDetail", "myjobDetailGroup", MyJob.class);

    and trigger details

    Ex.
    SimpleTrigger simpleTrigger = new SimpleTrigger ("mysimpleTrigger",mytriggerGroup");

    and schedule the job

    Ex.

    myscheduler.scheduleJob(job1, simpleTrigger);
    myscheduler.start();

    So basically when tomcat starts this servlet will run each and everytime .So each and every time i need to unschedule and delete the job before defining it.Else it will give error when tomcats starts.So what i am asking is "Is there any way that i don't want to define that job in servlet just retrieve it from the table that quartz provides to save the job.And then schedule it there."

    ReplyDelete
  29. I still don't understand. You want to reschedule the job?

    ReplyDelete
  30. This comment has been removed by the author.

    ReplyDelete
  31. I am getting this error n quartz is also not working i had googled but not able to found anything .y the log4j.xml is not parsed.Please help me.
    log4j:ERROR Could not parse url [file:/C:/Program%20Files/Apache%20Software%20Fo
    undation/Tomcat%205.5/webapps/PRIMARY05/WEB-INF/classes/log4j.xml].
    com.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException: Inval
    id byte 1 of 1-byte UTF-8 sequence.
    at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.invalidByte(Unk
    nown Source)
    at com.sun.org.apache.xerces.internal.impl.io.UTF8Reader.read(Unknown So
    urce)
    at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.load(Unknown
    Source)
    at com.sun.org.apache.xerces.internal.impl.XMLEntityScanner.skipSpaces(U
    nknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.skipSeparat
    or(Unknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.scanDecls(U
    nknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDTDScannerImpl.scanDTDExte
    rnalSubset(Unknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDri
    ver.dispatch(Unknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$DTDDri
    ver.next(Unknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl$Prolog
    Driver.next(Unknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentScannerImpl.next(U
    nknown Source)
    at com.sun.org.apache.xerces.internal.impl.XMLDocumentFragmentScannerImp
    l.scanDocument(Unknown Source)
    at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(U
    nknown Source)
    at com.sun.org.apache.xerces.internal.parsers.XML11Configuration.parse(U
    nknown Source)
    at com.sun.org.apache.xerces.internal.parsers.XMLParser.parse(Unknown So
    urce)
    at com.sun.org.apache.xerces.internal.parsers.DOMParser.parse(Unknown So
    urce)
    at com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderImpl.parse(Unk
    nown Source)
    at javax.xml.parsers.DocumentBuilder.parse(Unknown Source)
    at org.apache.log4j.xml.DOMConfigurator$2.parse(DOMConfigurator.java:612
    )
    at org.apache.log4j.xml.DOMConfigurator.doConfigure(DOMConfigurator.java
    :711)
    at org.apache.log4j.xml.DOMConfigurator.doConfigure(DOMConfigurator.java
    :618)
    at org.apache.log4j.helpers.OptionConverter.selectAndConfigure(OptionCon
    verter.java:470)
    at org.apache.log4j.LogManager.(LogManager.java:122)
    at org.apache.log4j.Logger.getLogger(Logger.java:104)
    at org.apache.commons.logging.impl.Log4JLogger.getLogger(Log4JLogger.jav
    a:283)
    at org.apache.commons.logging.impl.Log4JLogger.(Log4JLogger.java:1
    08)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)

    at sun.reflect.NativeConstructorAccessorImpl.newInstance(Unknown Source)

    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(Unknown Sou
    rce)
    at java.lang.reflect.Constructor.newInstance(Unknown Source)
    at org.apache.commons.logging.impl.LogFactoryImpl.createLogFromClass(Log
    FactoryImpl.java:1116)
    at org.apache.commons.logging.impl.LogFactoryImpl.discoverLogImplementat
    ion(LogFactoryImpl.java:914)
    at org.apache.commons.logging.impl.LogFactoryImpl.newInstance(LogFactory
    Impl.java:604)
    at org.apache.commons.logging.impl.LogFactoryImpl.getInstance(LogFactory
    Impl.java:336)
    at org.apache.commons.logging.LogFactory.getLog(LogFactory.java:704)
    at org.apache.catalina.core.ContainerBase.getLogger(ContainerBase.java:3
    81)
    at org.apache.catalina.core.StandardContext.start(StandardContext.java:4
    159)
    at org.apache.catalina.core.ContainerBase.addChildInternal(ContainerBase
    .java:760)
    at org.apache.catalina.core.ContainerBase.addChild(ContainerBase.java:74
    0)

    ReplyDelete
  32. at org.apache.catalina.core.StandardHost.addChild(StandardHost.java:544)

    at org.apache.catalina.startup.HostConfig.deployDirectory(HostConfig.jav
    a:980)
    at org.apache.catalina.startup.HostConfig.deployDirectories(HostConfig.j
    ava:943)
    at org.apache.catalina.startup.HostConfig.deployApps(HostConfig.java:500
    )
    at org.apache.catalina.startup.HostConfig.start(HostConfig.java:1203)
    at org.apache.catalina.startup.HostConfig.lifecycleEvent(HostConfig.java
    :319)
    at org.apache.catalina.util.LifecycleSupport.fireLifecycleEvent(Lifecycl
    eSupport.java:120)
    at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1022)

    at org.apache.catalina.core.StandardHost.start(StandardHost.java:736)
    at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1014)

    at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:443
    )
    at org.apache.catalina.core.StandardService.start(StandardService.java:4
    48)
    at org.apache.catalina.core.StandardServer.start(StandardServer.java:700
    )
    at org.apache.catalina.startup.Catalina.start(Catalina.java:552)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
    at java.lang.reflect.Method.invoke(Unknown Source)
    at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:295)
    at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:433)
    log4j:WARN No appenders could be found for logger (org.apache.commons.digester.D
    igester.sax).
    log4j:WARN Please initialize the log4j system properly.
    .......In HelloSchedule.........
    Pleas help me.
    The last line is the servlet that intitialize the quartz.Thanks in advance.

    ReplyDelete
  33. Seems like it may be something to do with invalid characters or encoding. See
    http://stackoverflow.com/questions/2513829/invalid-byte-1-of-1-byte-utf-8-sequence
    http://stackoverflow.com/questions/1871340/java-malformedbytesequenceexception-xml
    http://www.coderanch.com/t/451431/Struts/resolve-MalformedByteSequenceException-Invalid

    Perhaps you can google more in that direction (malformedbytesequence)

    ReplyDelete
  34. I am using xml file that is mentioned above.I made new xml file n place the content given above.previously it is working.But as of now it is giving error.

    ReplyDelete
  35. If you say it was working before, I don't know what has changed since and am unable to help.

    ReplyDelete
  36. error: JobDetail is abstract; cannot be instantiated
    JobDetail job1 = new JobDetail("myjobDetail", "myjobDetailGroup", MyJob.class);

    ReplyDelete
  37. Quartz sedular not taking properties file that you configured above, its taking default configure file from the library.
    Note : i have created quartz.properties in the /web-inf/classes

    ReplyDelete
  38. Bài viết của tác giả rất hữu ích, cám ơn bạn đã chia sẻ.
    Xem tại website : Gia công thạch anh

    ReplyDelete