Quarkus 中使用 CDI 的依赖注入:ArC、范围、限定符和构建时注入
依赖注入 (DI) 是现代企业 Java 应用程序开发的基石。多年来,Java 开发人员依赖 Spring IoC(控制反转)或标准 Jakarta EE CDI(上下文和依赖注入)容器(如 Hibernate Weld 或 OpenWebBeans)来解耦组件生命周期、管理服务依赖项和注入配置属性。
然而,随着企业应用程序向云原生微服务、Docker 和 Kubernetes 容器化以及无服务器执行模型(AWS Lambda、Knative)过渡,传统的依赖项注入容器暴露了一个严重缺陷:繁重的运行时反射和高内存开销。
输入 Quarkus 及其轻量级 CDI 引擎 ArC。 Quarkus 专为 Kubernetes 和 GraalVM 本机编译而全新构建,通过将反射、类路径扫描和 bean 图连接从应用程序运行时转移到构建时,重新定义了依赖注入。
在本深入指南中,我们将探索 Quarkus 中使用 CDI 的依赖注入,涵盖 ArC 容器的内部机制、CDI 范围、自定义限定符、生产者、拦截器、事件驱动的观察者、编程式查找以及编写高性能云原生 Java 应用程序的最佳实践。
1. 了解 ArC:Quarkus 构建时 CDI 引擎
标准 Jakarta EE CDI 容器完全在应用程序启动时运行。当旧应用程序启动时,CDI 容器会扫描类路径中的每个 JAR、解析元数据、使用 Java 反射 (java.lang.reflect) 检查注释、在内存中生成动态代理并构建依赖关系图。这种动态反射会消耗大量 RAM(元空间)并增加冷启动启动时间。
Quarkus 通过 ArC(基于 Jakarta CDI 4.0 规范的构建时 CDI 实现)解决了这个瓶颈。
ArC 与标准 CDI 有何不同:
- 构建时增强:在
mvn package或./gradlew build期间,ArC 扫描所有应用程序 bean、验证注入点并预构建依赖关系图。 - 提前 (AOT) 字节码生成:ArC 预先生成用于实例化、bean 代理和拦截器的直接 Java 字节码,从而消除运行时的反射。
- 未使用的 Bean 修剪(死代码消除):任何从未动态注入或引用的 CDI bean 在编译期间都会被识别,并自动从最终的二进制文件中删除,从而减少内存占用。
- GraalVM 本机映像优化:由于 bean 连接是在构建时解决的,因此 Quarkus 应用程序可以通过零反射配置提示无缝编译为本机可执行文件。
2. CDI Bean 范围和生命周期管理
在 CDI 中,范围 规定了 bean 实例的生命周期和可见性。 Quarkus ArC 通过构建时优化支持标准 Jakarta CDI 范围。
Quarkus 中的主要 CDI 范围:
| 范围注释 | 生命周期和行为 | 代理创建 | 最佳用例 |
|---|---|---|---|
@ApplicationScoped |
每个应用程序单个实例。首次调用时延迟初始化。所有线程共享状态。 | 客户端代理生成 | 无状态服务、存储库、配置。 |
@RequestScoped |
实例绑定到单个 HTTP 请求或事件调用。请求完成后销毁。 | 客户端代理生成 | 用户会话、请求上下文数据、安全令牌。 |
@Dependent |
如果未指定,则默认范围。实例直接与注入 bean 的生命周期相关。 | 无代理(直接实例) | 助手实用程序类,短暂的状态持有者。 |
@单例 |
每个应用程序单个实例。急切或懒惰地实例化。没有客户端代理包装器。 | 无代理(直接实例) | 必须避免客户端代理开销的高性能单例。 |
@不可移除 |
指示 ArC 在构建时优化期间不要删除 bean 的标记注释。 | 取决于范围 | 动态查找目标或反射引用的 bean。 |
代码示例:声明作用域 Bean
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. Quarkus 中的依赖注入模式
Quarkus 使用标准 jakarta.inject.Inject 注释支持多种注入样式。
A. 构造函数注入(推荐最佳实践)
构造函数注入保证不变性(final 字段),简化单元测试而不需要 CDI 容器,并使依赖关系显式化。
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);
}
}
专业提示:在 Quarkus ArC 中,如果一个 bean 只有 一个构造函数,您可以完全省略
@Inject注释。 ArC 自动将其识别为注入构造函数。
B. 现场注入
字段注入很简洁,但会使字段可变并使独立单元测试变得复杂。
@ApplicationScoped
public class NotificationService {
@Inject
EmailClient emailClient; // Field injection
}
4. 限定符和消除多重实现的歧义
当多个 bean 实现相同的接口时,CDI 在构建时会引发不明确的依赖异常。 Quarkus 使用 限定符 来安全地解析目标 bean 实现。
定义自定义限定符
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
}
}
实施合格的 Bean
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);
}
}
###注入合格的Bean
@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. 生产者和处置者:注入第三方对象
有时,您需要从第三方库(例如,AWS SDK 客户端、Redis 连接、自定义配置 bean)注入未使用 @ApplicationScoped 注释的类。 Quarkus 使用 ProducerMethods 和 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. CDI 拦截器:面向方面编程(AOP)
拦截器允许将横切关注点(日志记录、审计跟踪、指标、速率限制)附加到 bean 方法,而不会污染业务逻辑。
第 1 步:定义拦截器绑定
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 {
}
第 2 步:实现拦截器逻辑
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);
}
}
}
步骤 3:注释目标 Bean
@ApplicationScoped
public class AnalyticsService {
@LogExecutionTime
public void computeAggregates() {
// Business computation
}
}
由于 Quarkus 在构建时生成静态拦截器代理,因此与 Spring Boot 中的动态 CGLIB 运行时代理相比,方法调用开销几乎为零。
7. 具有 CDI 事件的事件驱动架构
CDI 提供内置的同步和异步事件发布,以实现解耦的进程内通信。
发布和观察活动
// 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());
}
}
应用程序生命周期事件
Quarkus 公开启动和关闭事件以在应用程序启动或终止时执行初始化代码:
@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. 使用 Instance<T> 和 Arc.container() 进行编程 Bean 查找
当必须在运行时(或在旧版非 CDI 实例内)动态选择目标实现时,请使用 Instance<T> 或 Quarkus Arc.container() 静态查找。
@ApplicationScoped
public class DynamicPluginRegistry {
@Inject
Instance<PaymentProcessor> processors;
public void executeAll(double amount) {
for (PaymentProcessor processor : processors) {
processor.process(amount);
}
}
}
9. 架构比较:Quarkus ArC、Spring IoC、Weld
| 特征 | 夸库斯ArC | Spring IoC 容器 | 标准雅加达焊缝 |
|---|---|---|---|
| 依赖解析 | 构建时 (AOT) | 运行时反射 | 运行时反射 |
| 元空间 RAM 开销 | 极低(元数据约为 0MB) | 高(动态代理类缓存) | 中到高 |
| 启动时间 | 毫秒(< 0.05 秒) | 秒(2.5 秒 - 8 秒) | 秒(1.5 秒 - 4 秒) |
| GraalVM 本机支持 | 本机开箱即用 | 需要 Spring Native 提示 | 配置复杂 |
| 未使用的豆修剪 | 自动构建时修剪 | 保存在类路径中 | 保存在类路径中 |
| 单一构造函数注入 | 隐式(无需注释) | 支持 | 需要@Inject |
10. 使用 @QuarkusTest 和 @InjectMock 对 CDI Bean 进行单元测试
Quarkus 使 CDI bean 测试与 JUnit 5 和 RestAssured 无缝衔接。在测试过程中,使用 @InjectMock 自动将依赖 bean 替换为 Mockito 模拟。
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. 结论
Quarkus 中使用 CDI (ArC) 的依赖注入标志着企业 Java 设计的巨大转变。通过将 Bean 元数据扫描、代理生成和注入图分辨率转移到编译时间,Quarkus 提供了超音速启动速度、亚原子内存占用和无缝 GraalVM 本机二进制支持。
无论您是从 Spring Boot 迁移还是在 Kubernetes 上构建全新的云原生微服务,掌握 Quarkus CDI 都能为您提供现代软件架构所需的性能和开发人员生产力。
Tags
Empower Your Digital Presence & Workflows
Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.