背景

本文是《JavaEE 后端从小白到大神》修仙系列第八篇,正式进入JavaEE后端世界。若想详细学习请点击首篇博文,我们开始吧。

第八篇:CDI(上下文依赖注入)机制

  • CDI 容器与 Bean 管理;
  • @Inject、@Qualifier、@Named;
  • Scope(@RequestScoped、@SessionScoped、@ApplicationScoped);
  • Interceptor 与 Decorator;
  • 事件机制;
  • CDI 与 EJB 的整合;
  • 与 Spring IoC 的对比。

一、CDI 概述

1. 什么是 CDI

CDI(Contexts and Dependency Injection,上下文与依赖注入)是 Jakarta EE(原 Java EE)中定义的依赖注入和上下文管理规范,是整个 Jakarta EE 平台的核心基础规范

CDI 提供了一套类型安全(Typesafe)的依赖注入机制,能够在开发时或部署时选择依赖,而无需繁琐的配置文件。它允许对象绑定到生命周期上下文、被注入、与拦截器和装饰器关联,并通过事件机制进行松耦合交互。

一句话总结:CDI = Jakarta EE 平台的标准依赖注入与上下文管理规范,是连接 Web 层、业务层和持久化层的“万能胶水”。

2. CDI 的核心价值

在 CDI 出现之前,Jakarta EE 各组件(Servlet、EJB、JPA 等)是相对孤立的。CDI 的出现解决了以下问题:

问题 CDI 的解决方案
组件间依赖需要手动创建 通过 @Inject 自动注入依赖
对象生命周期管理混乱 通过作用域(Scope)管理对象的生命周期
类型不安全(字符串配置) 类型安全的依赖解析
组件耦合度高 通过 Qualifier 和事件机制解耦
横切逻辑重复编写 通过拦截器(Interceptor)统一处理

3. CDI 的演进历程

版本 发布时间 核心变化
CDI 1.0(JSR 299) 2009 初始规范,Java EE 6 的一部分
CDI 1.1(JSR 346) 2013 Java EE 7 的一部分,小幅改进
CDI 2.0(JSR 365) 2017 Java EE 8 的一部分,新增异步事件、@Priority
CDI 3.0 2020 Jakarta EE 9,包名从 javax 迁移到 jakarta
CDI 4.0 2022 Jakarta EE 10,适配 Java SE 17
CDI 5.0 2026(开发中) Jakarta EE 12,支持懒加载依赖注入

二、CDI 容器与 Bean 管理

1. 什么是 CDI Bean

CDI Bean 是由 CDI 容器管理的 Java 对象。任何普通的 Java 类(POJO)都可以成为 CDI Bean,只需满足以下条件:

  • 类不是抽象类(或带有 @Decorator@Interceptor 的类除外);
  • 类有一个无参构造方法,或带有 @Inject 注解的构造方法;
  • 类没有被 @Vetoed 注解排除。

关键认知:CDI 不需要像 EJB 那样继承特定接口或父类——任何 POJO 都可以被 CDI 管理

2. CDI 容器

CDI 容器负责:

  • 扫描并发现 CDI Bean;
  • 管理 Bean 的生命周期(创建、注入、销毁);
  • 解析依赖关系并执行注入;
  • 管理 Bean 的作用域上下文;
  • 处理拦截器和装饰器;
  • 管理事件发布与观察。

在 Jakarta EE 应用服务器中,CDI 容器是自动启用的,无需额外配置。

3. 如何获取 CDI 容器

可以通过注入 BeanManager 获取对 CDI 容器的编程式访问:

1
2
3
4
5
6
7
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Inject;

public class MyService {
    @Inject
    private BeanManager beanManager;  // 注入 BeanManager
}

也可以通过 CDI.current() 获取当前容器的引用:

1
2
3
4
import jakarta.enterprise.inject.spi.CDI;

CDI<Object> cdi = CDI.current();
MyService service = cdi.select(MyService.class).get();

三、@Inject——依赖注入

1. 字段注入(Field Injection)

最常用的注入方式,直接在字段上使用 @Inject

1
2
3
4
5
6
7
8
9
import jakarta.inject.Inject;

public class OrderService {
    @Inject
    private UserService userService;  // 字段注入
    
    @Inject
    private PaymentProcessor paymentProcessor;
}

