深入 Spring 依赖注入底层原理

Spring 的依赖注入( DI )是其核心特性之一,但多数开发者仅停留在 @Autowired 的使用层面,对其底层如何解析依赖、处理复杂场景(如泛型、延迟注入)、解决歧义(如 @Primary )的逻辑知之甚少。本文将逐层拆解 Spring DI 的底层实现,带你看懂注解背后的 "黑盒"。

代码地址

一、Spring DI 底层核心组件铺垫

在分析具体场景前,需先明确支撑 Spring DI三大核心组件------ 它们是所有注入逻辑的基础,贯穿于后续所有场景:

组件类 核心作用
DefaultListableBeanFactory Spring 最核心的 Bean 工厂,负责 Bean 的注册、实例化、依赖解析(核心方法doResolveDependency
DependencyDescriptor 封装依赖的元信息:字段 / 方法参数、是否必填( required )、泛型类型、注解(如 @Lazy )等
ResolvableType 解决 Java 泛型擦除问题,精准获取泛型参数类型(如 List<Service> 中的 Service

二、基础依赖注入:从字段到延迟注入

BasicDependencyInjectionDemo 覆盖了 Spring DI 的基础场景,我们逐一拆解其底层逻辑。

1. 字段注入:@Autowired字段的解析流程

字段注入是最常用的场景,其底层核心是反射获取字段元信息 + doResolveDependency 解析依赖 ,对应代码中 testFieldInjection 方法:

底层步骤拆解:
  1. 反射获取目标字段 :通过InjectTargetBean.class.getDeclaredField("bean2"),模拟 Spring 扫描 Bean 时识别 @Autowired 字段的过程;

  2. 封装依赖描述符 :创建 DependencyDescriptor,传入字段和 required=false(对应 @Autowired(required=false) ),该对象会记录 "需要注入 Bean2 类型" 的核心需求;

  3. 核心依赖解析 :调用 beanFactory.doResolveDependency(...),这是 Spring DI 的 "心脏方法",内部流程包括:

    • 按字段类型(Bean2)查找所有候选 Bean

    • 处理 @Primary@Priority 等优先级注解;

    • 校验依赖是否存在(根据 required 属性);

    • 返回最终匹配的 Bean 实例。

关键结论:

字段注入的本质是 "反射获取字段元信息 → 封装为 DependencyDescriptor → 调用 doResolveDependency 解析",所有注解逻辑最终都转化为对该方法的参数控制。

2. 延迟注入:ObjectFactory与@Lazy的区别

testObjectFactoryInjectiontestLazyProxy 分别演示了两种延迟注入方式,但其底层实现完全不同:

(1)ObjectFactory:手动延迟获取
  • 底层逻辑ObjectFactorySpring 提供的 "延迟依赖接口",注入时并非直接返回 Bean2 实例,而是返回一个 ObjectFactory 代理;

  • 延迟关键 :只有调用 objectFactory.getObject() 时,才会触发 doResolveDependency 解析依赖,实现 "按需加载";

  • 适用场景 :循环依赖、Bean 创建成本高(如数据库连接)的场景。

(2)@Lazy:自动生成代理对象
  • 底层逻辑@Lazy 会触发 Spring 创建CGLIB/JDK 动态代理DemoBean2是类,故用 CGLIB),注入的是代理对象而非真实Bean2 实例;

  • 延迟关键 :首次调用代理对象的方法(如 bean2.toString() )时,才会触发真实 Bean2 的实例化和依赖注入;

  • 核心组件ContextAnnotationAutowireCandidateResolver 负责识别 @Lazy 注解,通过getLazyResolutionProxyIfNecessary 生成代理,代理的拦截器会管控实例化时机。

关键区别:
特性 ObjectFactory @Lazy
触发方式 手动调用 getObject() 自动触发(首次方法调用)
注入对象类型 ObjectFactory 实例 CGLIB/JDK 代理对象
适用场景 主动控制加载时机 被动延迟初始化(透明化)

3. Optional:Spring 对 Java 8 的特殊适配

testOptionalInjection 演示了 Optional 包装注入,其底层核心是泛型解析 + 自动包装

  1. DependencyDescriptor 识别字段类型为 Optional,调用 increaseNestingLevel() 提升泛型层级,获取内部真实类型( Bean2 );

  2. Bean2 类型解析依赖,若存在则用 Optional.of(targetBean) 包装,不存在则返回 Optional.empty()

  3. 本质:Spring 通过 ResolvableType 处理泛型,将 Optional 视为 "特殊容器类型",自动完成包装逻辑,避免 NPE

三、复杂场景注入:数组、泛型与特殊对象

ComplexDependencyInjectionDemo 覆盖了实际开发中更复杂的 DI 场景,核心是 "如何处理非单一 Bean 的依赖需求"。

1. 数组 / List 注入:批量获取同类型 Bean

数组( Service[] )和 List(List<Service>) 注入的底层逻辑一致,都是 "解析元素类型 → 批量查找 Bean → 组装为目标类型",以数组注入( testArrayInjection )为例:

底层步骤:
  1. 识别数组类型:通过 arrayDescriptor.getDependencyType().isArray() 判断为数组,调用 getComponentType() 获取元素类型( Service );

  2. 批量查找 Bean:用 BeanFactoryUtils.beanNamesForTypeIncludingAncestors(...) 获取所有 Service 类型的 Bean 名称(service1service2service3);

  3. 组装为数组:通过 beanFactory.getTypeConverter()Spring 内置类型转换器)将 List 转换为 Service[],完成注入。

