TIP💡 面试复习优先级与考察权重指引:
- 🔥【超高频 · ★★★】(出现率 85% ~ 95%):大厂/中小厂核心淘汰八股,面试必背、必须能流利答出底层机制与常见追问。
- ⚡【高频 · ★★☆】(出现率 60% ~ 80%):高频常规考察题,需讲清核心流程、架构设计与底层优势。
- 📌【中频 / 低频 · ★☆☆】(出现率 30% ~ 50%):进阶与次要八股,侧重考察微服务架构下的取舍与底层原理。
⚡【高频 · ★★☆】Spring 框架中的单例 Bean 是线程安全的吗?
一、这题在问什么
面试官其实是在考你 3 个点:
- Spring Bean 默认作用域是不是单例
- 单例对象在多线程环境下会不会有并发问题
- 什么情况下安全,什么情况下不安全,怎么解决
重点不是只回答“安全”或“不安全”,而是要说清楚: 单例 Bean 本身不天然线程安全,是否线程安全取决于这个 Bean 里有没有共享的可变状态。
二、怎么理解
你可以这样记:
1)Spring 默认是单例
Spring 里 Bean 默认作用域是 singleton,也就是整个容器里通常只有一个对象实例。
2)单例 Bean 会被多个线程共享
在 Web 项目里,很多请求会同时进来,这些请求线程拿到的可能都是同一个 Bean 对象。
3)真正的问题在“成员变量”
如果这个 Bean:
- 没有可变成员变量
- 或者是无状态 Bean
那一般就是安全的。
如果这个 Bean 里定义了:
- 会被修改的成员变量
- 并且多个线程都会访问它
那就可能出现线程安全问题。
三、标准回答思路
面试时推荐你按这个顺序答:
第一步:先给结论
Spring 中的单例 Bean 不一定线程安全。
第二步:解释原因
因为 Spring 默认单例 Bean 会被多个线程共享,如果 Bean 中存在共享的可变成员变量,就可能发生线程安全问题。
第三步:补充什么时候安全
如果 Bean 是无状态的,只包含方法局部变量,不保存请求过程中的数据,那么通常是线程安全的。
第四步:说解决办法
常见做法有:
- 尽量设计成无状态 Bean
- 把共享变量改成局部变量
- 必要时加同步控制(如
synchronized、锁) - 或者改成 prototype / request 等合适作用域
但一般核心思路还是:尽量不要在单例 Bean 里存可变状态
四、背诵版本(短版 · 适合 30 秒至 1 分钟回答)
Spring 中的单例 Bean 不一定是线程安全的。因为 Spring 默认 Bean 是 singleton,多个线程会共享同一个 Bean 实例。如果这个 Bean 是无状态的,只使用方法内部局部变量,那通常就是线程安全的;但如果 Bean 中定义了可变的成员变量,并且在并发场景下被多个线程共同访问和修改,就会出现线程安全问题。解决方式一般是尽量把 Bean 设计成无状态,避免使用共享可变成员变量,必要时再通过加锁或调整作用域来处理。
五、背诵版本(长版 · 适合 1 至 2 分钟展开回答)
Spring 框架中的单例 Bean 默认不是绝对线程安全的,是否线程安全要看这个 Bean 的实现方式。因为 Spring 默认作用域是 singleton,也就是整个 IOC 容器中通常只有一个 Bean 实例。在 Web 应用中,多个请求线程会同时访问这个单例对象。如果这个 Bean 本身是无状态的,比如只做业务处理,不保存中间数据,只使用方法中的局部变量,那么一般不会有线程安全问题。
但是如果这个 Bean 中包含可变的成员变量,比如把请求数据、计算中间结果放到成员变量里,那么多个线程同时访问时就可能发生数据覆盖、脏读等并发问题。所以在实际开发中,我们通常会把 Service、Controller 这类单例 Bean 设计成无状态,避免定义可变成员变量。如果确实需要共享状态,可以考虑加锁、使用线程安全的数据结构,或者调整 Bean 的作用域,但最推荐的方式还是无状态设计。
六、面试加分点
你可以再补一句,面试官会觉得你理解比较到位:
- 加分表达 1:所以不能简单地说“单例 Bean 线程安全”或者“单例 Bean 线程不安全”,更准确地说,是“默认单例 Bean 会被多线程共享,是否安全取决于是否存在共享的可变状态”。
- 加分表达 2:像 Spring 里的 Service 一般推荐设计成无状态,这也是为什么大多数情况下虽然是单例,但实际使用中问题不大。
七、超简洁记忆版
你可以直接背这一句:
Spring 单例 Bean 不一定线程安全;无状态则通常安全,有共享可变成员变量则不安全。
🔥【超高频 · ★★★】什么是 AOP 及其在项目中的实际应用?
1. 什么是 AOP(核心概念与原理)
-
理解: AOP 就是面向切面编程,主要用来把和核心业务无关、但很多地方都会重复出现的公共功能抽出来统一处理,比如日志、事务、权限校验、异常处理等。这样可以在不改业务代码的前提下,对方法进行增强,减少重复代码,降低耦合。
-
背诵版本: AOP 就是面向切面编程,它的核心思想是把日志、事务、权限校验这些和核心业务无关、但会被多个模块重复使用的公共功能抽取出来,统一进行处理。这样可以在不修改原有业务代码的情况下,对目标方法进行前置、后置或环绕增强,从而实现代码复用、降低耦合。
2. 你们项目中有没有使用到 AOP?(实战落地场景)
-
理解: 这题不能只回答“用过”,要说出具体场景。项目里最常见的 AOP 使用场景有:
- 记录操作日志
- Spring 声明式事务管理
- 缓存切面处理(如防缓存穿透/击穿)
- 统一权限校验与 Token 解析
- 接口耗时统计与慢调用报警
你可以重点说“操作日志”最容易展开:通过切点表达式找到需要拦截的方法,再通过环绕通知拿到方法信息、请求参数、执行结果、执行时间等,最后把这些日志异步保存到数据库或 Elasticsearch 系统里。
-
背诵版本: 我们项目中使用过 AOP,比较典型的场景是操作日志记录和 Spring 事务管理。比如在操作日志这块,我会通过切点表达式拦截带有自定义注解的业务方法,然后在环绕通知中获取方法名、类名、请求参数、执行结果以及执行时间等信息,最后统一记录到数据库或者日志系统中。这样做的好处是不用在每个业务方法里重复写日志代码,维护起来也更方便。
🔥【超高频 · ★★★】Spring 事务实现原理与失效场景深度剖析
1. Spring 中的事务是如何实现的?
-
理解: Spring 事务本质上就是基于 AOP + 代理模式 实现的。 当你在方法上加了
@Transactional,Spring 会为这个类生成一个代理对象。调用目标方法时,实际上先走代理逻辑:- 方法执行前:开启事务
- 方法正常结束:提交事务
- 方法出现异常:根据规则回滚事务 所以本质不是业务代码自己控制事务,而是 Spring 在方法前后帮你做了增强。
-
背诵版本: Spring 中的事务本质上是通过 AOP 和代理模式实现的。通常我们在方法上加上
@Transactional注解后,Spring 会为目标对象生成代理对象。当调用这个方法时,实际上会先进入代理逻辑,在方法执行前开启事务,方法执行成功后提交事务,如果执行过程中出现异常,就根据事务的回滚规则进行回滚。所以 Spring 事务本质上就是对目标方法前后进行增强,把事务控制逻辑统一交给框架处理。
2. Spring 事务失效的经典场景有哪些?如何解决?
在实际项目与面试中,最常见的几种导致事务失效的场景包括:
-
方法内部自调用(
this.xxx())导致代理失效:同一个类内的一个非事务方法调用了带@Transactional的事务方法,由于走的是this本地对象调用而非代理对象,AOP 增强逻辑根本不会被触发。- 解决办法:将方法拆到另一个 Service 中注入调用,或者通过
AopContext.currentProxy()获取当前代理对象调用。
- 解决办法:将方法拆到另一个 Service 中注入调用,或者通过
-
方法内部捕获并吞掉了异常(try-catch):如果方法内部捕获了异常且没有向外抛出,Spring 无法感知到异常,事务就不会回滚。

