Saturday, 4 July 2015

PART 1: Spring MVC web-services (Designing RESTful API)

This is the first article in a series that will detail how to use Spring modules to build a RESTful web-service.

What technologies will be used?

  • Spring MVC
  • Spring Data JPA
  • Hibernate Validation
  • Spring Cache abstraction

What is the project layout?

This project is divided into multiple modules, it will grow as we progress in our series. However, for this post it only has two modules. These are Persistence and Services
CoffeeBeansRest
    |
    |----- Persistence
    |           |----- src
    |                    |----- main
    |                             |----- java
    |                             |----- resources
    |                    |----- test
    |                             |----- java
    |                             |----- resources
    |----- Services
    |           |----- src
    |                    |----- main
    |                             |----- java
    |                             |----- resources
    |                    |----- webapps
    |                             |----- java
    |                             |----- resources
    |                    |----- test
    |                             |----- java
    |                             |----- resources
    |
    |----- Other modules

Figure.1 project layout


Creating the project:

1. Create a parent project

In CoffeeBeansRest directory add pom.xml. In this pom file we can add all the dependancies that will be common across all modules, for instance logging or testing

    4.0.0

    com.coffeebeans
    coffeebeans
    pom
    1.0-SNAPSHOT
    
        Persistence
        Services
    

    
        <java.version>1.8</java.version>
        <spring.version>4.1.4.RELEASE</spring.version>
    

    
        
            
                maven-compiler-plugin
                
                    <source>${java.version}</source>
                    <target>${java.version}</target>
                
            
        
    

    
        
            junit
            junit
            4.11
            test
        

        
            org.projectlombok
            lombok
            1.14.8
            provided
        

        
            ch.qos.logback
            logback-classic
            1.1.2
        

        
            ch.qos.logback
            logback-core
            1.1.2
        

Notice :
  • The value in packaging element is pom, which allows this to be parent for other projects.
  • The module element has two sub-elements telling us about the modules we intend to include in this project.


2. Create the Persistence module

  • Create a directory inside CoffeeBeansRest and call it Persistence
  • Create the usual maven layout for simple project inside it (refer Figure.1) (if you using intellij, add module choose maven then input the artifactid=persistence and then the module name=Persistence). 
  • The pom.xml in this directory contains dependancies that are related directly to the service module and are not being used anywhere else. should look like this,

    
        coffeebeans
        com.coffeebeans
        1.0-SNAPSHOT
    
    4.0.0

    persistence

The parent element that tells us that this module is part of the com.coffeebeans group and it's going to be deployed as a jar file. 


3. Create the Services module

  • Like what we did for Persistence module, create a directory inside CoffeeBeansRest and call it Services
  • Create the usual maven layout for webapp inside it (refer Figure.1) (if you using intellij, add module choose maven then input the artifactid=service and then the module name=Service). 
  • The pom.xml in this directory contains dependancies that are related directly to the service module and are not being used anywhere else. should look like this,

    
        com.coffeebeans
        coffeebeans
        1.0-SNAPSHOT
    
    4.0.0
    services
    war
    services
    1.0-SNAPSHOT

    
        <build.timestamp>${maven.build.timestamp}</build.timestamp>
    

    
        Services
        
            
                src/main/resources
                true
            
        
    
    
        
            com.coffeebeans
            persistence
            1.0-SNAPSHOT
        

        
            org.springframework
            spring-webmvc
            ${spring.version}
        

        
            javax.servlet
            javax.servlet-api
            3.0.1
            provided
        

        
            com.fasterxml.jackson.core
            jackson-core
            2.4.1
        

        
            com.fasterxml.jackson.core
            jackson-databind
            2.4.1.1
        
    


The parent element that tells us that this module is part of the com.coffeebeans group and it's going to be deployed as a war file. Also notice that the services module will depend on the persistence.

Now the lay out of the project is ready and it should look like what Figure.1 project layout.


4. Create a message file

In services/src/main/resources create messages.properties file. This file will contain the different messages that might be displayed to a user.
build.version=${project.version}
api.version=1
build.time=${build.timestamp}
It will retrieve the project.version and build.timestamp from service/pom.xml.


