Going Further with CDI 2.0

Antonin Stefanutti

  • Software Engineer
  • Red Hat
  • JBoss Fuse team
  • @astefanut
  • github.com/astefanutti

Antoine Sabot-Durand

  • Red Hat
  • CDI spec lead
  • @antoine_sd
  • next-presso.com
  • github.com/antoinesd

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 most of these you can stay

More concretely

What’s included:
  1. Introduction to portable extensions
  2. Real use cases from real projects
  3. Code in IDE with tests
What’s not included:
  1. Introduction to CDI
  2. Existing content on CDI extensions
  3. Work with contexts (need 2 more hours)

Tools used in the code

Arquillian

arquillian
  1. Arquillian is an integration testing platform
  2. It integrates with JUnit
  3. Create your SUT in a dedicated method
  4. Run tests in the target containers of your choice
  5. We’ll use the arquillian-weld-embedded container adapter
  6. The proper solution to test Java EE code
  7. More info on arquillian.org

Agenda

qr code
  • Meet CDI SPI
  • CDI Extensions
  • Metrics CDI
  • CDI Quizz
  • Camel CDI

Meet CDI SPI

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/2)

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

This SPI can be used in your code (2/2)

InjectionPoint contains info about requested type at @Inject
class MyMapProducer() {

    @Produces
    <K, V> Map<K, V> produceMap(InjectionPoint ip) {
        if (valueIsNumber(((ParameterizedType) ip.getType())))
            return new TreeMap<K, V>();
        return new HashMap<K, V>();
    }

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

SPI providing CDI entry points

entry points

SPI dedicated to extensions

spi extensions

All these SPI interfaces are events containing meta-model SPI

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

cart

A ProcessAnnotatedType<T> event is fired for each type being discovered at boot time
Observing ProcessAnnotatedType<Foo> allows you to prevent Foo to be deployed as a bean by calling ProcessAnnotatedType#veto()

CDI Extensions

Portable extensions

One of the most powerful feature of the CDI specification
Not really popularized, partly due to:
  1. Their high level of abstraction
  2. The pre-requisite knowledge about basic CDI and SPI
  3. Lack of information (CDI is often perceived as a basic DI solution)

Extensions, what for?

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

Extensions, how?

Observing SPI events at boot time related to the bean manager 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;

public 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

Metrics CDI

Integrating Dropwizard Metrics in CDI

Dropwizard Metrics provides

Different metric types: Counter, Gauge, Meter, Timer, …​
Different reporter: JMX, console, SLF4J, CSV, servlet, …​
MetricRegistry object which collects all your app metrics
Annotations for AOP frameworks: @Counted, @Timed, …​
…​ but does not include integration with these frameworks
More at dropwizard.github.io/metrics

Discover how we created CDI integration module for Metrics

Metrics out of the box (without 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 a Timer named "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 to achieve full CDI integration

Produce and inject multiple metrics of the same type
Enable Metrics with the provided annotations
Access same Metric instances through @inject or MetricRegistry API

GOAL 1 Produce and inject
multiple metrics of the same type

What’s the problem with multiple Metrics of the same type?

This code throws a deployment exception (ambiguous dependency)
@Produces
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, MINUTES)); (1)

@Produces
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, HOURS)); (2)

@Inject
Timer timer; (3)
1This timer that only keeps measurement of last minute is produced as a bean of type Timer
2This timer that only keeps measurement of last hour is produced as a bean of type Timer
3This injection point is ambiguous since 2 eligible beans exist

Solving the ambiguity

We could use the provided @Metric annotation to qualify our beans
@Produces
@Metric(name = "my_timer")
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, MINUTES));

@Produces
@Metric(name = "my_other_timer")
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, HOURS));

@Inject
@Metric(name = "my_timer")
Timer timer;
That won’t work out of the box since @Metric is not a qualifier

How to declare @Metric as a qualifier?

