Spring Boot Quartz Scheduler Integration

Configuring quartz scheduler in a spring boot application requires a number of steps. This tutorial assumes that you already have a simple spring boot application up and running. Please see this tutorial on developing spring boot applications with spring tool suite if you are not familiar with setting up a simple spring boot application.

The following example uses RAMJobStore for persisting quartz job. This is one of fastest jobstore implementations for storing quartz job schedules. RAMJobStore stores quartz job data in the memory and hence scheduling information is lost in case of a program crash. This may not be suitable for all use cases. However, we will use RAMJobStore in this tutorial to keep the configuration simple.

Step 1: Modify the build.gradle to add quartz and related dependencies. Please refer to the updated gradle file below for the dependency details,

1. build.gradle

buildscript {
    ext {
        springBootVersion = '1.5.2.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile('org.springframework:spring-context-support')
    compile('org.springframework:spring-tx')
    compile('org.quartz-scheduler:quartz:2.2.1')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

Step 2: Write the classes for the schedule job and the underlying application service. Following is a simple spring service used by the tutorial. It prints "Hello World!" in the console log. We will also write a quartz job to invoke this service.

2a. SimpleService.java

package com.quickprogrammingtips.springboot.services;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class SimpleService {
    private static final Logger LOG = LoggerFactory.getLogger(SimpleService.class);

    public void processData() {
        LOG.info("Hello World!");
    }
}

2b. SimpleJob.java

package com.quickprogrammingtips.springboot.jobs;

import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.springframework.beans.factory.annotation.Autowired;

import com.quickprogrammingtips.springboot.services.SimpleService;

public class SimpleJob implements Job{

    @Autowired
    private SimpleService service;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) {
        service.processData();
    }
}

Step 3: Create a custom SpringBeanJobFactory for enabling spring auto wiring for the SimpleJob and SimpleService written above. This is required because SpringBeanJobFactory manually instantiates job instances whenever a job needs to run. Hence spring autowiring needs to be manually applied on the job instance if we are using autowiring features in the classes invoked by job instance (SimpleJob and SimpleService above).

3. AutowiringSpringBeanJobFactory.java

package com.quickprogrammingtips.springboot;

import org.quartz.spi.TriggerFiredBundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.scheduling.quartz.SpringBeanJobFactory;

public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {

    private static final Logger LOG = LoggerFactory.getLogger(AutowiringSpringBeanJobFactory.class);

    private transient AutowireCapableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(final ApplicationContext context) {
        beanFactory = context.getAutowireCapableBeanFactory();
    }

    @Override
    protected Object createJobInstance(final TriggerFiredBundle bundle) throws Exception {
        final Object job = super.createJobInstance(bundle);
        LOG.info("create job instance");
        beanFactory.autowireBean(job);
        return job;
    }
}

Step 4: Create a spring bean java configuration class for configuring all the beans required for the quartz scheduler. We configure a job factory using the custom AutowiringSpringBeanJobFactory created above. We also setup the quartz scheduler using the SchedulerFactoryBean. Note how the SimpleJob is configured using JobDetailFactoryBean and SimpleTriggerFactoryBean.  The quartz configuration is loaded from the quartz.properties in classpath. The frequency of job is configured using the spring property simplejob.frequency. We will set this value in application.properties.

4. SchedulerConfig.java

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

import org.quartz.JobDetail;
import org.quartz.SimpleTrigger;
import org.quartz.Trigger;
import org.quartz.spi.JobFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
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.JobDetailFactoryBean;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean;

import com.quickprogrammingtips.springboot.jobs.SimpleJob;

@Configuration
public class SchedulerConfig {

    private static final Logger LOG = LoggerFactory.getLogger(SchedulerConfig.class);

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

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory, Trigger simpleJobTrigger)
            throws IOException {
        SchedulerFactoryBean factory = new SchedulerFactoryBean();
        factory.setJobFactory(jobFactory);
        factory.setQuartzProperties(quartzProperties());
        factory.setTriggers(simpleJobTrigger);
        LOG.info("starting jobs....");
        return factory;
    }

    @Bean
    public SimpleTriggerFactoryBean simpleJobTrigger(@Qualifier("simpleJobDetail") JobDetail jobDetail,
            @Value("${simplejob.frequency}") long frequency) {
        LOG.info("simpleJobTrigger");

        SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
        factoryBean.setJobDetail(jobDetail);
        factoryBean.setStartDelay(0L);
        factoryBean.setRepeatInterval(frequency);
        factoryBean.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
        return factoryBean;
    }

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

    @Bean
    public JobDetailFactoryBean simpleJobDetail() {
        JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
        factoryBean.setJobClass(SimpleJob.class);
        factoryBean.setDurability(true);
        return factoryBean;
    }
}

Step 5: Modify the main spring boot application to load the SchedulerConfig.

5. SpringbootquartzdemoApplication

package com.quickprogrammingtips.springboot;

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

@Import({ SchedulerConfig.class })
@SpringBootApplication
public class SpringbootquartzdemoApplication {

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

Step 6: Configure application.properties with simplejob.frequency entry. We set the job running every second!

6. application.properties

simplejob.frequency=1000

Step 7: Configure quartz.propertes with minimal quartz configuration.

7. quartz.properties

org.quartz.scheduler.instanceName=spring-boot-quartz-demo
org.quartz.scheduler.instanceId=AUTO
org.quartz.threadPool.threadCount=5
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore

Step 8: Run the application using the gradle bootRun task. If everything goes well, "Hello World!" will be printed on the console every second.