Build your custom plugins for your enterprise java applications with jakarta ee CDI

One fundamental feature of CDI is the integration of third-party libraries and frameworks into the CDI container of choice, providing the full benefits of CDI.

In this article we’ll showcase how you can integrate your own custom or any third-party Java library into the CDI container, thus leveraging the power and behaviour of CDI, to provide new functionality to your application, by implementing your own CDI Portable extension.

This article references the CDI 4.0 specification, leveraging the CDI Full scope. It will exclude the CDI Build Compatible Extension (introduced in CDI 4.0) entirely.

What is a Portable extension?

A CDI Portable extension is a mechanism to provide additional features and functionalities to be integrated into the CDI container. It allows developers to customise and extend the CDI container’s behaviour by observing and modifying its metadata during the application startup process. It essentially provides a way to hook into CDI’s core functionality and add new features, integrate with other technologies, or alter the way CDI handles beans.

Getting Started with CDI Portable extension

1. Implement the Extension interface.

To write a CDI extension, you must implement the jakarta.enterprise.inject.spi.Extension interface and register it using the ServiceLoader mechanism.

2. Register your extension:

We register our extension as a service provider by creating a file named META-INF/services/jakarta.enterprise.inject.spi.Extension (in that exact location), which contains the name of our extension class. For LangChain4J, our portable extension’s fully-qualified class name is added to the extension file:

dev.langchain4j.cdi.core.portableextension.LangChain4JAIServicePortableExtension

This allows you to observe container lifecycle events, add new beans, and modify existing ones.

The jakarta.enterprise.inject.spi.Extension interface is a marker interface (meaning it has no defined methods) but it is required to fulfil the Java SE service provider architecture.

3. Observe the Extension lifecycle events.

To understand how to work with extensions, we must first understand the container application lifecycle and its lifecycle events.

At application bootstrap, the CDI containers starts by loading and instantiating each CDI extension. The container fires a series of lifecycle events that can be broken down into the following phases:

CDI Extension Lifecycle
  • CDI extensions leverage observer methods to react to various container lifecycle events.
  • These methods are provided with observer events, which are annotated with @Observes followed by a specific event object (e.g., BeforeBeanDiscovery, AfterBeanDiscovery, ProcessAnnotatedType).
  • Any other argument to these methods, e.g. an argument of type BeanManager, is considered an injection point and will be provided by the CDI container.

The method structure for each observer event has the following method signature:

//[Prototype observer method]
void methodDefinitionNameDescribingTheObserverEvent(@Observes CDIObserverEventIntf intf);

Sometimes, you may need to interface with BeanManager during the event observation (e.g. to create an AnnotatedType, see ProcessAnnotatedType example below). In that case, add the BeanManager as a method parameter:

//[Prototype observer method]
void methodDefinitionNameDescribingTheObserverEvent(@Observes CDIObserverEventIntf intf, BeanManager bm);

Now, let’s follow each of the CDI container lifecycle, and how you can leverage each of the event to build your CDI plugin.

3.1 Application initialisation lifecycle in CDI

When an application is started, the container performs the following steps:

  • First, the container finds the implementation of the jakarta.enterprise.inject.spi.Extension interface via the Java Service Loader. Then, as defined in Container lifecycle events, it will instantiate a single instance of each extension and register their observer methods for the events.
  • Next, the container must fire an event of type BeforeBeanDiscovery, as defined in BeforeBeanDiscovery event.
  • Next, the container must perform type discovery, as defined in Type discovery in CDI Full. Then, for every type discovered the container must create an AnnotatedType that represents it and fire an event of type ProcessAnnotatedType for it.
  • Next, the container must fire an event of type AfterTypeDiscovery, as defined in AfterTypeDiscovery event.
  • Next, the container must perform bean discovery, as defined in Bean discovery in CDI Full.
  • Next, the container must fire an event of type AfterBeanDiscovery, as defined in AfterBeanDiscovery event, and abort initialisation of the application if any observer registers a definition error.
  • Next, the container must detect deployment problems by validating bean dependencies. It should abort initialization of the application if any deployment problems exist, as defined in Problems detected automatically by the container.
  • Next, the container must fire an event of type AfterDeploymentValidation, as defined in AfterDeploymentValidation event, and abort initialization of the application if any observer registers a deployment problem.
  • Finally, the container begins directing requests to the application.

