Adding Quartz to Spring Boot

Hello again. Especially for students of the course "Developer on the Spring Framework" prepared a translation of an interesting article.








In my article “Specifications to the Rescue”, I showed how you can use the JPA Specification in Spring Boot to implement filtering in the RESTful API. Then in the article “Testing those Specifications” it was shown how to test these same specifications.



The next step, I decided to demonstrate how to add a task scheduler to the same Spring Boot application.



Quartz Task Scheduler



The Spring team continues to facilitate Java development by providing various Spring Boot Starter plugins through simple maven dependency.



In this article, I will focus on the Quartz Scheduler starter, which can be added to the Spring Boot project with the following dependency:



<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-quartz</artifactId> </dependency>
      
      





The implementation is quite simple and is described here . You can see the full list of current Spring Boot Starter here .



Customization



Using a paper published by David Kiss , the first step is to add auto-binding for 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; } }
      
      





Next, add the basic Quartz configuration:



 @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; } }
      
      





You can put the properties used in the scheduler()



method out, but I specifically decided to simplify this example.



Then static methods are added that provide a programmatic way to create tasks 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 and useful method for creating tasks.

There are two options for triggers: based on CRON and simple triggers.



Services



Now the basic Quartz scheduler is ready to run tasks in our Spring Boot application. Next, we will create some examples of services that will be launched by the scheduler.



The first service displays simple membership statistics. If you remember, the example in the original project was related to a fitness club. In the MemberService



class MemberService



create the memberStats()



method:



 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 interests in fitness club classes, create a classStats()



method in classStats()



:



 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("=========================="); }
      
      





Tasks



To run the service code, you must create the appropriate job (Job). For MemberService



I created a MemberStatsJob



job MemberStatsJob



:



 @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()); } }
      
      





MemberClassService



class was created for MemberClassStatsJob



:



 @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()); } }
      
      





Task Schedule



In this project, we want all tasks to be scheduled when the Spring Boot server starts. To do this, I created the QuartzSubmitJobs



class, which includes four simple methods. Two methods create new tasks, and two methods create corresponding 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"); } }
      
      





Launch Spring Boot



When everything is ready, you can start the Spring Boot server and see the Quartz initialization:



 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 ossconcurrent.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 ossquartz.SchedulerFactoryBean : Starting Quartz Scheduler now 2019-07-14 14:36:52.054 ossquartz.LocalDataSourceJobStore : Freed 0 triggers from 'acquired' / 'blocked' state. 2019-07-14 14:36:52.056 ossquartz.LocalDataSourceJobStore : Recovering 0 jobs that were in-progress at the time of the last shut-down. 2019-07-14 14:36:52.056 ossquartz.LocalDataSourceJobStore : Recovery complete. 2019-07-14 14:36:52.056 ossquartz.LocalDataSourceJobStore : Removed 0 'complete' triggers. 2019-07-14 14:36:52.058 ossquartz.LocalDataSourceJobStore : Removed 0 stale fired job entries. 2019-07-14 14:36:52.058 org.quartz.core.QuartzScheduler : Scheduler MyInstanceName_$_Instance1 started.
      
      







And running the job memberStats()



:



 2019-07-14 14:36:52.096 cgjjpaspec.jobs.MemberStatsJob : Job ** Member Statistics Job ** starting @ Sun Jul 14 14:36:52 EDT 2019 2019-07-14 14:36:52.217 cgjjpaspec.service.MemberService : Member Statics: 2019-07-14 14:36:52.217 cgjjpaspec.service.MemberService : ============== 2019-07-14 14:36:52.217 cgjjpaspec.service.MemberService : Active member count: 7 2019-07-14 14:36:52.217 cgjjpaspec.service.MemberService : - Registered for Classes count: 6 2019-07-14 14:36:52.217 cgjjpaspec.service.MemberService : - Not registered for Classes count: 1 2019-07-14 14:36:52.217 cgjjpaspec.service.MemberService : Inactive member count: 3 2019-07-14 14:36:52.217 cgjjpaspec.service.MemberService : ========================== 2019-07-14 14:36:52.219 cgjjpaspec.jobs.MemberStatsJob : Job ** Member Statistics Job ** completed. Next job scheduled @ Sun Jul 14 14:37:51 EDT 2019
      
      





And then doing the classStats()



job:



 2019-07-14 14:40:00.006 cgjjpaspec.jobs.MemberClassStatsJob : Job ** Class Statistics Job ** starting @ Sun Jul 14 14:40:00 EDT 2019 2019-07-14 14:40:00.021 cgjjservice.MemberClassService : Class Statics: 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : ============= 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : Tennis: 4 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : FitCore 2000: 3 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : Spin: 2 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : Swimming: 4 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : New Class: 0 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : Basketball: 2 2019-07-14 14:40:00.022 cgjjservice.MemberClassService : ========================== 2019-07-14 14:40:00.022 cgjjpaspec.jobs.MemberClassStatsJob : Job ** Class Statistics Job ** completed. Next job scheduled @ Sun Jul 14 14:45:00 EDT 2019
      
      





Conclusion



In the above example, I used an existing project on Spring Boot and without much effort added the Quartz scheduler to it. I created service methods that performed simple data analysis. These service methods were launched by job classes. Finally, jobs and triggers were scheduled to run.



Full source code can be found here .



In the next article, I will show how to add a RESTful API to view Quartz settings information.



All Articles