Integrating Quartz with Spring
When it comes to scheduling jobs in a java application, Quartz is the first tool that comes into consideration.
Quartz is job scheduler backed up by most popular RDBMSes. It is really convenient and gets integrated with spring quite easy.
In order to create the quartz schema you have to download the quartz distribution and extract the folder located in quartz-2.2.3/docs/dbTables/
Choose the quartz schema according to the database that you use. In our case we will use a local h2 database therefore I will use the tables_h2.sql schema.
In order to avoid any manual sql actions i will use the Spring boot database initialization feature.
Let’s start with our gradle file.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | group 'com.gkatzioura' version '1.0-SNAPSHOT' apply plugin: 'java' sourceCompatibility = 1.8 buildscript { repositories { mavenCentral() } dependencies { classpath( "org.springframework.boot:spring-boot-gradle-plugin:1.3.3.RELEASE" ) } } apply plugin: 'idea' apply plugin: 'spring-boot' repositories { mavenCentral() } dependencies { compile group: 'org.springframework.boot' , name: 'spring-boot-starter-web' , version: '1.3.3.RELEASE' compile group: 'org.springframework' , name: 'spring-context-support' , version: '4.2.4.RELEASE' compile group: 'org.springframework' , name: 'spring-jdbc' , version: '4.2.4.RELEASE' compile group: 'org.quartz-scheduler' , name: 'quartz' , version: '2.2.3' compile group: 'ch.qos.logback' , name: 'logback-core' , version: '1.1.3' compile group: 'ch.qos.logback' , name: 'logback-classic' ,version: '1.1.3' compile group: 'org.slf4j' , name: 'slf4j-api' ,version: '1.7.13' compile group: 'com.h2database' , name: 'h2' , version: '1.4.192' testCompile group: 'junit' , name: 'junit' , version: '4.11' } |
Apart from the quartz, spring and h2 dependencies, we add the spring-jdbc dependencies since we want to have the database initialized through spring.
We will also add an application.yml file
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | spring: datasource: continueOnError: true org: quartz: scheduler: instanceName: spring-boot-quartz-demo instanceId: AUTO threadPool: threadCount: 5 job: startDelay: 0 repeatInterval: 60000 description: Sample job key: StatisticsJob |
Due to the schema creation statements (lack of create if not exists statements), I set spring.datasource.continueOnError to false. According to your implementation the workaround will vary.
The application class
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 | package com.gkatzioura.springquartz; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; /** * Created by gkatzioura on 6/6/16. */ @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication springApplication = new SpringApplication(); ApplicationContext ctx = springApplication.run(Application. class ,args); } } |
The h2 datasource configuration neeeded by quartz
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package com.gkatzioura.springquartz.config; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /** * Created by gkatzioura on 6/6/16. */ @Configuration public class QuartzDataSource { //Since it a test database it will be located at the temp directory private static final String TMP_DIR = System.getProperty( "java.io.tmpdir" ); @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL( "jdbc:h2:" +TMP_DIR+ "/test" ); return ds; } } |
In our case we want to sent ‘spam’ emails every minute, therefore we define a simple email service
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | package com.gkatzioura.springquartz.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** * Created by gkatzioura on 6/7/16. */ @Service public class EmailService { private static final Logger LOGGER = LoggerFactory.getLogger(EmailService. class ); public void sendSpam() { LOGGER.info( "Should send emails" ); } } |
I will also implement a SpringBeanJobFactory
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | package com.gkatzioura.springquartz.quartz; import org.quartz.spi.TriggerFiredBundle; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.scheduling.quartz.SpringBeanJobFactory; /** * Created by gkatzioura on 6/7/16. */ public class QuartzJobFactory extends SpringBeanJobFactory implements ApplicationContextAware { private transient AutowireCapableBeanFactory beanFactory; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { beanFactory = applicationContext.getAutowireCapableBeanFactory(); } @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception { final Object job = super .createJobInstance(bundle); beanFactory.autowireBean(job); return job; } } |
QuartzJobFactory will create the job instance and the will use the application context in order to inject any dependencies defined.
Next step is defining our job
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 | package com.gkatzioura.springquartz.job; import com.gkatzioura.springquartz.service.EmailService; import org.quartz.Job; import org.quartz.JobExecutionContext; import org.quartz.JobExecutionException; import org.springframework.beans.factory.annotation.Autowired; /** * Created by gkatzioura on 6/6/16. */ public class EmailJob implements Job { @Autowired private EmailService cronService; @Override public void execute(JobExecutionContext context) throws JobExecutionException { cronService.sendSpam(); } } |
Last step is adding quartz config
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 | package com.gkatzioura.springquartz.config; import com.gkatzioura.springquartz.job.EmailJob; import com.gkatzioura.springquartz.quartz.QuartzJobFactory; import org.quartz.SimpleTrigger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.quartz.JobDetailFactoryBean; import org.springframework.scheduling.quartz.SchedulerFactoryBean; import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; import javax.sql.DataSource; import java.util.Properties; /** * Created by gkatzioura on 6/7/16. */ @Configuration public class QuartzConfig { @Value ( "${org.quartz.scheduler.instanceName}" ) private String instanceName; @Value ( "${org.quartz.scheduler.instanceId}" ) private String instanceId; @Value ( "${org.quartz.threadPool.threadCount}" ) private String threadCount; @Value ( "${job.startDelay}" ) private Long startDelay; @Value ( "${job.repeatInterval}" ) private Long repeatInterval; @Value ( "${job.description}" ) private String description; @Value ( "${job.key}" ) private String key; @Autowired private DataSource dataSource; @Bean public org.quartz.spi.JobFactory jobFactory(ApplicationContext applicationContext) { QuartzJobFactory sampleJobFactory = new QuartzJobFactory(); sampleJobFactory.setApplicationContext(applicationContext); return sampleJobFactory; } @Bean public SchedulerFactoryBean schedulerFactoryBean(ApplicationContext applicationContext) { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setOverwriteExistingJobs( true ); factory.setJobFactory(jobFactory(applicationContext)); Properties quartzProperties = new Properties(); quartzProperties.setProperty( "org.quartz.scheduler.instanceName" ,instanceName); quartzProperties.setProperty( "org.quartz.scheduler.instanceId" ,instanceId); quartzProperties.setProperty( "org.quartz.threadPool.threadCount" ,threadCount); factory.setDataSource(dataSource); factory.setQuartzProperties(quartzProperties); factory.setTriggers(emailJobTrigger().getObject()); return factory; } @Bean (name = "emailJobTrigger" ) public SimpleTriggerFactoryBean emailJobTrigger() { SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean(); factoryBean.setJobDetail(emailJobDetails().getObject()); factoryBean.setStartDelay(startDelay); factoryBean.setRepeatInterval(repeatInterval); factoryBean.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); factoryBean.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT); return factoryBean; } @Bean (name = "emailJobDetails" ) public JobDetailFactoryBean emailJobDetails() { JobDetailFactoryBean jobDetailFactoryBean = new JobDetailFactoryBean(); jobDetailFactoryBean.setJobClass(EmailJob. class ); jobDetailFactoryBean.setDescription(description); jobDetailFactoryBean.setDurability( true ); jobDetailFactoryBean.setName(key); return jobDetailFactoryBean; } } |
What we did is creating a scheduler factory bean using the QuartzJobFactory we defined and we registered the triggers needed for our jobs to run. In our case we implemented a simple trigger running every minute.
You can find the source code on github
Reference: | Integrating Quartz with Spring from our JCG partner Emmanouil Gkatziouras at the gkatzioura blog. |