During the Bean discovery Phase, the following Bean discovery events are fired (some events may be fired multiple times):

  • ProcessAnnotatedType
  • ProcessInjectionPoint
  • ProcessInjectionTarget
  • ProcessBeanAttributes
  • ProcessBean/ProcessManagedBean/ProcessSessionBean/ProcessProducerField/ProcessProducerMethod/ProcessSynthenticBean
  • ProcessProducer
  • ProcessObserverMethod /ProcessSynthenticObserverMethod

3.2 The Types Discovery Phase

The goal of this phase is to create AnnotatedTypes that will be used for candidates to be elligible to become CDI beans.
The 3 events that are observed during this phase are:

  • BeforeBeanDiscovery event
  • ProcessAnnotatedType event
  • AfterTypeDiscovery event

The AnnotatedType can be registered manually or observed during the ProcessAnnotatedType event.

Let’s see how the Types Discovery phase, along with these observer events, works.

3.2.1 The BeforeBeanDiscovery event

The very first event that the CDI container fires is the BeforeBeanDiscovery event (of type jakarta.enterprise.inject.spi.BeforeBeanDiscovery). This event is fired before it begins the Type Discovery phase.
This event allows the addition of annotation types to be declared for a specific CDI annotation type.

public interface BeforeBeanDiscovery {
    public void addQualifier(Class<? extends Annotation> qualifier);
    public void addQualifier(AnnotatedType<? extends Annotation> qualifier);
    public void addScope(Class<? extends Annotation> scopeType, boolean normal, boolean passivating);
    public void addStereotype(Class<? extends Annotation> stereotype, Annotation... stereotypeDef);
    public void addInterceptorBinding(Class<? extends Annotation> bindingType, Annotation... bindingTypeDef);
    public void addInterceptorBinding(AnnotatedType<? extends Annotation> bindingType);
    public void addAnnotatedType(AnnotatedType<?> type, String id);
    
    // Since CDI 2.0
    public AnnotatedTypeConfigurator<?> addAnnotatedType(Class<T> type,String id);
    <T extends Annotation> AnnotatedTypeConfigurator<T> configureQualifier(Class<T> qualifier);
    <T extends Annotation> AnnotatedTypeConfigurator<T> configureInterceptorBinding(Class<T> bindingType);
}

For example, SmallRye Fault Tolerance registers its interceptors as AnnotatedType during the BeforeBeanDiscovery event:

void registerInterceptorBindings(@Observes BeforeBeanDiscovery bbd, BeanManager bm) {
	bd.addAnnotatedType(bm.createAnnotatedType(FaultToleranceInterceptor.class),FaultToleranceInterceptor.class.getName());
	// More code...
}

Since CDI 2.0, the AnnotatedTypeConfigurator SPI was introduced to easily configure an AnnotatedType. These AnnotatedType are added at the end of the event invocation.

3.2.2 Type scanning phase

After the first event the CDI container starts the process of type discovery. This entails scanning for classes and resources that qualify and are discoverable from your application classpath. The resources include bean archive files stored in JAR and/or WAR files. Bean archive files may contain the beans.xml file, which (when found) makes the archive a bean archive, eligible for bean discovery.

Since CDI 2.0, the CDI container has a default bean discovery mode of annotated. This means that only classes annotated with bean defining annotations will be discovered. You can override the default bean discovery by specifying the bean-discovery-mode attribute in beans.xml.

There are 3 bean discovery mode:

  • none: No classes will be discovered in the specified bean archive.
  • annotated (default): Only classes that are annotated with bean defining annotations will be discovered.
  • all: All classes will be discovered as CDI beans.

If no beans.xml exists in an archive, then the mode is annotated by default. Such an archive becomes an implicit bean archive, which means that it doesn’t contain beans.xml but classes annotated with bean defining annotations still become CDI beans.

3.2.3 Types Discovery Phase

After the scanning phase, the container creates an AnnotatedType for every discovered type. It, then, fires a ProcessAnnotatedType event, as defined in ProcessAnnotatedType event. Note that Java annotations are excluded on from this process.

public interface ProcessAnnotatedType<X> {
    public AnnotatedType<X> getAnnotatedType();
    public void setAnnotatedType(AnnotatedType<X> type);
    public AnnotatedTypeConfigurator<X> configureAnnotatedType(); // Since CDI 2.0
    public void veto();
}

