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:


   org.springframework.boot
   spring-boot-starter-quartz

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 render the properties used in the method scheduler ()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;
   }
}

Method createJobDetail () – This 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 class MemberService create a method memberStats ():

public void memberStats () {
 List 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 the classes of the fitness club, we create in MemberClassService method classStats ():

public void classStats () {
 List memberClasses = classRepository.findAll ();
 Map memberClassesMap = memberClasses
   .stream ()
   .collect (Collectors.toMap (MemberClass :: getName, c -> 0));
 List 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. For MemberService I created a task class 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 ());
   }
}

For service MemberClassService class was created 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. For this, I created a class QuartzSubmitJobswhich 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 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.

And the job launch memberStats ():

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

And then doing the job classStats ():

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 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.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *