opencodez

Easy Way to Configure Quartz Scheduler with Spring Boot – Example With Source Code

Quartz Scheduler is a widely accepted and used open-source job scheduling library. This library can be integrated with all types of Java applications. Using this, you can convert your simple java components into a job that you can schedule and run as per your need. The library supports various storage strategies like RAM, RDBMS, etc.

On top of this, Quartz supports JTA transactions and clustering which comes in very handy when you want to deploy your application to production servers. We have earlier published an article for using Quartz in a standalone application.

Configure Quartz Scheduler with Spring Boot:

Here in this article, we will see how we can configure Spring Boot and Quartz. We will create a Spring Boot Quartz Scheduler application with MySQL, as a data store for our Quartz jobs and triggers.

Software used in this example

Demo Project Structure

The project will look like below in your Eclipse IDE
Along with Spring annotations, I am using xml based configuration to setup data source and entity manager for this example. To configure Quartz for your Spring Boot application, you need to add below dependency to your pom.xml

xdependencyx
	xgroupIdxorg.quartz-schedulerx/groupIdx
	xartifactIdxquartzx/artifactIdx
	xversionx2.2.1x/versionx
x/dependencyx
xdependencyx
	xgroupIdxorg.quartz-schedulerx/groupIdx
	xartifactIdxquartz-jobsx/artifactIdx
	xversionx2.2.1x/versionx
x/dependencyx
xdependencyx
	xgroupIdxorg.springframeworkx/groupIdx
	xartifactIdxspring-context-supportx/artifactIdx
x/dependencyx

As we are using MySQL as our database, we need to download quartz distributable and use the table structures that they have defined for MySQL. Once you have downloaded and executed the sql in your environment, the table will look like as below


This is our Application Start Point. You can see that I am using Spring Boot auto configuration capabilities here and at the same time I am importing my xml configuration as well.

package com.opencodez;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

@SpringBootApplication
@ImportResource({ "classpath:dao-context.xml" })
public class QuartzDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(QuartzDemoApplication.class, args);
	}
}

We are defining a extra configuration class using annotation @Configuration that will do all required setup for Quartz.

/**
 * 
 */
package com.opencodez.configuration;

import java.io.IOException;
import java.util.List;
import java.util.Properties;

import javax.sql.DataSource;

import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.spi.JobFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.quartz.CronTriggerFactoryBean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;

import com.opencodez.util.AppUtil;

/**
 * @author pavan.solapure
 *
 */

@Configuration
@ConditionalOnProperty(name = "quartz.enabled")
public class ConfigureQuartz {

	@Autowired
	ListxTriggerx listOfTrigger;

	@Bean
	public JobFactory jobFactory(ApplicationContext applicationContext) {
		AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
		jobFactory.setApplicationContext(applicationContext);
		return jobFactory;
	}

	@Bean
	public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource, JobFactory jobFactory) throws IOException {
		SchedulerFactoryBean factory = new SchedulerFactoryBean();
		factory.setOverwriteExistingJobs(true);
		factory.setAutoStartup(true);
		factory.setDataSource(dataSource);
		factory.setJobFactory(jobFactory);
		factory.setQuartzProperties(quartzProperties());

		// Here we will set all the trigger beans we have defined.
		if (!AppUtil.isObjectEmpty(listOfTrigger)) {
			factory.setTriggers(listOfTrigger.toArray(new Trigger[listOfTrigger.size()]));
		}

		return factory;
	}

	@Bean
	public Properties quartzProperties() throws IOException {
		PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();
		propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));
		propertiesFactoryBean.afterPropertiesSet();
		return propertiesFactoryBean.getObject();
	}

	public static SimpleTriggerFactoryBean createTrigger(JobDetail jobDetail, long pollFrequencyMs) {
		SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
		factoryBean.setJobDetail(jobDetail);
		factoryBean.setStartDelay(0L);
		factoryBean.setRepeatInterval(pollFrequencyMs);
		factoryBean.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
		// in case of misfire, ignore all missed triggers and continue :
		factoryBean.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT);
		return factoryBean;
	}

	// Use this method for creating cron triggers instead of simple triggers:
	public static CronTriggerFactoryBean createCronTrigger(JobDetail jobDetail, String cronExpression) {
		CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
		factoryBean.setJobDetail(jobDetail);
		factoryBean.setCronExpression(cronExpression);
		factoryBean.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_FIRE_NOW);
		return factoryBean;
	}

	public static JobDetailFactoryBean createJobDetail(Class jobClass) {
		JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
		factoryBean.setJobClass(jobClass);
		// job has to be durable to be stored in DB:
		factoryBean.setDurability(true);
		return factoryBean;
	}

}