2. 构造方法注入(Constructor Injection)

通过构造方法注入依赖,推荐方式,便于单元测试:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class OrderService {
    private final UserService userService;
    private final PaymentProcessor paymentProcessor;
    
    @Inject  // CDI 容器调用此构造方法创建实例
    public OrderService(UserService userService, PaymentProcessor paymentProcessor) {
        this.userService = userService;
        this.paymentProcessor = paymentProcessor;
    }
}

3. Setter 方法注入(Setter Injection)

通过 Setter 方法注入依赖:

1
2
3
4
5
6
7
8
public class OrderService {
    private UserService userService;
    
    @Inject
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

4. 注入点总结

注入方式 语法 优点 缺点
字段注入 @Inject private X x; 简洁 不利于单元测试
构造方法注入 @Inject public X(Y y) { ... } 不可变、易测试 代码稍多
Setter 注入 @Inject public void setX(X x) { ... } 可选依赖 可变性

最佳实践:优先使用构造方法注入,保证依赖的不可变性和可测试性。

四、@Qualifier——类型安全的限定符

1. 为什么需要 Qualifier

当一个接口有多个实现时,CDI 容器无法确定注入哪个实现,会抛出 AmbiguousResolutionException@Qualifier 用于区分同一类型的多个实现

2. 自定义 Qualifier

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import jakarta.inject.Qualifier;
import java.lang.annotation.*;

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER})
public @interface PaymentType {
    PaymentMethod value();
}

public enum PaymentMethod {
    CREDIT_CARD,
    PAYPAL,
    WECHAT
}

3. 使用 Qualifier

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 实现类1:信用卡支付
@Stateless
@PaymentType(PaymentMethod.CREDIT_CARD)
public class CreditCardPayment implements PaymentProcessor {
    @Override
    public void process(double amount) {
        System.out.println("信用卡支付:" + amount);
    }
}

// 实现类2:微信支付
@Stateless
@PaymentType(PaymentMethod.WECHAT)
public class WechatPayment implements PaymentProcessor {
    @Override
    public void process(double amount) {
        System.out.println("微信支付:" + amount);
    }
}

// 使用 Qualifier 指定具体实现
@Stateless
public class OrderService {
    @Inject
    @PaymentType(PaymentMethod.WECHAT)  // 明确指定使用微信支付
    private PaymentProcessor paymentProcessor;
}

4. 标准 Qualifier:@Default 和 @Named

CDI 提供了两个内置的 Qualifier:

  • @Default默认限定符。当没有指定任何 Qualifier 时,CDI 容器默认使用 @Default
  • @Named:通过名称来区分 Bean,常与 EL 表达式配合使用。
1
2
3
4
5
6
7
8
9
// 使用 @Named 给 Bean 起名
@Named("userService")
@ApplicationScoped
public class UserService {
    // ...
}

// 在 JSP/JSF 中通过 EL 访问
// ${userService.getUser(1)}

五、作用域(Scope)

作用域决定了 CDI Bean 的生命周期可见性。CDI 提供了多个内置作用域。

1. @Dependent——默认作用域

@Dependent默认作用域,不需要显式标注。它的生命周期完全依赖于注入它的对象——每个注入点都有自己的实例。

1
2
3
4
// 默认就是 @Dependent,无需显式标注
public class HelperService {
    // 每次注入都会创建一个新实例
}

2. @RequestScoped——请求作用域

@RequestScoped 的 Bean 生命周期与单个 HTTP 请求绑定。同一个请求内多次注入得到同一个实例,不同请求得到不同实例。

1
2
3
4
5
6
7
8
import jakarta.enterprise.context.RequestScoped;

@RequestScoped
public class RequestContext {
    private String requestId;
    private Long userId;
    // 同一个请求内共享
}

3. @SessionScoped——会话作用域

@SessionScoped 的 Bean 生命周期与 HTTP Session 绑定。同一个用户会话内多次注入得到同一个实例。

1
2
3
4
5
6
7
8
9
import jakarta.enterprise.context.SessionScoped;
import java.io.Serializable;

@SessionScoped
public class UserSession implements Serializable {
    private String username;
    private List<String> permissions;
    // 同一个用户会话内共享
}

4. @ApplicationScoped——应用作用域

@ApplicationScoped 的 Bean 生命周期与整个应用绑定,所有用户共享同一个实例。

