Spring核心原理:从IoC容器到Bean生命周期与AOP实现

发布时间:2026/10/15 8:08:45
Spring核心原理:从IoC容器到Bean生命周期与AOP实现
1. 为什么Spring值得弄懂底层一个new出来的困惑先讲个几乎每个用Spring的开发者都经历过的场景刚学会new对象的时候觉得一切都很简单——UserService里需要UserDao那就new UserDao()需要MailService那就new MailService()。代码敲得飞快编译也全绿直到某天想给项目加个配置中心、加个缓存、加个消息队列突然发现到处都在new根本管不过来依赖关系像蜘蛛网一样绕成一团。这正是Spring容器要解决的核心问题——对象太多了依赖太乱了new不过来了。所以Spring核心原理这东西说到底就是解决三件事对象由谁来创建、对象之间的依赖由谁来管理、那些横跨业务的方法日志、事务、权限怎么优雅地织进去。明白了这三点你会突然看懂为什么项目里到处都是Autowired、为什么配置一个Transactional就能自动提交回滚、为什么AOP能悄无声息地增强方法——这些东西不再是规则而是逻辑推演的必然结果。这篇内容适合两类人一类是写过不少Spring代码但没系统梳理过底层机制的开发者另一类是准备面试、需要把会用变成能讲清楚的进阶选手。我不会按官方文档讲流水账尽量按照容器如何一步步把Bean造出来这条主线索往下拆。2. 容器启动的那一刻从配置文件到BeanDefinition你启动一个Spring项目Spring Boot也好传统XML也罢第一件发生的事情往往最容易被忽略——容器读入配置材料把它们转换成一堆Bean的施工图纸也就是BeanDefinition。2.1 BeanDefinition一张迟到图纸如何定义Bean的一生BeanDefinition这个名字在官方文档里出现频率不低但初学者容易理解偏。它不是一个被new出来的对象更不是Bean本身而是描述Bean应该怎么造的元数据。打个比方你去餐馆点菜菜单上的描述是BeanDefinition后厨端上来的那道菜才是Bean。菜单上写着少盐、多辣、加香菜后厨就按这个做BeanDefinition里也是同样的逻辑——记录Bean的类名、作用域单例还是原型、是否懒加载、构造参数值、属性值、初始化方法名、销毁方法名等等。在纯XML时代bean iduserService classxxx.UserService scopesingleton init-methodinit/这种标签就是最直观的BeanDefinition。到了注解驱动时代Spring扫描到Service、Component内部照样会为每个类生成一个ScannedGenericBeanDefinition本质没变只是入口从XML解析变成了类路径扫描。这个阶段需要留意的细节是BeanDefinition的注册只描述怎么造不负责真造。换句话说容器启动初期所有Bean还没有被实例化一切只停留在图纸层面。Spring容器之所以能实现先定义、后实例化的分离也正是因为有这一层抽象。2.2 refresh()整个IoC容器的总指挥如果你想看Spring启动流程的真面目AbstractApplicationContext.refresh()是个绕不开的入口。这个方法名起得很传神——刷新因为Spring容器允许你在运行期重新加载配置。但实际上一个Spring应用正常启动时refresh()就完整执行了一次它做的事可以用一个粗粒度的时间线概括prepareRefresh()准备容器环境设置启动时间、活跃标记初始化一些占位符属性源。obtainFreshBeanFactory()这个动作很关键它会销毁旧容器如果有、创建新的DefaultListableBeanFactory然后把前面说的BeanDefinition全部加载进去。prepareBeanFactory()给工厂装配基础工具比如类加载器、SpEL表达式解析器、ApplicationContextAwareProcessor等让Bean在后续阶段能回调容器相关接口。postProcessBeanFactory()留给子类的扩展点比如Web容器在这里注册一些特殊组件。invokeBeanFactoryPostProcessors()调用所有BeanFactoryPostProcessor。注意BeanFactoryPostProcessor和BeanPostProcessor只差一个单词但作用完全不同——前者在Bean实例化前修改BeanDefinition后者在Bean实例化后修改Bean实例。registerBeanPostProcessors()注册所有BeanPostProcessor为后续的Bean初始化拦截做准备。initMessageSource()/initApplicationEventMulticaster()/onRefresh()初始化国际化资源、事件广播器以及Web容器特定逻辑。registerListeners()把监听器Bean注册到事件广播器上。finishBeanFactoryInitialization()这一步平平无奇但极其重要——它触发所有非懒加载单例Bean的创建也就是我们常说的容器启动时就造好了一大批Bean。finishRefresh()清理缓存、发布容器刷新完成事件。每次看到这个方法全貌我都会感慨Spring的设计几乎全靠把扩展点留在流程的空隙处。你不需要继承整个容器去改东西只需要在某个阶段插入自己的处理器。理解refresh()的各个阶段对排查我的Bean为什么初始化时机不对为什么AOP没生效这类问题帮助极大——因为你可以判断问题出在哪个环节。2.3 BeanFactory和ApplicationContext不是一回事但很多人混着说BeanFactory是接口字面意思就是Bean工厂最核心的实现是DefaultListableBeanFactory它管着BeanDefinition的注册、Bean的创建和获取。ApplicationContext继承自BeanFactory但加了更多能力国际化、事件发布、资源加载、环境抽象。你可以把BeanFactory想成一台只负责造对象的裸机器ApplicationContext是给它加装了外观Environment、安保事件系统、后勤资源加载的完整工厂。我们平时用的ClassPathXmlApplicationContext、AnnotationConfigApplicationContext本质上都是ApplicationContext的实现类而它们在内部持有的核心容器仍然是DefaultListableBeanFactory。有一个面试常问的细节ApplicationContext相对BeanFactory多了哪些能力答案就在上面的对比里。如果只说IoC容器BeanFactory已经够了但Spring生态中的所有高级功能几乎都建立在ApplicationContext的扩展能力之上。3. Bean的一生从实例化到销毁的完整链路Bean的生命周期是Spring源码里最值得逐行跟读的部分也最容易在面试中讲得七零八落。这里我会按照实际代码执行的顺序把重要节点捋一遍。记住一个原则Spring尽量不直接操作对象的业务逻辑而是把控制点暴露成各种接口和注解。3.1 实例化只是造了个壳还没填充依赖很多人以为实例化和初始化是一回事其实不是。createBeanInstance()根据BeanDefinition里的信息通过构造器反射或工厂方法生成一个原始对象——此时这个对象的属性全是null依赖也全是null。这里有个常见坑如果你只有一个带参构造器且没有无参构造器Spring会尝试用带参构造器创建Bean。如果参数解析不到就会在启动阶段直接报错。所以写类时保留一个无参构造器或者在构造器上加Autowired本质上都是在帮Spring猜参数成功。3.2 属性填充与依赖注入Autowired到底在做什么对象创建出来后Spring执行populateBean()这一步会处理Autowired、Resource、Value注解。它的核心逻辑是遍历当前Bean的所有字段和方法找到标记了注入注解的地方检查类型、匹配候选Bean、完成赋值。Autowired默认按类型注入。if有多个同类型候选Spring会再按Qualifier指定的名字缩小范围或者按Primary选默认者。如果按类型和名字都找不到且required true容器直接抛异常——这解释了为什么启动时报错说找不到某个Bean往往在属性填充阶段。3.3 Aware回调与BeanPostProcessor给Bean插上感知容器的手脚属性填充完成后Spring会调用一系列Aware接口的回调方法。比如BeanNameAware.setBeanName()让Bean知道自己叫什么。BeanFactoryAware.setBeanFactory()让Bean能直接操作外围工厂。ApplicationContextAware.setApplicationContext()让Bean拿到整个容器上下文。这些接口看起来古朴但它们是早期Spring时代的标准扩展点。现在很多人直接用Autowired ApplicationContext效果一样只是方式从接口回调变成了字段注入。紧接着就是BeanPostProcessor的postProcessBeforeInitialization()——注意这个before发生在初始化方法之前但已经在属性填充之后。所有PostConstruct注解标记的方法本质就是由CommonAnnotationBeanPostProcessor在这个阶段调用的。这里需要说清楚一个顺序问题它是面试高频题实例化属性填充依赖注入BeanNameAware、BeanFactoryAware等Aware回调BeanPostProcessor.postProcessBeforeInitialization这里包含PostConstructInitializingBean.afterPropertiesSet()InitializingBean接口的回调自定义init-methodXML或Bean(initMethod xxx)BeanPostProcessor.postProcessAfterInitializationAOP代理通常在这一步生成如果按照注解先还是接口先再细分Spring的实现里PostConstruct在postProcessBeforeInitialization阶段就被处理了而afterPropertiesSet()是初始化阶段的动作所以PostConstruct执行时机更早。很多人只知道前中后三段却不清楚PostConstruct藏在哪一步写测试代码时容易踩到执行顺序不符合预期的问题。3.4 初始化完成之后代理的诞生地postProcessAfterInitialization这个阶段是所有AOP代理的出生地。AnnotationAwareAspectJAutoProxyCreator这个BeanPostProcessor会在这里检查当前Bean是否有匹配的切面Advisor如果匹配就生成一个代理对象替换掉原始对象。这也是为什么AOP生效的Bean在容器里存的其实是代理对象——你在ApplicationContext.getBean()拿到的很可能已经是被增强过的替身。后续其他Bean再注入这个Bean时注入的也是代理对象。理解这一步你就会明白为什么getBean()返回的对象类型在强转时偶尔出现ClassCastException——因为动态代理生成的类往往不是你原始类的直接实例。3.5 使用期与销毁单例缓存和销毁回调Bean创建完成后如果是singleton作用域会被放进一级缓存singletonObjects后续getBean()直接从缓存拿不再重复创建。prototype作用域则每次getBean()都新建Spring也不负责缓存它。容器关闭时Spring会调用所有Bean的销毁逻辑顺序大致是PreDestroy→DisposableBean.destroy()→ 自定义destroy-method。如果你用Bean(destroyMethod close)配置了一个连接池Bean容器关闭时会自动调用close()释放资源——这个设计省了不少心。4. 依赖注入的底层逻辑与循环依赖的破解思路依赖注入看起来就是给字段赋值但它牵涉到一个杀手级话题循环依赖。A依赖BB又依赖A如果容器是个完美主义者要求创建完一个Bean再把所有依赖塞进去那A等BB等A永远等不到头。Spring解决单例Bean循环依赖靠的是三级缓存。这里值得把机制掰开揉碎讲清楚。4.1 三级缓存各自的作用DefaultSingletonBeanRegistry里有三个Map名字很直接singletonObjects一级缓存存完全创建好的成品Bean。earlySingletonObjects二级缓存存半成品——已经实例化但还没完成属性填充的Bean。singletonFactories三级缓存存一个ObjectFactory它的getObject()方法能提前曝光一个Bean的早期引用。循环依赖的破解链条是这样走的创建A实例化出原始对象A0。把能返回A0的工厂放进三级缓存singletonFactories。填充A的属性发现需要B于是开始创建B。创建B实例化出原始对象B0把工厂放进三级缓存。填充B的属性发现需要A于是getSingleton(a)开始查缓存。一级缓存没有二级缓存没有三级缓存有——调用singletonFactories里的工厂得到A0放到二级缓存返回给B完成注入。B创建完成放进一级缓存。回到A的创建流程A完成属性填充完成初始化也放进一级缓存。这套机制能成立的前提是循环依赖发生时两个Bean都已经完成了实例化只是属性还没填完。所以三级缓存里存的是实例化后的早期引用而不是一个完整的成品。4.2 为什么必须三级缓存二级行不行这个问题面试官特别爱问因为它能考察你到底有没有看懂源码。常见答案是因为需要AOP代理但这个回答不够完整。假设有一个Bean有切面在早期曝光时必须生成代理对象否则B注入的就是原始类而不是代理类——那AOP就白配了。三级缓存里的ObjectFactory在调用时有机会执行getEarlyBeanReference()方法这个方法会给提前曝光的Bean套上一层代理。放在二级缓存里由于二级缓存没有延迟生成的特性只能存储固定对象无法做到要的时候再决定是否要代理。实际上如果所有Bean都不需要AOP二级缓存也够用。但现实项目里事务、日志、权限都靠AOP实现所以三级缓存的延迟工厂成了必要设计。Spring用三级缓存本质上是把一个最晚决定的钩子放在了跨Bean依赖发生的节点上。4.3 哪些循环依赖救不回来清楚一点构造器注入的循环依赖Spring救不了。原因很简单——构造器注入要求A在实例化前就必须拿到B此时A连实例都没有自然不存在早期曝光一说三级缓存里也没有A的任何痕迹死循环直接抛出BeanCurrentlyInCreationException。字段注入和setter注入可以破解循环依赖是因为实例化和属性填充拆成了两步实例化完成后就能提前曝光自己。所以实战中如果要避免循环依赖带来的隐患最好的办法是重新梳理组件边界而不是纠结注入方式。循环依赖本身就是一种设计信号——两个类彼此纠缠往往说明职责没拆干净。5. AOP的实现原理从动态代理到事务注解的完整闭环AOP面向切面编程是Spring最容易被用得很熟但讲不明白的部分。你需要知道三件事代理怎么来、通知怎么匹配、事务为什么能自动回滚。5.1 两种动态代理的选择逻辑Spring AOP底层有两种代理生成方式JDK动态代理基于接口通过java.lang.reflect.Proxy在运行时生成一个实现了目标接口的代理类。要求目标必须有接口。CGLIB代理基于父类通过生成目标类的子类来覆盖方法。目标类不需要接口但不能是final修饰方法也不能是final。Spring的默认策略是目标类实现了接口优先用JDK动态代理没有接口则用CGLIB。这在早期版本是一个经常让人困惑的差异——你的Service如果只有实现类没有接口代理方式就变了。到了Spring Boot 2.x之后配置项spring.aop.proxy-target-classtrue默认开启意味着一律优先使用CGLIB。这样带来的好处是代理行为一致不依赖接口是否定义坏处是CGLIB生成的类不能被final修饰有些框架限制就得注意。5.2 通知与切点的匹配过程写一个Aspect类里面定义Before、After、Around等方法Spring拿到后会把它们解析成Advisor切面通知器。每个Advisor里有一段切点表达式和一段通知逻辑。代理创建时Spring遍历所有Advisor用切点表达式去匹配当前Bean的所有方法。匹配上的方法在代理对象的方法调用链里会被套上对应的通知逻辑。匹配不上的方法就直接调用原始方法不走任何增强。所以为什么我这个方法切面没生效的排查思路很清晰先确认代理有没有创建成功再确认切点表达式是否命中方法再确认通知类型是否写对。这三个环节任何一个出错表现都是静默不增强很考验debug的耐心。5.3 Transactional的真实工作方式事务注解是AOP最经典的应用。当你在方法上加TransactionalSpring容器里对这个Bean生成的代理对象在调用这个方法时会额外做这些事检查当前是否存在事务。如果不存在开启一个新事务把数据库连接绑定到当前线程。执行目标方法。如果没有异常抛出提交事务。如果抛出异常且异常类型在回滚规则内回滚事务。默认的回滚规则是只回滚RuntimeException和Error受检异常checked exception默认不回滚。这坑过不少人——业务代码里抛了一个业务异常通常是受检异常事务竟然提交了。解决方式是在Transactional(rollbackFor Exception.class)里面显式指定。还有一个高频坑是事务自调用失效同一个类里methodA调用了带Transactional的methodB事务不生效。原因是代理对象在方法调用链上methodA内部直接this.methodB()走的是原始对象根本没经过代理。解决办法多半是拆分到另一个Bean里调用或者自己注入代理对象。6. 实际排查经验与几个常见误区前面几章把原理串了一遍最后分享一些我在项目里真实遇到过的问题。这些问题不大但排查起来非常费时间你能提前知道原因至少能省半天光景。6.1 不是所有加了Component的类都会被代理如果你跑了一个很简单的启动类里面定义一个Component类类里没有任何切面匹配那容器里存的就是原始对象不是代理。只有存在匹配的AdvisorpostProcessAfterInitialization才会返回代理对象。这个道理听起来显而易见但很多人调试时习惯性地用getClass()去看对象类型看到的不再是原始类就开始怀疑代理出了问题——其实只有被增强过的Bean才需要代理不被增强时保持原样反而是合理设计。6.2 BeanPostProcessor的注册顺序影响你的自定义逻辑生效时机所有BeanPostProcessor是按照注册顺序逐个执行的。如果你自己实现了一个BeanPostProcessor希望它在Spring内置的AutowiredAnnotationBeanPostProcessor之前生效就得通过Order或者Ordered接口控制优先级。有时候你看到字段注入的值不对某个自定义注解没处理排查半天往往不是逻辑问题而是顺序问题——自定义处理器的执行时机比Spring的属性填充时间点晚此时注都注完了当然轮不到你改。6.3 构造器注入和字段注入在启动速度上的差异构造器注入能在Bean实例化阶段就确定依赖关系一旦失败立即暴露字段注入则把依赖确定延后到属性填充阶段。对于强依赖组件推荐构造器注入因为Spring官方也是这么建议的而且写单元测试时直接new出来传参就行不用靠反射去改私有字段。如果一个类构造器参数特别多别急着吐槽构造器注入太啰嗦——那是类职责过多的信号该拆类而不是换注入方式。6.4 懒加载并不是随便用的优化Lazy能让Bean在第一次被使用时才创建。听起来能加快启动速度但代价是依赖它的Bean在注入时拿到的是一个代理对象真正的对象要等第一次方法调用才触发创建。这个延迟触发在某些冷启动场景里会让第一个请求变慢甚至出现意想不到的初始化顺序问题。所以我的建议是不是明确有性能痛点的场景别全局开懒加载否则排查问题时又多一层隐晦的代理干扰。7. 如果要深入源码从哪几个类看起最后如果你和我一样属于不读源码不放心的类型我建议按这个顺序去读性价比最高DefaultSingletonBeanRegistry看三级缓存、getSingleton()的完整逻辑这是理解循环依赖的关键。AbstractAutowireCapableBeanFactory看createBean()、doCreateBean()、populateBean()、initializeBean()这是Bean生命周期的主干。refresh()里的十个步骤AbstractApplicationContext建立对整个启动流程的空间感。AnnotationAwareAspectJAutoProxyCreator看AOP代理怎么筛选、怎么生成。TransactionalInterceptor看事务的提交回滚逻辑。不建议从太底层的工具类开始看容易迷失方向。按主干流程→关键节点→具体实现的顺序读效率高很多。我在实际项目中体会到的一个经验是原理不是背出来的是遇到问题—翻源码—恍然大悟—下次少踩坑这样循环出来的。比如第一次遇到循环依赖报错时你只知道setter注入能解决等你翻完三级缓存源码就会明白为什么会报BeanCurrentlyInCreationException以后再遇到时甚至能直接判断是不是构造器注入导致的死循环。Spring这套体系设计得很精巧——它用BeanDefinition解耦配置与实例化用三级缓存解决单例循环依赖用BeanPostProcessor在对象创建流程中留出无数扩展点用动态代理把横切逻辑织进业务方法。把这些机制装进脑子里再看任何Spring Boot的自动配置、任何中间件的Spring集成你会发现大家玩的都是同一套“钩子游戏”。