TaskScheduler, @Scheduled and quartz TaskScheduler, @Scheduled and quartz spring spring

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.