One of the nicest features of CDI is the portable extension SPI. According to the spec:
A portable extension may integrate with the container by:
- Providing its own beans, interceptors and decorators to the container
- Injecting dependencies into its own objects using the dependency injection service
- Providing a context implementation for a custom scope
- Augmenting or overriding the annotation-based metadata with metadata from some other source
We've got plenty of examples of CDI application code floating about, but not much in the way of examples for framework developers who want to integrate with CDI, or augment the built-in functionality. So here's a little taste of the what a CDI portable extension looks like.
Let's start with an example of an extension that provides support for the use of @Named at the package level. The package-level name is used to qualify the EL names of all beans defined in that package.
Portable extensions implement the marker interface Extension and observe container lifecycle events, in this case ProcessAnnotatedType, an event that is fired by the container when it discovers a class or interface in a bean archive. The portable extension wraps the AnnotatedType object and overrides the value() of the @Named annotation of the bean.
public class QualifiedNameExtension implements Extension {
<X> void processAnnotatedType(@Observes ProcessAnnotatedType<X> pat) {
//wrap this to override the annotations of the class
final AnnotatedType<X> at = pat.getAnnotatedType();
AnnotatedType<X> wrapped = new AnnotatedType<X>() {
@Override
public Set<AnnotatedConstructor<X>> getConstructors() {
return at.getConstructors();
}
@Override
public Set<AnnotatedField<? super X>> getFields() {
return at.getFields();
}
@Override
public Class<X> getJavaClass() {
return at.getJavaClass();
}
@Override
public Set<AnnotatedMethod<? super X>> getMethods() {
return at.getMethods();
}
@Override
public <T extends Annotation> T getAnnotation(final Class<T> annType) {
if ( Named.class.equals(annType) ) {
class NamedLiteral
extends AnnotationLiteral<Named>
implements Named {
@Override
public String value() {
Package pkg = at.getClass().getPackage();
String unqualifiedName = at.getAnnotation(Named.class).value();
final String qualifiedName;
if ( pkg.isAnnotationPresent(Named.class) ) {
qualifiedName = pkg.getAnnotation(Named.class).value()
+ '.' + unqualifiedName;
}
else {
qualifiedName = unqualifiedName;
}
return qualifiedName;
}
}
return (T) new NamedLiteral();
}
else {
return at.getAnnotation(annType);
}
}
@Override
public Set<Annotation> getAnnotations() {
return at.getAnnotations();
}
@Override
public Type getBaseType() {
return at.getBaseType();
}
@Override
public Set<Type> getTypeClosure() {
return at.getTypeClosure();
}
@Override
public boolean isAnnotationPresent(Class<? extends Annotation> annType) {
return at.isAnnotationPresent(annType);
}
};
pat.setAnnotatedType(wrapped);
}
}
We need to deploy this portable extension in a jar containing a file named javax.enterprise.inject.spi.Extension in the META-INF/services directory. This file should contain the full name of our QualifiedNameExtension class.
Here's a second example, of a portable extension that creates Bean objects for each class in a set of classes and registers the beans with the container. This is similar to what the container does for classes it discovers in bean archives.
public class RegisterExtension implements Extension {
List<Class<?>> getBeanClasses() {
//get some classes from somewhere
}
void afterBeanDiscovery(@Observes AfterBeanDiscovery abd, BeanManager bm) {
for ( final Class c: getBeanClasses() ) {
//use this to read annotations of the class
AnnotatedType at = bm.createAnnotatedType(c);
//use this to create the class and inject dependencies
final InjectionTarget it = bm.createInjectionTarget(at);
abd.addBean( new Bean() {
@Override
public Class<?> getBeanClass() {
return c;
}
@Override
public Set<InjectionPoint> getInjectionPoints() {
return it.getInjectionPoints();
}
@Override
public String getName() {
return null;
}
@Override
public Set<Annotation> getQualifiers() {
Set<Annotation> qualifiers = new HashSet<Annotation>();
qualifiers.add( new AnnotationLiteral<Default>() {} );
qualifiers.add( new AnnotationLiteral<Any>() {} );
return qualifiers;
}
@Override
public Class<? extends Annotation> getScope() {
return Dependent.class;
}
@Override
public Set<Class<? extends Annotation>> getStereotypes() {
return Collections.emptySet();
}
@Override
public Set<Type> getTypes() {
Set<Type> types = new HashSet<Type>();
types.add(c);
types.add(Object.class);
return types;
}
@Override
public boolean isAlternative() {
return false;
}
@Override
public boolean isNullable() {
return false;
}
@Override
public Object create(CreationalContext ctx) {
Object instance = it.produce(ctx);
it.inject(instance, ctx);
it.postConstruct(instance);
return instance;
}
@Override
public void destroy(Object instance, CreationalContext ctx) {
it.preDestroy(instance);
it.dispose(instance);
ctx.release();
}
} );
}
}
}
UPDATE: I've finally got a proper chapter on portable extensions written up in the Weld reference documentation. Check out chapter 16 of this version if this post piqued your interest.
Gavin, wouldn't
qualifiedName = unqualifiedName + '.' + pkg.getAnnotation(Named.class).value();
make more sense reversed so you have package.unqualifiedName?
Does anything need to go into the javax.enterprise.inject.spi.Extension file or is it just a marker?
Yes, thanks, fixed.
It needs to contain the fully-qualified class name of our extension. i.e. org.jboss.extensions.QualifiedNameExtension or whatever.
Speaking of the CDI ecosystem, do you think it would be a good idea to host (here?) some sort of where people could post their extensions?
Yes, this was the idea behind the Seam sandbox, a place where people could put there extensions. If we like them, then we can add them to the main Seam distro :-)
We've still got a way to go with infrastructure to support this nicely tho...
Hi,
Can i have an abstract Extension? If not, why?
Thanks.
No, because we need to be able to instantiate the class in order to be able to call methods on it.
Hi!
I want to create an Extension that reads some annotation data in the ProcessObserverMethod and later use the data to start another process inside a Bean using injection. I tried building an extension that gets the values. That´s ok. But when I try to inject the data later in the service bean, the data is equals to null.
Something like: http://snipt.org/xmplj
Is there a way to do this or I'm forgetting something?