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?
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 { private Scheduler scheduler; private StringValueResolver embeddedValueResolver; private Map 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 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 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 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:
Seems like there is no ready implementation. However, wiring-up your own shouldn't be very difficult:
@Service public class QuartzTaskScheduler implements TaskScheduler { //... }
And making Spring to use it:
If you go this path, consider contributing your code to Spring framework (org.springframework.scheduling.quartz
package) or at least opening an issue for that.