ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring @Autowired 深度指南:注入方式、多Bean处理与排错实战

Spring @Autowired 深度指南:注入方式、多Bean处理与排错实战 1. 从一次典型的报错开始为什么 Autowired 会被这么多开发者惦记做 Java 后端的人几乎天天和 Spring 打交道而Autowired大概是大家最早接触、也最常用到的一个注解。但越是常用的东西越容易在细节上翻车。我记得好几年前有次排障经历一个同事新接手了老项目启动时 Spring 容器直接抛了NoUniqueBeanDefinitionException他愣了半天跟我说“我只是想注入一个接口怎么容器说找到了两个 Bean让我指定名字”。其实这种问题在 Spring 项目里太典型了它背后牵扯到的正是Autowired的注入策略和 Bean 的解析逻辑。先给刚入门的朋友一个概念Autowired是 Spring 提供的自动装配注解用来告诉容器“我需要一个依赖你帮我找好并填进来”。你不需要手动new对象也不需要手工写 setter 调用容器会在合适的生命周期节点把依赖塞进你的 Bean 里。它能用在字段上、setter 方法上、构造器上甚至普通方法参数上只要你写对了Spring 就能自己完成依赖注入Dependency Injection简称 DI。这篇文章我想把Autowired讲透包括底层是怎么匹配 Bean 的、三种注入方式各自有什么坑、遇到同名或同类型多个 Bean 时该怎么选、required属性到底有什么用、跟Resource有什么区别以及我这些年实际踩过的问题和排障方法。无论你是刚学 Spring 的新手还是写了两三年业务代码但没认真抠过原理的工程师我觉得这篇文章都能帮你省下不少排查时间。2. Spring 依赖注入的核心机制Autowired 是怎么找到那个 Bean 的很多同学用Autowired用得很顺手但对它背后的查找逻辑是一团黑盒。我们要理解一个注解不能只记“怎么用”还得知道“容器在哪个环节、按什么规则在找”。这部分我会从 Spring 的 Bean 生命周期讲起尽量用大白话。2.1 类型优先的匹配规则然后才是名字Autowired的默认匹配方式是byType也就是按照字段或者参数声明时的类型去找容器里的 Bean。比如你写了一个OrderRepository接口字段声明为OrderRepositorySpring 就会在容器里找所有OrderRepository类型的 Bean。这里有一个关键细节容器里列出所有候选 Bean 后如果只有一个匹配那直接注入很顺畅如果候选超过一个才会再退回到byName的匹配规则也就是拿字段名去和 Bean 的默认名称做比对。比如字段名叫userRepository容器里刚好有一个 Bean 的名字也叫userRepository那就用这个。这个查找过程发生在 Bean 实例化之后的属性填充populateBean阶段。简单理解就是Spring 先把目标对象的实例建出来然后再去遍历那些被Autowired标记的属性、方法或构造器逐个把依赖填进去。如果依赖找不到又没设置required false那启动阶段就会直接报错你根本不用等到业务代码跑起来才发现问题。2.2 底层是谁在处理这个注解Autowired的实现核心是AutowiredAnnotationBeanPostProcessor。它实现了InstantiationAwareBeanPostProcessor接口在 Spring 容器创建 Bean 的过程中通过postProcessProperties方法去扫描当前 Bean 的字段和方法找到标注了Autowired的元信息然后调用InjectionMetadata去解析依赖并注入。我当年第一次看源码的时候也觉得这套东西绕但具体到使用层面你只需要知道几个结论就够了Autowired支持字段、构造器和任意方法包括 setter 方法。它默认情况下依赖不能为 null找不到 Bean 会直接抛异常。它按类型匹配类型有多个候选时再按名字匹配。解析的优先级是先构造器注入再字段和 setter 方法注入。2.3 构造器只有一个时新版 Spring 连 Autowired 都可以省Spring 4.3 之后有个优化如果一个类只有一个构造器那么这个构造器上的Autowired可以省略不写Spring 会自动把它作为注入点。尤其是 Spring Boot 项目写 Service 时经常看到这种风格Service public class OrderService { private final OrderRepository orderRepository; private final InventoryClient inventoryClient; public OrderService(OrderRepository orderRepository, InventoryClient inventoryClient) { this.orderRepository orderRepository; this.inventoryClient inventoryClient; } }这个写法实际上跟加了Autowired是等价的。我个人非常推荐这种风格理由我会在讲构造器注入的时候再展开。3. 三种注入方式深度对比字段注入、Setter 注入和构造器注入很多新手可能不知道Autowired能用在不同位置其实三种方式的适用场景和坑完全不一样。如果你在项目里见到的代码风格各异那大概率是大家对这些注入方式的理解不在一个层次上。3.1 字段注入写起来最爽但对测试不友好字段注入是最常见的写法尤其在老代码里Service public class OrderService { Autowired private OrderRepository orderRepository; Autowired private PaymentClient paymentClient; }优点非常明显代码最短加依赖只需要加一行字段注解几乎不用额外写构造器或 setter。这也是它在网上教程里出镜率最高的原因。但字段注入有两个容易忽略的问题第一测试环境很尴尬。你要对OrderService的单元测试里注入 mock 对象Spring 容器不启动的话orderRepository就是 null你没法在不启动容器的情况下直接 new 一个 OrderService 然后塞依赖因为字段是 private 的得靠反射或者改代码。这对写单元测试的人不友好。第二类职责容易失控。因为加依赖太容易了很多人就会无节制地往上堆Autowired字段一个 Service 里塞十几个依赖代码熵增就是这么来的。虽然这个问题在构造器注入下也能发生但至少加构造器参数时你会“手疼”多少会犹豫一下。另外还有一个细节字段注入时 Spring 在构造完对象后才会处理字段注入所以如果构造器里需要使用某个Autowired字段那在这个时间点是拿不到值的因为注入发生在构造器之后。3.2 Setter 注入可选依赖的一把好手Setter 注入也可以挂在字段之外的方法上Service public class NotificationService { private MessageSender messageSender; Autowired public void setMessageSender(MessageSender messageSender) { this.messageSender messageSender; } }Setter 注入的好处是你可以随时重新设置依赖比如在测试里直接调用 setter 换成 mock 实现不需要 Spring 容器。这比字段注入要灵活但也带来了一个问题依赖在构造完对象后的某一个时间点才被注入如果在注入之前有代码调用了这个依赖对应的方法就会空指针。实际上在 Spring 5 / Spring Boot 2 时代官方文档里对 setter 注入的态度是“主要给可选依赖用”因为配合Autowired(required false)很好使。如果你有的依赖是可选的比如某些环境下可能没有对应的 Bean那么 setter 注入 required false 是比较优雅的解决方案。3.3 构造器注入最适合业务 Bean 的默认选择构造器注入是我个人在工作中用得最多的方式特别对于那种“这个 Service 缺了那个依赖就无法运行”的场景构造器注入是最能表达意图的做法Service public class PaymentService { private final OrderRepository orderRepository; private final AccountClient accountClient; public PaymentService(OrderRepository orderRepository, AccountClient accountClient) { this.orderRepository orderRepository; this.accountClient accountClient; } }它的优点很明确依赖被final修饰不允许后续被替换对象创建后依赖关系不可变。构造器参数是显式的你在new PaymentService(...)时就必须把依赖传全编译期就能发现遗漏。测试非常方便直接new PaymentService(mockOrderRepo, mockAccountClient)就能测不需要反射。由于 Spring 在创建对象的时候就会把构造器参数准备好所以不会出现“构造器执行时依赖还没注入”的尴尬。如果你在 Spring Boot 项目里看到一个类只有一个构造器Spring 会自动把它当成注入点不需要再加Autowired。我觉得这个特性是 Spring 官方在“姿势正确性”上给的指引等于默认鼓励大家用构造器注入。我把三种方式整理成了一个对比表方便大家直接对照参考注入方式代码简洁度测试友好度依赖不可变性可选依赖支持推荐度字段注入最高较差否一般不推荐用于业务代码Setter 注入中等较好否很好可选依赖场景使用构造器注入较低最好是较差默认首选3.4 我因为“习惯性字段注入”付出的代价之前维护过一个老项目里面全是字段注入启动一切正常但后来要加单元测试的时候苦不堪言。每个类都要写一个ReflectionTestUtils或者InjectMocksMockito 里还得多次 set 私有字段非常痛苦。后来我们花了几周时间把所有 Service 的字段注入改成了构造器注入不仅代码直观看上去清晰了一个量级测试也好写了很多。这里想替很多“业务开发忙懒得改”的朋友说一句写代码的时候省下的十分钟在维护阶段往往要花十个小时来还。依赖注入方式这件事真是越早统一越好。4. 容器里出现多个同类型 BeanQualifier、Primary 和集合注入回到开头那个同学遇到的问题一个接口有两个实现类Autowired一启动就报NoUniqueBeanDefinitionException。这是使用Autowired时踩到率最高的问题之一而且特别容易在项目从小变大、实现类增多的时候突然出现。4.1 Qualifier按名称精确锁定最简单粗暴的解决办法就是在注入点上加Qualifier指定要用的 Bean 名称Service public class ReportService { private final ReportGenerator reportGenerator; public ReportService(Qualifier(pdfReportGenerator) ReportGenerator reportGenerator) { this.reportGenerator reportGenerator; } }这里pdfReportGenerator是 Bean 默认名称即类名首字母小写。如果你在定义 Bean 时显式写了名字比如Service(excelGen)那么注入点就写Qualifier(excelGen)。要注意的是Qualifier和Autowired要成对用别只写一个。构造器注入里Qualifier要写在参数前面字段注入写在字段前setter 注入写在 setter 方法参数前面。4.2 Primary给多个候选 Bean 指定一个“首选”如果你不是每次都想指定名字而是“大多数场景下都用 A个别场景用 B”可以给 A 加上Primary。一旦容器里有多候选Spring 会优先选择标注了Primary的那个。Repository Primary public class PrimaryOrderRepository implements OrderRepository { // ... }这个解决方式适合那种“默认实现 特殊实现”的组合。比如缓存场景下默认用 Redis 实现做压测或者本地调试时再用内存实现。Primary和Qualifier同时存在时Qualifier的优先级更高。也就是说你指定了 nameSpring 就按 name 来不再管Primary。这个优先级顺序务必记牢否则看到“我明明指定了 Qualifier为什么还按 Primary 注入”这类疑问时容易懵。4.3 把多个 Bean 一次性注入到集合还有一类场景和上面刚好相反你不是要选一个而是想把所有同类型的实现都拿到手。比如策略模式里一个订单处理器有多个实现Autowired直接注入一个List就行了Service public class OrderDispatcher { private final ListOrderHandler handlers; public OrderDispatcher(ListOrderHandler handlers) { this.handlers handlers; } }Spring 会按照类型把所有候选 Bean 按顺序塞进这个 List顺序规则是实现类上标注了Order注解的按值升序其次是Ordered接口最后是未排序的。这个 List 注入不是鸡肋功能它其实是很多框架代码的常用技巧。当初我看 Spring Security 的源码时就看见它大量使用这种方式收集各个过滤器链才意识到这个特性的威力。如果你需要按 Bean 名称访问某一个还可以注入MapString, 接口类型key 就是 Bean 名称。这对实现“按类型路由到具体处理类”的代码非常有帮助。4.4 多实现类场景的最优解构造器注入 Qualifier在我自己的项目里遇到多个实现的场景最推荐的是构造器注入 Qualifier。原因在于它把选择显式地写在了调用方之外可读性非常高后续同事接手代码也不需要满世界找接口到底有几个实现。如果多种实现之间存在明显的默认项我会用Primary减少大量重复的Qualifier只在特殊场景里显式指定。也就是说你可以把这个策略总结成两句话默认实现用Primary特殊实现用Qualifier。如果整个项目只有两三个场景用了某个特殊实现不如全部用Qualifier规则越统一越不会出错。5. 进阶用法与细节边界required、方法注入、泛型注入Autowired不是只有“字段上标一下”这么简单。项目做得越深越会遇到一些边界场景这节我把那些不太常见但关键时刻救命的知识点拿出来讲。5.1 required false让它成为一个可选项有的依赖在当前运行环境里“有就用没有也能跑”。比如某些可选的监控组件、某些只在特定 Profile 下才加载的客户端。Autowired默认遇到找不到 Bean 会直接抛异常但加一个required false就能把注入失败变成 “注入 null”Autowired(required false) private TracingSupport tracingSupport; // 或者配合 Optional Autowired private OptionalTracingSupport tracingSupportOptional;用Optional作为注入类型是我更推荐的写法因为它把“可能为空”做成了显式的语法调用的时候必须处理或得情况不容易忘记判空。需要注意required false只对字段和 setter 注入友好。如果用在构造器参数上依然有可能导致构造器里收到 null因为你没法在构造参数上简单标记“缺了就用默认值”。所以可选依赖我一般不建议放在构造器注入上setter 注入是更合适的载体。5.2 普通方法上的 Autowired不只是 setter 才能用Autowired可以标注在任何方法上Spring 会在依赖齐全后自动调用这个方法并传入参数。比如Component public class AppConfigurer { private DataSource dataSource; private CacheManager cacheManager; Autowired public void init(DataSource dataSource, CacheManager cacheManager) { this.dataSource dataSource; this.cacheManager cacheManager; // 做一些初始化逻辑 } }这个用法本质上和 setter 注入没太大区别但它可以一次性接收多个依赖。Spring 会按参数类型逐一解析每个参数。这个方法在 Spring 容器启动时会被调用一次如果你的初始化逻辑依赖了多个组件用这种方法会比写字段再写一个PostConstruct更简洁一点。但有一点必须提防方法执行时机在 Bean 构造完成之后、Bean 完全可用之前。这意味着如果你在方法里调用了某个代理对象的早期方法有概率因为 Bean 还在初始化中而得到意外结果。所以普通方法注入适合做简单赋值和轻量初始化重逻辑还是放ApplicationRunner或EventListener(ApplicationReadyEvent.class)更稳妥。5.3 泛型注入Spring 4.0 之后的大杀器Spring 4.0 开始容器能解析泛型信息。这个能力在处理泛型基类时特别好用。比如你有一个基础服务BaseServiceT然后有两个子类UserService extends BaseServiceUser和OrderService extends BaseServiceOrder在另一个类里可以直接这样写Service public class FacadeService { Autowired private BaseServiceUser userService; Autowired private BaseServiceOrder orderService; }Spring 可以基于泛型类型做精确匹配不需要你自己加Qualifier。这个特性的底层是在ResolvableType的辅助下完成的它在讲解源码时很常见但很多实战开发的同学根本不知道能这么用。我之前给一个做多租户系统的团队做 code review 时看到他们给每个租户策略写了一个单独的子类同时又用Qualifier硬绑我就建议改成泛型注入代码瞬间清爽了很多。5.4 静态字段能用 Autowired 吗这个问题被问过无数遍。结论是常规方式不行Spring 不会自动把依赖注入到 static 字段上因为静态字段不属于某个具体 Bean 实例AutowiredAnnotationBeanPostProcessor 的注入逻辑处理的是实例级别的属性。如果你真的需要静态字段拿到 Bean可以在注入后赋值Component public class AppContextHolder { private static ApplicationContext context; Autowired public void setContext(ApplicationContext context) { AppContextHolder.context context; } }说白了就是用 setter 方法绕了一下。但我不太推荐在业务代码里这么玩静态实例等于在全局放了一个隐式依赖测试和维护都不省心。能用依赖注入解决的问题就别绕到全局静态变量上去。6. 高频问题排查与避坑实录这部分我想集中写一下真实项目里和Autowired相关的高频问题和排查思路。每个问题都是我见过很多次有些坑自己也踩过的基本都能对应到现实的报错栈。6.1 NoUniqueBeanDefinitionException候选 Bean 不止一个报错信息大概长这样No qualifying bean of type com.example.ReportGenerator available: expected single matching bean but found 2: pdfReportGenerator,excelReportGenerator这个问题的排查思路是三步第一步确认接口有几个实现类是不是都是 Spring Bean。可以在 IDE 的Hierarchy视图里直接看实现类列表。第二步决定方案默认实现加Primary特殊点加Qualifier或者用集合注入直接拿到全部实现。第三步如果上了Primary还是报错看是不是有多个实现都标了Primary。这种情况 Spring 也无能为力需要把多余的Primary去掉或者仍然用Qualifier精确指定。一个小技巧你还可以用ObjectProviderT来替代Autowired注入它能在不报错的情况下让你自己决定取单个还是取多个。比如你只想要某个 Bean但不确定容器里有没有就可以objectProvider.getIfAvailable()或者objectProvider.getIfUnique()。6.2 Circular dependency循环依赖为什么换构造器注入就崩循环依赖是很多团队从字段注入转向构造器注入时遇到的第一个挫折。看下面这个例子Service public class ServiceA { private final ServiceB serviceB; public ServiceA(ServiceB serviceB) { this.serviceB serviceB; } } Service public class ServiceB { private final ServiceA serviceA; public ServiceB(ServiceA serviceA) { this.serviceA serviceA; } }启动时直接报The dependencies of some of the beans in the application context form a cycle为什么字段注入的时候没事换成构造器注入就有问题因为 Spring 解决循环依赖靠的是三级缓存核心机制是提前暴露对象的早期引用。字段注入是在 Bean 构造完成后才进行的所以 A 和 B 可以先各自 new 出实例再相互填充字段但构造器注入发生在 new 的阶段这时候 A 需要 B 的实例才能完成构造B 又需要 A 的实例才能完成构造形成一个无法破解的“先有鸡还是先有蛋”的局面。如果团队代码里出现了这种循环依赖正确的做法不是退回到字段注入而是重构业务把 A 和 B 互相依赖的那段逻辑拆出来放到第三个组件 C 里让 A 和 B 都只依赖 C。因为循环依赖本身往往是设计问题它表明类的职责边界没切干净。6.3 旧项目里 Autowired 注入的接口结果是 null这种问题一般出现在代码把new和 Spring 管理混用的情况下。比如你在一个普通工具类里new MyService()然后把一个Autowired字段放进去那这个字段肯定是 null因为这个对象不是 Spring 容器创建的AutowiredAnnotationBeanPostProcessor不会处理它。排查思路看这个类是不是被 Spring 扫描到了有没有加Component/Service/Repository。看调用方是通过容器获取的 Bean还是手动 new 出来的。如果确实在一个非 Spring 管理的类里需要 Spring Bean最简单的方式是让这个类也变成 Spring Bean或者在启动类里通过ApplicationContext手动 getBean 塞进去。这个问题最经典的场景是在一个用new创建的监听器或工具类里注入 Repository结果一调就空指针。遇到这种问题别急着怀疑Autowired坏了先确认对象到底是不是容器管的。6.4 单元测试里注入了 null根本启动不了容器很多人在写单元测试时也会遇到类似困扰。如果用的是RunWith(SpringRunner.class)或者SpringBootTest那容器是完整启动的Autowired工作正常。但如果只想做轻量级单元测试不启动容器而是new目标类那依赖自然进不去。这时候最友好的方案就是构造器注入。因为构造器参数是公开的直接传 mock 就能完成实例化。如果你非要测一个字段注入的类也不是不行Mockito 提供了InjectMocks但它底层用的是反射去塞字段遇到多个同类型依赖时偶尔会注入错对象定位问题还更麻烦。6.5 Autowired 与 Resource 的恩恩怨怨这个问题算得上是 Java 面试高频题了这里简单梳理一下。Resource是 JSR-250 标准里的注解Spring 也支持它但它的注入策略是“先按名称再按类型”而Autowired是 Spring 自有的策略是“先按类型再按名称”。对比项AutowiredResource来源Spring 2.5JSR-250默认策略先按类型再按名称先按名称再按类型支持位置字段、构造器、方法字段、setter 方法与 Spring 解耦否是可选依赖支持 requiredfalse不支持必须存在多实现扩展配合 Qualifier / Primary配合 name 属性如果你在项目里看到Resource通常是为了规避“多个同类型 Bean 时按类型匹配报错”的问题等同于给字段指定了一个名称Resource(name userRepository) private UserRepository userRepository;Autowired也有类似能力的Qualifier写法上稍微多一点关键字。至于到底选哪个我的建议是团队内统一即可。我个人更倾向Autowired因为它可以直接用在构造器上声明式更强加上 Spring Boot 生态的默认风格也是Autowired。7. 最佳实践我在真实项目里如何组织依赖注入讲完原理和坑最后分享一点我这些年形成的团队规范。这些规范不是哪里抄来的是在多个项目里被实际验证过的供大家参考。7.1 业务代码默认构造器注入所有需要 DI 的类只要依赖是强制的、必不可少的就统一用构造器注入。类里加final修饰依赖关系一目了然。这是最不容易出错的方案而且在做单元测试的时候最省心。7.2 可选依赖用 setter 注入当一个依赖可有可无时用Autowired(required false) setter 注入。如果这种依赖很多也可以用OptionalT包装一下。这样表面上的“可选”变成了语言层面强制处理的“可能为空”代码鲁棒性更强。7.3 多实现类优先考虑集合注入策略模式、模板方法模式在多实现类场景下特别常见。与其到处写Qualifier不如直接注入ListHandler或者MapString, Handler。前者适合“全部执行一遍”后者适合“按名称精确路由”。这种方式的扩展性非常好以后新增一个实现类什么都不用改只要类上加了注解容器就会自动收进来。7.4 禁止在循环依赖里用字段注入“解围”很多老代码里循环依赖之所以存在就是因为字段注入让构建过程可以绕过去。但循环依赖本身是设计有味道的信号迟早会在某些边缘条件下炸坑。我见过线上服务因为循环依赖的代理类型问题在 AOP 场景下表现怪异排查起来非常痛苦。所以建议团队在 code review 时看到循环依赖直接打回要求重构。7.5 把 Autowired 当成一种“声明”最后想说的其实是一种思维模式Autowired不是让你到处省事的魔法它是在声明“这个类的运行需要谁”。好的代码应该让人一眼就能看出依赖关系而不是靠“启动跑一遍”才能验证。你在设计类时多想想它的构造器需要几个参数这些参数合不合理当前类是不是塞了太多职责。照这个思路去写你的类会越来越清晰依赖关系也会越来越健康。我在实际项目里用Autowired这么多年最大的一个感悟就是注解本身不复杂复杂的是它背后那条依赖关系的链。我们把链理顺了代码的维护成本能降一大截。希望这篇文章能帮你在使用Autowired时少走一些弯路。
返回列表