Injection de dépendances dans Quarkus avec CDI : ArC, scopes, qualificateurs et injection au moment de la construction
L’injection de dépendances (DI) est le fondement du développement d’applications Java d’entreprise moderne. Pendant des années, les développeurs Java se sont appuyés sur des conteneurs Spring IoC (Inversion of Control) ou Jakarta EE CDI (Contexts and Dependency Injection) standard comme Hibernate Weld ou OpenWebBeans pour découpler les cycles de vie des composants, gérer les dépendances de service et injecter des propriétés de configuration.
Cependant, à mesure que les applications d’entreprise ont évolué vers des microservices cloud natifs, la conteneurisation avec Docker et Kubernetes et des modèles d’exécution sans serveur (AWS Lambda, Knative), les conteneurs d’injection de dépendances traditionnels ont révélé une faille critique : réflexion d’exécution importante et surcharge de mémoire élevée.
Entrez Quarkus et son moteur CDI léger, ArC. Construit à partir de zéro pour la compilation native Kubernetes et GraalVM, Quarkus redéfinit l’injection de dépendances en déplaçant la réflexion, l’analyse du chemin de classe et le câblage des graphiques de bean du moment de l’exécution de l’application au moment de la construction.
Dans ce guide approfondi, nous explorerons l’injection de dépendances dans Quarkus avec CDI, couvrant les mécanismes internes du conteneur ArC, les étendues CDI, les qualificatifs personnalisés, les producteurs, les intercepteurs, les observateurs pilotés par les événements, les recherches programmatiques et les meilleures pratiques pour l’écriture d’applications Java cloud natives hautes performances.
1. Comprendre ArC : moteur CDI Quarkus Build-Time
Les conteneurs Jakarta EE CDI standard fonctionnent entièrement au démarrage de l’application. Lorsqu’une application héritée démarre, le conteneur CDI analyse chaque JAR du chemin de classe, analyse les métadonnées, inspecte les annotations à l’aide de Java Reflection (java.lang.reflect), génère des proxys dynamiques en mémoire et crée le graphique de dépendances. Cette réflexion dynamique consomme beaucoup de RAM (Metaspace) et augmente le temps de démarrage à froid.
Quarkus résout ce goulot d’étranglement avec ArC (une implémentation CDI au moment de la construction basée sur les spécifications Jakarta CDI 4.0).
En quoi ArC diffère du CDI standard :
- Augmentation du temps de construction : pendant
mvn packageou./gradlew build, ArC analyse tous les beans d’application, valide les points d’injection et pré-construit le graphe de dépendances. - Génération de bytecode en avance (AOT) : ArC génère du bytecode Java direct pour l’instanciation, les proxys de bean et les intercepteurs dès le départ, éliminant ainsi la réflexion au moment de l’exécution.
- Élagage des haricots inutilisés (élimination du code mort) : tout bean CDI qui n’est jamais injecté ou référencé dynamiquement est identifié lors de la compilation et automatiquement supprimé du binaire final, réduisant ainsi l’empreinte mémoire.
- Optimisation d’image native GraalVM : étant donné que le câblage du bean est résolu au moment de la construction, les applications Quarkus se compilent de manière transparente en exécutables natifs avec des conseils de configuration sans réflexion.
2. Portées des beans CDI et gestion du cycle de vie
Dans CDI, un Scope dicte le cycle de vie et la visibilité d’une instance de bean. Quarkus ArC prend en charge les étendues CDI Jakarta standard avec des optimisations au moment de la construction.
Portées CDI principales dans Quarkus :
| Annotation de la portée | Cycle de vie et comportement | Création de proxy | Meilleur cas d’utilisation |
|---|---|---|---|
@ApplicationScoped |
Une seule instance par application. Initialisation paresseuse lors du premier appel. État partagé sur tous les threads. | Proxy client généré | Services, référentiels, configurations sans état. |
@RequestScoped |
Instance liée à une seule requête HTTP ou à un appel d’événement. Détruit une fois la demande terminée. | Proxy client généré | Sessions utilisateur, données contextuelles de demande, jetons de sécurité. |
@Dépendant |
Portée par défaut si aucune n’est spécifiée. Instance directement liée à la durée de vie du bean injecté. | Pas de proxy (instance directe) | Classes d’utilité auxiliaire, détenteurs d’État éphémères. |
@Singleton |
Une seule instance par application. Instancié avec impatience ou paresse. Aucun wrapper de proxy client. | Pas de proxy (instance directe) | Singletons hautes performances où la surcharge du proxy client doit être évitée. |
@Inamovible |
Annotation de marqueur indiquant à ArC de ne pas supprimer le bean pendant l’optimisation au moment de la construction. | Dépend de la portée | Cibles de recherche dynamique ou beans référencés par réflexion. |
Exemple de code : déclaration de scoped beans
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. Modèles d’injection de dépendances dans Quarkus
Quarkus prend en charge plusieurs styles d’injection à l’aide de l’annotation standard jakarta.inject.Inject.
A. Injection de constructeur (meilleures pratiques recommandées)
L’injection de constructeur garantit l’immuabilité (champs final), simplifie les tests unitaires sans nécessiter de conteneurs CDI et rend les dépendances explicites.
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);
}
}
Conseil de pro : Dans Quarkus ArC, si un bean n’a qu’un un constructeur, vous pouvez omettre entièrement l’annotation
@Inject. ArC le reconnaît automatiquement comme le constructeur d’injection.
B. Injection sur le terrain
L’injection de champ est concise mais rend les champs mutables et complique les tests unitaires autonomes.
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. Qualificateurs et levée de l’ambiguïté sur plusieurs implémentations
Lorsque plusieurs beans implémentent la même interface, CDI lève une exception de dépendance ambiguë au moment de la construction. Quarkus utilise des Qualifiers pour résoudre les implémentations du bean cible en toute sécurité.
Définir des qualificatifs personnalisés
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
}
}
Implémentation de beans qualifiés
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);
}
}
Injection de haricots qualifiés
@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. Producteurs & Disposeurs : Injection d’objets tiers
Parfois, vous devez injecter des classes à partir de bibliothèques tierces (par exemple, clients AWS SDK, connexions Redis, beans de configuration personnalisés) qui ne sont pas annotées avec @ApplicationScoped. Quarkus gère cela à l’aide de Méthodes de producteur et de Disposeurs.
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. Intercepteurs CDI : programmation orientée aspect (AOP)
Les intercepteurs permettent d’attacher des préoccupations transversales (journalisation, pistes d’audit, métriques, limitation de débit) aux méthodes des beans sans polluer la logique métier.
Étape 1 : Définir la liaison d’intercepteur
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 {
}
Étape 2 : implémenter la logique de l’intercepteur
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);
}
}
}
Étape 3 : Annoter les beans cibles
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
Étant donné que Quarkus génère des proxys d’intercepteur statiques au moment de la construction, la surcharge d’invocation de méthode est pratiquement nulle par rapport aux proxys d’exécution dynamiques CGLIB dans Spring Boot.
7. Architecture basée sur les événements avec les événements CDI
CDI fournit une publication d’événements synchrones et asynchrones intégrée pour une communication découplée en cours de processus.
Publication et observation d’événements
// 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());
}
}
Événements du cycle de vie des applications
Quarkus expose les événements de démarrage et d’arrêt pour exécuter le code d’initialisation lorsque l’application démarre ou se termine :
@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. Recherche de beans programmatiques avec Instance<T> et Arc.container()
Lorsque les implémentations cibles doivent être choisies dynamiquement au moment de l’exécution (ou dans des instances non CDI héritées), utilisez la recherche statique Instance<T> ou Quarkus Arc.container().
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. Comparaison architecturale : Quarkus ArC vs Spring IoC vs Weld
| Fonctionnalité | Quarkus ArC | Conteneur Spring IoC | Soudure standard Jakarta |
|---|---|---|---|
| Résolution des dépendances | Temps de construction (AOT) | Réflexion d’exécution | Réflexion d’exécution |
| Surcharge de RAM métaspace | Extrêmement faible (~ 0 Mo pour les métadonnées) | Élevé (Cache de classe proxy dynamique) | Modéré à élevé |
| Heure de démarrage | Millisecondes (< 0,05 s) | Secondes (2,5 s - 8 s) | Secondes (1,5 s - 4 s) |
| Support natif GraalVM | Natif prêt à l’emploi | Nécessite des conseils natifs de Spring | Configuration complexe |
| Taille des haricots inutilisée | Élagage automatique au moment de la construction | Conservé dans Classpath | Conservé dans Classpath |
| Injection de constructeur unique | Implicite (aucune annotation nécessaire) | Soutenu | Nécessite @Inject |
10. Test unitaire des beans CDI avec @QuarkusTest et @InjectMock
Quarkus facilite le test des beans CDI avec JUnit 5 et RestAssured. Utilisez @InjectMock pour remplacer automatiquement les beans de dépendance par des simulations Mockito pendant les tests.
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. Conclusion
L’injection de dépendances dans Quarkus avec CDI (ArC) marque un changement monumental dans la conception Java d’entreprise. En transférant l’analyse des métadonnées du bean, la génération de proxy et la résolution des graphiques d’injection au moment de la compilation, Quarkus offre des vitesses de démarrage supersoniques, des empreintes de mémoire subatomiques et une prise en charge binaire native transparente de GraalVM.
Que vous migraciez depuis Spring Boot ou que vous créiez de nouveaux microservices cloud natifs sur Kubernetes, la maîtrise de Quarkus CDI vous offre les performances et la productivité des développeurs requises pour une architecture logicielle moderne.
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.