夜雨聆风学习资料网

ARTICLE · 1162236

@Autowired 注入源码:字段怎么被反射 set 进去的

@Autowired 注入源码:字段怎么被反射 set 进去的

@Autowired 注入源码:字段怎么被反射 set 进去的

💡 提示

《Spring 源码现形记》第 4 篇 | 源码基于 Spring Framework 6.2.x 分支

背完"byType 和 byName",追问就来了

面试官:"@Autowired 和 @Resource 有什么区别?"

候选人:"@Autowired 默认按类型注入,@Resource 默认按名称注入。"

面试官:"嗯。那 @Autowired具体怎么把一个 Bean 塞进你的字段里的?用的是反射吗?反射做了什么?"

候选人:……

"默认按类型"这句话,能背出来的人很多;能说出"类型匹配之后是谁把值写进字段"的,不多。这一篇就把这最后一百米走完——看 field.set() 那一行。

旧问法 vs 新问法

旧问法(背就完事)新问法(要真读过源码)

@Autowired 按类型还是按名称?类型匹配完之后,是谁把值写进字段的?

@Autowired 和 @Resource 的区别?多个同类型 Bean 时,源码按什么顺序裁决用哪个?

为什么推荐构造器注入?字段注入和构造器注入,在源码里走的是两条完全不同的路——差在哪?

一、谁在处理 @Autowired

处理 @Autowired 的是 AutowiredAnnotationBeanPostProcessor——它是个 InstantiationAwareBeanPostProcessor(还记得篇2 讲的扩展点吗?)。它有两个入口:

java

// 入口一:收集注入元数据(在 doCreateBean 的第②步)publicvoidpostProcessMergedBeanDefinition(RootBeanDefinitionbd, Class<?>beanType, StringbeanName) {findInjectionMetadata(beanName, beanType, bd);}// 入口二:真正执行注入(在 populateBean 的 postProcessProperties)publicPropertyValuespostProcessProperties(PropertyValuespvs, Objectbean, StringbeanName) {InjectionMetadatametadata=findAutowiringMetadata(beanName, bean.getClass(), pvs);metadata.inject(bean, beanName, pvs);   // ← 注入在这里发生returnpvs;}

注意这个分工:元数据提前收集(还带缓存),注入在属性填充阶段执行。这就是"注解解析一次、注入多次复用"的实现方式。

二、metadata.inject:逐个注入点执行

InjectionMetadata 内部存着一份"注入点清单"(字段、方法各算一个 element):

java

