十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Spring @Autowired 匹配机制:从源码到实战的完整解析

Spring @Autowired 匹配机制:从源码到实战的完整解析 有一次我在一个老项目里加上一个新的接口实现类启动时Spring直接给我甩了个红牌No qualifying bean of type xxx.PaymentService available: expected single matching bean but found 2: alipayService,wechatPayService当时我的第一反应是Spring 不是号称“智能容器”吗两个实现类到底注入哪个它自己心里没点数吗凭什么把选择权丢给我后来我把 Spring 的源码一路追下去才发现Autowired 的“魔法”并不是猜心术而是一套有明确优先级、有完整后备方案的匹配机制。Spring 不是找不到 Bean而是它太想找到了——只有当它用尽所有策略仍然无法唯一确定时才会把这个“幸福的烦恼”抛回给你。这一节我就从源码和实战两个维度把 Autowired 在千百个 Bean 里锁定“真命天子”的完整过程拆开揉碎。1. 先搞清楚一件事Autowired 背后到底是谁在干活很多人用 Autowired 用了好几年问它内部是怎么工作的只能答出“Spring 自动注入”。但只要往源码里走两步你就会发现这件事远没有想象中那么神秘。1.1 一切的起点是 BeanPostProcessorSpring 容器启动时会创建我们定义的 Bean。Bean 创建完成、属性还没赋值之前会经过一个非常关键的扩展点BeanPostProcessor。你可以把它理解为 Spring 在“Bean 生命周期的流水线”上留出的一排挂钩任何人都可以往挂钩上挂自己的处理逻辑。Autowired 的解析就挂在名为AutowiredAnnotationBeanPostProcessor的处理器上。这个类是 Spring 处理 Autowired、Value、Inject 注解的“总调度室”。具体调用链路大致是AbstractAutowireCapableBeanFactory.createBean创建 Bean 实例populateBean方法给 Bean 填充属性在填充属性前Spring 会遍历所有已注册的BeanPostProcessor找到InstantiationAwareBeanPostProcessor类型的处理器调用它的postProcessProperties方法真正去解析类里的 Autowired 字段和 setter 方法顺着这条线往下翻AutowiredAnnotationBeanPostProcessor里有两个核心方法值得记一下findAutowiringMetadata解析当前 Bean 类里所有带有 Autowired 注解的字段和方法缓存成元数据inject执行真正的注入动作1.2 字段注入和 setter 注入走到的是同一个入口Autowired 可以放在字段上也可以放在 setter 方法上但它们的处理入口是一致的。Spring 会把字段和 setter 方法统一抽象为InjectedElement然后逐个处理。字段注入时inject方法直接对字段进行ReflectionUtils.makeAccessible然后赋值setter 注入时会先拿到方法的参数类型再调用方法完成赋值。不管哪种方式核心动作都一样先解析“要注入什么类型”再找容器里有没有匹配的 Bean最后通过反射塞进去。这也是为什么很多人把 Autowired 字段做成private还能注入成功——Spring 根本没走常规的 Java 访问控制它直接setAccessible(true)了。1.3 关键源码点resolveDependency解析“要注入什么类型”这一步最终会汇聚到DefaultListableBeanFactory.resolveDependency。这是整个 Autowired 匹配机制里最核心的入口。你如果去翻源码会看到这个方法要处理四种情况需要注入的是ObjectProvider、ObjectFactory这类延迟注入类型需要注入的是ListT、MapString, T、T[]这类集合类型标注了Lazy的代理注入常规的单 Bean 匹配其中第 4 种才是我们说的“千百个 Bean 里找真命天子”的主战场后面的匹配优先级全都发生在这一条分支里。2. Spring 的“找 Bean”第一站按类型匹配候选Autowired 的第一个决策依据不是“名字”而是“类型”。这符合 Spring 面向接口编程的核心理念——调用方只声明“我需要什么能力”由容器负责把具备这种能力的实例找出来。2.1 从 resolveDependency 开始的完整链路当你在一个字段上写Autowired private PaymentService paymentService;时Spring 内部大致会经历这么几步读取字段类型PaymentService调用resolveDependency进入doResolveDependency通过findAutowireCandidates从容器里找所有类型匹配的候选 Bean对候选 Bean 做优先级筛选确定唯一实例后通过反射赋值findAutowireCandidates在DefaultListableBeanFactory里逻辑核心是String[] candidateNames BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this, requiredType, true, descriptor.isEager());这一步会把容器里所有类型是PaymentService或子类型的 Bean 的名字全部捞出来。注意这里是“类型匹配”不要求完全相等子类、实现类都算。2.2 数组、集合和 Map 注入的特殊通道doResolveDependency在一开始会先检查字段类型是不是ObjectProvider、List、Map、数组这些特殊形态。如果你写的是Autowired private ListPaymentService paymentServices;Spring 会把容器里所有实现 PaymentService 的 Bean 全部收集成一个 List 注入进来不会走“找唯一匹配”的逻辑所以也不会报错。这在实现策略模式时非常实用。但如果字段是单个PaymentServiceSpring 就必须解决“唯一性”问题。这也是最容易踩坑的地方。2.3 当候选只有一个时注入立刻完成如果findAutowireCandidates最终只找到 1 个匹配的 Bean那么没有任何优先级判断的必要直接返回。这也是绝大多数场景下的情况——项目里没有两个类实现同一个接口Spring 也就不需要纠结。从这一步可以看出Autowired 的设计默认是“单候选直接命中多候选才走裁决逻辑”。这也解释了一个现象很多时候你根本不用关心 Bean 叫什么名字类型对上就行。3. 多个候选 Bean“神仙打架”Primary、Qualifier 与字段名的优先级真相当类型匹配的候选有多个Spring 就开始走一套非常严谨的“择偶标准”了。这套标准在源码里的落点是determineAutowireCandidate方法。3.1 NoUniqueBeanDefinitionException 不是一开始就抛的很多人以为候选多就一定会报NoUniqueBeanDefinitionException其实不是。Spring 会先尝试用各种策略缩小范围只有当所有策略都用完了还是无法确定时才抛出异常。在doResolveDependency里对于多个候选能看到这样一段逻辑if (matchingBeans.size() 1) { // 处理 Primary、Priority、Qualifier 的自动匹配 autowiredBeanName determineAutowireCandidate(candidates, descriptor); if (autowiredBeanName null) { // 按字段名/参数名再匹配一次 if (descriptor.getDependencyName() ! null) { autowiredBeanName BeanFactoryUtils.beanNamesForTypeIncludingAncestors(...); // 精确名字匹配 } } if (autowiredBeanName null) { throw new NoUniqueBeanDefinitionException(...); } }也就是说Spring 至少会尝试三轮筛选注解优先、参数名匹配、报错兜底。3.2 determineAutowireCandidate 的判定顺序看determineAutowireCandidate的源码能非常清晰地看到匹配优先级protected String determineAutowireCandidate(MapString, Object candidates, DependencyDescriptor descriptor) { Class? requiredType descriptor.getDependencyType(); String primaryCandidate determinePrimaryCandidate(candidates, requiredType); if (primaryCandidate ! null) { return primaryCandidate; } String priorityCandidate determineHighestPriorityCandidate(candidates, requiredType); if (priorityCandidate ! null) { return priorityCandidate; } // Fallback: 按字段名匹配 ... }完整顺序如下找标注了Primary的 Bean一票定终身如果没有 Primary找实现了jakarta.annotation.Priority注解的 Bean取优先级值最高的如果还没有尝试用字段名/参数名和 Bean 名字做精确匹配以上都不行才抛出NoUniqueBeanDefinitionException这个顺序非常关键。很多人在两个实现类上同时标了不同的注解却不清楚谁说了算也有人只用了Qualifier期待它能压过Primary结果没生效其实是因为Qualifier是在更外层的地方过滤的。3.3 Qualifier 到底在哪一步起作用Qualifier的匹配其实发生在determineAutowireCandidate之前。descriptor里如果带了Qualifier注解findAutowireCandidates会提前用这个限定名去容器里找对应的 Bean 名字直接缩小候选范围。所以正确理解是Qualifier是在候选池阶段就做了过滤Primary和Priority是在候选池已经确定后做裁决。两者不在同一个阶段自然不存在谁“压过”谁的问题。::: tip 实操建议 如果某个接口的实现类中有且仅有一个是“默认选择”就在这个类上标Primary。其他特殊场景用Qualifier精确指定。这样大部分情况下都不需要改代码。 :::这个匹配优先级我建议你记成一张表匹配策略生效阶段优先级说明Qualifier候选池过滤无先缩小范围按 Bean 名称或自定义限定符Primary候选池裁决第一顺位标注了 Primary 的直接中选Priority候选池裁决第二顺位判断顺序在 Primary 之后Bean 名称匹配候选池裁决第三顺位字段名/参数名与 Bean 名一致异常兜底全部失败后-抛 NoUniqueBeanDefinitionException3.4 实测案例一个 Controller 注入两个相同类型实现我拿一个支付场景举个例子。假如有AlipayServiceImpl和WechatPayServiceImpl都实现了PaymentServicepublic interface PaymentService { void pay(BigDecimal amount); } Service public class AlipayServiceImpl implements PaymentService { public void pay(BigDecimal amount) { System.out.println(支付宝支付 amount); } } Service public class WechatPayServiceImpl implements PaymentService { public void pay(BigDecimal amount) { System.out.println(微信支付 amount); } }此时写Autowired private PaymentService paymentService;启动会直接报错因为两个候选都没有 Primary、没有 Priority字段名paymentService也和任何一个 Bean 名都不一致。但如果你把字段名改成alipayServiceImplAutowired private PaymentService alipayServiceImpl;Spring 会在第三轮匹配中命中alipayServiceImpl启动成功。这就是很多老项目“莫名其妙就能用”的原因——字段名碰巧和某个实现类的 Bean 名一致了。这个行为是隐式的很容易被忽略。我见过不少同事在一个实现类改名后另一处注入突然失效排查半天才发现是原来的匹配路径断了。因此我建议如果必须用字段名去区分至少显式加上Qualifier让意图在代码里可见。4. 构造器注入、字段注入和三级缓存为什么“找真命天子”的顺序这么重要Autowired 的匹配机制本身和 Spring 三级缓存、循环依赖有着很深的关系。很多人单独学过三级缓存却不知道它跟“找 Bean”这个动作是怎么扣在一起的。4.1 不同注入方式的真正区别Autowired 可以放在字段上、setter 方法上、构造器上。这三者在 Bean 创建流程中的执行时机完全不同构造器注入发生在 Bean 实例化阶段也就是createBeanInstance时此时 Bean 对象还没创建出来setter 注入发生在populateBean阶段Bean 对象已经通过无参构造器或默认构造器创建出来了字段注入同样发生在populateBean阶段这个差异直接决定了循环依赖能不能被处理。4.2 三级缓存是怎么为字段注入“兜底”的Spring 解决循环依赖的核心是那三张著名的 Map// 一级缓存完整单例 private final MapString, Object singletonObjects new ConcurrentHashMap(256); // 二级缓存早期单例引用 private final MapString, Object earlySingletonObjects new ConcurrentHashMap(16); // 三级缓存单例工厂 private final MapString, ObjectFactory? singletonFactories new HashMap(16);正常创建 Bean AA 依赖 BB 又依赖 A 时流程是这样的A 开始创建实例化完成对象地址已存在A 还没填充属性Spring 把 A 的ObjectFactory放入三级缓存singletonFactoriesA 填充属性时发现自己需要 B转去创建 BB 实例化完成填充属性时发现自己需要 AB 调用getSingleton(a, false)在一级缓存没找到在二级缓存没找到在三级缓存找到了 A 的ObjectFactory调用这个ObjectFactory.getObject()拿到 A 的早期引用放入二级缓存B 成功注入 AB 完成创建进入一级缓存Spring 回过头继续为 A 填充其他属性填充完成后把 A 从二级缓存搬到一级缓存注意第 6 步getObject()返回的早期引用还只是一个“半成品”——A 的其他属性可能还没填充完。但是对于 B 来说它已经能拿到 A 的引用了这就够了。4.3 构造器循环依赖为什么救不了构造器注入的场景就麻烦了。A 的构造器需要 BB 的构造器需要 A。两个 Bean 在createBeanInstance阶段就互相卡住了A 还没有实例化完成三级缓存里根本没有 A 的ObjectFactoryB 也同理两个对象都拿不到对方的引用死锁就产生了。Spring 官方的解决方案也很直接对于构造器循环依赖直接用BeanCurrentlyInCreationException拒绝提示你把构造器注入改为 setter/字段注入或者用Lazy打破延迟。从 Autowired 的角度理解就是字段注入时Spring 能在 Bean“还没完全整备好”的时候先把引用暴露出去所以有机会兜住循环依赖构造器注入时连“暴露”的时机都没有。4.4 三级缓存和 AOP 的隐藏联动很多人还忽略了一点三级缓存里存的是ObjectFactory不是实例。之所以设计成工厂而不是直接放实例是为了给 AOP 提前代理留出口子。当一个 Bean 需要 AOP 代理时getEarlyBeanReference会通过SmartInstantiationAwareBeanPostProcessor的getEarlyBeanReference方法提前生成代理对象。如果三级缓存里直接放原始实例代理就没机会介入了。这也是“找真命天子”里最容易被忽视的一环Spring 在循环依赖场景下提前暴露的 Bean可能是代理对象也可能是原始对象全看这个 Bean 是否需要 AOP 增强。排查注入结果异常时如果发现类型对不上先怀疑这里。5. 手写一个微型 Autowired把匹配过程完整走一遍光看源码容易飘我建议你自己动手写一个简化版的“注解注入器”把类型匹配、Primary、字段名匹配、异常兜底这几个环节都实现一遍。这样你对 Spring 的匹配优先级就会有肌肉记忆。5.1 简化版本的注入器应该包含什么我们要模拟的目标是给定一个目标类型和一堆注册的 Bean按照规则返回唯一实例。算法跟 Spring 的determineAutowireCandidate保持一致。需要的组件一个简单的 Bean 注册表MapString, Object一个类型过滤器按 Class 的可赋值性找出候选一个MyPrimary注解模拟 Primary一个字段名匹配的逻辑一个抛异常的兜底5.2 核心代码实现先定义两个注解Target(ElementType.TYPE) Retention(RetentionPolicy.RUNTIME) public interface MyPrimary { } Target(ElementType.FIELD) Retention(RetentionPolicy.RUNTIME) public interface MyAutowired { }再定义自动注入器public class MyAutowiredProcessor { private final MapString, Object beanMap new HashMap(); public void registerBean(String name, Object bean) { beanMap.put(name, bean); } public void process(Object target) throws IllegalAccessException { Class? clazz target.getClass(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(MyAutowired.class)) { Class? requiredType field.getType(); Object value resolveDependency(requiredType, field.getName()); field.setAccessible(true); field.set(target, value); } } } private Object resolveDependency(Class? requiredType, String fieldName) { // 1. 按类型找候选 MapString, Object candidates new HashMap(); for (Map.EntryString, Object entry : beanMap.entrySet()) { if (requiredType.isAssignableFrom(entry.getValue().getClass())) { candidates.put(entry.getKey(), entry.getValue()); } } if (candidates.isEmpty()) { throw new RuntimeException(No qualifying bean of type requiredType.getName()); } if (candidates.size() 1) { return candidates.values().iterator().next(); } // 2. 优先 MyPrimary for (Map.EntryString, Object entry : candidates.entrySet()) { if (entry.getValue().getClass().isAnnotationPresent(MyPrimary.class)) { return entry.getValue(); } } // 3. 按字段名匹配 Bean 名 if (candidates.containsKey(fieldName)) { return candidates.get(fieldName); } // 4. 兜底抛异常 throw new RuntimeException( expected single matching bean but found candidates.size() : String.join(,, candidates.keySet())); } }这个过程把 Spring 的匹配核心压缩到了 30 行左右。你可以自己跑一下注册两个同类型 Bean分别测试多个候选但字段名匹配、加了 MyPrimary、都不匹配时抛异常。跑通之后你对 Spring 的 “三步裁决法”就有很直观的感受了。5.3 跑通后的常见误区写这个小工具时会踩到一个 Java 基础坑getDeclaredFields()只能拿到当前类声明的字段拿不到父类的字段。Spring 在处理 Autowired 时会递归遍历整个类层级这也是为什么你在父类里写 Autowired子类也能被注入。另一个坑是isAssignableFrom的方向requiredType.isAssignableFrom(bean.getClass())这表示requiredType是bean.getClass()的父类型或相同类型。方向写反了就是在判断bean.getClass()是不是requiredType的父类型结果会完全反过来而且编译期不会报错只能在运行时发现匹配不到。这个细节在写任何类型匹配逻辑时都要留神。6. 真遇到“注入错/注入不了”时我的排查流程Autowired 的报错常见的有那么几类。每一类背后对应的原因不同排查思路也不同。我按实际踩坑频率整理一下。6.1 从报错信息快速定位问题类别第一类是启动时直接抛异常最常见的是No qualifying bean of type xxx available如果后面跟着expected single matching bean but found 2说明是多个候选但没有裁决依据按第 3 节的优先级规则补 Primary 或 Qualifier 即可。如果只有No qualifying bean of type xxx available没有 “found 2”那说明容器里根本没有这个类型的 Bean。常见原因有类没加Service、Component、Repository等注解类加了注解但所在包没有被扫描到Spring Boot 启动类不在合适的位置默认扫描范围覆盖不到引入了依赖但依赖里的 Bean 没有通过EnableAutoConfiguration或auto-configuration生效排查时先看ApplicationContext启动日志里有没有“扫描到了 xxx”的痕迹再用actuator看一眼实际注册的 Bean。第二类是注入成功但是 NPE这种更隐蔽。说明 Bean 本身存在但另一个 Bean 是在PostConstruct或构造器里使用了它而那个 Bean 还没有被注入完成。这类问题通常需要用ObjectProvider延迟获取或者调整初始化顺序。6.2 利用 actuator 的 beans 端点查看实例项目里如果引入了spring-boot-starter-actuator直接访问GET /actuator/beans响应里会列出所有 Bean 的名字、类型、依赖关系和作用域。这是排查 Autowired 问题最直接的入口。比如你想看PaymentService到底注册了几个实例直接在返回结果里搜索paymentService能立刻看到 bean 名是alipayServiceImpl还是wechatPayServiceImpl还能看到它们是单例还是原型。如果没有引入 actuator也可以用 IDEA 的 Spring 插件。在Spring工具窗口里展开Beans输入类型名直接搜索能直观看到同一个接口的所有实现类。这个方式对本地排查更快不用改任何代码。6.3 测试类里扫描不到 Autowired 的常见原因测试场景里的 Autowired 失效是另一个高频问题尤其是在 Maven 项目里。常见错误写法是把测试类和主类放在不同根包下src/main/java/com/example/demo/DemoApplication.java src/test/java/com/example/test/DemoApplicationTests.java当SpringBootTest在被执行时默认会找和测试类同包或父包下的SpringBootConfiguration。如果测试类的包名是com.example.test它去com.example.test下找配置类找不到就会直接报错Autowired 自然也就注入不了。解决办法是用SpringBootTest(classes DemoApplication.class)显式指定启动类或者在测试类上加上ContextConfiguration(classes DemoApplication.class)。还有一个隐藏问题测试方法不是Test或/mvn test没有把测试类编入 classpath也会导致扫描不到。::: tip 注意SpringBootTest默认是不回滚事务的如果测试里因为注入了 Mapper 而修改了数据库记得加Transactional否则数据会真实落库。 :::6.4 Resource 和 Autowired 的差异到底该用哪个说到排查绕不开另一个高频争执Resource 和 Autowired 选哪个。一句话总结差别对比项AutowiredResource来源Spring 提供JSR-250 标准匹配顺序先按类型找候选再按名称/注解裁决先按名称找找不到再按类型是否支持 Primary支持不支持是否支持 Qualifier支持支持但语义略有不同包名org.springframework.beans.factory.annotationjavax.annotation / jakarta.annotation如果你的项目是纯粹的 Spring/Spring Boot用 Autowired 完全没问题。如果你有多个同类型 Bean 且希望“按名字注入优先”用 Resource 更符合直觉因为它默认先按变量名找。但 Resource 不支持 Primary这意味着它的裁决逻辑和 Autowired 完全不同混用时要特别当心。我个人在工程上的习惯是接口有且仅有一个实现时用 Autowired靠类型语义清晰地表达“我需要这个能力”接口有多个实现时在字段上同时使用 Autowired Qualifier 显式写死目标 Bean 名或者用一个集中配置类返回Bean并给不同方法起不同名字用方法名表达意图。前者适合快速开发后者适合多人维护的中大型项目。还有一种更“现代”的做法是构造器注入 Qualifier因为构造器注入能让依赖关系在编译期就暴露出来配合 final 字段做不可变设计代码可测性也更好。在 Spring 官方文档里构造器注入也是被推荐的方式。最后说一个我自己的感受Autowired 的“魔法感”其实源于 Spring 把一类很复杂的决策逻辑封装成了“类型优先 多级裁决 异常兜底”的固定流程。一旦你把这几层优先级吃透了再看到那些 “expected single matching bean but found N” 的报错时你看到的就不是异常而是 Spring 在向你汇报候选名单。这时候你只需要告诉它到底选谁。
返回列表