同一个接口出现两个实现以后

订单应用按渠道选择处理策略。Channel 有 Web、Store 两个实现,消费方声明一个 Channel 构造参数。只有 Web 时依赖可以确定;同时注册两个实现以后,容器需要额外的选择条件。把参数改成 List<Channel>,需求则变成取得所有匹配策略,不再要求从中选一个。

这一差别决定了 @Qualifier、@Primary 和集合注入的作用范围。Qualifier 缩小可用候选集,Primary 帮助单值依赖确定优先对象,集合表达多值需求。三者不能用一张“注解优先级”表解释。官方 Qualifier 文档也允许多个 Bean 具有同一限定值,并一起进入集合。

本文固定 Spring Framework 6.2.11、JDK 21,使用原生 AnnotationConfigApplicationContext。源码对应完整提交 4c134254642d88e058aa004bdaf44168e1be7bb2。完整实验位于仓库 examples/spring-framework-lab/src/main/java/blog/spring/Chapter05.java,包含导入、配置、断言与上下文关闭;正文的对象表用于解释实验,不替代可执行代码。

输入配置 请求形状 预期结果
不注册 Channel 实现 必需的 Channel 缺少候选,消费者创建失败
仅注册 Web 必需的 Channel 注入 Web
注册 Web、Store,无额外条件 必需的 Channel 候选不唯一,消费者创建失败
Web 标记 Primary,另有 Store Channel 与 List<Channel> 单值选择 Web;集合保留两者
注册 Box<String>、Box<Integer> Box<String> 选择字符串工厂方法对应的 Bean

依赖描述、候选过滤与单值选择的分支

注入点保存的不只是一个 Class

构造参数 Box<String> 包含原始类型、类型参数、参数注解和参数名称。Spring 将注入点包装成 DependencyDescriptor,保留这些信息以及是否必需、是否允许 eager 类型检查等属性。只拿 Box.class 查一次容器,无法表达完整的依赖要求。

入口 DefaultListableBeanFactory.resolveDependency() 先识别依赖形状。Optional<T> 使用可缺省解析,ObjectProvider<T> 和 ObjectFactory<T> 生成基于描述符的取得入口;支持延迟解析的注入点还可能返回懒代理。普通依赖随后进入 doResolveDependency()。Provider 在这一步被注入,并不代表它包裹的目标已经创建。依赖解析实现

doResolveDependency() 也不总是扫描所有 Bean。它先尝试描述符已有的快捷结果、@Value 提供的值,以及依赖名称或 qualifier 建议名称命中的快捷路径。6.2.11 的名称快捷路径仍检查类型匹配、候选资格、fallback 标志、primary 冲突与自引用。参数碰巧与某个 Bean 同名,不会跳过这些条件。

因此,“先按类型,再按名称”适合作为语义模型,却不是每次执行的完整方法轨迹。断点追踪时若没有进入候选全量扫描,应先检查前面的快捷分支。参数名称参与选择还要求编译产物保留名称;依赖参数名完成业务装配会使重命名产生语义影响,显式的语义限定更容易审阅。

过滤候选以后才知道有几个可用对象

普通候选搜索由 findAutowireCandidates() 取得本容器及祖先容器中类型相符的名称,排除通常不应参加当前注入的自引用,再逐个调用候选解析器。注解配置上下文提供的解析器支持泛型与 qualifier;直接新建裸 DefaultListableBeanFactory 时,不能假定这些注解能力已经全部安装。

GenericTypeAwareAutowireCandidateResolver 根据 ResolvableType 比较依赖和目标元数据。目标信息可能来自 BeanDefinition 的 target type、工厂方法返回类型或 Bean 类型。@Bean Box<String> textBox() 与 @Bean Box<Integer> integerBox() 的返回签名给出了区分依据,所以 Box<String> 不必再靠名字挑选。

这不要求 Java 在任意对象上恢复被擦除的所有类型实参。若注册来源只暴露 raw Box 或过宽的 Object 返回类型,预测所能使用的信息就减少。实现还存在对无法解析泛型的 fallback 匹配,不能把“泛型参与筛选”扩大成“任何注册形式下泛型都严格排除全部 raw 候选”。保持工厂方法签名准确,是让解析发生在元数据阶段的直接办法。泛型解析器

