CDI portable extension for Apache Camel compliant with JSR 346: Contexts and Dependency Injection for JavaTM EE 1.2.
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 theBeanManager
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 theCamelContext
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.
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>
Besides depending on Camel (camel-core
and camel-core-osgi
optionally), Camel CDI requires a CDI enabled environment running in Java 8 or greater.
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.
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 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.
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.
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.
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;
}
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 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 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 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) {
//...
}
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("...");
}
}
Copyright © 2014-2016, Antonin Stefanutti
Published under Apache Software License 2.0, see LICENSE