- 抛出的是检查型异常(Checked Exception)未配置回滚:Spring 声明式事务默认只在遇到
RuntimeException和Error时回滚。如果方法抛出的是检查异常(如SQLException、IOException),默认不会触发回滚。- 解决办法:显式配置
@Transactional(rollbackFor = Exception.class)。
- 解决办法:显式配置

- 事务注解的方法不是
public修饰:Spring 事务底层基于 AOP 动态代理,默认要求目标方法必须是public,否则不会为其生成代理拦截逻辑。

- 背诵总结版本:
Spring 事务失效常见场景主要有四种:
第一,同类方法内部自调用(
this.xxx()),没有经过 Spring 生成的 AOP 代理对象; 第二,方法内部把异常 try-catch 自己捕获处理了,没有继续抛出,导致 Spring 感知不到异常; 第三,抛出的是检查异常(Checked Exception),Spring 默认只对运行时异常回滚,必须显式指定rollbackFor = Exception.class; 第四,事务方法不是 public 的,因为 AOP 代理默认只拦截 public 方法。
🔥【超高频 · ★★★】Spring Bean 的生命周期


1. 核心生命周期完整流程(八步法)
Spring 中 Bean 的完整生命周期主要包括以下 8 大核心阶段:
- 解析定义(BeanDefinition):Spring 解析配置类、XML 或
@Component注解,生成BeanDefinition描述元数据。 - 实例化(Instantiation):通过反射调用构造函数在堆内存中为 Bean 分配内存空间(此时仅是裸对象,属性均为 null)。
- 属性注入(Populate Properties):为 Bean 填充属性,完成依赖注入(如处理
@Autowired、@Resource或 setter 注入)。 - Aware 接口回调(Aware Callbacks):如果 Bean 实现了 Aware 接口,Spring 注入容器感知资源:
BeanNameAware:传入当前 Bean 的名称;BeanFactoryAware:传入所属的 BeanFactory;ApplicationContextAware:传入当前的 ApplicationContext 容器上下文。
- BeanPostProcessor 前置处理:执行所有已注册后置处理器的
postProcessBeforeInitialization()方法(如@PostConstruct注解解析)。 - 初始化(Initialization):执行初始化逻辑:
- 先执行
InitializingBean的afterPropertiesSet()方法; - 再执行自定义配置的
init-method(如@Bean(initMethod = "init"))。
- 先执行
- BeanPostProcessor 后置处理:执行所有已注册后置处理器的
postProcessAfterInitialization()方法(AOP 动态代理通常在这一步生成!)。 - 注册销毁回调与容器销毁(Destruction):Bean 正常投入业务使用;当容器关闭时:
- 执行
@PreDestroy标注的方法; - 执行
DisposableBean接口的destroy()方法; - 执行自定义配置的
destroy-method。
- 执行
2. 核心追问 1:BeanPostProcessor 是做什么的?
- 面试官真实意图:考察你对 Spring 扩展机制(SPI)与底层拦截原理的理解。
- 核心解答:
BeanPostProcessor(Bean 后置处理器)是 Spring 框架中最核心的全局扩展点。它允许开发者在 Bean 初始化之前(Before) 和 初始化之后(After) 插入自定义逻辑,甚至替换或增强 Bean 实例。 - 两大典型职责:
- 注解增强与属性篡改:例如处理
@PostConstruct、@PreDestroy、@Value注解解析; - 生成动态代理对象(AOP 核心机制):Spring 的
AbstractAutoProxyCreator(如事务切面、自定义切面)本质上就是一个BeanPostProcessor,在它的后置处理阶段将目标原始对象包装为 JDK 或 CGLIB 代理对象。
- 注解增强与属性篡改:例如处理
3. 核心追问 2:AOP 动态代理是在哪一步生成的?
- 正常流程:
在 Bean 生命周期的**第 7 步 —— 初始化之后(BeanPostProcessor 后置处理器)**生成。具体的实现类是
AnnotationAwareAspectJAutoProxyCreator的postProcessAfterInitialization()方法。 - 特殊例外(循环依赖场景):
如果发生了 循环依赖(例如 A 依赖 B,B 依赖 A),且 A 需要被 AOP 代理,此时 AOP 代理生成会被提前到实例化之后、依赖注入之前,由三级缓存中的
ObjectFactory(调用SmartInstantiationAwareBeanPostProcessor.getEarlyBeanReference())提前生成代理对象,以保证注入给 B 的是 A 的代理对象而非原始对象。
4. 面试 1 分钟高分背诵版
Spring Bean 的生命周期大致可以总结为四个核心阶段:实例化、属性赋值、初始化和销毁。
展开来说共有八个关键步骤:首先解析 BeanDefinition;然后通过构造函数完成实例化;接着进行属性依赖注入;随后处理 Aware 接口回调传入容器上下文;之后进入 BeanPostProcessor 的前置处理;再调用 InitializingBean 和自定义的 init-method 进行初始化;随后在 BeanPostProcessor 的后置处理中,Spring 会在此阶段为 Bean 生成 AOP 动态代理对象;最后在容器关闭时触发 DisposableBean 的销毁回调。
这里最核心的扩展点是 BeanPostProcessor,它负责在初始化前后对 Bean 进行拦截增强,比如注解解析和 AOP 代理包装。通常 AOP 代理是在初始化后的 BeanPostProcessor 中生成的;但如果存在循环依赖,则会由三级缓存提前在实例化后生成代理对象,以保证注入引用的唯一一致性。
🔥【超高频 · ★★★】Spring 循环依赖底层原理与三级缓存深度剖析
1. 什么是循环依赖与三级缓存结构?
循环依赖发生在两个或两个以上的 Bean 互相持有对方(例如 A 依赖 B,B 依赖 A),形成闭环依赖。Spring 框架允许单例 Bean 的循环依赖存在,并通过三级缓存机制进行解决:
- 一级缓存(
singletonObjects):单例池,存放已经完全初始化完成的可用 Bean 对象。 - 二级缓存(
earlySingletonObjects):存放尚未完成完整生命周期的早期原始/代理 Bean 对象(半成品)。 - 三级缓存(
singletonFactories):存放ObjectFactory函数式工厂,用于延迟生成早期 Bean 的代理引用。
2. 三级缓存解决循环引用的完整执行流程



