{{announcement.body}}
{{announcement.title}}

Adding Quartz to Spring Boot

DZone 's Guide to

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.

· Java Zone ·
Free Resource

In 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!

Topics:
java, quartz, quartz scheduler, spring boot, spring boot starters, starters, tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}