By observing the BeforeBeanDiscovery lifecycle event in an extension
javax.enterprise.inject.spi.BeforeBeanDiscovery
public interface BeforeBeanDiscovery {
    void addQualifier(Class<? extends Annotation> qualifier);  (1)
    void addQualifier(AnnotatedType<? extends Annotation> qualifier);
    void addScope(Class<? extends Annotation> scopeType, boolean normal, boolean passivation);
    void addStereotype(Class<? extends Annotation> stereotype, Annotation... stereotypeDef);
    void addInterceptorBinding(AnnotatedType<? extends Annotation> bindingType);
    void addInterceptorBinding(Class<? extends Annotation> bindingType, Annotation... bindingTypeDef);
    void addAnnotatedType(AnnotatedType<?> type);
    void addAnnotatedType(AnnotatedType<?> type, String id);
    <T> AnnotatedTypeConfigurator<T> addAnnotatedType(Class<T> type, String id);
    <T extends Annotation> AnnotatedTypeConfigurator<T> configureQualifier(Class<T> qualifier);
    <T extends Annotation> AnnotatedTypeConfigurator<T> configureInterceptorBinding(Class<T> bindingType);
}
1The method we need to declare the @Metric annotation as a CDI qualifier
And use addQualifier() method in the event

BeforeBeanDiscovery is first in lifecycle

lifecycle BBD lifecycle legend

Our first extension

A CDI extension is a class implementing the Extension tag interface
org.cdi.further.metrics.MetricsExtension
public class MetricsExtension implements Extension {

    void addMetricAsQualifier(@Observes BeforeBeanDiscovery bdd) {
        bdd.addQualifier(Metric.class);
    }
}
Extension is activated by adding this file to META-INF/services
javax.enterprise.inject.spi.Extension
org.cdi.further.metrics.MetricsExtension

Goal 1 achieved

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

@Produces
@Metric(name = "my_other_timer")
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, HOURS));

@Inject
@Metric(name = "my_timer")
Timer timer;
And have the Timer injection points satisfied

GOAL 2 Apply Metrics with the provided annotations

Goal 2 in detail

We want to be able to write:
@Timed("timer")  (1)
void timedMethod() {
    // Business code
}
And have the timer "timer" activated during method invocation
The solution is to declare an interceptor and bind it to @Timed

Goal 2 step by step

Create an interceptor for the timer’s technical code
Make @Timed (provided by Metrics) a valid interceptor binding
Programmatically add @Timed as an interceptor binding

Preparing interceptor creation

We should find 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 code is highlighted below
@Interceptor
class TimedInterceptor {
    @Inject MetricRegistry registry; (1)

