news 2026/9/3 2:21:57

Spring Framework 6.1源码调试入门:12个可运行案例+三级缓存实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Framework 6.1源码调试入门:12个可运行案例+三级缓存实战

简介:本资源是一份面向Java初学者与中级开发者的Spring框架源码学习套件,聚焦Spring核心机制的透彻理解,有效解决入门者面对庞杂源码无从下手、缺乏上下文注释与实践引导的痛点。压缩包为ZIP格式,大小36.14MB,包含完整Spring各模块源码(如spring-beans、spring-context、spring-aop等),所有类文件均附带中文注释,并配套IoC容器初始化、@Autowired依赖注入、AOP代理织入、JdbcTemplate数据访问及DispatcherServlet请求处理等典型场景的可运行案例工程,便于边读边验。目前已有1535人学习下载,目录结构严格对应Spring官方模块划分,关键类如DefaultListableBeanFactory、AutowiredAnnotationBeanPostProcessor、AspectJAutoProxyCreator、JdbcTemplate和DispatcherServlet均标注核心流程与设计意图,显著降低源码阅读门槛,助力开发者扎实掌握Spring底层原理与扩展能力。

1. 这不是一本“源码书”,而是一套可运行、可调试、可复刻的Spring学习操作系统

你点开这个标题,大概率是刚学完Spring Boot基础,写过几个@RestController,配过几回application.yml,但一看到@Service里@Autowired背后到底发生了什么,就卡住;或者正被面试官问“BeanFactory和FactoryBean区别在哪”“三级缓存为什么能解决循环依赖”,答得支离破碎;又或者想啃《Spring源码深度解析》这类经典,翻到第3章AbstractBeanFactory的doCreateBean方法就头晕目眩——不是代码看不懂,是根本不知道该从哪一行开始下断点,不知道哪个类才是真正的入口,更不知道那些密密麻麻的注释,到底是作者随手写的,还是真能帮你理解流程的“路标”。

我带过67个Java后端新人,从2015年Spring 4.1时代就开始带团队读源码。最常听到的抱怨不是“太难”,而是“找不到起点”“注释像天书”“案例跑不起来”“看完还是不会改”。所以这次,我把十年来沉淀下来的Spring源码学习路径,彻底重构了一遍:它不是把Spring Framework 6.1.0所有.java文件打包扔给你,而是用一套可执行、带断点、有上下文、每行注释都经过验证的工程结构,把Spring从启动到Bean创建、AOP织入、事务开启、MVC分发的完整生命周期,拆成12个可独立运行的最小闭环案例。每个案例都对应一个真实问题场景——比如“为什么加了@Transactional的方法内部调用不生效”,你就直接打开transaction-case模块,在TransactionAspectSupport.invokeWithinTransaction方法里下断点,看代理对象怎么绕过、切点表达式如何匹配、事务传播行为怎么决策。所有注释不是翻译API文档,而是写在关键逻辑旁的“现场笔记”:这里为什么用ConcurrentHashMap而不是HashMap?这个if判断漏掉会引发什么死锁?这个try-catch捕获的是哪个具体异常?实测下来,新人平均用2.3天就能独立走通第一个IOC容器初始化流程,比纯看书快4倍。

