- Senior Software Engineer
- CDI co-spec lead, Java EE 8 EG
- Red Hat, Inc.
- @antoine_sd
- www.next-presso.com
- github.com/antoinesd
CDI as the productivity ecosystem to build connectivity interfaces |
If you know the most of these you can stay |
@Inject
@Produces
Event<T>
@Observes
@Qualifier
InjectionPoint
What’s included: |
What’s not included: |
Apache Deltaspike
Arquillian
weld-ee-embedded
containerSlides available at astefanutti.github.io/further-cdi |
==!
Type meta-model |
CDI meta-model |
CDI entry points |
SPI dedicated to extensions |
Because @Annotations are configuration |
but they are also read-only |
So to configure we need a mutable meta-model… |
…for annotated types |
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
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)
}
}
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
}
}
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")
}
}
These events fired at boot time can only be observed in a CDI extension |
For instance: |
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() |
One of the most powerful feature of the CDI specification |
Not really popularized, partly due to: |
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 |
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 |
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) {
}
}
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();
}
}
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
How to modernize a legacy component with the CDI Programming Model
Transformation engine used at Murex to transform message formats from external systems into Murex canonical format |
fpml2mxml.jar
that we want to reuse as is@Formula
class Fpml2mxmlTransformation {
public MxML transformation(FpML fpml) { /* transformation logic */ }
}
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();
}
}
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);
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
We need to write an extension that will: |
@Formula(…)
, required to instantiate the legacy component by observing the ProcessInjectionPoint
lifecycle eventBean
SPI and observing the AfterBeanDiscovery
lifecycle eventTransformer
facade instances and use the legacy component CDI bean@Observes
these 2 events to add our featuresDefine 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
}
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;
...
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);
}
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)
}
}
1 | Observe every injection point of type Transformer on any declaring bean |
2 | Collect the injection point @Formula path attribute |
Why do we need to add a bean programmatically?
The legacy component is not a bean archive:
beans.xml
(explicit bean archive)We cannot touch the component source / binary because we don’t own it
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();
}
Bean
SPIclass 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();
}
...
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;
}
}
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);
}
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));
}
}
Transformer
facadeFinally 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. |
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);
How to integrate a 3rd party Library (Dropwizard Metrics) into the CDI Programming Model
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 |
More at dropwizard.github.io/metrics |
Discover how we created CDI integration module for Metrics
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();}
}
}
1 | Note that if "timer" Timer doesn’t exist, MetricRegistry will create a default one and register it |
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 |
@Timed("timer") (1)
void timedMethod() {
...
}
@Produces @Metric(name="myTimer") (1)
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, TimeUnit.MINUTES));
...
@Timer("myTimer") (1)
void timedMethod() { ... }
1 | Annotations provided by Metrics |
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 |
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(); }
}
}
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(); }
}
}
1 | In CDI an interceptor is a bean: you can inject other beans in it |
2 | Here the "business" of the application is called. All the code around is the technical one |
@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(); }
}
}
1 | Giving 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. |
@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(); }
}
}
1 | We’ll use metrics @Timed annotation as interceptor binding |
An interceptor binding is an annotation used in 2 kind of places: |
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: |
@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)
}
1 | Lack of @InterceptorBinding annotation and we have no code to add it programmatically. |
2 | None 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) |
@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
?
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();
}
1 | This creates an instance of @NonBinding annotation |
2 | It 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 |
@Timed
to the list of interceptor binding with an extensionBy 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);
}
1 | This method is the one we need to use for our @Timed AnnotatedType |
BeforeBeanDiscovery
is first in lifecycleclass 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();
}
}
We can now write: |
@Timed("timer")
void timedMethod(){
//Business code
}
And have a Metrics Timer
applied to the method
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();}
}
1 | The 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. |
We want to write this: |
@Produces @Metric(name="myTimer")
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, TimeUnit.MINUTES));
And have: |
Metric
produced when needed (first use)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 |
We need to write an extension that will: |
@Metric
as a new Qualifier to ease injection and name resolution in a BeforeBeanDiscovery
observerProcessProducer
lifecycle eventProducer
to add this new behaviour.Metric
at the end of boot time to have them in registry for runtimeAfterDeploymentValidation
event@Observes
these 3 events to add our features@Metric
to the list of qualifiersThis 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);
}
...
}
Metric
producing processWe 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)
}
...
1 | BeanProvider is a DeltaSpike helper class to easily retrieve a bean or bean instance |
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();
}
}
1 | We don’t want to have the produced metric destroyed by CDI container |
Producer<Metric>
in a ProcessProducer
observerThru 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);
}
1 | Used to retrieve annotations associated to @Produces |
2 | Get the default producer (useful to decorate it) |
3 | Change the producer by ours |
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));
}
...
}
Metric
at the end of boot timeWe 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)
}
...
}
1 | Getting all the Metric beans |
2 | Retrieving its name from bean qualifiers |
3 | Requesting an instance that will use our custom producer and thus will fill the registry |
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 ) |
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);
}
}
}
How to use CDI as dependency injection container for an integration framework (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 |
Discover how we created CDI integration module for Camel
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();
}
}
1 | This route watch a directory every second and send new files content to a JMS queue |
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;
}
}
@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 |
CamelContext
manually.to("sjms:queue:outputDest"); // Lookup by name (sjms) and type (Component)
@PropertyInject(value = "jms.queue.maxConnections", defaultValue = "10")
Integer maxConnections;
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 |
We need to write an extension that will: |
CamelContext
bean if no such bean is already deployed by observing the AfterBeanDiscovery
lifecycle eventRouteBuilder
and add them to the Camel contextAfterDeploymentValidation
event is fired (resp. the BeforeShutdown
event)BeanManager
to lookup CDI beans by name and typeProcessAnnotatedType
and modify how they get injected by observing the ProcessInjectionTarget
lifecycle event@Observes
these 5 events to add our featuresCamelContext
beanAutomatically 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
}
}
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();
}
Bean
SPIclass 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) {
}
...
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;
}
}
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 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));
}
}
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();
}
...
}
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();
}
}
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 |
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));
}
}
CdiRegistry
to the Camel contextpublic 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();
}
...
}
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;
}
}
…
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();
}
}
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?
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 |
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);
}
...
}
1 | Call the Camel default bean post-processor after CDI injection |
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()));
}
}
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()));
}
}
1 | Detect all the types containing Camel annotations with @WithAnnotations |
2 | Decorate the InjectionTarget corresponding to these types with a custom post-processor |
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;
}
}
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();
}
}
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 |