Adding Quartz to Spring Boot
Add a Quartz scheduler to an existing repository, showing just how quick and easily this can be accomplished — thanks to Spring Boot Starters.
Join the DZone community and get the full member experience.
Join For FreeIn my "Specifications to the Rescue" article, I presented the ease and ability to leverage the JPA Specification in Spring Boot to provide filter options for your RESTful API. I followed up with another article, called "Testing those Specifications," which covered how those very specifications can be tested.
Taking things another step further, I thought I would demonstrate just how easy it is to add a job scheduler into the same Spring Boot application.
Quartz Scheduler
The team at Spring has continued to make Java development easier, by continuing to introduce Spring Boot Starters and providing baseline functionality for a specified integration via a simple Maven dependency.
In this article, I am going to focus on the Quartz Scheduler starter, which can be added to a Spring Boot project by adding the following dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
The implementation is quite simple and is explained here. And you can review a full list of current Spring Boot Starters here.
Setting Everything Up
Leveraging work published by David Kiss, the first step is to add auto-wiring support for Quartz jobs created in the project:
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;
}
}
Next, we can add the base configuration for Quartz:
@Configuration
public class QuartzConfig {
private ApplicationContext applicationContext;
private DataSource dataSource;
public QuartzConfig(ApplicationContext applicationContext, DataSource dataSource) {
this.applicationContext = applicationContext;
this.dataSource = dataSource;
}
@Bean
public SpringBeanJobFactory springBeanJobFactory() {
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
@Bean
public SchedulerFactoryBean scheduler(Trigger... triggers) {
SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean();
Properties properties = new Properties();
properties.setProperty("org.quartz.scheduler.instanceName", "MyInstanceName");
properties.setProperty("org.quartz.scheduler.instanceId", "Instance1");
schedulerFactory.setOverwriteExistingJobs(true);
schedulerFactory.setAutoStartup(true);
schedulerFactory.setQuartzProperties(properties);
schedulerFactory.setDataSource(dataSource);
schedulerFactory.setJobFactory(springBeanJobFactory());
schedulerFactory.setWaitForJobsToCompleteOnShutdown(true);
if (ArrayUtils.isNotEmpty(triggers)) {
schedulerFactory.setTriggers(triggers);
}
return schedulerFactory;
}
}
We could externalize the properties used in the scheduler()
method but I decided to keep things really simple for this example.
Next, static methods are added to provide a programmatic way to schedule jobs and triggers:
@Slf4j
@Configuration
public class QuartzConfig {
...
static SimpleTriggerFactoryBean createTrigger(JobDetail jobDetail, long pollFrequencyMs, String triggerName) {
log.debug("createTrigger(jobDetail={}, pollFrequencyMs={}, triggerName={})", jobDetail.toString(), pollFrequencyMs, triggerName);
SimpleTriggerFactoryBean factoryBean = new SimpleTriggerFactoryBean();
factoryBean.setJobDetail(jobDetail);
factoryBean.setStartDelay(0L);
factoryBean.setRepeatInterval(pollFrequencyMs);
factoryBean.setName(triggerName);
factoryBean.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY);
factoryBean.setMisfireInstruction(SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NEXT_WITH_REMAINING_COUNT);
return factoryBean;
}
static CronTriggerFactoryBean createCronTrigger(JobDetail jobDetail, String cronExpression, String triggerName) {
log.debug("createCronTrigger(jobDetail={}, cronExpression={}, triggerName={})", jobDetail.toString(), cronExpression, triggerName);
// To fix an issue with time-based cron jobs
Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
CronTriggerFactoryBean factoryBean = new CronTriggerFactoryBean();
factoryBean.setJobDetail(jobDetail);
factoryBean.setCronExpression(cronExpression);
factoryBean.setStartTime(calendar.getTime());
factoryBean.setStartDelay(0L);
factoryBean.setName(triggerName);
factoryBean.setMisfireInstruction(CronTrigger.MISFIRE_INSTRUCTION_DO_NOTHING);
return factoryBean;
}
static JobDetailFactoryBean createJobDetail(Class jobClass, String jobName) {
log.debug("createJobDetail(jobClass={}, jobName={})", jobClass.getName(), jobName);
JobDetailFactoryBean factoryBean = new JobDetailFactoryBean();
factoryBean.setName(jobName);
factoryBean.setJobClass(jobClass);
factoryBean.setDurability(true);
return factoryBean;
}
}
The createJobDetail()
method is a simple, helpful method to create jobs. Two trigger options exist, one for CRON-based triggers and the other for simple triggers.
Creating Services
At this point the base Quartz scheduler is ready to run jobs against our Spring Boot application. The next step is to create some sample services to have the scheduler execute.
The first service is to create some very simple membership stats. If you recall, the sample data in the original project was to track information related to a fitness club. Within the MemberService
class, a memberStats()
method was created:
public void memberStats() {
List<Member> members = memberRepository.findAll();
int activeCount = 0;
int inactiveCount = 0;
int registeredForClassesCount = 0;
int notRegisteredForClassesCount = 0;
for (Member member : members) {
if (member.isActive()) {
activeCount++;
if (CollectionUtils.isNotEmpty(member.getMemberClasses())) {
registeredForClassesCount++;
} else {
notRegisteredForClassesCount++;
}
} else {
inactiveCount++;
}
}
log.info("Member Statics:");
log.info("==============");
log.info("Active member count: {}", activeCount);
log.info(" - Registered for Classes count: {}", registeredForClassesCount);
log.info(" - Not registered for Classes count: {}", notRegisteredForClassesCount);
log.info("Inactive member count: {}", inactiveCount);
log.info("==========================");
}
To track fitness club interest in classes, a simple classStats()
method was created in the MemberClassService
:
public void classStats() {
List<MemberClass> memberClasses = classRepository.findAll();
Map<String, Integer> memberClassesMap = memberClasses
.stream()
.collect(Collectors.toMap(MemberClass::getName, c -> 0));
List<Member> members = memberRepository.findAll();
for (Member member : members) {
if (CollectionUtils.isNotEmpty(member.getMemberClasses())) {
for (MemberClass memberClass : member.getMemberClasses()) {
memberClassesMap.merge(memberClass.getName(), 1, Integer::sum);
}
}
}
log.info("Class Statics:");
log.info("=============");
memberClassesMap.forEach((k,v) -> log.info("{}: {}", k, v));
log.info("==========================");
}
Creating the Jobs
With the services in place, jobs need to be created to launch the code in the appropriate service. For the MemberService
job, I created the MemberStatsJob
class:
@Slf4j
@Component
@DisallowConcurrentExecution
public class MemberStatsJob implements Job {
@Autowired
private MemberService memberService;
@Override
public void execute(JobExecutionContext context) {
log.info("Job ** {} ** starting @ {}", context.getJobDetail().getKey().getName(), context.getFireTime());
memberService.memberStats();
log.info("Job ** {} ** completed. Next job scheduled @ {}", context.getJobDetail().getKey().getName(), context.getNextFireTime());
}
}
For the MemberClassService
job, the MemberClassStatsJob
class was created:
@Slf4j
@Component
@DisallowConcurrentExecution
public class MemberClassStatsJob implements Job {
@Autowired
MemberClassService memberClassService;
@Override
public void execute(JobExecutionContext context) {
log.info("Job ** {} ** starting @ {}", context.getJobDetail().getKey().getName(), context.getFireTime());
memberClassService.classStats();
log.info("Job ** {} ** completed. Next job scheduled @ {}", context.getJobDetail().getKey().getName(), context.getNextFireTime());
}
}
Scheduling the Jobs
For this project, we want to make sure all jobs are scheduled when the Spring Boot server starts. In order to make this happen, I created the QuartzSubmitJobs
class, which includes four simple methods. Two methods will create new jobs and two methods will create the appropriate triggers.
@Configuration
public class QuartzSubmitJobs {
private static final String CRON_EVERY_FIVE_MINUTES = "0 0/5 * ? * * *";
@Bean(name = "memberStats")
public JobDetailFactoryBean jobMemberStats() {
return QuartzConfig.createJobDetail(MemberStatsJob.class, "Member Statistics Job");
}
@Bean(name = "memberStatsTrigger")
public SimpleTriggerFactoryBean triggerMemberStats(@Qualifier("memberStats") JobDetail jobDetail) {
return QuartzConfig.createTrigger(jobDetail, 60000, "Member Statistics Trigger");
}
@Bean(name = "memberClassStats")
public JobDetailFactoryBean jobMemberClassStats() {
return QuartzConfig.createJobDetail(MemberClassStatsJob.class, "Class Statistics Job");
}
@Bean(name = "memberClassStatsTrigger")
public CronTriggerFactoryBean triggerMemberClassStats(@Qualifier("memberClassStats") JobDetail jobDetail) {
return QuartzConfig.createCronTrigger(jobDetail, CRON_EVERY_FIVE_MINUTES, "Class Statistics Trigger");
}
}
Starting Spring Boot
With everything ready, the Spring Boot server can be started and the Quartz scheduler initialized:
2019-07-14 14:36:51.651 org.quartz.impl.StdSchedulerFactory : Quartz scheduler 'MyInstanceName' initialized from an externally provided properties instance.
2019-07-14 14:36:51.651 org.quartz.impl.StdSchedulerFactory : Quartz scheduler version: 2.3.0
2019-07-14 14:36:51.651 org.quartz.core.QuartzScheduler : JobFactory set to: com.gitlab.johnjvester.jpaspec.config.AutowiringSpringBeanJobFactory@79ecc507
2019-07-14 14:36:51.851 o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2019-07-14 14:36:51.901 aWebConfiguration$JpaWebMvcConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2019-07-14 14:36:52.051 o.s.s.quartz.SchedulerFactoryBean : Starting Quartz Scheduler now
2019-07-14 14:36:52.054 o.s.s.quartz.LocalDataSourceJobStore : Freed 0 triggers from 'acquired' / 'blocked' state.
2019-07-14 14:36:52.056 o.s.s.quartz.LocalDataSourceJobStore : Recovering 0 jobs that were in-progress at the time of the last shut-down.
2019-07-14 14:36:52.056 o.s.s.quartz.LocalDataSourceJobStore : Recovery complete.
2019-07-14 14:36:52.056 o.s.s.quartz.LocalDataSourceJobStore : Removed 0 'complete' triggers.
2019-07-14 14:36:52.058 o.s.s.quartz.LocalDataSourceJobStore : Removed 0 stale fired job entries.
2019-07-14 14:36:52.058 org.quartz.core.QuartzScheduler : Scheduler MyInstanceName_$_Instance1 started.
The memberStats()
job fired next:
2019-07-14 14:36:52.096 c.g.j.jpaspec.jobs.MemberStatsJob : Job ** Member Statistics Job ** starting @ Sun Jul 14 14:36:52 EDT 2019
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : Member Statics:
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : ==============
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : Active member count: 7
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : - Registered for Classes count: 6
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : - Not registered for Classes count: 1
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : Inactive member count: 3
2019-07-14 14:36:52.217 c.g.j.jpaspec.service.MemberService : ==========================
2019-07-14 14:36:52.219 c.g.j.jpaspec.jobs.MemberStatsJob : Job ** Member Statistics Job ** completed. Next job scheduled @ Sun Jul 14 14:37:51 EDT 2019
Then, the classStats()
job also executed:
2019-07-14 14:40:00.006 c.g.j.jpaspec.jobs.MemberClassStatsJob : Job ** Class Statistics Job ** starting @ Sun Jul 14 14:40:00 EDT 2019
2019-07-14 14:40:00.021 c.g.j.j.service.MemberClassService : Class Statics:
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : =============
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Tennis: 4
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : FitCore 2000: 3
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Spin: 2
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Swimming: 4
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : New Class: 0
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : Basketball: 2
2019-07-14 14:40:00.022 c.g.j.j.service.MemberClassService : ==========================
2019-07-14 14:40:00.022 c.g.j.jpaspec.jobs.MemberClassStatsJob : Job ** Class Statistics Job ** completed. Next job scheduled @ Sun Jul 14 14:45:00 EDT 2019
Conclusion
In the example above, I leveraged an existing Spring Boot repository and added the Quartz scheduler without a lot of effort. From there, I was able to create service methods, which would perform a simple analysis against the data. Those service methods were then launched by job classes. Finally, the jobs and triggers were scheduled to complete the installation.
If you would like to see the full source code, my repository can be found here.
In my next article, I will demonstrate how a RESTful API can be added to an interface with the Quartz scheduler.
Have a really great day!
Opinions expressed by DZone contributors are their own.
Comments