    @AroundInvoke
    Object timedMethod(InvocationContext context) throws Exception {
        Timer timer = registry.timer(context.getMethod().getAnnotation(Timed.class).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 code of the application is called. All the code around is the technical code.

Activating the interceptor

@Interceptor
@Priority(Interceptor.Priority.LIBRARY_BEFORE)  (1)
class TimedInterceptor {

    @Inject
    MetricRegistry registry;

    @AroundInvoke
    Object timedMethod(InvocationContext context) throws Exception {
        Timer timer = registry.timer(context.getMethod().getAnnotation(Timed.class).name());
        Timer.Context time = timer.time();
        try {
            return context.proceed();
        } finally {
            time.stop();
        }
    }
}
1Giving a @Priority to an interceptor activates and orders it

Add a binding to the interceptor

@Timed  (1)
@Interceptor
@Priority(Interceptor.Priority.LIBRARY_BEFORE)
class TimedInterceptor {

    @Inject
    MetricRegistry registry;

    @AroundInvoke
    Object timedMethod(InvocationContext context) throws Exception {
        Timer timer = registry.timer(context.getMethod().getAnnotation(Timed.class).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 places:
  1. On the interceptor class to bind it to this annotation
  2. On the methods or classes to be intercepted by this interceptor
An interceptor binding should have the @InterceptorBinding annotation or should be declared programmatically
If the interceptor binding annotation has members:
  1. Their values are taken into account to resolve interceptor
  2. Unless members are annotated with @NonBinding

@Timed annotation is 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 the @InterceptorBinding annotation
2None of the members have the @NonBinding annotation, so @Timed(name = "timer1") and @Timed(name = "timer2") will be 2 different interceptor bindings

The required @Timed source code 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 achieve the required @Timed declaration?

We cannot touch the component source / binary!

Using the AnnotatedTypeConfigurator SPI

We observe BeforeBeanDiscovery to add a new interceptor binding
Use the AnnotatedTypeConfigurator<T> API introduced in CDI 2.0
public class MetricsExtension implements Extension {

    void addTimedInterceptorBinding(@Observes BeforeBeanDiscovery bdd) {
        bbd.configureInterceptorBinding(Timed.class).methods()
            .forEach(method -> method.add(Nonbinding.Literal.INSTANCE)); (1)
    }
}
1@NonBinding is added to all members of the @Timed annotation

Goal 2 achieved

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

And have a Metrics Timer applied to the method

  1. Interceptor code should be enhanced to support @Timed on classes
  2. Other interceptors should be developed for other metric types

Our goals

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

GOAL 3 Access same Metric instances through @Inject or MetricRegistry API

Goal 3 in detail

When writing:
@Inject
@Metric(name = "my_timer")
Timer timer1;

@Inject
MetricRegistry registry;
Timer timer2 = registry.timer("my_timer");
…​ We want that timer1 == timer2

Goal 3 in detail

@Produces
@Metric(name = "my_timer") (1)
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, TimeUnit.MINUTES));

@Inject
@Metric(name = "my_timer")
Timer timer;

@Inject
MetricRegistry registry;
Timer timer = registry.timer("my_timer"); (2)
1Produced Timer should be added to the Metrics registry when produced
2When retrieved from the registry, a Metric should be identical to the produced instance and vice versa
There are 2 Metric classes, the com.codahale.metrics.Metric interface and the com.codahale.metrics.annotation.Metric annotation

Goal 3 step by step

We need to write an extension that will:
  1. Change how a Metric instance is produced by looking it up in the registry first and producing (and registering) 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 behavior
  2. Produce all Metric instances at the end of bootstrap to have them in registry for runtime
    1. we’ll do this by observing the AfterDeploymentValidation event

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

lifecycle BBD PP ADV lifecycle legend

Customizing Metric producing process

Observe the ProcessProducer lifecycle event to override the original producer using the ProducerConfigurator<T> API introduced in CDI 2.0
javax.enterprise.inject.spi.ProcessProducer
public interface ProcessProducer<T, X> {
    AnnotatedMember<T> getAnnotatedMember(); (1)
    Producer<X> getProducer(); (2)
    void setProducer(Producer<X> producer);
    ProducerConfigurator<X> configureProducer();  (3)
    void addDefinitionError(Throwable t);
}
1Gets the AnnotatedMember associated to the @Produces field or method
2Gets the default producer (useful to decorate it)
3Overrides the producer

Customizing Metric producing process (end)

public class MetricsExtension implements Extension {
    // ...
    <T extends c.c.m.Metric> void decorateMetricProducer(@Observes ProcessProducer<?, T> pp, BeanManager manager) {
       if (pp.getAnnotatedMember().isAnnotationPresent(Metric.class)) {
           String name = pp.getAnnotatedMember().getAnnotation(Metric.class).name(); (1)
           Producer<T> producer = pp.getProducer();
           pp.configureProducer().produceWith(context -> { (2)
               MetricRegistry registry = manager.createInstance().select(MetricRegistry.class).get();
               if (registry.getMetrics().containsKey(name))
                   return (T) registry.getMetrics().get(name);
               return registry.register(name, producer.produce(context)); (3)
           });
       }
    }
    // ...
}
1We retrieve metric’s name by calling the name() member from @Metric
2The produceWith method is used by the container at runtime to decorate declared producer with our logic
3If metric name is not in the registry, the original producer is called and its result is added to the registry

Producing all the Metric instances at the end of boot time

We do that by observing the AfterDeploymentValidation event
public class MetricsExtension implements Extension {
  // ...
  void registerProducedMetrics(@Observes AfterDeploymentValidation adv, BeanManager manager) {
      manager.createInstance().select(com.codahale.metrics.Metric.class, Any.Literal.INSTANCE)
        .forEach(Object::toString);
  }
  // ...
}

Goal 3 achieved

We can now write:
@Produces
@Metric(name = "my_timer")
Timer timer1 = new Timer(new SlidingTimeWindowReservoir(1L, MINUTES));

@Inject
@Metric(name = "my_timer")
Timer timer2;

@Inject
MetricRegistry registry;
Timer timer3 = registry.timer("my_timer");
And make sure that timer1 == timer2 == timer3

Complete extension code

public class MetricsExtension implements Extension {

    void addMetricAsQualifier(@Observes BeforeBeanDiscovery bdd) {
        bdd.addQualifier(Metric.class);
    }

    void addTimedInterceptorBinding(@Observes BeforeBeanDiscovery bbd) {
        bbd.configureInterceptorBinding(Timed.class).methods().forEach(method -> method.add(Nonbinding.Literal.INSTANCE));
    }

    <T extends com.codahale.metrics.Metric> void decorateMetricProducer(@Observes ProcessProducer<?, T> pp, BeanManager manager) {
        if (pp.getAnnotatedMember().isAnnotationPresent(Metric.class)) {
            String name = pp.getAnnotatedMember().getAnnotation(Metric.class).name();
            Producer<T> producer = pp.getProducer();
            pp.configureProducer().produceWith(context -> {
                MetricRegistry registry = manager.createInstance().select(MetricRegistry.class).get();
                if (registry.getMetrics().containsKey(name))
                    return (T) registry.getMetrics().get(name);
                return registry.register(name, producer.produce(context));
            });
        }
    }

    void registerProduceMetrics(@Observes AfterDeploymentValidation adv, BeanManager manager) {
        manager.createInstance().select(com.codahale.metrics.Metric.class, Any.Literal.INSTANCE).forEach(Object::toString);
    }
}

Quizz time

Test your CDI knowledge

Find the valid injections points

class MySuperBean {

    @Inject
    Bean<MySuperBean> myMeta;                                           // A [ ]

    @Inject
    Bean<MyService> serviceMeta;                                        // B [ ]

    public MySuperBean(@Inject MyService service) {/*...*/}             // C [ ]

    @Inject
    private void myInitMethod(MyService service) {/*...*/}              // D [ ]

    @Inject
    @PostConstruct
    public void myInitMethod2(MyService service) {/*...*/}              // E [ ]
}

Solution

class MySuperBean {

    @Inject
    Bean<MySuperBean> myMeta;                                           // A [X]

    @Inject
    Bean<MyService> serviceMeta;                                        // B [ ]

    public MySuperBean(@Inject MyService service) {/*...*/}             // C [ ]

    @Inject
    private void myInitMethod(MyService service) {/*...*/}              // D [X]

    @Inject
    @PostConstruct
    public void myInitMethod2(MyService service) {/*...*/}              // E [ ]
}

Find Beans candidates without beans.xml in JAR (CDI 1.2)

@Decorator
public abstract class MyDecorator implements MyService {/*...*/}        // A [ ]

@Stateless
public class MyServiceImpl implements MyService {/*...*/}               // B [ ]

public class MyBean {/*...*/}                                           // C [ ]

@Model
public class MyBean {/*...*/}                                           // D [ ]

@Singleton
public class MyBean {/*...*/}                                           // E [ ]

@ConversationScoped
public class MyBean {/*...*/}                                           // F [ ]

Solution

@Decorator
public abstract class MyDecorator implements MyService {/*...*/}        // A [X]

@Stateless
public class MyServiceImpl implements MyService {/*...*/}               // B [X]

public class MyBean {/*...*/}                                           // C [ ]

@Model
public class MyBean {/*...*/}                                           // D [X]

@Singleton
public class MyBean {/*...*/}                                           // E [ ]

@ConversationScoped
public class MyBean {/*...*/}                                           // F [X]

Find the valid producers

@ApplicationScoped
public class MyBean {

  @Produces
  Service produce1(InjectionPoint ip, Bean<Service> myMeta) {/*...*/}   // A [ ]

  @Produces
  @SessionScoped
  Service produce2(InjectionPoint ip) {/*...*/}                         // B [ ]

  @Produces
  Map<K, V> produceMap(InjectionPoint ip) {/*...*/}                     // C [ ]

  @Produces
  Map<String, ? extends Service> produceMap2() {/*...*/}                // D [ ]
}

Solution

@ApplicationScoped
public class MyBean {

  @Produces
  Service produce1(InjectionPoint ip, Bean<Service> myMeta) {/*...*/}   // A [X]

  @Produces
  @SessionScoped
  Service produce2(InjectionPoint ip) {/*...*/}                         // B [ ]

  @Produces
  Map<K, V> produceMap(InjectionPoint ip) {/*...*/}                     // C [X]

  @Produces
  Map<String, ? extends Service> produceMap2() {/*...*/}                // D [ ]
}

Which observers will be triggered?

class FirstBean {

    @Inject
    Event<Post> postEvent;

    public void saveNewPost(Post myPost) {
        postEvent.select(new AnnotationLiteral()<French> {}).fire(myPost);
    }
}

class SecondBean {

    void listenFrPost(@Observes @French Post post) {/*...*/}            // A [ ]
    void listenPost(@Observes Post post) {/*...*/}                      // B [ ]
    void listenEnPost(@Observes @English Post post) {/*...*/}           // C [ ]
    void listenObject(@Observes Object obj) {/*...*/}                   // D [ ]
}

Solution

class FirstBean {

    @Inject
    Event<Post> postEvent;

    public void saveNewPost(Post myPost) {
        postEvent.select(new AnnotationLiteral()<French> {}).fire(myPost);
    }
}

class SecondBean {

    void listenFrPost(@Observes @French Post post) {/*...*/}            // A [X]
    void listenPost(@Observes Post post) {/*...*/}                      // B [X]
    void listenEnPost(@Observes @English Post post) {/*...*/}           // C [ ]
    void listenObject(@Observes Object obj) {/*...*/}                   // D [X]
}

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 (without CDI)

 public static void main(String[] args) {
    CamelContext context = new DefaultCamelContext();
    context.addRoutes(new RouteBuilder() {
      public void configure() {
        from("file:target/input?delay=1s")
          .log("Sending message [${body}] to JMS ...")
          .to("sjms:queue:output"); (1)
      }
    });

    PropertiesComponent properties = new PropertiesComponent();
    properties.setLocation("classpath:camel.properties");
    context.addComponent("properties", properties); // Registers the "properties" component

    SjmsComponent component = new SjmsComponent();
    component.setConnectionFactory(new ActiveMQConnectionFactory("vm://broker?broker.persistent=false"));
    jms.setConnectionCount(Integer.valueOf(context.resolvePropertyPlaceholders("{{jms.maxConnections}}")));
    context.addComponent("sjms", jms); // Registers the "sjms" component

    context.start();
}
1This route watches a directory every second and sends new files content to a JMS queue

Why CDI?

before after

Basic CDI integration (1/3)

  1. Camel components and route builder as CDI beans
  2. Bind the Camel context lifecycle to that of the CDI container
class FileToJmsRouteBean extends RouteBuilder {

    @Override
    public void configure() {
        from("file:target/input?delay=1s")
            .log("Sending message [${body}] to JMS...")
            .to("sjms:queue:output");
    }
}

Basic CDI integration (2/3)

class PropertiesComponentFactoryBean {

    @Produces @ApplicationScoped
    PropertiesComponent propertiesComponent() {
        PropertiesComponent properties = new PropertiesComponent();
        properties.setLocation("classpath:camel.properties");
        return properties;
    }
}
class JmsComponentFactoryBean {

    @Produces @ApplicationScoped
    SjmsComponent sjmsComponent(PropertiesComponent properties) throws Exception {
        SjmsComponent jms = new SjmsComponent();
        jms.setConnectionFactory(new ActiveMQConnectionFactory("vm://broker?broker.persistent=false"));
        jms.setConnectionCount(Integer.valueOf(properties.parseUri("{{jms.maxConnections}}")));
        return component;
    }
}

Basic CDI integration (3/3)

@ApplicationScoped
class CamelContextBean extends DefaultCamelContext {

    @Inject
    CamelContextBean(FileToJmsRouteBean route, SjmsComponent jms, PropertiesComponent properties) {
        addComponent("properties", properties);
        addComponent("sjms", jms);
        addRoutes(route);
    }

    @PostConstruct
    void startContext() {
        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:output"); // Lookup by name (sjms) and type (Component)
    
    context.resolvePropertyPlaceholders("{{jms.maxConnections}}");
    // Lookup by name (properties) and type (Component)
  3. Support Camel annotations in CDI beans
    @PropertyInject(value = "jms.maxConnections", defaultValue = "10")
    int maxConnections;

Steps to integrate Camel and CDI

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

How to achieve this?

We need to write an extension that will:
  1. Declare a CamelContext bean 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 bean destroy method is called)
  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 event and modify how they get injected by observing the ProcessInjectionTarget lifecycle event

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

lifecycle PAT PIT ABD ADV lifecycle legend

Adding the CamelContext bean

Automatically add a CamelContext bean in the deployment archive

How to add a bean programmatically?

Declaring a bean programmatically

Use the BeanConfigurator<T> API introduced in CDI 2.0
javax.enterprise.inject.spi.configurator.BeanConfigurator<T>
public interface BeanConfigurator<T> {
    BeanConfigurator<T> beanClass(Class<?> beanClass);
    <U extends T> BeanConfigurator<U> createWith(Function<CreationalContext<U>, U> callback);
    <U extends T> BeanConfigurator<U> produceWith(Function<Instance<Object>, U> callback);
    BeanConfigurator<T> destroyWith(BiConsumer<T, CreationalContext<T>> callback);
    BeanConfigurator<T> disposeWith(BiConsumer<T, Instance<Object>> callback);
    <U extends T> BeanConfigurator<U> read(AnnotatedType<U> type);
    BeanConfigurator<T> read(BeanAttributes<?> beanAttributes);
    BeanConfigurator<T> addType(Type type);
    BeanConfigurator<T> scope(Class<? extends Annotation> scope);
    BeanConfigurator<T> addQualifier(Annotation qualifier);
    BeanConfigurator<T> name(String name);
    // ...
}

Adding a programmatic bean to the deployment

Access the BeanConfigurator<T> API by observing the AfterBeanDiscovery lifecycle event
public class CamelExtension implements Extension {

    void addCamelContextBean(@Observes AfterBeanDiscovery abd) {
        abd.addBean()
            .types(CamelContext.class)
            .scope(ApplicationScoped.class)
            .produceWith(instance -> new DefaultCamelContext());
    }
}

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 manager) {
        CamelContext context = manager.createInstance().select(CamelContext.class).get();
        manager.createInstance().select(RoutesBuilder.class).forEach(context::addRoutes);
    }
}

Managed the Camel context lifecycle (start)

Start the context when the AfterDeploymentValidation event is fired
public class CamelExtension implements Extension {
    // ...
    void configureContext(@Observes AfterDeploymentValidation adv, BeanManager manager) {
        CamelContext context = manager.createInstance().select(CamelContext.class).get();
        manager.createInstance().select(RoutesBuilder.class).forEach(context::addRoutes);
        context.start();
    }
}

Managed the Camel context lifecycle (stop)

Stop the context when the associated bean is destroyed
public class CamelExtension implements Extension {
    // ...
    void addCamelContextBean(@Observes AfterBeanDiscovery abd) {
        abd.addBean()
            .types(CamelContext.class)
            .scope(ApplicationScoped.class)
            .produceWith(instance -> new DefaultCamelContext())
            .disposeWith((context, instance) -> context.stop());
    }
}

First goal achieved

We can get rid of the following code:
@ApplicationScoped
class CamelContextBean extends DefaultCamelContext {

    @Inject
    CamelContextBean(FileToJmsRouteBean route, SjmsComponent jms, PropertiesComponent properties) {
        addComponent("properties", propertiesComponent);
        addComponent("sjms", sjmsComponent);
        addRoutes(route);
    }

    @PostConstruct
    void startContext() {
        super.start();
    }

    @PreDestroy
    void stopContext() {
        super.stop();
    }
}

Second goal: Access CDI beans from the Camel DSL

How to retrieve CDI beans from the Camel DSL?

.to("sjms:queue:output"); // Lookup by name (sjms) and type (Component)
context.resolvePropertyPlaceholders("{{jms.maxConnections}}");
// Lookup by name (properties) 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 CamelCdiRegistry implements Registry {

    private final BeanManager manager;

    CamelCdiRegistry(BeanManager manager) {
        this.manager = manager;
    }

    public Object lookupByName(String name) {
        return lookupByNameAndType(name, Object.class);
    }

    @Override
    public <T> T lookupByNameAndType(String name, Class<T> type) {
        return manager.createInstance().select(type, NamedLiteral.of(name)).stream()
            .findAny().orElse(null);
    }
    // ...
}

Add CamelCdiRegistry to the Camel context

public class CamelExtension implements Extension {

    void addCamelContextBean(@Observes AfterBeanDiscovery abd, BeanManager manager) {
        abd.addBean()
            .types(CamelContext.class)
            .scope(ApplicationScoped.class)
            .produceWith(instance -> new DefaultCamelContext(new CamelCdiRegistry(manager)))
            .disposeWith((context, instance) -> context.stop());
    }
}

Second goal achieved 1/3

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

    @Produces
    @Named("sjms")
    @ApplicationScoped
    SjmsComponent sjmsComponent(PropertiesComponent properties) {
        SjmsComponent jms = new SjmsComponent();
        jms.setConnectionFactory(new ActiveMQConnectionFactory("vm://broker?..."));
        jms.setConnectionCount(
            Integer.valueOf(properties.parseUri("{{jms.maxConnections}}")));
        return component;
    }
}

Second goal achieved 2/3

Declare the properties component with the @Named qualifier
class PropertiesComponentFactoryBean {

