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.