Going further with CDI 1.2

Antoine Sabot-Durand

  • Senior Software Engineer
  • CDI co-spec lead, Java EE 8 EG
  • Red Hat, Inc.
  • @antoine_sd
  • www.next-presso.com
  • github.com/antoinesd

Antonin Stefanutti

  • Software Engineer
  • Murex
  • www.murex.com
  • @astefanut
  • github.com/astefanutti

CDI @ Murex

murex
CDI as the productivity ecosystem to build connectivity interfaces

Should I stay or should I go?

  • A talk about advanced CDI
  • Might be hard for beginners
  • Don’t need to be a CDI guru

Should I stay or should I go ?

If you know the most of these you can stay

More concretely

What’s included:
  1. Real use cases from real life with real users
  2. new approach to introduce portable extension concepts
  3. Code in IDE with tests
What’s not included:
  1. Introduction to CDI
  2. Old content on extension
  3. Work with Context (need 2 more hours)

Tools used in the code 1/2

Apache Deltaspike

deltaspike
  1. Apache DeltaSpike is a great CDI toolbox
  2. Provide helpers to develop extension
  3. And a collection of modules like:
    1. Security
    2. Data
    3. Scheduler
  4. More info on deltaspike.apache.org

Tools used in the code 2/2

Arquillian

arquillian
  1. Arquillian is an integration test paltform
  2. It integrates with Junit
  3. Create your deployment in a dedicated method
  4. And launch your tests against the container of your choice
  5. We’ll use the weld-ee-embedded container
  6. The right solution to test Java EE code
  7. More info on arquillian.org

Agenda

  • Meet CDI SPI
  • Introducing CDI Extensions
  • Legacy Code
  • Metrics CDI
  • Camel CDI

Meet CDI SPI

3d-chess ==!

SPI can be split in 4 parts

Type meta-model
CDI meta-model
CDI entry points
SPI dedicated to extensions

Why Having a Type Meta-Model?

Because @Annotations are configuration
but they are also read-only
So to configure we need a mutable meta-model…​
…​for annotated types

SPI for Type meta-model

type meta

SPI Dedicated to CDI Meta-Model

cdi meta

This SPI can be used in your code 1

InjectionPoint can be used to get info about what’s being injected
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface HttpParam {
  @Nonbinding public String value()
}
...
@Produces @HttpParam("")
String getParamValue(InjectionPoint ip,HttpServletRequest req) {
  return req.getParameter(ip.getAnnotated().getAnnotation(HttpParam.class).value())
}
...
@HttpParam("productId") @Inject String productId

This SPI can be used in your code 2

InjectionPoint contains info about requested type at @Inject
public class MyMapProducer() {
  @Produces
  public <K, V> Map<K, V> produceMap(InjectionPoint ip) {
       if (valueIsNumber(((ParameterizedType)ip.getType())))
           return new TreeMap<K, V>()
       return new HashMap<K, V>()
  }

  private boolean valueIsNumber(ParameterizedType t) {
        Class<?> valueClass = (Class<?>) t.getActualTypeArguments()[1]
        return Number.class.isAssignableFrom(valueClass)
  }
}

This SPI can be used in your code 3

An abstract superclass can use meta to provide generic resolution
public abstract class AbstractService {
  //resolver with same qualifiers than the current bean
  private Resolver resolver;

  @Inject //@Inject can be used on an initializer method
  void initResolver(Bean<AbstractService> meta, @Any Instance<Resolver> resolvers)
 {
    Annotation[] beanQualifiers = (Annotation[]) meta.getQualifiers().toArray()
    resolver = resolvers.select(beanQualifiers).get()
  }

public Resolver getResolver() {
    return resolver
  }
}

This SPI can be used in your code 4

EventMetadata contains type and qualifier info about the event
@ApplicationScoped
public class MyService {

  private void strictListen(@Observes @Qualified Payload evt, EventMetadata meta) {
    if(meta.getQualifiers().contains(new QualifiedLiteral())
       && meta.getType().equals(Payload.class))
         System.out.println("Do something")
       else
         System.out.println("ignore")
  }
}

SPI providing CDI Entry points

entry points

SPI Dedicated to Extensions

spi extensions

All these SPI interaces are events containing meta-model SPI

These events fired at boot time can only be observed in a CDI extension
For instance:

processAnnotatedTypeCart

A ProcessAnnotatedType<T> event is triggered for each type discovered at boot time
Observing ProcessAnnotatedType<Foo> allow you to prevent Foo to become a bean by calling ProcessAnnotatedType#veto()

Introducing CDI Portable Extensions

Portable Extensions

powerful
One of the most powerful feature of the CDI specification
Not really popularized, partly due to:
  1. Their high level of abstraction
  2. The good knowledge on Basic CDI and SPI
  3. Lack of information (CDI is often reduced to a basic DI solution)

Extensions What for?

To integrate 3rd party libraries, frameworks or legacy components
To change exisiting configuration or behavior
To extend CDI and Java EE
thanks to them, Java EE can evolve between major release

Extensions How?

rubik
Observing SPI Events at boot time according to BeanManger Lifecycle
Checking what meta-data are being created
Modifying these meta-data or creating new ones

More concretely

Service provider of the service javax.enterprise.inject.spi.Extension declared in META-INF/services
Just put the fully qualified name of your extension class in this file
import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.Extension;

class CdiExtension implements Extension {

    void beforeBeanDiscovery(@Observes BeforeBeanDiscovery bbd) {
    }
    ...

    void afterDeploymentValidation(@Observes AfterDeploymentValidation adv) {
    }
}

Bean Manager Lifecycle

lifecycle simple lifecycle legend

Example: Ignoring JPA Entities

The following extension prevents CDI to manage entities
This is a commonly admitted good practice
public class VetoEntity implements Extension {

    void vetoEntity(@Observes @WithAnnotations(Entity.class)
                    ProcessAnnotatedType<?> pat) {
        pat.veto();
    }
}