    @Produces
    @Named("properties")
    @ApplicationScoped
    PropertiesComponent propertiesComponent() {
        PropertiesComponent properties = new PropertiesComponent();
        properties.setLocation("classpath:camel.properties");
        return properties;
    }
}

Second goal achieved 3/3

And get rid of the code related to the components registration:
@ApplicationScoped
class CamelContextBean extends DefaultCamelContext {

    @Inject
    CamelContextBean(FileToJmsRouteBean route, SjmsComponent jms, PropertiesComponent properties) {
        addComponent("properties", propertiesComponent);
        addComponent("sjms", sjmsComponent);
        addRoutes(route);
    }

    @PostConstruct
    void startContext() {
        super.start();
    }

    @PreDestroy
    void stopContext() {
        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.maxConnections", defaultValue = "10")
int 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
javax.enterprise.inject.spi.InjectionTarget
public interface InjectionTarget<T> extends Producer<T> {
    void inject(T instance, CreationalContext<T> ctx);
    void postConstruct(T instance);
    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 InjectionTarget<T> delegate;

    final DefaultCamelBeanPostProcessor processor;

    CamelInjectionTarget(InjectionTarget<T> target, final BeanManager manager) {
        delegate = target;
        processor = new DefaultCamelBeanPostProcessor() {
            public CamelContext getOrLookupCamelContext() {
                return manager.createInstance().select(CamelContext.class).get();
            }
        };
    }

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

Register the custom InjectionTarget

Observe the ProcessInjectionTarget event and set the InjectionTarget
javax.enterprise.inject.spi.ProcessInjectionTarget
public interface ProcessInjectionTarget<X> {
    AnnotatedType<X> getAnnotatedType();
    InjectionTarget<X> getInjectionTarget();
    void setInjectionTarget(InjectionTarget<X> injectionTarget);
    void addDefinitionError(Throwable t);
}
To decorate it with the CamelInjectionTarget
public class CamelExtension implements Extension {

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

But only for beans containing Camel annotations

public class CamelExtension implements Extension {

    final Set<AnnotatedType<?>> camelBeans = new HashSet<>();

    void camelAnnotatedTypes(@Observes @WithAnnotations(PropertyInject.class)
      ProcessAnnotatedType<?> pat) { (1)
        camelBeans.add(pat.getAnnotatedType());
    }

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

Third goal achieved 1/2

Instead of injecting the PropertiesComponent bean to resolve a configuration property
class JmsComponentFactoryBean {

    @Produces
    @Named("sjms")
    @ApplicationScoped
    SjmsComponent sjmsComponent(PropertiesComponent properties) {
        SjmsComponent jms = new SjmsComponent();
        jms.setConnectionFactory(new ActiveMQConnectionFactory("vm://broker?..."));
        jms.setConnectionCount(Integer.valueOf(properties.parseUri("{{jms.maxConnections}}")));
        return component;
    }
}

Third goal achieved 2/2

We can directly rely on the @PropertyInject Camel annotation in CDI beans
class JmsComponentFactoryBean {

    @PropertyInject("jms.maxConnections")
    int maxConnections;

    @Produces
    @Named("sjms")
    @ApplicationScoped
    SjmsComponent sjmsComponent() {
        SjmsComponent component = new SjmsComponent();
        jms.setConnectionFactory(new ActiveMQConnectionFactory("vm://broker?..."));
        component.setConnectionCount(maxConnections);
        return component;
    }
}

Bonus goal: Camel DSL AOP

AOP instrumentation of the Camel DSL
from("file:target/input?delay=1s")
    .log("Sending message [${body}] to JMS...")
    .to("sjms:queue:output");
With CDI observers
from("file:target/input?delay=1s").to("sjms:queue:output").id("join point");
void advice(@Observes @Node("join point") Exchange exchange) {
    logger.info("Sending message [{}] to JMS...", exchange.getIn().getBody(String.class));
}

How to achieve this?

We can create a CDI qualifier to hold the Camel node id metadata:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface Node {
    String value();
}
And create an extension that will:
  1. Detect the CDI beans containing observer methods with the @Node qualifier by observing the ProcessObserverMethod event and collect the Camel processor nodes to be instrumented
  2. Customize the Camel context by providing an implementation of the Camel InterceptStrategy interface that will fire a CDI event each time an Exchange is processed by the instrumented nodes

Detect the Camel DSL AOP observer methods

Observe the ProcessObserverMethod lifecycle event
javax.enterprise.inject.spi.ProcessObserverMethod
public interface ProcessObserverMethod<T, X> {
    AnnotatedMethod<X> getAnnotatedMethod();
    ObserverMethod<T> getObserverMethod();
    void addDefinitionError(Throwable t);
}
And collect the observer method metadata
public class CamelExtension implements Extension {

    final Set<Node> nodePointcuts = new HashSet<>();

    void camelNodePointcuts(@Observes ProcessObserverMethod<Exchange, ?> pom) {
        pom.getObserverMethod().getObservedQualifiers().stream()
            .filter(q -> q instanceof Node).map(Node.class::cast).forEach(nodePointcuts::add);
    }
}

Instrument the Camel context

Intercept matching nodes and fire a CDI event
void configureCamelContext(@Observes AfterDeploymentValidation adv, BeanManager manager) {
    CamelContext context = manager.createInstance().select(CamelContext.class).get();
    context.addInterceptStrategy((camel, definition, target, next) -> definition.hasCustomIdAssigned()
        ? nodePointcuts.stream().filter(node -> definition.getId().equals(node.value())).findFirst() (1)
            .map(node -> (Processor) new DelegateAsyncProcessor(target) {
                public boolean process(Exchange exchange, AsyncCallback callback) {
                    manager.fireEvent(exchange, node);  (2)
                    return super.process(exchange, callback);
                }
            }).orElse(target) (2)
        : target);
    manager.createInstance().select(RoutesBuilder.class).forEach(context::addRoutes);
    context.start();
}
1Checks if there is a @Node qualifier matching the processor id (with customIdAssigned)
2Returns a delegate processor that fires a event whenever an exchange is processed, or returns the default processor

Bonus goal achieved

We can define join points in the Camel DSL
from("file:target/input?delay=1s").to("sjms:queue:output").id("join point");
And advice them with CDI observers
void advice(@Observes @Node("join point") Exchange exchange) {
    List<MessageHistory> history = exchange.getProperty(Exchange.MESSAGE_HISTORY,
                                                        List.class);
    logger.info("Sending message [{}] to [{}]...",
                exchange.getIn().getBody(String.class),
                history.get(history.size() - 1).getNode().getLabel());
}

Complete extension code

public class CamelExtension implements Extension {
  Set<AnnotatedType<?>> camelBeans = new HashSet<>(); Set<Node> nodes = new HashSet<>();
  void camelAnnotatedTypes(@Observes @WithAnnotations(PropertyInject.class) ProcessAnnotatedType<?> pat) {
      camelBeans.add(pat.getAnnotatedType());
  }
  <T> void camelBeansPostProcessor(@Observes ProcessInjectionTarget<T> pit, BeanManager m) {
    if (camelBeans.contains(pit.getAnnotatedType())) pit.setInjectionTarget(new CamelInjectionTarget<>(pit.getInjectionTarget(), m));
  }
  void camelNodePointcuts(@Observes ProcessObserverMethod<Exchange, ?> pom) {
    pom.getObserverMethod().getObservedQualifiers().stream().filter(q -> q instanceof Node).map(Node.class::cast).forEach(nodes::add);
  }
  void addCamelContext(@Observes AfterBeanDiscovery abd, BeanManager m) {
    abd.addBean().types(CamelContext.class).scope(ApplicationScoped.class)
      .produceWith(instance -> new DefaultCamelContext(new CamelCdiRegistry(m))).disposeWith((context, instance) -> context.stop());
  }
  void configureCamelContext(@Observes AfterDeploymentValidation adv, BeanManager manager) {
    CamelContext context = manager.createInstance().select(CamelContext.class).get();
    context.addInterceptStrategy((camel, definition, target, next) -> definition.hasCustomIdAssigned() ? nodes.stream()
      .filter(node -> definition.getId().equals(node.value())).findFirst().map(node -> new DelegateAsyncProcessor(target) {
        public boolean process(Exchange exchange, AsyncCallback callback) {
          manager.fireEvent(exchange, node);
          return super.process(exchange, callback);
        }
      }).orElse(target) : target);
    manager.createInstance().select(RoutesBuilder.class).forEach(context::addRoutes);
    context.start();
  }
}

Conclusion

References

CDI Specification - cdi-spec.org
Slides sources - github.com/astefanutti/further-cdi
Metrics CDI sources - github.com/astefanutti/metrics-cdi
Camel CDI sources - github.com/astefanutti/camel-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