
Spring全家桶源码是Java后端领域绕不开的一座山也是很多人心里过不去的坎。刚工作那会儿我也买了市面上很火的源码解析书从Spring core模块第一行开始做笔记结果记了两个月只画完了一堆类图真正问到“三级缓存为什么非要三级”“事务为什么自调用会失效”的时候脑子里还是一团浆糊。后来换了一种方式才把整条链路读通。我的建议很简单读Spring全家桶源码千万别按目录从第一章读到最后一章也别指望每个类都弄懂。先用“一个请求从进来到出去”为主线把IOC、AOP、MVC、事务、自动装配这些点串起来每个点上找三四个关键方法和断点用调试器代替眼睛两周就能搭起整体框架。下面我按这个思路把几个关键点拆开讲。目标读者是能熟练使用Spring Boot、但深入源码时总觉得无从下手的开发者。我会把Spring全家桶里最关键的IOC启动、三级缓存、AOP织入、MVC分发、自动装配和微服务组合逻辑逐一展开每一条都给出关键类和方法路径顺便分享一些我实际调试源码时踩过的坑和用起来很顺手的技巧。1. 先定主线再抠细节我的Spring全家桶源码打开方式1.1 按模块扫描为何走不通很多人的习惯是拿到Spring源码工程从spring-core模块开始逐包展开。这个做法不是不行但效率非常低。Spring源码的继承体系铺得很开一个简单的ClassPathXmlApplicationContext往上翻能看到ApplicationContext、EnvironmentCapable、ListableBeanFactory、HierarchicalBeanFactory再往上是MessageSource、ResourcePatternResolver每个接口旁边还跟着一堆support类和impl实现。如果一上来就把这些类图当知识树背大脑很容易陷入“每个类都见过但它们怎么协作的一点都不知道”的状态。更关键的是Spring全家桶在不同模块之间是层层包装的。Spring Framework只解决IOC和AOP的底层问题Spring Boot负责自动装配和简化启动Spring MVC把HTTP请求转成方法调用Spring Cloud再包一层微服务配置和治理。按模块目录顺序读相当于在还没看到整体地图的时候先研究每一块砖自然读不到重点。我自己的经验是源码阅读应该先建立“运行链路”概念再回头回填类名和方法名。这也符合Spring框架本身的设计哲学——它的大量设计模式模板方法、策略、责任链、适配器只有在看调用链时才能真正显露出价值。1.2 一条主线组合拳等你来抄我自己跑通的那条主线可以概括成五个断点。第一个断点打在SpringApplication.run里看启动过程如何一路走到AbstractApplicationContext.refresh()。第二个断点打在AbstractBeanFactory.doGetBean里看单例Bean是怎样从缓存、类定义走到实例化和属性填充的。第三个断点打在AbstractAutoProxyCreator.wrapIfNecessary里看一个普通Bean在什么情况下会被包成代理对象。第四个断点打在DispatcherServlet.doDispatch里看一次HTTP请求如何找到对应的Handler并完成调用。第五个断点打在TransactionInterceptor.invokeWithinTransaction里看一个标注Transactional的方法在事务上发生了什么。每条线先跑通记下几个关键类和方法名再回头补细节效果比在一个类文件里死磕要好得多。工具上用IDEA自带的调试功能就够关键是熟练使用“跳入”和“跳出”两个操作尤其在循环和很深的调用栈里要敢于跳出别陷进去。这样下来一周到两周你对Spring全家桶源码就不会再有“不知道从哪看起”的茫然感。下面几个章节我会把这条主线里最值钱、也最常被拿出来问的几个点掰开揉碎讲清楚。2. refresh()这个“总导演”IOC容器启动背后的设计骨架2.1 BeanDefinition从哪来、又去了哪Spring容器对Bean的管理核心不是“创建对象”而是“管理BeanDefinition”——你可以把BeanDefinition理解成每个Bean的“出生档案”里面写着类名、初始化方法、属性值、作用域、是否懒加载这些元数据。在refresh()方法的invokeBeanFactoryPostProcessors阶段容器会先把所有这些BeanDefinition收集起来存到DefaultListableBeanFactory的beanDefinitionMap和beanDefinitionNames里。在Spring Boot环境下收集工作主要由ConfigurationClassPostProcessor完成。它本身也是一个BeanDefinitionRegistryPostProcessor会在普通BeanFactoryPostProcessor之前被调用。它扫描Configuration类时会处理ComponentScan找到的组件类通过解析Bean方法生成ConfigurationClassBeanDefinition还会处理Import把第三方类导入容器。这一环是整个自动装配的前提。beanDefinitionMap维护了“beanName - BeanDefinition”的映射关系而beanDefinitionNames是一个按注册顺序排列的列表。这个列表的顺序很关键它决定了后置处理器扫描Bean时先看到谁、后看到谁也会影响Order或Priority等排序注解的处理时机。2.2 getBean()到doCreateBean的生命周期路径容器启动后真正开始创建Bean的时机在finishBeanFactoryInitialization。这里核心方法叫preInstantiateSingletons它会遍历所有非懒加载的单例BeanDefinition逐个调用getBean(beanName)。getBean只是入口内部跳到doGetBean。doGetBean的粗逻辑是先查singletonObjects一级缓存查不到就根据作用域决定走单例分支还是原型分支最终落到doCreateBean。doCreateBean这个方法是整个IOC链路最核心的地方。它分成四步createBeanInstance根据构造器信息实例化对象populateBean做属性填充initializeBean执行初始化前、初始化、初始化后的钩子最后把成品放入singletonObjects缓存。值得留意的是循环依赖的关键处理其实发生在doCreateBean的早期步骤里——当对象刚new出来、还没有填充属性时就会提前暴露一个早期引用。这部分我会专门在下一章展开。很多初学者容易把IOC理解成“框架帮你new对象”这是不够的。读源码后你会意识到控制反转不仅体现在创建时机上更体现在Bean的整个生命周期都由容器统一管理所有需要扩展的点都放在BeanPostProcessor里而不是散落在各个业务代码中。处理器类型执行阶段典型实现能干什么BeanFactoryPostProcessorBeanDefinition加载完毕后、Bean实例化之前ConfigurationClassPostProcessor、PropertySourcesPlaceholderConfigurer修改BeanDefinition、注册新的BeanDefinition、处理占位符BeanPostProcessorBean实例化并属性填充后、初始化前后AnnotationAwareAspectJAutoCreator、AutowiredAnnotationBeanPostProcessor对已创建对象做二次包装、注入依赖、生成代理这个表格很直观。BeanPostProcessor在refresh()阶段就被提前实例化并注册这样后面创建普通Bean时才能及时介入。Autowire注解的解析就发生在AutowiredAnnotationBeanPostProcessor.postProcessProperties里。3. 三级缓存的取舍逻辑为什么循环依赖注定要造三层结构3.1 循环依赖发生时容器内部发生了什么A依赖BB依赖A这在业务中很常见也最容易让容器创建陷入死循环。Spring用三张Map来解决这个问题三张Map都维护在DefaultSingletonBeanRegistry里singletonObjects是最终成品缓存earlySingletonObjects是早期单例缓存singletonFactories是单例工厂缓存。正常流程中A创建时会先new出半成品A把这个半成品A包在一个ObjectFactory里放进singletonFactories再去填充属性、注入B。B创建时发现需要注入A它会回到getSingleton里找A此时从三级缓存singletonFactories中拿出那个ObjectFactory调用getObject得到半成品A存入earlySingletonObjects返回给B做属性填充。B创建完成后A继续走完剩余流程最终把自己放入singletonObjects。这个机制听起来不复杂但设计得很讲究。三级缓存的名字容易让人误以为只是“加了层缓存减少重复创建”实际上它最重要的作用是把“提前暴露原始对象”和“生成代理对象”这两个时机解耦开。3.2 二级缓存为什么救不了有AOP的场景有个经典追问既然二级缓存earlySingletonObjects已经能存早期对象为什么还要三级缓存里的ObjectFactory答案藏在AOP的代理生成时机里。Spring的代理并不是创建Bean时就立刻做的而是要等所有BeanPostProcessor都就绪后由AnnotationAwareAspectJAutoProxyCreator在postProcessAfterInitialization阶段才把对象包成代理。如果只有二级缓存A在被循环依赖提前暴露时就必须在这个时刻立即决定要不要代理、生成哪个代理对象。可问题是这个提前暴露的A可能只是被临时引用并不一定需要代理而且如果过早生成代理等A真正走完生命周期、执行后置处理时又会产生“代理套代理”的混乱。三级缓存存的是ObjectFactory等于把“要不要生成代理”的决定延迟到真正有人找它的时候。ObjectFactory.getObject默认返回原始对象但getEarlyBeanReference这个方法才是让SmartInstantiationAwareBeanPostProcessor有机会提前生成代理的地方。我第一次跑到这段代码时心里感慨这个设计根本不是靠“想到了”就能造的它是在长年累月处理真实容器问题后沉淀出来的。对读源码的人来说看懂二级和三级之间的区别比背下三张Map的名字有价值得多。3.3 官方对循环依赖态度的转变Spring Boot从2.6版本开始默认把spring.main.allow-circular-references设置为false。这个变化和三级缓存机制并不矛盾——缓存机制始终在Spring Framework层面保留着官方只是希望应用层别依赖它。因为循环依赖本身设计味道不好两个对象互相耦合很难做到职责清晰而且三级缓存对AOP场景确实存在一些边界问题多个代理Bean互相引用时容易出现对象版本不一致。我在实际项目里确实见过因为循环依赖导致代理对象丢失增强的故障。所以现在的建议很明确新项目直接用构造器注入、消除互相依赖让代码天然没有循环依赖老项目如果已经出现循环依赖用Lazy在某个依赖上打标记比强行关掉allow-circular-references再去适配靠谱得多。4. AOP代理的织入位置与事务拦截器链路4.1 AbstractAutoProxyCreator在生命周期中的站位AOP在Spring里不是独立运行的魔法而是建立在BeanPostProcessor基础之上的一层扩展。核心类是AbstractAutoProxyCreator它实现了SmartInstantiationAwareBeanPostProcessor接口。在每个普通Bean的initializeBean阶段容器会调用在registerBeanPostProcessors阶段注册好的所有BeanPostProcessor。AbstractAutoProxyCreator的postProcessAfterInitialization会调用wrapIfNecessary给符合条件的Bean创建代理对象并替换掉原始对象。所谓“符合条件”是指能找到匹配的Advisor。Advisor是Pointcut和Advice的组合——Pointcut负责判断哪些方法需要拦截Advice负责描述拦截后干什么。Spring的切面类加Aspect、Before、After、Around注解后会被AnnotationAwareAspectJAutoProxyCreator解析成一系列Advisor。拦截逻辑最终落到动态代理类的方法上业务方法被调用时先走Advice链再进入真正的方法体。这一套设计让AOP的使用者完全不感知代理对象的存在这就是控制反转在横切关注点上的延伸。4.2 JDK动态代理与CGLIB的切换判断代理对象具体怎么生成由DefaultAopProxyFactory.createAopProxy()决定。老版本Spring的判断逻辑里如果目标类有接口且没有设置proxyTargetClass就默认用JDK动态代理如果目标类没有接口或者明确配置proxyTargetClasstrue就用CGLIB。Spring Boot从2.x开始把spring.aop.proxy-target-class默认设为true所以现在绝大多数场景走的都是CGLIB。CGLIB通过继承目标类生成子类来代理因此目标类不能被final修饰否则代理创建会直接报错。JDK动态代理只能代理接口接口方法上的注解和返回值处理方式也不一样。这些差异看起来是底层细节实际影响很大——我曾见过一个项目把类写成final后升级Spring Boot结果启动直接报“Unable to proxy method”的尴尬情况。这段源码还解释了另一个常见问题为什么有时候给同一个类加了接口注入进去的对象类型还能对上因为Spring Boot默认走CGLIB后代理类与被代理类是子父类关系Autowired按类型注入依然能匹配。如果你在Stack Overflow上搜过“Spring Boot代理总是CGLIB”的讨论回过头再看createAopProxy的这段判断逻辑一切就都通了。4.3 TransactionInterceptor源码里藏着事务失效的所有答案事务也是AOP的一种具体应用。Transactional方法会被解析成一个事务切面对应的Advice就是TransactionInterceptor。它实现了MethodInterceptor接口核心方法是invokeWithinTransaction先让TransactionManager开启事务然后进入方法体如果方法正常返回就提交事务如果抛出异常且满足回滚条件就回滚事务。DataSourceTransactionManager会把Connection的自动提交关掉把事务状态挂在当前线程上。理解了这条链路就能解释为什么同类内部方法自调用会让Transactional失效——自调用根本不经过代理对象而是直接调用目标类的this方法。源码里真正执行事务逻辑的是TransactionInterceptor这个环绕通知不走代理就没人开启事务。另外方法若被private修饰、异常被catch吞掉、或者抛出一个非RuntimeException类型错误事务回滚也都不会触发。要验证的话在TransactionInterceptor.invokeWithinTransaction方法入口打断点观察代理链是否进来一次调试就能彻底明白。5. Spring MVC请求生命周期从DispatcherServlet到参数解析器的策略组合5.1 HandlerMapping如何把URL定位到HandlerMethodSpring MVC对一次HTTP请求的处理可用一句话概括请求先由DispatcherServlet统一接住再由各种HandlerMapping定位到具体的Handler最后由HandlerAdapter挑选适配器完成调用。DispatcherServlet是前端控制器模式的核心它把请求处理、异常处理、视图解析这些职责分散给一组策略组件每个组件都放在容器里方便扩展替换。RequestMappingHandlerMapping是日常开发中最常碰到的HandlerMapping。它实现了InitializingBean接口在容器启动时通过afterPropertiesSet扫描所有带Controller或RequestMapping的类把URL和请求方法构建成RequestMappingInfo注册到内部维护的MappingRegistry里形成URL到HandlerMethod的映射。请求进入时doDispatch调用getHandler(request)逐个询问HandlerMapping是否有匹配的handler命中后返回一个HandlerExecutionChain——这里面既包含HandlerMethod本身也包含该请求匹配到的所有拦截器。5.2 适配器模式与参数解析器的实际协同Handler只是方法位置真正调用它还需要适配器。RequestMappingHandlerAdapter是专门处理RequestMapping方法的适配器它在handle方法内部创建ServletInvocableHandlerMethod然后通过invokeAndHandle完成参数解析、反射调用、返回值处理三步动作。参数解析是面试高频点。Spring在RequestMappingHandlerAdapter中维护了一长串HandlerMethodArgumentResolver比如解析RequestParam的RequestParamMethodArgumentResolver、解析PathVariable的PathVariableMethodArgumentResolver、解析RequestBody的RequestResponseBodyMethodProcessor、解析ModelAttribute的ModelAttributeMethodProcessor等等。参数解析器就是策略模式的最佳教材——每个解析器负责一类参数类型supportsParameter方法判断能不能处理resolveArgument方法负责真正取参数值。我建议你把断点打在InvocableHandlerMethod.getMethodArgumentValues里。这个方法会遍历方法参数依次调用参数解析器判断是否支持。真正跑起来你才会发现一个看起来简单的RequestBody User user底层要经过多少层判断才能从请求体里读出JSON并转成对象。5.3 一次RestController请求的完整源码足迹我们一步步还原一次GET请求的源码足迹请求先经过Servlet容器里的FilterChain随后到达DispatcherServlet.doDispatch。doDispatch里先getHandler拿到HandlerExecutionChain再getHandlerAdapter找到RequestMappingHandlerAdapter接着执行handler并填充ModelAndView最后经过HandlerExceptionResolver统一处理异常。对于RestController接口返回值处理阶段用的是RequestResponseBodyMethodProcessor它会把方法返回值通过HttpMessageConverter序列化成JSON写进HttpServletResponse。读这段源码时有个很深的感受Spring MVC最优雅的设计不是某个具体功能而是它把“找处理器、调用处理器、解析参数、处理返回值、解析视图”全部设计成可替换的策略接口。想扩展一个新功能往往只需要往容器里塞一个新的HandlerMapping或ArgumentResolver原有链路不需要改一行代码。6. 自动装配背后spring.factories如何撑起全家桶的扩展空间6.1 SpringBootApplication背后那串importSpring Boot最让人省心的能力是自动装配入口就藏在SpringBootApplication这个组合注解里。它由SpringBootConfiguration、EnableAutoConfiguration和ComponentScan三个注解合成。其中EnableAutoConfiguration通过Import引入了AutoConfigurationImportSelector这个核心类它的职责是扫描所有依赖jar包中的META-INF/spring.factories文件读取以EnableAutoConfiguration为键的配置项把这些自动配置类全部加载到BeanDefinitionRegistry里。这一步看完会觉得“全家桶的规模感”特别清晰你依赖一个starter它的jar包里就带着一个spring.factoriesSpring Boot启动时统一把候选的自动配置类列出来。比如spring-boot-autoconfigure这个包里的spring.factories写着数百个*AutoConfiguration类名包括DataSourceAutoConfiguration、WebMvcAutoConfiguration、HttpMessageConvertersAutoConfiguration等等。这就是全家桶“开箱即用”的底层来源。6.2 条件注解自动装配为什么不会全军覆没如果只是把几百个AutoConfiguration类全部实例化容器一定会启动失败——比如项目里没有DataSource依赖DataSourceAutoConfiguration如果盲目加载就会因为找不到类或者没有配置而崩溃。所以Spring Boot依靠一套Conditional条件体系来做“一票否决”典型注解是ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty。Condition的eval逻辑在ConditionEvaluator类里主要判断ClassLoader里能否加载指定类、容器里是否已有指定Bean、环境变量里是否有指定配置。只有所有条件都满足对应的自动配置类才会被解析和注册。这套机制让“全局扫描候选配置”和“按环境精准生效”达成平衡也是全家桶能支持几十种可选组件却互不干扰的根本原因。6.3 从Spring Cloud视角看全家桶的扩展方式到了Spring Cloud整条链路的复杂度又上了一个台阶。以Spring Cloud Alibaba为例Nacos作为注册中心和配置中心是通过spring.factories里的BootstrapApplicationListener在应用上下文刷新前插入一个引导上下文加载bootstrap.yml配置并以NacosPropertySourceLocator把远程配置塞进Environment。服务发现则通过NacosServiceRegistry实现Registration和Registry接口再对接负载均衡和OpenFeign最终把一个普通Spring Boot应用扩展成微服务架构中的节点。读全家桶源码进入这个阶段后我的建议是从入口类往回看每个starter的AutoConfiguration和spring.factories就是一张地图先看它注册了哪些Bean再看这些Bean分别实现什么接口。接口就是框架留给你的扩展点理解了这个比背任何一个组件的方法列表都重要。从Spring Framework到Spring Boot、Spring Cloud全家桶源码的核心其实是同一套设计思想的反复应用把“创建与组装”交给容器把“扩展能力”留给接口把“生效条件”交给配置。我读源码这几年最大的体会是不要怕源码英文注释多、类名长真正让人卡住的是没有入口和主线。如果某天你又被某个源码绕晕了建议立刻停下手头的事反问三个问题这个类是从哪里被调进来的调用链上层是谁被调用时容器里已经有哪些Bean把三个问题顺着调试器往上追一遍大部分源码困惑都能烟消云散。这篇文章里的代码路径我都是在Spring Framework 5.3.x配合Spring Boot 2.7.x的调试环境中走通的。你也可以照着相同版本搭一个调试环境在几个关键方法里打断点亲自走一遍启动和请求链路。源码这东西别人的解析只负责带你进门真正的手感一定是从断点和调用栈里一点一点磨出来的。