Enterprise Java

Spring & Quartz Integration with Custom Annotation, the SPANN way

In a previous post, we demonstrated how to create and configure Quartz jobs with annotations in a Spring container. We used a class-level annotation to add some metadata to a bean which implements Quartz’s Job; the annotation defines the job’s name, group, and its cron-expression. Later, a big portion of the code is dedicated to handling that annotation: find the beans, read the annotation, create the JobDetail and the CronTrigger, apply their properties, and pass them over to the scheduler.

If you are working on an average to big-size spring project, you will probably soon enough start to see boilerplate configuration and code which can be often refactored by encapsulating it in annotations; the @QuartzJob annotation is a good example.

At masetta we tried to use the Polyforms project to use annotations for implementing DAO methods (which usually consist of some boilerplate code around a JPA Query). Soon enough we found it was not as configurable and extendable as we needed, had problems handling named query parameters and initialization-order problems (because how Polyforms uses aspects to to implement abstract methods). In addition, we used custom annotations and handled them “manually”, but they were getting too many…

What we came up with is spann. Spann is an open source extension for the spring framework, which allows advanced configuration of spring beans using annotations. To give a peek at one of spann’s features I will rely on our previous post and implement similar functionality. Instead of coding, I will use spann. As you will see, the implementation is very brief.

Overview

The code uses Spring’s native Quartz scheduling implementation (as explained in the spring reference). Spring’s MethodInvokingJobDetailFactoryBean is used to create a JobDetail bean that delegates the job execution to another bean’s method. As a Trigger I use spring’s implementation of CronTrigger.

To create and configure the JobDetail and the CronTrigger beans I will create method-level annotations using spann’s @BeanConfig annotation.

The code

The example-code can be checked out as a maven project from the spann trunk using

svn co http://spann.googlecode.com/svn/trunk/spann-quartz-example

It includes a pom with all needed dependency-coordinates and a functional test case.

1. Create an annotation to configure a MethodInvokingJobDetailFactoryBean

package com.masetta.spann.quartzexample.annotations;

import java.lang.annotation.*;
import org.springframework.scheduling.quartz.MethodInvokingJobDetailFactoryBean;
import com.masetta.spann.metadata.common.Artifact;
import com.masetta.spann.spring.base.beanconfig.*;
import com.masetta.spann.spring.base.beanconfig.factories.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@BeanConfig(
 create=MethodInvokingJobDetailFactoryBean.class,
 attached=@Attached(role="quartzJob",scope=Artifact.METHOD),
 explicit=true,
 wire={
  @WireMeta(property="targetObject",scope=Artifact.CLASS,factory=BeanReferenceFactory.class),
  @WireMeta(property="targetMethod",scope=Artifact.METHOD,factory=MetadataNameFactory.class)
 })
public @interface QuartzJob {
 
 String name();
 
 String group();
 
 boolean concurrent() default true;

}

The @BeanConfig annotation creates and configures a MethodInvokingJobDetailFactoryBean using the QuartzJob-annotation’s attributes (name, group and concurrent).

The configured bean is “attached” to the annotated method with the ‘quartzJob‘ role. This will be used later to inject the JobDetail bean to the trigger. “Attaching” is an internal spann concept. It allows referencing beans by specifying an artifact (e.g. a class or a method) and a semantic role (here ‘quartzJob’) instead of by name. This enables annotation composition, spann’s most powerful feature, which is also demonstrated here.

The wire attribute sets the targetObject and targetMethod properties with values populated from the current artifact’s Metadata (in this case MethodMetadata), ScanContext and Annotation using the given factories.

2. Create a cron trigger Annotation

package com.masetta.spann.quartzexample.annotations;

import java.lang.annotation.*;
import org.springframework.scheduling.quartz.CronTriggerBean;
import com.masetta.spann.metadata.common.Artifact;
import com.masetta.spann.spring.base.beanconfig.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@BeanConfig(
 create=CronTriggerBean.class,
 attached=@Attached(role="quartzTrigger",scope=Artifact.METHOD), 
 explicit=true,
 references=@SpannReference(property="jobDetail",role="quartzJob", scope=Artifact.METHOD)
)
public @interface Cron {
 
 String cronExpression();
 
 String timeZone() default "";
 
 String misfireInstructionName() default "";
 
 String[] triggerListenerNames() default {}; 
 
}

Again I use the @BeanConfig annotation, this time creating and configuring a CronTriggerBean.

The explicit attribute indicates how to handle default annotation-attribute values. When explicit is true, default attribute values are ignored. For example, the timeZone , misfireInstructionName and triggerListenerNames properties of the CronTriggerBean will only be set if the corresponding annotation-attribute value is set; the default value will be silently ignored.