Remember

Extensions are launched during
bootstrap
and are based on CDI events

Once the application is bootstrapped,
the Bean Manager is in read-only mode (no runtime bean registration)

You only have to @Observes built-in CDI events to create your extensions

Legacy Code

How to modernize a legacy component with the CDI Programming Model

A legacy component

Transformation engine used at Murex to transform message formats from external systems into Murex canonical format
legacy

Legacy component out of the box (NO CDI)

Module containing the transformation logic fpml2mxml.jar that we want to reuse as is
@Formula
class Fpml2mxmlTransformation {
    public MxML transformation(FpML fpml) { /* transformation logic */ }
}
The tranformation engine that uses that module
class FpmlToMxmlTransformationMain throws Exception {
    public static void main(String[] args) {
        FpML inputFpml = (FpML) JAXBContext.newInstance(FpML.class)
            .createUnmarshaller().unmarshal(new File("fpml.xml"));
        MultiReader reader = new MultiReader(Collections.emptyList(),
            Arrays.asList("fpml2mxml.jar"));
        Executor executor = new Executor(reader, null, "", "", "",
            FpmlToMxmlTransformationMain.class.getClassLoader(), true);
        SDDParameterProvider parameters = new SDDParameterProvider(new Properties());
        parameters.setObject(inputFpml);
        DataDictionaryFormulaResult result = executor.executeFormula("Fpml2mxmlTransformation", parameters);
        Mxml outputMxml = (Mxml) result.getObjectValue(0, 0);
        executor.cleanUp();
    }
}

Modernize the legacy component API with CDI

We want the end-user to write that code instead to achieve the same result:
@Inject
@Formula(name = "Fpml2mxmlTransformation", path = "fpml2mxml.jar")
Transformer<FpML, MxML> transformer;

FpML input = (FpML) JAXBContext.newInstance(FpML.class)
                               .createUnmarshaller()
                               .unmarshal(new File("fpml.xml"));

Mxml result = transformer.transform(input);

Our goals

Define a facade for the legacy component that’s functional and typesafe

Hide the technical code that manages the configuration and the lifecycle of the legacy component

Without touching the component source / binary because we don’t own it

How to achieve this?

We need to write an extension that will:
  1. Collect the configuration metadata, i.e. the @Formula(…​), required to instantiate the legacy component by observing the ProcessInjectionPoint lifecycle event
  2. Declare the legacy component as a CDI bean programmatically by implementing the Bean SPI and observing the AfterBeanDiscovery lifecycle event
  3. Declare a producer method that will produce the Transformer facade instances and use the legacy component CDI bean

So we will @Observes these 2 events to add our features

lifecycle PIP ABD lifecycle legend light

First goal: Define a facade for the legacy component

Define a functional interface:
public interface Transformer<I, O> {
    O transform(I input);
}
And a CDI qualifier with configuration metadata:
import javax.enterprise.util.Nonbinding;
import javax.inject.Qualifier;
@Qualifier
public @interface Formula {
    @Nonbinding
    String name() default ""; // The tranformation name
    @Nonbinding
    String path() default ""; // The tranformation module
}

Configuration at injection points

Distribute configuration closer to the code:
@Inject
@Formula(name = "Fpml2mxmlTransformation", path = "fpml2mxml.jar")
Transformer<FpML, MxML> fpmlToMxmlTransformer;

...

@Inject
@Formula(name = "Fixml2mxmlTransformation", path = "fixml2mxml.jar")
Transformer<FixML, MxML> fixmlToMxmlTransformer;

...

Second goal: Hide the technical code

We first need to collect all the @Formula pathes metadata required to instantiate the legacy component to get rid of the following code
MultiReader reader = new MultiReader(Collections.emptyList(), Arrays.asList("fpml2mxml.jar", "fixml2mxml.jar"));
By observing the ProcessInjectionPoint lifecycle event
public interface ProcessInjectionPoint<T, X> {
    public InjectionPoint getInjectionPoint();
    public void setInjectionPoint(InjectionPoint injectionPoint);
    public void addDefinitionError(Throwable t);
}

Collect the legacy component configuration

In a CDI extension
class LegacyComponentExtension implements Extension {

  final List<String> pathes = new ArrayList<>();

  void collectPathes(@Observes ProcessInjectionPoint<?, Transformer> pit) { (1)
    Annotated annotated = pit.getInjectionPoint().getAnnotated();
    if (annotated.isAnnotationPresent(Formula.class))
        pathes.add(annotated.getAnnotation(Formula.class).path()); (2)
  }
}
1Observe every injection point of type Transformer on any declaring bean
2Collect the injection point @Formula path attribute

Declaring the legacy component as a CDI bean programmatically

Why do we need to add a bean programmatically?

The legacy component is not a bean archive:

  1. It does not contain a beans.xml (explicit bean archive)
  2. Nor it contains one or more bean classes with a bean defining annotation (implicit bean archive)

We cannot touch the component source / binary because we don’t own it

The Bean SPI

So how to declare a bean programmatically?

We need to implement the Bean SPI
public interface Bean<T> extends Contextual<T>, BeanAttributes<T> {
    public Class<?> getBeanClass();
    public Set<InjectionPoint> getInjectionPoints();
    public T create(CreationalContext<T> creationalContext); // Contextual<T>
    public void destroy(T instance, CreationalContext<T> creationalContext);
    public Set<Type> getTypes(); // BeanAttributes<T>
    public Set<Annotation> getQualifiers();
    public Class<? extends Annotation> getScope();
    public String getName();
    public Set<Class<? extends Annotation>> getStereotypes();
    public boolean isAlternative();
}

Implementing the Bean SPI

class LegacyExecutorBean implements Bean<Executor> {
    private final Set<Annotation> qualifiers = new HashSet<>(Arrays.asList(new AnnotationLiteral<Any>(){},
                                                                       new AnnotationLiteral<Default>(){}));
    List<String> pathes = new ArrayList<>();
    LegacyExecutorBean(List<String> pathes) {
        this.pathes = pathes;
    }
    public Class<? extends Annotation> getScope() {
        return ApplicationScoped.class;
    }
    public Set<Annotation> getQualifiers() {
        return Collections.unmodifiableSet(qualifiers);
    }
    public Set<Type> getTypes() {
        return Collections.singleton(Executor.class);
    }
    public Executor create(CreationalContext<Executor> creational) {
        MultiReader reader = new MultiReader(Collections.emptyList(), pathes);
        return new Executor(reader, null, "", "", "", Executor.class.getClassLoader(), true);
    }
    public void destroy(Executor instance, CreationalContext<Executor> creational) {
        executor.cleanUp();
    }
    ...

Implementing the Bean SPI (continued)

    ...
    public Class<?> getBeanClass() {
        return Executor.class;
    }
    public Set<InjectionPoint> getInjectionPoints() {
        return Collections.emptySet();
    }
    public String getName() { // Only called for @Named bean
        return "";
    }
    public Set<Class<? extends Annotation>> getStereotypes() {
        return Collections.emptySet();
    }
    public boolean isAlternative() {
        return false;
    }
    public boolean isNullable() { // Deprecated since CDI 1.1
        return false;
    }
}

Adding a programmatic bean to the deployment

Then, how to add a bean declared programmatically?

By observing the AfterBeanDiscovery lifecycle event
public interface AfterBeanDiscovery {
    public void addDefinitionError(Throwable t);
    public void addBean(Bean<?> bean);
    public void addObserverMethod(ObserverMethod<?> observerMethod);
    public void addContext(Context context);
    public <T> AnnotatedType<T> getAnnotatedType(Class<T> type, String id);
    public <T> Iterable<AnnotatedType<T>> getAnnotatedTypes(Class<T> type);
}

Adding the legacy component bean

In the CDI extension…​
class LegacyComponentExtension implements Extension {

    final List<String> pathes = new ArrayList<>();

    void collectPathes(@Observes ProcessInjectionPoint<?, Transformer> pit) {
        Annotated annotated = pit.getInjectionPoint().getAnnotated();
        if (annotated.isAnnotationPresent(Formula.class))
            pathes.add(annotated.getAnnotation(Formula.class).path());
    }

    void addLegacyComponentBean(@Observes AfterBeanDiscovery abd) {
        abd.addBean(new LegacyExecutorBean(pathes));
    }
}

Declare the Transformer facade

Finally we declare a producer method for the Transformer facade that uses the InjectionPoint API
@Produces @Formula
<I, O> Transformer<I, O> produceTransformer(Executor executor, InjectionPoint ip) {
    final String formula = ip.getAnnotated().getAnnotation(Formula.class).name();
    return new Transformer<I, O>() {
        public O transform(I input) {
            SDDParameterProvider parameters = new SDDParameterProvider(new Properties());
            parameters.setObject(input);
            DataDictionaryFormulaResult result = executor.executeFormula(formula, parameters);
            return (O) result.getObjectValue(0, 0);
        }
    };
}
Parameterized types are not erased by CDI and accessible from InjectionPoint.getType(), so that they can be used as metadata as well. For example for type check / conversion.

Second goal achieved

We can get rid of the following technical code
FpML inputFpml = (FpML) JAXBContext.newInstance(FpML.class)
                 .createUnmarshaller().unmarshal(new File("fpml.xml"));
MultiReader reader = new MultiReader(Collections.emptyList(), Arrays.asList("fpml2mxml.jar"));
Executor executor = new Executor(reader, null, "", "", "", Executor.class.getClassLoader(), true);
SDDParameterProvider parameters = new SDDParameterProvider(new Properties());
parameters.setObject(inputFpml);
DataDictionaryFormulaResult result = executor.executeFormula("Fpml2mxmlTransformation", parameters);
Mxml outputMxml = (Mxml) result.getObjectValue(0, 0);
executor.cleanUp();
}
And write instead:
@Inject
@Formula(name = "Fpml2mxmlTransformation", path = "fpml2mxml.jar")
Transformer<FpML, MxML> transformer;
Mxml result = transformer.transform(inputFpml);

3rd party Library

How to integrate a 3rd party Library (Dropwizard Metrics) into the CDI Programming Model

About Dropwizard Metrics

Provides different metric types: Counter, Gauge, Meter, Timer, …​
Provides different reporter: JMX, console, SLF4J, CSV, servlet, …​
Provides a MetricRegistry which collects all your app metrics
Provides annotations for AOP frameworks: @Counted, @Timed, …​
…​ but does not include integration with these frameworks

Discover how we created CDI integration module for Metrics

Metrics out of the box (NO CDI)

class MetricsHelper {
    public static MetricRegistry registry = new MetricRegistry();
}
class TimedMethodClass {
    void timedMethod() {
        Timer timer = MetricsHelper.registry.timer("timer"); (1)
        Timer.Context time = timer.time();
        try { ... }
        finally { time.stop();}
    }
}
1Note that if "timer" Timer doesn’t exist, MetricRegistry will create a default one and register it

Basic CDI integration…​

class MetricRegistryBean {
    @Produces @ApplicationScoped
    MetricRegistry registry = new MetricRegistry();
}
...
class TimedMethodBean {
    @Inject MetricRegistry registry;
    void timedMethod() {
        Timer timer = registry.timer("timer");
        Timer.Context time = timer.time();
        try { ... } finally { time.stop();}
    }
}
We could have a lot more with advanced CDI features

Our goals

  1. Apply a metric with the provided annotation in AOP style
    @Timed("timer") (1)
    void timedMethod() {
      ...
    }
  2. Register automatically produced custom metrics
    @Produces @Metric(name="myTimer") (1)
    Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, TimeUnit.MINUTES));
    ...
    @Timer("myTimer") (1)
    void timedMethod() { ... }
    1Annotations provided by Metrics

