camel-cdi icon indicating copy to clipboard operation
camel-cdi copied to clipboard

:camel: CDI extension for Apache Camel

CDI Extension for Camel

Build Status Coverage Status Maven Central

CDI portable extension for Apache Camel compliant with JSR 346: Contexts and Dependency Injection for JavaTM EE 1.2.

About

Version 1.2.0 of this component has been merged into Apache Camel as part of CAMEL-9201. It now serves as an upstream project to explore possible evolution of the Camel CDI integration using the latest versions of the underlying technologies.

Hereafter is the original project statement:

Since version 2.10 of Camel, the Camel CDI component supports the integration of Camel in CDI enabled environments. However, some experiments and battlefield tests prove it troublesome to use because of the following concerns:

  • It relies on older CDI 1.0 version of the specification and makes incorrect usages of container lifecycle events w.r.t. Assignability of type variables, raw and parameterized types. As a consequence, it does not work properly with newer implementation versions like Weld 2.x and containers like WildFly 8.x as reported in CAMEL-7760 among other issues.
  • It relies on Apache DeltaSpike and its BeanManagerProvider class to retrieve the BeanManager instance during the CDI container initialisation. That may not be suitable in complex container configurations, for example, in multiple CDI containers per JVM context, as reported in CAMEL-6338 and that causes CAMEL-6095 and CAMEL-6937.
  • It relies on DeltaSpike and its configuration mechanism to source configuration locations for the Properties component. While this is suitable for most use cases, it relies on the ServiceLoader mechanism to support custom configuration sources that may not be suitable in more complex container configurations and relates to CAMEL-5986.
  • Besides, while DeltaSpike is a valuable addition to the CDI ecosystem, Camel CDI having a direct dependency on it is questionable from a design standpoint as opposed to relying on standard Camel mechanism for producing the Camel Properties component and delegating, as a plugable strategy, the configuration sourcing concern and implementation choice to the application itself or eventually using the Java EE Configuration JSR when available.
  • It declares a CamelContext CDI bean that's automatically instantiated and started with a @PostConstruct lifecycle callback called before the CDI container is completely initialized. That prevents, among other side effects like CAMEL-9336, proper configuration of the Camel context as reported in CAMEL-8325 and advising of Camel routes as documented in Camel AdviceWith.
  • It uses the @ContextName annotation to bind routes to the CamelContext instance specified by name as an attempt to provide support for multiple Camel contexts per application. However, that is an incomplete feature from the CDI programming model standpoint as discussed in CAMEL-5566 and that causes CAMEL-5742.

The objective of this project is to alleviate all these concerns, provide additional features, and have that improved version of the Camel CDI component contributed back into the official codeline.

Getting Started

Using Maven

Add the camel-cdi library as a dependency:

<dependency>
    <groupId>io.astefanutti.camel.cdi</groupId>
    <artifactId>camel-cdi</artifactId>
    <version>1.2.0</version>
</dependency>

Required Dependencies

Besides depending on Camel (camel-core and camel-core-osgi optionally), Camel CDI requires a CDI enabled environment running in Java 8 or greater.

Supported Containers

This version of Camel CDI is currently successfully tested with the following containers:

Container Version Environment
Weld 2.4.0.Final Java SE 8 / CDI 1.2
OpenWebBeans 1.7.0 Java SE 8 / CDI 1.2
WildFly 8 8.2.1.Final Java EE 7
WildFly 9 9.0.2.Final Java EE 7
WildFly 10 10.1.0.Final Java EE 7
WildFly Camel 4.3.0 Java EE 7
Karaf
PAX CDI Weld
4.0.4
1.0.0.RC1
OSGi 6

WildFly 8.1 requires to be patched with Weld 2.2+ as documented in Weld 2.2 on WildFly.

Usage

CDI Event Camel Endpoint

The CDI event endpoint bridges the CDI events facility with the Camel routes so that CDI events can be seamlessly observed / consumed (respectively produced / fired) from Camel consumers (respectively by Camel producers).

The CdiEventEndpoint<T> bean can be used to observe / consume CDI events whose event type is T, for example:

@Inject
CdiEventEndpoint<String> cdiEventEndpoint;

from(cdiEventEndpoint).log("CDI event received: ${body}");

This is equivalent to writing:

@Inject
@Uri("direct:event")
ProducerTemplate producer;

void observeCdiEvents(@Observes String event) {
    producer.sendBody(event);
}

from("direct:event").log("CDI event received: ${body}");

Conversely, the CdiEventEndpoint<T> bean can be used to produce / fire CDI events whose event type is T, for example:

@Inject
CdiEventEndpoint<String> cdiEventEndpoint;

from("direct:event").to(cdiEventEndpoint).log("CDI event sent: ${body}");

This is equivalent to writing:

@Inject
Event<String> event;