AnnotatedType can be created during the BeforeBeanDiscovery or AfterTypeDiscovery event by calling their addAnnotatedType() method.

AnnotatedType is a parameterised type, which allows you to process annotated types of a specific type.

The annotation @WithAnnotations may be applied to the event parameter. If the annotation is applied, the container must only deliver ProcessAnnotatedType events for types which contain at least one of the annotations specified.

This event is often used to override the configuration of the existing type.
You can even force the CDI container to ignore this type by calling the veto() method.

Since CDI 2.0 and later versions offer AnnotatedTypeConfigurator for easier modification to the AnnotatedType that will replace the original one at the end of the observer invocation.

Example: In Langchain4J-CDI, we process all beans that are annotated with @RegisterAIService (using @WithAnnotations annotation) for bean detection. Only interfaces that are annotated are registered, while the other types are vetoed.

<T> void processAnnotatedType(@Observes @WithAnnotations({ RegisterAIService.class }) ProcessAnnotatedType<T> pat) {
	if (pat.getAnnotatedType().getJavaClass().isInterface()) {
		LOGGER.info("processAnnotatedType register " + pat.getAnnotatedType().getJavaClass().getName());
		detectedAIServicesDeclaredInterfaces.add(pat.getAnnotatedType().getJavaClass());
	} else {
		LOGGER.warn("processAnnotatedType reject " + pat.getAnnotatedType().getJavaClass().getName()
				+ " which is not an interface");
		pat.veto();
	}
}

3.2.4 The AfterTypeDiscovery event

This final event is fired after the Types Discovery Phase is fully completed, before the Bean Discovery Phase begins.

public interface AfterTypeDiscovery {
    public List<Class<?>> getAlternatives();
    public List<Class<?>> getInterceptors();
    public List<Class<?>> getDecorators();
    public void addAnnotatedType(AnnotatedType<?> type, String id);
    
    //Since CDI 2.0
    public AnnotatedTypeConfigurator<?> addAnnotatedType(Class<T> type,String id);
}

These methods provide list of beans, decorators and/or interceptors that were discovered. You can use these methods to see if your types are registered.

Theses lists are mutable, so new classes can be added (or existing classes can be removed by the CDI extension).

Like BeforeBeanDiscovery event, you can register custom AnnotatedType to the set of discovered AnnotatedType which will be scanned during bean discovery, with an identifier.

3.3 The Bean Discovery Phase

After the Type Discovery Phase, the discovered types undergo various events for validation, to check if they are eligible to become beans.
The container checks if any extension has vetoed the bean. If the bean was not vetoed, the container fires validation events. These events allow for bean validation and modification where applicable. This phase is considered as the bean discovery process.
After the bean discovery process has fully completed, validated and there are no definition errors, the bean and/or observer methods can be registered in the AfterBeanDiscovery event that must be fired by the container.
This phase ends when the container has found no validation and deployment errors, but before moving to the “Application running/request” phase, it is completed by firing the AfterDeploymentValidation phase.
Should an exception be provided during the AfterDeploymentValidation phase, the container will treat this exception as a deployment problem, and the registration of all beans and observers is terminated.

3.3.1 The ProcessInjectionPoint event.

The container fires this event when it encounters an injection point. An injection point is a place where a bean is injected. This event fires for every bean, decorator, and interceptor. It allows extensions to inspect and potentially modify the injection process.

public interface ProcessInjectionPoint<T, X> {
    public InjectionPoint getInjectionPoint();
    public void setInjectionPoint(InjectionPoint injectionPoint);
    public InjectionPointConfigurator configureInjectionPoint(); // From CDI 2.0
    public void addDefinitionError(Throwable t);
}

The event is a parameterised type allowing the observer to target a specific class T containing the injection point or a specific injection point type X.

This event allows you either work from a returned InjectionPoint or to customise and replace InjectionPoint.

Since CDI 2.0 and later versions offer InjectionPointConfigurator for easier modification to the InjectionPoint that will replace the original one at the end of the observer invocation.

Adding a definition error to addDefinitionError allows the extension to abort deployment.

For example, in LangChain4J-CDI, whenever a CDI bean contains an injection point of an AI service (when a bean @Inject an AI service):

