ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Spring6 IoC容器底层原理:BeanFactory、BeanDefinition与依赖注入全解析

Spring6 IoC容器底层原理:BeanFactory、BeanDefinition与依赖注入全解析 如果你刚接触Spring问一句“IoC是什么”大概率能听到标准答案控制反转把对象的创建和管理交给容器。但再追问一句“Spring6的容器到底怎么把Bean创建出来、又把依赖塞进去的”很多人就开始含糊了。这篇笔记就来填这个空从Spring6的IoC实现出发把BeanFactory和ApplicationContext的分工、BeanDefinition的注册、单例池、生命周期、依赖注入、循环依赖处理这些底层链路一条条捋清楚最后落到“用IoC容器获取Bean信息”的具体实操上。适合正在啃Spring6源码的人也适合用了几年Spring但始终没把容器机制搞明白的同学。1. 从BeanFactory到ApplicationContextSpring6的两个容器怎么分工1.1 BeanFactoryIoC容器的原始底盘Spring的IoC实现不是一上来就给你一个巨无霸容器而是从最底层的BeanFactory接口开始。这个接口定义的能力非常克制getBean获取实例、containsBean判断是否存在、isSingleton判断作用域、getType获取类型、getAliases获取别名。就这些没了。真正干活的是它的实现类DefaultListableBeanFactory。你可以把它理解成一个“超级仓库管理员”既能注册BeanDefinition又能按需创建Bean还能做依赖解析和单例缓存。很多框架集成场景里不依赖ApplicationContext直接用DefaultListableBeanFactory就够了。我自己刚学的时候干过一件“脱裤子放屁”的事不用Spring的ApplicationContext直接new一个DefaultListableBeanFactory手动注册BeanDefinition再拿Bean。但恰恰是这一步让我看懂了容器本质——所谓IoC容器核心就是一个BeanDefinition的注册表加一个getBean方法。Spring后来加的各种花哨功能都是在这个底盘上长出来的。DefaultListableBeanFactory factory new DefaultListableBeanFactory(); GenericBeanDefinition bd new GenericBeanDefinition(); bd.setBeanClassName(com.example.UserService); factory.registerBeanDefinition(userService, bd); UserService userService factory.getBean(UserService.class);这段代码跑起来容器就已经完成了一次最原始的IoC对象的创建权交给你这里手动new的Factory了而不是业务代码里直接new UserService()。1.2 ApplicationContext在BeanFactory之上加了哪些能力ApplicationContext是绝大多数Java后端项目真正用的容器。它继承了BeanFactory但不是一个平级的扩展而是叠加了企业级能力能力说明日常使用场景国际化继承MessageSource多语言资源文件的加载事件发布ApplicationEventPublisher自定义事件解耦业务逻辑资源加载ResourcePatternResolver读取classpath下的文件环境抽象EnvironmentCapable读取profile、properties配置自动实例化启动时默认实例化单例Bean项目启动就能发现配置错误AOP集成内置AOP织入支持声明式事务等功能的基石常见的实现有ClassPathXmlApplicationContext和AnnotationConfigApplicationContext。前者读XML配置后者读注解和Java配置类Spring6时代基本以注解容器为主了。两者的关键差异在于BeanFactory默认在你调用getBean时才创建对象而ApplicationContext在refresh()阶段就会把大多数单例Bean提前实例化好。这也是为什么SpringBoot应用启动时如果你的Bean构造器里抛了异常启动会直接失败——容器在启动阶段就把对象都new了一遍而不是等你用到才报错。1.3 实际开发中用哪个容器结论很干脆业务项目用ApplicationContext框架或工具类场景才考虑直接操作BeanFactory。ApplicationContext的加载过程其实是一个委派过程它自己不重新实现getBean而是内部持有一个DefaultListableBeanFactory把所有getBean请求转交给它。所以严格说ApplicationContext是个“大管家”BeanFactory才是真正干活的那个。理解这种组合关系后面看源码就不会懵。2. BeanDefinition容器认识Bean的“登记表”2.1 一个Bean在容器里的“档案”都记什么BeanDefinition是Spring IoC里最容易被忽略但最关键的概念。它是容器视角对“一个Bean应该怎么被创建”的完整描述。你写的类只是类型定义比如UserService这个class它描述了有哪些字段、哪些方法。但容器拿到UserService.class之后它还需要知道这个Bean叫什么名字是单例还是原型要不要延迟初始化构造器参数是什么Property值怎么填充有没有init方法、destroy方法这些信息全部装进BeanDefinition里。打个比方你的班级名单上写着“张三、男、学号1”这是Class层面的信息。而BeanDefinition相当于班主任手里的学生档案记录了“张三家庭住址、监护人电话、是否需要校车接送”——容器正是根据这份档案来决定怎么“安排”这个对象的。BeanDefinition接口里比较核心的方法包括getBeanClassName()Bean对应的类名getScope()singleton还是prototypeisLazyInit()是否懒加载getInitMethodName() / getDestroyMethodName()初始化与销毁回调getConstructorArgumentValues()构造器参数getPropertyValues()属性值集合你可能会问“这些配置我在注解或XML里写过了容器怎么知道”答案就是配置被解析成了BeanDefinitionBeanDefinition再被注册进容器。2.2 三类配置来源如何生成BeanDefinitionSpring6里BeanDefinition的来源主要有三个但殊途同归。第一类是XML配置。XmlBeanDefinitionReader解析 标签把class、scope、property、constructor-arg这些属性填充到一个GenericBeanDefinition里然后注册进容器。第二类是注解扫描。ClassPathBeanDefinitionScanner扫描指定包路径下的.class文件凡是带Component及其派生注解Service、Repository、Controller的类都会被包装成ScannedGenericBeanDefinition。第三类是Bean方法。这种属于JavaConfig方式由ConfigurationClassPostProcessor处理Configuration类把每个标注了Bean的方法转换成BeanDefinition方法返回值类型就是Bean的类型。也就是说不管你用XML、注解还是JavaConfig最终在DefaultListableBeanFactory里看到的东西都是同一种形态BeanDefinition。这也是为什么Spring能兼容多种配置风格——它们只是前端语法不同后端都收敛到同一套模型。2.3 BeanDefinition在Spring6阶段值得注意的变化Spring6里BeanDefinition本身没有革命性调整但有两个细节值得提一下。一是别名和名称的生成规则更加规范化。默认bean名称由AnnotationBeanNameGenerator生成规则很简单类名首字母小写。UserService变成userServiceUserServiceImpl变成userServiceImpl。如果你显式指定了名称比如Service(userServiceImpl)那就以显式名称为准。二是BeanDefinition的“继承”能力。你可以定义parent BeanDefinition子BeanDefinition会继承父级的配置包括scope、initMethod、属性值等。这个机制在处理“多个Bean有公共配置”的场景里很好用但在注解驱动的时代用得比较少了。理解BeanDefinition的最直接收益是排查“为什么启动少了一个Bean”时不要先去翻类上有没有注解应该先看这个类有没有被扫描到、有没有生成BeanDefinition。八成问题都出在扫描路径没覆盖或者过滤器把它排除了。3. 单例池与Bean生命周期Spring6在背后管的那些事3.1 单例池是怎么实现的Spring默认把单例Bean放进一个叫singletonObjects的Map里这个Map就是传说中的“单例池”。定义在DefaultSingletonBeanRegistry里private final MapString, Object singletonObjects new ConcurrentHashMap(256);单例池的意思很清楚同一个容器内同一个Bean名称只会创建一个实例后续getBean都是拿缓存。这个Map用ConcurrentHashMap实现保证了并发环境下的线程安全。创建流程大致是调用getBean时先尝试从单例池获取拿不到就进入createBean流程创建完成后放进单例池并返回。后续再调用getBean直接从池里取不会再执行构造器。Spring的单例和设计模式里的单例不是一回事。设计模式单例靠类自身的static实例控制Spring单例是靠容器控制同一名称下只有一个实例。同一个类你可以注册成两个不同名称的Bean它们就是两个不同实例这跟“全局唯一”不冲突。3.2 Bean生命周期中的关键节点与扩展点一个单例Bean从出生到销毁会经历下面这些关键阶段阶段具体动作常见扩展点实例化通过构造器创建对象构造器选择、InstantiationAwareBeanPostProcessor.postProcessBeforeInstantiation属性填充给Bean的属性赋值包括自动装配AutowiredAnnotationBeanPostProcessor在这里处理Autowired初始化前置调用Aware接口系列方法BeanNameAware、ApplicationContextAware等初始化执行自定义初始化逻辑PostConstruct、InitializingBean.afterPropertiesSet、init-method初始化后置AOP代理通常在这里生成BeanPostProcessor.postProcessAfterInitialization使用业务代码调用无销毁前置执行自定义销毁逻辑PreDestroy、DisposableBean.destroy、destroy-method其中属性填充和初始化后置是扩展最密集的两个阶段。AOP动态代理就是在初始化后置阶段完成的所以代理对象替换的是原始Bean而不是在实例化阶段直接生成代理。这也是为什么在BeanPostProcessor里拿到的对象和最终容器里保存的对象可能是不同的——后者可能是代理。3.3 三级缓存与循环依赖再往深走一层Spring6如何解决循环依赖答案是通过三级缓存。这三级缓存都在DefaultSingletonBeanRegistry里一级缓存singletonObjects存放完全创建好的单例Bean二级缓存earlySingletonObjects存放提前暴露的早期Bean还没完成属性填充和初始化三级缓存singletonFactories存放ObjectFactory用来提前生成Bean的早期引用循环依赖的解决思路是A依赖B、B依赖A时在A的属性填充阶段发现需要B但B还没创建完。此时不再傻等而是把A的早期引用通过三级缓存的ObjectFactory暴露出去让B先拿到A等B创建完成后A再从B那里拿到完整引用。但注意Spring6的默认行为是禁止循环依赖。从Spring Boot 2.6开始官方就把allow-circular-references默认值改成了falseSpring Framework 6同样沿用了这个策略。原因很直接循环依赖本来就是一个设计坏味道依赖容器帮你兜底只会掩盖问题。遇到BeanCurrentlyInCreationException时正确的做法是重构代码——抽出下层依赖、调整初始化顺序或者用Lazy延迟加载临时缓解而不是把开关重新打开去纵容它。4. 依赖注入的三种姿势Setter、构造器、注解的选型逻辑4.1 Setter注入灵活但依赖不完整Setter注入是最古老的注入方式XML时代大量使用。它在Bean实例化之后通过setter方法把依赖塞进去。Service public class UserService { private UserDao userDao; Autowired public void setUserDao(UserDao userDao) { this.userDao userDao; } }这种方式的好处是灵活运行时可以随时更换依赖实现。坏处也明显对象在创建完成后的某个时间窗口内依赖可能是null。如果你在某个方法里忘了判空就会等到调用时才发现空指针。另一个问题是它破坏了不可变性字段没法用final修饰多线程环境下容易出现状态不一致。Spring官方文档对Setter注入的态度是“仅在可选依赖时考虑”我实际项目里基本不主动用它。4.2 构造器注入Spring官方推荐的默认姿势构造器注入的核心思想是依赖通过构造器参数传入对象一旦创建依赖就是完整的。Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } }字段用final修饰保证不可变创建对象时必须提供完整依赖不可能出现“对象建好了但依赖没塞”的半成品状态测试时直接new UserService(mockDao)就行不需要反射去改私有字段。Spring官方文档里写得非常明确“Since you can mix both, Constructor Injection is mandatory and Setter Injection as an optional complement.”翻译过来就是能用构造器注入的就用构造器Setter只作为可选依赖的补充。Spring6默认禁止循环依赖之后构造器注入反而更安全了——因为构造器循环依赖在Spring6里是直接被拦截报错的不会像Setter注入那样靠三级缓存偷偷摸摸绕过去。4.3 Autowired和Resource注解注入的细节差异注解注入是Spring2.5之后的主流方式但很多人没搞清楚Autowired和Resource的区别。对比项AutowiredResource来源Spring框架自带JSR-250规范JDK扩展包默认策略byType类型匹配byName名称匹配类型冲突时抛NoUniqueBeanDefinitionException回退到byType配合限定Qualifier指定名称name属性指定名称找不到时默认报错可配requiredfalse默认报错一个很容易踩的坑接口有多个实现类时Autowired会直接抛NoUniqueBeanDefinitionException。解决办法有三个给其中一个实现加Primary在注入处加Qualifier(具体Bean名)或者直接把字段名写成和Bean名一致Spring也会做byName回退。我个人的建议是Spring项目里统一用Autowired Qualifier不要混用Resource。混用会导致代码风格不一致排查问题的时候还得猜别人用的是哪个策略。4.4 我现在的项目里怎么定规则我们团队现在的约定是四句话必选依赖用构造器注入字段加final可选依赖用Setter注入或者Autowired(required false)多实现分发用Map注入比如注入MapString, UserService所有Bean依赖都画清楚方向严禁出现环形依赖。配合Lombok的RequiredArgsConstructor构造器注入写起来也很简洁Service RequiredArgsConstructor public class UserController { private final UserService userService; }这样写出来代码短、依赖清晰、测试友好同时天然规避了循环依赖。遇到那种“三个类互相new来new去”的烂代码第一件事就是把依赖关系捋直而不是想去改容器配置。5. 注解驱动IoC的实现细节从Component扫描到Autowired装配链路5.1 AnnotationConfigApplicationContext启动时究竟做了什么Spring6里最常用的容器启动方式是AnnotationConfigApplicationContext ctx new AnnotationConfigApplicationContext(AppConfig.class);这句代码内部其实做了三件事调用无参构造器初始化AnnotatedBeanDefinitionReader和ClassPathBeanDefinitionScanner把传入的配置类AppConfig注册为BeanDefinition调用refresh()方法执行容器启动流程。refresh()是AbstractApplicationContext里的核心方法十几个步骤里最值得关注的是这三个invokeBeanFactoryPostProcessors处理配置类、扫描Bean、registerBeanPostProcessors注册后置处理器、finishBeanFactoryInitialization实例化所有非懒加载单例Bean。5.2 Component扫描背后是ClassPathBeanDefinitionScanner当你写了ComponentScan(com.example)之后扫描工作由ClassPathBeanDefinitionScanner完成。它会遍历指定包下的所有.class文件用ASM读取类的元数据判断是否有Component注解。判断规则值得细看不是只认Component三个字而是认所有被Component元注解标注的注解。Service、Repository、Controller、Configuration这些注解本身标了Component所以扫描器统统认识。扫描到的类会被封装成ScannedGenericBeanDefinition然后注册进容器。后置处理器ConfigurationClassPostProcessor紧随其后处理Configuration类里的Bean方法、Import导入的类、PropertySource这些。5.3 Autowired装配的核心AutowiredAnnotationBeanPostProcessor依赖注入真正执行的地方是AutowiredAnnotationBeanPostProcessor。这个后置处理器在Bean属性填充阶段介入扫描Bean里的Autowired、Value、Inject注解然后解析依赖。它干的活分两步第一步按类型找候选。比如UserController里有个Autowired private UserService userService它会把所有UserService类型的Bean找出来。如果只有一个直接选中如果有多个进入第二步。第二步按名称收窄。字段名叫userService容器里正好也有个bean叫userService那就选它。如果还收窄不了再考虑Primary标注的优先。全都解决不了抛NoUniqueBeanDefinitionException。我还遇到过一种情况字段名和Bean名不一致也没加Qualifier启动直接报错。排查了很久最后发现是重构时改了字段名但忘了改Qualifier。这里有个小技巧——只要报错信息里出现了多个候选Bean先打印出Bean名称列表十有八九就是名称匹配不上。5.4 整个链路走一遍一个最简单的装配实例拿最常见的Controller依赖Service、Service依赖Dao的场景把容器启动到装配完成的链路串一遍Configuration ComponentScan(com.example) public class AppConfig { } Repository public class UserDao { public String getUserName() { return zhangsan; } } Service public class UserService { Autowired private UserDao userDao; public String getUserName() { return userDao.getUserName(); } } RestController public class UserController { Autowired private UserService userService; GetMapping(/name) public String getName() { return userService.getUserName(); } }启动流程refresh()触发ConfigurationClassPostProcessor读取AppConfig里的ComponentScanClassPathBeanDefinitionScanner扫描com.example包发现UserDao、UserService、UserController三个类生成三个BeanDefinition实例化阶段按照依赖顺序创建Bean先创建没有依赖的UserDao创建UserService时AutowiredAnnotationBeanPostProcessor发现Autowired的UserDao字段从容器里找到UserDao实例并注入创建UserController时同理注入UserService所有Bean创建完成后容器持有完整对象图Controller方法调用时一路向下拿到结果。你会发现所谓的依赖注入本质就是后置处理器在Bean创建过程中扫描注解、自动帮你去容器里查依赖、再调用反射把依赖写进字段。跟你自己写userController.setUserService(userService)没有本质区别只是换成了容器自动执行。6. 实操用Spring6容器获取Bean信息与常见坑6.1 getBean的三种写法与适用场景容器启动之后第一件事往往就是从容器里拿Bean。getBean常见三种写法// 方式一按名字获取需要强转 UserService userService (UserService) context.getBean(userService); // 方式二按类型获取最常见 UserService userService context.getBean(UserService.class); // 方式三按名字类型获取最推荐 UserService userService context.getBean(userService, UserService.class);方式三最推荐因为它既指定了Bean名称又做了类型校验不会出现强转异常。方式二简洁但遇到同类型多个Bean时会报NoUniqueBeanDefinitionException。方式一最原始不建议在代码里出现强转容易埋雷。6.2 拿到容器里所有Bean信息很多时候你想确认“容器里到底有哪些Bean”可以用ApplicationContext自带的ListableBeanFactory能力// 获取所有Bean名称 String[] beanNames context.getBeanDefinitionNames(); for (String name : beanNames) { System.out.println(name); } // 按类型获取所有Bean实例 MapString, UserService beans context.getBeansOfType(UserService.class); // 获取某个类型的全部名称 String[] names context.getBeanNamesForType(UserService.class);这些方法在调试时非常有用。我在排查“某个Bean没被创建”时第一件事就是打印getBeanDefinitionNames()看它到底有没有被注册。如果注册了但没有实例化那就是生命周期阶段的问题如果压根没注册那就是扫描或者配置的问题。这个判断逻辑能快速把问题缩小到一半。6.3 获取Bean信息时常见的三个异常异常场景解决方案NoSuchBeanDefinitionException按名称或类型找不到Bean检查类是否被扫描、Bean名称是否正确NoUniqueBeanDefinitionException按类型匹配到多个Bean加Primary或Qualifier指定BeanNotOfRequiredTypeException获取类型与实际类型不匹配检查Bean的实际类型与强转类型这三个异常里NoSuchBeanDefinitionException最常见的原因是ComponentScan路径写错或者类上没有加注解。我记得有一次排查了很久最后发现是扫描路径写了com.example.controller但实际类在com.example.service包里少扫描了一个包。另一种隐蔽情况是Bean是懒加载的你调用getBean时才创建但容器启动阶段它并不在“已实例化”列表里。用getBeanDefinitionNames()看到的是所有已注册Bean用getSingletonNames()看到的是已实例化的单例Bean两个列表对不上很正常。6.4 把容器能力用在日常开发里除了常规的getBean还有几个偏实用的容器操作技巧。一是注入一组实现类做策略分发。比如你有多个支付渠道都实现了PayService接口可以直接Autowired private MapString, PayService payServiceMap;Spring会把所有PayService类型的Bean按名称注入进Mapkey是Bean名。这种动态派发比写一堆if-else优雅得多。二是用ObjectProvider做延迟获取。Spring6里当你不确定某个Bean一定存在时可以注入ObjectProviderAutowired private ObjectProviderUserService userServiceProvider; public void doSomething() { UserService userService userServiceProvider.getIfAvailable(() - new UserService()); }这种方式在框架代码、可选组件场景下很实用避免直接注入导致启动失败。三是手动在启动阶段调用容器。比如CommandLineRunner里获取Bean做数据初始化Component public class DataInitializer implements CommandLineRunner { Override public void run(String... args) { UserService userService ApplicationContextHolder.getContext().getBean(UserService.class); userService.initData(); } }不过要提醒一句业务代码里不要到处传ApplicationContext也不要把getBean当饭吃。能依赖注入解决的就用注入手动获取Bean只适合启动初始化、策略分发这种少数场景。到处getBean会让依赖关系变成一团乱麻排查起来极度痛苦。最后说点实在的把Spring6的IoC实现整个链路走完之后你会发现真正核心的东西并不复杂BeanDefinition定义“怎么创建”单例池管理“创建几个”后置处理器解决“创建时怎么扩展”依赖注入搞定“创建时依赖从哪来”。四个问题串起来就是IoC的全貌。我在实际排查Bean相关报错时第一反应永远是定位当前处于哪个阶段是扫描注册没进去还是实例化阶段爆了还是属性填充时依赖匹配不上这一步想清楚报错基本就能缩小到很小的范围了。源码看得再多不如下手调试一次建议你找一个只有十几个Bean的小项目打开debug日志自定义一个BeanPostProcessor打印每个Bean创建前后的状态亲手把生命周期摸一遍比看十遍文档都管用。
返回列表