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 :

[code language=”xml”]
<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>
[/code]

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.

[code language=”text”]

schedule :
jobs :

cronExpression : 0 0/2 * * * ?
dataToWrite : frst job

cronExpression : 0 0/1 * * * ?
dataToWrite : second job
[/code]

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 :

[code language=”text”]
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
[/code]

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

[code language=”xml”]
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-web</artifactid>
</dependency>
[/code]

so that we can enable bean validation.

We will also add the spring-tx dependency :

[code language=”xml”]
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
[/code]

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.

[code language=”java”]
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;
}
}
[/code]

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 :

[code language=”java”]
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;
}
}
[/code]

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 :

[code language=”java”]
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();
}
}
[/code]

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 :

[code language=”java”]
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;
}
}
[/code]

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 :

[code language=”java”]
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;
}
}
[/code]

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

[code language=”java”]
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;
}
}
[/code]

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

[code language=”java”]
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;
}
}
[/code]

Finally we are ready to create the scheduler class :

[code language=”java”]
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
}
}
}
[/code]

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

[code language=”text”]
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
[/code]

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 🙂

Spread the love

5 Comments

  1. 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)}

  2. Author

    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

  3. Thanks a lot, Ice,
    solution is simple and it’s working, i’ve spent a few days in looking for such good approach.

Leave a Reply

Your email address will not be published.