@ApplicationScoped
@Path("/chat")
public class ChatResource {

    @Inject
    private ChatAiService aiService; //<-- trigger processInjectionPoints() Extension event.

	//REST resource methods ignore, for brevity...
}

The CDI container will fire LangChain4JAIServicePortableExtension.processInjectionPoints to register the AiService, if not already discovered:

void processInjectionPoints(@Observes ProcessInjectionPoint<?, ?> event) {
	if (event.getInjectionPoint().getBean() == null) {
		Class<?> rawType = Reflections.getRawType(event.getInjectionPoint().getType());
		if (classSatisfies(rawType, RegisterAIService.class))
			detectedAIServicesDeclaredInterfaces.add(rawType);
	}

	if (Instance.class.equals(Reflections.getRawType(event.getInjectionPoint().getType()))) {
		Class<?> parameterizedType = Reflections.getRawType(getFacadeType(event.getInjectionPoint()));
		if (classSatisfies(parameterizedType, RegisterAIService.class))
			detectedAIServicesDeclaredInterfaces.add(parameterizedType);
	}
} 

3.3.2 The ProcessInjectionTarget event

The container fires this event for Jakarta EE component classes that support injection. This includes classes that the container may instantiate at runtime. Examples include managed beans declared with @ManagedBean, EJB session beans or message-driven beans, enabled beans, enabled interceptors or enabled decorators.

By observing this event, extensions can decorate or replace the InjectionTarget, which is the core mechanism for injecting dependencies into a class.

The event is a parameterised type to target a specific base type, where X is the bean class.

Adding a definition error to addDefinitionError allows the extension to abort deployment.

public interface ProcessInjectionTarget<X> {
    public AnnotatedType<X> getAnnotatedType();
    public InjectionTarget<X> getInjectionTarget();
    public void setInjectionTarget(InjectionTarget<X> injectionTarget);
    public void addDefinitionError(Throwable t);
}

An example might be to modify the injection process by decorating or replacing the InjectionTarget.

public class MyCustomInjectionExtension implements Extension {

	<T> void processInjectionTarget(@Observes ProcessInjectionTarget<T> event) {
		InjectionTarget<T> originalInjectionTarget = event.getInjectionTarget();
		AnnotatedType<T> annotatedType = event.getAnnotatedType();

		// Create a decorator for the InjectionTarget
		InjectionTarget<T> decoratedInjectionTarget = new InjectionTargetDecorator<>(originalInjectionTarget, annotatedType);

		// Replace the original InjectionTarget with the decorated one
		event.setInjectionTarget(decoratedInjectionTarget);
	}

	// Inner class implementing the InjectionTargetDecorator
	static class InjectionTargetDecorator<T> implements InjectionTarget<T> {
		private final InjectionTarget<T> delegate;
		private final AnnotatedType<T> annotatedType;

		InjectionTargetDecorator(InjectionTarget<T> delegate, AnnotatedType<T> annotatedType) {
			this.delegate = delegate;
			this.annotatedType = annotatedType;
		}

		@Override
		public void inject(T instance, CreationalContext<T> ctx) {
			// Perform custom injection logic before delegate injection
			System.out.println("Custom injection logic before delegate injection for: " + annotatedType.getJavaClass().getSimpleName());
			delegate.inject(instance, ctx);
			// Perform custom injection logic after delegate injection
			System.out.println("Custom injection logic after delegate injection for: " + annotatedType.getJavaClass().getSimpleName());
		}

		// Implement the rest of the InjectionTarget methods (dispose, preDestroy, postConstruct, produce)

		// ... (Implement the other InjectionTarget methods)

	}
}

An example. If you have a RESTful Web service resource that injects an AI service,

@ApplicationScoped
@Path("/chat")
public class ChatResource {

    @Inject
    private ChatAiService aiService;

	//REST resource methods ignore, for brevity...
}

the processInjectionTarget method in the CDI extension will trigger the ProcessInjectionTarget<ChatResource> when ChatResource class is discovered.

3.3.3 The ProcessBeanAttributes event

The container fires this event for all managed beans, interceptors, and decorators. This happens before the bean (Bean<?>) is registered in the container. The event is fired for all beans, including Producer Fields, Producer Methods, Managed beans, Session beans and custom beans. This provides a way for extensions to influence bean metadata, such as adding qualifiers, changing scope, or even vetoing the bean entirely.

