- Red Hat
- CDI spec lead
- @antoine_sd
- next-presso.com
- github.com/antoinesd
Apache Deltaspike
Arquillian
weld-se-embedded
and weld-ee-embedded
container
==!
CDI entry points |
Type meta-model |
CDI meta-model |
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 |
These events fired at boot time can only be observed in CDI extensions |
For instance: |
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() |
Read my SPI post: j.mp/spipost
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 existing configuration or behavior |
To extend CDI and Java EE |
Thanks to them, Java EE can evolve between major releases |
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 |
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) {
}
}
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();
}
}
Adding service in JPA entities with CDI
Not very used in Java EE (bad practice or difficult) |
Some platform use them without questioning |
CDI allows it so let’s test it ! |
Don’t do this at home without re-thinking your model and practices |
Discover how to use CDI to make your JPA entities smarter
@Entity
public class Person {
@Id
public Long getId() { ...}
public void setId(Long id) { ... }
public String getName() { ... }
public void setName(String name) { ... }
}
@Entity
public class Person {
@Id
public Long getId() { ...}
public void setId(Long id) { ... }
public String getName() { ... }
public void setName(String name) { ... }
public void persist() { ... }
public void remove() { ... }
public List<Person> getAllPersonWithMyName() { ... }
}
Make the entity a @Dependent cdi bean |
Define a service bean and inject it in the Entity |
Create a JPA entity listener to perform injection in loaded entities |
GOAL 1 Make the entity a @Dependent
bean
and inject a service in it
Fat entity should be dependent scope with no interceptor or decorator |
Otherwise CDI container will creat client proxy for it |
@Dependent (1)
@Entity
public class Person {
@Id
public Long getId() { ...}
public void setId(Long id) { ... }
public String getName() { ... }
public void setName(String name) { ... }
}
1 | Fat entity can only be dependent scope |
@Transactional
This bean can have a normal scope and support interceptors |
@ApplicationScoped
@Transactional
public class PersonService {
@PersistenceContext
EntityManager em;
public List<Person> getAll() { ... }
public void persist(Person person) { ... }
public void remove(Person person) { ... }
public List<Person> getPersonsWithName(String name) { ... }
public Person getPersonById(Long id) { ... }
}
@Dependent @Entity
public class Person {
@Inject
PersonService service;
...
public void persist() { service.persist(this);}
public void remove() { service.remove(this);}
@Transient (1)
public List<Person> getPersonWithMyName() {
return service.getPersonsWithName(name);
}
}
1 | required to avoid JPA taking this for a property to persist |
public class MyService {
@Inject
Instance<Person> instPers; (1)
Public void createPersonWithName(String name) {
Person p = instPers.get();
p.setName(name);
}
}
1 | Instance<> allows you to request new dependent bean instances in code |
Ok for new entities but what about entities loaded by EntityManager?
Goal 2 Create a JPA entity listener to perform injection in loaded entities
public class EntityInjector<T> {
private final InjectionTarget<T> injectionTarget; (1)
private final CreationalContext<T> ctx;
public EntityInjector(BeanManager beanManager, Class<T> clazz) {
AnnotatedType<T> type = beanManager.createAnnotatedType(clazz);
this.injectionTarget = beanManager.getInjectionTargetFactory(type).createInjectionTarget(null); (2)
this.ctx = beanManager.createCreationalContext(null); (2)
}
public T inject(T instance) {
injectionTarget.inject(instance, ctx);
injectionTarget.postConstruct(instance);
return instance;
}
}
1 | InjectionTarget is a CDI SPI class allowing you to perform injection in an unmanaged instance (A bean instance is a managed InjectionTarget). |
2 | As it is unmanaged, the context is empty |
public class ManagingListener {
@Inject (1)
BeanManager bm;
@PostLoad (2)
public void postLoad(Object entity) {
Class<?> clazz = entity.getClass();
EntityInjector manager = new EntityInjector(bm,clazz);
manager.inject(entity); (3)
}
}
1 | it supports injection (in fact it is a CDI unmanaged instance) |
2 | JPA annotation to perform action on loaded entities |
3 | Perform injection in the Entity |
@Dependent
@Entity
@EntityListeners({ ManagingListener.class })
public class Person {
....
}
Goal achieved. Our entity is now Fat |
When created or loaded with EntityManager |
Adding fire and forget event in CDI 1.2
We’d like to have the following bean method called asynchronously when an event of the matching type is fired |
@ApplicationScoped
public class MyBean {
...
public void myObserver(@AsyncObs MyEventType evt) { (1)
...
}
}
1 | @AsyncObs is a parameter annotation we introduce to add this feature |
create annotation @AsyncObs |
Define a new ObserverMethod implementation doing notification asynchronously |
Create an extension to detect @AsyncObs occurrences and add observer for them |
@AsyncObs
annotationIt has nothing special: |
@Target({PARAMETER})
@Retention(RUNTIME)
@Documented
public @interface AsyncObs {
}
public class AsyncObserverMethod<T> implements ObserverMethod<T> {
public static int invoked = 0;
private BeanManager bm;
private Class<?> beanClass;
private Method method;
private Class<T> observedType;
private Set<Annotation> observedQualifiers;
public AsyncObserverMethod(BeanManager bm, Class<?> bc, Method m, Class<T> ot, Set<Annotation> oq) {
this.bm = bm;
this.beanClass = bc;
this.method = m;
this.observedType = ot;
this.observedQualifiers = oq;
}
public Class<?> getBeanClass() { return beanClass; }
public Type getObservedType() { return observedType; }
public Set<Annotation> getObservedQualifiers() { return observedQualifiers; }
public Reception getReception() { return Reception.ALWAYS; } (1)
public TransactionPhase getTransactionPhase() { return TransactionPhase.IN_PROGRESS; } (1)
public void notify(T event) {
Bean bean = bm.resolve(bm.getBeans(beanClass));
Object beanInstance = bm.getReference(bean,beanClass,bm.createCreationalContext(bean));
CompletableFuture.runAsync(() -> { (2)
try {
method.invoke(beanInstance,event);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
});
invoked++;
}
}
1 | Hard coded behaviour to simplify the example |
2 | In true EE world runAsync should be called with a Managed Executor obtained with concurrent utilities API |
extension collect all classes having methods containing @AsyncObs … |
public class AsyncEventExtension implements Extension {
Map<Class<?>, Set<Method>> classWithAsyncObs = new HashMap<>();
public void processAsyncObs(@Observes @WithAnnotations(AsyncObs.class) ProcessAnnotatedType<?> pat) {
AnnotatedType<?> at = pat.getAnnotatedType();
classWithAsyncObs.put(at.getJavaClass(), at.getMethods()
.stream()
.filter(m -> m.getJavaMember().getParameters()[0].isAnnotationPresent(AsyncObs.class))
.map(AnnotatedMethod::getJavaMember)
.collect(Collectors.toSet()));
}
… and use this list to register AsyncObserverMethods if the type was eligible to be a bean |
public void addAsyncObservers(@Observes AfterBeanDiscovery abd, BeanManager bm) {
for (Class<?> clazz : classWithAsyncObs.keySet()) {
Set<Bean<?>> beans = bm.getBeans(clazz);
if (!beans.isEmpty()) {
for (Method m : classWithAsyncObs.get(clazz)) {
if (m.getParameterCount() != 1)
abd.addDefinitionError(new IllegalArgumentException("An Async Observer method can't have more than one arguments"));
else
abd.addObserverMethod(new AsyncObserverMethod(bm,clazz, m, m.getParameterTypes()[0], Collections.EMPTY_SET));
}
}
}
}
}
Goal achieved: we have now fire and forget event observers
Integrating Dropwizard Metrics in CDI
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
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 a Timer named "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 |
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
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)
1 | This timer that only keeps measurement of last minute is produced as a bean of type Timer |
2 | This timer that only keeps measurement of last hour is produced as a bean of type Timer |
3 | This injection point is ambiguous since 2 eligible beans exist |
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 |
@Metric
as a qualifier?By observing BeforeBeanDiscovery lifecycle event in an extension |
javax.enterprise.inject.spi.BeforeBeanDiscovery
public interface BeforeBeanDiscovery {
addQualifier(Class<? extends Annotation> qualifier); (1)
addQualifier(AnnotatedType<? extends Annotation> qualifier);
addScope(Class<? extends Annotation> scopeType, boolean normal, boolean passivation);
addStereotype(Class<? extends Annotation> stereotype, Annotation... stereotypeDef);
addInterceptorBinding(AnnotatedType<? extends Annotation> bindingType);
addInterceptorBinding(Class<? extends Annotation> bindingType, Annotation... bindingTypeDef);
addAnnotatedType(AnnotatedType<?> type);
addAnnotatedType(AnnotatedType<?> type, String id);
}
1 | The method we need to declare the @Metric annotation as a CDI qualifier |
And use addQualifier() method in the event |
BeforeBeanDiscovery
is first in lifecycleA 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
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
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 |
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 |
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();
}
}
}
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();
}
}
}
1 | In CDI an interceptor is a bean, you can inject other beans in it |
2 | Here the business code of the application is called. All the code around is the technical code. |
@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();
}
}
}
1 | Giving a @Priority to an interceptor activates and orders it |
@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();
}
}
}
1 | We’ll use Metrics @Timed annotation as interceptor binding |
An interceptor binding is an annotation used in 2 places: |
An interceptor binding should have the @InterceptorBinding annotation or should be declared programmatically |
If the interceptor binding annotation has members: |
@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)
}
1 | Lack the @InterceptorBinding annotation |
2 | None of the members have the @NonBinding annotation, so @Timed(name = "timer1") and @Timed(name = "timer2") will be 2 different interceptor bindings |
@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!
AnnotatedType
SPIThanks 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 from the Metrics @Timed annotation. |
3 | @NonBinding is added to both members of the @Timed annotation |
We observe BeforeBeanDiscovery to add a new interceptor binding |
public class MetricsExtension implements Extension {
void addTimedBinding(@Observes BeforeBeanDiscovery bdd) throws Exception {
Annotation nonBinding = new AnnotationLiteral<Nonbinding>() {};
bdd.addInterceptorBinding(new AnnotatedTypeBuilder<Timed>()
.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
@Timed
on classes@Timed("timer") (1)
void timedMethod() {
// Business code
}
@Produces
@Metric(name = "my_timer") (1)
Timer timer = new Timer(new SlidingTimeWindowReservoir(1L, MINUTES));
// ...
@Timed("my_timer") (1)
void timedMethod() { /*...*/ }
1 | Annotations provided by Metrics |
GOAL 3 Access same Metric
instances through @Inject
or MetricRegistry
API
When writing: |
@Inject
@Metric(name = "my_timer")
Timer timer1;
@Inject
MetricRegistry registry;
Timer timer2 = registry.timer("my_timer");
… We want that timer1 == timer2 |
@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)
1 | Produced Timer should be added to the Metrics registry when produced |
2 | When 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 |
We need to write an extension that will: |
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:ProcessProducer
lifecycle eventProducer
to add this new behaviorMetric
instances at the end of bootstrap to have them in registry for runtimeAfterDeploymentValidation
event@Observes
these 2 events to add our featuresMetric
producing processWe first need to create our implementation of the Producer<X> SPI |
class MetricProducer<X extends Metric> implements Producer<X> {
final Producer<X> decorate;
final String metricName;
MetricProducer(Producer<X> decorate, String metricName) {
this.decorate = decorate;
this.metricName = metricName;
}
// ...
Metric
producing process (continued) public X produce(CreationalContext<X> ctx) { (1)
MetricRegistry reg = getContextualReference(MetricRegistry.class, false); (2)
if (!reg.getMetrics().containsKey(metricName)) (3)
reg.register(metricName, decorate.produce(ctx));
return (X) reg.getMetrics().get(metricName);
}
public void dispose(X instance) {}
public Set<InjectionPoint> getInjectionPoints() {
return decorate.getInjectionPoints();
}
}
1 | The produce method is used by the container at runtime to decorate declared producer with our logic |
2 | BeanProvider.getContextualReference is helper class from DeltaSpike to easily retrieve a bean or bean instance |
3 | If metric name is not in the registry, the original producer is called and its result is added to the registry |
MetricProducer
in a ProcessProducer
observerThis event allow us to substitute the original producer with ours |
javax.enterprise.inject.spi.ProcessProducer
public interface ProcessProducer<T, X> {
AnnotatedMember<T> getAnnotatedMember(); (1)
Producer<X> getProducer(); (2)
void setProducer(Producer<X> producer); (3)
void addDefinitionError(Throwable t);
}
1 | Gets the AnnotatedMember associated to the @Produces field or method |
2 | Gets the default producer (useful to decorate it) |
3 | Overrides the producer |
Metric
producing process (end)Here’s the extension code to do this producer decoration |
public class MetricsExtension implements Extension {
// ...
<X extends Metric> void decorateMetricProducer(@Observes ProcessProducer<?, X> pp) {
String name = pp.getAnnotatedMember().getAnnotation(Metric.class).name(); (1)
new pp.setProducer(new MetricProducer<>(pp.getProducer(), name)); (2)
}
// ...
}
1 | We retrieve metric’s name by calling the name() member from @Metric |
2 | We replace the original producer by our producer (which decorates the former) |
Metric
instances at the end of boot timeWe do that by observing the AfterDeploymentValidation event |
public class MetricsExtension implements Extension {
// ...
void registerProducedMetrics(@Observes AfterDeploymentValidation adv) {
BeanProvider.getContextualReferences(com.codahale.metrics.Metric.class, true); (1)
}
// ...
}
1 | BeanProvider.getContextualReferences() is a method from DeltaSpike BeanProvider helper class.It creates the list of bean instances for a given bean type (ignoring qualifiers). |
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 |
public class MetricsExtension implements Extension {
void addMetricAsQualifier(@Observes BeforeBeanDiscovery bdd) {
bdd.addQualifier(Metric.class);
}
void addTimedBinding(@Observes BeforeBeanDiscovery bdd) throws Exception {
Annotation nonBinding = new AnnotationLiteral<Nonbinding>() {};
bdd.addInterceptorBinding(new AnnotatedTypeBuilder<Timed>().readFromType(Timed.class)
.addToMethod(Timed.class.getMethod("name"), nonBinding)
.addToMethod(Timed.class.getMethod("absolute"),nonBinding).create());
}
<T extends com.codahale.metrics.Metric> void decorateMetricProducer(@Observes ProcessProducer<?, T> pp) {
if (pp.getAnnotatedMember().isAnnotationPresent(Metric.class)) {
String name = pp.getAnnotatedMember().getAnnotation(Metric.class).name();
pp.setProducer(new MetricProducer(pp.getProducer(), name));
}
}
void registerProducedMetrics(@Observes AfterDeploymentValidation adv) {
BeanProvider.getContextualReferences(com.codahale.metrics.Metric.class, true);
}
}
CDI Specification - cdi-spec.org |
Code Repository - github.com/antoinesd/mute-javaee-with-cdi |
Slides generated with Asciidoctor, PlantUML and DZSlides backend |
Original slide template - Dan Allen & Sarah White |