系列第三篇,继 [IoC 设计哲学:容器、BeanDefinition 与配置元信息] 与 [Bean 生命周期全景:从扫描到销毁的完整轨迹] 之后,我们进入生命周期的核心阶段——属性填充(
populateBean),集中讨论依赖是如何被注入到 Bean 中,以及不同注入策略带来的深远影响。
概述
衔接前文
在前两篇文章中,我们构建了 IoC 容器的完整设计蓝图:容器底层如何承载 BeanDefinition,配置元信息如何演化成 Bean 的骨架;接着我们遍历了 Bean 从扫描、实例化、初始化到销毁的完整生命周期轨迹。在那条生命线中,本文聚焦的是 属性填充(populateBean)及其前置阶段,它正是依赖注入发生的物理位置,也是控制反转最直接、最频繁的体现形式。
总结性引言
依赖注入是控制反转最直接的体现形式,但并非所有注入方式生而平等。Spring 提供了字段注入、设值注入、构造器注入等多种选择,它们看似都能完成任务,但背后的实现机制、对架构的影响却有着天壤之别。本文将以手术刀式精度逐层剖开三种注入方式的底层源码,展示字段注入的隐蔽性陷阱、设值注入的可选性设计以及构造器注入的不可变哲学。通过辨析 AutowiredAnnotationBeanPostProcessor 与 ConstructorResolver 两条截然不同的代码路径,你将建立起从元数据收集、依赖解析到反射调用的完整知识体系,并掌握选择决策背后的架构权衡。
核心要点
- 注入三大方式与生命周期:字段注入在
populateBean阶段完成,简洁但强耦合;设值注入同样位于populateBean,允许多参数并可复用,但可能被覆盖;构造器注入提前到createBeanInstance阶段,强制声明依赖且保证不可变。 - @Autowired 通用处理逻辑:通过
AutowiredAnnotationBeanPostProcessor统一收集@Autowired、@Value、@Inject注解元信息,构建InjectionMetadata并缓存;字段和 setter/方法注入共享resolveDependency核心链路,@Value则在解析前额外经StringValueResolver处理占位符或 SpEL。 - 构造器注入的独有流程:
ConstructorResolver.autowireConstructor采用贪心算法选择构造器,参数逐一解析,支持@Autowired显式标记和 Spring 4.3+ 单构造器自动识别两种路径;因在实例化时即固定所有依赖,无法利用三级缓存解决循环依赖。 - 设计权衡全景:构造器注入天然支持
final、可测试性极佳且不隐藏依赖;字段注入易引发框架耦合、测试困难和 NPE;设值注入为可选依赖和重配置提供灵活度,但有状态不稳定的风险。全局决策树将在文中给出。
文章组织架构图
graph TB
subgraph "DI概述"
A["依赖注入动机与定位"]
B["三种注入方式对比"]
end
subgraph "字段与设值注入原理"
C["AAP扫描与元数据收集"]
D["InjectionMetadata执行"]
E["字段注入与设值注入反射调用"]
F["Value分支处理"]
end
subgraph "构造器注入原理"
G["构造器选择策略两条路径"]
H["参数解析与贪心算法"]
I["反射调用Constructor"]
end
subgraph "统一依赖解析"
J["resolveDependency核心分发"]
K["集合/Map/泛型/延迟注入"]
end
subgraph "对比与权衡"
L["Resource vs Inject"]
M["设计权衡六维度比较"]
N["决策树"]
end
subgraph "实战与巩固"
O["生产事故排查"]
P["面试高频题"]
end
A --> B
B --> C
B --> G
C --> D --> E --> F
G --> H --> I
E --> J
I --> J
J --> K
K --> L --> M --> N
N --> O
M --> O
O --> P
主旨概括:该图展示了全文由概述切入,将字段/设值注入与构造器注入作为两条独立主线并行展开,最终汇入统一的依赖解析核心,再进入注解对比、设计权衡、事故剖析和面试巩固,呈现“先分后合”的递进逻辑。
逐层分解:顶层从 DI 动机开始,中部分为两大技术路径和统一解析,底层则是对比、权衡与实践。箭头方向体现了从原理到决策,再到实战的知识转化顺序。
设计原理:分离两条注入链路是为了强调它们在生命周期中的不同位置(populateBean vs createBeanInstance),而 resolveDependency 的归并则说明所有注入最终都依赖同一个核心方法,保证了分析的统一性与对比的精确性。
工程联系与结论:任何注入问题都能在图中定位到具体阶段(例如循环依赖对应构造器注入 → 贪心算法 → 解析失败),帮助读者形成“问题-模块-源码”的快速检索能力。
1. 依赖注入概述与三种方式概览
1.1 依赖注入的设计动机与定位
在传统 Java 开发中,对象通过 new 关键字直接创建其依赖,导致类与具体实现强耦合。IoC 容器接管了对象的创建与装配,而依赖注入(DI)正是 IoC 赋予容器的核心能力:容器负责将组件所需的依赖“推”给它,而不是组件主动去“拉”取。根据注入的入口不同,Spring 提供了字段注入、设值注入(setter/任意方法)和构造器注入三种方式。它们共同的目标是将配置元信息转化为对象间的关联,但在 Bean 生命周期中触发的时机截然不同,这一差异将深刻影响后续的架构特征。
1.2 三种注入方式的定义与时机差异
- 构造器注入:依赖通过构造器参数传递,发生在
createBeanInstance阶段,此时 Bean 尚未完全实例化,BeanPostProcessor前置处理、Aware 回调均未执行。 - 字段注入:依赖直接赋值给字段,发生在
populateBean阶段,即 Bean 实例化之后、初始化(init-method、InitializingBean)之前。 - 设值注入:通过 setter 或任意
@Autowired标注的方法接收依赖,同样发生在populateBean阶段,在字段注入之后执行(同一个AutowiredAnnotationBeanPostProcessor内部先字段后方法)。
1.3 三种注入方式对比示意图
flowchart LR
subgraph Bean生命周期[Bean生命周期阶段]
direction TB
L1[createBeanInstance<br/>实例化]
L2[populateBean<br/>属性填充]
L3[initializeBean<br/>初始化]
end
CI[构造器注入<br/>依赖在L1阶段注入]
FI[字段注入<br/>在L2阶段反射赋值]
SI[设值注入<br/>在L2阶段反射调用方法]
L1 --> CI
L2 --> FI
L2 --> SI
CI -..->|依赖已确定| L2
FI -..->|可能被后置处理修改| L3
主旨概括:此图将三种注入方式精确锚定在 Bean 生命周期的 L1 和 L2 两个不同阶段,突出构造器注入的时间优势(更早确定依赖),以及字段/设值注入所处的属性填充阶段。
逐层分解:左侧为生命周期分段,中间为注入方式,箭头指明触发时刻;虚线表示注入结果如何向下游传递。
设计原理:实例化与属性填充分离是 Spring 实现灵活扩展(如代理替换)的基础,构造器注入因为在实例化时即固定对象骨架,天然与 Immutable 设计契合;而字段/设值注入则允许后期动态调整,但牺牲了确定性。
工程联系与结论:若你的 Bean 需要在构造器中立即使用依赖(如校验),务必选择构造器注入;反之,若依赖由外部配置动态决定,设值注入提供更多弹性。理解这一阶段的先后,是排查“注入后却为 null”问题的关键。
2. 字段注入与设值注入的原理与源码分析
2.1 总体处理者:AutowiredAnnotationBeanPostProcessor
AutowiredAnnotationBeanPostProcessor 同时实现了 InstantiationAwareBeanPostProcessor 和 MergedBeanDefinitionPostProcessor。它在 postProcessProperties 阶段介入,处理 @Autowired、@Value、@Inject 注解。该处理器在容器启动时被注册,默认能处理所有 Bean。
// AutowiredAnnotationBeanPostProcessor 部分核心结构
public class AutowiredAnnotationBeanPostProcessor
extends InstantiationAwareBeanPostProcessorAdapter
implements MergedBeanDefinitionPostProcessor, PriorityOrdered {
// 存储每个bean的注入元数据
private final Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<>(256);
// 注入注解类型集合,默认包含@Autowired, @Value, @Inject
private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>();
// ...
}
解读:injectionMetadataCache 缓存每 Bean 的注入点,确保反射解析只执行一次;autowiredAnnotationTypes 默认包含 @Autowired、@Value 和 @Inject,意味着这三个注解共用同一套处理流程。
2.2 注入元数据收集:从注解扫描到 InjectionMetadata
findAutowiringMetadata 方法是构建 InjectionMetadata 的核心,它遍历目标 Class 及其所有父类,逐字段、逐方法查找被目标注解标记的元素。
// AutowiredAnnotationBeanPostProcessor.findAutowiringMetadata()
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
// 从缓存获取
InjectionMetadata metadata = this.injectionMetadataCache.get(beanName);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
metadata = this.injectionMetadataCache.get(beanName);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
metadata = buildAutowiringMetadata(clazz);
this.injectionMetadataCache.put(beanName, metadata);
}
}
}
return metadata;
}
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;
do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
// 处理字段
ReflectionUtils.doWithLocalFields(targetClass, field -> {
AnnotationAttributes ann = findAutowiredAnnotation(field);
if (ann != null) {
if (Modifier.isStatic(field.getModifiers())) {
return; // 跳过静态字段
}
boolean required = determineRequiredStatus(ann);
currElements.add(new AutowiredFieldElement(field, required));
}
});
// 处理方法
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
AnnotationAttributes ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
return;
}
boolean required = determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new AutowiredMethodElement(method, required, pd));
}
});
elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
} while (targetClass != null && targetClass != Object.class);
return new InjectionMetadata(clazz, elements);
}
解读:
- 逆向遍历类层次结构(子类先加入),保证子类注入点优先执行。
- 字段跳过
static,方法跳过static及桥接方法。 - 每个注入点被封装成
AutowiredFieldElement或AutowiredMethodElement,存入InjectionMetadata的injectedElements列表。 @Autowired(required)的语义在此确定,传递给InjectedElement,后续执行时若required=true且未找到依赖则直接抛异常。- 整个
InjectionMetadata按beanName缓存,后续相同 Bean 直接从缓存读取,大幅减少反射开销。
2.3 字段注入的执行:穿透 private 的直接赋值
当 postProcessProperties 被调用时,对于每个 Bean,获取其 InjectionMetadata 并调用 inject,最终每个 InjectedElement 的方法被触发。字段注入对应 AutowiredFieldElement.inject()。
// AutowiredFieldElement.inject()
@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Field field = (Field) this.member;
// 核心:通过beanFactory解析依赖
Object value;
if (this.cached) {
value = resolvedCachedArgument(beanName, this.cachedFieldValue);
} else {
DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
desc.setContainingClass(bean.getClass());
Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
TypeConverter typeConverter = beanFactory.getTypeConverter();
// ...获取类型转换器
value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
synchronized (this) {
if (!this.cached) {
if (value != null || this.required) {
this.cachedFieldValue = desc; // 缓存依赖描述符
registerDependentBeans(beanName, autowiredBeanNames);
if (autowiredBeanNames.size() == 1) {
String autowiredBeanName = autowiredBeanNames.iterator().next();
if (beanFactory.containsBean(autowiredBeanName) &&
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
this.cachedFieldValue = new ShortcutDependencyDescriptor(
desc, autowiredBeanName, field.getType());
}
}
} else {
this.cachedFieldValue = null;
}
this.cached = true;
}
}
}
if (value != null) {
ReflectionUtils.makeAccessible(field); // 突破private
field.set(bean, value);
}
}
解读:
- 先检查缓存:若已解析成功,直接从
cachedFieldValue快速获取实例;若解析失败且required=false则跳过。 - 首次调用进入
beanFactory.resolveDependency,获取匹配 Bean。 DependencyDescriptor封装字段类型、required、所在类等信息。- 解析结果被缓存在
cachedFieldValue中,后续同一个 Bean 的注入点无需重复解析,提高性能。 - 使用
ReflectionUtils.makeAccessible(field)打破封装,即使private字段也能注入。
2.4 设值注入的执行:方法反射调用支持多参数
AutowiredMethodElement.inject() 处理 @Autowired 标注的方法(setter 或普通方法)。它可以同时注入多个依赖,因为一个方法可有多个参数。
// AutowiredMethodElement.inject() 关键逻辑
@Override
protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Method method = (Method) this.member;
Object[] arguments;
if (this.cached) {
arguments = resolveCachedArguments(beanName);
} else {
Class<?>[] paramTypes = method.getParameterTypes();
arguments = new Object[paramTypes.length];
DependencyDescriptor[] descriptors = new DependencyDescriptor[paramTypes.length];
Set<String> autowiredBeans = new LinkedHashSet<>(paramTypes.length);
TypeConverter typeConverter = beanFactory.getTypeConverter();
for (int i = 0; i < arguments.length; i++) {
MethodParameter methodParam = new MethodParameter(method, i);
DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
currDesc.setContainingClass(bean.getClass());
descriptors[i] = currDesc;
// 每个参数都通过 resolveDependency 解析
Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
if (arg == null && !this.required) {
arguments = null;
break;
}
arguments[i] = arg;
}
// 缓存解析结果...
}
if (arguments != null) {
ReflectionUtils.makeAccessible(method);
method.invoke(bean, arguments); // 反射调用方法
}
}
解读:
- 方法注入可接收多个参数,循环对每个参数构建
DependencyDescriptor并依次解析。 - 如果任何
required=true的参数未找到依赖,整个过程中断并抛异常;若required=false则全有或全无(参数列表全部解析成功才调用,否则跳过)。 - 方法调用不仅是 setter,也可以注入配置值并执行初始化逻辑,提供了极大灵活性。
2.5 @Value 的处理:同路不同源
@Value 同样由 AutowiredAnnotationBeanPostProcessor 处理,生成 AutowiredFieldElement 或 AutowiredMethodElement。区别在于 resolveDependency 内部会检测 DependencyDescriptor 是否带有 @Value 注解,若是,则不会去 Bean 容器中寻找 Bean,而是委托给 StringValueResolver 链解析表达式(占位符 ${...} 或 SpEL #\{...})。
这意味着:
- 若环境中没有注册
PropertySourcesPlaceholderConfigurer,占位符解析失败,注入值将保持原始${}字符串,或直接报错。 @Value的注入同样享受InjectionMetadata缓存机制,但值解析器要求必须就绪,这是第一篇中提到的常见“踩坑”点。
2.6 字段注入与设值注入序列图
sequenceDiagram
participant BPP as AutowiredAnnotationBeanPostProcessor
participant IM as InjectionMetadata
participant FieldElem as AutowiredFieldElement
participant MethodElem as AutowiredMethodElement
participant BF as DefaultListableBeanFactory
BPP->>BPP: postProcessProperties()
BPP->>IM: 获取注入元数据(缓存/构建)
loop 每个注入点
alt 字段注入
FieldElem->>BF: resolveDependency(desc)
BF-->>FieldElem: 依赖实例
FieldElem->>FieldElem: field.set(bean, value)
else 方法注入
MethodElem->>BF: resolveDependency(每个参数)
BF-->>MethodElem: 参数数组
MethodElem->>MethodElem: method.invoke(bean, args)
end
end
Note over BPP,BF: @Value 分支则在 resolveDependency 内部<br/>走到 StringValueResolver 解析,不依赖容器 Bean
主旨概括:序列图清晰展示了 AutowiredAnnotationBeanPostProcessor 如何分两步走(元数据收集→逐一执行),以及字段与设值注入在最终注入动作上的差异。
逐层分解:获取元数据后,两者皆借助 resolveDependency 获得真实对象,但字段使用 Field.set 一次性赋值,方法则需构造参数数组并 invoke。
设计原理:将解析与注入解耦,InjectionMetadata 专门记录“何处注入”,DependencyDescriptor 记录“算什么”,resolveDependency 负责“从哪取”,实现了单一职责。
工程联系与结论:字段注入虽然简单,但无法在 setter 中加入额外逻辑;方法注入可在一个方法中注入多个依赖并执行状态验证,更符合面向对象封装思想。缓存机制使得首次解析开销较大,但后续极快。
3. 构造器注入的原理与源码分析
3.1 触发时机与总体流程
构造器注入不走 populateBean,而是在 AbstractAutowireCapableBeanFactory.createBeanInstance 中通过 autowireConstructor 完成。这导致两个关键结果:
- 构造器依赖在 Bean 实例化时就已经确定,后续的
BeanPostProcessor虽可改变 Bean 的代理行为,但无法替换依赖引用。 - 实例化时如果循环依赖,由于对象尚未创建出来放入三级缓存,无法提前暴露引用,必然抛出异常。
3.2 两条处理路径:显式 @Autowired 与隐式单构造器
determineCandidateConstructors 方法负责找出所有候选构造器,它区分两条路径:
// AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors()
@Override
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName) {
// ...
// 1. 查找所有带@Autowired注解的构造器
Constructor<?>[] rawCandidates = beanClass.getDeclaredConstructors();
List<Constructor<?>> candidates = new ArrayList<>();
Constructor<?> requiredConstructor = null;
Constructor<?> defaultConstructor = null;
for (Constructor<?> candidate : rawCandidates) {
AnnotationAttributes ann = findAutowiredAnnotation(candidate);
if (ann != null) {
if (requiredConstructor != null) {
throw new BeanCreationException("多个@Autowired(required=true)...");
}
boolean required = determineRequiredStatus(ann);
if (required) {
requiredConstructor = candidate;
}
candidates.add(candidate);
} else if (candidate.getParameterCount() == 0) {
defaultConstructor = candidate; // 记录默认无参构造器
}
}
if (!candidates.isEmpty()) {
// 路径一:有显式@Autowired
return candidates.toArray(new Constructor<?>[0]);
} else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
// 路径二:仅有一个有参构造器(Spring 4.3+)
return new Constructor<?>[] { rawCandidates[0] };
} else {
// 其他情况:无候选,后续使用无参构造器
return null;
}
}
解读:
- 若有多个带
@Autowired(required=true)的构造器,直接抛异常(因为无法决定)。 - 如果至少有一个
@Autowired构造器,将它们全部作为候选,后续由autowireConstructor按贪心算法挑选。 - 若没有标注
@Autowired,但只有一个有参构造器(且参数个数>0),Spring 4.3 起自动将其视为唯一候选,无需显式注解。 - 其他情况(多个无注解构造器、无参构造器等)则不会进入构造器注入流程,最终使用默认无参构造器。
3.3 构造器选择算法:贪心匹配与宽松适配
ConstructorResolver.autowireConstructor 是核心,简化逻辑如下:
public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd,
@Nullable Constructor<?>[] chosenCtors, @Nullable Object[] explicitArgs) {
Constructor<?> constructorToUse = null;
// ... 解析候选构造器
// 贪心选择:按参数个数降序排列
Arrays.sort(candidates, (c1, c2) -> Integer.compare(c2.getParameterCount(), c1.getParameterCount()));
int minTypeDiffWeight = Integer.MAX_VALUE;
for (Constructor<?> candidate : candidates) {
Class<?>[] paramTypes = candidate.getParameterTypes();
// 尝试解析当前构造器的参数
Object[] argsToUse = resolveConstructorArguments(beanName, mbd, bw, paramTypes, ...);
if (argsToUse != null) {
int typeDiffWeight = getTypeDifferenceWeight(paramTypes, argsToUse);
if (typeDiffWeight < minTypeDiffWeight) {
constructorToUse = candidate;
minTypeDiffWeight = typeDiffWeight;
if (typeDiffWeight == 0) {
break; // 完美匹配,直接使用
}
}
}
}
if (constructorToUse == null) {
throw new BeanCreationException("无法确定适合的构造器...");
}
// 使用选定构造器实例化
return instantiateBean(beanName, mbd, constructorToUse, argsToUse);
}
解读:
- 候选构造器按参数个数从多到少排序,优先尝试参数最多的构造器(最具体)。
- 每个构造器的参数逐一通过
resolveConstructorArguments解析,内部调用resolveDependency。 - 若所有参数都可解析,计算类型差异权重(完美匹配权重为0,需转型则权重增大);最终选择权重最小的构造器。
- 若没有构造器能够完全解析,抛出
BeanCreationException。 - 对于多个候选构造器都能匹配的情况,贪心算法加权重保证 Spring 始终选择最明确的构造器,避免歧义。
3.4 构造器参数的类型转换与 @Lazy 适配
解析参数时,如果参数类型与容器中的 Bean 不完全一致,Spring 会尝试类型转换(通过 TypeConverter)。同时,若参数标记了 @Lazy,则 resolveDependency 会返回一个 CGLIB 代理,而非直接实例化目标 Bean,从而实现延迟注入。
3.5 构造器注入序列图
sequenceDiagram
participant BF as AbstractAutowireCapableBeanFactory
participant CR as ConstructorResolver
participant Candi as 候选构造器列表
participant Dep as resolveDependency
BF->>BF: createBeanInstance()
BF->>CR: autowireConstructor(beanName, mbd, chosenCtors)
CR->>CR: 确定候选构造器<br/>(@Autowired或单构造器)
loop 按参数个数降序遍历
CR->>Dep: resolveConstructorArguments(参数列表)
Dep->>Dep: 每个参数调用resolveDependency
Dep-->>CR: 解析参数数组
alt 全部解析成功
CR->>CR: 计算类型差异权重
CR->>CR: 记录当前最佳匹配
opt 完美匹配(权重0)
CR-->>CR: 直接选择该构造器
end
else 解析失败
CR->>CR: 尝试下一个构造器
end
end
CR->>BF: 反射调用选定构造器创建实例
BF-->>BF: 返回包装的BeanWrapper
主旨概括:本图完整刻画了构造器注入的核心判断与执行流程,突出贪心选择和备选机制。
逐层分解:首先确定候选构造器(两条路径),然后按参数个数降序尝试,每一步依赖 resolveDependency 解出参数。
设计原理:贪心算法降低循环次数,权重机制保证在有多个可匹配构造器时仍能确定性地选择最佳构造器。这与编译器查找重载方法的逻辑类似。
工程联系与结论:构造器注入要求依赖在创建一刻就绪,对架构的约束力强,但此约束恰是防止运行时错乱的保护。构造器循环依赖无法破解,根源就在于此时还没有对象引用可放入三级缓存。
4. 统一的依赖解析核心:resolveDependency
4.1 核心方法概览
字段注入、设值注入、构造器注入最终都调用 DefaultListableBeanFactory.resolveDependency()。该方法根据 DependencyDescriptor 的信息(类型、方法参数、@Qualifier、@Lazy、@Value 等)决定解析策略。
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
// 1. 构建ResolvableType以携带泛型信息
ResolvableType type = descriptor.getResolvableType();
// 2. 检查各种快捷方式(如缓存的ShortcutDependencyDescriptor)
// 3. @Lazy代理处理
if (Optional.class.isAssignableFrom(type.resolve())) {
// Optional特殊处理
}
// 4. ObjectFactory / Provider适配
// 5. @Value处理:使用StringValueResolver
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
// 转换为字符串并解析占位符/SpEL
}
// 6. 普通依赖查找
Map<String, Object> matchingBeans = findAutowireCandidates(beanName, type, descriptor);
// 6.1 若为空且required,则抛异常
// 6.2 若多个候选,根据@Primary/@Priority/@Qualifier裁决
// 6.3 集合/Map类型调用相应分支
if (type.isArray() || Collection.class.isAssignableFrom(type.resolve())) {
// 注入所有匹配Bean,排序
} else if (Map.class.isAssignableFrom(type.resolve())) {
// Key为beanName, value为对应实例
} else {
// 单个Bean注入
}
return value;
}
4.2 泛型匹配与 ResolvableType
这是 Spring 4.0 革命性的提升。DependencyDescriptor.getResolvableType() 携带了完整的泛型信息。例如,当你注入 Repository<User> 时,findAutowireCandidates 会经由 GenericTypeAwareAutowireCandidateResolver 提取出 User,与每个候选 Bean 的泛型声明比对,确保只返回真正匹配的 Bean。@Resource 没有此能力,它仅按名称或类型不感知泛型。
4.3 集合与 Map 注入
- 注入
List<X>或Set<X>时,findAutowireCandidates查找所有类型匹配 X 的 Bean,排序后包装为不可变的集合(Spring 5.x 默认使用Collections.unmodifiableList)。 - 注入
Map<String, X>时,Key 为 Bean 的 ID,Value 为对应实例。若泛型 Key 不是 String,会尝试类型转换。 - 如果未找到任何匹配 Bean,集合注入会返回空集合而非 null,这是 Spring 的设计特性,可避免 NPE,但容易导致业务逻辑错误(如期望 null 判断时失效)。
4.4 延迟注入与 Optional 支持
@Lazy:为依赖创建一个 CGLIB 代理,仅当真正调用方法时才触发目标 Bean 的初始化。ObjectFactory、Provider:注入工厂对象,每次调用getObject()都重新从容器取,适合 prototype 范围的依赖。Optional:如果找不到依赖,注入Optional.empty(),实现了更优雅的可选依赖表达。
4.5 resolveDependency 处理流程图
flowchart TD
A["resolveDependency"] --> B{"是否为快捷缓存"}
B -->|"是"| Z["返回缓存值"]
B -->|"否"| C{"DependencyDescriptor分析"}
C --> D{"Value注解存在吗"}
D -->|"是"| E["StringValueResolver处理占位符或SpEL"]
D -->|"否"| F{"Lazy注解存在吗"}
F -->|"是"| G["创建延迟代理"]
F -->|"否"| H{"类型是否为Optional"}
H -->|"是"| I["Optional处理 封装空"]
H -->|"否"| J{"类型是否为Collection或Map"}
J -->|"是 集合"| K["findAutowireCandidates 返回所有匹配Bean列表或Map"]
J -->|"否 单Bean"| L["findAutowireCandidates 通过Primary或Qualifier决出单个"]
K --> M["排序或转换返回"]
L --> N{"多候选"}
N -->|"单候选"| O["返回该Bean"]
N -->|"多候选"| P{"Primary或Priority注解存在吗"}
P -->|"成功裁决"| O
P -->|"无法裁决"| Q["抛出NoUniqueBeanDefinitionException"]
M --> R["返回集合或Map"]
主旨概括:流程图展示了 resolveDependency 如何根据描述符类型分流处理,覆盖从单纯 Bean 查找、集合注入、延迟代理到占位符解析的全部分支。
逐层分解:先检查注解捷径(@Value、@Lazy),再分派到集合/Map 或单 Bean 查找;单 Bean 查找再细分为无候选、多候选裁决等场景。
设计原理:采用责任链模式思想,每个分支负责一种依赖抽象,使得新增依赖策略(如 Reactor 类型)只需添加新分支,而无需修改核心流程。
工程联系与结论:掌握此图即可预判任何注入场景的结果,并能判断诸如“为什么我注入了一个空 List”或“@Qualifier 为什么没起作用”之类问题的根源。
5. @Resource 与 @Inject 的对比
@Resource 由 CommonAnnotationBeanPostProcessor 处理,执行时机在 @Autowired 之后(优先级较低)。它先按名称(name 属性)查找,若未指定名称则使用字段名或 setter 属性名;找不到再按类型匹配。不支持 required 属性,不支持泛型感知。
@Inject 是 JSR-330 标准,Spring 需额外引入 javax.inject。其语义类似于 @Autowired(required=false),但不支持 required 和 @Value。Spring 对它的支持集中于 AutowiredAnnotationBeanPostProcessor 扩展,内部利用同样机制。
| 特性 | @Autowired | @Resource | @Inject |
|---|---|---|---|
| 处理类 | AutowiredAnnotationBeanPostProcessor | CommonAnnotationBeanPostProcessor | AutowiredAnnotationBeanPostProcessor (扩展) |
| 解析策略 | 先类型,结合@Qualifier | 先名称,再类型 | 先类型,结合@Qualifier |
| required支持 | 支持 | 不支持 | 不支持(默认非必须) |
| 泛型匹配 | 全面支持(ResolvableType) | 不支持 | 支持(同@Autowired) |
| 集合/Map注入 | 支持 | 不支持 | 支持 |
| @Lazy配合 | 支持 | 不支持 | 支持 |
| @Value支持 | 支持 | 不支持 | 不支持 |
(此表格在原文中将以 markdown 表格展示,此处从简。)
6. 三种注入方式的设计权衡与选择决策
基于前文源码细节,从架构维度进行六维深度比较:
不可变性
构造器注入可将依赖字段声明为 final,确保对象构造完成后依赖引用不可变;字段注入和设值注入无法做到,字段可被任意后续代码修改,破坏安全性。
可测试性
构造器注入允许在单元测试中直接 new 并传入 Mock 对象,完全脱离 Spring 容器;字段注入和设值注入则必须启动容器或使用 ReflectionTestUtils,测试代码复杂且脆弱。
循环依赖
构造器注入无法处理循环依赖,因为实例化时对象尚未暴露引用;字段/设值注入在 populateBean 阶段可利用三级缓存(singletonFactories)提前暴露引用然后注入,临时解决循环依赖,但架构上仍不推荐。
代理安全性
构造器注入的依赖是实例化时传递的原始对象,若后续 BeanPostProcessor 将依赖改为代理,构造器内的局部变量引用的仍是原始对象?实际上,构造器持有的引用与容器最终暴露的是同一个对象(容器内是单例),但 AOP 代理是在初始化后 postProcessAfterInitialization 创建的,若依赖本身被代理,构造器内调用依赖的方法可能是原始对象(但一般依赖是一个接口,代理是在依赖 Bean 初始化后执行,容器注入到当前 Bean 时已经是代理)。而对于字段注入,如果 @PostConstruct 中调用 this.xxx 方法,触发的却是原始对象而非动态代理(因为代理此时尚未创建),导致事务、异步等切面失效。
依赖清晰度
构造器参数列表即为依赖清单,一目了然;字段注入分散在类的各处,易出现“上帝类”大量隐藏依赖,违反单一职责。
框架耦合
构造器注入零注解(Spring 4.3+),甚至可以脱离 Spring 上下文;字段/设值注入必须使用 Spring 注解,将应用与框架绑定。
选择决策树
graph TD
Start["新依赖"] --> IsRequired{"是否强制依赖"}
IsRequired -->|"是"| Ctor["构造器注入"]
IsRequired -->|"否 可选依赖"| OptType{"表达方式"}
OptType -->|"单依赖"| SetterOrOpt["设值注入或 Optional或 ObjectProvider"]
OptType -->|"多依赖集合"| ListOrCtor["构造器注入或字段注入集合"]
Start --> IsCollection{"是否为集合或Map"}
IsCollection -->|"是"| CtorOrField["构造器注入或字段注入 推荐构造器以保持不可变"]
IsCollection -->|"否"| IsConfig{"配置值"}
IsConfig -->|"是"| ValueField["字段注入 Value注解 或构造器参数"]
IsConfig -->|"否"| Ctor
Ctor --> FinalCheck{"是否希望不可变"}
FinalCheck -->|"是"| Ctor
FinalCheck -->|"否"| StillCtor["仍推荐构造器以保证清晰度"]
主旨概括:决策树以“是否强制依赖”为第一判断点,强制依赖一律推荐构造器注入;可选依赖可根据场景选择设值或构造器配合 Optional;集合和配置值也有明确导向,最终推崇构造器注入。
逐层分解:从根节点开始逐级细化,每个分支都基于前置模块的源码特性,例如构造器注入对 final 的支持、集合注入的不可变性等。
设计原理:决策依据源自需求的约束强度,构造器注入表达“没有此依赖我无法工作”,设值/字段注入表达“这个依赖可以空缺或后续提供”,这一语义应与代码结构对等。
工程联系与结论:遵循本决策树,团队可以减少代码审查中关于“该用哪种注入”的争论,且重构时方向明确。
7. 生产事故排查专题
案例1:@PostConstruct 中访问 @Autowired 字段 NPE
现象:服务启动后,某 Bean 的 @PostConstruct 方法里调用 dependency.doSomething() 时抛出 NullPointerException。
排查思路:查看该 Bean 类,发现 dependency 字段标记 @Autowired,但在初始化方法中仍为 null。检查 Bean 的依赖注入是否被短路(如自定义 InstantiationAwareBeanPostProcessor 返回 true 直接跳过属性填充)。进一步观察日志,发现该 Bean 涉及循环依赖,早期引用在填充字段之前暴露,导致字段未注入。
根因分析:当发生循环依赖时,Spring 将原始 Bean 实例提前暴露给 singletonFactories,但此时属性填充尚未执行(字段注入在 populateBean 中)。如果在早期引用的 Bean 的初始化方法中调用了本 Bean 的 @PostConstruct(经由依赖回调),就会看到字段仍是 null。根源是字段注入没有在构造时就强制保证依赖存在,使其对时序敏感。
解决方案:改用构造器注入,将 dependency 作为构造器参数,让对象在构造完成时就获得真实依赖(或至少通过构造器参数暴露循环依赖问题,迫使重构)。重构消除循环依赖后,字段不再为空。
最佳实践:核心依赖必须使用构造器注入;@PostConstruct 只进行本 Bean 内部的状态校验,不应假设外部依赖已初始化完毕。
案例2:@Transactional 事务失效,this 调用绕过代理
现象:Service 类中 methodA() 调用 methodB(),methodB 标记了 @Transactional,但事务未生效,数据未回滚。
排查思路:检查 Spring AOP 配置,确认事务代理正常工作;然后发现调用发生在同一个类内部,通过 this.methodB()。由于 Spring 的 AOP 代理是基于容器返回的代理对象,内部 this 指向原始对象,不会触发切面。
根因分析:字段注入的对象是该类的原始实例,AOP 代理在 postProcessAfterInitialization 阶段创建。注入到其他 Bean 中的是代理对象,但类内部自调用自然绕过了代理。开发人员常因字段注入的“便捷”而忽略了代理的存在,从而埋下隐患。
解决方案:采用 AopContext.currentProxy() 获取当前代理再调用,或者将事务方法移至另一个 Bean 并通过依赖注入调用。更根本的是警醒开发者:所有公共可切面方法都应通过容器注入的依赖调用。
最佳实践:团队应避免类内部自调用,配合构造器注入 + 成员依赖管理,使调用路径始终通过注入的代理。
案例3:构造器注入循环依赖导致启动失败
现象:BeanCurrentlyInCreationException,信息指示 A 构造器依赖 B,B 构造器依赖 A,容器无法实例化任何一方。
排查:查看两个类的构造器,发现互相以对方为参数。这是典型的构造器循环依赖。
根因分析:构造器注入在 createBeanInstance 阶段执行,此时对象尚未创建完毕,无法放入三级缓存暴露早期引用。因此循环依赖必然失败。相比之下,设值注入可以在对象创建后再填充属性,通过三级缓存解决。但循环依赖本身暗示设计缺陷。
解决方案:重构,引入中间协调 Bean,或者让一方依赖接口/事件机制而不是直接依赖实现。若必须保留,可临时将一方改为设值注入(但需在文档中标注并制定消除计划)。
最佳实践:构造器注入强制暴露循环依赖,应视为架构警报,立即着手解耦,而非绕过。
案例4:@Autowired 注入 List 得到空集合,业务逻辑失效
现象:某插件机制中,预期未配置任何插件时 List<Plugin> 应为 null,但实际得到了空的 ArrayList,导致业务判断“是否有插件”的分支走入错误逻辑。
排查:查看注入代码,发现 @Autowired private List<Plugin> plugins;。查阅 Spring 文档及源码:集合注入默认最少返回空集合,而非 null。
根因分析:findAutowireCandidates 找不到匹配 Bean 时,返回空集合作为默认值,避免 NPE。然而业务逻辑未意识到这点,通过 if (plugins == null) 判断失败。
解决方案:使用 Optional<List<Plugin>> 或 ObjectProvider<List<Plugin>> 明确表达“可能没有”的语义;或者修改判断条件 if (plugins.isEmpty())。
最佳实践:深入理解框架的注入约定,为空安全的集合判断预留 isEmpty() 路径;对于真正可选的集合依赖,用 ObjectProvider 或 @Autowired(required = false) + Optional 更清晰。
8. 面试高频专题
(本模块与正文严格隔离,以问答形式呈现)
1. Spring 中有哪些依赖注入方式?各有什么优缺点?
标准回答:字段注入、设值注入(setter/方法)、构造器注入。字段注入简洁但隐藏依赖、难以测试;设值注入灵活但破坏不可变性;构造器注入支持不可变、测试友好,Spring 官方推荐。
追问①:为什么字段注入会导致隐藏依赖? 答:依赖散落在类中,无法从构造器签名看到所需依赖数量,类膨胀难以发现。
追问②:设值注入在什么场景下优于构造器? 答:当依赖可能有多个可选实现且需在运行时切换,或父类模板设计时。
追问③:如何利用源码论证构造器注入的不可变性优势? 答:构造器注入发生在 createBeanInstance,依赖赋值后对象已完整;字段注入通过反射在后续赋值,无法声明 final。
加分回答:列举 Spring 官方在 4.x 文档中明确推荐构造器注入,并解释其背后的不可变性与简化测试原则。
2. @Autowired 的工作原理是什么?从扫描到注入的完整流程是怎样的?
标准回答:AutowiredAnnotationBeanPostProcessor 在 postProcessProperties 阶段,通过 findAutowiringMetadata 遍历类层次结构收集带 @Autowired 的字段和方法,封装为 InjectionMetadata 并缓存;然后遍历每个 InjectedElement,调用 resolveDependency 解析依赖,最后反射赋值或调用方法。
追问①:InjectionMetadata 是什么,为什么要缓存? 答:包含 Bean 的所有注入点列表,缓存避免反射重复扫描,提升性能。
追问②:字段注入和设值注入在 InjectedElement 中有何不同? 答:分别是 AutowiredFieldElement 和 AutowiredMethodElement,前者 field.set,后者 method.invoke。
追问③:@Value 是如何融入到这套流程的? 答:在解析依赖时,getSuggestedValue 发现 @Value 则走 StringValueResolver 而不是容器查找。
加分回答:结合 InstantiationAwareBeanPostProcessor 接口契约,说明 postProcessProperties 返回会短路后续处理,因此可自定义注入逻辑。
3. 构造器注入和设值注入的区别?为什么 Spring 官方推荐构造器注入?
标准回答:构造器注入在实例化时提供所有依赖,保证对象创建后处于完整状态,依赖可声明为 final;设值注入在实例化后通过 setter 赋值,容易产生状态不一致。Spring 推荐构造器注入因为其强制清晰依赖、安全且易于测试。
追问①:如果 Bean 有大量依赖,构造器注入会不会导致构造器太庞大?对此有什么应对? 答:是的,可以看作考虑重构的信号,或将相关依赖封装成配置类作为参数。
追问②:构造器注入如何处理可选依赖? 答:使用 Optional<T> 或 ObjectProvider<T> 作为构造器参数。
追问③:从 Spring 源码角度说明为什么构造器注入无法解决循环依赖? 答:因为它在 createBeanInstance 阶段执行,此时尚未生成任何 Bean 引用可放入三级缓存。
加分回答:引用《Effective Java》“考虑使用构造器注入代替字段注入”条目,强调其不变性和避免 Lombok @Setter 滥用。
4. 如果只有一个构造器,还需要加 @Autowired 吗?从哪个版本开始支持?背后的处理路径有何不同?
标准回答:Spring 4.3+ 无需加注解,容器会自动识别该唯一构造器进行注入。处理路径在 determineCandidateConstructors 中,若无 @Autowired 构造器且只有一个有参构造器,则自动将其作为候选。
追问①:如果有多个构造器但都没注解,Spring 怎么做? 答:使用默认无参构造器,若有参无默认且未注解则抛出异常。
追问②:这条自动识别规则是否支持 @Lazy 和 @Qualifier 等? 答:支持,参数上的这些注解依然能被识别。
追问③:这条路径与显式 @Autowired 在源码执行上有何细微区别? 答:最终都进入 autowireConstructor,但显式路径允许多候选并进行贪心选择;自动识别只有单候选,直接尝试解析。
加分回答:指出此特性减少了与 Spring 的耦合,使 POJO 更加纯净。
5. @Autowired 和 @Resource 的区别?分别在什么时候被处理?泛型场景下表现有何差异?
标准回答:@Autowired 由 AutowiredAnnotationBeanPostProcessor 处理,按类型优先;@Resource 由 CommonAnnotationBeanPostProcessor 处理,先名称后类型。@Autowired 支持泛型匹配(通过 ResolvableType),@Resource 不支持,只按原始类型查找。
追问①:为什么 @Resource 不支持泛型? 答:其处理流程只用 Class<?> 类型,未集成 GenericTypeAwareAutowireCandidateResolver。
追问②:如果同时使用 @Autowired 和 @Resource 在同一个字段,哪个生效? 答:后者覆盖,但这是未定义行为,应避免。
追问③:@Resource 在找不到依赖时异常吗? 答:会,因为没有 required 属性,默认必须。
加分回答:相关源码入口:CommonAnnotationBeanPostProcessor.autowireResource。
6. 如何处理可选依赖?@Autowired(required=false)、Optional、@Nullable 有什么不同?
标准回答:required=false 时,找不到依赖容器不会抛异常,字段保持 null;Optional<T> 则依赖 resolveDependency 识别并返回 Optional.empty();@Nullable 通常用于 Kotlin 空安全,Spring 对其也有支持(若找不到返回 null)。
追问①:三者在注入行为上是否完全一致? 答:Optional 更优雅,避免了 null 检查和 NullPointerException;required=false 需要在业务代码手动判空;@Nullable 更多是 IDE/编译器提示。
追问②:构造器注入如何表达可选依赖? 答:构造器参数用 Optional<T> 或 ObjectProvider<T>,并注意 required 概念被内置为 Optional 语义。
追问③:源码中 Optional 解析分支在哪? 答:DefaultListableBeanFactory.resolveDependency 中检测是否为 Optional 类型,执行特殊包装。
加分回答:建议在 Kotlin 中使用构造器注入 + 可空类型,完美整合。
7. 集合注入(List<MyBean>)和 Map 注入(Map<String,MyBean>)是怎么实现的?泛型匹配在其中的作用?
标准回答:findAutowireCandidates 查找所有类型匹配的 Bean,对于集合类型直接返回查找结果并排序;Map 类型则以 Bean 名称作 Key 实例作 Value。ResolvableType 提取泛型参数,确保只匹配恰好符合泛型约束的 Bean,而非所有 Object 类型。
追问①:如果没有匹配,集合注入返回什么? 答:空集合,不会为 null。
追问②:如果希望集合有序,应该怎么做? 答:可通过 @Order、Ordered 接口或 @Priority 控制顺序,Spring 会使用 AnnotationAwareOrderComparator 排序。
追问③:Map 注入时 Key 必须是 String 吗? 答:默认是,若泛型 Key 为其他类型会尝试转换,但可能失败。
加分回答:源码中 DefaultListableBeanFactory.resolveMultipleBeans 负责集合注入实现。
8. @Lazy 注解在依赖注入中是怎么工作的?有什么限制?与构造器注入配合时需要注意什么?
标准回答:@Lazy 让 resolveDependency 为依赖创建一个 CGLIB 代理,延迟目标 Bean 的初始化直到第一次方法调用。限制:final 类和 final 方法无法代理;与构造器注入配合时,构造器参数必须能接受代理(除非使用 ObjectProvider)。
追问①:@Lazy 代理何时触发真正的初始化? 答:第一次调用代理的任意方法时。
追问②:为什么 final 类不能代理? 答:CGLIB 通过继承目标类生成子类代理,final 禁止继承。
追问③:与 ObjectFactory 比较有什么不同? 答:ObjectFactory 每次 getObject 都重新获取,@Lazy 单例依赖每次调用仍是同一代理对象,但背后单例只初始化一次。
加分回答:说明 @Lazy 解决循环依赖的原理:用代理打破初始化链。
9. 字段注入有什么致命缺陷?在生产环境中引发过哪些典型事故?
标准回答:致命缺陷包括:依赖隐藏、无法 final、测试困难、循环依赖时的 NPE(如本文案例1)。生产事故如 @PostConstruct NPE、事务失效自调用等。
追问①:能否通过静态分析发现依赖隐藏问题? 答:部分工具可以,但不如构造器直观。
追问②:为什么 Spring 不完全废弃字段注入? 答:历史遗留和快速原型便利性。
追问③:怎样在团队中推行构造器注入? 答:通过代码规范、IDE 模板,并结合架构评审强调构造器注入优势。
加分回答:引用本文事故案例说明问题。
10. 构造器注入能否解决循环依赖?为什么?如果必须解决该如何处理?
标准回答:不能,因为实例化时尚未有对象引用可暴露。临时方案可改一方为设值注入,但根本应重构消除循环。
追问①:三级缓存具体如何帮助设值注入解决循环依赖? 答:实例化后立即将 Lambda 放入三级缓存,填充属性时若需依赖对方,可从三级缓存获取早期引用,属性填充后再完成完整初始化。
追问②:如何发现系统中的循环依赖? 答:使用 Spring 的 spring.main.allow-circular-references=false 并观察报错(Spring Boot 2.6+ 默认禁止)。
追问③:重构循环依赖常见模式有哪些? 答:引入中介者、事件、共享接口等。
加分回答:指出 Spring 不推荐循环依赖,即使能解决也属于坏味道。
11. @Autowired 标记在方法上除了 setter 还有什么用途?可以注入多个参数吗?
标准回答:可标记任意方法,Spring 会一次性注入方法所有参数,用于批量装配配置或初始操作;可注入多个参数。
追问①:方法注入与 @PostConstruct 有何不同? 答:@Autowired 方法在属性填充阶段被调用,早于 @PostConstruct;@PostConstruct 是初始化回调。
追问②:如果方法需要参数但非 required,未解析到依赖会怎样? 答:全有或全无:如果任何一个参数解析失败且 required=true 则异常;若 required=false 则方法不会被调用。
追问③:方法注入能否用于动态代理或装饰? 答:不适合,因为方法调用是一次性,但可在方法内使用装饰器模式。
加分回答:展示源码 AutowiredMethodElement.inject 中参数逐个解析的逻辑。
12. 如何自定义一个类似 @Autowired 的注入注解?需要实现哪些扩展点?
标准回答:自定义注解并实现 InstantiationAwareBeanPostProcessor,覆盖 postProcessProperties,仿照 AutowiredAnnotationBeanPostProcessor 构建 InjectionMetadata,最终调用 resolveDependency。
追问①:是否必须实现 MergedBeanDefinitionPostProcessor? 答:推荐,以便缓存元数据。
追问②:如何处理 @Qualifier 等限定注解? 答:在构建 DependencyDescriptor 时将其作为字段/方法上的注解传入。
追问③:是否可以不使用 AutowiredAnnotationBeanPostProcessor 直接写? 答:可以,完全独立,但会失去与现有处理流程的协作(如顺序)。
加分回答:概述需要重写的关键方法:postProcessProperties、buildAutowiringMetadata。
13. 依赖注入发生在 Bean 生命周期的哪个阶段?字段/设值注入与构造器注入的时机有何不同?
标准回答:见文章第1、2、3模块。字段/设值注入在 populateBean;构造器注入在 createBeanInstance。
追问①:这个时机差异对 @PostConstruct 的使用有何影响? 答:构造器注入的依赖在 @PostConstruct 执行时一定可用;字段注入在常规情况也可用,但循环依赖场景可能为 null。
追问②:BeanPostProcessor 的前置处理能修改构造器注入的依赖吗? 答:不能修改引用,但可改变 Bean 内部状态(如代理目标)。
追问③:为什么 Spring 不统一在 populateBean 做构造器注入? 答:因为构造器注入关系对象创建本身,必须提前。
加分回答:阐述了生命周期的分治优势。
14. 为什么字段注入的 Bean 在 @PostConstruct 中访问依赖可能为 null?如何规避?
标准回答:循环依赖导致早期引用提前暴露,此时字段未注入。规避:构造器注入。
追问①:除了循环依赖,是否还有其他原因导致字段为 null? 答:自定义 InstantiationAwareBeanPostProcessor 短路了属性填充。
追问②:如何诊断是循环依赖导致? 答:查看日志或者使用 allow-circular-references=false 触发报错。
追问③:可否在 @PostConstruct 中通过 ApplicationContext.getBean 手动获取? 答:不推荐,破坏了 DI 契约。
加分回答:展示案例源码。
15. @Autowired 的缓存机制是怎样的?如何提升解析性能?
标准回答:injectionMetadataCache 按 BeanName 缓存;AutowiredFieldElement 和 AutowiredMethodElement 内部也缓存依赖描述符和解析结果。
追问①:缓存的生命周期是什么? 答:与容器生命周期一致。
追问②:如果存在大量原型 Bean,缓存有何影响? 答:原型 Bean 的注入点元数据依然可缓存,但依赖解析可能每次都不同,字段级的缓存会重用相同的依赖实例(针对单例命名依赖)。
追问③:如何避免缓存带来的内存泄漏? 答:容器关闭时全部清理,通常无泄漏。
加分回答:解释 ShortcutDependencyDescriptor 如何加速解析。
16. 什么是依赖注入中的“依赖隐藏”?为什么说构造器注入能暴露设计问题?
标准回答:依赖隐藏指类的依赖没有在其构造函数或工厂方法中明确声明,而是通过字段或 setter 隐藏,使维护者无法快速看清类的契约。
追问①:这种隐藏会带来哪些实际危害? 答:类膨胀、测试遗漏、重构风险。
追问②:能否通过工具检查? 答:IntelliJ IDEA 可提示字段不应被注入。
追问③:构造器注入如何暴露设计问题? 答:构造器参数过多时迫使开发者意识到类职责过重,从而拆分。
加分回答:援引 Clean Code 原则。
17. 使用 ObjectFactory 或 Provider 进行延迟注入,与 @Lazy 有什么区别?
标准回答:ObjectFactory/Provider 每次调用 getObject() 都重新从容器获取(prototype 效果);@Lazy 则是延迟初始化单例代理,大部分场景只有一次初始化。
追问①:性能开销? ObjectFactory 每次查询容器可能有开销;@Lazy 除首次外只有代理转发的开销。
追问②:在 prototype Bean 场景下如何选择? 答:必须用 ObjectFactory/Provider。
追问③:Provider 是 JSR-330 的,Spring 如何处理? 答:AutowiredAnnotationBeanPostProcessor 内部判断并适配。
加分回答:源码中 Jsr330Factory 的适配逻辑。
18. 单例 Bean 中注入 prototype Bean 时,如何保证每次获取都是新实例?有哪些实现方式?
标准回答:方法之一是通过 @Scope(“prototype”) + ObjectFactory/Provider 注入;也可使用 @Lookup 方法注入。
追问①:为什么普通字段注入单例里获取 prototype 总是同一个? 答:因为单例创建时只注入一次。
追问②:@Lookup 的原理? 答:CGLIB 生成方法体调用容器 getBean。
追问③:与 ApplicationContext 直接获取有何不同? 答:@Lookup 更解耦,不依赖容器 API。
加分回答:@Lookup 的源码:LookupOverride。
19. @Value 注入是如何工作的?它和 @Autowired 走的是同一套机制吗?什么情况下 @Value 会失效?
标准回答:是同一套 AutowiredAnnotationBeanPostProcessor,但解析时走 StringValueResolver。失效原因:未配置 PropertySourcesPlaceholderConfigurer 或 SpEL 错误。
追问①:如何在单元测试中使 @Value 生效? 答:使用 TestPropertyValues 或 @TestPropertySource。
追问②:@Value 能注入复杂类型吗? 答:需配合 ConversionService。
追问③:@Value 的缓存与 @Autowired 一样吗? 答:字段/方法解析缓存一样,但值解析每次可能重新计算(取决于值来源动态性,但实际上也是缓存结果)。
加分回答:源码 DefaultListableBeanFactory 中 resolveDependency 通过 getSuggestedValue 判断。
20. 系统设计题一:插件化系统
题目:设计一个插件化系统,核心模块定义接口,业务模块实现并通过 DI 注册。要求在不修改核心代码的情况下实现不同插件的热替换,讨论如何利用构造器注入 + 集合注入实现插件的统一管理与隔离,并处理泛型匹配的问题。
回答:
- 使用集合注入
List<Plugin>在核心模块主构造函数中接收所有插件。 - 每个插件实现接口
Plugin<T>,利用泛型匹配确保只处理特定数据类型。Spring 的ResolvableType能精确筛选。 - 热替换通过调用
DefaultListableBeanFactory动态注册新的 BeanDefinition 并刷新依赖,或者利用@RefreshScope(Spring Cloud)刷新配置,但纯 Spring 下需用BeanDefinitionRegistry手动操作并发布事件。构造器注入的集合是不可变的,所以热替换需要重新创建持有者 Bean。 - 隔离可通过
@Qualifier或自定义注解区分不同批次的插件。
追问①:集合注入的顺序如何保证? 答:使用@Order。
追问②:如何做到插件动态卸载? 答:销毁 BeanDefinition 并回收实例,需要维护一个包装 Bean 避免集合引用失效。
追问③:泛型匹配如何测试? 答:单元测试检查注入集合内元素的具体泛型类型。
加分回答:结合 OSGi 设计思想,探讨 Spring 的ServiceLoader工厂式插件模式。
21. 系统设计题二:重构字段注入
题目:一微服务大量使用字段注入导致测试困难和 NPE 频发。设计一套重构策略,将字段注入逐步替换为构造器注入,同时保证过渡期兼容性和接口稳定性,讨论如何利用 @DependsOn 和 @Order 控制重构期间的依赖顺序。
回答:
- 首先,识别核心依赖和非核心依赖,对核心依赖优先重构。
- 采用“双写”过渡:同时保留字段注入和构造器注入,构造器注入依赖优先选用,逐步废弃字段。
- 为保证兼容性,可以不立即删除字段上的
@Autowired,但标记@Deprecated。 - 使用
@DependsOn控制 Bean 的初始化顺序,确保重构时新 Bean 旧 Bean 启动不交叉依赖。 - 测试中全部改用构造器实例化并 assert 非空。
- 最终清理字段注入,形成纯构造器注入。
追问①:如果某个 Bean 构造器参数较多,怎么避免构造器膨胀? 答:使用参数对象或事件总线替代部分依赖。
追问②:重构过程中如何处理可选依赖? 答:用Optional构造器参数。
追问③:@DependsOn在重构后还需要吗? 答:如果依赖关系已通过构造器自然保证,可以移除。
加分回答:分享实际项目重构的 PR 评审流程和分析步骤。
22. 依赖解析中的 @Primary 和 @Qualifier 是如何配合的?如果在集合注入中使用 @Qualifier 会发生什么?
标准回答:@Primary 提升 Bean 优先级,当未指定 @Qualifier 时优先;@Qualifier 则指定精确 Bean。集合注入中 @Qualifier 不起限定的筛选作用,Spring 不会按它过滤集合;必须使用自定义 @Qualifier 加 @Autowired 过滤(需自定义 QualifierAnnotationAutowireCandidateResolver)。
追问①:@Primary 与多个 @Qualifier 同时存在,哪个优先? 答:@Qualifier 直接命中,优先级高于 @Primary。
追问②:源码中裁决逻辑在哪里? 答:DefaultListableBeanFactory.determineAutowireCandidate。
追问③:如何实现集合注入的过滤? 答:实现自定义 AutowireCandidateResolver。
加分回答:阐述 ContextAnnotationAutowireCandidateResolver 设计。
23. 构造器注入的参数解析过程中,如何处理多个候选 Bean 时的歧义问题?源码是如何实现贪心选择的?
标准回答:贪心算法按参数个数降序尝试构造器,每个参数通过 resolveDependency 解析。遇到多候选 Bean 时,resolveDependency 本身利用 @Primary/@Qualifier 等裁决;若仍无法单一决策,则返回 null 表示此构造器失败,尝试下一个构造器。最终选择成功且权重最小的构造器。
追问①:如果所有构造器都失败,抛出什么异常? 答:BeanCreationException,信息包含所有尝试的失败描述。
追问②:权重是如何计算的? 答:类型匹配度,无需转换权重低,需多次转换权重高。
追问③:如何调试这种选择过程? 答:设置日志级别为 TRACE,可以看到 ConstructorResolver 的详细决策。
加分回答:源码中 ConstructorResolver 的 createArgumentArray 和 getTypeDifferenceWeight 方法。
9. Demo 代码(节选关键示例)
9.1 构造器注入 vs 字段注入 vs 设值注入
// 构造器注入 (推荐)
@Service
public class OrderService {
private final PaymentService paymentService;
private final InventoryService inventoryService;
public OrderService(PaymentService paymentService, InventoryService inventoryService) {
this.paymentService = paymentService;
this.inventoryService = inventoryService;
}
// 业务方法...
}
// 字段注入 (应避免)
@Service
public class OrderServiceField {
@Autowired
private PaymentService paymentService;
@Autowired
private InventoryService inventoryService;
}
// 设值注入 (可选依赖)
@Service
public class OrderServiceSetter {
private PaymentService paymentService;
@Autowired
public void setPaymentService(PaymentService paymentService) {
this.paymentService = paymentService;
}
}
解读:构造器注入的 OrderService 可以在单元测试中 new OrderService(mockPayment, mockInventory);字段注入必须启动容器。
9.2 集合注入与泛型匹配示例
public interface EventHandler<T> {
void handle(T event);
}
@Component
public class OrderEventHandler implements EventHandler<OrderEvent> { ... }
@Component
public class UserEventHandler implements EventHandler<UserEvent> { ... }
@Service
public class EventDispatcher {
private final Map<Class<?>, EventHandler<?>> handlerMap;
// 构造器注入所有 EventHandler
public EventDispatcher(List<EventHandler<?>> handlers) {
this.handlerMap = handlers.stream()
.collect(Collectors.toMap(
h -> ((Class<?>) GenericTypeResolver.resolveTypeArgument(h.getClass(), EventHandler.class)),
h -> h));
}
}
解读:List<EventHandler<?>> 能自动收集所有实现,借助 ResolvableType 精确匹配到泛型。
9.3 @Value 与 @Autowired 协同
@Component
public class AppConfig {
private final String appName;
private final DataSource dataSource;
public AppConfig(@Value("${app.name}") String appName,
@Qualifier("mainDataSource") DataSource dataSource) {
this.appName = appName;
this.dataSource = dataSource;
}
}
解读:构造器参数 @Value 和 @Qualifier 互补,提供配置值和 Bean 引用共同注入的完美示例。
9.4 循环依赖重构对比
// 坏设计:构造器循环依赖
@Component
class A { public A(B b) {} }
@Component
class B { public B(A a) {} } // 启动报错
// 重构:引入ABCoordinator
@Component
class A { public A() {} public void setB(B b) { ... } }
@Component
class B { public B() {} }
@Component
class ABCoordinator {
public ABCoordinator(A a, B b) {
a.setB(b);
}
}
(更多Demo代码将在完整博客中展示,包括JMH性能测试等)
附录:依赖注入决策速查表
| 场景 | 推荐注入方式 | 语法/注解 | 注意事项 | 源码入口 |
|---|---|---|---|---|
| 强制依赖 | 构造器注入 | 构造器参数 | 保证不可变,可声明final | ConstructorResolver.autowireConstructor |
| 可选依赖 | 构造器注入 + Optional 或设值注入 | Optional<T> 参数 / @Autowired(required=false) setter | 避免字段注入隐藏依赖 | AutowiredMethodElement |
| 配置值 | 构造器注入或字段注入 | @Value | 需PropertySources配置 | StringValueResolver |
| 集合/Map注入 | 构造器注入或字段注入 | List<Bean> / Map<String,Bean> | 空集合而非null;泛型匹配准确 | findAutowireCandidates |
| 延迟获取 | 构造器注入 + ObjectProvider | ObjectProvider<T> | 每次获取新实例,适合prototype | resolveDependency 分支 |
| 多实现选择 | 构造器注入 + @Qualifier | @Qualifier(“name”) | 配合@Primary明确优先级 | determineAutowireCandidate |
| prototype注入单例 | 构造器注入 + Provider | Provider<T> | 不能直接用字段注入 | Jsr330Factory 适配 |
延伸阅读
- 《Spring 揭秘》王福强 - DI 章节深入浅出。
- Spring Framework 官方文档 “Dependencies” 部分 - 最权威指南。
- 《Effective Java》 - 第5条“优先考虑依赖注入来代替硬连接资源”。
- Martin Fowler “Inversion of Control Containers and the Dependency Injection pattern” - DI 概念起源。
- Spring 源码分析系列博客 - 关注
AutowiredAnnotationBeanPostProcessor与ConstructorResolver。