Qualifier 解析建立在已有候选资格检查之上。@Qualifier("store") 可以与限定元数据匹配,也可以在规定条件下与 Bean 名或别名匹配。名称相同但类型不兼容的对象仍不可注入。若两个类型兼容的 Bean 都具有 offline 限定,@Qualifier("offline") Channel 仍可能歧义;@Qualifier("offline") List<Channel> 则可以表达取得两者。限定解析器

注册元数据还应与实际对象分开核对。例如,工厂方法只声明 Box,即使方法体最终返回保存字符串的实现,也不能假定未创建对象时能预测 Box<String>。反过来,上游测试 genericMatchingWithBeanNameDifferentiation() 注册两个未完整区分泛型的 NumberStore,按带泛型类型查询名称时没有得到对应结果,构造参数却可以通过各自的名字完成匹配。查询 API、依赖描述与 fallback 条件不同,结果就不一定相同。

故障定位可以保存一张注入点记录:消费方名称、字段或参数签名、限定注解、查询到的名称、每个候选的注册来源。若把泛型参数删掉后查询成功,问题可能是暴露类型信息不足;若去掉 qualifier 后成功,则应核对限定值和元注解;若单值失败而同类型集合成功,则首先检查基数与优先规则。每次只改一个条件,才能把成功关联到具体分支。这些是诊断改动,不应全部留成生产配置。

模式提炼:先约束集合,再选择结果

令 C 表示类型与限定均相容的候选集。单值请求执行 select(C),多值请求执行 collect(C)。过滤条件回答“哪些对象有资格”,选择规则回答“合格对象中返回谁”;在策略注册、服务路由与插件发现中,这两个问题也应分别建模。

误把 Primary 当成全局过滤条件,会把集合应有的实现删掉;误把 Qualifier 当成唯一键,又会漏掉同限定的多候选冲突。调试时先列出 C,再检查消费方要求的结果数量,往往比继续添加注解更快。

零、一、多候选分别在哪里结束

集合、数组、流和常见 Map 的多值解析在普通单值决策前尝试完成。集合元素仍接受类型与限定过滤,但不会因为某个元素是 Primary 就排除其余匹配元素。@Order 影响适用的多值结果顺序,也不能直接当成单值依赖的 Primary 替代品。

对于普通单值请求,零候选且必需时抛出缺少 Bean 的异常;一个候选时直接使用该候选;多个候选时进入 determineAutowireCandidate()。外部看到的异常可能是消费者创建过程包装后的 UnsatisfiedDependencyException,其根因才是 NoSuchBeanDefinitionException 或 NoUniqueBeanDefinitionException。定位时应同时记录消费方、注入参数和根因,不只搜索最外层异常名。

6.2.11 的单值选择先检查 primary 候选。该步骤还包含唯一非 fallback 候选的分支。若仍未决定,再考虑依赖名称、qualifier 建议名称、最高 priority、唯一 default candidate 和直接登记的可解析依赖。多个本地 primary 本身就会造成冲突,不能用“任意一个 Primary 优先”解释。

候选表里的值也未必都是已创建的对象。addCandidateEntry() 在部分单值路径先存类型,选定后才调用 resolveCandidate() 取得对象,避免为了挑选而先创建所有普通候选。多值收集则需要取得各个实际元素。类型检查还可能触发 FactoryBean 的工厂实例化,因此这项优化不能推广成“候选筛选绝无实例化副作用”;第 06 篇单独区分工厂与产品。

最终结果还要检查空产品与类型适配。过滤通过只说明元数据满足条件,目标创建失败、工厂返回空值或实际结果类型不符,仍可能让注入失败。异常阶段从“没有可用定义”转为“定义对应的对象无法取得”,排查方向随之改变。

Provider 改变取得时点,也保留歧义规则

ObjectProvider<T> 适合把目标取得推迟到显式方法调用,但其 API 对失败有不同契约。getObject() 要求能得到对象;getIfAvailable() 允许缺失,不会自动容忍无法解决的多候选;getIfUnique() 可以在没有唯一可选结果时返回空。这里的“唯一”包含 Primary 等选择规则确定的唯一结果,不等同于原始候选数只能为一。