from("direct:event").process(new Processor() {
    @Override
    public void process(Exchange exchange) {
        event.fire(exchange.getBody(String.class));
    }
}).log("CDI event sent: ${body}");

Or using a Java 8 lambda expression:

@Inject
Event<String> event;

from("direct:event")
    .process(exchange -> event.fire(exchange.getIn().getBody(String.class)))
    .log("CDI event sent: ${body}");

The type variable T, respectively the qualifiers, of a particular CdiEventEndpoint<T> injection point are automatically translated into the parameterized event type, respectively into the event qualifiers, e.g.:

@Inject
@FooQualifier
CdiEventEndpoint<List<String>> cdiEventEndpoint;

from("direct:event").to(cdiEventEndpoint);

void observeCdiEvents(@Observes @FooQualifier List<String> event) {
    logger.info("CDI event: {}", event);
}

When multiple Camel contexts exist in the CDI container, the @ContextName qualifier can be used to qualify the CdiEventEndpoint<T> injection points, e.g.:

@Inject
@ContextName("foo")
CdiEventEndpoint<List<String>> cdiEventEndpoint;
// Only observes / consumes events having the @ContextName("foo") qualifier
from(cdiEventEndpoint).log("Camel context 'foo' > CDI event received: ${body}");
// Produces / fires events with the @ContextName("foo") qualifier
from("...").to(cdiEventEndpoint);

void observeCdiEvents(@Observes @ContextName("foo") List<String> event) {
    logger.info("Camel context 'foo' > CDI event: {}", event);
}

Note that the CDI event Camel endpoint dynamically adds an observer method for each unique combination of event type and event qualifiers and solely relies on the container typesafe observer resolution, which leads to an implementation as efficient as possible.

Besides, as the impedance between the typesafe nature of CDI and the dynamic nature of the Camel component model is quite high, it is not possible to create an instance of the CDI event Camel endpoint via URIs. Indeed, the URI format for the CDI event component is:

cdi-event://PayloadType<T1,...,Tn>[?qualifiers=QualifierType1[,...[,QualifierTypeN]...]]

With the authority PayloadType (respectively the QualifierType) being the URI escaped fully qualified name of the payload (respectively qualifier) raw type followed by the type parameters section delimited by angle brackets for payload parameterized type. Which leads to unfriendly URIs, e.g.:

cdi-event://org.apache.camel.cdi.se.pojo.EventPayload%3Cjava.lang.Integer%3E?qualifiers=org.apache.camel.cdi.se.qualifier.FooQualifier%2Corg.apache.camel.cdi.se.qualifier.BarQualifier

But more fundamentally, that would prevent efficient binding between the endpoint instances and the observer methods as the CDI container doesn't have any ways of discovering the Camel context model during the deployment phase.

Camel Events to CDI Events

Camel provides a set of management events that can be subscribed to for listening to Camel context, service, route and exchange events. This version of Camel CDI seamlessly translates these Camel events into CDI events that can be observed using CDI observer methods, e.g.:

void onContextStarting(@Observes CamelContextStartingEvent event) {
    // Called before the default Camel context is about to start
}

When multiple Camel contexts exist in the CDI container, the @ContextName qualifier can be used to refine the observer method resolution to a particular Camel context as specified in observer resolution, e.g.:

void onRouteStarted(@Observes @ContextName("first") RouteStartedEvent event) {
    // Called after the route (event.getRoute()) for the
    // Camel context ("first") has started
}

Similarly, the @Default qualifier can be used to observe Camel events for the default Camel context if multiples contexts exist, e.g.:

void onExchangeCompleted(@Observes @Default ExchangeCompletedEvent event) {
    // Called after the exchange (event.getExchange()) processing has completed
}

In that example, if no qualifier is specified, the @Any qualifier is implicitly assumed, so that corresponding events for all the Camel contexts deployed get received.

Note that the support for Camel events translation into CDI events is only activated if observer methods listening for Camel events are detected in the deployment, and that per Camel context.

Type Converter Beans

CDI beans annotated with the @Converter annotation are automatically registered into all the deployed Camel contexts, e.g.:

@Converter
public class TypeConverter {

    @Converter
    public Output convert(Input input) {
        //...
    }
}

Note that CDI injection is supported within the type converters.

Multiple Camel Contexts

The @ContextName qualifier can be used to declared multiple Camel contexts, e.g.:

@ApplicationScoped
@ContextName("foo")
class FooCamelContext extends DefaultCamelContext {

}

@ApplicationScoped
@ContextName("bar")
class BarCamelContext extends DefaultCamelContext {

}

And then use that same qualifier to declare injected fields, e.g.:

@Inject
@ContextName("foo")
CamelContext fooCamelContext;

@Inject
@ContextName("bar")
CamelContext fooCamelContext;

Note that Camel CDI provides the @ContextName qualifier for convenience though any CDI qualifiers can be used to declare the Camel context beans and the injection points.

Configuration Properties