为什么一级缓存无法解决?
单靠一级缓存无法区分“完整初始化好的 Bean”与“刚实例化的半成品 Bean”,并发场景下其他线程会拿到未完成初始化的脏对象。

为什么二级缓存还不够?必须要有第三级缓存?
假设 Bean A 需要被 AOP 动态代理。按照 Bean 的标准生命周期,代理对象是在**初始化之后(BeanPostProcessor 后置处理器)**才生成的。如果只使用二级缓存,在实例化阶段就必须把原始对象暴露出去,导致后续注入的是原始对象,而最终容器生成的是代理对象,引发对象引用不一致问题。



-
核心理解: 三级缓存的作用,就是在发生循环依赖时,通过工厂方法决定提前生成并暴露代理对象还是原始对象,从而既打破了循环死锁,又保证了全局对象引用的唯一一致性。
-
背诵版本: Spring 解决循环依赖主要依赖三级缓存机制。一级缓存存放的是完整初始化后的单例 Bean,二级缓存存放的是提前暴露的早期半成品 Bean,三级缓存存放的是
ObjectFactory工厂。当 A 依赖 B、B 又依赖 A 时,Spring 在实例化 A 后,会先将 A 的工厂放入三级缓存;当 B 实例化并需要注入 A 时,可以通过三级缓存提前获取 A 的早期引用(若 A 有 AOP 则生成代理对象),并放入二级缓存。从而打破循环依赖。三级缓存的核心价值就在于完美兼顾了循环依赖与 AOP 动态代理对象的一致性。
3. 构造方法出现循环依赖的解决方案