Steps to apply a timer in AOP style

Create an interceptor for the timer technical code
Make the Metrics annotation @Timed a valid interceptor binding annotation
Programmatically add @Timed as an interceptor binding
Use the Magic

Preparing interceptor creation

To create an interceptor we should start by detecting the "technical code" that will wrap the "business code"
class TimedMethodBean {
    @Inject MetricRegistry registry;
    void timedMethod() {
        Timer timer = registry.timer("timer");
        Timer.Context time = timer.time();
        try { //business code }
        finally { time.stop(); }
    }
}

Creating the interceptor

Interceptor is an independent specification (JSR 318).
Highlighted code below is part of it.
@Interceptor
class TimedInterceptor {
    @Inject MetricRegistry registry; (1)
    @AroundInvoke
    Object timeMethod(InvocationContext context) throws Exception {
        String name = context.getMethod().getAnnotation(Timed.class).name();
        Timer timer = registry.timer(name);
        Timer.Context time = timer.time();
        try { return context.proceed(); } (2)
        finally { time.stop(); }
    }
}
1In CDI an interceptor is a bean: you can inject other beans in it
2Here the "business" of the application is called. All the code around is the technical one

Activating the interceptor

@Interceptor
@Priority(Interceptor.Priority.LIBRARY_BEFORE)  (1)
class TimedInterceptor {
    @Inject MetricRegistry registry;
    @AroundInvoke
    Object timeMethod(InvocationContext context) throws Exception {
        String name = context.getMethod().getAnnotation(Timed.class).name();
        Timer timer = registry.timer(name);
        Timer.Context time = timer.time();
        try { return context.proceed(); }
        finally { time.stop(); }
    }
}
1Giving a @Priority to an interceptor activates it. This annotation is part of the "Common Annotation" specification (JSR 250). In CDI, interceptor activation can also be done in beans.xml file.

Add a binding to the interceptor

@Interceptor
@Timed  (1)
@Priority(Interceptor.Priority.LIBRARY_BEFORE)
class TimedInterceptor {
    @Inject MetricRegistry registry;
    @AroundInvoke
    Object timeMethod(InvocationContext context) throws Exception {
        String name = context.getMethod().getAnnotation(Timed.class).name();
        Timer timer = registry.timer(name);
        Timer.Context time = timer.time();
        try { return context.proceed(); }
        finally { time.stop(); }
    }
}
1We’ll use metrics @Timed annotation as interceptor binding

Back on interceptor binding

An interceptor binding is an annotation used in 2 kind of places:
  1. On the interceptor definitions to associates them to this annotation
  2. On the Methods / Classes we want to be intercepted by this interceptor
An interceptor binding should be annotated with the @InterceptorBinding meta annotation or should be declared as an interceptor binding programmatically
If the interceptor binding annotation has members:
  1. Their values are taken into account to distinguish two instances
  2. Unless members are annotated with @NonBinding

@Timed source code tells us it’s not an interceptor binding

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
(1)
public @interface Timed {
    String name() default ""; (2)
    boolean absolute() default false; (2)
}
1Lack of @InterceptorBinding annotation and we have no code to add it programmatically.
2None of the members have the @NonBinding annotation so they’ll be used to distinguish two instances (i.e. @Timed(name="timer1") and @Timed(name="timer2") will be 2 different interceptor bindings)

The needed @Timed to make it an interceptor binding

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.ANNOTATION_TYPE })
@InterceptorBinding
public @interface Timed {
    @NonBinding String name() default "";
    @NonBinding boolean absolute() default false;
}

How to obtain the required @Timed?

Remember the AnnotatedType SPI?

Thanks to DeltaSpike we can easily create the required AnnotatedType
AnnotatedType createTimedAnnotatedType() throws Exception {
    Annotation nonBinding = new AnnotationLiteral<Nonbinding>() {}; (1)
    return new AnnotatedTypeBuilder().readFromType(Timed.class) (2)
        .addToMethod(Timed.class.getMethod("name"), nonBinding) (3)
        .addToMethod(Timed.class.getMethod("absolute"), nonBinding) (3)
        .create();
}
1This creates an instance of @NonBinding annotation
2It would have been possible but far more verbose to create this AnnotatedType without the help of DeltaSpike. The AnnotatedTypeBuilder is initialized with Metrics Timed annotation.
3@NonBinding is added to both members of @Timed annotation

Add @Timed to the list of interceptor binding with an extension

By observing BeforeBeanDiscovery lifecycle event
public interface BeforeBeanDiscovery {
  addQualifier(Class<? extends Annotation> qual);
  addQualifier(AnnotatedType<? extends Annotation> qual);
  addScope(Class<? extends Annotation> scp, boolean nm, boolean psv);
  addStereotype(Class<? extends Annotation> strt, Annotation... sd);
  addInterceptorBinding(AnnotatedType<? extends Annotation> type);  (1)
  addInterceptorBinding(Class<? extends Annotation> type, Annotation... btd);
  addAnnotatedType(AnnotatedType<?> type);
  addAnnotatedType(AnnotatedType<?> type, String id);
}
1This method is the one we need to use for our @Timed AnnotatedType

BeforeBeanDiscovery is first in lifecycle

lifecycle BBD lifecycle legend light

This extension will do the job

class MetricsExtension implements Extension {

    void addTimedBinding(@Observes BeforeBeanDiscovery bbd) throws Exception {
        bbd.addInterceptorBinding(createTimedAnnotatedType());
    }

    private AnnotatedType createTimedAnnotatedType() throws Exception {
        Annotation nonBinding = new AnnotationLiteral<Nonbinding>() {};
        return new AnnotatedTypeBuilder().readFromType(Timed.class)
            .addToMethod(Timed.class.getMethod("name"), nonBinding)
            .addToMethod(Timed.class.getMethod("absolute"), nonBinding)
            .create();
    }
}

