🔥 FREE PRO OFFER OnlyLink.click Pro Version is 100% Free of Cost till 31 December, 2026! Claim Free Pro

Quarkus 中使用 CDI 的依赖注入:ArC、范围、限定符和构建时注入

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 实现)解决了这个瓶颈。

带反射的传统运行时 CDI 与 Quarkus ArC 构建时连线

ArC 与标准 CDI 有何不同:

  1. 构建时增强:在 mvn package 或 ./gradlew build 期间,ArC 扫描所有应用程序 bean、验证注入点并预构建依赖关系图。
  2. 提前 (AOT) 字节码生成:ArC 预先生成用于实例化、bean 代理和拦截器的直接 Java 字节码,从而消除运行时的反射。
  3. 未使用的 Bean 修剪(死代码消除):任何从未动态注入或引用的 CDI bean 在编译期间都会被识别,并自动从最终的二进制文件中删除,从而减少内存占用。
  4. GraalVM 本机映像优化:由于 bean 连接是在构建时解决的,因此 Quarkus 应用程序可以通过零反射配置提示无缝编译为本机可执行文件。

2. CDI Bean 范围和生命周期管理

在 CDI 中,范围 规定了 bean 实例的生命周期和可见性。 Quarkus ArC 通过构建时优化支持标准 Jakarta CDI 范围。

Quarkus CDI Bean 范围和生命周期图

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 都能为您提供现代软件架构所需的性能和开发人员生产力。

Ghaznix Ecosystem Products

Empower Your Digital Presence & Workflows

Explore top-tier tools built by Ghaznix to streamline your links, surveys, and brand growth.