-
理解: 构造方法循环依赖,Spring 默认解决不了。因为构造器注入发生在 Bean 实例化最开始的阶段,A 实例化时必须先拿到 B,B 实例化时又必须先拿到 A,此时两个 Bean 均未分配堆内存,Spring 无法像属性注入那样先实例化半成品再提前暴露,所以三级缓存无能为力。
-
解决策略:
- 改用 Setter /
@Autowired属性注入; - 在构造器参数上添加
@Lazy注解:通过动态代理生成代理垫片对象注入,等到真正调用方法时再触发真实对象的实例化。
- 改用 Setter /
-
背诵版本: 构造器循环依赖 Spring 默认无法解决,因为构造器发生在实例化阶段,两个 Bean 都未实例化完成,三级缓存无法提前暴露对象。解决方案通常是将构造器注入改为属性注入(Setter 或
@Autowired),或者在其中一个构造参数前加上@Lazy注解,利用延迟代理打破实例化死锁。
⚡【高频 · ★★☆】Spring MVC 核心执行流程与原理




Spring MVC 的核心请求处理链路:
- 用户发送请求:到达前端控制器
DispatcherServlet。 - 查找处理器:
DispatcherServlet调用HandlerMapping处理器映射器,根据请求 URL 找到对应的Handler(Controller)及拦截器链。 - 适配执行器:
DispatcherServlet调用HandlerAdapter处理器适配器。 - 执行业务方法:
HandlerAdapter经过参数解析与数据绑定后,执行 Controller 业务方法。 - 处理返回结果:
- 若返回视图:Controller 返回
ModelAndView,由ViewResolver视图解析器解析为具体视图并渲染呈现; - 若前后端分离(常见):使用
@ResponseBody注解,通过HttpMessageConverter(如 Jackson)直接序列化为 JSON 字符串写入 HTTP 响应体。
- 若返回视图:Controller 返回
⚡【高频 · ★★☆】介绍 Spring Boot 及其优势(解决的痛点)
一、这题在问什么
面试官其实在考察三个层次:
- 你知不知道 Spring Boot 是做什么的;
- 你能不能说出它相比传统 Spring 解决了哪些开发痛点;
- 你是不是停留在概念表面,还是理解了自动配置与起步依赖的关键设计。
二、怎么理解 Spring Boot
Spring Boot 是基于 Spring 框架的快速开发脚手架,用于简化 Spring 应用的搭建、配置和部署。
在传统 Spring 开发中,痛点非常明显:
- 需要编写大量繁琐的 XML 或 JavaConfig 配置;
- 各种组件(数据源、事务、Web、缓存)需要手动装配;
- Maven 依赖版本复杂,容易出现 Jar 包依赖冲突;
- 部署需要单独安装并配置外部 Tomcat 服务器。
Spring Boot 便是为解决上述痛点而生,其核心特点归纳为四大支柱:
- 约定大于配置(Convention over Configuration):提供成熟的默认配置规则,开发者无需从零配置。
- 自动配置(Auto-Configuration):根据引入的依赖自动装配对应的 Bean 与环境(“带什么原料,就配什么厨房”)。
- 起步依赖(Starter):聚合常用依赖并统一锁定版本,避免依赖地狱。
- 内嵌 Web 容器:内置 Tomcat / Jetty / Undertow,支持
java -jar一键启动与云原生容器化部署。
三、背诵版本(1 分钟速记)
Spring Boot 是基于 Spring 的快速开发框架,核心目标是简化 Spring 应用的搭建、配置和部署。 相比传统 Spring,它最核心的优势有三点: 第一是约定大于配置与自动配置,它会根据项目中引入的 Starter 依赖自动装配相关的 Bean 和运行环境,省去了大量繁琐的 XML 和配置类编写; 第二是起步依赖(Starter),它把常用模块依赖进行了标准化整合并统一管理版本,彻底解决了 Jar 包冲突; 第三是内嵌 Tomcat 容器,支持直接打包为可执行 Jar 包运行,极大提升了开发调试和运维部署效率。 总结来说,Spring Boot 不是替代 Spring,而是站在 Spring 的肩膀上,让开发者将核心精力聚焦在业务逻辑开发上。
🔥【超高频 · ★★★】Spring Boot 自动配置底层原理深度剖析


Spring Boot 自动配置的核心驱动注解是启动类上的 @SpringBootApplication,它是一个复合注解,底层由 3 个核心注解组成:
@SpringBootConfiguration:标记当前类为配置类(本质上就是@Configuration)。@ComponentScan:指定组件扫描范围(默认扫描主启动类所在包及其子包)。@EnableAutoConfiguration(核心驱动):- 内部通过
@Import(AutoConfigurationImportSelector.class)导入自动配置选择器; - 读取类路径下的配置文件:
- Spring Boot 2.x:读取
META-INF/spring.factories; - Spring Boot 3.x:读取
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports;
- Spring Boot 2.x:读取
- 获取所有候选自动配置类的全限定类名列表;
- 条件注解按需装配:结合
@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty等条件注解进行过滤,只有满足特定依赖或配置时,对应的 AutoConfiguration 类才会被激活并注入 IOC 容器。
- 内部通过
⚡【高频 · ★★☆】Spring Boot 完整启动原理与 refresh 核心链路
一、启动流程核心脉络
Spring Boot 的启动入口是 SpringApplication.run(Main.class, args),整体过程可以拆解为 7 大关键步骤:
- 创建
SpringApplication实例:推断应用类型(Reactive / Servlet / NONE)、加载配置的ApplicationContextInitializer与ApplicationListener、推断主启动类。 - 准备运行环境
Environment:加载系统环境变量、JVM 参数、application.yml/properties配置文件及命令行入参。 - 创建容器
ApplicationContext:Web 项目默认创建AnnotationConfigServletWebServerApplicationContext。 - 准备容器上下文
prepareContext:应用环境变量、执行 Initializer 初始化器。 - 执行容器刷新
refresh()(最核心):- 解析配置类与组件扫描;
- 注册 BeanDefinition;
- 实例化并初始化所有非懒加载单例 Bean;
- 执行 BeanPostProcessor;
- 创建并启动内嵌 Web 容器(如 Tomcat)。
- 刷新后扩展回调:执行
CommandLineRunner与ApplicationRunner。 - 返回容器:启动完成,对外提供 HTTP/RPC 服务。
二、面试背诵版本
Spring Boot 启动过程以 SpringApplication.run() 为入口,核心流程可总结为:准备环境 创建容器 刷新容器(refresh) 启动内嵌服务器。
其中最核心的步骤是容器的 refresh() 方法。在这一阶段,Spring 完成了配置类解析、组件扫描、BeanDefinition 注册、单例 Bean 的实例化与依赖注入,并在 Web 环境下同步启动内嵌的 Tomcat 服务器。
⚡【高频 · ★★☆】Spring / Spring MVC / Spring Boot 常见核心注解总结
1. Spring 常见核心注解