First goal achieved

We can now write:
@Timed("timer")
void timedMethod(){
  //Business code
}

And have a Metrics Timer applied to the method

Second goal: Register automatically produced custom metrics

Why would we want custom metrics?

@AroundInvoke
Object timedMethod(InvocationContext context) throws Exception {
    String name = context.getMethod().getAnnotation(Timed.class).name();
    Timer timer = registry.timer(name); (1)
    Timer.Context time = timer.time();
    try {
        return context.proceed();
    } finally { time.stop();}
}
1The registry provide a Default Timer (if none was registered by the user). The default timer histogram is exponentially biased to the past 5 minutes of measurements. We may want to have an other behaviour.

Register automatically produced custom metrics

We want to write this:
@Produces @Metric(name="myTimer")
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, TimeUnit.MINUTES));
And have:
  1. The possibility to retrieved this metric from the registry when it’s injected (instead of having a new instance created)
  2. This Metric produced when needed (first use)
  3. This Metric registered in the registry with its name (here "myTimer")
There are 2 Metric: the com.codahale.metrics.Metric interface and the com.codahale.metrics.annotation.Metric annotation

How to achieve this?

We need to write an extension that will:
  1. Declare @Metric as a new Qualifier to ease injection and name resolution in a BeforeBeanDiscovery observer
  2. Change how a Metric instance will be produced to look for it in the registry and produce (and register) it only if it’s not found. We’ll do this by:
    1. observing the ProcessProducer lifecycle event
    2. decorating Metric Producer to add this new behaviour.
  3. Produce all Metric at the end of boot time to have them in registry for runtime
    1. we’ll do this by observing AfterDeploymentValidation event

So we will @Observes these 3 events to add our features

lifecycle BBD PP ADV lifecycle legend light

Adding @Metric to the list of qualifiers

This time we need annotation members to be binding (@Metric("a") and @Metric("b") should be distinguished)
So we don’t have to add @Nonbinding annotation on them
public class MetricExtension implements Extension {

    void addMetricQualifier(@Observes BeforeBeanDiscovery bbd) {
        bbd.addQualifier(Metric.class);
    }
    ...
}

Customizing Metric producing process

We first need to create our implementation of Producer<T> SPI
public class MetricProducer implements Producer<com.codahale.metrics.Metric> {

        private Producer<com.codahale.metrics.Metric> decorated;
        private BeanManager bm;
        private String name;

        public MetricProducer(Producer<com.codahale.metrics.Metric> decorated, String name, BeanManager bm) {
            this.decorated = decorated;
            this.bm = bm;
            this.name = name;
        }

        MetricRegistry getRegistry() {
            return BeanProvider.getContextualReference(bm, MetricRegistry.class, false); (1)
        }
...
1BeanProvider is a DeltaSpike helper class to easily retrieve a bean or bean instance

Customizing Metric producing process (continued)

...
        @Override
        public com.codahale.metrics.Metric produce(CreationalContext<com.codahale.metrics.Metric> ctx) {
            MetricRegistry reg = getRegistry();
            if (!reg.getMetrics().containsKey(name))
                reg.register(name, decorated.produce(ctx));
            return reg.getMetrics().get(name);
        }

        @Override
        public void dispose(com.codahale.metrics.Metric instance) { } (1)

        @Override
        public Set<InjectionPoint> getInjectionPoints() {
            return decorated.getInjectionPoints();
        }
    }
1We don’t want to have the produced metric destroyed by CDI container

We’ll use our Producer<Metric> in a ProcessProducer observer

Thru this event we can substitute the standard producer by ours
public interface ProcessProducer<T, X> {
    public AnnotatedMember<T> getAnnotatedMember(); (1)

    public Producer<X> getProducer(); (2)

    public void setProducer(Producer<X> producer); (3)

    public void addDefinitionError(Throwable t);
}
1Used to retrieve annotations associated to @Produces
2Get the default producer (useful to decorate it)
3Change the producer by ours

Customizing Metric producing process (end)

Here’s the extension code to do this producer decoration
public class MetricExtension implements Extension {
...
    void reproduceMetric(@Observes ProcessProducer<?, com.codahale.metrics.Metric> pp,
                         BeanManager bm) {

        String name = pp.getAnnotatedMember().getAnnotation(Metric.class).name();
        pp.setProducer(new MetricProducer(pp.getProducer(),name,bm));
    }
...
}

Producing all the Metric at the end of boot time

We do that by observing the AfterDeploymentValidation event
public class MetricExtension implements Extension {
...
    void regMetrics(@Observes AfterDeploymentValidation adv, BeanManager bm) {
        Set<Bean<?>> metricBeans = bm.getBeans(com.codahale.metrics.Metric.class, new AnyLiteral()); (1)
        for (Bean<?> bean : metricBeans) {
            Metric qual = AnnotationUtils.findAnnotation(bm, (Annotation[])(bean.getQualifiers().toArray()), Metric.class);
            String name = qual.name(); (2)
            BeanProvider.getContextualReference(bm, com.codahale.metrics.Metric.class, false, qual); (3)
        }
...
}
1Getting all the Metric beans
2Retrieving its name from bean qualifiers
3Requesting an instance that will use our custom producer and thus will fill the registry

Second Goal achieved

We can now write:
@Produces @Metric(name="myTimer")
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, TimeUnit.MINUTES));

@Inject
MetricRegistry registry;

@Inject @Metric("myTimer")
Metric timer;
And be sure that registry.getMetrics.get("myTimer") and timer are the same object (our custom Timer)

Complete extension code

public class MetricExtension implements Extension {