5. Configure logback

In services/src/main/resources create logback.xml file.

    
        
            
                [SRV] %d{HH:mm:ss.SSS} %-5level %logger - %msg%n
            
        
    
    

    
        
    
    


Initialise the webapp

Traditionally web.xml would be used to configure the ServletContext using something like this,

    mvc-dispatcher
    org.springframework.web.servlet.DispatcherServlet
    
        contextConfigLocation
        /WEB-INF/services-servlet.xml
    
    1



    mvc-dispatcher
    /1/*

However, since we are Servlet 3.0+ServletContext can be configured programmatically. To achieve that add  com.coffeebeans.initializer.WebAppInitializer class in Service module.
package com.coffeebeans.initializer;

import com.coffeebeans.config.MvcConfig;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

/**
 * Created by muhamadto on 5/07/2015.
 */
public class WebAppInitializer implements WebApplicationInitializer {
    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();

        servletContext.addListener(new ContextLoaderListener(rootContext));

        AnnotationConfigWebApplicationContext dispatcherServlet = new AnnotationConfigWebApplicationContext();
        dispatcherServlet.register(MvcConfig.class);

        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(dispatcherServlet));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/1/*");

    }
}

Here you might realised that the code created two Application contexts. Namely, rootContext and dispatcherServlet. So, what are the difference between them.
In Spring you can build multilevel application contexts, for example in a webapp there are usually root context and servlet context levels.

  • Root context contains the beans that can be used for the entire application (e.g. Spring Security). 
  • The other level which is the servlet contexts, this can be of multiple contexts for example a servlet content to serve web pages and another one to serve web-services 

This will allow the different contexts to avoid name clashes between beans. If a bean is not existing in the current servlet context. Spring will look it up in the higher level which is root context. 


Configure Spring MVC

You might realise that the WebAppInitializer#onStartup() referenced MvcConfig.classWebAppInitializer told our application that rootContext and dispatcherServlet are two contexts that will be use and it registers some config class (e.g. MvcConfig.class) to each context. In MvcConfig.class  we would tell our application to enable Spring MVC and where to find its controllers. Later we will add configuration class Spring Data JPA and other modules from Spring framework.
package com.coffeebeans.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;

/**
 * Created by muhamadto on 5/07/2015.
 */

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = {"com.coffeebeans.controller"})
public class MvcConfig extends WebMvcConfigurerAdapter{

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource resource = new ResourceBundleMessageSource();
        resource.setBasename("messages");
        resource.setDefaultEncoding("UTF-8");
        return resource;
    }
}
@EnableWebMvc  enable Spring MVC in our app.
@ComponentScan(basePackages = {"com.coffeebeans.controller"}  instructs the app to look for controllers in com.coffeebeans.controller  package.

Building controllers

Now, let's build a controller for our API. We can build a simple thing like "Hello, world". However, it's more fun to build something useful like a controller to return the api version from the messages.properties file we created earlier. 


1. Create a class to model the version

In Persistence module create com.coffeebeans.processing.utils.Version
package com.coffeebeans.processing.utils;

import lombok.Data;

/**
 * Created by muhamadto on 5/07/2015.
 */
public @Data
class Version {
    private String buildVersion;
    private String timestamp;
    private String apiVersion;
} 

I used lombok to create setters, getters and default constructor of the class to reduce the boilerplate code.



2. Define VersionService

package com.coffeebeans.service.version;

import com.coffeebeans.service.base.BaseService;
import com.coffeebeans.api.version.ApiVersion;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;

import java.util.Locale;

/**
 * Created by muhamadto on 12/07/2015.
 */

public interface VersionService{
    public ApiVersion getVersion();
}
and a class to implement this interface
package com.coffeebeans.service.version;

import com.coffeebeans.api.version.ApiVersion;
import com.coffeebeans.service.base.BaseService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Service;

import java.util.Locale;

/**
 * Created by muhamadto on 19/07/2015.
 */
@Service("versionService")
public class VersionServiceImpl extends BaseService implements VersionService{
    public static Logger LOGGER = LoggerFactory.getLogger(VersionService.class);

    @Autowired
    MessageSource messageSource;

    @Override
    public ApiVersion getVersion() {
        LOGGER.debug("Getting ApiVersion of the API STARTED");
        ApiVersion version = new ApiVersion();

        String buildVersion = messageSource.getMessage("build.version", null,  Locale.getDefault());
        version.setBuildVersion(buildVersion);

        String buildTime = messageSource.getMessage("build.time", null,  Locale.getDefault());
        version.setTimestamp(buildTime);

        String apiVersion = messageSource.getMessage("api.version", null,  Locale.getDefault());
        version.setApiVersion(apiVersion);

        LOGGER.debug("API version is {}", version);
        return version;
    }
}

3. Configure the Services

Add com.coffeebeans.config.ServicesConfig
package com.coffeebeans.config;

import com.coffeebeans.service.version.VersionService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

import javax.validation.Validator;
import java.nio.charset.StandardCharsets;

/**
 * Created by muhamadto on 12/07/2015.
 */

@Configuration
@ComponentScan(basePackages = {"com.coffeebeans.service"})
public class ServicesConfig {
    @Bean
    public ResourceBundleMessageSource messageSource() {
        ResourceBundleMessageSource resource = new ResourceBundleMessageSource();
        resource.setBasename("messages");
        resource.setDefaultEncoding(StandardCharsets.UTF_8.toString());
        return resource;
    }

    @Bean
    public Validator validator() {
        return new LocalValidatorFactoryBean();
    }
}
then register this config with the rootContext in com.coffeebeans.initializer.WebAppInitializer#onStartup()
rootContext.register(ServicesConfig.class);
servletContext.addListener(new ContextLoaderListener(rootContext));

4. Create BaseController interface

Before creating our controller, we need to create an abstract class to have the common functionality in it
package com.coffeebeans.controller.base;

import com.coffeebeans.api.error.ErrorResponse;
import com.coffeebeans.exception.general.DuplicateEntryException;
import com.coffeebeans.exception.general.NotFoundException;
import com.coffeebeans.exception.validation.ValidationException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import java.net.URI;

/**
 * Created by muhamadto on 4/07/2015.
 */
public abstract class BaseController {

}

This class will be used later to add functionality like generic Exception handling.


5. Create our controller

Spring will inject the VersionService into this controller thanks to org.springframework.beans.factory.annotation.Autowired annotation.
package com.coffeebeans.controller.rest.version;

import com.coffeebeans.api.version.ApiVersion;
import com.coffeebeans.controller.base.BaseController;
import com.coffeebeans.service.version.VersionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

/**
 * Created by muhamadto on 4/07/2015.
 */

@RestController
public class ApiVersionController extends BaseController{

    @Autowired
    private VersionService versionService;

    @RequestMapping(value="version", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public ApiVersion getAPIVersion() throws Exception {
        return versionService.getVersion();
    }
}

This controller will use getAPIVersion() of  VersionService to return the version of the api and it will do so as a response to restful GET call. Moreover, the returning content-type will be JSON. But, how did the application transform the version object to json format, for this automatic transformation to work. you need to add:
    1. org.springframework.web.bind.annotation.ResponseBody before the method. 
    2. Add jackson dependancies to the pom file which we did before in the services/pom.xml
In the browser go to http://localhost:8080/services/1/version, you should see something like this 
{"buildVersion":"1.0-SNAPSHOT","timestamp":"20150705-0611","apiVersion":"1"}

6. Serving JSP pages from controller

What if we need to serve JSP pages as well, to do so 
  • Add the following method to MvcConfig
 @Bean
    public InternalResourceViewResolver jspViewResolver() {
        InternalResourceViewResolver bean = new InternalResourceViewResolver();
        bean.setPrefix("/WEB-INF/jsp/");
        bean.setSuffix(".jsp");
        return bean;
    }
  • Add the following IndexController:
import com.coffeebeans.controller.base.BaseController;
import com.coffeebeans.service.version.VersionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.ModelAndView;

import java.util.HashMap;
import java.util.Map;

/**
 * Created by muhamadto on 4/07/2015.
 */

@Controller
public class IndexController extends BaseController{

    @Autowired
    private VersionService versionService;

    @RequestMapping(value={"","index**", "welcome**"}, method = RequestMethod.GET, produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView displayAPIVersion() throws Exception {
        Map model = new HashMap<>();
        model.put("apiversion",  versionService.getVersion().getApiVersion());
        ModelAndView modelAndView = new ModelAndView("version", model);

        return modelAndView;
    }
}
displayAPIVersion() will respond to request http://localhost:8080/services/1/index, http://localhost:8080/services/1/welcome or simply http://localhost:8080/services/1/ serving jsp called version.jsp as this line instructs the app
ModelAndView modelAndView = new ModelAndView("version", model);
  • Also, a page in WEB-INF/jsp/version.jsp has to be created
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags">


API Version ${apiversion}
Build Version:  
Build Time: 


In the browser go to http://localhost:8080/services/1/index, you should see a formatted html page:
API Version 1
Build Version: 1.0-SNAPSHOT
Build Time: 20150705-0611 


What is next

The next post will detail how to add support for Spring Data JPA


Wednesday, 17 June 2015

Integrating Quartz Scheduler with Java webapp on multiple nodes

Recently I needed to schedule some background jobs in a legacy web app that uses Java. I found Quartz to be very useful to accomplish my task.

Contents
  • Add dependencies
  • Logging
  • Create Job Class
  • Create scheduler on the start up using org.quartz.ee.servlet.QuartzInitializerListener
  • Create JobDetails
  • Create Triggers.
  • Put it all together
  • Clustering

Add dependencies

    
      org.quartz-scheduler
      quartz
      2.2.1
    
    
      org.quartz-scheduler
      quartz-jobs
      2.2.1
    
Enable logging in order to follow what is happening through the logs.
log4j.rootLogger=WARN, ROOT
log4j.appender.ROOT=org.apache.log4j.RollingFileAppender
log4j.logger.org.quartz=DEBUG
log4j.appender.ROOT.File=/tmp/quartz.log

Creating Job Class

package com.coffeebeans.quartz.job;

import com.coffeebeans.quartz.utils.Constants;
import com.coffeebeans.quartz.utils.DateUtils;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.ParseException;
import java.util.Date;

/**
 * Created by muhamadto on 6/9/15.
 */
public class QuartzJob implements Job {
    private static final Logger LOGGER =  LoggerFactory.getLogger(QuartzJob.class);
    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        JobDetail jobDetail = jobExecutionContext.getJobDetail();
        try {
            LOGGER.debug(String.format("Scheduled Job (%s) started - %s", jobDetail.getKey(), DateUtils.formatDate(Constants.DATE_FORMAT, new Date())));
        } catch (ParseException e) {
            e.printStackTrace();
            LOGGER.error("Could not execute job: {}, Exception details: {}", jobDetail.getKey(), e);
            throw new JobExecutionException(e);
        }
    }
}

Create scheduler

You can get scheduler and start it using 
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.start(); 
However, a better way to accomplish the same task is to start/shutdown the scheduler with the application server. Paste the following snippet into your web.xml
    
    
        quartz:config-file
        quartz.properties
    
    
        quartz:shutdown-on-unload
        true
    
    
        quartz:wait-on-shutdown
        true
    
    
        quartz:start-on-load
        true
    

    
        
            org.quartz.ee.servlet.QuartzInitializerListener
        
    
  • Set quartz:start-on-load=true to specify that you want the scheduler.start() method to be called when the listener is first loaded. 
  • Set quartz:shutdown-on-unload=true to specify that you want scheduler.shutdown() to be called when server is being shutdown.
  • Set quartz:wait-on-shutdown to true to execute scheduler.shutdown(true). This will cause the scheduler to wait for existing jobs to complete. 
  • If you skipped quartz:config-file, the scheduler will be initialized with the default config.
Now get an instance of the scheduler. StdSchedulerFactory instance is stored into the ServletContextYou can gain access to the factory from a ServletContext.
try {
    StdSchedulerFactory factory = (StdSchedulerFactory) config.getServletContext()
        .getAttribute(QuartzInitializerListener.QUARTZ_FACTORY_KEY);
    scheduler = factory.getScheduler();
} catch (SchedulerException e) {
    throw new ServletException();        
}

JobDetail

JobDetail is a job instance definition with its own set of properties. For example, a CleanUpJob, could have instance to clean files from tmp directory, another to clean cache, and so on. Use JobMetaData to pass parameters to your job.
JobKey jobKey = JobKey.jobKey(jobName, groupName);
JobDetail job =newJob(QuartzJob.class).withIdentity(jobKey).requestRecovery().build();

Create a trigger 

There are two types of triggers:
SimpleTrigger, used to schedule a job that will be executed only one time at a specific time, or at a specific time then repeats at a certain interval.
Trigger simpleTrigger = newTrigger()
        .withIdentity(triggerName, groupName)
        .withSchedule(simpleSchedule()
                .withRepeatCount(repeatCount)
                .withIntervalInSeconds(repeatIntervalInSeconds))
        .startAt(startTime)
        .forJob(job)
        .build();

CronTrigger, used to specify times to run a job in a manner similar to Unix cron expression.
CronExpression cexp = new CronExpression(“0 0 11 * * ?”);
Trigger cronTrigger = newTrigger()
        .withIdentity(triggerName, groupName)
        .withSchedule(cronSchedule(cronExpression))
        .forJob(job)
        .build();

Note: In quartz a trigger can fire only one job while a job can be associated with multiple triggers

Put it all together 

Now create a servlet to serve as a client in which we will call and use the job
package com.coffeebeans.quartz.servlet;

import com.coffeebeans.quartz.job.QuartzJob;
import com.coffeebeans.quartz.utils.NumberUtils;
import org.quartz.*;
import org.quartz.ee.servlet.QuartzInitializerListener;
import org.quartz.impl.StdSchedulerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;

import static org.quartz.JobBuilder.newJob;
import static org.quartz.SimpleScheduleBuilder.simpleSchedule;
import static org.quartz.TriggerBuilder.newTrigger;

/**
 * Created by muhamadto on 6/9/15.
 */
public class QuartzServlet extends HttpServlet {
    private static final Logger LOGGER = LoggerFactory.getLogger(QuartzServlet.class);

    private Scheduler scheduler;

    @Override
    public void init(ServletConfig config) throws ServletException {
        try {
            StdSchedulerFactory factory = (StdSchedulerFactory) config.getServletContext()
                    .getAttribute(QuartzInitializerListener.QUARTZ_FACTORY_KEY);

            scheduler = factory.getScheduler();
        } catch (SchedulerException e) {
            throw new ServletException();
        }
        super.init(config);
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try {
            String jobName = req.getParameter("jobName");
            String groupName = req.getParameter("groupName");

            String mills = req.getParameter("mills");

            Date startTimeForSimpleTrigger = null;
            if (!"".equals(mills) && NumberUtils.isNumeric(mills)) { // isNumeric copied from commons-lang StringUtils
                startTimeForSimpleTrigger = new Date(Long.parseLong(mills));
            }

            JobKey jobKey = JobKey.jobKey(jobName, groupName);
            JobDetail job = newJob(QuartzJob.class).withIdentity(jobKey).requestRecovery().build();

            String triggerName = String.format("simple-%s", jobKey);
            Trigger simpleTrigger = newTrigger()
                    .withIdentity(triggerName, groupName)
                    .withSchedule(simpleSchedule()
                            .withRepeatCount(0)
                            .withIntervalInSeconds(0))
                    .startAt(startTimeForSimpleTrigger)
                    .forJob(job)
                    .build();

            boolean jobExists = scheduler.checkExists(job.getKey());
            if (!jobExists) {
                scheduler.scheduleJob(job, simpleTrigger);
            } else {
                scheduler.rescheduleJob(simpleTrigger.getKey(), simpleTrigger);
            }
        } catch (Exception e) {
            LOGGER.error("Job was not scheduled", e);
            resp.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, String.format("Failed! Job was not saved. %s", e.getMessage()));
        }
    }

    @Override
    protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    }
}
Map the servlet in web.xml
    
        QuartzServlet
        com.coffeebeans.quartz.servlet.QuartzServlet
    
    
        QuartzServlet
        /schedule
    
  • rescheduleJob() will cause the scheduler to remove the old trigger with the given key, and put the new one in its place.
  • Calling requestRecovery() will cause the scheduler to re-executea job in case of server hard-shutdown.
