Spring核心原理:容器、依赖注入、AOP与事务全解析

发布时间:2026/10/15 22:45:59

Spring核心原理:容器、依赖注入、AOP与事务全解析 1. 先搞清楚Spring到底解决什么问题很多人学Spring上来就看依赖注入、看AOP、看事务照着教程能跑通但一旦报错就完全蒙了。原因很简单不知道框架底层在做什么遇到问题只能瞎猜。搞Spring原理我觉得第一件事不是看源码而是先想明白一个原始问题在没有Spring的年代我们写Java后端是怎么活过来的举个最直接的例子。以前写一个用户注册功能Service里要操作数据库就得自己创建连接、写DAO、再手动把DAO塞进Service。改成Oracle数据库你得把所有DriverManager.getConnection的代码全换一遍想给方法加个性能日志你得在每个方法里前后手写System.currentTimeMillis()再try-finally打日志。代码量一上来这些重复工作会把人逼疯。Spring说到底做的是这么几件事对象谁创建、对象之间怎么互相拿到引用、公共逻辑怎么织入业务方法、数据库事务怎么统一管理。这四个问题分别对应IoC容器、依赖注入、AOP、事务抽象。把这四条主线串起来Spring原理的框架感就出来了。Spring解决这些问题的手段核心就一句话把控制权从业务代码手里反转给容器。这个反转IoC说得玄乎翻译成人话就是——业务代码不再自己new依赖对象而是向容器声明我需要什么容器在启动时负责创建好并递到手里。理解了这个后面看Bean生命周期、看依赖注入都是顺着一根藤摸瓜。还有一个容易被新手忽略的点Spring并不是一个单体它从最初的核心容器慢慢长出AOP、事务、MVC、Boot等一大套生态。但底层地基始终是BeanFactory这套对象管理模型。哪怕你用的是Spring Bootmain方法里跑起来之后干的活仍然是创建并刷新一个ApplicationContext本质还是那个容器。所以我说掌握了容器原理就掌握了整个Spring家族的七寸。2. 容器核心原理Bean生命周期不是背出来的网上面试八股文里常有人背Bean生命周期什么实例化、属性填充、初始化、销毁背得滚瓜烂熟问一句为什么是这个顺序就哑了。我建议换个学法把生命周期当成容器替你做家务的过程。2.1 从BeanDefinition到对象容器手里拿到的第一手资料不是对象本身而是Bean的定义信息也就是BeanDefinition。这个类里面装的是Bean的全限定类名、作用域singleton还是prototype、是否懒加载、初始化方法名、属性值、构造参数值、依赖关系等。你可以把它理解成一张生产图纸容器拿着图纸去造对象而不是图纸本身。这个区别很重要。Spring的配置不管是XML里的bean标签还是配置类里的Bean方法或者是组件扫描到的Component类最终都会被解析成BeanDefinition并注册进BeanDefinitionRegistry。这也解释了为什么Spring能支持这么多配置方式——因为它们殊途同归都统一到同一张图纸上。这里有个实战体会你在配置类里写了Bean方法容器不会立刻执行这个方法它只是把方法信息记录下来生成BeanDefinition。真正执行是在容器刷新阶段的finishBeanFactoryInitialization也就是所有单例Bean默认预实例化那一刻。我见过不少人在Bean方法里做复杂计算以为启动时就会跑其实得看有没有Bean依赖它。如果你标记了Lazy它可能拖到第一次被使用才真正执行。2.2 实例化、属性填充和初始化拿到BeanDefinition之后容器开始按图纸造人主要分这么几步第一步实例化。通过反射调用构造器创建一个原始对象此时对象的属性全是默认值。很多人问Spring会不会用无参构造其实不一定如果明确指定了构造参数它就用ConstructorResolver去匹配最合适的构造器。这就是为什么Spring推荐单一构造器因为多构造器多参数时匹配规则会让新手绕晕。第二步属性填充。也叫populateBean这一步会把BeanDefinition里记录的属性值挨个注入进去。注入的可能是字面量值也可能是别的Bean的引用。如果是引用就触发依赖查找从容器里找到那个Bean塞进来。注意Aware接口回调也发生在这个阶段附近比如BeanNameAware让你知道自己叫什么名字ApplicationContextAware让你能拿到容器本身。第三步初始化。顺序是先走各种BeanPostProcessor的前置处理再执行InitializingBean接口的afterPropertiesSet再执行自定义的init-method最后走后置处理。BeanPostProcessor是整个Spring扩展体系里的神器AOP代理对象就是在这个环节偷偷替换掉原始对象的。第四步销毁。容器关闭时单例Bean会依次走PreDestroy、DisposableBean的destroy方法、自定义destroy-method。提示你实际开发中很少会直接实现InitializingBean这类接口因为有PostConstruct注解和Bean(initMethod xxx)这种更优雅的写法。但理解接口顺序对排查初始化方法为什么没执行这类问题极有帮助。2.3 三级缓存与循环依赖循环依赖说白了就是A依赖BB又依赖A两边互相引用。如果容器傻乎乎地先造A需要B再造B又发现B需要A这就成死循环了。Spring解决这个问题靠的是三张缓存表也就是常说的三级缓存。一级缓存singletonObjects放最终成品Bean二级缓存earlySingletonObjects放半成品Bean已经实例化但还没完成属性填充三级缓存singletonFactories放的是ObjectFactory这个东西可以在需要时提前生成一个早期引用。处理流程大致是创建A时先把A的ObjectFactory放进三级缓存然后填充A的属性时发现需要B转去创建BB创建时发现需要A此时从三级缓存里找到A的ObjectFactory调用它得到一个A的早期引用放进二级缓存然后B继续完成属性填充和初始化B完成后A拿回B的引用继续完成自己的属性填充和初始化最后A进入一级缓存。这套机制巧妙在A被提前暴露给了B但A的完整初始化并没有结束。为什么用三级缓存而不是直接把半成品放二级因为Spring希望给BeanPostProcessor留出干预的机会比如某些增强器需要提前生成代理对象。二级缓存存的是最终暴露出去的早期引用三级缓存存的是生成这个引用的工厂将对生成时机的决策延迟到了真正需要的那一刻。实际开发中我不建议你依赖循环依赖虽然Spring能处理但设计上就是坏味道。更关键的是Spring Boot 2.6开始默认禁止循环依赖了ApplicationContext启动时如果检测到会直接报错。我的态度就一句话能避免就避免别拿框架的容错当设计挡箭牌。真要遇到循环依赖优先考虑用构造器注入重构出分层结构把互相依赖的公共部分抽出去。3. 依赖注入三种方式背后同一个原理依赖注入的主体其实是依赖查找容器先把Bean都管理起来再通过注入这个动作把引用递到需要的地方。三种常见注入方式在不同项目里都见过我把它们放一起对比这样选型时心里有底。3.1 构造器注入为什么受官方推荐构造器注入就是通过构造方法的参数把依赖传进来。Spring在选择构造器时会看参数类型和容器里现成的Bean能不能匹配上。单个构造器最简单直接按参数类型找多个构造器时会按参数个数、Autowired标注情况综合判断规则稍微有点绕。这个方式最突出的好处是依赖不可变用final修饰字段Bean在创建时就必须把依赖给齐没人能事后偷偷改。而且测试非常方便直接new出来传mock对象就行不需要反射或者mock容器。Spring官方文档也一直推荐这种方式。我在项目里的习惯是核心依赖全部用构造器注入尤其是那些一个Bean离不开的协作对象比如Repository、外部ApiClient。这样能保证对象要么完整创建要么创建失败不会出现半初始化状态。3.2 Setter注入和字段注入的区别Setter注入通过setXxx方法注入Spring在属性填充阶段会调用这些setter。它比构造器松一点允许创建后再改依赖适合那些有默认实现、可以替换的依赖。字段注入就是我们最熟悉的Autowired直接打在字段上。代码最简洁但问题也最明显外部无法直接创建对象因为字段是private的测试只能靠反射或Spring上下文而且依赖在创建对象时是缺失状态直到容器介入才补齐。这相当于把对象必须是完整状态这个约束放弃了。我踩过字段注入的坑把一个Bean从Spring容器里抠出来做单元测试发现new出来之后依赖全是null想mock一个都塞不进去最后只能改用构造器。从原理上说字段注入是Spring通过反射直接给字段赋值绕过了构造器和setter这在运行时没问题但对代码的可测试性伤害极大。3.3 自动装配是怎么找到目标的自动装配分两种Autowired按类型找Resource按名字找。Autowired查找逻辑是先找出所有类型匹配的Bean如果只有一个直接用如果多个再结合Qualifier或字段名缩小范围一个都没有就抛异常除非把required设成false。Resource是Java标准注解先按字段名或属性名找Bean找不到再退化到按类型。很多老项目混用这两个注解出过不少为什么换了个名字就注入失败的问题。我的经验是在一个项目里统一下来不要一会儿Autowired一会儿Resource不然排查依赖问题的时候会多出一堆干扰项。这里顺带说一下Java Config里Bean方法之间的参数依赖。Spring处理时会把被Bean方法作为参数传入的Bean先解析出来然后传入方法内。整套机制和字段注入原理相同但表现形式更明确。4. AOP到底是怎么把逻辑织进去的AOP的全称是面向切面编程抽象概念听起来高大上其实解决的问题特别朴素把贯穿各业务模块的公共逻辑抽出来统一处理。日志、权限校验、性能监控、事务管理这些都是典型的横切逻辑。Spring AOP的实现核心是动态代理。4.1 JDK动态代理和CGLIB代理怎么选Spring AOP如果目标类实现了接口默认用JDK动态代理。JDK动态代理基于java.lang.reflect.Proxy在运行时为接口生成一个代理类代理类实现了同样的接口并在调用方法时转发给InvocationHandler的invoke方法。如果目标类没有实现接口那就用CGLIB。CGLIB通过生成目标类的子类来代理子类重写目标方法在方法调用前后插入增强逻辑。关键在于CGLIB是生成子类所以目标类和方法不能被final修饰否则无法重写。这里有个很隐蔽的问题JDK动态代理生成的代理对象只能转成接口类型不能转成具体类类型。如果你在代码里把代理对象强转成实现类会直接抛ClassCastException。而CGLIB代理转成原类型没问题。Spring Boot 2.x之后默认把spring.aop.proxy-target-class设为true也就是默认走CGLIB因此这类强转问题少了很多。4.2 代理对象的创建链路Spring AOP的入口是AbstractAutoProxyCreator它是一个BeanPostProcessor在前面提到的Bean初始化阶段会介入。原始对象创建好之后它会检查这个Bean是否匹配当前容器中所有的Advisor通知器匹配的话就生成代理对象并把这个代理对象返回给容器容器接下来往别的Bean里注入的就是这个代理。Advisor本身由两部分组成Pointcut切点和Advice通知。切点决定了哪些方法需要增强通知决定了增强的具体逻辑比如Before、AfterReturning、Around这些。它们都是从Aspect配置类解析出来的每一个切面方法会被包装成InstantiationModelAwarePointcutAdvisor注册到容器中供代理创建器消费。调用方法时代理对象把方法调用包装成MethodInvocation按照拦截器链逐个执行。如果你写过Around应该感觉得到ProceedingJoinPoint.proceed()方法继续往下走其实就是在链条上推进到下一个拦截器或目标方法。理解了这个链条模型AOP的执行顺序问题基本就能自己推断出来了。注意代理对象在容器内部就已经被替换了你在业务代码里拿到的任何引用都可能是代理。千万不要在业务代码里instanceof判断具体类然后强转这是很脆弱的写法一旦代理方式调整直接运行时报错。5. 声明式事务的本质还是代理那点事Spring的事务管理一开始容易给人错觉好像用了Transactional数据库就自动知道要回滚。实际上Spring自己不处理数据库事务它只是把JDBC或JPA等资源的事务操作统一封装成了PlatformTransactionManager然后通过AOP在业务方法前后自动调用commit或者rollback。5.1 事务管理器在干什么常见的DataSourceTransactionManager底层就是拿到一个数据库连接调用连接的setAutoCommit(false)执行业务代码最后根据情况调用commit()或rollback()。Spring事务的核心抽象是TransactionStatus和TransactionDefinition分别用来描述事务当前状态和事务的传播行为、隔离级别、超时时间、只读标志。传播行为这个词也是劝退新手的重灾区。其实它就是回答一个问题当前方法被调用时如果调用方已经有事务我这个方法是加入它还是挂起它另开一个REQUIRED是默认值加入已有事务或者没有则新建REQUIRES_NEW是挂起当前事务另开新事务NESTED是在嵌套事务里做保存点。理解这些写分布式事务服务的时候才不容易出乱子。5.2 Transactional失效场景的底层原因网上流传的失效场景很多我从原理上归纳基本逃不出这四类第一类方法不是public。Spring AOP默认只对public方法生效因为代理类只能重写public方法protected或private方法根本不会被拦截器链经过。哪怕你在private方法上写了Transactional它也不会生效。第二类自调用。同一个类内部一个方法调用另一个被Transactional标记的方法this调用不会经过代理对象而是直接调到目标方法当然不会触发事务拦截器。解决办法是拆出另一个Bean或者自己注入代理对象但前者更干净。第三类异常被吞掉。默认情况下事务只在RuntimeException和Error时回滚受检异常是不回滚的。如果方法内部自己catch住异常没往外抛事务框架根本感知不到。这其实是很多人说事务没生效的真正原因。第四类多线程调用。Spring事务和线程绑定事务信息存在ThreadLocal里新开的线程拿不到当前事务上下文自然不能加入同一个事务。很多人在这里踩坑以为是框架坏了其实原理上就决定了事务无法跨线程传播。6. Spring Boot自动配置本质还是容器扩展Spring Boot把Spring的约定大于配置推到极致但它的魔法并不是凭空出现的底层还是容器那一套机制只是多了几步自动装配。6.1 自动配置的装配入口Spring Boot的SpringBootApplication同时组合了Configuration、EnableAutoConfiguration和ComponentScan。EnableAutoConfiguration这个注解通过Import(AutoConfigurationImportSelector.class)引入了自动配置类选择器。这个选择器做的事情很直白读META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件旧版本是spring.factories拿到一堆自动配置类的类名然后把这些类注册成BeanDefinition。每个自动配置类本身还是一个Configuration配置类里面又有很多Bean方法。相当于在容器刷新前往里面塞进了一大批候选配置。这一步解释了Spring Boot的一个经典现象项目里明明没写配置为什么RedisTemplate、DataSource都能直接用因为自动配置类里已经按默认条件定义好了这些Bean而条件注解精确地控制了它们什么时候生效。6.2 条件注解是如何工作的ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty这些条件注解是自动配置的灵魂。以ConditionalOnClass为例它检查classpath下是否存在指定类存在才加载对应的配置类。ConditionalOnMissingBean检查容器中是否缺少某个Bean缺少才定义默认Bean。这就是为什么你可以覆盖自动配置容器里已经有了你自定义的Bean自动配置里的ConditionalOnMissingBean判断不成立默认Bean就不会创建。整个判断过程是Spring在解析配置类时通过Condition接口的matches方法完成的它是在BeanDefinition注册阶段就被执行的不是在Bean实例化阶段。如果你需要调试某个自动配置为什么不生效可以在启动参数里加--debug查看条件评估报告Spring Boot会输出每个配置类中每个条件的命中情况。这个报告是排查自动配置问题的第一利器比盲猜强得多。7. 排查Spring问题的几个真实经验最后这部分分享几个我在项目里反复遇到的故障排查过程每个都是血泪教训换来的。7.1 循环依赖启动报错一次升级Spring Boot版本后项目启动直接报循环依赖的异常提示信息明确指出了A和B互相引用。这种情况在旧版本里可能只是警告新版本默认禁止后就变成硬错误了。排查思路是先看异常里点名的两个Bean分析到底是构造器循环还是Setter循环。构造器循环是无解的只能重构Setter循环可以通过Lazy注解打破。我当时是两个类互相调同一个核心服务的方法后来把公共部分抽成第三个Bean问题直接消失。7.2 切面不生效的排查步骤碰到Aspect标记了但日志不打印的情况我一般是按顺序查三处。第一查切面类有没有被Spring扫描到光标Aspect不够还得让类进入容器第二查切面表达式execution表达式写得太严或者包名对不上是最常见的第三查代理方式如果目标类或方法有final修饰而代理又是CGLIB那增强不会执行。有一个容易忽略的点Spring AOP是基于代理的它只能增强Spring容器管理的Bean。如果你自己new了一个对象然后调用它的方法无论切面写得有多完美都不会生效。这是代理机制天然的限制不是Bug。7.3 Transactional没有回滚数据库里数据照常提交事务没回滚。这是老问题我通常直接看异常有没有被捕获再看方法是不是被同一个类内部调用最后看异常类型是不是受检异常。有一次排查半天发现是方法上同时标了Transactional和Async异步线程把事务给隔断了改掉异步后事务恢复。这类问题的统一排查方法论是先确认方法确实走代理调用链再确认异常确实传播到了事务拦截器那一层最后确认传播行为和回滚规则配得对。按照这个顺序查基本上十分钟内都能定位。我个人做Spring相关项目多年最大的体会是框架的原理不是让你背八股文用的而是给你一套在出问题时推理的方向。遇到报错先想容器创建Bean这一步走到了哪里先想代理对象有没有介入先想事务上下文还在不在当前线程问题往往就迎刃而解。Spring生态的东西每年都在更新但底层这套对象管理、代理增强和条件装配的思维五年十年都不会变。把这三根柱子吃透后续读任何Spring系源码都会轻松很多。
延伸阅读