    void reproduceMetric(@Observes ProcessProducer<?, com.codahale.metrics.Metric> pp, BeanManager bm) {
        String name = pp.getAnnotatedMember().getAnnotation(Metric.class).name();
        pp.setProducer(new MetricProducer(pp.getProducer(),name,bm));
    }
    void addTimedBinding(@Observes BeforeBeanDiscovery bbd) throws Exception {
        bbd.addInterceptorBinding(createTimedAnnotatedType());
    }
    private AnnotatedType createTimedAnnotatedType() throws Exception {
        Annotation nonBinding = new AnnotationLiteral<Nonbinding>() {};
        return new AnnotatedTypeBuilder().readFromType(Timed.class)
                .addToMethod(Timed.class.getMethod("name"), nonBinding)
                .addToMethod(Timed.class.getMethod("absolute"), nonBinding).create();
    }
    void addMetricQualifier(@Observes BeforeBeanDiscovery bbd) { bbd.addQualifier(Metric.class); }
    void regMetrics(@Observes AfterDeploymentValidation adv, BeanManager bm) {
        Set<Bean<?>> metricBeans = bm.getBeans(com.codahale.metrics.Metric.class, new AnyLiteral());
        for (Bean<?> bean : metricBeans) {
            Metric qual = AnnotationUtils.findAnnotation(bm, (Annotation[])(bean.getQualifiers().toArray()), Metric.class);
            String name = qual.name();
            BeanProvider.getContextualReference(bm, com.codahale.metrics.Metric.class, false, qual);
        }
    }
}

Camel CDI

How to use CDI as dependency injection container for an integration framework (Apache Camel)

About Apache Camel

Open-source integration framework based on known Enterprise Integration Patterns
Provides a variety of DSLs to write routing and mediation rules
Provides support for bean binding and seamless integration with DI frameworks
eip

Discover how we created CDI integration module for Camel

Camel out of the box (NO CDI)

class CamelMain {
    static CamelContext camelContext = new DefaultCamelContext();
    static CountDownLatch shutdownLatch = new CountDownLatch(1);
    public static void main(String[] args) {
        camelContext.addRoutes(new RouteBuilder() {
            public void configure() {
              from("file:inputDir?delay=1000").convertBodyTo(String.class).to("sjms:queue:outputDest"); (1)
            }
        });
        SjmsComponent component = new SjmsComponent();
        component.setConnectionFactory(/*...*/);
        Integer maxConnections = Integer.valueOf(camelContext
            .resolvePropertyPlaceholders("{{jms.queue.maxConnections}}"));
        component.setConnectionCount(maxConnections);
        camelContext.addComponent("sjms", component); // Registers the "sjms" component
        camelContext.start();
        shutdownLatch.await(); // Shutdown hook
        camelContext.stop();
    }
}
1This route watch a directory every second and send new files content to a JMS queue

Basic CDI integration 1/2

class FileToJmsRouteBean extends RouteBuilder {
    @Override
    public void configure() {
        from("file:inputDir?delay=1000")
          .convertBodyTo(String.class)
          .to("sjms:queue:outputDest");
    }
}
class JmsComponentFactoryBean {
    @Produces
    @ApplicationScoped
    SjmsComponent sjmsComponent() {
        SjmsComponent component = new SjmsComponent();
        component.setConnectionFactory(/*...*/);
        return component;
    }
}

Basic CDI integration 2/2

@ApplicationScoped
class CamelContextBean extends DefaultCamelContext {
    @Inject
    CamelContextBean(FileToJmsRouteBean route, SjmsComponent sjmsComponent) {
        Integer maxConnections = Integer.valueOf(
            resolvePropertyPlaceholders("{{jms.queue.maxConnections}}"));
        sjmsComponent.setConnectionCount(maxConnections);
        addComponent("sjms", sjmsComponent);
        addRoutes(route);
    }
    @PostConstruct
    void postConstruct() {
        super.start();
    }
    @PreDestroy
    void preDestroy() {
        super.stop();
    }
}
We could have a lot more with advanced CDI features

Our goals

  1. Avoid assembling and configuring the CamelContext manually
  2. Access CDI beans from the Camel DSL automatically
    .to("sjms:queue:outputDest"); // Lookup by name (sjms) and type (Component)
  3. Support Camel annotations in CDI beans
    @PropertyInject(value = "jms.queue.maxConnections", defaultValue = "10")
    Integer maxConnections;

Steps to integrate Camel and CDI

Manage the creation and the configuration of the CamelContext bean
Bind the CamelContext lifecycle to the CDI container events
Implement the Camel SPI to look up CDI bean references
Use a custom InjectionTarget for CDI beans containing Camel annotations
Use the Magic

How to achieve this?

We need to write an extension that will:
  1. Declare a CamelContext bean if no such bean is already deployed by observing the AfterBeanDiscovery lifecycle event
  2. Instantiate the beans of type RouteBuilder and add them to the Camel context
  3. Start (resp. stop) the Camel context when the AfterDeploymentValidation event is fired (resp. the BeforeShutdown event)
  4. Customize the Camel context to query the BeanManager to lookup CDI beans by name and type
  5. Detect CDI beans containing Camel annotations by observing the ProcessAnnotatedType and modify how they get injected by observing the ProcessInjectionTarget lifecycle event

So we will @Observes these 5 events to add our features

lifecycle PAT PIT ABD ADV BS lifecycle legend light

Adding the CamelContext bean

Automatically add a CamelContext bean if no such a bean already exists in the deployment archive

How to detect if a bean exists in the deployment?

By querying the BeanManager during the AfterBeanDiscovery lifecycle event
public class CamelExtension implements Extension {
    void addCamelContextBean(@Observes AfterBeanDiscovery abd, BeanManager bm) {
        Set<Bean<?>> beans = bm.getBeans(CamelContext.class);
        if (beans.isEmpty())
            // No CamelContext bean is already deployed
    }
}

Declaring a bean programmatically

How to add a bean programmatically?

