dependencies {
annotationProcessor("org.eclipse.odi:micronaut-odi-processor-cdi:<version>")
implementation("org.eclipse.odi:micronaut-odi-cdi:<version>")
implementation("jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0")
}
ODI
CDI Lite implementation backed by Micronaut
Version: 1.0.0-SNAPSHOT
1 Introduction
ODI is a CDI Lite implementation backed by Micronaut’s compile-time dependency injection infrastructure.
The runtime uses CDI APIs while the build uses an annotation processor to generate Micronaut bean definitions and proxy classes. Applications keep the ODI processor on the annotation processor path and run with the ODI CDI runtime plus the Jakarta CDI API.
The implementation targets CDI Lite. CDI Full runtime extension points, decorators, and passivation are intentionally outside the runtime surface.
2 Setup
Add the CDI processor to the annotation processor path and keep the CDI runtime on the application classpath.
Build-compatible extension artifacts are compile-time inputs. Add extension libraries to the annotation processor path, and also to the implementation/runtime classpath when the application uses extension-provided annotations, creators, or runtime types.
Gradle:
Maven:
<dependencies>
<dependency>
<groupId>org.eclipse.odi</groupId>
<artifactId>micronaut-odi-cdi</artifactId>
<version>${odi.version}</version>
</dependency>
<dependency>
<groupId>jakarta.enterprise</groupId>
<artifactId>jakarta.enterprise.cdi-api</artifactId>
<version>4.1.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.eclipse.odi</groupId>
<artifactId>micronaut-odi-processor-cdi</artifactId>
<version>${odi.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
3 Quick Start
The Java example in docs-examples/cdi-lite-java demonstrates constructor injection, qualifiers, producer methods, programmatic lookup with Instance<T>, events, and CDI SE bootstrap.
Define a contract and a qualifier-specific implementation:
package org.eclipse.odi.docs.cdi;
/**
* Contract implemented by payment processor beans.
*/
public interface PaymentProcessor {
/**
* Charges a payment.
*
* @param payment payment request
* @return approved payment result
*/
PaymentResult charge(Payment payment);
}
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Qualifier used to select the credit-card implementation of {@link PaymentProcessor}.
*/
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface CreditCard {
/**
* Runtime literal used for programmatic lookup with {@code Instance.select(...)}.
*/
final class Literal extends AnnotationLiteral<CreditCard> implements CreditCard {
/**
* Singleton literal instance.
*/
public static final Literal INSTANCE = new Literal();
private static final long serialVersionUID = 1L;
private Literal() {
}
}
}
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.odi.docs.cdi.extension.PaymentGateway;
/**
* Credit-card payment processor selected by the CDI qualifier and the
* extension-defined gateway qualifier.
*/
@CreditCard
@PaymentGateway("credit-card")
@ApplicationScoped
public class CreditCardProcessor implements PaymentProcessor {
/**
* Creates the credit-card processor.
*/
public CreditCardProcessor() {
}
@Override
public PaymentResult charge(Payment payment) {
return new PaymentResult("cc-" + payment.accountId(), payment.cents());
}
}
Producer methods can supply values used by other beans:
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Named;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Produces shared payment resources and observes approved-payment events.
*/
@ApplicationScoped
public class PaymentResources {
private final AtomicInteger approvedPayments = new AtomicInteger();
/**
* Creates payment resources for the example.
*/
public PaymentResources() {
}
@Produces
@Named("currency")
String currencyCode() {
return "USD";
}
void onPaymentApproved(@Observes PaymentApproved event) {
approvedPayments.incrementAndGet();
}
/**
* Returns how many payment approval events this bean observed.
*
* @return number of observed payment approvals
*/
public int approvedPayments() {
return approvedPayments.get();
}
}
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.Locale;
/**
* Formats payment results using a produced currency code.
*/
@ApplicationScoped
public class ReceiptFormatter {
private final String currency;
/**
* Creates a receipt formatter.
*
* @param currency currency code produced by {@link PaymentResources}
*/
@Inject
public ReceiptFormatter(@Named("currency") String currency) {
this.currency = currency;
}
/**
* Formats a payment result as a receipt.
*
* @param result approved payment result
* @return formatted receipt
*/
public Receipt format(PaymentResult result) {
String total = String.format(Locale.ROOT, "%s %.2f", currency, result.cents() / 100.0);
return new Receipt(result.reference(), total);
}
}
Constructor injection, Instance<T> lookup, and CDI events work together in a normal application bean:
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Event;
import jakarta.enterprise.inject.Any;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
/**
* Application service that demonstrates constructor injection, programmatic
* lookup with {@link Instance}, and event publication.
*/
@ApplicationScoped
public class CheckoutService {
private final Instance<PaymentProcessor> processors;
private final ReceiptFormatter receiptFormatter;
private final Event<PaymentApproved> paymentApproved;
/**
* Creates the checkout service.
*
* @param processors all payment processors, queried by qualifier at runtime
* @param receiptFormatter formatter used to produce user-facing receipts
* @param paymentApproved event emitter for approved payments
*/
@Inject
public CheckoutService(
@Any Instance<PaymentProcessor> processors,
ReceiptFormatter receiptFormatter,
Event<PaymentApproved> paymentApproved) {
this.processors = processors;
this.receiptFormatter = receiptFormatter;
this.paymentApproved = paymentApproved;
}
/**
* Charges the account and returns a formatted receipt.
*
* @param accountId account identifier to charge
* @param cents amount to charge in cents
* @return formatted receipt for the approved payment
*/
public Receipt checkout(String accountId, int cents) {
Payment payment = new Payment(accountId, cents);
PaymentProcessor processor = processors.select(CreditCard.Literal.INSTANCE).get();
PaymentResult result = processor.charge(payment);
paymentApproved.fire(new PaymentApproved(result.reference(), result.cents()));
return receiptFormatter.format(result);
}
}
Start CDI SE with SeContainerInitializer and resolve the entry-point bean:
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.inject.se.SeContainer;
import jakarta.enterprise.inject.se.SeContainerInitializer;
/**
* Minimal CDI SE entry point for the documentation example.
*/
public final class CheckoutApplication {
private CheckoutApplication() {
}
/**
* Starts a CDI SE container, resolves the checkout service, and performs one payment.
*
* @param args command-line arguments, unused by this example
*/
public static void main(String[] args) {
try (SeContainer container = SeContainerInitializer.newInstance().initialize()) {
CheckoutService checkoutService = container.select(CheckoutService.class).get();
Receipt receipt = checkoutService.checkout("acct-100", 4999);
System.out.println(receipt);
}
}
}
4 Build-Time Extensions
CDI build-compatible extensions run during annotation processing. Make the extension artifact discoverable from META-INF/services/jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension on the annotation processor path. If application code also uses annotations, synthetic bean creators, or runtime types from the extension artifact, keep that artifact on the application classpath too.
Gradle:
dependencies {
annotationProcessor("org.eclipse.odi:micronaut-odi-processor-cdi:<version>")
annotationProcessor("com.example:payment-cdi-extension:<version>")
implementation("org.eclipse.odi:micronaut-odi-cdi:<version>")
implementation("com.example:payment-cdi-extension:<version>")
implementation("jakarta.enterprise:jakarta.enterprise.cdi-api:4.1.0")
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add("-Amicronaut.cdi.build.compatible.extensions=true")
}
Maven:
<dependencies>
<dependency>
<groupId>org.eclipse.odi</groupId>
<artifactId>micronaut-odi-cdi</artifactId>
<version>${odi.version}</version>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>payment-cdi-extension</artifactId>
<version>${payment.extension.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.eclipse.odi</groupId>
<artifactId>micronaut-odi-processor-cdi</artifactId>
<version>${odi.version}</version>
</path>
<path>
<groupId>com.example</groupId>
<artifactId>payment-cdi-extension</artifactId>
<version>${payment.extension.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amicronaut.cdi.build.compatible.extensions=true</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
The Java docs example keeps the extension in a separate Gradle module. The extension module contributes a marker annotation and a synthetic runtime bean:
package org.eclipse.odi.docs.cdi.extension;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marker annotation that the build-compatible extension registers as a CDI
* qualifier during the discovery phase.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
public @interface PaymentGateway {
/**
* Identifies the logical payment gateway.
*
* @return the logical gateway name
*/
String value();
}
package org.eclipse.odi.docs.cdi.extension;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.build.compatible.spi.Parameters;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticBeanCreator;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Synthetic bean produced by {@link PaymentBuildExtension}.
*
* <p>The bean is not declared by application source. Instead, the extension
* synthesizes it during annotation processing and passes the discovered gateway
* names to {@link Creator} as build-time parameters.</p>
*/
public final class PaymentCatalog {
private final Set<String> gateways;
private PaymentCatalog(Set<String> gateways) {
this.gateways = Collections.unmodifiableSet(new LinkedHashSet<>(gateways));
}
/**
* Returns the gateway names discovered by the build-compatible extension.
*
* @return the gateway names discovered by the build-compatible extension
*/
public Set<String> gateways() {
return gateways;
}
/**
* Checks whether a gateway was discovered by the extension.
*
* @param gateway gateway name to check
* @return whether the gateway was discovered at build time
*/
public boolean contains(String gateway) {
return gateways.contains(gateway);
}
/**
* Runtime creator used by ODI to instantiate the synthetic bean.
*/
public static final class Creator implements SyntheticBeanCreator<PaymentCatalog> {
/**
* Creates the synthetic bean creator.
*/
public Creator() {
}
@Override
public PaymentCatalog create(Instance<Object> lookup, Parameters params) {
String[] gatewayNames = params.get("gateways", String[].class);
return new PaymentCatalog(new LinkedHashSet<>(Arrays.asList(gatewayNames)));
}
}
}
The service descriptor contains the extension implementation class:
org.eclipse.odi.docs.cdi.extension.PaymentBuildExtension
The extension registers the marker annotation as a CDI qualifier during discovery, collects annotated payment gateway beans during registration, and synthesizes a PaymentCatalog bean:
package org.eclipse.odi.docs.cdi.extension;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.build.compatible.spi.BeanInfo;
import jakarta.enterprise.inject.build.compatible.spi.BuildCompatibleExtension;
import jakarta.enterprise.inject.build.compatible.spi.Discovery;
import jakarta.enterprise.inject.build.compatible.spi.MetaAnnotations;
import jakarta.enterprise.inject.build.compatible.spi.Registration;
import jakarta.enterprise.inject.build.compatible.spi.Synthesis;
import jakarta.enterprise.inject.build.compatible.spi.SyntheticComponents;
import java.util.Set;
import java.util.TreeSet;
/**
* Build-compatible extension used by the documentation example.
*
* <p>The extension shows the usual CDI Lite extension flow: discovery registers
* extension-provided annotations, registration observes application beans, and
* synthesis contributes runtime beans derived from build-time metadata.</p>
*/
public class PaymentBuildExtension implements BuildCompatibleExtension {
private final Set<String> gateways = new TreeSet<>();
/**
* Creates the example build-compatible extension.
*/
public PaymentBuildExtension() {
}
/**
* Promotes {@link PaymentGateway} to a CDI qualifier before bean discovery
* completes, so application injection points can select gateway-specific
* {@code PaymentProcessor} beans with the annotation.
*
* @param metaAnnotations mutable build-time meta-annotation registry
*/
@Discovery
void registerPaymentGatewayQualifier(MetaAnnotations metaAnnotations) {
metaAnnotations.addQualifier(PaymentGateway.class);
}
/**
* Collects every class bean annotated with {@link PaymentGateway}. The
* collected values are later passed to a synthetic runtime bean.
*
* @param bean bean metadata exposed by the registration phase
*/
@Registration(types = Object.class)
void collectPaymentGatewayBeans(BeanInfo bean) {
if (bean.isClassBean() && bean.declaringClass().hasAnnotation(PaymentGateway.class)) {
gateways.add(bean.declaringClass()
.annotation(PaymentGateway.class)
.value()
.asString());
}
}
/**
* Registers a synthetic {@link PaymentCatalog} bean containing the gateway
* names collected during registration.
*
* @param components synthetic component registry
*/
@Synthesis
void registerPaymentCatalog(SyntheticComponents components) {
components.addBean(PaymentCatalog.class)
.type(PaymentCatalog.class)
.scope(ApplicationScoped.class)
.qualifier(Default.class)
.withParam("gateways", gateways.toArray(String[]::new))
.createWith(PaymentCatalog.Creator.class);
}
}
Application code can then use the extension-defined qualifier and inject the synthetic bean:
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.odi.docs.cdi.extension.PaymentGateway;
/**
* Credit-card payment processor selected by the CDI qualifier and the
* extension-defined gateway qualifier.
*/
@CreditCard
@PaymentGateway("credit-card")
@ApplicationScoped
public class CreditCardProcessor implements PaymentProcessor {
/**
* Creates the credit-card processor.
*/
public CreditCardProcessor() {
}
@Override
public PaymentResult charge(Payment payment) {
return new PaymentResult("cc-" + payment.accountId(), payment.cents());
}
}
package org.eclipse.odi.docs.cdi;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import org.eclipse.odi.docs.cdi.extension.PaymentCatalog;
import org.eclipse.odi.docs.cdi.extension.PaymentGateway;
import java.util.Set;
/**
* Application bean that consumes extension-provided CDI metadata.
*
* <p>The constructor proves that {@link PaymentGateway} was registered as a
* qualifier at build time. The injected {@link PaymentCatalog} proves that the
* extension synthesized a runtime bean from the beans it saw during
* registration.</p>
*/
@ApplicationScoped
public class GatewayCatalogService {
private final PaymentProcessor processor;
private final PaymentCatalog catalog;
/**
* Creates a service using the extension-defined qualifier and synthetic bean.
*
* @param processor selected with the extension-defined qualifier
* @param catalog synthetic bean created by the build-compatible extension
*/
@Inject
public GatewayCatalogService(
@PaymentGateway("credit-card") PaymentProcessor processor,
PaymentCatalog catalog) {
this.processor = processor;
this.catalog = catalog;
}
/**
* Returns the gateway names collected by the extension.
*
* @return gateway names collected by the extension
*/
public Set<String> gateways() {
return catalog.gateways();
}
/**
* Charges through the processor selected by {@link PaymentGateway}.
*
* @param accountId account identifier used by the example processor
* @param cents amount to charge
* @return payment reference produced by the selected processor
*/
public String chargeReference(String accountId, int cents) {
return processor.charge(new Payment(accountId, cents)).reference();
}
}
5 CDI Lite Support
ODI focuses on CDI Lite. The supported programming model includes bean-defining annotations, constructor and field injection, qualifiers, producer methods and fields, disposers, observer methods, events, interceptors, Instance<T> lookup, the CDI SE bootstrap API, and build-compatible extensions.
CDI Full features that depend on runtime extension processing or container services are not part of the target runtime surface. That includes portable runtime extensions, decorators, passivation, and APIs that require a full BeanManager implementation.
The CDI Lite TCK runner is configured to exclude CDI Full coverage while exercising the CDI Lite behavior implemented by ODI.
6 Implementation Notes
ODI delegates bean discovery and dependency injection mechanics to Micronaut’s compile-time model.
The CDI annotation processor maps CDI annotations onto Micronaut metadata and emits bean definitions during compilation. At runtime, micronaut-odi-cdi adapts the generated definitions to CDI APIs such as SeContainer, CDI, BeanContainer, Instance<T>, and event dispatch.
This split keeps the processor out of the runtime classpath. Applications should depend on micronaut-odi-processor-cdi only as an annotation processor and use micronaut-odi-cdi as the runtime implementation.
Build-compatible extensions run during annotation processing. Generated proxies and supporting metadata are available to the runtime without requiring CDI Full runtime extension discovery.
7 Release History
1.0.0-SNAPSHOT
Initial CDI Lite documentation and Java documentation example.
8 Repository
The ODI source code is hosted at eclipse-ee4j/odi.
Use the repository issue tracker for bug reports and feature requests.