You can see that above I am creating different factory beans, that will be used to create jobs,that can be run by either simple trigger or cron trigger. Also we are specifying the quartz properties file, which will have different parameters to that are specific to quartz.

#============================================================================
# Configure Main Scheduler Properties
#============================================================================
 
org.quartz.scheduler.instanceId = AUTO
org.quartz.scheduler.makeSchedulerThreadDaemon = true
 
#============================================================================
# Configure ThreadPool
#============================================================================
 
org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.makeThreadsDaemons = true
org.quartz.threadPool.threadCount: 20
org.quartz.threadPool.threadPriority: 5
 
#============================================================================
# Configure JobStore
#============================================================================

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX 
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.StdJDBCDelegate
org.quartz.jobStore.tablePrefix = QRTZ_
org.quartz.jobStore.isClustered = false
org.quartz.jobStore.dataSource = myDs
org.quartz.jobStore.misfireThreshold = 25000


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

org.quartz.dataSource.myDS.driver = com.mysql.jdbc.Driver
org.quartz.dataSource.myDS.URL = jdbc:mysql://localhost:3306/localdb
org.quartz.dataSource.myDS.user = lessroot
org.quartz.dataSource.myDS.password = lessroot
org.quartz.dataSource.myDS.maxConnections = 5
org.quartz.dataSource.myDS.validationQuery = select 1

Also please note that, we are configuring the Quartz based on a property in our application.properties file. The property you can find on head of the class. If this is set to true then only we will configure Quartz, else we will skip this configuration.

Now we are all set to define our first Job. The example below shows you a Job configured with Simple Trigger

/**
 * 
 */
package com.opencodez.quartz.jobs;

import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobDetail;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.quartz.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;
import org.springframework.stereotype.Component;

import com.opencodez.configuration.ConfigureQuartz;
import com.opencodez.util.AppLogger;

/**
 * @author pavan.solapure
 *
 */
@Component
@DisallowConcurrentExecution
public class JobWithSimpleTrigger implements Job {

	private final static AppLogger logger = AppLogger.getInstance();
	
	@Value("${cron.frequency.jobwithsimpletrigger}")
    private long frequency;

	@Override
	public void execute(JobExecutionContext jobExecutionContext) {
		logger.info("Running JobWithSimpleTrigger | frequency {}", frequency);
	}
	
	@Bean(name = "jobWithSimpleTriggerBean")
    public JobDetailFactoryBean sampleJob() {
        return ConfigureQuartz.createJobDetail(this.getClass());
    }

    @Bean(name = "jobWithSimpleTriggerBeanTrigger")
    public SimpleTriggerFactoryBean sampleJobTrigger(@Qualifier("jobWithSimpleTriggerBean") JobDetail jobDetail) {
    	return ConfigureQuartz.createTrigger(jobDetail,frequency);
    }
}

Please make sure that the bean  names for Job and Trigger we define here are unique and referenced correctly. You can define as many jobs as you want in similar fashion and they will be scheduled automatically. No other configuration is needed. Thanks to Spring IOC, below code will look for all the Triggers that are defined as beans and will autowire them in a list which can be supplied to scheduler. Please check above ConfigureQuartz.java for code.

Lastly you can see below the output/console for the jobs that we have scheduled in our example.

Update x Adding DI to Spring Boot Quartz Scheduler Job Class

Few of our fellow developers were facing issues when tried to use @Inject in the Job Class. I could not pin point the root cause for that, but I found a solution. In earlier examples, I have used @Qualifier to inject JobDetail object and when we try to use Dependency Injection (Inject, Autowired) in the job class it failed. So instead we used @Qualifier to inject JobDetailFactoryBean instance. From this factory bean we then pulled JobDetails for further processing. Code Ref-

@Inject
private ArbitraryDependency fieldInjectDependency;

@Bean(name = "jobWithDITesterBean")
public JobDetailFactoryBean sampleJob() {
	return ConfigureQuartz.createJobDetail(this.getClass());
}

@Bean(name = "jobWithDITesterBeanTrigger")
public CronTriggerFactoryBean sampleJobTrigger(@Qualifier("jobWithDITesterBean") JobDetailFactoryBean jdfb ) {
	return ConfigureQuartz.createCronTrigger(jdfb.getObject(), frequency);
}

You can download and run the code from GitHub.