We need to implement the Bean SPI
public interface Bean<T> extends Contextual<T>, BeanAttributes<T> {
    public Class<?> getBeanClass();
    public Set<InjectionPoint> getInjectionPoints();
    public T create(CreationalContext<T> creationalContext); // Contextual<T>
    public void destroy(T instance, CreationalContext<T> creationalContext);
    public Set<Type> getTypes(); // BeanAttributes<T>
    public Set<Annotation> getQualifiers();
    public Class<? extends Annotation> getScope();
    public String getName();
    public Set<Class<? extends Annotation>> getStereotypes();
    public boolean isAlternative();
}

Implementing the Bean SPI

class CamelContextBean implements Bean<DefaultCamelContext> {
    private final Set<Annotation> qualifiers = new HashSet<>(Arrays.asList(new AnnotationLiteral<Any>(){},
                                                                       new AnnotationLiteral<Default>(){}));
    private final Set<Type> types;

    public CamelContextBean(BeanManager bm) {
        types = bm.createAnnotatedType(DefaultCamelContext.class).getTypeClosure();
    }
    public Class<? extends Annotation> getScope() {
        return ApplicationScoped.class;
    }
    public Set<Annotation> getQualifiers() {
        return Collections.unmodifiableSet(qualifiers);
    }
    public Set<Type> getTypes() {
        return Collections.unmodifiableSet(types);
    }
    public DefaultCamelContext create(CreationalContext<DefaultCamelContext> creational) {
        return new DefaultCamelContext();
    }
    public void destroy(DefaultCamelContext instance, CreationalContext<DefaultCamelContext> creational) {
    }
    ...

Implementing the Bean SPI (continued)

    ...
    public Class<?> getBeanClass() {
        return DefaultCamelContext.class;
    }
    public Set<InjectionPoint> getInjectionPoints() {
        return Collections.emptySet();
    }
    public String getName() { // Only called for @Named bean
        return "";
    }
    public Set<Class<? extends Annotation>> getStereotypes() {
        return Collections.emptySet();
    }
    public boolean isAlternative() {
        return false;
    }
    public boolean isNullable() { // Deprecated since CDI 1.1
        return false;
    }
}

Adding a programmatic bean to the deployment

Then add the CamelContextBean bean programmatically by observing the AfterBeanDiscovery lifecyle event
public class CamelExtension implements Extension {

    void addCamelContextBean(@Observes AfterBeanDiscovery abd, BeanManager bm) {
        Set<Bean<?>> beans = bm.getBeans(CamelContext.class);
        if (beans.isEmpty())
            abd.addBean(new CamelContextBean(bm));
    }
}

Instantiate and assemble the Camel context

Instantiate the CamelContext bean and the RouteBuilder beans in the AfterDeploymentValidation lifecycle event
public class CamelExtension implements Extension {
    ...
    void configureContext(@Observes AfterDeploymentValidation adv, BeanManager bm) {
        CamelContext context = getReference(bm, CamelContext.class);
        for (Bean<?> bean : bm.getBeans(RoutesBuilder.class))
            context.addRoutes(getReference(bm, RouteBuilder.class, bean));
    }
    <T> T getReference(BeanManager bm, Class<T> type) {
        return getReference(bm, type, bm.resolve(bm.getBeans(type)));
    }
    <T> T getReference(BeanManager bm, Class<T> type, Bean<?> bean) {
        return (T) bm.getReference(bean, type, bm.createCreationalContext(bean));
    }
}

Managed the Camel context lifecycle

Start (resp. stop) the Camel context when the AfterDeploymentValidation event is fired (resp. the BeforeShutdown)
public class CamelExtension implements Extension {

    private CamelContext context;

