Spring Boot 1.4 and Quartz, scheduling runtime created job instances from a configuration file

Hello guys and gals :). In this post i will write my solution on how to create runtime instances of a number of configured jobs from a configuration file with Spring Boot 1.4 and Quartz.

So let us begin.

First we have to add the Quartz dependencies :

<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz</artifactId>
    <version>2.2.1</version>
</dependency>
<dependency>
    <groupId>org.quartz-scheduler</groupId>
    <artifactId>quartz-jobs</artifactId>
    <version>2.2.1</version>
</dependency>

Here you can find more about the Quartz scheduler.

Next we will create our external application.yml file which needs to be in the same directory as our executable jar or just under our project root if we are running our app through IntelliJ Idea or Eclipse.


schedule :
  jobs :
    -
      cronExpression : 0 0/2 * * * ?
      dataToWrite : frst job
    -
      cronExpression : 0 0/1 * * * ?
      dataToWrite : second job

In the application.yml file we specify a root node which is “schedule”. Under the root node we specify a list of nodes “jobs”. To specify a single job, we will need to a new node with only a dash “-“. Under the dash “-” node we can specify our properties or configuration for each job instance. In my example i will be using a Cron expression to trigger my jobs so i have set a “cronExpression” property and i will pass some data that my job needs through the “dataToWrite” property.

We also need to create a quartz.properties file to setup some configuration for the quartz scheduler :

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

Next we need to add the spring-boot-starter-web dependency :

<dependency>
    <groupid>org.springframework.boot</groupid>
    <artifactid>spring-boot-starter-web</artifactid>
</dependency>

so that we can enable bean validation.

We will also add the spring-tx dependency :

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
</dependency>

Next we create two classes that will map to our configuration and use spring’s type-safe properties to get the data from the application.yml file.

package com.example.quartz.dynamic.job.config;

import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;

import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;

/**
 * Maps to the root of the configuration and has a property of a List of
 * JobProperties objects.
 */
@ConfigurationProperties(prefix = "schedule")
public class JobScheduleProperties {

    @NotNull
    @NotEmpty
    @Valid
    private List<JobProperties> jobs;

    public List<JobProperties> getJobs() {
       return jobs;
    }

    public void setJobs(List<JobProperties> jobs) {
       this.jobs = jobs;
    }
}

We mark the list jobProperties with some bean validation rules, so the list can’t be empty, null or invalid. What the last @Valid anottation means is that each JobProperties object inside the list also has to be validated. So next we create the JobProperties class :

package com.example.quartz.dynamic.job.config;

import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.stereotype.Component;

import javax.validation.constraints.NotNull;

/**
* Properties for a single job.
*/
@Component
public class JobProperties {

   @NotNull
   @NotEmpty
   private String cronExpression;

   @NotNull
   @NotEmpty
   private String dataToWrite;

   public String getCronExpression() {
      return cronExpression;
   }

   public void setCronExpression(String cronExpression) {
      this.cronExpression = cronExpression;
   }

   public String getDataToWrite() {
      return dataToWrite;
   }

   public void setDataToWrite(String dataToWrite) {
      this.dataToWrite = dataToWrite;
   }
}

As we can see here, there is also bean validation applied to the properties so that we can be sure that all the properties are set and configured.

Now we need to create a configuration class which will inject a SchedulerFactoryBean and a JobFactory bean :

package com.example.quartz.dynamic.job.config;

import org.quartz.spi.JobFactory;
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.SchedulerFactoryBean;

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

/**
* Configuration for the Quartz implementation with Spring Boot
*/
@Configuration
public class SchedulerConfig {

    public static final String QUARTZ_PROPERTIES_PATH = "/quartz.properties";

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

    @Bean
    public SchedulerFactoryBean schedulerFactoryBean(JobFactory jobFactory) throws   IOException {
      SchedulerFactoryBean factory = new SchedulerFactoryBean();
      factory.setAutoStartup(true);
      factory.setJobFactory(jobFactory);
      factory.setQuartzProperties(quartzProperties());
      return factory;
   }

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

In order for the jobs to be injected as beans, we need to create an AutowiringSpringBeanJobFactory class which extends SpringBeanJobFactory and implements the ApplicationContextAware interface :

package com.example.quartz.dynamic.job.config;

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

/**
* Adds autowiring support to quartz jobs.
*/
public final class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory implements
ApplicationContextAware {

   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);
     beanFactory.autowireBean(job);
     return job;
  }
}

Now that we have all the configuration and properties set it’s time to create the scheduler and job runner classes.

First we will create a model class to hold the JobDetail and Trigger for each job :

package com.example.quartz.dynamic.job.schedule;

import org.quartz.JobDetail;
import org.quartz.Trigger;

/**
 *  Model containing the JobDetails and Trigger of a Job.
 */
public class JobScheduleModel {

    private JobDetail jobDetail;
    private Trigger trigger;

    public JobScheduleModel(JobDetail jobDetail, Trigger trigger) {
        this.jobDetail = jobDetail;
        this.trigger = trigger;
    }

    public JobDetail getJobDetail() {
        return jobDetail;
    }

    public Trigger getTrigger() {
        return trigger;
    }
}

Next we will create the JobRunner class which will do the actual task of the job :

package com.example.quartz.dynamic.job.schedule;

import com.example.quartz.dynamic.job.service.SomeService;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;

/**
 * Created by Ice on 11/4/2016.
 */
@PersistJobDataAfterExecution
@DisallowConcurrentExecution
public class JobRunner implements Job {

    private String dataToWrite;

    @Autowired
    private SomeService someService;

    @Override
    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
        someService.writeDataToLog(dataToWrite);
    }

    public void setDataToWrite(String dataToWrite) {
        this.dataToWrite = dataToWrite;
    }
}

Now we can create the JobScheduleModelGenerator class which will generate a list of job models :

package com.example.quartz.dynamic.job.schedule;

import com.example.quartz.dynamic.job.config.JobProperties;
import com.example.quartz.dynamic.job.config.JobScheduleProperties;
import org.quartz.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;

import static org.quartz.CronScheduleBuilder.cronSchedule;

/**
 * Generates a list of JobScheduleModel from the JobScheduleProperties
 */
@Component
public class JobSchedulerModelGenerator {

    public static final String JOB_NAME = "JobName";
    public static final String GROUP_NAME = "Group";
    public static final String DATA_TO_WRITE = "dataToWrite";

    private JobScheduleProperties jobScheduleProperties;

    @Autowired
    public JobSchedulerModelGenerator(JobScheduleProperties jobScheduleProperties) {
        this.jobScheduleProperties = jobScheduleProperties;
    }

    public List<JobScheduleModel> generateModels() {
        List<JobProperties> jobs = jobScheduleProperties.getJobs();
        List<JobScheduleModel> generatedModels = new ArrayList<>();
        for (int i = 0; i < jobs.size(); i++) {
            JobScheduleModel model = generateModelFrom(jobs.get(i), i);
            generatedModels.add(model);
        }
        return generatedModels;
    }

    private JobScheduleModel generateModelFrom(JobProperties job, int jobIndex) {
        JobDetail jobDetail = getJobDetailFor(JOB_NAME + jobIndex, GROUP_NAME, job);

        Trigger trigger = getTriggerFor(job.getCronExpression(), jobDetail);
        JobScheduleModel jobScheduleModel = new JobScheduleModel(jobDetail, trigger);
        return jobScheduleModel;
    }

    private JobDetail getJobDetailFor(String jobName, String groupName, JobProperties job) {
        JobDetail jobDetail = JobBuilder.newJob(JobRunner.class)
                .setJobData(getJobDataMapFrom(job.getDataToWrite()))
                .withDescription("Job with data to write : " + job.getDataToWrite() +
                        " and CRON expression : " + job.getCronExpression())
                .withIdentity(jobName, groupName)
                .build();
        return jobDetail;
    }

    private JobDataMap getJobDataMapFrom(String dataToWrite) {
        JobDataMap jobDataMap = new JobDataMap();
        jobDataMap.put(DATA_TO_WRITE, dataToWrite);
        return jobDataMap;
    }

    private Trigger getTriggerFor(String cronExpression, JobDetail jobDetail) {
        Trigger trigger = TriggerBuilder.newTrigger()
                .forJob(jobDetail)
                .withSchedule(cronSchedule(cronExpression))
                .build();
        return trigger;
    }
}

Finally we are ready to create the scheduler class :

package com.example.quartz.dynamic.job.schedule;

import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.List;

/**
 * Scheduler to schedule and start the configured jobs
 */
@Component
public class QuartzScheduler {

    private SchedulerFactoryBean schedulerFactoryBean;
    private JobSchedulerModelGenerator jobSchedulerModelGenerator;

    @Autowired
    public QuartzScheduler(SchedulerFactoryBean schedulerFactoryBean, JobSchedulerModelGenerator jobSchedulerModelGenerator) {
        this.schedulerFactoryBean = schedulerFactoryBean;
        this.jobSchedulerModelGenerator = jobSchedulerModelGenerator;
    }

    @PostConstruct
    public void init() {
        scheduleJobs();
    }

    public void scheduleJobs() {
        Scheduler scheduler = schedulerFactoryBean.getScheduler();
        List<JobScheduleModel> jobScheduleModels = jobSchedulerModelGenerator.generateModels();
        for (JobScheduleModel model : jobScheduleModels) {
            try {
                scheduler.scheduleJob(model.getJobDetail(), model.getTrigger());
            } catch (SchedulerException e) {
                // log the error
            }
        }
        try {
            scheduler.start();
        } catch (SchedulerException e) {
            // log the error
        }
    }
}

After we run the application, we can see that the jobs wrote the values in the property “dataToWrite” to the log :

2016-11-05 12:46:06.769 INFO 1504 --- [ main] c.e.q.d.job.QuartzDynamicJobApplication : Started QuartzDynamicJobApplication in 7.852 seconds (JVM running for 8.622)
2016-11-05 12:47:00.016 INFO 1504 --- [ryBean_Worker-1] c.e.q.dynamic.job.service.SomeService : The data is : second job
2016-11-05 12:48:00.003 INFO 1504 --- [ryBean_Worker-2] c.e.q.dynamic.job.service.SomeService : The data is : second job
2016-11-05 12:48:00.007 INFO 1504 --- [ryBean_Worker-3] c.e.q.dynamic.job.service.SomeService : The data is : frst job

And that’s it :). You can find the code in the Git repo.

I would love to hear from you either in the comments section or on Twitter 🙂

5 thoughts on “Spring Boot 1.4 and Quartz, scheduling runtime created job instances from a configuration file

  1. Sorry i only needed this to work with a RamJobStore. I tried to use the JDBC Job Store once but i wasn’t able to get it to work and it wasn’t really necessary for my use case :S

    Like

  2. Hi,
    Thanks for the sample code. Have you been able to get this to work with a jdbc job store?? Your example works perfectly as-is, but as soon as I change it to a jdbc job store (so job are persistent), it will no longer Autowire.

    org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name ‘com.example.quartz.dynamic.job.schedule.JobRunner’: Unsatisfied dependency expressed through field ‘someService’; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type ‘com.example.quartz.dynamic.job.service.SomeService’ available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

    Liked by 1 person

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s