1
2
3
4
5
6
7
8
import jakarta.enterprise.context.ApplicationScoped;

@ApplicationScoped
public class SystemConfig {
    private String appName;
    private int maxRetryCount;
    // 整个应用只有一个实例
}

5. @ConversationScoped——对话作用域

@ConversationScoped 用于跨多个请求保持状态,但比 Session 更细粒度(如多步表单、向导流程)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import jakarta.enterprise.context.ConversationScoped;
import jakarta.enterprise.context.Conversation;
import jakarta.inject.Inject;

@ConversationScoped
public class WizardData implements Serializable {
    private String step1Data;
    private String step2Data;
    
    @Inject
    private Conversation conversation;
    
    public void beginConversation() {
        conversation.begin();
    }
    
    public void endConversation() {
        conversation.end();
    }
}

6. 作用域总结

作用域 注解 生命周期 适用场景
@Dependent 默认 随注入者 无状态工具类
@RequestScoped @RequestScoped 单个 HTTP 请求 请求上下文、参数传递
@SessionScoped @SessionScoped HTTP Session 用户登录状态、购物车
@ApplicationScoped @ApplicationScoped 整个应用 全局配置、缓存
@ConversationScoped @ConversationScoped 手动控制 多步表单、向导

六、拦截器(Interceptor)

1. 什么是拦截器

CDI 拦截器(Interceptor)用于在业务方法执行前后插入横切逻辑(如日志、事务、性能监控、权限校验等)。它与 EJB 的 @Interceptors 注解类似,但更灵活、更强大

2. 拦截器的核心概念

使用 CDI 拦截器需要三个步骤:

  1. 定义拦截器绑定(Interceptor Binding) —— 一个自定义注解,用于标记哪些方法需要被拦截;
  2. 实现拦截器类 —— 带有 @Interceptor 注解的类,包含拦截逻辑;
  3. 应用拦截器绑定 —— 在目标类或方法上使用拦截器绑定注解。

3. 完整示例

步骤 1:定义拦截器绑定

1
2
3
4
5
6
7
8
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.*;

@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Logged {
}

步骤 2:实现拦截器类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

@Logged
@Interceptor
public class LoggingInterceptor {
    
    @AroundInvoke
    public Object logMethod(InvocationContext ctx) throws Exception {
        // 方法执行前
        long start = System.currentTimeMillis();
        String methodName = ctx.getMethod().getName();
        System.out.println("【开始】" + methodName);
        
        try {
            // 执行目标方法
            Object result = ctx.proceed();
            
            // 方法执行后(正常返回)
            long cost = System.currentTimeMillis() - start;
            System.out.println("【完成】" + methodName + ",耗时:" + cost + "ms");
            return result;
        } catch (Exception e) {
            // 方法执行后(抛出异常)
            System.out.println("【异常】" + methodName + ":" + e.getMessage());
            throw e;
        }
    }
}

步骤 3:应用拦截器绑定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Stateless
public class OrderService {
    
    @Logged  // 该方法会被拦截
    public void createOrder(Order order) {
        // 业务逻辑
    }
    
    public void getOrder(Long id) {
        // 不会被拦截
    }
}

4. 拦截器优先级

从 CDI 1.1 开始,可以使用 @Priority 注解控制拦截器的执行顺序——数字越小,优先级越高,越先执行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import jakarta.annotation.Priority;

@Logged
@Interceptor
@Priority(Interceptor.Priority.APPLICATION)
public class LoggingInterceptor {
    // ...
}

@Secured
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 100)
public class SecurityInterceptor {
    // ...
}

5. 拦截器 vs EJB @Interceptors