核心关键词spring、源码、注释、案例、入门级,不是堆砌,而是五个必须同时满足的硬指标:

  • spring:严格限定在Spring Framework 6.1.0(非Boot封装层),所有代码基于spring-beans、spring-context、spring-aop等原生jar反编译+重注释;
  • 源码:不是截图,不是PDF,是IntelliJ IDEA可直接导入、F9单步调试、Ctrl+Click跳转的完整工程;
  • 注释:每行关键逻辑必有中文注释,且标注来源(如“来自DefaultListableBeanFactory#preInstantiateSingletons第187行,Spring官方注释原文为…”);
  • 案例:12个独立Maven子模块,每个聚焦一个核心机制(如ioc-case、aop-case、tx-case、mvc-case),附带README.md说明触发条件、预期现象、调试路径;
  • 入门级:零Spring Boot经验也可上手——所有案例均用ClassPathXmlApplicationContext或AnnotationConfigApplicationContext手动启动,避开自动配置干扰,直面Spring最原始的API调用链。

如果你的目标是“能看懂Spring官网Javadoc背后的实现逻辑”,而不是“背下BeanDefinitionRegistryPostProcessor的17个实现类”,那这套材料就是为你设计的。它不教你如何成为Spring Committer,但能让你在下次CR时,一眼看出同事提交的BeanPostProcessor是否会在Bean初始化前就修改final字段——这才是入门级该有的深度。

2. 为什么必须放弃“通读源码”的幻想?我的三年踩坑路线图

刚接触Spring源码时,我也信过“通读论”。2016年,我花了整整三个月,用Notepad++逐行抄写spring-beans包下所有类,边抄边翻译Javadoc,结果抄完refresh()方法的12个子步骤,连DefaultListableBeanFactory的registerBeanDefinition()参数含义都没搞清。后来带新人时发现,92%的人卡在同一个地方:他们试图用“阅读小说”的方式读源码,却忘了源码是“执行剧本”——没有运行时上下文,没有数据流向,没有调用栈压栈弹栈,再详细的注释也是空中楼阁。

2.1 入门级最大的认知陷阱:混淆“源码结构”与“执行路径”

Spring Framework源码目录看着很清晰:spring-core、spring-beans、spring-context…但实际执行时,代码流根本不是按包名顺序走的。比如你写一个@Component类,你以为流程是:
spring-context → ClassPathBeanDefinitionScanner → spring-beans → BeanDefinitionRegistry → spring-core → BeanFactory

实际上,当你调用new AnnotationConfigApplicationContext(AppConfig.class)时,第一行执行的是AnnotatedBeanDefinitionReader.register(…),它直接调用BeanDefinitionRegistry.registerBeanDefinition(),而这个接口的实现类GenericApplicationContext又委托给DefaultListableBeanFactory——此时你甚至还没碰到spring-context包里的任何类。更致命的是,@ComponentScan的扫描逻辑藏在ClassPathScanningCandidateComponentProvider里,这个类在spring-core包下,但它的findCandidateComponents()方法调用了ResourcePatternResolver(spring-core)→PathMatchingResourcePatternResolver(spring-core)→ClassPathResource(spring-core),全程没进spring-context半步。

我当年就是在这里栽了跟头:在spring-context目录下疯狂搜索“ComponentScan”,却不知道真正干活的是spring-core里的ClassPathScanningCandidateComponentProvider。直到某次在AnnotationConfigApplicationContext构造函数里打了个断点,看着调用栈一层层往下钻,才明白Spring的模块划分是编译期解耦,运行时是高度交织的。所以这套材料的第一个设计原则就是:抛弃包结构,按执行路径组织案例。ioc-case模块里,你看到的不是spring-beans包的类列表,而是从AnnotationConfigApplicationContext构造开始,到finishBeanFactoryInitialization()结束的完整调用链,每个类都标注了“此处进入spring-core”“此处跳转spring-aop”,让执行路径一目了然。

2.2 注释不是越多越好,而是要“注在刀刃上”

网上很多所谓“带注释源码”,其实是把IDEA自动生成的Javadoc翻译成中文,或者把Spring官网Wiki复制粘贴。这种注释对入门者毫无价值。比如AbstractBeanFactory.doGetBean()方法开头有段注释:“Return the bean instance that should be exposed for this bean definition.”——这等于没说。真正有用的注释,应该回答“为什么这里要加synchronized?”“为什么这个变量用volatile修饰?”“如果这里抛出NoSuchBeanDefinitionException,上层怎么处理?”

我在重注释时,只保留三类内容:

  1. 决策型注释:解释代码为何选择此方案而非彼方案。例如DefaultSingletonBeanRegistry.getSingleton()中,singletonObject = this.singletonObjects.get(beanName)之后有一行注释:“此处不直接返回singletonObject,因为可能正在创建中(earlySingletonObjects存在),需检查singletonsCurrentlyInCreation避免循环依赖检测失效”。
  2. 陷阱型注释:标注易错点。如AutowiredAnnotationBeanPostProcessor.postProcessProperties()里,在field.setAccessible(true)后加注释:“JDK17+默认禁用反射访问private字段,需添加--add-opens java.base/java.lang=ALL-UNNAMED JVM参数,否则抛InaccessibleObjectException”。
  3. 溯源型注释:标明该逻辑的官方依据。如ConfigurationClassPostProcessor.processConfigBeanDefinitions()中处理@Bean方法时,注释写:“此逻辑对应Spring官方文档‘@Bean Method Processing’章节,要求@Bean方法必须是非static的,否则忽略(见ConfigurationClassUtils.isBeanMethod())”。

所有注释都经过实测验证:我在JDK8、JDK11、JDK17三个环境分别运行对应案例,确认注释描述的现象真实存在。比如关于JDK17反射限制的注释,就是我在跑ioc-case时连续三次报InaccessibleObjectException后补上的——这种血泪教训,比一百句理论都管用。

2.3 案例不是Demo,而是“故障注入式”学习单元

很多入门案例喜欢写“Hello World”式的Spring应用:定义一个Service,注入一个Dao,打印一句“Hello Spring”。这种案例的问题在于,它掩盖了Spring最核心的复杂性——状态管理、时机控制、边界条件。真正的Spring难点,永远出现在“意外发生时”。

所以这套材料的12个案例,全部采用“故障注入”设计:

  • ioc-case:故意在Bean定义里设置scope="prototype",然后在单例Bean里@Autowired它,观察每次getBean()是否真的创建新实例(答案是否定的,因为@Autowired注入的是容器提前创建的原型Bean引用);
  • aop-case:在目标方法里throw new RuntimeException(),然后对比@Transactional(propagation = Propagation.REQUIRED)Propagation.REQUIRES_NEW下事务回滚范围差异;
  • mvc-case:把@RequestMapping写成@RequestMapping("/user/{id}"),但Controller方法参数用@PathVariable Long id,故意不加@PathVariable("id"),看Spring如何解析路径变量(会报MissingPathVariableException,注释里详解ParameterDescriptor如何匹配)。

每个案例的pom.xml都精确锁定Spring版本(6.1.0)、JDK版本(17)、Maven版本(3.8.6),并预置好mvn clean compile exec:java一键运行脚本。你不需要配置任何环境,下载解压后,cd进对应模块,敲./run.sh(Linux/Mac)或run.bat(Windows),就能看到控制台输出完整的调用栈、Bean创建日志、AOP代理生成过程。这种“所见即所得”的反馈,比看十页文字描述都有效。

3. 核心细节拆解:从AnnotationConfigApplicationContext启动到Bean创建的17个关键节点

现在我们以ioc-case为例,完整走一遍Spring IOC容器初始化流程。这不是泛泛而谈的“refresh()八步法”,而是精确到每一行代码、每一个对象状态变化的实战记录。所有路径、类名、方法名、参数值,均来自实际调试截图。

3.1 第1步:AnnotationConfigApplicationContext构造——真正的入口在此

很多人以为refresh()是起点,其实AnnotationConfigApplicationContext的构造函数才是整个流程的发动机。当你写下:

ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);

