TaskScheduler, @Scheduled and quartz
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")@Overridepublic 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;@Overrideprotected 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);}@Overridepublic 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>
Seems like there is no ready implementation. However, wiring-up your own shouldn't be very difficult:
@Servicepublic class QuartzTaskScheduler implements TaskScheduler { //...}
And making Spring to use it:
<task:annotation-driven/><bean class="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor"> <property name="scheduler" ref="quartzTaskScheduler"/></bean>
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.