22

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?

skaffman
  • 398,947
  • 96
  • 818
  • 769
Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • None i've come across. My guess is that most people don't see the need of stacking them on top of eachother. After all, the Spring task scheduler does, in all essentials, the same things as Quartz so what would be the benefit of delegating? – pap Jul 22 '11 at 14:16
  • 1
    afaik, quartz has more features. – Bozho Jul 22 '11 at 14:23

2 Answers2

24

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>
Bozho
  • 588,226
  • 146
  • 1,060
  • 1,140
  • Thank you very much for posting your result. Seems a little complex, so it would certainly be a great to include it in future Spring releases. – Guido Jan 24 '12 at 17:37
  • By the way, do you know if your code could work with quartz using a database to persist the jobs? I've found your answer from this question I am trying to solve http://stackoverflow.com/questions/8991244 – Guido Jan 24 '12 at 17:39
  • 1
    it should, with some modifications of course. I don't know what should they be, though. – Bozho Jan 24 '12 at 17:43
1

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:

<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.

Tomasz Nurkiewicz
  • 334,321
  • 69
  • 703
  • 674