    void configureContext(@Observes AfterDeploymentValidation adv, BeanManager bm) {
        context = getReference(bm, CamelContext.class);
        for (Bean<?> bean : bm.getBeans(RoutesBuilder.class)
            context.addRoutes(getReference(bm, RouteBuilder.class, bean);
        context.start();
    }
    void stopCamelContext(@Observes BeforeShutdown bs, BeanManager bm) {
        context.stop();
    }
    ...
}

First goal achieved

We can get rid of the following code:
@ApplicationScoped
class CamelContextBean extends DefaultCamelContext {
    @Inject
    CamelContextBean(FileToJmsRouteBean route, SjmsComponent sjmsComponent) {
        Integer maxConnections = Integer.valueOf(
            resolvePropertyPlaceholders("{{jms.queue.maxConnections}}"));
        sjmsComponent.setConnectionCount(maxConnections);
        addComponent("sjms", sjmsComponent);
        addRoutes(route);
    }
    @PostConstruct
    void postConstruct() {
        super.start();
    }
    @PreDestroy
    void preDestroy() {
        super.stop();
    }
}

Second goal: Access CDI beans from the Camel DSL

How to retrieve CDI beans from the Camel DSL?

.to("sjms:queue:outputDest"); // Lookup by name (sjms) and type (Component)

// And also...
.bean(MyBean.class); // Lookup by type and Default qualifier
.beanRef("beanName"); // Lookup by name
Implement the Camel registry SPI and use the BeanManager to lookup for CDI bean contextual references by name and type

Implement the Camel registry SPI

class CdiRegistry implements Registry {
    private final BeanManager bm;
    public CdiRegistry(BeanManager bm) {
        this.bm = bm;
    }
    public <T> T lookupByNameAndType(String name, Class<T> type) {
        return getReference(bm, type, bm.resolve(bm.getBeans(name)));
    }
    public <T> Set<T> findByType(Class<T> type) {
        return getReference(bm, type, bm.resolve(bm.getBeans(type)));
    }
    public Object lookupByName(String name) {
        return lookupByNameAndType(name, Object.class);
    }
    <T> T getReference(BeanManager bm, Type type, Bean<?> bean) {
        return (T) bm.getReference(bean, type, bm.createCreationalContext(bean));
    }
}

Add the CdiRegistry to the Camel context

public class CamelExtension implements Extension {
    ...
    void configureContext(@Observes AfterDeploymentValidation adv, BeanManager bm) {
        context = getReference(bm, DefaultCamelContext.class);
        for (Bean<?> bean : bm.getBeans(RoutesBuilder.class)
            context.addRoutes(getReference(bm, RouteBuilder.class, bean);

        context.setRegistry(new CdiRegistry(bm));
        context.start();
    }
    ...
}

Second goal achieved 1/2

We can declare the sjms component with the @Named qualifier
class JmsComponentFactoryBean {

    @Produces
    @Named("sjms")
    @ApplicationScoped
    Component sjmsComponent() {
        SjmsComponent component = new SjmsComponent();
        component.setConnectionFactory(/*...*/);
        return component;
    }
}

…​

Second goal achieved 2/2

And get rid of the code related to the sjms component registration
@ApplicationScoped
class CamelContextBean extends DefaultCamelContext {
    @Inject
    CamelContextBean(FileToJmsRouteBean route, SjmsComponent sjmsComponent) {
        Integer maxConnections = Integer.valueOf(
            resolvePropertyPlaceholders("{{jms.queue.maxConnections}}"));
        sjmsComponent.setConnectionCount(maxConnections);
        addComponent("sjms", sjmsComponent);
        addRoutes(route);
    }
    @PostConstruct
    void postConstruct() {
        super.start();
    }
    @PreDestroy
    void preDestroy() {
        super.stop();
    }
}

Third goal: Support Camel annotations in CDI beans

Camel provides a set of DI framework agnostic annotations for resource injection
@PropertyInject(value = "jms.queue.maxConnections", defaultValue = "10")
Integer maxConnections;

// But also...
@EndpointInject(uri="jms:queue:foo")
Endpoint endpoint;

@BeanInject("foo")
FooBean foo;

How to support custom annotations injection?

How to support custom annotations injection?

Create a custom InjectionTarget that uses the default Camel bean post processor DefaultCamelBeanPostProcessor
public interface InjectionTarget<T> extends Producer<T> {
    public void inject(T instance, CreationalContext<T> ctx);
    public void postConstruct(T instance);
    public void preDestroy(T instance);
}
Hook it into the CDI injection mechanism by observing the ProcessInjectionTarget lifecycle event
Only for beans containing Camel annotations by observing the ProcessAnnotatedType lifecycle and using @WithAnnotations

Create a custom InjectionTarget

class CamelInjectionTarget<T> implements InjectionTarget<T> {

  final DefaultCamelBeanPostProcessor processor = new DefaultCamelBeanPostProcessor();

  final InjectionTarget<T> delegate;

  CamelInjectionTarget(InjectionTarget<T> target) {
      delegate = target;
  }

  public void inject(T instance, CreationalContext<T> ctx) {
      processor.postProcessBeforeInitialization(instance); (1)
      delegate.inject(instance, ctx);
  }
  ...
}
1Call the Camel default bean post-processor after CDI injection

Register the custom InjectionTarget

Observe the ProcessInjectionTarget lifecyle event and set the CamelInjectionTarget
public interface ProcessInjectionTarget<X> {
    public AnnotatedType<X> getAnnotatedType();
    public InjectionTarget<X> getInjectionTarget();
    public void setInjectionTarget(InjectionTarget<X> injectionTarget);
    public void addDefinitionError(Throwable t);
}
class CamelExtension implements Extension {

  <T> void camelBeansPostProcessor(@Observes ProcessInjectionTarget<T> pit) {
      pit.setInjectionTarget(new CamelInjectionTarget<T>(pit.getInjectionTarget()));
  }
}

But only for beans containing Camel annotations

class CamelExtension implements Extension {
  final Set<AnnotatedType<?>> camelBeans = new HashSet<>();

  void camelAnnotations(@Observes @WithAnnotations({BeanInject.class, (1)
      Consume.class, EndpointInject.class, Produce.class, PropertyInject.class})
      ProcessAnnotatedType<?> pat) {
        camelBeans.add(pat.getAnnotatedType());
  }

  <T> void camelBeansPostProcessor(@Observes ProcessInjectionTarget<T> pit) {
      if (camelBeans.contains(pit.getAnnotatedType())) (2)
        pit.setInjectionTarget(new CamelInjectionTarget<>(pit.getInjectionTarget()));
  }
}
1Detect all the types containing Camel annotations with @WithAnnotations
2Decorate the InjectionTarget corresponding to these types with a custom post-processor

Third goal achieved 1/2

We can add Camel annotations in CDI beans
class JmsComponentFactoryBean {

    @PropertyInject(value = "jms.queue.maxConnections", defaultValue = "10")
    Integer maxConnections;

    @Produces
    @Named("sjms")
    @ApplicationScoped
    SjmsComponent sjmsComponent() {
        SjmsComponent component = new SjmsComponent();
        component.setConnectionFactory(/*...*/);
        component.setConnectionCount(maxConnections);
        return component;
    }
}

Third goal achieved 2/2

And get rid of all the code related to the Camel context config
@ApplicationScoped
class CamelContextBean extends DefaultCamelContext {
    @Inject
    CamelContextBean(FileToJmsRouteBean route, SjmsComponent sjmsComponent) {
        Integer maxConnections = Integer.valueOf(
            resolvePropertyPlaceholders("{{jms.queue.maxConnections}}"));
        sjmsComponent.setConnectionCount(maxConnections);
        addComponent("sjms", sjmsComponent);
        addRoutes(route);
    }
    @PostConstruct
    void postConstruct() {
        super.start();
    }
    @PreDestroy
    void preDestroy() {
        super.stop();
    }
}

Conclusion

References

CDI Specification - cdi-spec.org
Metrics CDI sources - github.com/astefanutti/metrics-cdi
Camel CDI sources - github.com/astefanutti/camel-cdi
Slides sources - github.com/astefanutti/further-cdi
Slides generated with Asciidoctor, PlantUML and DZSlides backend
Original slide template - Dan Allen & Sarah White

Antoine Sabot-Durand Antonin Stefanutti

Annexes

Complete Lifecycle Events

lifecycle complete