执行的第一行是AnnotationConfigApplicationContext的父类GenericApplicationContext构造函数:

public GenericApplicationContext() { this.beanFactory = new DefaultListableBeanFactory(); }

注意这里:beanFactory被初始化为DefaultListableBeanFactory实例,但此时它还是个空壳——没有BeanDefinition,没有注册任何Bean。真正的“活化”发生在AnnotationConfigApplicationContext自己的构造函数里:

public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) { // 关键!此处创建AnnotatedBeanDefinitionReader,负责解析@Configuration类 this.reader = new AnnotatedBeanDefinitionReader(this); // 关键!此处创建ClassPathBeanDefinitionScanner,负责扫描@Component等注解 this.scanner = new ClassPathBeanDefinitionScanner(this); // 关键!此处注册AppConfig.class为Configuration类 this.register(annotatedClasses); // 关键!此处触发refresh(),但注意:此时容器尚未初始化 this.refresh(); }

提示:this.register(annotatedClasses)这行代码,会调用AnnotatedBeanDefinitionReader.register(),将AppConfig.class包装成AnnotatedGenericBeanDefinition,然后调用BeanDefinitionRegistry.registerBeanDefinition()存入beanFactory.beanDefinitionMap。这意味着,在refresh()执行前,容器里已经有一个BeanDefinition了——就是你的配置类本身。这是后续所有@Bean方法解析的基础。

3.2 第2步:refresh()——12个子方法的执行顺序与依赖关系

refresh()是Spring容器初始化的总控方法,但它本身不干活,只是按顺序调用12个protected方法。这12个方法不是并列关系,而是强依赖链:前一个方法的输出,是后一个方法的输入。比如obtainFreshBeanFactory()必须在prepareBeanFactory()之前执行,因为后者需要前者创建的ConfigurableListableBeanFactory

我们重点关注前5个方法,它们构成了IOC容器的骨架:

  1. prepareRefresh():设置容器启动时间、活跃标志位、初始化PropertySources(用于占位符解析)。
  2. obtainFreshBeanFactory():销毁旧的BeanFactory(如果有),创建新的DefaultListableBeanFactory,并将其赋值给this.beanFactory
  3. prepareBeanFactory(beanFactory):为BeanFactory配置核心组件:
    • 设置ClassLoader(默认为当前线程上下文类加载器);
    • 添加EmbeddedValueResolver(用于解析${}占位符);
    • 注册ApplicationContextAwareProcessor(实现ApplicationContextAware接口的Bean,会在此处被注入ApplicationContext);
    • 最关键的一步:注册ApplicationListenerDetector,这是一个BeanPostProcessor,它会在Bean初始化后检查是否实现了ApplicationListener接口,如果是,则将其加入事件监听器列表。
  4. postProcessBeanFactory(beanFactory):留给子类扩展的钩子方法。AnnotationConfigApplicationContext在此处调用reader.loadBeanDefinitions(),将@Configuration类中的@Bean方法转换为BeanDefinition并注册到beanFactory
  5. invokeBeanFactoryPostProcessors(beanFactory):执行BeanFactoryPostProcessor。最典型的是ConfigurationClassPostProcessor,它会:
    • 扫描所有@Configuration类;
    • 解析其中的@Bean@Import@ComponentScan等注解;
    • 生成对应的BeanDefinition并注册到beanFactory
    • 特别注意@ComponentScan扫描出的Bean,其BeanDefinitionscope属性默认为singleton,而@Bean方法生成的BeanDefinition,其scope由方法上的@Scope注解决定,未声明则默认singleton

注意:invokeBeanFactoryPostProcessors()执行完毕后,beanFactory.beanDefinitionMap里已经有了所有用户定义的BeanDefinition(包括配置类自身、@Bean方法生成的Bean、@Component扫描出的Bean)。但此时这些Bean都还没被实例化,singletonObjects(单例缓存)还是空的。

3.3 第3步:finishBeanFactoryInitialization()——单例Bean的批量创建与三级缓存登场

refresh()执行到第9步finishBeanFactoryInitialization(beanFactory)时,真正的Bean创建风暴才开始。这个方法的核心逻辑是遍历beanFactory.getBeanNamesForType(Object.class, true, false)获取所有非懒加载的单例Bean名称,然后对每个名称调用beanFactory.preInstantiateSingletons()

preInstantiateSingletons()方法是理解Spring三级缓存的关键。它不是简单地for循环调用getBean(),而是分三阶段处理:

阶段1:预创建(Pre-instantiation)
对每个BeanName,先检查beanFactory.isFactoryBean(beanName),如果是FactoryBean,则获取其创建的Object(即factoryBean.getObject()),否则直接调用getBean(beanName)

阶段2:getBean()的递归调用链
getBean(beanName)最终会走到AbstractBeanFactory.doGetBean(),这里就是三级缓存的主战场:

// doGetBean()核心逻辑节选 Object sharedInstance = getSingleton(beanName); // 1. 查一级缓存 singletonObjects if (sharedInstance != null) { return getObjectForBeanInstance(sharedInstance, name, beanName, null); } // 如果一级缓存没命中,且是单例Bean,则尝试创建 if (isSingleton()) { // 2. 创建前,先将beanName放入 singletonsCurrentlyInCreation(正在创建中集合) beforeSingletonCreation(beanName); try { // 3. 调用 createBean() 创建Bean实例 sharedInstance = createBean(beanName, mbd, args); // 4. 创建成功后,放入一级缓存,并从正在创建集合移除 afterSingletonCreation(beanName); } finally { if (newlyCreated) { addSingleton(beanName, sharedInstance); } } }

createBean()过程可能触发循环依赖。比如A依赖B,B依赖A。这时就需要二级和三级缓存介入:

  • 一级缓存singletonObjects:存放完全初始化好的单例Bean(已执行完构造、属性注入、初始化方法)。
  • 二级缓存earlySingletonObjects:存放提前暴露的、尚未完成初始化的Bean(仅执行完构造,属性还未注入)。
  • 三级缓存singletonFactories:存放ObjectFactory,用于延迟创建早期引用(避免每次getEarlyBeanReference都新建代理)。

具体流程:当A开始创建时,beforeSingletonCreation(A)将其加入singletonsCurrentlyInCreation;然后A的属性注入需要B,于是调用getBean(B);B创建时也需A,此时getSingleton(A)在一级缓存查不到,就去二级缓存查,也查不到,最后查三级缓存——singletonFactories.get(A)返回一个ObjectFactory,调用其getObject()生成A的早期引用(可能是原始对象,也可能是CGLIB代理),放入二级缓存,再返回给B。这样B就能完成属性注入,接着B初始化完成,放入一级缓存;最后A继续完成属性注入和初始化,也放入一级缓存。

实操心得:三级缓存的设计精妙之处在于,它用ObjectFactory替代了直接存原始对象,避免了“提前暴露未初始化对象”的风险。我在调试时,特意在addSingletonFactory()后加断点,观察singletonFactories里存的ObjectFactory如何被getEarlyBeanReference()调用——你会发现,这个工厂对象里封装了getEarlyBeanReference()的完整逻辑,包括是否需要生成代理、代理类型选择等。这才是读懂“三级缓存原理”的正确姿势。

3.4 第4步:BeanPostProcessor的介入时机与作用域

BeanPostProcessor是Spring最强大的扩展点之一,但新手常混淆它的两个方法postProcessBeforeInitialization()postProcessAfterInitialization()的触发时机。

ApplicationContextAwareProcessor为例(它在prepareBeanFactory()中注册):

  • 它的postProcessBeforeInitialization()在Bean的afterPropertiesSet()(InitializingBean)和init-method之前执行;
  • 它的postProcessAfterInitialization()init-method之后、Bean放入一级缓存之前执行。

AutowiredAnnotationBeanPostProcessor(处理@Autowired)的postProcessProperties(),则在populateBean()(属性注入阶段)执行,早于postProcessBeforeInitialization()

更关键的是,BeanPostProcessor本身也是Bean,它的创建和注册有严格顺序:

  1. invokeBeanFactoryPostProcessors()执行时,ConfigurationClassPostProcessor会扫描并注册所有BeanPostProcessor类型的BeanDefinition;
  2. registerBeanPostProcessors()方法(refresh第6步)会按PriorityOrdered>Ordered> 普通顺序,将这些BeanPostProcessor实例注册到beanFactorybeanPostProcessors列表中;
  3. 后续所有Bean的创建,都会按此列表顺序调用每个BeanPostProcessor的相应方法。

我在ioc-case里专门做了个实验:定义两个BeanPostProcessor,一个实现PriorityOrdered,一个实现Ordered,然后观察它们对同一个Service Bean的处理顺序。结果证实,PriorityOrderedpostProcessBeforeInitialization()一定在Ordered的同名方法之前执行——这个顺序保证了高优先级处理器(如CommonAnnotationBeanPostProcessor处理@Resource)能先于低优先级处理器(如自定义日志处理器)工作。

4. 实操过程:手把手带你跑通ioc-case,从断点设置到日志解读

现在我们进入最硬核的部分:如何真正运行、调试、理解ioc-case。这不是概念讲解,而是你打开IDEA后,每一步该做什么、为什么这么做、预期看到什么的详细指南。

4.1 环境准备:三分钟完成零配置启动

这套材料对环境要求极简:

  • JDK:17(必须,因Spring 6.1.0最低要求JDK17);
  • IDE:IntelliJ IDEA 2023.2+(免费社区版即可);
  • 构建工具:Maven 3.8.6+(自带,无需额外安装)。

操作步骤:

  1. 下载项目压缩包,解压到任意目录(如/home/user/spring-source-study);
  2. 打开IDEA,选择“Open” → 选中解压后的根目录;
  3. IDEA会自动识别为Maven项目,等待依赖下载完成(约1-2分钟);
  4. 在项目根目录下,找到ioc-case模块,展开src/main/java → com.example.ioc → IocCaseApplication.java;
  5. 右键点击IocCaseApplication.java→ “Run 'IocCaseApplication.main()'”。

注意:首次运行时,IDEA可能会提示“Maven home directory not specified”,点击“Auto-detect”即可。所有依赖(spring-framework-6.1.0、junit-jupiter等)均已声明在pom.xml中,无需手动添加。

4.2 断点设置:聚焦四个黄金断点位置

不要一上来就在refresh()打满断点。根据十年经验,这四个位置能覆盖90%的IOC核心逻辑:

断点位置类名/方法设置理由预期观察点
1AnnotationConfigApplicationContext.<init>()第1行观察容器初始化起点,确认readerscanner创建时机this.reader是否为AnnotatedBeanDefinitionReader实例
2AbstractApplicationContext.refresh()第1行理解refresh总控流程,确认12个子方法执行顺序调用栈是否显示refresh()prepareRefresh()obtainFreshBeanFactory()
3AbstractBeanFactory.doGetBean()第1行进入Bean创建核心逻辑,观察三级缓存交互getSingleton(beanName)返回值,singletonFactories是否包含当前beanName
4AbstractAutowireCapableBeanFactory.populateBean()第1行深入属性注入阶段,理解@Autowired如何工作bw.setPropertyValues(pvs)执行前后,Bean字段值变化

设置方法:

  • 在IDEA左侧行号区点击,出现红色圆点即为断点;
  • 右键断点 → “Edit Breakpoint” → 勾选“Suspend: Thread”,确保断点暂停;
  • 对于doGetBean()断点,建议右键 → “More” → 在Condition里输入beanName.equals("userService"),避免被Spring内部Bean打断。

4.3 调试实录:以UserService Bean创建为例的完整跟踪

我们以UserService为例(它被@Service标记,且依赖UserRepository)。以下是我在IDEA中实际调试的完整记录:

Step 1:触发getBean("userService")
IocCaseApplication.main()里,context.getBean(UserService.class)这一行执行后,程序停在doGetBean()断点。此时:

  • beanName = "userService"
  • getSingleton("userService")返回null(一级缓存为空);
  • isSingleton()返回true;
  • beforeSingletonCreation("userService")userService加入singletonsCurrentlyInCreation

Step 2:进入createBean()
F8单步,进入AbstractAutowireCapableBeanFactory.createBean()。这里会:

  • 调用resolveBeforeInstantiation()尝试使用InstantiationAwareBeanPostProcessor生成代理(如@Async);
  • 调用doCreateBean()执行核心创建。

Step 3:doCreateBean()中的三级缓存操作
doCreateBean()里,关键步骤:

  • instanceWrapper = createBeanInstance(beanName, mbd, args):调用构造函数创建UserService原始对象;
  • addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)):将ObjectFactory存入三级缓存;
  • populateBean(beanName, mbd, instanceWrapper):开始属性注入,此时userServiceuserRepository字段为空。

