资讯详情

Spring原理实战:从IOC容器到AOP与事务的完整解析

📅 2026/10/12 7:55:06 | 华诺云谱 👁 阅读
Spring原理实战:从IOC容器到AOP与事务的完整解析
聊到 Spring 原理很多人的第一反应是“八股文”——面试背一背 IOC、AOP、Bean 生命周期就完了。但真正在团队里排查过问题的人都知道不懂原理的代价特别贵循环依赖报错看不懂、事务悄悄失效找不到原因、想给老项目加一个扩展点却无从下手。我写过几年 Spring 相关代码也翻过源码今天这篇就把 Spring 里最核心的运行机制拆开讲一遍结合实际踩过的坑和排查经验尽量让不熟悉源码的人也能建立起一幅完整的地图。这篇文章适合这样的人群刚接触 Spring 的初学者想知道容器到底做了什么工作两三年但停留在“会用注解”阶段的开发以及准备做一些二次封装、公共组件设计的同学。我会从 IOC 容器如何管理对象讲起延伸到 Bean 的完整生命周期、循环依赖的三级缓存、AOP 与事务的原理再补充 Spring Boot 自动配置和整个 Spring 生态的演进逻辑最后给出一份可操作的源码阅读和问题排查路线。1. 先搞清楚Spring 真正解决的三个核心问题1.1 对象从哪来从 new 对象到 IOC 容器在没有 Spring 的年代Java 项目的对象管理是极其原始且痛苦的。比如一个订单服务要调用库存服务你需要在订单服务里new StockService()而库存服务又依赖数据库连接于是你还要在构造方法里传入数据源。一旦依赖层级加深构造对象的代码会像蜘蛛网一样散落在项目各处改一个依赖就要全局搜索所有new的地方。这不是代码风格问题是架构设计上的失控。Spring 做的事情本质上就是把“谁负责创建对象、谁负责维护对象之间的依赖关系”这个职责从业务代码里剥离出来统一交给一个容器。这个容器就是 ApplicationContext它像一个超级工厂根据你提供的配置或者注解扫描到所有需要管理的类创建出实例然后按照依赖关系自动注入。业务代码不再关心对象怎么来的只关心“我要用什么”容器会在合适的时间把合适的对象递到你手上。这里的关键转变是控制权的反转对象创建和组装的控制权从开发者手中交到了框架容器手里。你告诉容器“我需要一个 UserService”容器就去管理好的对象池里找到 UserService把它依赖的 UserMapper、DataSource、事务管理器等全部组装好再给你一个完整可用的实例。这种思想就是控制反转Inversion of Control简称 IOC。1.2 依赖如何组装DI 的价值与取舍IOC 是思想依赖注入Dependency Injection简称 DI是落地手段。Spring 提供了三种注入方式构造器注入、Setter 注入、字段注入Autowired。很多人刚学时觉得字段注入最方便一行注解就完事。但深入源码和长期维护的角度看构造器注入更推荐原因在于它能让依赖关系在对象创建时就固化下来不会出现对象已经 new 出来但依赖还没设置的情况也方便写单元测试时直接传入 mock 对象。我在维护一个老项目时做过一次重构那个项目大量使用字段注入结果想给某个服务类加单元测试发现new出来的对象总是报空指针——因为字段注入在脱离 Spring 容器时不会自动执行。后来把核心类改成构造器注入测试瞬间清爽很多。构造器注入的优势在编写公共组件和库时尤其明显它强制调用方把依赖暴露在外部而不是藏进类内部。需要特别注意的是DI 并不是越多越好。如果一个类依赖了十几个对象说明它自身职责过重已经不是“注入”能解决的问题而是需要拆分类。我在评审代码时经常看到一些 Service 类动辄注入七八个 Mapper 和 Service这种类虽然能运行但耦合程度高、测试困难后续维护成本极大。Spring 提供了容器能力开发者更应该用它来维护清晰的职责边界而不是用它掩盖设计问题。1.3 通用逻辑怎么抽AOP 的横切思维业务系统里有大量与主流程无关、但又不得不写的重复逻辑请求日志、权限校验、性能监控、事务管理。如果把这些逻辑硬编码进每个方法代码会变得又长又乱而且一旦需求变化比如日志格式从 JSON 改成文本你就得把所有方法改一遍。Spring 的面向切面编程Aspect Oriented Programming简称 AOP就是专门解决这类横切关注点的。AOP 的核心思路是在目标方法执行前后由框架织入额外的逻辑。开发者只需要定义切面、切点、通知Spring 会在运行时生成一个代理对象由代理对象来包装目标方法。业务代码本身无感知但每次调用都会被代理拦截在合适的时机执行切面逻辑。这样一来日志、权限、事务这些公共关注点就和业务逻辑解耦了。我经常用生活中的类比来解释 AOP你把一个重要包裹放进快递柜快递员会在包裹外面套一层气泡膜、贴上运单、录入系统这些操作和包裹里的物品本身没有关系是快递流程附加的。AOP 的逻辑注入也是这样目标方法就像包裹里的物品切面逻辑就像快递公司附加的包装和服务。理解了这个思维后面看代理模式、切点表达式都会顺畅很多。2. Bean 生命周期容器里最重要的那条流水线2.1 从 BeanDefinition 到实例化的完整链条Spring 管理对象的基本单位是 Bean但在真正创建对象之前Spring 先要做的是读取配置、解析类、生成 BeanDefinition。BeanDefinition 可以理解为对象的“档案”里面记录了类的全限定名、作用域singleton 还是 prototype、初始化方法、销毁方法、属性配置、是否懒加载等信息。配置解析完成后所有 BeanDefinition 都被注册进 BeanFactory 的一个 Map 中等待后续实例化。实例化阶段并不像我们想象的只是new一个对象。Spring 会先通过反射或工厂方法创建原始实例然后进行属性填充把配置里或者在容器中能找到的依赖一一注入进来。填充完成后Spring 会调用各种 Aware 回调比如 BeanNameAware、BeanFactoryAware、ApplicationContextAware让你有机会拿到容器中的上下文信息。紧接着是 BeanPostProcessor 的postProcessBeforeInitialization方法这里可以在初始化逻辑之前做自定义处理。然后执行自定义的init-method或者PostConstruct注解标注的初始化方法。初始化完成后还会经过postProcessAfterInitialization此时拿到的对象已经是完整可用的 Bean如果存在代理需求也往往是这个阶段生成代理对象。整个流程走完Bean 才被放入一级缓存供后续获取。2.2 三级缓存循环依赖是这样被绕过去的循环依赖指的是 A 依赖 BB 又依赖 A这种情况下如果直接创建 A 就会陷入死循环创建 A 需要 B创建 B 需要 A一直互相等待。Spring 为了解决单例 Bean 的无参构造循环依赖设计了三层缓存一级缓存singletonObjects存放已经完全初始化好的成品 Bean。二级缓存earlySingletonObjects存放已经实例化但尚未完成属性填充的早期 Bean 引用。三级缓存singletonFactories存放能生成早期 Bean 引用的工厂对象。大致流程如下创建 A 时先实例化 A得到早期引用把 A 的工厂放入三级缓存填充 A 时需要 B于是去创建 BB 实例化后填充属性时需要 A此时从三级缓存拿到 A 的工厂调用工厂得到 A 的早期引用放入二级缓存同时从三级缓存删除B 拿到 A 引用后完成创建注册 destroy 方法放到一级缓存A 继续执行剩余属性填充和初始化流程最终也放入一级缓存。这里有一个特别容易踩的坑三级缓存只能解决单例 Bean 且默认构造器或没有显式构造参数依赖的循环依赖。多例 Beanprototype 作用域遇到循环依赖会直接抛异常因为容器管理它们的生命周期比较短不做缓存。构造器注入的循环依赖无法通过三级缓存解决因为实例化阶段就需要完整依赖早期引用还没有生成。如果你在开发中遇到BeanCurrentlyInCreationException第一反应就应该是检查有没有复杂的构造器循环依赖而不是盲目加 Lazy。2.3 被忽略的扩展点BeanPostProcessor 与 Aware 接口很多人学 Spring 只盯着注解用很少会主动使用扩展点。但如果你做过中间件、组件封装或者在公司里维护公共的日志、缓存、权限模块BeanPostProcessor 这类扩展点几乎是绕不开的。Spring 框架的一大设计哲学就是“留好口子给你改”而不是把一切做死。BeanPostProcessor 接口里有两个方法postProcessBeforeInitialization和postProcessAfterInitialization分别会在 Bean 初始化前后被调用你可以拿到目标 Bean 和它的 BeanName在此处进行包装、代理、属性补充等操作。Spring 自己的很多特性AOP 自动代理、Autowired 注入、Value 解析本质上都是借助各种 BeanPostProcessor 实现的。比如AutowiredAnnotationBeanPostProcessor负责解析 Autowired 注解并完成依赖注入AbstractAutoProxyCreator负责生成 AOP 代理。需要提醒的是BeanPostProcessor 本身也是 BeanSpring 会提前创建它们。如果你自定义的 BeanPostProcessor 内部又依赖了其他普通 Bean要特别注意初始化顺序问题。我遇到过一个新的公共组件引入后某个自定义 BeanPostProcessor 里注入了一个 Service结果这个 Service 的初始化又依赖了别的 Bean导致启动时出现循环依赖。排查了半天最后把自定义处理逻辑改成在postProcessAfterInitialization里延迟获取依赖才解决。这类问题在团队协作规模变大时特别常见设计扩展点时务必把依赖关系理清楚。3. 深入 AOP 与事务实际开发里最常踩的雷区3.1 动态代理的两种实现怎么选Spring AOP 实现代理有两种方式JDK 动态代理和 CGLIB 代理。JDK 动态代理要求目标类必须实现接口它通过接口生成代理对象CGLIB 代理通过继承目标类生成子类来代理不要求实现接口。Spring 的默认策略是如果目标类实现了接口就使用 JDK 动态代理如果没有实现接口则使用 CGLIB。在 Spring Boot 2.x 之后的版本中默认设置为spring.aop.proxy-target-classtrue即优先使用 CGLIB。CGLIB 功能更强能代理没有接口的类但它是通过生成子类实现的所以 final 类和方法无法被代理。JDK 动态代理生成代理对象的性能未必比 CGLIB 高但在 JDK 8 之后JDK 代理迭代了很多次很多场景下已可接受。实际开发中经常忽略的一个问题是同一个类内部的方法自调用并不会触发代理。比如在 Service 类的saveOrder方法里直接调用this.sendMessage()而sendMessage上有事务或日志切面这种情况下代理对象不会被调用切面逻辑不会生效。解决思路有三个拆分为两个 Bean、注入代理对象自调用、或者使用AopContext.currentProxy()强制从代理对象发起调用。我在代码评审时发现过很多次类似自调用导致的事务不生效问题排查起来特别隐蔽因为日志里没有任何异常只是行为不符合预期。3.2 Transactional 为什么会失效原理层面的解释事务是 AOP 最典型的应用场景之一。Spring 通过事务代理把事务开启、提交、回滚的逻辑织入到带有 Transactional 注解的方法周围。它不是魔法而是基于代理机制实现的横切逻辑。理解了这一层很多“事务不生效”的问题都可以直接推断出原因。最常见的失效场景包括访问权限不是 public、方法在类内部自调用、异常被 catch 吞掉、异常类型非运行时异常。其中“异常被吞掉”尤其危险。默认情况下 Spring 只对 RuntimeException 和 Error 回滚如果业务方法里 catch 了异常但不重新抛出事务感知不到异常自然无法回滚。如果确实要 catch 后做补偿需要在 catch 块里手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。还有一类问题与代理方式有关如果事务方法所在的类没有实现接口Spring Boot 默认使用 CGLIB如果类被 final 修饰或者使用了 JDK 代理模式但类没有实现接口则 Bean 无法被代理Transactional 自然不生效。另外要注意自调用问题事务方法被同类的非事务方法调用不会经过代理事务不生效。用 Transactional 时最好把事务边界放在入口方法不要让事务散落在内部私有方法里。3.3 传播行为与事务边界设计事务传播行为定义了多个事务方法相互调用时如何协同。默认的 REQUIRED 行为是如果当前存在事务则加入如果不存在则新建一个。大多数业务场景用默认值就够了但碰到“主方法报错后日志子操作必须保留”之类的需求就需要引入 REQUIRES_NEW让子方法开启独立事务独立提交不受外部事务回滚影响。传播行为的理解不能只停留在背表格要结合业务场景来设计。比如一个导入任务主流程是解析文件并批量保存数据子任务是记录每条数据的处理日志。解析和主保存出错了可以全部回滚但日志必须每一条都写进数据库方便后续排查。这种场景就可以用 REQUIRES_NEW 把日志记录单独托管。如果日志方法也参与了主事务主事务一旦回滚日志就会全部消失排查起来等于没有日志。在配置传播行为时要特别小心嵌套调用带来的性能开销和死锁风险。REQUIRES_NEW 会挂起当前事务并创建新的数据库连接如果调用链特别长连接数会上升资源消耗变大同时新事务持有的锁等待旧事务释放资源时如果事务之间互相涉及对方持有的锁可能产生死锁。我建议事务边界的粒度尽量按“一个业务操作对应一个事务”来设计不要为了写法好看而在事务方法内部再去嵌套事务方法。4. Spring Boot 的自动配置原理约定大于配置到底是怎么做到的4.1 EnableAutoConfiguration 与 spring.factoriesSpring Boot 最大的贡献是让 Spring 上手成本大幅降低以前你用 Spring MVC 需要写 web.xml、配数据源、配事务管理器现在引入一个 starter 依赖应用启动后它自动就把该配的组件配好了。这个“自动配置”能力核心就是EnableAutoConfiguration以及spring.factories/AutoConfiguration.imports文件的加载机制。当 Spring Boot 启动时EnableAutoConfiguration会触发加载META-INF/spring.factories老版本或META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports新版本中注册的自动配置类。这些配置类本质上是一堆带Configuration、ConditionalOnClass、ConditionalOnMissingBean的类它们根据当前项目的类路径、属性配置、Bean 定义来判断“该装配的组件是否装配”。比如你引入了spring-boot-starter-web类路径上出现了DispatcherServlet和相关类自动配置类就会创建DispatcherServlet、HandlerMapping、ViewResolver等一系列 Web 组件。如果你自己在容器里已经定义了一个DataSource自动配置会通过ConditionalOnMissingBean检测到不再创建默认的DataSource避免冲突。理解这个机制对排查“为什么我加了一个组件但配置不生效”非常有帮助。4.2 条件装配Conditional 系列注解到底怎么判断自动配置的灵魂是条件装配核心是各种Conditional注解。ConditionalOnClass判断类路径上是否存在指定的类ConditionalOnProperty判断配置项是否满足条件ConditionalOnMissingBean判断容器中是否没有某个 BeanConditionalOnBean则相反。这些注解让配置类可以“感知环境”环境不同行为的装配结果也不同。实际开发中我们也可以在自己的项目里用这些条件注解来编写具有灵活性的配置类。比如封装一个公共的 Redis 组件希望在某个服务中通过配置文件决定是否启用或者写一个多环境适配的数据源配置开发环境用 H2、生产环境用 MySQL这些都可以用条件装配实现。Spring Boot 的自动配置就是这么干的它把“你告诉我要什么”变成了“我看看你现在有什么再决定给你装什么”。需要注意条件注解的判断时机是注册配置类时不是运行时。如果你在代码里动态修改了类路径或者运行期才决定是否加载某些类条件注解是来不及反映的。另外多个自动配置类之间的加载顺序由AutoConfigureOrder和AutoConfigureBefore/AutoConfigureAfter控制如果你在自定义自动配置时把控不好顺序很可能会出现 Bean 重复定义或依赖缺失的问题。我在做一个公共 starter 时就踩过这种坑最后通过给自动配置类显式定义顺序才解决。4.3 从 Spring 到 Spring Boot 再到 Spring Cloud、Spring AI生态演进路线Spring 自身不是万能的它提供了容器、事务、MVC、数据访问等基础能力但大型分布式系统的服务注册发现、配置中心、统一网关、负载均衡、链路追踪等场景需要另外一套解决方案于是 Spring Cloud 系列出现了。很多人把 Spring Cloud 和 Spring Boot 混为一谈其实它们不是替代关系——Spring Cloud 是一系列分布式工具集的整合它依赖 Spring Boot 作为基础框架。Spring Cloud 各个子项目利用 Spring Boot 的自动配置把注册中心、HTTP 调用、熔断、配置管理封装成开箱即用的组件。在微服务实践中一个典型的组合是 Spring Boot Spring Cloud Alibaba其中 Nacos 做服务注册与配置中心OpenFeign 做服务间声明式 HTTP 调用Sentinel 做流量控制和熔断降级Seata 处理分布式事务。这里面的服务注册与发现、配置动态刷新能力底层都离不开 Spring 容器的扩展点。比如动态刷新配置的原理就是利用了 Spring 对RefreshScopeBean 的重新创建机制。更近一点的演进是 Spring AI 的出现。Spring 官方开始拥抱大模型生态提供了对 OpenAI、智谱、通义千问等模型的统一抽象把 RAG、Agent、结构化输出等能力嵌入到 Spring 的开发模型里。也就是说你写 AI 应用时依然可以沿用熟悉的 Spring Boot 工程结构和自动装配思想。Spring AI 的理念跟 Spring Boot 一致的约定大于配置用 starter 统一封装外部模型的调用差异。这背后依然是依赖注入、自动配置、扩展点这些基础原理所以把 Spring 原理学扎实接触任何上层框架都能更快上手。5. 常见问题排查与源码阅读技巧5.1 实际排查中最多见的三类问题每隔一段时间就会碰到同事一脸疑惑地拿着错误日志来找我说 Bean 注入失败或者事务不生效。问题样本多了以后我发现高频问题可以归纳成三类。第一类是NoSuchBeanDefinitionException含义是容器里找不到对应的 Bean。原因通常是类没有被 Spring 扫描到包路径不对、没有加Component系列注解、或者该 Bean 是接口而容器的实现类不是默认命名解决时优先检查类路径和扫描配置。第二类是高并发环境下偶发的循环依赖问题。老项目里用了三级缓存解决循环依赖后正常启动没问题但偶尔在高并发或延迟初始化下出现BeanCurrentlyInCreationException。这种问题往往不只是简单的 A 依赖 B而是多层循环依赖。排查时要顺藤摸瓜把项目初始化的 Bean 打印出来看依赖链路必要时通过Lazy或者调整初始化顺序缓解。第三类是BeanNotOfRequiredTypeException报错说容器中的 Bean 类型与期望不符。这通常跟代理有关类实现了接口但被 CGLIB 代理后类型变成了子类如果你直接按类类型获取 Bean会拿到代理子类而不是原始类类型不匹配。用接口类型获取或者确定项目是否统一设置了 proxy-target-class可以快速排除。5.2 阅读 Spring 源码的入手顺序阅读 Spring 源码很多人一开始就乱了打开 AbstractApplicationContext 的 refresh 方法一看全是抽象调用瞬间放弃。我给的建议是按照主路径来读先把启动流程的骨架拉通再往里填充细节。入门可以从ClassPathXmlApplicationContext或AnnotationConfigApplicationContext的 refresh 方法开始主线就是BeanFactory 创建准备 - BeanDefinition 解析注册 - 实例化及初始化所有单例 Bean。第二步可以聚焦finishBeanFactoryInitialization和preInstantiateSingletons这里会看到所有单例 Bean 的实例化入口。第三步是理解依赖注入的核心doCreateBean实例化、属性填充、初始化、注册销毁以及三级缓存的处理逻辑。第三块再单独看 AOP 的入口AbstractAutoProxyCreator和事务的TransactionInterceptor。读源码的时候建议带着问题去读而不是从头到尾逐行看。比如“Autowired 是怎么完成注入的”直接定位到AutowiredAnnotationBeanPostProcessor比如“事务是怎么开启和回滚的”看TransactionInterceptor比如“自动配置类是怎么加载的”看AutoConfigurationImportSelector。带着问题定位代码效率远高于通读全书。5.3 手写一个迷你 Spring 框架的启发我在带新人时经常建议一个练习不要直接啃完整源码而是花两三天手写一个仅支持最基本功能的迷你 Spring。不用考虑复杂场景只要实现注解扫描、依赖注入、单例缓存、一个 BeanPostProcessor 扩展点就足以让原理变得具象。这个练习做完很多“抽象”的概念就落地了。我自己写那个迷你脚手架时最大的收获是理解了 BeanDefinition 的重要性。刚开始我只是扫描类名直接反射创建对象结果依赖注入的顺序一乱就出问题。后来按照真实 Spring 的思路扫描阶段就把所有类的元数据注册成 BeanDefinition 存起来实例化阶段再逐个创建整个流程瞬间清晰。这也印证了一条原则设计一个带容器的框架先收集元数据、再按顺序创建比零散反射调用要可靠得多。手写练习还能帮你发现一个问题为什么 Spring 要搞那么多接口和扩展点。当你试图在自己的框架里增加“AOP 代理”或者“配置属性解析”时会发现如果没有统一的生命周期回调方法就只能把逻辑写死在创建流程里改一处影响全局。而 BeanPostProcessor 通过回调机制把变化点下沉到了流程之外架构的稳定性就体现出来了。写在最后Spring 原理这个东西说难很难因为它层次很多说容易也容易只要你抓住容器和代理两条主线大部分特性都能顺着理解。我在日常工作中能看到凡是能熟练排查容器问题、事务问题、自动配置问题的工程师无一例外都对 Bean 生命周期和 AOP 的机制有清晰认知。最后再分享一个小技巧排查 Spring 相关问题时不要一开始就猜打开 debug 日志看 Bean 的创建顺序、代理类的生成位置很多时候问题当场就暴露了。Spring 是一个管道原理就是管道图图在心里遇到问题自然有解题思路。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