Java EE CDI Extension example

26 April 2014
By Gonçalo Marques
In this article we will see how to implement a Java EE CDI portable extension from scratch. CDI extensions are used mainly by framework developers (ex: Apache DeltaSpike project, the former MyFaces CODI).

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
}

Note: In this article we consider that the key parameter must be the first one in the property resolver method. If the Locale parameter is present, the Locale parameter must in turn be the first parameter present in the resolver method, while the key parameter becomes the second.

This tutorial considers the following environment:

  1. Ubuntu 12.04
  2. JDK 1.7.0.21
  3. 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:

/META-INF/services/javax.enterprise.inject.spi.Extension

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:

Property.java

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();

}

PropertyResolver.java

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 {

}

PropertyLocale.java

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:

Note: The CDI extension itself could have been obviously divided in modules, but I kept it in a single Java class for clarity in this article.

PropertyExtension.java

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.

Note: The code has comments in the relevant sections in order to help you with the understanding. This time I preferred to do it this way instead of writing a huge section of explanatory text that would make this blog post hard to read and follow. If you have any questions regarding the extension you may leave your comments below in the comments section (or alternatively drop me an email, but I would prefer the comments section because other readers may have the same doubts).

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):

Properties.java

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:

TestBean.java

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;
  }

}

OtherBean.java

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:

home.xhtml

<!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:

Resulting output
Result of the CDI extension

The article source code is available for download at the end of this page.

Reference

Weld Documentation

Download source code from this article

Related Articles

Comments

About the author
Gonçalo Marques is a Software Engineer with several years of experience in software development and architecture definition. During this period his main focus was delivering software solutions in banking, telecommunications and governmental areas. He created the Bytes Lounge website with one ultimate goal: share his knowledge with the software development community. His main area of expertise is Java and open source.

GitHub profile: https://github.com/gonmarques

He is also the author of the WiFi File Browser Android application: