CDI如何实现类似Guice Assisted Inject的非托管依赖注入?
当然可以!CDI(Contexts and Dependency Injection)在Java EE环境下完全能实现类似Guice AssistedInject的功能——也就是创建同时包含托管依赖(比如EJB、CDI托管bean)和非托管依赖(比如调用时传入的业务参数)的实例。结合你提到的命令模式场景,我来一步步拆解实现方式:
核心思路
本质上是通过一个CDI托管的工厂类,将容器管理的依赖注入到工厂中,然后在工厂的方法里,把这些托管依赖和调用时传入的非托管参数组合起来,生成目标实例。如果是EJB这类特殊的托管组件,只需要稍微调整实例获取的方式即可。
具体实现步骤
1. 定义基础命令接口
先把你提到的Command接口明确下来:
public interface Command { void execute() throws CommandException; } // 自定义异常类 public class CommandException extends Exception { public CommandException(String message) { super(message); } public CommandException(String message, Throwable cause) { super(message, cause); } }
2. 准备托管依赖与混合依赖的Command实现
假设我们需要一个处理订单的Command,它依赖一个托管的事务型EJB,同时需要调用时传入订单ID作为非托管参数:
托管的EJB服务
@Stateless public class OrderProcessingService { // 这里可以注入其他EJB/CDI bean,或者使用JPA处理事务 public void processOrder(String orderId) { // 实际的订单处理逻辑,自动享受EJB的事务管理 System.out.println("处理订单:" + orderId); } }
带有混合依赖的Command实现
如果Command是普通CDI bean(非EJB),可以直接用带参数的构造方法:
// 普通CDI bean,也可以加@RequestScoped等作用域注解 public class ProcessOrderCommand implements Command { private final OrderProcessingService orderService; // 托管依赖,由CDI注入 private final String orderId; // 非托管参数,调用时传入 // 构造方法:组合托管依赖和非托管参数 public ProcessOrderCommand(OrderProcessingService orderService, String orderId) { this.orderService = orderService; this.orderId = orderId; } @Override public void execute() throws CommandException { try { orderService.processOrder(orderId); } catch (Exception e) { throw new CommandException("订单处理失败", e); } } }
如果Command本身是@Stateless的EJB(需要容器管理事务),由于EJB要求无参构造方法,我们可以通过setter传递非托管参数:
@Stateless public class ProcessOrderCommand implements Command { @Inject private OrderProcessingService orderService; // 托管依赖 private String orderId; // 非托管参数 // EJB必须有无参构造方法 public ProcessOrderCommand() {} // 用于设置非托管参数 public void setOrderId(String orderId) { this.orderId = orderId; } @Override public void execute() throws CommandException { if (orderId == null) { throw new CommandException("未设置订单ID"); } try { orderService.processOrder(orderId); } catch (Exception e) { throw new CommandException("订单处理失败", e); } } }
3. 实现CDI托管的工厂类
工厂类负责注入托管依赖,并生成包含混合依赖的Command实例。
针对普通CDI bean的工厂
@ApplicationScoped // 让工厂成为CDI托管bean public class CommandFactory { @Inject private OrderProcessingService orderService; // 注入托管依赖 // 工厂方法:接收非托管参数,生成Command实例 public ProcessOrderCommand createProcessOrderCommand(String orderId) { return new ProcessOrderCommand(orderService, orderId); } // 如果有其他Command实现,可以在这里添加对应的create方法 }
针对EJB类型Command的工厂
由于EJB实例必须由容器创建,我们用Instance接口获取EJB实例:
@ApplicationScoped public class CommandFactory { @Inject private Instance<ProcessOrderCommand> processOrderCommandInstance; public ProcessOrderCommand createProcessOrderCommand(String orderId) { ProcessOrderCommand command = processOrderCommandInstance.get(); command.setOrderId(orderId); return command; } }
4. 在Controller中使用工厂
最后,在你的CommandController里注入工厂,生成Command并执行:
@Stateless // 假设Controller是EJB,享受事务管理 public class CommandController { @Inject private CommandFactory commandFactory; public void execute(Queue<String> orderIdQueue) throws CommandException { while (!orderIdQueue.isEmpty()) { String orderId = orderIdQueue.poll(); // 生成带有托管依赖和非托管参数的Command实例 Command command = commandFactory.createProcessOrderCommand(orderId); command.execute(); } } }
进阶简化:使用DeltaSpike AssistedInject
如果你想更接近Guice AssistedInject的简洁体验,不需要手动编写工厂类,可以使用Apache DeltaSpike的AssistedInject扩展(它是Java EE生态的一部分,和CDI完全兼容):
- 给Command构造方法中的非托管参数添加
@Assisted注解:
public class ProcessOrderCommand implements Command { private final OrderProcessingService orderService; private final String orderId; @Inject public ProcessOrderCommand(OrderProcessingService orderService, @Assisted String orderId) { this.orderService = orderService; this.orderId = orderId; } // ... execute方法不变 }
- 定义工厂接口并添加
@AssistedFactory注解:
@AssistedFactory public interface CommandFactory { ProcessOrderCommand createProcessOrderCommand(String orderId); }
- 直接在Controller中注入这个工厂接口,CDI会自动生成实现类,无需手动编写工厂:
@Stateless public class CommandController { @Inject private CommandFactory commandFactory; // ... 执行逻辑不变 }
总结
不管是用原生CDI手动实现工厂,还是借助DeltaSpike扩展简化代码,都能在Java EE环境下完美实现“托管依赖+非托管参数”的实例装配需求,同时还能无缝结合EJB的事务、安全等托管特性。
内容的提问来源于stack exchange,提问作者user212926