Quick things to notice here
  • For the recover to work we will need to use datastore. 
  • Set the DB properties in quartz.properties and find the db script for mysql in /path/to/quartz-2.2.1/docs/dbTables/tables_mysql.sql

Clustering, and Datastore Configuration

In /path/to/tomcat/context.xml
  
Then add quartz.properties
#============================================================================
# Configure Main Scheduler Properties
#============================================================================

org.quartz.scheduler.instanceName=CoffeeBeansClusteredScheduler
org.quartz.scheduler.instanceId=AUTO

#============================================================================
# Configure ThreadPool
#============================================================================

org.quartz.threadPool.class=org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount=25
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.StdJDBCDelegate
org.quartz.jobStore.lockHandler.class=org.quartz.impl.jdbcjobstore.UpdateLockRowSemaphore
org.quartz.jobStore.lockHandler.updateLockRowSQL=UPDATE {0}LOCKS SET LOCK_NAME = LOCK_NAME WHERE LOCK_NAME = ?
org.quartz.jobStore.useProperties=false
org.quartz.jobStore.dataSource=myDS
org.quartz.jobStore.tablePrefix=QRTZ_
org.quartz.jobStore.isClustered=true
org.quartz.jobStore.clusterCheckinInterval=20000

#============================================================================
# Configure Datasources
#============================================================================