更多相关文章

2026/10/15 22:40:58

Java工程师构建AI Agent的实战路径与架构设计

1. 这不是“转行”,是Java工程师的自然演进路径“13年Java老兵转型AI Agent”——这个标题里最需要被立刻纠正的认知偏差,就是“转型”这个词。我带过不下二十个从传统后端、中间件、金融核心系统走出来的Java开发者,他们做AI Agent项目时&am…

2026/10/15 22:40:58

AI漫剧生产全流程:从剧本到成片的降本增效实操指南

坦白说,我第一次在“生产短剧”这件事上认真考虑AI,是被成本逼的。真人短剧一集的拍摄成本摆在那,场景、演员、服化道、灯光、后期……任何一个环节想省,画面质感立刻露馅。后来看到几个团队用生成式AI做漫剧,单集成本…

2026/10/15 22:40:58

H5原生调用摄像头全攻略:从getUserMedia到移动端适配

先说一个我自己的真实经历。之前做移动端H5项目,页面需要直接用浏览器能力打开摄像头做人像采集。第一版Demo在电脑Chrome上跑,结果白屏,控制台日志打出一个NotAllowedError。我第一反应是权限被浏览器拦了,排查一圈下来才发现&am…

2026/10/15 23:56:10

自部署OpenClaw前先做环境体检:完整避坑指南