对比维度 CDI 拦截器 EJB @Interceptors
绑定方式 通过注解(Interceptor Binding) 通过 @Interceptors(Class)
类型安全 是(使用自定义注解) 否(使用类引用)
优先级控制 支持(@Priority 不支持
适用范围 CDI Bean + EJB 仅 EJB
灵活性 更高 较低

七、装饰器(Decorator)

1. 什么是装饰器

装饰器(Decorator)与拦截器类似,但目的不同

  • 拦截器:在方法执行前后插入横切逻辑(关注“何时”执行);
  • 装饰器:增强或修改业务接口的行为(关注“做什么”)。

装饰器实现了业务接口,可以包装原始实现并添加额外功能。

2. 装饰器示例

业务接口

1
2
3
public interface PaymentService {
    void pay(double amount);
}

原始实现

1
2
3
4
5
6
7
@Stateless
public class DefaultPaymentService implements PaymentService {
    @Override
    public void pay(double amount) {
        System.out.println("支付:" + amount);
    }
}

装饰器(增强支付行为——添加日志和风控检查):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import jakarta.decorator.Decorator;
import jakarta.decorator.Delegate;
import jakarta.inject.Inject;

@Decorator
public class PaymentDecorator implements PaymentService {
    
    @Inject
    @Delegate
    private PaymentService delegate;  // 原始实现
    
    @Override
    public void pay(double amount) {
        // 前置增强:风控检查
        if (amount > 10000) {
            System.out.println("【风控】大额支付,需人工审核");
        }
        
        // 调用原始实现
        delegate.pay(amount);
        
        // 后置增强:记录日志
        System.out.println("【日志】支付完成:" + amount);
    }
}

启用装饰器(需要在 beans.xml 中配置):

1
2
3
4
5
<beans>
    <decorators>
        <class>com.example.PaymentDecorator</class>
    </decorators>
</beans>

3. 装饰器 vs 拦截器

对比维度 拦截器(Interceptor) 装饰器(Decorator)
关注点 横切逻辑(日志、事务、安全) 业务逻辑增强
是否实现业务接口
执行时机 方法调用前后 方法调用前后(但可修改返回值)
适用场景 通用横切关注点 业务行为的增量修改

八、事件机制(Event)

1. 什么是 CDI 事件

CDI 事件机制实现了观察者模式,允许 Bean 之间进行松耦合的通信。发送方(生产者)通过 Event 接口发布事件,接收方(观察者)通过 @Observes@ObservesAsync 注解监听事件。

一句话总结:CDI 事件 = 发布-订阅模式的标准实现,实现 Bean 之间的解耦通信。

2. 同步事件

步骤 1:定义事件类

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class UserLoginEvent {
    private Long userId;
    private String username;
    private LocalDateTime loginTime;
    
    public UserLoginEvent(Long userId, String username) {
        this.userId = userId;
        this.username = username;
        this.loginTime = LocalDateTime.now();
    }
    // getter/setter
}

步骤 2:发布事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;

@Stateless
public class LoginService {
    
    @Inject
    private Event<UserLoginEvent> loginEvent;  // 注入事件发布器
    
    public void login(String username, String password) {
        // 登录逻辑...
        Long userId = 123L;
        
        // 发布事件
        loginEvent.fire(new UserLoginEvent(userId, username));
        System.out.println("登录事件已发布");
    }
}

步骤 3:监听事件(同步)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import jakarta.enterprise.event.Observes;

@Stateless
public class LoginLogger {
    
    // 观察者方法:使用 @Observes 监听事件
    public void onUserLogin(@Observes UserLoginEvent event) {
        System.out.println("【日志】用户登录:" + event.getUsername() 
            + ",时间:" + event.getLoginTime());
    }
}

@Stateless
public class NotificationService {
    
    public void sendWelcomeEmail(@Observes UserLoginEvent event) {
        System.out.println("【邮件】发送欢迎邮件给:" + event.getUsername());
    }
}

3. 异步事件

CDI 2.0 引入了异步事件支持,通过 Event.fireAsync()@ObservesAsync 实现非阻塞的事件处理。

发布异步事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Stateless
public class LoginService {
    @Inject
    private Event<UserLoginEvent> loginEvent;
    
    public void login(String username, String password) {
        // ...
        // 异步发布事件(不阻塞主线程)
        loginEvent.fireAsync(new UserLoginEvent(userId, username));
        System.out.println("异步登录事件已发布");
    }
}

异步监听

1
2
3
4
5
6
7
8
9
@Stateless
public class AsyncNotificationService {
    
    // 异步观察者:使用 @ObservesAsync
    public void sendEmailAsync(@ObservesAsync UserLoginEvent event) {
        // 在独立线程中执行,不阻塞主流程
        System.out.println("【异步邮件】发送欢迎邮件给:" + event.getUsername());
    }
}

4. 带 Qualifier 的事件

事件可以结合 Qualifier 实现精细化的消息分发:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 定义 Qualifier
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.PARAMETER})
public @interface LoginEventType {
    LoginType value();
}

