Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TaskScheduler, @Scheduled and quartz

Tags:

Is there a way to have @Scheduled with quartz as the underlying scheduler?

Two things that I can think of, but both require some work:

  • create a custom BeanPostProcessor that will parse the @Scheduled annotation and register quartz jobs
  • implement TaskScheduler to delegate to the quartz Scheduler.

The question is: is there something already written for the above two options and is there another option?

like image 864
Bozho Avatar asked Jul 22 '11 10:07

Bozho


People also ask

Does spring @scheduled use Quartz?

Based on the docs, Spring provides three way of scheduling: @Scheduled. Via Quartz. Via JDK Timer.

Does spring use Quartz?

Spring Boot has built-in support for Quartz. It automatically creates a Quartz Scheduler bean with the configuration that we supplied in the application. properties file. That's why we could directly inject the Scheduler in the controller.

Is Quartz distributed scheduler?

Quartz is distributed as a small java library (. jar file) that contains all of the core Quartz functionality. The main interface (API) to this functionality is the Scheduler interface. It provides simple operations such as scheduling/unscheduling jobs, starting/stopping/pausing the scheduler.


1 Answers

I ended up making my own spring-quartz "bridge". I plan on suggesting it as improvement to spring.

First, I created a new annotation, that is to be placed on classes implementing the quartz Job interface:

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Component @Scope("prototype") public @interface ScheduledJob {     String cronExpression() default "";     long fixedRate() default -1;     boolean durable() default false;     boolean shouldRecover() default true;     String name() default "";     String group() default ""; } 

(Note the prototype scope - quartz assumes each job execution is a new instance. I am not a quartz expert, so I conformed to that expectation. If it turns out redundant, you can simply remove the @Scope annotation)

Then I defined an ApplicationListener that, whenever the context is refreshed (or started) looks up all classes annotated with @ScheduledJob and registers them in the quartz scheduler:

/**  * This class listeners to ContextStartedEvent, and when the context is started  * gets all bean definitions, looks for the @ScheduledJob annotation,  * and registers quartz jobs based on that.  *  * Note that a new instance of the quartz job class is created on each execution,  * so the bean has to be of "prototype" scope. Therefore an applicationListener is used  * rather than a bean postprocessor (unlike singleton beans, prototype beans don't get  * created on application startup)  *  * @author bozho  *  */  public class QuartzScheduledJobRegistrar implements     EmbeddedValueResolverAware, ApplicationContextAware,     ApplicationListener<ContextRefreshedEvent> {  private Scheduler scheduler;  private StringValueResolver embeddedValueResolver;  private Map<JobListener, String> jobListeners;  private ApplicationContext applicationContext;  public void setEmbeddedValueResolver(StringValueResolver resolver) {     this.embeddedValueResolver = resolver; }  public void setApplicationContext(ApplicationContext applicationContext) {     this.applicationContext = applicationContext; }  @SuppressWarnings("unchecked") @Override public void onApplicationEvent(ContextRefreshedEvent event) {     if (event.getApplicationContext() == this.applicationContext) {         try {             scheduler.clear();              for (Map.Entry<JobListener, String> entry : jobListeners.entrySet()) {                 scheduler.getListenerManager().addJobListener(entry.getKey(), NameMatcher.nameStartsWith(entry.getValue()));             }         } catch (SchedulerException ex) {             throw new IllegalStateException(ex);         }          DefaultListableBeanFactory factory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();         String[] definitionNames = factory.getBeanDefinitionNames();         for (String definitionName : definitionNames) {             BeanDefinition definition = factory.getBeanDefinition(definitionName);             try {                 if (definition.getBeanClassName() != null) {                     Class<?> beanClass = Class.forName(definition.getBeanClassName());                     registerJob(beanClass);                 }             } catch (ClassNotFoundException e) {                 throw new IllegalArgumentException(e);             }         }     } }  public void registerJob(Class<?> targetClass) {     ScheduledJob annotation = targetClass.getAnnotation(ScheduledJob.class);      if (annotation != null) {         Assert.isTrue(Job.class.isAssignableFrom(targetClass),                 "Only classes implementing the quartz Job interface can be annotated with @ScheduledJob");          @SuppressWarnings("unchecked") // checked on the previous line         Class<? extends Job> jobClass = (Class<? extends Job>) targetClass;          JobDetail jobDetail = JobBuilder.newJob()             .ofType(jobClass)             .withIdentity(                     annotation.name().isEmpty() ? targetClass.getSimpleName() : annotation.name(),                     annotation.group().isEmpty() ? targetClass.getPackage().getName() : annotation.group())             .storeDurably(annotation.durable())             .requestRecovery(annotation.shouldRecover())             .build();          TriggerBuilder<Trigger> triggerBuilder = TriggerBuilder.newTrigger()             .withIdentity(jobDetail.getKey().getName() + "_trigger", jobDetail.getKey().getGroup() + "_triggers")             .startNow();          String cronExpression = annotation.cronExpression();         long fixedRate = annotation.fixedRate();         if (!BooleanUtils.xor(new boolean[] {!cronExpression.isEmpty(), fixedRate >=0})) {             throw new IllegalStateException("Exactly one of 'cronExpression', 'fixedRate' is required. Offending class " + targetClass.getName());         }          if (!cronExpression.isEmpty()) {             if (embeddedValueResolver != null) {                 cronExpression = embeddedValueResolver.resolveStringValue(cronExpression);             }             try {                 triggerBuilder.withSchedule(CronScheduleBuilder.cronSchedule(cronExpression));             } catch (ParseException e) {                 throw new IllegalArgumentException(e);             }         }           if (fixedRate >= 0) {             triggerBuilder.withSchedule(                         SimpleScheduleBuilder.simpleSchedule()                             .withIntervalInMilliseconds(fixedRate)                             .repeatForever())                 .withIdentity(jobDetail.getKey().getName() + "_trigger", jobDetail.getKey().getGroup() + "_triggers");         }          try {             scheduler.scheduleJob(jobDetail, triggerBuilder.build());         } catch (SchedulerException e) {             throw new IllegalStateException(e);         }     } }  public void setScheduler(Scheduler scheduler) {     this.scheduler = scheduler; }  public void setJobListeners(Map<JobListener, String> jobListeners) {     this.jobListeners = jobListeners; } } 

Then I needed a custom JobFactory to plug in quartz so that jobs are created by the spring context:

public class QuartzSpringBeanJobFactory extends SpringBeanJobFactory implements ApplicationContextAware {  private SchedulerContext schedulerContext; private ApplicationContext ctx;  @Override protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {     Job job = ctx.getBean(bundle.getJobDetail().getJobClass());     BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(job);     MutablePropertyValues pvs = new MutablePropertyValues();     pvs.addPropertyValues(bundle.getJobDetail().getJobDataMap());     pvs.addPropertyValues(bundle.getTrigger().getJobDataMap());     if (this.schedulerContext != null) {         pvs.addPropertyValues(this.schedulerContext);     }     bw.setPropertyValues(pvs, true);     return job; }  public void setSchedulerContext(SchedulerContext schedulerContext) {     this.schedulerContext = schedulerContext;     super.setSchedulerContext(schedulerContext); }  @Override public void setApplicationContext(ApplicationContext applicationContext)         throws BeansException {     this.ctx = applicationContext; } } 

Finally, the xml configuration:

    <bean id="quartzScheduler" class="org.springframework.scheduling.quartz.SchedulerFactoryBean">     <property name="jobFactory">         <bean class="com.foo.bar.scheduling.QuartzSpringBeanJobFactory" />     </property> </bean>  <bean id="scheduledJobRegistrar" class="com.foo.bar.scheduling.QuartzScheduledJobRegistrar">     <property name="scheduler" ref="quartzScheduler" />     <property name="jobListeners">         <map>             <entry value=""> <!-- empty string = match all jobs -->                 <key><bean class="com.foo.bar.scheduling.FailuresJobListener"/></key>             </entry>         </map>     </property> </bean> 
like image 198
Bozho Avatar answered Sep 28 '22 12:09

Bozho