上游 BeanFactoryGenericsTests.genericMatchingWithFullTypeDifferentiation() 对三种方法分别断言。未区分泛型时,provider 调用前两种方法会因歧义失败,调用 getIfUnique() 返回空。携带完整泛型时,provider 得到对应实例,流式遍历则取得所有适用对象。这是已阅读的上游回归测试,本地实验没有运行整个 Spring 上游测试套件。上游泛型测试

Provider 也没有自行定义新 scope。对 singleton 重复取得通常得到同一容器对象,对 prototype 重复取得可以触发新建。若目标本身是普通 eager singleton,容器预实例化阶段仍会创建它;仅把某一个消费者改成 provider,不会取消目标在其他路径上的创建需求。

模式提炼:把可选性与延迟性分开

取得时点 × 缺失策略 × 唯一性策略 × scope 是四个独立维度。需要延迟取得,不等于允许依赖缺失;允许缺失,也不等于容忍配置歧义。插件可选安装、按请求取得对象与按渠道选择实现,都可以用这组维度检查 API 是否准确表达需求。

运行四组隔离场景

实验工程下载和 JDK 21 环境准备见第 00 篇。执行前通过 java -version 确认当前终端使用 JDK 21;Maven 与应用运行环境应保持一致。

从仓库根目录执行:

1
2
cd examples/spring-framework-lab
./mvnw -q compile exec:java -Dexec.mainClass=blog.spring.Chapter05

实验先后创建零候选、单候选、多候选三个上下文,再运行含 Primary、Qualifier、集合、泛型与 lazy provider 的配置。失败上下文使用断言确认异常,随后关闭;异常日志是预期反例,不表示整个实验失败。

本地运行使用 Spring 6.2.11、Java 21.0.11,保存于 examples/spring-framework-lab/evidence/05/local-20261002/run.txt。输出包含:

1
2
3
4
5
6
7
8
9
10
CHECK PASS 05 zero failure root is missing candidate
CHECK PASS 05 one candidate injected
CHECK PASS 05 multiple failure root is ambiguity
CHECK PASS 05 Primary resolves single injection
CHECK PASS 05 Qualifier narrows candidate
CHECK PASS 05 collection contains both even with Primary
CHECK PASS 05 generic String candidate selected
CHECK PASS 05 ObjectProvider injection leaves lazy target uncreated
CHECK PASS 05 provider resolves one lazy singleton on demand
CHAPTER 05 PASS

这些断言检查对象身份、集合内容和构造计数。Expensive.created 在 provider 注入后仍为零;两次 getObject() 后为一,并且返回同一个对象。目标同时声明 lazy,所以计数能够区分取得 provider 与取得目标。它没有测量容器启动性能,也没有证明 provider 可以解决任意循环依赖。

练习与答案

反例题:把两个 Channel 都标记为同一个 @Qualifier("offline"),消费者声明该 qualifier 的单值参数,且没有其他选择规则,能否保证成功?答案是不能。限定只将 C 缩小到两个对象,单值选择仍可能歧义。若消费者改为同限定集合,两个对象都可成为元素。

改动题:在 Chapter05.Config.web() 上移除 @Primary,保留参数名 channel,且 Bean 仍名为 web、store,重新执行原命令。答案是单值 Channel 不能通过名称确定候选,配置创建会失败;集合本身有两个合法元素,但包含该单值参数的 selection 工厂方法无法完成。修复应由业务意图决定:需要固定渠道就添加明确 qualifier,需要全部渠道就改变消费方接口,不能随意标一个 Primary 只为消除报错。

模式速查表

继续阅读06:构造器、工厂与延迟创建。

现象 对应模式 优先核对
注册了 Bean 仍找不到依赖 先约束集合,再选择结果 类型、泛型、限定和候选资格
多个实现导致启动失败 先约束集合,再选择结果 结果基数、Primary 与名称是否能唯一确定
集合出现多个实现 先约束集合,再选择结果 是否本来就声明了多值需求
可选取得仍报歧义 把可选性与延迟性分开 getIfAvailable 与 getIfUnique 的不同
provider 注入后目标已创建 把可选性与延迟性分开 eager 预实例化和其他直接依赖