Step 4:属性注入触发userRepository创建
populateBean()调用autowireByType(),发现需要UserRepository,于是递归调用getBean("userRepository")。此时:

  • userRepositorydoGetBean()执行,同样经历getSingleton()createBean()
  • userRepository创建完成后,userServiceuserRepository字段被注入;
  • userService执行initializeBean()afterPropertiesSet()init-method);
  • 最终addSingleton("userService", userService),放入一级缓存。

实操心得:在addSingletonFactory()后,你可以直接在IDEA的“Variables”窗口里,展开this.singletonFactories,找到key为"userService"的entry,双击value(ObjectFactory),再点击右侧的“Evaluate”按钮,就能看到这个工厂对象getObject()返回的正是userService的早期引用。这种“现场验证”,比看一百遍文字描述都深刻。

4.4 日志解读:读懂Spring启动日志背后的执行逻辑

Spring默认日志级别为INFO,但ioc-case模块已预置logback-spring.xml,将org.springframework包日志设为DEBUG。运行时你会看到类似这样的输出:

[main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'appConfig' [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Creating shared instance of singleton bean 'userService' [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Returning cached instance of singleton bean 'userRepository' [main] DEBUG o.s.b.f.s.DefaultListableBeanFactory - Eagerly caching bean 'userService' to allow for resolving potential circular references

这些日志不是随便打印的,每一句都对应核心逻辑:

  • “Creating shared instance…” 表明getBean()正在创建该Bean;
  • “Returning cached instance…” 表明从一级缓存直接返回,无需创建;
  • “Eagerly caching bean…” 是addSingletonFactory()的日志,意味着三级缓存已介入。

特别注意最后一句:“Eagerly caching bean 'userService' to allow for resolving potential circular references”。这句日志出现,就证明Spring检测到了循环依赖风险,并主动将userServiceObjectFactory放入三级缓存——这是你理解三级缓存是否生效的最直接证据。

5. 常见问题与排查技巧实录:那些让我熬夜三天的坑

即使有了这套材料,你在调试过程中依然会遇到各种“看似诡异、实则必然”的问题。以下是我和团队成员在过去三年里,踩过的最典型、最高频的12个坑,每个都附带真实场景、错误现象、根本原因和一招解决法。

5.1 问题1:断点进了refresh(),但never hit doGetBean()——容器根本没创建Bean

现象:
IocCaseApplication.main()里打了断点,F9运行,程序停在refresh(),但无论怎么F8,都进不了doGetBean(),最后抛出NoSuchBeanDefinitionException

排查过程:

  1. 检查AppConfig.class是否被正确@Configuration标记;
  2. 检查@ComponentScan是否扫描到了UserService所在包;
  3. refresh()执行完后,添加临时代码:System.out.println(context.getBeanDefinitionCount());,输出为0。

根本原因:
@ComponentScan的basePackages参数写错了。比如UserServicecom.example.service包下,但@ComponentScan("com.example.dao")只扫描了dao包。Spring不会报错,只是默默不注册任何Bean。

解决方案:

  • AppConfig类上,将@ComponentScan改为@ComponentScan("com.example"),确保覆盖所有子包;
  • 或者更稳妥:删除@ComponentScan,改用@SpringBootApplication(但注意,这会引入Boot自动配置,偏离纯Framework学习目标);
  • 独家技巧:在refresh()执行后,立即调用((ConfigurableApplicationContext) context).getBeanFactory().getBeanDefinitionNames(),打印所有注册的BeanName,快速定位扫描失败。

5.2 问题2:UserService创建成功,但userRepository字段为null——@Autowired失效

现象:
UserService的构造函数和setUserRepository()方法都被调用,但最终userRepository字段仍是null。

排查过程:

  1. 检查UserRepository类是否有@Repository注解;
  2. 检查UserRepository是否被@ComponentScan扫描到;
  3. populateBean()断点处,观察bw.setPropertyValues(pvs)执行前后的字段值。

根本原因:
UserRepository类被@Service标记,而非@Repository。虽然@Service也继承@Component,但AutowiredAnnotationBeanPostProcessor默认只处理@Autowired@Value@Inject,而@Resource(JSR-250)需要CommonAnnotationBeanPostProcessor。但更常见的是:UserRepository没有被Spring管理,即它不是通过getBean()创建的,而是new UserRepository()手动创建的。

解决方案:

  • 确保UserRepository类上有@Repository@Component
  • 关键检查:在IocCaseApplication.main()里,不要写new UserService(),必须用context.getBean(UserService.class)
  • 独家技巧:在UserService@PostConstruct方法里,添加System.out.println("userRepository = " + userRepository);,如果输出null,说明注入失败;如果输出com.example.repository.UserRepository@xxxx,说明注入成功。

5.3 问题3:三级缓存日志出现,但循环依赖仍报错——BeanCreationException

现象:
日志显示Eagerly caching bean 'userService',但最终还是抛出BeanCurrentlyInCreationException

排查过程:

  1. 确认A和B确实互相依赖(A@Autowired B,B@Autowired A);
  2. 检查A和B是否都是单例(@Scope("singleton"));
  3. doGetBean()里,观察getSingleton()返回值。

根本原因:
A和B中有一个是原型(@Scope("prototype"))。Spring三级缓存只解决单例Bean之间的循环依赖。如果A是单例,B是原型,那么每次getBean("B")都会创建新实例,无法放入缓存,导致循环依赖检测失败。

解决方案:

  • 将B也改为@Scope("singleton")
  • 或者,如果业务确实需要原型,改用ObjectFactory<UserRepository>注入,而不是直接@Autowired UserRepository
  • 独家技巧:在AbstractBeanFactory.beforeSingletonCreation()里加断点,观察singletonsCurrentlyInCreation.contains("userService")是否为true,这是循环依赖检测的开关。

5.4 问题4:JDK17运行报InaccessibleObjectException——反射访问失败

现象:
populateBean()执行时,field.setAccessible(true)抛出java.lang.reflect.InaccessibleObjectException

排查过程:

  1. 确认JDK版本为17+;
  2. 查看异常堆栈,定位到Field.setAccessible()调用;
  3. 检查pom.xmlmaven-compiler-pluginsourcetarget是否为17。

根本原因:
JDK17默认启用强封装(Strong Encapsulation),禁止反射访问模块内非公开成员。Spring的ReflectionUtils.makeAccessible()方法失效。

解决方案:

  • 在IDEA的“Run Configuration” → “VM Options”里,添加:
    --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED
  • 或者,在项目根目录mvnw.cmd(Windows)或mvnw(Mac/Linux)脚本里,在MAVEN_OPTS中添加相同参数;
  • 独家技巧:在pom.xmlmaven-surefire-plugin配置中,预置argLine,确保所有测试环境都生效。

5.5 问题5:注释显示“此处应生成CGLIB代理”,但实际是JDK动态代理——代理类型判断失误

现象:
UserService实现了UserServiceInterface接口,但日志显示Creating JDK dynamic proxy,而注释说“此处应生成CGLIB代理”。

排查过程:

  1. 检查UserService是否实现了接口;
  2. 检查@EnableAspectJAutoProxy是否启用;
  3. DefaultAopProxyFactory.createAopProxy()断点,观察proxyFactory.getProxy()参数。

根本原因:
Spring AOP默认优先使用JDK动态代理(基于接口),只有当目标类没有实现接口时,才用CGLIB。UserService实现了接口,所以生成JDK代理是正确的。

解决方案:

  • 如果强制要用CGLIB,添加@EnableAspectJAutoProxy(proxyTargetClass = true)
  • 独家技巧:在AspectJAwareAdvisorAutoProxyCreator.wrapIfNecessary()里,观察proxyFactory.isProxyTargetClass()返回值,这就是代理类型决策的源头。

提示:以上5个问题,覆盖了90%的入门级调试障碍。每个问题我都附上了“独家技巧”,这些技巧来自真实项目中的血泪经验——比如那个getBeanDefinitionNames()技巧,就是我在帮一个学员排查扫描失败时,临时加的debug代码,后来发现比看日志快10倍,就固化成了标准操作。

6. 进阶延伸:从这套材料出发,你能走多远?

这套“spring源码深度解析”材料,定位非常明确:它是你Spring源码学习的

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/3 2:18:42

300MHz ARM核塞进Arduino Nano:交叉编译与烧录实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 2:16:58

fer2013数据集Python提取与可视化:CSV转图片完整指南

简介&#xff1a;fer2013数据集及配套Python提取代码&#xff0c;是一份面向面部表情识别与深度学习入门实践的完整资料包&#xff0c;适合计算机视觉学习者、算法初学者及科研人员快速上手。压缩包共2000个文件&#xff0c;以jpg表情图片、fer2013.csv原始标注、Python提取脚本…

作者头像 李华
网站建设 2026/9/3 2:16:41

反激式开关电源CCM、DCM、BCM工作模式详解与设计选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 2:14:11

Jalium UI:高性能跨平台桌面UI框架的技术解析与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/3 2:14:09

从零打造智能灯:单片机选型、PWM调光与蓝牙APP控制全解析

简介&#xff1a;本资源是一套完整的智能照明系统毕业设计与课程设计解决方案&#xff0c;面向电子类、自动化及物联网方向的本科生与实践开发者&#xff0c;解决单片机嵌入式系统中灯光控制功能集成、人机交互与软硬件协同开发等典型工程问题。压缩包共584个文件&#xff0c;7…

作者头像 李华
网站建设 2026/9/3 2:14:03

飞行模式离线部署实战:本地AI模型断网推理与自动化控制

这次我们来看一个叫AIRPLANE MODE&#xff08;飞行模式&#xff09;的工程主题。标题副句是“我将独自飞行&#xff0c;无人理会”&#xff0c;放到开发场景里其实非常贴切&#xff1a;很多服务要在完全断网、不依赖公网 API 的环境里独立跑起来&#xff0c;没有外部依赖&#…

作者头像 李华