看到标题估计会有人想:装个OpenClaw而已,有必要把“第1件事”强调得这么严重吗?我之前也这么想,直到有一次装到一半开始报错,又花了一整个下午排查,最后发现所有问题都出在动手之前那几分钟少了点准备。Ope…

2026/10/15 23:56:10

结构化复盘实操指南:用一页纸模板把失败变成可复用资产

看到《3月13日(进阶5)》这个标题,我先说明一下它在我这里的含义:它是我正在参加的一个“进阶计划”训练营里,正好在3月13日发布的第五阶段任务。前四个阶段分别练过目标拆解、信息筛选、案例模仿和问题迁移&#xff0c…

2026/10/15 23:56:10

CSAPP大作业Hello’s P2P:从源码到进程的全链路验证

简介:一份哈工大CSAPP大作业《程序人生——Hello’s P2P》完整课程报告,面向计算机系统基础课程学生及备考者,以经典hello程序为线索,系统梳理从预处理到进程终止的完整生命周期,帮助读者打通编译、链接、运行之间的断…

2026/10/15 23:56:10

Java异常处理实战:从底层设计到工程实践

写Java的人几乎没有没被异常坑过的。空指针、数组越界、类型转换失败,这些是入门课,可真到生产环境,你面对的多半是异常被catch住之后日志里只有一排堆栈,或者一段业务报了错,你怎么都查不到最初的根因,再或…

2026/10/15 23:56:10

OpenClaw斜杠命令完全手册:会话管理与输出控制实战指南

1. OpenClaw 会话里的斜杠命令是什么,为什么值得专门写一份手册先说我自己的使用背景。我大概在半年前开始认真用 OpenClaw 做会话式自动化处理,一开始只把它当成一个普通的对话工具,有需求就打字,没需求就关掉。但用得越深入越发…

2026/10/15 23:51:10

PyTorch张量拆分与拼接:split、chunk、cat、stack核心技巧与避坑指南

1. 为什么张量拆分与拼接是绕不开的基本功刚接触PyTorch那会儿,我总觉得张量操作里最不起眼的就是torch.split、torch.chunk、torch.cat、torch.stack这几个函数。直到有一次在做一个多分支网络的特征融合模块,输入特征图需要按通道切成四份分别送进不同…

2026/10/14 11:31:03

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/14 17:41:07

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/15 13:42:16

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