public interface ProcessBeanAttributes<T> {
    public Annotated getAnnotated();
    public BeanAttributes<T> getBeanAttributes();
    public void setBeanAttributes(BeanAttributes<T> beanAttributes);
    public BeanAttributesConfigurator<T> configureBeanAttributes(); // Since CDI 2.0
    public void addDefinitionError(Throwable t);
    public void veto();
    public void ignoreFinalMethods();
}

Since CDI 2.0 and later versions offer BeanAttributesConfigurator for easier modification. of the BeanAttributes , which will replace the original one at the end of the observer invocation. You can obtain a configurator through ProcessBeanAttributes.configureBeanAttributes().

To prevent a bean from being registered, use event.veto().

Example:

public class MyExtension implements Extension {

    <X> void processBeanAttributes(@Observes ProcessBeanAttributes<X> event) {
        // Your logic here to inspect or modify bean attributes
    }
}

  • The event is a parameterised type to target a specific base type, where X is the bean class, the return type of the producer method, or the type of the producer field.
  • getBeanAttributes() returns the BeanAttributes object that the container uses to manage instances of the bean.
  • setBeanAttributes() replaces the BeanAttributes.

Adding a definition error to addDefinitionError allows the extension to abort deployment.

3.3.4 The ProcessBean/ProcessManagedBean/ProcessSessionBean/ProcessProducerField/ProcessProducerMethod/ProcessSynthenticBean event

The container fires this event for each bean, interceptor, or decorator deployed in a bean archive. The timing is specific: it occurs after firing ProcessBeanAttributes for the bean but before registering the Bean object. It allows extensions to observe and potentially modify a bean’s lifecycle before it’s registered with the container.

public interface ProcessBean<X> {
    public Annotated getAnnotated();
    public Bean<X> getBean();
    public void addDefinitionError(Throwable t);
}

The event is a parameterised type to target a specific base type, where X is the bean class, the return type of the producer method, or the type of the producer field.

The event object type depends upon what kind of bean was discovered:

  • For a managed bean with bean class X, the container must raise an event of type ProcessManagedBean.
  • For a session bean with bean class X, the container must raise an event of type ProcessSessionBean.
  • For a producer method with method return type X of a bean with bean class T, the container must raise an event of type ProcessProducerMethod.
  • For a producer field with field type X of a bean with bean class T, the container must raise an event of type ProcessProducerField.
  • For a custom implementation of Bean, the container must raise an event of type ProcessSyntheticBean.

Adding a definition error to addDefinitionError allows the extension to abort deployment.

3.3.5 The ProcessProducer event

The container must fire this event for each producer (whether producer method and/or producer field) of each enabled bean, including resources.

public interface ProcessProducer<T, X> {
    public AnnotatedMember<T> getAnnotatedMember();
    public Producer<X> getProducer();
    public void setProducer(Producer<X> producer);
    public ProducerConfigurator<X> configureProducer(); // Since CDI 2.0
    public void addDefinitionError(Throwable t);
}

The event is a parameterised type to target a specific base type, whereT is the bean class of the bean containing the producer and X is the return type of the producer method or the type of the producer field.

  • getProducer() returns the Producer object that will be used by the container to call the producer method or read the producer field.
  • setProducer() replaces the Producer.

Since CDI 2.0 and later versions offer ProducerConfigurator for easier modification to the Producer, which will replace the original one at the end of the observer invocation.

Adding a definition error to addDefinitionError allows the extension to abort deployment.

Example:

<T, X> void processProducer(@Observes final ProcessProducer<T, X> pp) {
	LOGGER.info("ProcessProducer " + pp.getAnnotatedMember().getDeclaringType().getJavaClass().getSimpleName());
}

3.3.6 The ProcessObserverMethod /ProcessSynthenticObserverMethod event

The container must fire this event for each observer method of each enabled bean.

public interface ProcessObserverMethod<T, X> {
    public AnnotatedMethod<X> getAnnotatedMethod();
    public ObserverMethod<T> getObserverMethod();
    public void addDefinitionError(Throwable t);
    public void setObserverMethod(ObserverMethod<T> observerMethod);
    public ObserverMethodConfigurator<T> setObserverMethod(); // Since CDI 2.0
    public void veto();
}

