背景
本文是《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 拦截器需要三个步骤:
- 定义拦截器绑定(Interceptor Binding) —— 一个自定义注解,用于标记哪些方法需要被拦截;
- 实现拦截器类 —— 带有
@Interceptor 注解的类,包含拦截逻辑;
- 应用拦截器绑定 —— 在目标类或方法上使用拦截器绑定注解。
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. 核心差异
- 标准化 vs 生态化:CDI 是 Jakarta EE 的标准规范,Spring 是完整框架生态;
- 动态 vs 静态:CDI 在运行时解析依赖,Spring 在容器启动时完成依赖注入;
- 作用域实现:CDI 为每个作用域创建独立容器,Spring 的 Scope 是容器内的管理手段;
- 学习曲线: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. 关键理解
- CDI 是 Jakarta EE 的“万能胶水”:它连接了 Web 层、EJB 层和 JPA 层,是整个平台的依赖注入和上下文管理基础;
- 任何 POJO 都可以是 CDI Bean:无需继承特定类或实现特定接口,极大降低了使用门槛;
- 作用域管理生命周期:通过
@RequestScoped、@SessionScoped、@ApplicationScoped 等注解,精确控制 Bean 的生命周期和可见性;
- 类型安全的依赖注入:通过
@Qualifier 解决同一接口多个实现的问题,编译时即可发现错误;
- 拦截器处理横切关注点:日志、事务、安全等横切逻辑通过拦截器统一处理,业务代码更纯净;
- 事件机制实现松耦合:发布者不关心谁监听,监听者不关心谁发布,组件间完全解耦;
- 与 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 等框架的底层原理都有极大帮助。