Using the references attribute, the jobDetail property is set to the bean created in step 1: spann will look up the bean attached to the annotated method with the ‘quartzJob‘ role.

Note that the timeZone annotation-attribute type is String, while the type of CronTriggerBean‘s timeZone property is TimeZone. The value is handled natively by Spring, transparently converted to TimeZone using Spring’s PropertyEditor facility. You can even use Spring’s ${…} syntax for expression substitution.

The checked-in code contains a third annotation to create an interval trigger, used later in this example.

3. Configuring spann and spring’s SchedulerFactoryBean

Our applicationContext.xml is very brief:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:spann="http://os.masetta.com/spann/schema/spann-1.0"
    xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
http://os.masetta.com/spann/schema/spann-1.0 http://os.masetta.com/spann/schema/spann-1.0.xsd">

    <context:component-scan base-package="com.masetta.spann.quartzexample"/>
    
    <spann:scan base-package="com.masetta.spann.quartzexample"/>
    
    <bean class="org.springframework.scheduling.quartz.SchedulerFactoryBean" autowire=”byType”/>

</beans> 

If you know spring, there should not be any magic for you here: I configure a spring’s component scan, a spann scan and the SchedulerFactoryBean, as described in the spring reference, only here I let spring autowire all trigger beans to the corresponding property, hence autowire=’byType’.

4. Using the annotations

package com.masetta.spann.quartzexample.test;

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.stereotype.Component;

import com.masetta.spann.quartzexample.annotations.*;
import com.masetta.spann.spring.core.annotations.VisitMethods;

@Component
@VisitMethods
public class Savana {
 
 private AtomicInteger newElemphants = new AtomicInteger();
 
 private AtomicInteger newZebras = new AtomicInteger();
 
 @QuartzJob(name="zebraBorn",group="savana")
 @Interval(repeatInterval=200)
 public void zebraBorn() {
  newZebras.incrementAndGet();
 }

 @QuartzJob(name="elephantBorn",group="savana")
 @Cron(cronExpression="0/2 * * ? * * *")
 public void elephantBorn() {
  newElemphants.incrementAndGet();
 }
 
 public int getNewZebras() {
  return newZebras.get();
 }
 
 public int getNewElephants() {
  return newElemphants.get();
 }

}

The bean is configured via spring’s @Component annotation. Its a normal Spring bean, and any Spring- or aspect-annotation (@Autowired, @Resource, @Transactional) will be natively processed by Spring.

By default, spann processes only class level annotations; @VisitMethods instructs spann to also visit this class’ methods and process their annotations, if present.

The use of the new annotations is straight forward: each scheduled method should be annotated with both @QuartzJob (to create the delegating JobDetail) and either the @Cron or the @Interval annotation (not shown here but available in svn) to create the trigger.

This also demonstrates spann’s annotation composition, which allows annotations to be granular and plugable: @QuartzJob can be used with any annotation which configures a Trigger bean, while @Cron and @Interval can be used with any annotation which configures a JobDetail bean.

Summary

Spann is an open source extension for the Spring framework which allows advanced bean configuration using annotations. The code demonstrates the use of spann’s @BeanConfig annotation to create Quartz-scheduled jobs using annotations.

The example uses spann’s high-level API, namely the @BeanConfig annotation, implemented in the spann project itself. Spann’s high-level API includes other annotations that allow method-replacement (for implementing abstract methods on runtime, internally using cglib), synthetic-adapter creation and comprehensive JPA Query support.

Spann’s integration with spring is very tight: it creates “plain old spring beans”, just like the ones defined in XML or by the @Component annotation. This allows you to leverage all of spring’s bean-features: the beans can be retrieved via spring’s ApplicationContext, have the normal bean lifecycle, can be post-processed (e.g. for expression substitution), autowired, intercepted using aspects, managed via JMX and so on. You don’t need hacks and workarounds and don’t need to reimplement or copy and adjust existing spring-code. In addition, you have less boilerplate-code and less boilerplate-configuration.

As flexible as @BeanConfig and spann’s other annotations are, there are use-cases they do no cover. But spann’s low-level API allows creating new annotations from scratch, giving developers fine grained control over creation and configuration of bean definitions. You can even use spann to process any other class metadata by implementing your own MetadataVisitor, optionally ignoring annotations all together.

Reference: Spring & Quartz Integration with Custom Annotation, the SPANN way from our W4G partner Ron Piterman.

Related Articles :

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button