Camel CDI relies on standard Camel abstractions and CDI mechanisms. The configuration sourcing concern is delegated to the application so that it can provide any PropertiesComponent bean that's tailored for its need, e.g.:

@Produces
@ApplicationScoped
@Named("properties")
PropertiesComponent propertiesComponent() {
    Properties properties = new Properties();
    properties.put("property", "value");
    PropertiesComponent component = new PropertiesComponent();
    component.setInitialProperties(properties);
    component.setLocation("classpath:placeholder.properties");
    return component;
}

Camel Context Customization

Any CamelContext class can be used to declare a custom Camel context bean that uses the @PostConstruct and @PreDestroy lifecycle callbacks, e.g.:

@ApplicationScoped
class CustomCamelContext extends DefaultCamelContext {

    @PostConstruct
    void customize() {
        // Sets the Camel context name
        setName("custom");
        // Adds properties location
        getComponent("properties", PropertiesComponent.class)
            .setLocation("classpath:placeholder.properties");
    }

    @PreDestroy
    void cleanUp() {
        // ...
    }
}

Producer and disposer methods can be used as well to customize the Camel context bean, e.g.:

class CamelContextFactory {

    @Produces
    @ApplicationScoped
    CamelContext customize() {
        DefaultCamelContext context = new DefaultCamelContext();
        context.setName("custom");
        return context;
    }

    void cleanUp(@Disposes CamelContext context) {
        // ...
    }
}

Similarly, producer fields can be used, e.g.:

@Produces
@ApplicationScoped
CamelContext context = new CustomCamelContext();

class CustomCamelContext extends DefaultCamelContext {

    CustomCamelContext() {
        setName("custom");
    }
}

This pattern can be used to avoid having the Camel context started automatically at deployment time by calling the setAutoStartup method, e.g.:

@ApplicationScoped
class ManualStartupCamelContext extends DefaultCamelContext {

    @PostConstruct
    void manual() {
        setAutoStartup(false);
    }
}

Camel Route Builders

Camel CDI detects any beans of type RouteBuilder and automatically adds the declared routes to the corresponding Camel context at deployment time, e.g.:

@ContextName("foo")
class FooCamelContextRoute extends RouteBuilder {

    @Override
    public void configure() {
        from("direct:inbound")
            .setHeader("context").constant("foo")
            .to("mock:outbound");
    }
}

Camel CDI Beans

Camel CDI declares some producer method beans that can be used to inject Camel objects of types Endpoint, MockEndpoint, ProducerTemplate and TypeConverter, e.g.:

@Inject
@Uri("direct:inbound")
ProducerTemplate producerTemplate;

@Inject
MockEndpoint outbound; // URI defaults to the member name, i.e. mock:outbound

@Inject
@Uri("direct:inbound")
Endpoint endpoint;

@Inject
@ContextName("foo")
TypeConverter converter;

Camel Annotations Support

Camel comes with a set of annotations that are supported by Camel CDI for both standard CDI injection and Camel bean integration, e.g.:

@PropertyInject("property")
String property;

@Produce(uri = "mock:outbound")
ProducerTemplate producer;

// Equivalent to:
// @Inject @Uri("direct:inbound")
// Endpoint endpoint;
@EndpointInject(uri = "direct:inbound")
Endpoint endpoint;

// Equivalent to:
// @Inject @ContextName("foo") @Uri("direct:inbound")
// Endpoint contextEndpoint;
@EndpointInject(uri = "direct:inbound", context = "foo")
Endpoint contextEndpoint;

// Equivalent to:
// @Inject MyBean bean;
@BeanInject
MyBean bean;

@Consume(uri = "seda:inbound")
void consume(@Body String body) {
    //...
}

Black Box Camel Contexts

The context component enables the creation of Camel components out of Camel contexts and the mapping of local endpoints within these components from other Camel contexts based on the identifiers used to register these black box Camel contexts in the Camel registry.

For example, given the two Camel contexts declared as CDI beans:

@ApplicationScoped
@Named("blackbox")
@ContextName("foo")
class FooCamelContext extends DefaultCamelContext {

}
@ApplicationScoped
@ContextName("bar")
class BarCamelContext extends DefaultCamelContext {

}

With the foo Camel context being registered into the Camel registry as blackbox by annotating it with the @Named("blackbox") qualifier, and the following route being added to it:

@ContextName("foo")
FooRouteBuilder extends RouteBuilder {

    @Override
    public void configure() {
        from("direct:in")/*...*/.to("direct:out");
    }
}

It is possible to refer to the local endpoints of foo from the bar Camel context route:

@ContextName("bar")
BarRouteBuilder extends RouteBuilder {

    @Override
    public void configure() {
        from("...").to("blackbox:in");
        //...
        from("blackbox:out").to("...");
    }
}

License

Copyright © 2014-2016, Antonin Stefanutti

Published under Apache Software License 2.0, see LICENSE