- 组件声明:
@Component、@Service、@Repository、@Controller。 - 依赖注入:
@Autowired:Spring 原生,默认按类型(byType)注入,可搭配@Qualifier按名称匹配;@Resource:JDK 规范,默认按名称(byName)注入,找不到再按类型匹配。
- 配置与作用域:
@Configuration、@Bean、@ComponentScan、@Scope。 - 切面编程(AOP):
@Aspect、@Pointcut、@Before、@After、@Around。
2. Spring MVC 常见注解

- 路径映射:
@RequestMapping、@GetMapping、@PostMapping、@PutMapping、@DeleteMapping。 - 请求参数绑定:
@RequestParam:接收 Query 查询参数或表单数据;@PathVariable:接收 URL 路径变量(如/users/{id});@RequestBody:接收 HTTP 请求体中的 JSON 数据并反序列化为 Java 对象;@RequestHeader:获取 HTTP 请求头信息。
- 响应体转换:
@ResponseBody(将返回值转为 JSON 格式写入响应体)。
3. Spring Boot 常见注解

- 主启动注解:
@SpringBootApplication(组合@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan)。 - REST 控制器:
@RestController(组合@Controller+@ResponseBody)。 - 属性配置绑定:
@ConfigurationProperties、@Value。 - 条件装配注解:
@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty等。
📌【中频 · ★★☆】MyBatis 底层核心执行流程