public enum LoginType { SUCCESS, FAILED }

// 发布带 Qualifier 的事件
@Inject
@LoginEventType(LoginType.SUCCESS)
private Event<UserLoginEvent> successEvent;

@Inject
@LoginEventType(LoginType.FAILED)
private Event<UserLoginEvent> failedEvent;

public void login(String username, String password) {
    if (valid) {
        successEvent.fire(new UserLoginEvent(userId, username));
    } else {
        failedEvent.fire(new UserLoginEvent(null, username));
    }
}

// 监听特定类型的事件
public void onLoginSuccess(@Observes @LoginEventType(LoginType.SUCCESS) UserLoginEvent event) {
    // 只处理登录成功事件
}

5. 事件机制总结

特性 同步事件 异步事件
发布方法 Event.fire() Event.fireAsync()
监听注解 @Observes @ObservesAsync
执行线程 发布者线程 独立线程
是否阻塞 是(等待所有观察者完成)
异常处理 观察者异常会传播到发布者 观察者异常不影响发布者

九、CDI 与 EJB 的整合

1. EJB 可以使用 CDI 注入

EJB(无状态、有状态、单例)中可以使用 @Inject 注入 CDI Bean:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;

@Stateless
public class OrderService {
    
    @Inject
    private PaymentProcessor paymentProcessor;  // CDI Bean
    
    @Inject
    private SystemConfig config;  // CDI Bean
    
    public void processOrder(Order order) {
        paymentProcessor.process(order.getAmount());
    }
}

2. CDI Bean 可以注入 EJB

CDI Bean 中可以使用 @Inject@EJB 注入 EJB:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import jakarta.ejb.EJB;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Named;

@Named("userController")
@RequestScoped
public class UserController {
    
    @EJB
    private UserService userService;  // 注入 EJB
    
    @Inject
    private UserSession session;  // 注入 CDI Bean
    
    public User getUser(Long id) {
        return userService.findById(id);
    }
}

3. EJB 注解与 CDI 注解可以共存

同一个类可以同时使用 EJB 注解和 CDI 注解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import jakarta.ejb.Stateless;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Named;

@Stateless        // EJB 容器管理
@ApplicationScoped // CDI 容器管理
@Named("configService")  // 可通过 EL 访问
public class SystemConfigService {
    // 这个类同时是 EJB 和 CDI Bean
    // 既享受 EJB 的事务管理,又享受 CDI 的依赖注入和作用域管理
}

4. @Inject vs @EJB 的选择

对比维度 @Inject @EJB
适用对象 CDI Bean 和 EJB 仅 EJB
是否支持远程 EJB
类型安全
推荐用法 注入本地 EJB 和 CDI Bean 注入远程 EJB

最佳实践

  • 注入本地 EJB:使用 @Inject(与 CDI 统一风格);
  • 注入远程 EJB:使用 @EJB
  • 注入CDI Bean:使用 @Inject

十、与 Spring IoC 的对比

1. 定位不同

对比维度 CDI Spring IoC
类型 Jakarta EE 标准规范 第三方框架
范围 依赖注入 + 上下文管理 完整的 IoC 容器 + 生态(Spring Boot、Spring Cloud 等)
体积 轻量(规范本身很小) 较大(完整生态)
标准化 是(Jakarta EE 标准) 否(第三方)

2. 注解对比

功能 CDI Spring
依赖注入 @Inject @Autowired
命名 @Named @Component@Service
作用域 @RequestScoped@SessionScoped@ApplicationScoped @RequestScope@SessionScope@ApplicationScope
限定符 @Qualifier @Qualifier
构造方法注入 @Inject 在构造方法上 @Autowired 在构造方法上

3. 作用域对比

概念 CDI Spring
默认作用域 @Dependent singleton
请求作用域 @RequestScoped @RequestScope
会话作用域 @SessionScoped @SessionScope
应用作用域 @ApplicationScoped @ApplicationScope
原型作用域 无直接等价(@Dependent 类似) @Scope("prototype")

4. 核心差异

  1. 标准化 vs 生态化:CDI 是 Jakarta EE 的标准规范,Spring 是完整框架生态
  2. 动态 vs 静态:CDI 在运行时解析依赖,Spring 在容器启动时完成依赖注入;
  3. 作用域实现:CDI 为每个作用域创建独立容器,Spring 的 Scope 是容器内的管理手段;
  4. 学习曲线:CDI 更轻量、更专注,Spring 更全面、更复杂。