org.quartz.dataSource.myDS.jndiURL=java:comp/env/jdbc/quartz
#============================================================================
# Logging
#============================================================================

org.quartz.plugin.triggerHistory.class=org.quartz.plugins.history.LoggingTriggerHistoryPlugin
org.quartz.plugin.triggerHistory.class=org.quartz.plugins.history.LoggingTriggerHistoryPlugin
org.quartz.plugin.triggerHistory.triggerFiredMessage=Trigger [{1}.{0}] fired job [{6}.{5}] scheduled at: {2, date, dd-MM-yyyy HH:mm:ss.SSS}, next scheduled at: {3, date, dd-MM-yyyy HH:mm:ss.SSS}
org.quartz.plugin.triggerHistory.triggerCompleteMessage=Trigger [{1}.{0}] completed firing job [{6}.{5}] with resulting trigger instruction code: {9}. Next scheduled at: {3, date, dd-MM-yyyy HH:mm:ss.SSS}
org.quartz.plugin.triggerHistory.triggerMisfiredMessage=Trigger [{1}.{0}] misfired job [{6}.{5}]. Should have fired at: {3, date, dd-MM-yyyy HH:mm:ss.SSS}
org.quartz.plugin.jobHistory.class=org.quartz.plugins.history.LoggingJobHistoryPlugin
org.quartz.plugin.jobHistory.jobToBeFiredMessage=Job [{1}.{0}] to be fired by trigger [{4}.{3}], re-fire: {7}
org.quartz.plugin.jobHistory.jobSuccessMessage=Job [{1}.{0}] execution complete and reports: {8}
org.quartz.plugin.jobHistory.jobFailedMessage=Job [{1}.{0}] execution failed with exception: {8}
org.quartz.plugin.jobHistory.jobWasVetoedMessage=Job [{1}.{0}] was vetoed. It was to be fired by trigger [{4}.{3}] at: {2, date, dd-MM-yyyy HH:mm:ss.SSS}
  • Set org.quartz.jobStore.isClustered=true to indicate that you want to activate cluster mode. 
  • Make sure org.quartz.scheduler.instanceId=AUTO; otherwise you will need to have multiple configuration files.