深入 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 逻辑)时,做到 "知其然且知其所以然"。

相关推荐
API快乐传递者4 小时前
抓取淘宝商品详情商品数据API接口调用说明文档|获取淘宝商品价格主图数据等
数据库
济南java开发,求内推4 小时前
Redis一个服务器部署多个节点
服务器·数据库·redis
花菜会噎住4 小时前
Django视图与路由全解析:从URL到页面,一篇讲透
数据库·django·sqlite·函数
-雷阵雨-4 小时前
MySQL——数据库约束
数据库·mysql
大筒木老辈子4 小时前
MySQL笔记---C/C++访问MySQL数据库
数据库·笔记·mysql
友友马4 小时前
『 数据库 』MySQL复习(表的约束)
数据库·mysql
奥尔特星云大使5 小时前
MySQL分布式架构:MyCat详解
数据库·分布式·mysql·mycat·高可用
SelectDB技术团队5 小时前
Apache Doris 内部数据裁剪与过滤机制的实现原理 | Deep Dive
大数据·数据库·apache·数据库系统·数据裁剪
像风一样!5 小时前
学习MySQL数据库的高级特性(上)
数据库·mysql