publicvoidinject(Objecttarget, StringbeanName, PropertyValuespvs) throwsThrowable {for (InjectedElementelement : elementsToIterate) {element.inject(target, beanName, pvs);   // 字段元素、方法元素各走各的    }}

三、字段注入的真身:field.set()

@Autowired 字段对应的是 AutowiredFieldElement,它的 inject 只有几行,但每一行都是考点:

java

protectedvoidinject(Objectbean, StringbeanName, PropertyValuespvs) throwsThrowable {Fieldfield= (Field) this.member;                    // ① 拿到字段Objectvalue=resolveFieldValue(field, bean, beanName);  // ② 解析出要注入的对象if (value!=null) {ReflectionUtils.makeAccessible(field);            // ③ 干掉 private 限制field.set(bean, value);                           // ④ 反射写入!    }}

resolveFieldValue 里则是标准的依赖解析:

java

DependencyDescriptordesc=newDependencyDescriptor(field, this.required);Objectvalue=beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);

到这里,"字段怎么被塞进去的"就有答案了:ReflectionUtils.makeAccessible(field) + field.set(bean, value)。它绕过了 private、绕过了构造方法——因为它压根不是通过构造方法注入的。

四、类型匹配之后:多个候选怎么裁决

resolveDependency 走到 DefaultListableBeanFactory.doResolveDependency,按类型找到候选。候选超过一个时,determineAutowireCandidate 按固定的五步裁决(6.2.x 源码注释就是 Step 1~5):

顺序依据说明

Step 1@Primary有 primary 直接胜出;两个 primary 直接报错

Step 2a依赖名 = bean 名字段名/参数名与 bean 名匹配

Step 2b@Qualifier按限定符建议的名称匹配

Step 3@Priority优先级值最小(最高)者胜出

Step 4default-candidate唯一一个 default 候选才有效

Step 5直接注册的依赖兜底:self reference 场景

一个都没有 → NoSuchBeanDefinitionException;裁决不出来(多个候选、规则全平)→ NoUniqueBeanDefinitionException。你线上遇到的"注入歧义"报错,就出自这里。

五、构造器注入是另一条路

字段注入走 populateBean,构造器注入走的是更早的 createBeanInstance:

java

// createBeanInstance 内部Constructor<?>[]ctors=determineConstructorsFromBeanPostProcessors(beanClass, beanName);if (ctors!=null|| ...) {returnautowireConstructor(beanName, mbd, ctors, args);   // ← ConstructorResolver 出场}

ConstructorResolver.autowireConstructor 会:候选构造器排序 → 逐个尝试按类型解析参数 → 有歧义再按 @Qualifier/@Primary 收窄 → 最后选出一个构造器并反射调用。

看懂这张图,就理解了关键差别:

—构造器注入:依赖在实例化阶段解析 → 依赖是"造对象的前提";

—字段注入:对象先用无参构造造出来(此时依赖全是 null)→ 再到 populateBean 里反射塞进去。

六、加分点:为什么官方推荐构造器注入(用源码说话)

不吹不黑,就事论事,源码层面有三条:

[1]完整性:字段注入出来的对象,存在一段"依赖是 null"的窗口期;构造器注入的对象,一造出来就是"完整可用"的——字段可以用 final 修饰;

[2]可测性:构造器注入的依赖是显式参数,单元测试可以直接 new;字段注入则必须启动容器(或反射注入)才能拿到依赖;

[3]失败更早:构造器注入在实例化阶段就暴露问题,字段注入要等 populateBean 才报错——报错越早,排查越省事。

所以"官方推荐构造器注入"不是风格偏好,是依赖注入的时机决定的。

附:@Resource 走的是另一套

@Resource 由 CommonAnnotationBeanPostProcessor 处理,核心在 autowireResource:

java

Stringname=element.name;if (this.fallbackToDefaultTypeMatch&&element.isDefaultName&&!factory.containsBean(name)) {// 名称不存在时,才回退到按类型resource=autowireCapableBeanFactory.resolveDependency(...);}else {// 默认:按名称解析resource=autowireCapableBeanFactory.resolveBeanByName(name, element.getDependencyDescriptor());}

所以准确表述是:@Resource 默认按名称(未写 name 就取字段名),找不到才回退按类型——"按名称"来自 resolveBeanByName,不是背出来的。

速查表

问题答案(源码依据)

谁处理 @Autowired?AutowiredAnnotationBeanPostProcessor(两入口:收集元数据 + 执行注入)

字段怎么被写入?ReflectionUtils.makeAccessible(field) + field.set(bean, value)

什么时候写入?populateBean 的 postProcessProperties(属性填充阶段)

多个候选怎么选?determineAutowireCandidate:@Primary → 名字 → @Qualifier → @Priority → default

构造器注入在哪解析?createBeanInstance 阶段,ConstructorResolver.autowireConstructor

@Resource 怎么解析?CommonAnnotationBeanPostProcessor.autowireResource,默认 resolveBeanByName

动手验证(10 分钟)

[1]在 AutowiredFieldElement.inject 的 field.set(bean, value) 打断点——你会亲眼看到值是怎么被反射塞进去的;

[2]造两个同类型 Bean,一个加 @Primary,观察 determinePrimaryCandidate 怎么一票当选;

[3]两个都不加、直接注入,故意制造 NoUniqueBeanDefinitionException,看它把候选名单全列给你;

[4]把字段注入改成构造器注入,把字段声明成 final——感受"对象一造出来就完整"。

下一篇预告

@Autowired 注入的是对象,但有切面的 Bean 注入进去的其实是代理。Spring 到底怎么决定用 JDK 动态代理还是 CGLIB?代理链 proceed() 又是怎么一层层执行的?下一篇:《AOP 代理生成:Spring 怎么决定用 JDK 还是 CGLIB》。

本文由公众号原创 · 转载请注明出处

相关学习资料