For a custom implementation of ObserverMethod<T>, the container must raise an event of type ProcessSynthenticObserverMethod<T,X>.

The event is a parameterised type to target a specific base type, where T is the observed event type of the observer method and X is the bean class of the bean that declares the observer method.

The getAnnotatedMethod() returns the AnnotatedMethod representing the observer method (i.e., the method that @Observes the event). If invoked upon a ProcessSyntheticObserverMethod event, non-portable behaviour results and the returned value should be ignored.

Since CDI 2.0 and later versions offer ObserverMethodConfigurator for easier modification to the ObserverMethod, which will replace the original one at the end of the observer invocation.

To force the container to ignore the observer method ObserverMethod , use event.veto().

Adding a definition error to addDefinitionError, allows the extension to abort deployment after bean discovery is complete.

Example:

import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;

class Payment {
    private String type;
    private double amount;

    public Payment(String type, double amount) {
        this.type = type;
        this.amount = amount;
    }

    public String getType() {
        return type;
    }

    public double getAmount() {
        return amount;
    }
}

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD, PARAMETER})
@interface Credit {}

@Qualifier
@Retention(RUNTIME)
@Target({FIELD, TYPE, METHOD, PARAMETER})
@interface Debit {}

class PaymentEvent {
    private Payment payment;

    public PaymentEvent(Payment payment) {
        this.payment = payment;
    }

    public Payment getPayment() {
        return payment;
    }
}

class PaymentProcessor {
    @Inject
    jakarta.enterprise.event.Event<PaymentEvent> paymentEvent;

    public void processPayment(String type, double amount) {
        Payment payment = new Payment(type, amount);
        PaymentEvent event;

        if ("credit".equals(type)) {
            event = new PaymentEvent(payment);
            paymentEvent.select(Credit.class).fire(event);
        } else if ("debit".equals(type)) {
            event = new PaymentEvent(payment);
            paymentEvent.select(Debit.class).fire(event);
        }
    }
}

class CreditPaymentHandler {
    public void onCreditPayment(@Observes @Credit PaymentEvent event) {
        Payment payment = event.getPayment();
        System.out.println("Handling credit payment of " + payment.getAmount());
    }
}

class DebitPaymentHandler {
    public void onDebitPayment(@Observes @Debit PaymentEvent event) {
        Payment payment = event.getPayment();
        System.out.println("Handling debit payment of " + payment.getAmount());
    }
}

  1. Payment represents a payment object.
  2. PaymentEvent is a CDI event carrying the Payment object.
  3. @Credit and @Debit are custom qualifiers to differentiate between credit and debit payments.
  4. PaymentProcessor is responsible for firing events. It uses jakarta.enterprise.event.Event to fire events with the correct qualifiers.
  5. CreditPaymentHandler and DebitPaymentHandler are observer methods. The @Observes annotation is used to specify that these methods should be invoked when a PaymentEvent with the corresponding qualifier is fired.
  6. The onCreditPayment method is invoked when a PaymentEvent with the @Credit qualifier is fired.
  7. The onDebitPayment method is invoked when a PaymentEvent with the @Debit qualifier is fired.

Thus, to observe observer methods (as highlighted on point 5) using CDI extension:

private <T, X> void processObserverMethod(@Observes final ProcessObserverMethod<T, X> event) {
	LOGGER.info("ProcessProducer " + pp.getAnnotatedMember().getDeclaringType().getJavaClass().getSimpleName());
}

The event.getAnnotatedMethod() returns the AnnotatedMethod representing the observer method (for DebitPaymentHandler observer method, its AnnotatedMethod will point to onDebitPayment method, as it @Observes the PaymentEvent).
The event.getObserverMethod() returns the ObserverMethod object that will be used by the container to call the observer method. In our example, it can only be an ObserverMethod of CreditPaymentHandler or DebitPaymentHandler.

Essentially, this provides a way for extensions to wrap or modify their ObserverMethod, which will be used to replace the original one at the end of observer invocation.

3.3.7 The AfterBeanDiscovery event

The container fires this event after completing several steps. First, it fully completes the bean discovery process. Then, it validates that there are no definition errors for the discovered beans, registered Bean and ObserverMethod objects for the discovered beans.