关键结论:

数组 / List 注入的核心是 "先解析元素类型,再批量获取所有匹配 Bean",Spring 会自动处理类型转换,无需手动组装。

2. 泛型匹配注入:Dao如何精准匹配?

Java 泛型存在 "擦除" 问题,Spring 通过 ResolvableType 解决该问题,实现 Dao<Teacher>Dao<Student> 的精准匹配(对应testGenericMatchingInjection):

底层步骤:
  1. 获取目标字段:InjectionTarget.teacherDao(类型 Dao<Teacher>),创建 DependencyDescriptor

  2. 解析泛型类型:dependencyDescriptor.getResolvableType() 获取泛型元数据,调用 getGeneric().resolve() 得到真实泛型参数(Teacher);

  3. 筛选候选 Bean:遍历所有 Dao 类型的 BeanstudentDaoteacherDao),通过ContextAnnotationAutowireCandidateResolver.isAutowireCandidate() 对比泛型:

  • studentDao 的泛型是 Student → 不匹配;

  • teacherDao的泛型是 Teacher → 匹配,注入该 Bean

关键技术:

ResolvableType 通过解析类的字节码(如 TeacherDao implements Dao` 的接口声明),保存泛型元数据,突破泛型擦除限制,实现精准匹配。

3. 特殊对象注入:ApplicationContext为何无需@Component?

testSpecialObjectInjection 演示了 ApplicationContext 的注入,其底层核心是Spring 预注册的 "特殊对象池"

底层原理:
  1. Spring 容器初始化时,会将 ApplicationContextBeanFactoryEnvironment 等核心对象,注册到DefaultListableBeanFactory 的私有字段 resolvableDependencies(类型 Map<Class<?>, Object>);

  2. 依赖解析时,Spring优先从 resolvableDependencies 查找 ,而非普通 Bean 池;

  3. 匹配逻辑:通过 entry.getKey().isAssignableFrom(targetType) 判断类型兼容性(如 ApplicationContext.classConfigurableApplicationContext 的父类,故匹配)。

关键结论:

ApplicationContext 等特殊对象无需 @Component,因为 Spring 在容器启动时已自动将其放入 "特殊对象池",依赖解析时优先获取。

4. @Qualifier:解决同类型 Bean 歧义

当同类型存在多个 Bean(如 Service1Service2),@Qualifier("service2")通过 "标识匹配" 筛选 Bean(对应testQualifierInjection):

底层步骤:
  1. 获取目标字段:InjectionTarget.qualifiedService(带 @Qualifier("service2"));

  2. 解析注解元数据:ContextAnnotationAutowireCandidateResolver 读取字段的 @Qualifier 值(service2);

  3. 筛选候选 Bean:遍历所有 Service 类型的 Bean,对比 Bean@Qualifier 值与字段的 @Qualifier 值,仅 service2 匹配,注入该 Bean

优先级:

@Qualifier 的优先级高于 @Primary(后续会讲),是解决 Bean 歧义的 "最高优先级" 手段。

四、依赖注入优先级:谁先被注入?

DependencyPriorityInjectionDemo 明确了 Spring DI 的优先级规则,这是解决 "同类型多 Bean" 歧义的核心依据。

1. 优先级排序(从高到低)

通过 DemotestPrimaryPrioritytestDefaultNameMatching,可总结出优先级规则:

  1. @Qualifier 标识匹配 :精准匹配 @Qualifier 的值,优先级最高;

  2. @Primary 优先注入 :同类型 Bean 中,标记 @PrimaryBean 优先注入;

  3. 默认名称匹配 :字段名 / 方法参数名与 Bean 名一致(如 service3 字段匹配 service3 Bean);

  4. 类型唯一匹配 :同类型仅存在一个 Bean,直接注入。

2. @Primary的底层实现

@Primary 的核心是通过 BeanDefinitionprimary 属性控制优先级:

  1. Spring 扫描 @Primary 注解时,会将对应 BeanBeanDefinitionprimary 属性设为 true

  2. 依赖解析时,遍历所有同类型 Bean,通过 beanFactory.getMergedBeanDefinition(beanName).isPrimary() 筛选出primary=trueBean(如service2),优先注入;

  3. 注意:同类型 Bean 中不能存在多个 @Primary,否则会抛出 "NoUniqueBeanDefinitionException"。

五、底层逻辑总结:所有场景的 "统一入口"

通过对三个 Demo 的拆解,可发现 Spring DI 的所有场景(基础 / 复杂 / 优先级),最终都收敛到 DefaultListableBeanFactory.doResolveDependency() 这一核心方法,其整体流程可概括为:

六、实用启示:从底层原理到开发实践

  1. 避免过度依赖 @Autowired :字段注入耦合性高,复杂场景建议用构造器注入(Spring 推荐),其底层逻辑与字段注入一致(都是doResolveDependency 解析);

  2. @Lazy 的坑@Lazy注入的是代理对象,若依赖 Bean 的初始化逻辑有副作用(如加载配置),需注意首次调用时机;

  3. 泛型 Bean 的命名 :泛型 Bean(如Dao<Teacher>)建议按 "泛型参数 + 类型" 命名(如teacherDao),便于名称匹配;

  4. 排查 Bean 歧义 :若出现 "NoUniqueBeanDefinitionException",优先用@Qualifier精准匹配,而非 @Primary@Primary 更适合 "默认优先" 场景)。

Spring DI 的底层逻辑看似复杂,但核心是 "围绕 doResolveDependency ,通过各种组件(DependencyDescriptorResolvableType)处理不同场景的依赖需求"。理解这些原理,不仅能解决日常开发中的 DI 问题,更能在面对复杂场景(如自定义 BeanPostProcessor、扩展 DI 逻辑)时,做到 "知其然且知其所以然"。

相关推荐
oradh1 小时前
Oracle XTTS实现跨版本迁移和升级(Oracle 11g单库升级至19C RAC集群)
数据库·oracle·11g升级19c·xtts跨版本迁移和升级
z123456789862 小时前
2026最新两款AI编程工具深度对比实测
java·数据库·ai编程
程序猿DD2 小时前
一个 API Key,统一调用大模型、生图和联网搜索
数据库·网关
TlSfoward3 小时前
抓包代理链路下的 TLS 指纹变化分析 TLSFOWARD抓包工具
数据库·爬虫·网络协议·搜索引擎·php
数智化管理手记3 小时前
账龄分析手工统计易遗漏?自动账龄分析工具怎么搭建
大数据·网络·数据库·人工智能·数据挖掘
LabVIEW开发3 小时前
NI Package Manager安装LabVIEW时InternalServerError错误
网络·数据库·labview·labview知识·labview功能·labview程序
SelectDB3 小时前
PostgreSQL 实时同步到 Apache Doris:一站式 CDC 解决方案
数据库
Database_Cool_4 小时前
AI Agent 长期记忆怎么存?阿里云 PolarDB-X 对接 Mem0 记忆框架实践
数据库·阿里云
会编程的土豆4 小时前
数据库范式
数据库·oracle
L1624764 小时前
MySQL 全环境生产快速安装 + 完整配置手册(整合国产银河麒麟适配 + 主从补充版)
数据库·mysql