Abhängigkeitsinjektion in Quarkus mit CDI: ArC, Scopes, Qualifiers und Build-Time-Injection
Dependency Injection (DI) ist die Grundlage der modernen Java-Anwendungsentwicklung für Unternehmen. Jahrelang verließen sich Java-Entwickler auf Spring IoC (Inversion of Control) oder standardmäßige Jakarta EE CDI (Contexts and Dependency Injection)-Container wie Hibernate Weld oder OpenWebBeans, um Komponentenlebenszyklen zu entkoppeln, Dienstabhängigkeiten zu verwalten und Konfigurationseigenschaften einzufügen.
Als Unternehmensanwendungen jedoch auf Cloud-native Mikrodienste, Containerisierung mit Docker und Kubernetes sowie serverlose Ausführungsmodelle (AWS Lambda, Knative) umstellten, offenbarten herkömmliche Dependency-Injection-Container einen kritischen Fehler: starke Laufzeitreflexion und hoher Speicher-Overhead.
Hier kommen Quarkus und seine leichte CDI-Engine ArC ins Spiel. Quarkus wurde von Grund auf für die native Kompilierung von Kubernetes und GraalVM entwickelt und definiert die Abhängigkeitsinjektion neu, indem Reflektion, Klassenpfad-Scanning und Bean-Graph-Verkabelung von der Anwendungslaufzeit zur Erstellungszeit verlagert werden.
In diesem ausführlichen Leitfaden befassen wir uns mit Abhängigkeitsinjektion in Quarkus mit CDI und behandeln dabei die inneren Mechanismen des ArC-Containers, CDI-Umfänge, benutzerdefinierte Qualifizierer, Produzenten, Interceptoren, ereignisgesteuerte Beobachter, programmgesteuerte Lookups und Best Practices für das Schreiben leistungsstarker cloudnativer Java-Anwendungen.
1. ArC verstehen: Quarkus Build-Time CDI Engine
Standard-Jakarta-EE-CDI-Container werden vollständig beim Anwendungsstart ausgeführt. Wenn eine Legacy-Anwendung gestartet wird, scannt der CDI-Container jedes JAR im Klassenpfad, analysiert Metadaten, prüft Annotationen mithilfe von Java Reflection (java.lang.reflect), generiert dynamische Proxys im Speicher und erstellt das Abhängigkeitsdiagramm. Diese dynamische Reflexion verbraucht erheblich RAM (Metaspace) und verlängert die Bootzeit beim Kaltstart.
Quarkus löst diesen Engpass mit ArC (einer Build-Time-CDI-Implementierung basierend auf den Jakarta CDI 4.0-Spezifikationen).
Wie sich ArC vom Standard-CDI unterscheidet:
- Build-Time Augmentation: Während
mvn packageoder./gradlew buildscannt ArC alle Anwendungs-Beans, validiert Injektionspunkte und erstellt den Abhängigkeitsgraphen vorab. - Ahead-Of-Time (AOT) Bytecode-Generierung: ArC generiert direkten Java-Bytecode für Instanziierung, Bean-Proxys und Interceptoren im Voraus und eliminiert so Reflexionen zur Laufzeit.
- Bereinigung nicht verwendeter Beans (Eliminierung von totem Code): Jede CDI-Bean, die niemals dynamisch injiziert oder referenziert wird, wird während der Kompilierung identifiziert und automatisch aus der endgültigen Binärdatei entfernt, wodurch der Speicherbedarf reduziert wird.
- Native Image-Optimierung von GraalVM: Da die Bean-Verkabelung zur Build-Zeit aufgelöst wird, werden Quarkus-Anwendungen nahtlos in native ausführbare Dateien kompiliert, ohne dass Konfigurationshinweise reflektiert werden.
2. CDI-Bean-Bereiche und Lebenszyklusmanagement
In CDI bestimmt ein Scope den Lebenszyklus und die Sichtbarkeit einer Bean-Instanz. Quarkus ArC unterstützt Standard-CDI-Bereiche von Jakarta mit Optimierungen zur Buildzeit.
Primäre CDI-Bereiche in Quarkus:
| Bereichsanmerkung | Lebenszyklus und Verhalten | Proxy-Erstellung | Bester Anwendungsfall |
|---|---|---|---|
| „@ApplicationScoped“. | Einzelne Instanz pro Anwendung. Verzögerte Initialisierung beim ersten Aufruf. Status in allen Threads geteilt. | Client-Proxy generiert | Zustandslose Dienste, Repositorys, Konfigurationen. |
| „@RequestScoped“. | Instanz, die an eine einzelne HTTP-Anfrage oder einen einzelnen Ereignisaufruf gebunden ist. Wird zerstört, wenn die Anfrage abgeschlossen ist. | Client-Proxy generiert | Benutzersitzungen, Anforderungskontextdaten, Sicherheitstoken. |
| „@Dependent“. | Standardbereich, wenn keiner angegeben ist. Die Instanz ist direkt mit der Lebensdauer der injizierenden Bohne verknüpft. | Kein Proxy (direkte Instanz) | Hilfsdienstprogrammklassen, kurzlebige Zustandsinhaber. |
| „@Singleton“. | Einzelne Instanz pro Anwendung. Eifrig oder träge instanziiert. Kein Client-Proxy-Wrapper. | Kein Proxy (direkte Instanz) | Hochleistungs-Singletons, bei denen der Client-Proxy-Overhead vermieden werden muss. |
| „@Unremovable“. | Markierungsanmerkung, die ArC anweist, die Bean während der Build-Time-Optimierung nicht zu entfernen. | Hängt vom Umfang ab | Dynamische Suchziele oder reflexionsreferenzierte Beans. |
Codebeispiel: Scoped Beans deklarieren
package com.ghaznix.services;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import java.util.UUID;
@ApplicationScoped
public class OrderProcessingService {
public String processOrder(String orderId) {
return "Order " + orderId + " processed successfully.";
}
}
@RequestScoped
public class RequestContextHolder {
private final String requestId = UUID.randomUUID().toString();
public String getRequestId() {
return requestId;
}
}
3. Abhängigkeitsinjektionsmuster in Quarkus
Quarkus unterstützt mehrere Injektionsstile mithilfe der Standardannotation jakarta.inject.Inject.
A. Konstruktorinjektion (empfohlene Best Practice)
Die Konstruktorinjektion garantiert Unveränderlichkeit (final-Felder), vereinfacht Unit-Tests, ohne dass CDI-Container erforderlich sind, und macht Abhängigkeiten explizit.
package com.ghaznix.resources;
import com.ghaznix.services.OrderProcessingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/orders")
public class OrderResource {
private final OrderProcessingService orderService;
// In Quarkus, if a class has a single constructor, @Inject is OPTIONAL!
@Inject
public OrderResource(OrderProcessingService orderService) {
this.orderService = orderService;
}
@POST
@Path("/{id}")
public String checkout(@PathParam("id") String id) {
return orderService.processOrder(id);
}
}
Profi-Tipp: Wenn in Quarkus ArC eine Bean nur einen Konstruktor hat, können Sie die Annotation
@Injectvollständig weglassen. ArC erkennt es automatisch als Injektionskonstruktor.
B. Feldinjektion
Die Feldinjektion ist prägnant, macht Felder jedoch veränderbar und erschwert das Testen eigenständiger Einheiten.
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. Qualifizierer und Eindeutigkeit mehrerer Implementierungen
Wenn mehrere Beans dieselbe Schnittstelle implementieren, löst CDI zur Erstellungszeit eine mehrdeutige Abhängigkeitsausnahme aus. Quarkus verwendet Qualifizierer, um Ziel-Bean-Implementierungen sicher aufzulösen.
Benutzerdefinierte Qualifizierer definieren
package com.ghaznix.payment;
import jakarta.inject.Qualifier;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Qualifier
@Retention(RUNTIME)
@Target({TYPE, METHOD, FIELD, PARAMETER})
public @interface PaymentType {
Mode value();
enum Mode {
CREDIT_CARD,
PAYPAL,
CRYPTO
}
}
Qualifizierte Beans implementieren
public interface PaymentProcessor {
void process(double amount);
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.CREDIT_CARD)
public class CreditCardProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing Credit Card payment: $" + amount);
}
}
@ApplicationScoped
@PaymentType(PaymentType.Mode.PAYPAL)
public class PayPalProcessor implements PaymentProcessor {
@Override
public void process(double amount) {
System.out.println("Processing PayPal payment: $" + amount);
}
}
Qualifizierte Bohnen injizieren
@ApplicationScoped
public class CheckoutService {
private final PaymentProcessor creditCardProcessor;
private final PaymentProcessor payPalProcessor;
@Inject
public CheckoutService(
@PaymentType(PaymentType.Mode.CREDIT_CARD) PaymentProcessor creditCardProcessor,
@PaymentType(PaymentType.Mode.PAYPAL) PaymentProcessor payPalProcessor) {
this.creditCardProcessor = creditCardProcessor;
this.payPalProcessor = payPalProcessor;
}
}
5. Produzenten und Entsorger: Einschleusen von Objekten Dritter
Manchmal müssen Sie Klassen aus Bibliotheken von Drittanbietern (z. B. AWS SDK-Clients, Redis-Verbindungen, benutzerdefinierte Konfigurations-Beans) einfügen, die nicht mit @ApplicationScoped annotiert sind. Quarkus handhabt dies mithilfe von Produzentenmethoden und Entsorgern.
package com.ghaznix.config;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
@ApplicationScoped
public class DatabaseConnectionProducer {
@Produces
@ApplicationScoped
public Connection createDatabaseConnection() throws SQLException {
return DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb", "user", "pass");
}
// Disposer method called automatically when bean is destroyed
public void closeConnection(@Disposes Connection connection) throws SQLException {
if (connection != null && !connection.isClosed()) {
connection.close();
}
}
}
6. CDI-Interceptoren: Aspektorientierte Programmierung (AOP)
Mit Interceptoren können übergreifende Anliegen (Protokollierung, Audit-Trails, Metriken, Ratenbegrenzung) an Bean-Methoden angehängt werden, ohne die Geschäftslogik zu beeinträchtigen.
Schritt 1: Definieren Sie die Interceptor-Bindung
package com.ghaznix.interceptors;
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@InterceptorBinding
@Retention(RUNTIME)
@Target({TYPE, METHOD})
public @interface LogExecutionTime {
}
Schritt 2: Interceptor-Logik implementieren
package com.ghaznix.interceptors;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@LogExecutionTime
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LogExecutionTimeInterceptor {
@AroundInvoke
public Object logInvocation(InvocationContext context) throws Exception {
long startTime = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
System.out.printf("[AUDIT] Method %s executed in %d ms%n",
context.getMethod().getName(), duration);
}
}
}
Schritt 3: Ziel-Beans mit Anmerkungen versehen
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
Da Quarkus zum Zeitpunkt der Erstellung statische Interceptor-Proxys generiert, ist der Overhead beim Methodenaufruf im Vergleich zu dynamischen CGLIB-Laufzeit-Proxys in Spring Boot praktisch gleich Null.
7. Ereignisgesteuerte Architektur mit CDI-Ereignissen
CDI bietet integrierte synchrone und asynchrone Ereignisveröffentlichung für entkoppelte In-Process-Kommunikation.
Ereignisse veröffentlichen und beobachten
// 1. Define Event Payload
public record OrderCreatedEvent(String orderId, double totalAmount) {}
// 2. Publish Event
@ApplicationScoped
public class OrderService {
@Inject
Event<OrderCreatedEvent> orderEventPublisher;
public void createOrder(String orderId, double amount) {
// Create order logic...
orderEventPublisher.fire(new OrderCreatedEvent(orderId, amount));
}
}
// 3. Observe Event Synchronously or Asynchronously
@ApplicationScoped
public class InventoryObserver {
public void onOrderCreated(@Observes OrderCreatedEvent event) {
System.out.println("Updating inventory for order: " + event.orderId());
}
public void onOrderCreatedAsync(@ObservesAsync OrderCreatedEvent event) {
System.out.println("Async audit log for order: " + event.orderId());
}
}
Anwendungslebenszyklusereignisse
Quarkus macht Start- und Shutdown-Ereignisse verfügbar, um Initialisierungscode auszuführen, wenn die Anwendung gestartet oder beendet wird:
@ApplicationScoped
public class ApplicationLifecycleBean {
void onStart(@Observes StartupEvent ev) {
System.out.println("Quarkus Application starting up...");
}
void onStop(@Observes ShutdownEvent ev) {
System.out.println("Quarkus Application shutting down...");
}
}
8. Programmatic Bean Lookup mit Instance<T> & Arc.container()
Wenn Zielimplementierungen dynamisch zur Laufzeit (oder innerhalb älterer Nicht-CDI-Instanzen) ausgewählt werden müssen, verwenden Sie die statische Suche Instance<T> oder Quarkus Arc.container().
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. Architekturvergleich: Quarkus ArC vs. Spring IoC vs. Weld
| Besonderheit | Quarkus ArC | Spring IoC-Container | Standard-Jakarta-Schweißnaht |
|---|---|---|---|
| Abhängigkeitsauflösung | Build-Zeit (AOT) | Laufzeitreflexion | Laufzeitreflexion |
| Metaspace-RAM-Overhead | Extrem niedrig (~0 MB für Metadaten) | Hoch (Dynamischer Proxy-Klassen-Cache) | Mäßig bis hoch |
| Startzeit | Millisekunden (< 0,05 s) | Sekunden (2,5 s – 8 s) | Sekunden (1,5 s – 4 s) |
| Native Unterstützung von GraalVM | Native, sofort einsatzbereit | Erfordert Spring Native Hints | Komplexe Konfiguration |
| Unbenutzter Bohnenschnitt | Automatisches Bereinigen zur Build-Zeit | Wird im Klassenpfad gespeichert | Wird im Klassenpfad gespeichert |
| Einzelkonstruktorinjektion | Implizit (keine Anmerkung erforderlich) | Unterstützt | Erfordert „@Inject“. |
10. Unit-Test von CDI-Beans mit @QuarkusTest und @InjectMock
Quarkus ermöglicht das nahtlose Testen von CDI-Beans mit JUnit 5 und RestAssured. Verwenden Sie @InjectMock, um Abhängigkeits-Beans während des Tests automatisch durch Mockito-Mocks zu ersetzen.
package com.ghaznix;
import com.ghaznix.services.OrderProcessingService;
import io.quarkus.test.InjectMock;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusTest
public class OrderServiceTest {
@Inject
OrderService orderService;
@InjectMock
OrderProcessingService orderProcessingService;
@Test
public void testOrderCheckout() {
Mockito.when(orderProcessingService.processOrder("ORD-101"))
.thenReturn("Mocked Success");
String result = orderService.checkout("ORD-101");
Assertions.assertEquals("Mocked Success", result);
}
}
11. Fazit
Die Abhängigkeitsinjektion in Quarkus mit CDI (ArC) markiert einen monumentalen Wandel im Enterprise-Java-Design. Durch die Übertragung von Bean-Metadaten-Scanning, Proxy-Generierung und Injektionsdiagrammauflösung auf die Kompilierungszeit liefert Quarkus Überschall-Startgeschwindigkeiten, subatomaren Speicherbedarf und nahtlose native GraalVM-Binärunterstützung.
Ganz gleich, ob Sie von Spring Boot migrieren oder cloudnative Microservices auf der grünen Wiese auf Kubernetes erstellen, die Beherrschung von Quarkus CDI stattet Sie mit der Leistung und Entwicklerproduktivität aus, die für eine moderne Softwarearchitektur erforderlich sind.
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.