3.2 依赖注入的三种姿势


3.2 依赖注入的三种注入姿势

本节摘要:同一个"把依赖递进来"的想法有三种落法:构造器注入、setter 注入、字段注入。本节给出三段对等的代码,从不可变性、可测性、循环依赖暴露时机三个维度做取舍,并给出我在真实项目里的默认选择与例外情况。

三段对等的代码

同一个控制器,三种写法并排放:

// 姿势一:构造器注入(Lombok 或记录类还能再省) @RestController public class OrderController { private final OrderService service; public OrderController(OrderService service) { this.service = service; } } // 姿势二:setter 注入 @RestController public class OrderController { private OrderService service; @Autowired public void setService(OrderService service) { this.service = service; } } // 姿势三:字段注入 @RestController public class OrderController { @Autowired private OrderService service; }

三种都能跑,区别在"什么时候出问题、出什么问题"。

取舍的三个维度

不可变性。 构造器注入的字段能声明成 final,对象构造完成后依赖不可更换,线程安全顺带成立。setter 与字段注入做不到,字段随时可能被改写或忘了赋值,排查时空指针的来源多了一种可能。

可测试性。 单元测试想绕开容器直接构造对象时,构造器注入一行就够:

OrderService fake = new StubOrderService(); OrderController controller = new OrderController(fake);

字段注入则必须依赖反射工具或起完整容器,测试代码反而更重。

循环依赖的暴露时机。 A 依赖 B、B 又依赖 A,构造器注入会在启动期直接报错——早失败是好事,逼你在设计期解开环。setter 与字段注入在过去能靠容器的提前暴露机制侥幸通过,问题被推迟到运行期才爆。新版本框架已默认收紧这一豁免,循环依赖越来越难藏身。

维度 构造器注入 setter 注入 字段注入
不可变支持 支持 final 不支持 不支持
脱容器测试 直接 new 需手动调用 setter 需反射工具
循环依赖 启动期暴露 可能延迟暴露 可能延迟暴露
适用场景 默认选择 可选依赖、需重配置 快速演示

我的默认立场:业务代码一律构造器注入;只有依赖确实可选(没有也能工作)时才用 setter;字段注入只在写演示片段时用。参数过多导致构造器难看时,那不是注入姿势的问题,是这个类职责太多该拆了。

图 3-2 注入姿势决策路径

图 3-2 注入姿势决策路径

💡 关键直觉:注入姿势的选择本质是"把错误往哪个阶段推"。构造器注入把错误推到启动期,是三个里唯一能让问题在部署前现形的。

实战:一次循环依赖的暴露与解开

背景:评审中发现订单服务要调库存服务,库存服务又想回调订单服务查明细,两个类都用构造器注入。操作:直接启动应用,启动期立刻失败,报错信息里画出了依赖环:订单服务构造时需要库存服务,库存服务构造时又回头要订单服务。这正是构造器注入的价值——问题在部署前现形,而不是上线后在某个深夜的请求里炸开。

解法按优先级排三种。第一种(首选):解开环。把库存服务需要的"查明细"抽到第三方类里,环变成两条不回头的链,依赖关系重新成为有向无环图。第二种:改事件。库存服务不再主动查订单,改为订阅"订单创建"事件,第 5 章的事件机制专门服务这种解耦。第三种(兜底):对确实无法拆分的遗留代码,用延迟注入注解让其中一方在使用时才解析,环在运行期被绕开——但要明白这是止痛不是治病,能拆还是拆。

结果验证:解环后启动成功,且两个类的职责都比原来更清楚。解读:循环依赖几乎总是设计问题的信号——要么两个类其实该合并,要么该抽出的公共逻辑被漏掉了。把它当成重构提示而不是报错去消灭,收获会大得多。

三个边界问题

其一,同类型有多个实现时注入哪一个?容器默认因歧义而报错,解法有三种:给其中一个标首选、用名字匹配、或在注入点加限定注解指名道姓。团队规范建议用限定注解——它把选择语义写在类型旁边,比散落的名字约定可读。其二,可选依赖怎么表达?在构造器参数上标注"无则给空",容器找不到候选时不报错而传空值;但真正的可选依赖很少见,多数"可选"其实该拆成两个 Bean。其三,记录类能不能做配置载体?可以,构造器注入与记录类是天作之合——全字段不可变、无样板,3.3 节会看到它在生命周期回调之外的另一面。姿势之争的终点不是语法偏好,而是"错误暴露时机"的工程判断,这一点贯穿本节始终。

本节要点回顾

  • 三姿势都能跑,差别在错误暴露时机与测试成本
  • 构造器注入是默认答案:不可变、易测、循环依赖早暴露
  • setter 留给真正可选的依赖
  • 构造器膨胀是拆类信号,不是换姿势的理由

读完自测

给自己出三道题:为什么字段注入的单元测试更难写;构造器注入在循环依赖场景下报错发生在哪个阶段,这为什么是优点;你的项目里哪些注入点可以立刻改成构造器注入。第三题落到自己的代码上,本节才算学完。

最后用一次真实取舍收尾:某团队在代码评审里发现一个控制器注入了十四个依赖,第一反应是改用字段注入让代码好看点。正确的处置是把类拆成三个各带三四个依赖的协作类——注入姿势掩盖不了设计问题,反而会把它藏到运行期。把这个案例讲给同事听,本节的价值观就传递出去了:注入姿势是术,职责划分才是道。
顺带回应一个常见顾虑:构造器注入多起来的构造函数样板,现代开发环境用一个注解即可生成,记录类甚至连构造器都不用手写——工具链早已消化了语法成本,剩下的选择理由里不再有"省事"这一条,只有工程判断。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U