Inyección de dependencia en Quarkus con CDI: ArC, alcances, calificadores e inyección en tiempo de compilación
La inyección de dependencia (DI) es la base del desarrollo de aplicaciones Java empresariales modernas. Durante años, los desarrolladores de Java confiaron en Spring IoC (Inversión de control) o contenedores estándar Jakarta EE CDI (Contexts and Dependency injection) como Hibernate Weld u OpenWebBeans para desacoplar los ciclos de vida de los componentes, administrar las dependencias de los servicios e inyectar propiedades de configuración.
Sin embargo, a medida que las aplicaciones empresariales hicieron la transición hacia microservicios nativos de la nube, la contenedorización con Docker y Kubernetes y modelos de ejecución sin servidor (AWS Lambda, Knative), los contenedores tradicionales de inyección de dependencia expusieron una falla crítica: pesada reflexión en tiempo de ejecución y alta sobrecarga de memoria.
Ingrese Quarkus y su motor CDI liviano, ArC. Creado desde cero para la compilación nativa de Kubernetes y GraalVM, Quarkus redefine la inyección de dependencia al cambiar la reflexión, el escaneo de classpath y el cableado de gráficos de beans desde el tiempo de ejecución de la aplicación hasta el tiempo de compilación.
En esta guía detallada, exploraremos la Inyección de dependencia en Quarkus con CDI, cubriendo la mecánica interna del contenedor ArC, alcances CDI, calificadores personalizados, productores, interceptores, observadores controlados por eventos, búsquedas programáticas y mejores prácticas para escribir aplicaciones Java nativas de la nube de alto rendimiento.
1. Comprensión de ArC: motor CDI de tiempo de construcción Quarkus
Los contenedores estándar Jakarta EE CDI funcionan completamente al iniciar la aplicación. Cuando se inicia una aplicación heredada, el contenedor CDI escanea cada JAR en el classpath, analiza los metadatos, inspecciona las anotaciones usando Java Reflection (java.lang.reflect), genera servidores proxy dinámicos en la memoria y construye el gráfico de dependencia. Esta reflexión dinámica consume una cantidad significativa de RAM (Metaespacio) y aumenta el tiempo de arranque en frío.
Quarkus resuelve este cuello de botella con ArC (una implementación CDI en tiempo de compilación basada en las especificaciones Jakarta CDI 4.0).
En qué se diferencia ArC del CDI estándar:
- Aumento del tiempo de compilación: durante
mvn packageo./gradlew build, ArC escanea todos los beans de la aplicación, valida los puntos de inyección y crea previamente el gráfico de dependencia. - Generación de código de bytes anticipada (AOT): ArC genera código de bytes de Java directo para creación de instancias, servidores proxy de beans e interceptores por adelantado, eliminando la reflexión en tiempo de ejecución.
- Poda de beans no utilizados (eliminación de código muerto): cualquier bean CDI que nunca se inyecta o al que se hace referencia dinámicamente se identifica durante la compilación y se elimina automáticamente del binario final, lo que reduce el uso de memoria.
- Optimización de imagen nativa de GraalVM: debido a que el cableado del bean se resuelve en el momento de la compilación, las aplicaciones Quarkus se compilan sin problemas en ejecutables nativos con sugerencias de configuración de reflexión cero.
2. Alcance del Bean CDI y gestión del ciclo de vida
En CDI, un Alcance dicta el ciclo de vida y la visibilidad de una instancia de bean. Quarkus ArC admite osciloscopios CDI estándar de Yakarta con optimizaciones en el tiempo de construcción.
Alcances CDI primarios en Quarkus:
| Anotación de alcance | Ciclo de vida y comportamiento | Creación de proxy | Mejor caso de uso |
|---|---|---|---|
@ApplicationScoped |
Instancia única por aplicación. Inicialización diferida tras la primera invocación. Estado compartido en todos los hilos. | Proxy de cliente generado | Servicios, repositorios, configuraciones sin estado. |
@RequestScoped |
Instancia vinculada a una única solicitud HTTP o invocación de evento. Destruido cuando se completa la solicitud. | Proxy de cliente generado | Sesiones de usuario, solicitar datos de contexto, tokens de seguridad. |
@Dependiente |
Alcance predeterminado si no se especifica ninguno. Instancia vinculada directamente a la vida útil del bean de inyección. | Sin proxy (instancia directa) | Clases de utilidad auxiliares, titulares de estados efímeros. |
@Singleton |
Instancia única por aplicación. Instanciado con avidez o pereza. Sin contenedor de proxy de cliente. | Sin proxy (instancia directa) | Singletons de alto rendimiento donde se debe evitar la sobrecarga del proxy del cliente. |
@Inamovible |
Anotación de marcador que indica a ArC que no elimine el bean durante la optimización del tiempo de compilación. | Depende del alcance | Objetivos de búsqueda dinámica o beans referenciados por reflexión. |
Ejemplo de código: declaración de beans con alcance
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. Patrones de inyección de dependencia en Quarkus
Quarkus admite múltiples estilos de inyección utilizando la anotación estándar jakarta.inject.Inject.
A. Inyección de constructor (mejor práctica recomendada)
La inyección de constructor garantiza la inmutabilidad (campos final), simplifica las pruebas unitarias sin requerir contenedores CDI y hace que las dependencias sean explícitas.
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);
}
}
Consejo profesional: En Quarkus ArC, si un bean tiene solo un constructor, puedes omitir la anotación
@Injectpor completo. ArC lo reconoce automáticamente como el constructor de inyección.
B. Inyección de campo
La inyección de campo es concisa pero hace que los campos sean mutables y complica las pruebas unitarias independientes.
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. Calificadores y desambiguación de implementaciones múltiples
Cuando varios beans implementan la misma interfaz, CDI genera una excepción de dependencia ambigua en el momento de la compilación. Quarkus utiliza Calificadores para resolver implementaciones de beans de destino de forma segura.
Definición de calificadores personalizados
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
}
}
Implementación de beans calificados
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);
}
}
Inyectar frijoles calificados
@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. Productores y Eliminadores: Inyección de Objetos de Terceros
A veces es necesario inyectar clases de bibliotecas de terceros (por ejemplo, clientes de AWS SDK, conexiones de Redis, beans de configuración personalizados) que no están anotadas con @ApplicationScoped. Quarkus maneja esto usando Métodos de productor y Disposers.
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. Interceptores CDI: programación orientada a aspectos (AOP)
Los interceptores permiten adjuntar preocupaciones transversales (registro, pistas de auditoría, métricas, limitación de velocidad) a los métodos de beans sin contaminar la lógica empresarial.
Paso 1: Definir el enlace del interceptor
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 {
}
Paso 2: implementar la lógica del interceptor
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);
}
}
}
Paso 3: Anotar los beans de destino
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
Debido a que Quarkus genera servidores proxy de interceptor estáticos en el momento de la compilación, la sobrecarga de invocación de métodos es prácticamente nula en comparación con los servidores proxy de tiempo de ejecución CGLIB dinámicos en Spring Boot.
7. Arquitectura basada en eventos con eventos CDI
CDI proporciona publicación de eventos sincrónica y asincrónica integrada para comunicación desacoplada en proceso.
Publicación y observación de eventos
// 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());
}
}
Eventos del ciclo de vida de la aplicación
Quarkus expone eventos de inicio y apagado para ejecutar el código de inicialización cuando la aplicación se inicia o finaliza:
@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. Búsqueda programática de beans con Instance<T> y Arc.container()
Cuando las implementaciones de destino deben elegirse dinámicamente en tiempo de ejecución (o dentro de instancias heredadas que no sean CDI), use la búsqueda estática Instance<T> o Quarkus Arc.container().
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. Comparación arquitectónica: Quarkus ArC vs Spring IoC vs Weld
| Característica | Quarkus ArC | Contenedor de COI de primavera | Soldadura estándar de Yakarta |
|---|---|---|---|
| Resolución de dependencia | Tiempo de construcción (AOT) | Reflexión en tiempo de ejecución | Reflexión en tiempo de ejecución |
| Sobrecarga de RAM de metaespacio | Extremadamente bajo (~0 MB para metadatos) | Alto (caché de clase de proxy dinámico) | Moderado a alto |
| Hora de inicio | Milisegundos (< 0,05 s) | Segundos (2,5 s - 8 s) | Segundos (1,5s - 4s) |
| Soporte nativo de GraalVM | Nativo listo para usar | Requiere sugerencias nativas de Spring | Configuración compleja |
| Poda de frijol no utilizado | Poda automática en tiempo de construcción | Mantenido en Classpath | Mantenido en Classpath |
| Inyección de un solo constructor | Implícito (no se necesita anotación) | Apoyado | Requiere @Inject |
10. Prueba unitaria de beans CDI con @QuarkusTest y @InjectMock
Quarkus facilita las pruebas de beans CDI con JUnit 5 y RestAssured. Utilice @InjectMock para reemplazar los beans de dependencia con simulacros de Mockito automáticamente durante las pruebas.
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. Conclusión
La inyección de dependencia en Quarkus con CDI (ArC) marca un cambio monumental en el diseño empresarial de Java. Al transferir el escaneo de metadatos de beans, la generación de proxy y la resolución de gráficos de inyección al tiempo de compilación, Quarkus ofrece velocidades de inicio supersónicas, huellas de memoria subatómica y soporte binario nativo GraalVM perfecto.
Ya sea que esté migrando desde Spring Boot o creando microservicios nativos de la nube totalmente nuevos en Kubernetes, dominar Quarkus CDI le brinda el rendimiento y la productividad del desarrollador necesarios para la arquitectura de software moderna.
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.