5. 选型建议

场景 推荐
Jakarta EE 标准应用 CDI(天然集成 EJB、JPA、JAX-RS 等)
Spring Boot 项目 Spring IoC(生态完善)
追求标准化、可移植性 CDI
需要完整的微服务生态 Spring(Spring Cloud 更成熟)
云原生、GraalVM 原生镜像 CDI + Quarkus 或 Spring Native

十一、实战案例:用户登录系统

1. 业务需求

实现一个用户登录系统,包含:

  • 登录逻辑(UserService)
  • 登录状态管理(UserSession——Session 作用域)
  • 登录日志(通过拦截器自动记录)
  • 登录成功事件(发送欢迎邮件和记录审计日志)

2. 代码实现

实体类

1
2
3
4
5
6
public class User {
    private Long id;
    private String username;
    private String password;
    // getter/setter
}

Service 层(EJB + CDI)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import jakarta.ejb.Stateless;
import jakarta.inject.Inject;

@Stateless
public class UserService {
    
    @Inject
    private UserRepository userRepository;  // CDI Bean
    
    public User findByUsername(String username) {
        return userRepository.findByUsername(username);
    }
    
    public boolean validatePassword(User user, String password) {
        return user.getPassword().equals(password);
    }
}

用户会话(Session 作用域)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import jakarta.enterprise.context.SessionScoped;
import jakarta.inject.Named;
import java.io.Serializable;

@Named("userSession")
@SessionScoped
public class UserSession implements Serializable {
    private User currentUser;
    private boolean loggedIn;
    
    public void login(User user) {
        this.currentUser = user;
        this.loggedIn = true;
    }
    
    public void logout() {
        this.currentUser = null;
        this.loggedIn = false;
    }
    
    public User getCurrentUser() { return currentUser; }
    public boolean isLoggedIn() { return loggedIn; }
}

登录事件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class LoginEvent {
    private User user;
    private LocalDateTime loginTime;
    
    public LoginEvent(User user) {
        this.user = user;
        this.loginTime = LocalDateTime.now();
    }
    // getter
}

登录服务(发布事件)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import jakarta.ejb.Stateless;
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;

@Stateless
public class LoginService {
    
    @Inject
    private UserService userService;
    
    @Inject
    private UserSession userSession;
    
    @Inject
    private Event<LoginEvent> loginEvent;
    
    public boolean login(String username, String password) {
        User user = userService.findByUsername(username);
        if (user == null || !userService.validatePassword(user, password)) {
            return false;
        }
        
        // 保存登录状态
        userSession.login(user);
        
        // 发布登录事件
        loginEvent.fire(new LoginEvent(user));
        
        return true;
    }
}

拦截器绑定

1
2
3
4
5
6
7
8
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.*;

@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Logged {
}

日志拦截器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;

@Logged
@Interceptor
public class LoggingInterceptor {
    
    @AroundInvoke
    public Object log(InvocationContext ctx) throws Exception {
        String method = ctx.getMethod().getName();
        System.out.println("【调用】" + method + ",参数:" + 
            java.util.Arrays.toString(ctx.getParameters()));
        
        long start = System.currentTimeMillis();
        try {
            Object result = ctx.proceed();
            System.out.println("【返回】" + method + ",耗时:" + 
                (System.currentTimeMillis() - start) + "ms");
            return result;
        } catch (Exception e) {
            System.out.println("【异常】" + method + ":" + e.getMessage());
            throw e;
        }
    }
}

事件观察者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import jakarta.ejb.Stateless;
import jakarta.enterprise.event.Observes;

@Stateless
public class LoginEventHandlers {
    
    // 发送欢迎邮件
    public void sendWelcomeEmail(@Observes LoginEvent event) {
        System.out.println("【邮件】欢迎 " + event.getUser().getUsername() + " 登录");
    }
    
    // 记录审计日志
    public void auditLogin(@Observes LoginEvent event) {
        System.out.println("【审计】用户 " + event.getUser().getUsername() + 
            " 于 " + event.getLoginTime() + " 登录系统");
    }
}