MyBatis 的底层执行流程围绕四大核心对象展开:
- 读取配置:解析
mybatis-config.xml以及 Mapper XML,构建Configuration对象。 - 构建工厂:通过
SqlSessionFactoryBuilder构建线程安全的SqlSessionFactory会话工厂。 - 创建会话:通过工厂创建
SqlSession(线程不安全,单次数据库交互上下文)。 - 执行器调度:
SqlSession将具体的 CRUD 任务委托给Executor执行器(如SimpleExecutor、ReuseExecutor)。 - 核心组件协作:
StatementHandler:负责创建 JDBCStatement并与数据库交互;ParameterHandler:负责将 Java 入参映射设置到 SQL 预编译语句中;ResultSetHandler:负责将 JDBC 查询结果集映射并组装为 Java 实体对象。
📌【低频 · ★☆☆】MyBatis 是否支持延迟加载及其底层原理
1. 延迟加载概念与使用场景

- 概念:延迟加载(Lazy Loading)即按需加载。在执行主查询时,仅查询主表数据,关联表(如
<association>或<collection>)的数据不会立即查询;只有在业务代码中真正调用关联对象的 getter 方法(如user.getOrders())时,MyBatis 才会触发并发送第二条 SQL 查询关联数据。 - 配置方式:在配置文件中开启
lazyLoadingEnabled=true。 - 现状与冷门原因:在现代主流微服务与高并发互联网开发规范中,通常严禁使用级联联合查询与延迟加载,普遍推荐采用“单表单查 + 业务层在应用内存中组装”,以避免不可控的 N+1 查询风暴与数据库长事务,因此该题在实战面试中考频较低。
2. 延迟加载的底层原理(CGLIB 动态代理)

延迟加载底层基于 CGLIB 动态代理 实现:
- 主查询执行完毕后,MyBatis 创建并返回目标实体类的 CGLIB 代理对象。
- 当调用代理对象的 getter 方法(例如
getOrders())时,进入代理拦截器的intercept()方法。 - 拦截器检测到关联属性尚未加载(当前为 null),立即根据预存的 MappedStatement 触发执行关联 SQL。
- 获得查询结果后,通过反射将属性填充到对象中,并返回给调用方。
📌【中频 · ★★☆】MyBatis 一级缓存与二级缓存机制

1. 一级缓存与二级缓存对比



| 缓存类型 | 作用域 | 默认开关 | 存储实现 | 适用与失效机制 |
|---|---|---|---|---|
| 一级缓存 | SqlSession 级别(本地会话) | 默认开启 | 基于 PerpetualCache 的 HashMap 本地内存 | 同一 SqlSession 内相同 SQL 会命中缓存;当该 Session 执行了 commit、close 或任何 insert/update/delete 增删改操作后,缓存立即清空。 |
| 二级缓存 | Namespace / Mapper 级别(跨会话全局) | 需手动开启 | 基于 PerpetualCache,支持装饰器(LRU、FIFO) | 多个 SqlSession 共享同一个命名空间的缓存;必须等 SqlSession commit() 提交后,数据才会从暂存区刷入二级缓存。 |
2. 缓存清理时机与微服务实践思考
- 何时清理数据: 当当前作用域(一级缓存对应的 SqlSession 或二级缓存对应的 Namespace)内执行了任何新增(INSERT)、修改(UPDATE)、删除(DELETE)并提交后,该作用域下的所有 SELECT 查询缓存将被全量清空,以防止脏读。
- 为什么微服务分布式架构中严禁开启二级缓存? MyBatis 二级缓存是单 JVM 进程内的本地缓存。在多实例分布式或微服务集群环境下,节点 A 执行了更新操作只会刷新本地缓存,节点 B 无法感知并依然返回旧的本地二级缓存数据,极易产生严重的分布式数据脏读。因此现代架构中通常直接禁用 MyBatis 二级缓存,统一使用 Redis 分布式缓存实现集中式数据一致性管理。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时