public interface AfterBeanDiscovery {
    public void addDefinitionError(Throwable t);
    public void addBean(Bean<?> bean);
    public BeanConfigurator<?> addBean(); // Since CDI 2.0
    public void addObserverMethod(ObserverMethod<?> observerMethod);
    public ObserverMethodConfigurator<?> addObserverMethod(); // Since CDI 2.0
    public void addContext(Context context);
    public <T> AnnotatedType<T> getAnnotatedType(Class<T> type, String id);
    public <T> Iterable<AnnotatedType<T>> getAnnotatedTypes(Class<T> type);
}

A portable extension may take advantage of this event to register beans, interceptors, decorators, observer methods and custom context objects with the container.

void afterBeanDiscovery(@Observes AfterBeanDiscovery event, BeanManager manager) { ... }

The addBean() method will fire the ProcessSynthenticBean event, containing the given Bean, before registering the Bean with the container. The Bean interface (type jakarta.enterprise.inject.spi.Bean) has multiple implementation options. It can be implemented as an Interceptor (of interface jakarta.enterprise.inject.spijakarta.enterprise.inject.spi.Interceptor). It can also be implemented as a Decorator (of interface jakarta.enterprise.inject.spi.Decorator). When any of these interfaces are added via the addBean() method, they are considered synthetic beans.

Since CDI 2.0 and later versions offer BeanConfigurator for easier modification to the Bean that will be added at the end of the observer invocation.

The addObserverMethod() will fire the ProcessObserverMethod event containing the given ObserverMethod before registering the ObserverMethod with the container.

Since CDI 2.0 and later versions offer ObserverMethodConfigurator for easier modification to the ObserverMethod that will be added at the end of the observer invocation.

The addContext() registers a custom Context object with the container.

Adding a definition error to addDefinitionError, allows the extension to abort deployment after bean discovery is complete.

Example: In Langchain4J-CDI’s portable extension, all discovered AI service types (meaning, interfaces annotated with @RegisterAIService), the AI service proxy is registered as a Bean LangChain4JAIServiceBean for bean registration to the container.

void afterBeanDiscovery(@Observes AfterBeanDiscovery afterBeanDiscovery, BeanManager beanManager) throws ClassNotFoundException {
	for (Class<?> aiServiceClass : detectedAIServicesDeclaredInterfaces) {
		LOGGER.info("afterBeanDiscovery create synthetic:  " + aiServiceClass.getName());
		afterBeanDiscovery.addBean(new LangChain4JAIServiceBean<>(aiServiceClass, beanManager));
	}
}

3.3.8 The AfterDeploymentValidation event

This is the last event that is fired before creating contexts or processing requests (happening at the “Application running phase”). The container fires this event after it has validated that there are no deployment problems.

public interface AfterDeploymentValidation {
    public void addDeploymentProblem(Throwable t);
}

The addDeploymentProblem() registers a deployment problem with the container, causing the container to abort deployment after all observers have been notified.

Example:

void afterDeploymentValidation(@Observes AfterDeploymentValidation event, BeanManager manager) {
	//If there are any exceptions caught during any event, or through any validations, you can stop the full registration by calling `event.addDeploymentProblem(throwable)`.
}

Finally, the container must not allow any request to be processed by the deployment until all observers of this event return.

3.4 The Shutdown Phase

After the application finishes running all its requests, it reaches the shutdown stage.

3.4.1 The BeforeShutdown event

The container must fire a final event after it has finished processing requests and destroyed all contexts.

public  interface  BeforeShutdown {}

Example:

void beforeShutdown(@Observes BeforeShutdown event) {
 //Do some housekeeping. 
}

OR

void beforeShutdown(@Observes BeforeShutdown event, BeanManager manager) {
 //Do some housekeeping. 
}

If any observer method of the BeforeShutdown event throws an exception, the exception is ignored by the container.

4. Conclusion

The CDI Portable extension is a powerful feature that provides developers the ability to build frameworks on top of Jakarta EE. It may seem difficult at first, but once you understand the CDI lifecycle, this may be easier to build your own library.

Thank you to Ondro Mihályi (from OmniFish) for reviewing and contributing to this article.

Happy coding! ☕

Total
0
Shares
Previous Post

01-2026 | Agentic AI Meets Java

Next Post

Always Up to Date – with Every New Free PDF Edition!

Related Posts