13/03/2020

[Java] Schedule tasks with Quartz and Spring

We saw how to schedule tasks with ScheduledExecutorService, and we now we will see how to achieve the same result using the Quartz framework.

Much like before, we have two key concepts:
  • job: a job is a runnable class that implements the Job interface
  • trigger: a Trigger is a specific schedule for a job. A trigger can only be linked to a job, and a job can be linked to multiple triggers.
You can find Quartz on Maven.

Some things to be mindful about:

1. by default Quartz will generate jobs with NO support for DI. To achieve that we need to thank Brian Matthews and jelies:

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


2. the easiest implementation of Quartz uses an in-memory storage, meaning if the application is restarted or crashes, ALL schedules are lost. You can use a JDBC storage instead for more reliability. You can configure your scheduler in a quartz.properties file, for example minimal settings:

org.quartz.scheduler.instanceName=SOME_NAME
org.quartz.threadPool.threadCount=3
org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore

We then create a properties bean:

 @Bean  
 public Properties quartzProperties() {  
      PropertiesFactoryBean propertiesFactoryBean = new PropertiesFactoryBean();  
      propertiesFactoryBean.setLocation(new ClassPathResource("/quartz.properties"));  
      Properties properties;  
      try {  
           propertiesFactoryBean.afterPropertiesSet();  
           properties = propertiesFactoryBean.getObject();  
   
      } catch (IOException e) {  
           System.err.println("Cannot load quartz.properties!"); 
      }  
   
      return properties;  
 }  


And the scheduler bean:

 @Bean  
 public SchedulerFactoryBean quartzScheduler() {  
      SchedulerFactoryBean quartzScheduler = new SchedulerFactoryBean();  
        
      AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();  
      jobFactory.setApplicationContext(applicationContext);  
      quartzScheduler.setJobFactory(jobFactory);  
   
      quartzScheduler.setQuartzProperties(quartzProperties());  
   
      return quartzScheduler;  
 }  


3. Quartz uses java Calendar and java Date objects for its operations, you can force a system-wide TimeZone by changing the default (for example to set UTC):

 import java.util.TimeZone;  
   
 TimeZone.setDefault("UTC");  


Then we can start the scheduler:

 quartzScheduler.start();  


4. after a scheduler has been shut down IT CANNOT BE RESTARTED. To shut down the scheduler AND wait for running jobs to complete, use:

 quartzScheduler.shutdown(true);  


Now we can start our coding, for example creating a job:

 //or also .newJob(SomeJob.class)  
 JobDetail job = JobBuilder.newJob(Class.forName("SOME_JOB").asSubclass(Job.class))  
 .withIdentity("SOME_NAME", "SOME_GROUP") //unique identifier for this job  
 .usingJobData(new JobDataMap()) //a HashMap-like structure containing data that every schedule running this job will have  
 .build();  


And our job class might look like this (we inject the scheduler just for testing, it is not necessary):

 public class SampleJob implements Job {  
   
  @Autowired  
  private Scheduler quartzScheduler;  
    
  public void execute(JobExecutionContext context) throws JobExecutionException {  
    
   try {  
    JobDataMap dataMap = context.getMergedJobDataMap();  
      
    System.out.println("Sample job! From scheduler: " + quartzScheduler.getSchedulerName());  
      
    for(Map.Entry<String, Object> data : dataMap.entrySet()) {  
     System.out.println("Got: " + data.getKey() + ": " + data.getValue());  
    }  
      
   } catch (SchedulerException e) {  
    System.err.println("BAD LUCK");  
   }  
  }  
 }  


then we create a simple schedule for it:

 //a trigger that runs forever every second and on a misfire it runs immediately, then continues on normal schedule  
 ScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()  
 .repeatForever() //this trigger will run forever  
 .withMisfireHandlingInstructionFireNow() //you can set whatever misfire handling you need here  
 .withIntervalInSeconds(1); //also you can configure as you need here  


or a more complex schedule:

 //a trigger that runs every day according to "onDaysOfTheWeek", it is a set of integers representing the active days, with Monday = 1  
 //on a misfire it runs immediately, then continues on normal schedule  
 //it starts every day at the specified time (FIRST execution here)  
 //it stops every day at the specified time (LAST execution here). This is NOT a cron expression in format FROM-TO, because in cron, the last execution (TO) would NOT happen  
 //running with a frequency of 1 second  
 ScheduleBuilder scheduleBuilder = DailyTimeIntervalScheduleBuilder.dailyTimeIntervalSchedule()  
 .withMisfireHandlingInstructionFireAndProceed()  
 .startingDailyAt(TimeOfDay.hourAndMinuteFromDate(new Date()))  
 .endingDailyAt(TimeOfDay.hourAndMinuteFromDate(new Date()))  
 .withIntervalInSeconds(1)  
 .onDaysOfTheWeek(new HashSet<Integer>());  


We can now create a trigger for that job with our schedule:

 Trigger trigger = newTrigger()  
 .withIdentity("SOME_NAME", "SOME_GROUP") //unique identifier for this trigger  
 .forJob(job)  
 .withSchedule(scheduleBuilder)  
 .usingJobData(new JobDataMap()) //a HashMap-like structure containing data that only this schedule will have  
 .startNow() //or startAt(new Date())  
 .build();  


and finally schedule it:

 quartzScheduler.scheduleJob(job, trigger);  


Lastly, if we want to unschedule ALL jobs (for example in a test environment), we can run:

   
 public void unscheduleJobs() throws SchedulerException {  
   
  for (String group : quartzScheduler.getJobGroupNames()) {  
     
   for (JobKey jobKey : quartzScheduler.getJobKeys(GroupMatcher.jobGroupEquals(group))) {  
     
    final List<Trigger> triggers = (List<Trigger>) quartzScheduler.getTriggersOfJob(jobKey);  
      
    final List<TriggerKey> triggerKeys = new ArrayList<>(triggers.size());  
      
    for(Trigger trigger : triggers) {  
     triggerKeys.add(trigger.getKey());  
    }  
      
    quartzScheduler.unscheduleJobs(triggerKeys);  
    quartzScheduler.deleteJob(jobKey);  
     
   }  
  }  
   
 }  

No comments:

Post a Comment

With great power comes great responsibility