登录 Servlet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import jakarta.ejb.EJB;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    
    @EJB
    private LoginService loginService;
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        
        if (loginService.login(username, password)) {
            resp.sendRedirect("/home");
        } else {
            resp.sendRedirect("/login.html?error=1");
        }
    }
}

3. 架构图

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
┌─────────────────────────────────────────────────────────────────────────┐
│                           客户端(浏览器)                              │
└────────────────────────────────┬────────────────────────────────────────┘
                                 │ POST /login
┌─────────────────────────────────────────────────────────────────────────┐
│                         Servlet(Web 层)                              │
│                      LoginServlet.doPost()                             │
│                         调用 LoginService                              │
└────────────────────────────────┬────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│                     EJB + CDI 层(业务逻辑)                           │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    LoginService(@Stateless)                   │   │
│  │  ┌──────────────────────────────────────────────────────────┐  │   │
│  │  │  @Inject UserService  (EJB + CDI)                     │  │   │
│  │  │  @Inject UserSession  (@SessionScoped)                │  │   │
│  │  │  @Inject Event<LoginEvent>  (事件发布)                 │  │   │
│  │  └──────────────────────────────────────────────────────────┘  │   │
│  └─────────────────────────────────────────────────────────────────┘   │
│                              │                                         │
│               ┌──────────────┼──────────────┐                         │
│               ▼              ▼              ▼                         │
│  ┌─────────────────┐ ┌─────────────┐ ┌─────────────────────────┐     │
│  │  UserService    │ │ UserSession │ │  LoginEventHandlers     │     │
│  │  (EJB)        │ │(Session)  │ │  (观察者)              │     │
│  │  查询用户       │ │ 保存登录态  │ │  发送邮件、审计日志      │     │
│  └─────────────────┘ └─────────────┘ └─────────────────────────┘     │
│                                                                        │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │              LoggingInterceptor(@Interceptor)                 │   │
│  │              自动记录所有 @Logged 方法的调用日志                │   │
│  └─────────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────────┘

十二、总结

1. 核心知识脉络

层级 技术 核心概念
依赖注入 @Inject 字段注入、构造方法注入、Setter 注入
类型安全 @Qualifier 自定义限定符、@Named@Default
生命周期 Scope @Dependent@RequestScoped@SessionScoped@ApplicationScoped
横切逻辑 Interceptor @InterceptorBinding@AroundInvoke@Priority
行为增强 Decorator @Decorator@Delegate
松耦合通信 Event Event.fire()@Observes@ObservesAsync
与 EJB 整合 @Inject + @EJB EJB 注入 CDI、CDI 注入 EJB

2. 关键理解

  1. CDI 是 Jakarta EE 的“万能胶水”:它连接了 Web 层、EJB 层和 JPA 层,是整个平台的依赖注入和上下文管理基础;
  2. 任何 POJO 都可以是 CDI Bean:无需继承特定类或实现特定接口,极大降低了使用门槛;
  3. 作用域管理生命周期:通过 @RequestScoped@SessionScoped@ApplicationScoped 等注解,精确控制 Bean 的生命周期和可见性;
  4. 类型安全的依赖注入:通过 @Qualifier 解决同一接口多个实现的问题,编译时即可发现错误;
  5. 拦截器处理横切关注点:日志、事务、安全等横切逻辑通过拦截器统一处理,业务代码更纯净;
  6. 事件机制实现松耦合:发布者不关心谁监听,监听者不关心谁发布,组件间完全解耦;
  7. 与 EJB 深度整合:EJB 可以使用 CDI 注入,CDI Bean 可以注入 EJB,两者无缝协作。

3. CDI 在现代架构中的定位

虽然 Spring 框架在轻量级企业应用开发中占据主导地位,但 CDI 作为 Jakarta EE 的核心规范,在以下场景中仍有重要价值:

  • 使用 Jakarta EE 应用服务器(WildFly、Payara)的企业项目;
  • 追求标准化、避免供应商锁定的场景;
  • 与 EJB、JPA、JAX-RS 等 Jakarta EE 规范深度集成的项目;
  • 云原生微服务(配合 MicroProfile 和 Quarkus)。

更重要的是,理解 CDI 的设计思想——类型安全的依赖注入、上下文管理、事件驱动——对理解整个 Jakarta EE 体系、乃至理解 Spring 等框架的底层原理都有极大帮助。