Java EE CDI Extension example
Introduction
Java EE CDI portable extensions is an API that provides the means for an application to listen for specific CDI container initialization events and act accordingly, like completely modify CDI beans metadata, override CDI beans creation, among many other features. It's a very powerful API that is mainly used by framework developers (ex: the Apache DeltaSpike project, formerly known as MyFaces CODI, is a CDI extension).
In this article we will implement a sample CDI extension from scratch, that will enable the following capabilities to our application:
We will be able to annotate CDI bean fields with a custom annotation - @Property - that will make the container call a property resolver method in order to resolve the field value during bean initialization.
@Property("property.one") private String text;
The property resolver will consist in a method annotated with @PropertyResolver that may be placed inside any CDI bean of our choice.
@PropertyResolver public String resolveProperty(String key) { // return the property accordingly }
Additionally we will also be able to pass an optional Locale parameter, that must be annotated with @PropertyLocale custom annotation.
@PropertyResolver public String resolveProperty(@PropertyLocale Locale locale, String key) { // return the property accordingly }
Finally, all the additional parameters in the property resolver method will be treated like injection points by the CDI container. This means that we may add any other parameters we desire to the property resolver method. The additional parameters will be injected by the CDI container.
@PropertyResolver public String resolveProperty(@PropertyLocale Locale locale, String key, BeanManager beanManager, OtherBean other) { // return the property accordingly }
This tutorial considers the following environment:
- Ubuntu 12.04
- JDK 1.7.0.21
- Glassfish 4.0
CDI extension configuration
We start by configuring our CDI extension. Any CDI extension must implement the javax.enterprise.inject.spi.Extension interface:
public class PropertyExtension implements Extension { }
Additionally one must provide a file named javax.enterprise.inject.spi.Extensioninside /META-INF/services folder. This file must contain our extension class fully qualified name:
com.byteslounge.property.extension.PropertyExtension
Preparing the required annotations
As we have mentioned earlier, we will use three annotations: @Property, @PropertyResolver, and @PropertyLocale. Their definition follows next:
package com.byteslounge.property.extension; import static java.lang.annotation.ElementType.FIELD; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(value = RetentionPolicy.RUNTIME) @Target({ FIELD }) public @interface Property { String value(); }
package com.byteslounge.property.extension; import static java.lang.annotation.ElementType.METHOD; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(value = RetentionPolicy.RUNTIME) @Target({ METHOD }) public @interface PropertyResolver { }
package com.byteslounge.property.extension; import static java.lang.annotation.ElementType.PARAMETER; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(value = RetentionPolicy.RUNTIME) @Target({ PARAMETER }) public @interface PropertyLocale { }
Each annotation has a specific target that defines where it may be used. The @Property annotation has a value() property that will hold the key to be resolved.
The CDI extension
Now the CDI extension itself:
package com.byteslounge.property.extension; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Member; import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.enterprise.context.spi.CreationalContext; import javax.enterprise.event.Observes; import javax.enterprise.inject.spi.AfterDeploymentValidation; import javax.enterprise.inject.spi.Annotated; import javax.enterprise.inject.spi.AnnotatedMethod; import javax.enterprise.inject.spi.AnnotatedParameter; import javax.enterprise.inject.spi.AnnotatedType; import javax.enterprise.inject.spi.Bean; import javax.enterprise.inject.spi.BeanManager; import javax.enterprise.inject.spi.Extension; import javax.enterprise.inject.spi.InjectionPoint; import javax.enterprise.inject.spi.InjectionTarget; import javax.enterprise.inject.spi.ProcessAnnotatedType; import javax.enterprise.inject.spi.ProcessInjectionTarget; import javax.faces.context.FacesContext; public class PropertyExtension<R> implements Extension { private PropertyResolverBean propertyResolverBean; void processAnnotatedType(@Observes ProcessAnnotatedType<R> pat, BeanManager beanManager) { AnnotatedType<R> at = pat.getAnnotatedType(); // Check if there is any method defined as Property Resolver for (AnnotatedMethod<? super R> method : at.getMethods()) { if (method.isAnnotationPresent(PropertyResolver.class)) { // Found Property Resolver method so let's create our // property resolver bean propertyResolverBean = new PropertyResolverBean(method, beanManager); // Break the loop. In this example we assume that the first // resolver method to be found is the one that will be used break; } } } void AfterDeploymentValidation( @Observes AfterDeploymentValidation adv) { // Create our Property Resolver bean instance. // Additionally initialize any eventual injectable parameter // that is passed into our Property Resolver method. propertyResolverBean.initializePropertyResolverBean(); } <X> void processInjectionTarget( @Observes ProcessInjectionTarget<X> pit) { final InjectionTarget<X> it = pit.getInjectionTarget(); final AnnotatedType<X> at = pit.getAnnotatedType(); // Here we wrap all available Injection Targets in a // custom wrapper that will add custom behavior to // inject() method InjectionTarget<X> wrapper = new InjectionTarget<X>() { @Override public X produce(CreationalContext<X> ctx) { return it.produce(ctx); } @Override public void dispose(X instance) { it.dispose(instance); } @Override public Set<InjectionPoint> getInjectionPoints() { return it.getInjectionPoints(); } // The container calls inject() method when it's performing field // injection and calling bean initializer methods. // Our custom wrapper will also check for fields annotated with // @Property and resolve them by invoking the Property Resolver // method @Override public void inject(X instance, CreationalContext<X> ctx) { it.inject(instance, ctx); for (Field field : at.getJavaClass().getDeclaredFields()) { Property annotation = field.getAnnotation(Property.class); if (annotation != null) { String key = annotation.value(); field.setAccessible(true); try { field.set(instance, propertyResolverBean .resolveProperty(key, ctx)); } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) { throw new RuntimeException( "Could not resolve property", e); } } } } @Override public void postConstruct(X instance) { it.postConstruct(instance); } @Override public void preDestroy(X instance) { it.preDestroy(instance); } }; pit.setInjectionTarget(wrapper); } public class PropertyResolverBean { private final AnnotatedMethod<? super R> propertyResolverMethod; private final BeanManager beanManager; private Object propertyResolverInstance; private List<InjectionPoint> propertyResolverParameters; private final boolean propertyLocalePresent; private PropertyResolverBean( AnnotatedMethod<? super R> propertyResolverMethod, BeanManager beanManager) { this.propertyResolverMethod = propertyResolverMethod; this.beanManager = beanManager; this.propertyLocalePresent = checkLocaleParameter(); } private void initializePropertyResolverBean() { // Get any existing eligible bean based on the type of the Property // Resolver method containing class. Set<Bean<?>> beans = beanManager.getBeans(propertyResolverMethod .getJavaMember().getDeclaringClass()); final Bean<?> propertyResolverBean = beanManager.resolve(beans); CreationalContext<?> creationalContext = beanManager .createCreationalContext(propertyResolverBean); // Create the Property Resolver bean instance propertyResolverInstance = beanManager.getReference( propertyResolverBean, propertyResolverMethod .getJavaMember().getDeclaringClass(), creationalContext); propertyResolverParameters = new ArrayList<>(); // We assume that the first parameter is the property to be resolved int startIndex = 1; if (propertyLocalePresent) { // If we have the additional locale property then the first // couple of parameters are the locale and the property key // (first is the locale; second is the property key) startIndex = 2; } // Create injection points for any additional Property Resolver // method parameters. They will be later injected by the container if (propertyResolverMethod.getParameters().size() > startIndex) { int currentIndex = 0; for (final AnnotatedParameter<? super R> parameter : propertyResolverMethod.getParameters()) { if (currentIndex++ < startIndex) { continue; } propertyResolverParameters.add(new InjectionPoint() { @Override public Type getType() { return parameter.getBaseType(); } @Override public Set<Annotation> getQualifiers() { Set<Annotation> qualifiers = new HashSet<Annotation>(); for (Annotation annotation : parameter .getAnnotations()) { if (beanManager.isQualifier(annotation .annotationType())) { qualifiers.add(annotation); } } return qualifiers; } @Override public Bean<?> getBean() { return propertyResolverBean; } @Override public Member getMember() { return parameter.getDeclaringCallable() .getJavaMember(); } @Override public Annotated getAnnotated() { return parameter; } @Override public boolean isDelegate() { return false; } @Override public boolean isTransient() { return false; } }); } } } public String resolveProperty(String key, CreationalContext<?> ctx) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { List<Object> parameters = new ArrayList<>(); // If the Locale property is present, it must be the first // parameter in the Property Resolver method if (propertyLocalePresent) { parameters.add(FacesContext.getCurrentInstance().getViewRoot() .getLocale()); } // The property key is the next parameter parameters.add(key); // Resolve any eventual additional parameter to be injected by the // CDI container for (InjectionPoint parameter : propertyResolverParameters) { parameters.add(beanManager.getInjectableReference(parameter, ctx)); } // Call the property resolver method return (String) propertyResolverMethod.getJavaMember().invoke( propertyResolverInstance, parameters.toArray()); } private boolean checkLocaleParameter() { for (Annotation[] annotations : propertyResolverMethod .getJavaMember().getParameterAnnotations()) { for (Annotation annotation : annotations) { if (annotation.annotationType() .equals(PropertyLocale.class)) { return true; } } } return false; } } }
We are listening for three CDI container initialization events.
During ProcessAnnotatedType event listening we search for the property resolver method and create our inner PropertyResolverBean instance accordingly.
Then in ProcessInjectionTarget event we wrap every injection target with a wrapper that will add a feature to the inject() method. The inject() method is called by the CDI container when it is injecting bean fields and calling initializer methods of a just created CDI bean instance.
We let the container initialize the bean as it would do by default by calling it.inject(instance, ctx) and then look for fields annotated with @Property annotation. If we find any field with this annotation we resolve the property and inject the value into the field.
Finally in AfterDeploymentValidation event we create our CDI property resolver bean instance, that is a property of the inner PropertyResolverBean class. Why only initialize the property resolver CDI bean during this event because we need the CDI BeanManager to be ready for creating CDI instances.
Testing
In order to test the extension we will create a property resolver inside an application scoped bean (note that the Locale and the extra parameters [BeanManager and OtherBean] are optional):
package com.byteslounge.property.resolver; import java.util.Locale; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.spi.BeanManager; import com.byteslounge.beans.OtherBean; import com.byteslounge.property.extension.PropertyLocale; import com.byteslounge.property.extension.PropertyResolver; @ApplicationScoped public class Properties { @PropertyResolver public String resolveProperty(@PropertyLocale Locale locale, String key, BeanManager beanManager, OtherBean other) { // We could use the Locale, or any other bean that is injected into the // method in order to help us finding the correct property value. // We may go for a regular property file, a database, or any other // existent property container. // In this example we have hard coded the returned values. if (key.equals("property.one")) { return "ONE"; } else if (key.equals("property.two")) { return "TWO " + other.getOtherBeanText(); } return "NONE " + other.getOtherBeanText(); } }
And a couple of CDI managed beans:
package com.byteslounge.beans; import javax.enterprise.context.RequestScoped; import javax.inject.Named; import com.byteslounge.property.extension.Property; @Named @RequestScoped public class TestBean { @Property("property.one") private String text; public String getText() { return text; } }
package com.byteslounge.beans; import java.io.Serializable; import javax.annotation.PostConstruct; import javax.enterprise.context.SessionScoped; @SessionScoped public class OtherBean implements Serializable { private static final long serialVersionUID = 1L; private String otherBeanText; @PostConstruct private void init() { otherBeanText = "other text"; } public String getOtherBeanText() { return otherBeanText; } }
Finally we may access the TestBean managed bean in a JSF view and observe the results:
<!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html"> <h:head> <title>CDI Extension example</title> </h:head> <h:body> <h:outputText value="#{testBean.text}" /> </h:body> </html>
The following output will be generated:
The article source code is available for download at the end of this page.