先问三个问题,测测你对 Spring 的掌握程度:
- 面试官问"Spring 怎么解决循环依赖",你能讲清楚为什么必须是三级缓存、而不是两级吗?
@Transactional加了却没回滚,你能一口气说出至少 4 种可能的原因吗?- 如果让你给一个新人讲"Spring 到底做了什么",你能用两句话讲明白吗?
只要有一题卡壳,这篇文章就是为你写的。
这篇文章的目标不是让你背 API,而是先给你两个能解释 Spring 90% 行为的心智模型 ,再带着这两个模型,把 IoC、Bean 生命周期、循环依赖、AOP、事务、MVC、自动配置全部过一遍------每个知识点都标注了面试考点 和线上踩坑,不编造、不注水,引用的类名、注解、默认行为都对应 Spring 源码中真实存在的实现。
怎么读这篇文章(按你的层次对号入座)
- 初学者:从头读到尾。先把两个心智模型(见 1.2 节)记牢,后面每一章都是它们的展开;
- 面试者:重点看每章的「面试考点」,以及第 9 章的「面试官连环追问」------建议对着镜子演练;
- 老手:直接跳第 9 章「易错点清单」查漏补缺,欢迎对照你自己的踩坑记录。
目录
- 开篇:Spring 到底是什么
- IoC 容器与依赖注入
- Bean 的生命周期
- 循环依赖与三级缓存
- AOP 与动态代理
- 声明式事务
- Spring MVC 请求全流程
- Spring Boot 与自动配置
- 高频面试题与易错点清单
- 学习建议:怎么把 Spring 真正吃透
1. 开篇:Spring 到底是什么
1.1 一句话定义
Spring Framework 是一个开源的 Java 应用开发框架,它提供的核心能力只有两个:
- IoC(Inversion of Control,控制反转)容器 :对象的创建、组装、生命周期管理交给容器,而不是在代码里
new。 - AOP(Aspect Oriented Programming,面向切面编程) :把日志、事务、安全等横切逻辑从业务代码中剥离出来。
你听到的"Spring 全家桶"里,Spring Framework 是地基,其余都是建在地基上的:
arduino
Spring Framework ← 本文主角(IoC + AOP + MVC + 事务......)
├── Spring Boot 基于 Framework 的"约定优于配置"快速开发框架
├── Spring Cloud 微服务全套(配置中心、网关、注册中心......)
├── Spring Data 统一的数据访问抽象(JPA、Redis、MongoDB......)
├── Spring Security 认证授权
├── Spring Batch 批处理
└── Spring Integration 企业集成(消息、管道)
面试考点 :区分"Spring Framework"和"Spring Boot"是基本功。Spring Boot 不是替代 Spring,而是让 Spring 用起来更简单(自动配置 + 内嵌服务器 + 起步依赖),底层跑的还是 Spring Framework 的 IoC/AOP 机制。
1.2 两个心智模型:记住这两条线,Spring 就通了一半
Spring 功能很多,但真正称得上"核心机制"的只有两个,其他一切知识点都是从它们长出来的:
java
┌──────────────────────────────────────────────────────┐
│ Spring 两大核心机制 │
│ │
│ 模型一:容器模型(IoC) 模型二:代理模型(AOP) │
│ ├─ 依赖注入 ├─ 动态代理(JDK/CGLIB) │
│ ├─ Bean 生命周期 ├─ @Transactional │
│ ├─ 循环依赖与三级缓存 ├─ @Async / @Cacheable │
│ ├─ 作用域 / 懒加载 ├─ 事务失效 / 自调用失效 │
│ └─ BeanFactory/Context └─ 通知与切点 │
│ │
│ Spring MVC = 容器 + 代理在 Web 层的应用 │
│ Spring Boot = 把容器和代理"自动化" │
└──────────────────────────────────────────────────────┘
- 模型一(容器模型) :一切皆 Bean,Bean 由容器创建、装配、管理生命周期。你不再
new对象,而是声明"我要什么"。 - 模型二(代理模型) :
@Transactional、@Async、@Cacheable、自定义切面......注解只是声明,真正干活的是容器在 Bean 外面包的那层代理。
记住两句话:一切皆 Bean,Bean 由容器管理;一切注解能力,能力靠代理执行。 后面 8 章,每一章都是这两条线的展开。
1.3 版本演进(面试常考年份)
| 版本 | 时间 | 关键变化 |
|---|---|---|
| Spring 1.0 | 2004 | 框架诞生,源自 Rod Johnson 的《Expert One-on-One J2EE Design and Development》 |
| Spring 2.0 | 2006 | 引入 AOP 命名空间、@Transactional 等注解的雏形 |
| Spring 3.0 | 2009 | JavaConfig(@Configuration、@Bean)、SpEL、REST 支持(@ComponentScan 在 3.1 引入) |
| Spring 4.0 | 2013 | @RestController、Java 8 支持 |
| Spring 5.0 | 2017 | JDK 8 基线,引入响应式 WebFlux、@Nullable、Kotlin 支持 |
| Spring 6.0 | 2022 | JDK 17 基线,javax.* → jakarta.* 命名空间迁移,支持 AOT 编译 |
| Spring Boot 1.x | 2014 起 | 快速开发脚手架 |
| Spring Boot 2.x | 2018 起 | 基于 Spring 5;2.0 默认 CGLIB 代理;2.6 默认禁止循环依赖;2.7 引入 AutoConfiguration.imports |
| Spring Boot 3.x | 2022 起 | 基于 Spring 6,JDK 17+,jakarta 命名空间,GraalVM Native Image 支持 |
面试考点 :javax.* → jakarta.* 是 Spring 6 / Boot 3 最容易被问到的迁移点------因为 Spring 6 基于 Jakarta EE 9,javax.servlet、javax.persistence 等包整体改名 jakarta.*,老项目升级 Boot 3 第一件事就是改 import。
一句话带走 :Spring 的本质只有两件事------用容器管好对象,用代理管好横切逻辑。
2. IoC 容器与依赖注入
2.1 概念:什么是 IoC,什么是 DI
- IoC(控制反转) :创建对象、组装依赖的"控制权"从业务代码反转给容器。以前是
A a = new A(); a.setB(new B());,现在是容器替你完成这些,你只需要声明"我要什么"。 - DI(依赖注入) :IoC 的一种实现方式。容器在创建 Bean 时,把它的依赖"注入"进去。
IoC 是思想,DI 是手段。面试时先答思想,再落到 DI 的三种方式,就完整了。
2.2 三种依赖注入方式(实践对比)
kotlin
// ① 构造器注入 ------ 官方推荐
@Service
public class OrderService {
private final OrderRepository orderRepository; // final,不可变
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
// Spring 4.3+ 单构造器自动注入,连 @Autowired 都可以省
// ② Setter 注入 ------ 适合可选依赖
@Service
public class OrderService {
private OrderRepository orderRepository;
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
// ③ 字段注入 ------ 最简洁,但社区普遍反对
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
}
| 方式 | 优点 | 缺点 | 结论 |
|---|---|---|---|
| 构造器注入 | 依赖不可变、必填依赖不会为 null、易单测 | 依赖多时构造器冗长;构造器循环依赖无法解决 | 强制依赖用它 |
| Setter 注入 | 可重赋值、适合可选依赖 | 依赖可变、可能漏配 | 可选依赖用它 |
| 字段注入 | 代码最少 | 无法脱离容器 new 出来测试、与容器强耦合、掩盖依赖关系 | 不推荐(Spring 官方文档也只正面介绍前两种) |
面试考点 :"为什么官方推荐构造器注入?"答案四连:① final 不可变 ② 必填依赖编译期/启动期就能发现缺失 ③ 单测时直接 new 传 mock,不依赖容器 ④ 避免循环依赖里"看起来能跑、改着就炸"的隐患(构造器循环依赖启动即报错,字段循环依赖偷偷能跑)。
2.3 常用注解与装配规则
- 注册 Bean :
@Component(通用)、@Service、@Repository、@Controller(语义化子注解,本质都是@Component)、@Configuration(配置类)、@Bean(方法级,显式声明)。 - 注入 :
@Autowired(Spring 自带)、@Resource(JSR-250)、@Inject(JSR-330)。 - 限定 :
@Qualifier("name")按名称限定、@Primary标记首选、@Value注入配置值。
@Autowired 的解析顺序(真实机制,DefaultListableBeanFactory#doResolveDependency):
- 先按类型找候选 Bean;
- 找到多个 候选时依次尝试: ① 标了
@Primary的优先; ② 按@Priority(JSR-330)值排序取最小; ③ 与字段名/参数名匹配; - 都不行 → 抛
NoUniqueBeanDefinitionException。
@Autowired vs @Resource(高频面试):
| @Autowired | @Resource | |
|---|---|---|
| 来源 | Spring 注解 | JSR-250 标准注解(Java 自带) |
| 默认策略 | 按类型 | 先按名称,再按类型(默认按字段名找,找不到按类型) |
| 指定名称 | @Qualifier | name 属性 |
2.4 BeanFactory vs ApplicationContext
- BeanFactory :IoC 容器的最顶层接口,懒加载 (
getBean时才创建实例),功能只有最基本的 Bean 管理。 - ApplicationContext :
BeanFactory的子接口,是实际开发中用的容器,增强能力: -
- 国际化(
MessageSource); - 事件发布(
ApplicationEventPublisher); - 资源加载(
ResourceLoader,统一 file/classpath/url 前缀); - 环境抽象(
Environment+Profile); - 自动注册
BeanFactoryPostProcessor/BeanPostProcessor(这个最关键,注解驱动全靠它); - 启动时立即初始化所有 singleton Bean(能尽早暴露配置错误)。
- 国际化(
常见实现类:AnnotationConfigApplicationContext(注解驱动)、ClassPathXmlApplicationContext(XML 时代)、AnnotationConfigServletWebServerApplicationContext(Spring Boot Web 实际用的)。
面试考点 :"Spring 启动时 Bean 是立即创建还是懒加载?"------singleton 默认在容器刷新时立即创建(eager),@Lazy 可改为懒加载;prototype 永远是 getBean 时才创建。
一句话带走:对象不再主动找依赖,而是等待依赖被给到------这就是控制反转;强制依赖用构造器,可选依赖用 setter,别用字段注入。
3. Bean 的生命周期
这是 Spring 面试出现率最高的题之一,必须能按顺序背下来并讲清每个阶段的时机。它也是"容器模型"最完整的展开。
3.1 完整生命周期(singleton Bean)
perl
① 实例化(构造器 / 工厂方法)
② 属性填充(populateBean:注入 @Autowired / @Value / XML 属性)
③ Aware 回调:
BeanNameAware → BeanClassLoaderAware → BeanFactoryAware
(容器是 ApplicationContext 时,再经 ApplicationContextAwareProcessor 调用
EnvironmentAware → EmbeddedValueResolverAware → ResourceLoaderAware →
ApplicationEventPublisherAware → MessageSourceAware → ApplicationContextAware)
④ BeanPostProcessor#postProcessBeforeInitialization
(@PostConstruct 在此阶段被 CommonAnnotationBeanPostProcessor 调用)
⑤ InitializingBean#afterPropertiesSet
⑥ 自定义 init 方法(@Bean(initMethod = "xxx") / XML init-method)
⑦ BeanPostProcessor#postProcessAfterInitialization
(★ AOP 代理就是在这里由 AbstractAutoProxyCreator 生成的)
⑧ Bean 就绪,开始被使用
⑨ 容器关闭时销毁:
@PreDestroy → DisposableBean#destroy → 自定义 destroy 方法
必背结论:
- 初始化回调顺序:
@PostConstruct→afterPropertiesSet()→init-method; - 销毁回调顺序:
@PreDestroy→destroy()→destroy-method; - AOP 代理在
postProcessAfterInitialization阶段生成------所以代理是"初始化完成之后"才套上的; - prototype Bean 不执行销毁回调 :容器创建后就不追踪它了,
destroy不会调用(这点常被忽略)。
3.2 实践:怎么参与生命周期
参与生命周期回调有两条路:接口/注解 (适合自己写的类)和 @Bean 属性(适合第三方类,没法改它的源码)。
typescript
// 方式一:自己写的类 ------ 注解 + 接口
@Component
public class MyBean implements InitializingBean, DisposableBean {
public MyBean() { System.out.println("① 实例化"); }
@Autowired
private SomeDep dep; // ② 属性填充
@PostConstruct
public void postConstruct() { System.out.println("④ @PostConstruct"); }
@Override
public void afterPropertiesSet() { System.out.println("⑤ afterPropertiesSet"); }
@PreDestroy
public void preDestroy() { System.out.println("⑦ @PreDestroy"); }
@Override
public void destroy() { System.out.println("⑧ destroy"); }
}
// 方式二:第三方类 ------ 在 @Configuration 里用 @Bean 指定 init/destroy 方法
@Configuration
public class AppConfig {
@Bean(initMethod = "customInit", destroyMethod = "customDestroy")
public ConnectionPool pool() {
return new ConnectionPool(); // ConnectionPool 是别人写的,不能加注解
}
}
注意:@Bean(initMethod = ...) 只能出现在 @Bean 方法上(即 @Configuration 类里),它修饰的是"被创建的那个对象的方法",不是配置方法本身。
面试考点 :把上面的顺序口述一遍 + 补一句"AOP 代理在最后一步生成",基本就是满分答案。再加一个加分项:BeanPostProcessor 本身也是 Bean,但它优先于普通 Bean 被实例化 (容器会先找出所有 BeanPostProcessor 并实例化),否则普通 Bean 的注解没人处理。
一句话带走 :Bean 的一生------出生(实例化)、安家(属性填充)、认亲(Aware)、成人礼(初始化回调)、上岗(套上 AOP 代理)、善终(销毁);回调顺序
@PostConstruct→afterPropertiesSet→init-method。
4. 循环依赖与三级缓存
这是"容器模型"里最精彩的一章,也是面试深挖的必经之路。
4.1 什么是循环依赖
less
@Service
public class A {
@Autowired private B b; // A 依赖 B
}
@Service
public class B {
@Autowired private A a; // B 依赖 A
}
Spring 用三级缓存解决了 singleton Bean 的 setter/字段注入循环依赖。
4.2 三级缓存到底是什么
三个 Map,都在 DefaultSingletonBeanRegistry 里(真实字段名):
| 级别 | 缓存 | 存放内容 |
|---|---|---|
| 一级 | singletonObjects | 创建完成的成品 Bean |
| 二级 | earlySingletonObjects | 提前暴露的半成品(已实例化、未完成属性填充/初始化) |
| 三级 | singletonFactories | ObjectFactory 工厂,用来生成"早期引用"(处理 AOP 代理) |
4.3 一个类比,帮你建立直觉(理解后请回到术语)
把 Bean 的创建想象成**"下单---预售---到货"**:
- 一级缓存 = 现货仓库(创建完成的 Bean);
- 二级缓存 = 预售登记处(提前暴露的半成品占位);
- 三级缓存 = 生产计划单 (
ObjectFactory:只有别人来"要货"时,才现场决定交付原始对象还是代理)。
A 和 B 互相依赖时发生的事:A 先下生产计划单(进三级缓存)→ B 来要 A 时 A 还没完工,于是按计划单先给 B 一份"当前版本的 A"(同时登记到二级缓存)→ B 完工入仓 → A 补完工序,最终也入仓。
关键点:B 拿到的 A 和最终入仓的 A 是同一个对象(或其代理),所以不会"货不对板"。
⚠️ 类比只帮你建立直觉,面试时请用术语回答 :singletonObjects / earlySingletonObjects / singletonFactories + getEarlyBeanReference。
4.4 解决过程(背下来)
以 A → B → A 为例:
- 创建 A:实例化(构造器执行完)后,把
singletonFactories里放入 A 的ObjectFactory(此时还没填充属性); - A 填充属性,发现需要 B → 触发创建 B;
- B 实例化,填充属性时发现需要 A;
getSingleton("a"):一级没有 → 二级没有 → 三级有 ,取出 A 的ObjectFactory生成"早期引用"(如果 A 需要代理,这里就提前生成代理),放入二级缓存并移除三级缓存;- B 拿到 A 的引用,完成创建,进入一级缓存;
- A 继续完成属性填充(拿到 B)、初始化,最后进入一级缓存。
4.5 为什么是三级缓存而不是两级
这是面试里最刁钻的追问。答案核心:三级缓存是为了配合 AOP 代理。
- 如果只有两级(直接暴露原始对象):A 被 B 引用时,暴露的是原始对象 ;等 A 完成初始化,
postProcessAfterInitialization再生成代理------此时 B 持有的还是原始对象,代理就失效了(B 调 A 的方法不会走增强逻辑)。 - 三级缓存存的是
ObjectFactory,只有在真正发生循环依赖(别人来取)时才调用 ,通过SmartInstantiationAwareBeanPostProcessor#getEarlyBeanReference提前生成代理,保证 B 拿到的就是最终会被使用的那个代理对象。 - 同时,没有循环依赖的 Bean 不会走提前代理 ,仍然在
postProcessAfterInitialization正常代理------避免所有 Bean 无谓地提前代理。
一句话总结:二级缓存管"提前暴露",三级缓存管"需要时再决定暴露原始对象还是代理" 。
4.6 哪些情况解决不了
| 场景 | 结果 |
|---|---|
| 构造器循环依赖 | ❌ 报 BeanCurrentlyInCreationException(实例化阶段就要依赖,Bean 还没进三级缓存) |
| prototype 循环依赖 | ❌ 直接抛异常(prototype 不做提前暴露) |
| 单例 setter/字段循环依赖 | ✅ 三级缓存解决 |
Spring Boot 2.6+ 默认禁止循环依赖 :spring.main.allow-circular-references 默认为 false,启动就报错(Spring Framework 本身默认允许)。官方态度很明确:循环依赖是设计问题,不是特性,三级缓存只是兜底。
正确的解决姿势:
- 重构:拆出独立的依赖关系(最根本);
@Lazy:延迟注入其中一方,打破启动期依赖;- 用
ObjectProvider/ApplicationContext.getBean延迟获取。
一句话带走:先给"预售",再补货------三级缓存让互相依赖的 Bean 也能同时诞生;面试讲流程用术语,讲直觉用"预售"类比。
5. AOP 与动态代理
这一章是"代理模型"的本体。记住一个总纲:你写的每个 @Aspect、每个 @Transactional、每个 @Async,容器都会在 Bean 外包一层代理,注解只是"说明书",代理才是"执行者"。
5.1 核心原理:代理
Spring AOP 的实现机制是运行时动态代理,两种:
- JDK 动态代理 :
java.lang.reflect.Proxy,要求目标实现接口,基于接口生成代理类; - CGLIB :基于继承 ,用字节码技术(ASM)生成目标类的子类,不要求接口。
选择规则:
- 传统 Spring 默认:目标有接口 → JDK 代理;没接口 → CGLIB;
- Spring Boot 2.0+ 默认一律 CGLIB (
spring.aop.proxy-target-class=true)。
面试考点 :JDK 代理基于接口,所以目标类必须实现接口才有效 ;CGLIB 基于继承,所以 final 方法/类无法被代理(不能覆写)。这个结论是"自调用为什么失效"题的底层原因之一。
5.2 通知类型与切点
less
@Aspect
@Component
public class LogAspect {
// 切点:com.example.service 包下所有 public 方法
@Pointcut("execution(public * com.example.service..*(..))")
public void serviceMethods() {}
@Before("serviceMethods()")// 方法执行前
public void before(JoinPoint jp) { /* ... */ }
@AfterReturning("serviceMethods()", returning = "ret")// 正常返回后
public void afterReturning(Object ret) { /* ... */ }
@AfterThrowing("serviceMethods()", throwing = "ex")// 抛异常后
public void afterThrowing(Exception ex) { /* ... */ }
@After("serviceMethods()")// 无论是否异常,finally 语义
public void after() { /* ... */ }
@Around("serviceMethods()")// 全权控制,可替代上面全部
public Object around(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // 放行目标方法
System.out.println("耗时 " + (System.currentTimeMillis() - start) + "ms");
return result;
}
}
通知执行顺序(Spring 5.2.7+ / Boot 2.2.7+ 修正后的语义) :
- 正常路径:
@Around前半 →@Before→ 目标方法 →@AfterReturning→@After→@Around后半; - 异常路径:
@Around前半 →@Before→ 目标方法抛异常 →@AfterThrowing→@After。
多个切面按 @Order 排序(值越小越靠外),呈洋葱模型:外层切面先 @Before、后 @After。
常用切点表达式:
| 表达式 | 含义 |
|---|---|
| execution(public * com.example.service..*(..)) | 匹配包及子包下所有 public 方法 |
| within(com.example.service.UserService) | 匹配类内所有方法 |
| @annotation(com.example.annotation.Log) | 匹配标了 @Log 的方法 |
| args(java.lang.String) | 匹配单 String 参数的方法 |
| bean(userService) | 按 Bean 名匹配 |
5.3 失效场景与解决方案(面试必考)
| 场景 | 原因 | 解决 |
|---|---|---|
| 同类内部自调用 this.method() | 走的是原始对象,不是代理对象 | 注入自身、AopContext.currentProxy()、拆到别的 Bean |
| 方法非 public | JDK 代理只能代理接口方法;CGLIB 无法覆写 private | 改成 public |
| final 方法 / final 类 | CGLIB 无法继承/覆写 | 去掉 final |
| static 方法 | 不参与实例代理 | 不用 static |
| 对象不是 Spring 管理的 Bean(new 出来的) | 没有代理 | 交给容器管理 |
自调用失效是最常见的线上 bug,附代码:
typescript
@Service
public class UserService {
public void outer() {
this.inner(); // ❌ 走原始对象,AOP/事务全部失效
}
@Transactional
public void inner() { /* ... */ }
}
解决方案:
typescript
// 方案 1:注入自己(Spring 支持,循环依赖用三级缓存兜底)
@Service
public class UserService {
@Autowired private UserService self;
public void outer() { self.inner(); }
}
// 方案 2:AOP 上下文获取当前代理(需开启 exposeProxy)
// @EnableAspectJAutoProxy(exposeProxy = true)
public void outer() {
((UserService) AopContext.currentProxy()).inner();
}
// 方案 3(最推荐):把 inner 拆到独立的 Bean 里
面试考点 :@Transactional 自调用失效、@Async 自调用失效,本质都是同一个问题------注解靠代理生效,自调用没走代理。答完代理原理再答三个解决方案,这题就稳了。
一句话带走:注解只是声明,代理才是执行;自调用失效,是因为你绕过了代理,直接调了原始对象。
6. 声明式事务
事务是"代理模型"最典型的应用,也是线上问题最多的地方。
6.1 原理
@Transactional 基于 AOP 实现:TransactionInterceptor 切面在目标方法执行前开启事务、执行后提交/回滚。核心默认行为:
默认只对 RuntimeException 和 Error 回滚,checked exception 不回滚!
因为默认的 rollbackFor 是 RuntimeException.class, Error.class。IOException、SQLException 这类 checked exception 抛出来,事务不会回滚------这是最容易踩的坑。
java
@Transactional
public void transfer(Account from, Account to, BigDecimal amount) throws IOException {
// ...
throw new IOException("网络抖动"); // ❌ 事务不回滚!
}
@Transactional(rollbackFor = Exception.class)// ✅ 正确姿势
public void transfer(Account from, Account to, BigDecimal amount) throws IOException {
// ...
}
6.2 传播行为(Propagation,必背)
| 传播行为 | 语义 |
|---|---|
| REQUIRED(默认) | 有事务则加入,无则新建 |
| REQUIRES_NEW | 挂起当前事务,新建一个独立事务(内外互不影响) |
| SUPPORTS | 有则加入,无则以非事务方式执行 |
| NOT_SUPPORTED | 挂起当前事务,非事务执行 |
| MANDATORY | 必须有事务,否则抛 IllegalTransactionStateException |
| NEVER | 必须无事务,否则抛异常 |
| NESTED | 基于 Savepoint 的嵌套事务:内层回滚只回滚到 Savepoint;外层整体回滚时内层随之回滚 |
实践要点:
REQUIRES_NEW是"内部事务异常不能连累外部"的经典解法(比如把某个失败操作记入日志表,即使业务失败日志也要入库);NESTED依赖底层数据库/Savepoint 支持,DataSourceTransactionManager支持,但要注意不是所有环境都支持。
6.3 隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ_UNCOMMITTED | 可能 | 可能 | 可能 |
| READ_COMMITTED | 否 | 可能 | 可能 |
| REPEATABLE_READ | 否 | 否 | 标准定义允许;InnoDB 靠 MVCC + 间隙锁实际避免了 |
| SERIALIZABLE | 否 | 否 | 否 |
@Transactional(isolation = Isolation.READ_COMMITTED);- 默认
DEFAULT= 用数据库默认值(MySQL InnoDB 默认REPEATABLE_READ,Oracle 默认READ_COMMITTED); - 面试加分点:MySQL InnoDB 的
REPEATABLE_READ靠 MVCC + 间隙锁(Next-Key Lock) 实际避免了幻读,所以生产环境用默认级别即可。
6.4 事务失效场景大全(背下来)
- 自调用 :
this.method()不走代理(同 AOP 失效); - 方法非 public :
@Transactional只对 public 方法生效(官方文档明确,代理无法拦截 private); - 异常被 catch 吞掉:事务感知不到异常,自然不回滚;
- checked exception 没配
rollbackFor:见 6.1; - Bean 不是 Spring 管理的 :
new出来的、没被扫描到的类,注解不生效; - 数据库引擎不支持事务:MySQL 的 MyISAM 引擎不支持事务;
- 多线程:Spring 事务与线程绑定(连接保存在 ThreadLocal),子线程里的事务是独立的,父线程异常回滚不了子线程的写操作;
- 传播行为导致提前提交 :内层方法用了
REQUIRES_NEW,外层 catch 住异常,内层已独立提交。
typescript
@Transactional
public void outer() {
try {
inner(); // inner 是 REQUIRES_NEW,异常时已独立回滚/提交
} catch (Exception e) {
// 外层 catch 住 → 外层事务不会回滚
}
}
6.5 编程式事务(实践推荐)
声明式事务解决不了"方法内部分逻辑要事务"的场景,用 TransactionTemplate:
java
@Service
public class OrderService {
private final TransactionTemplate txTemplate; // Boot 自动注入
public void save() {
txTemplate.executeWithoutResult(status -> {
// 这里面的代码在一个事务里
// 想回滚:throw new RuntimeException(); 或 status.setRollbackOnly();
});
}
}
面试考点 :能说清楚"声明式事务 = AOP + TransactionInterceptor + PlatformTransactionManager(如 DataSourceTransactionManager)",并主动补充"readOnly=true 只是给底层框架的提示(JDBC setReadOnly、Hibernate 调 FlushMode),不是数据库层的强制只读",就超过 90% 的候选人。
一句话带走 :事务默认只回滚运行时异常;checked exception 需要你显式告诉它
rollbackFor------它不会读心术。
7. Spring MVC 请求全流程
MVC 是"容器模型 + 代理模型"在 Web 层的应用:Controller 是 Bean,拦截器、@Transactional 都是代理。
7.1 核心:DispatcherServlet
Spring MVC 采用前端控制器(Front Controller)模式 ,所有请求先进 DispatcherServlet,再委派给各组件。
sql
请求 → DispatcherServlet
→ HandlerMapping 找对应的 Handler 方法 + 拦截器链
→ HandlerAdapter 适配并调用 Controller 方法(RequestMappingHandlerAdapter)
├── 拦截器 preHandle
├── 执行 Controller 方法(参数由 HandlerMethodArgumentResolver 解析)
├── 返回 ModelAndView 或 @ResponseBody 结果
└── 拦截器 postHandle
→ HandlerExceptionResolver 异常统一处理(@ControllerAdvice + @ExceptionHandler)
→ 渲染:ViewResolver + View 渲染页面
或 HttpMessageConverter 把对象写回 JSON(Jackson)
→ 拦截器 afterCompletion
→ 响应返回
7.2 核心组件一句话记忆
| 组件 | 职责 |
|---|---|
| DispatcherServlet | 前端控制器,总调度 |
| HandlerMapping | 请求 URL → Handler(@RequestMapping 方法),如 RequestMappingHandlerMapping |
| HandlerAdapter | 适配不同 Handler 类型并调用,如 RequestMappingHandlerAdapter |
| HandlerMethodArgumentResolver | 解析方法参数(@RequestParam、@PathVariable、@RequestBody...) |
| HandlerMethodReturnValueHandler | 处理返回值(@ResponseBody 走 RequestResponseBodyMethodProcessor) |
| HttpMessageConverter | 消息转换:JSON ↔ 对象(Jackson 的 MappingJackson2HttpMessageConverter) |
| HandlerExceptionResolver | 统一异常处理,如 ExceptionHandlerExceptionResolver |
| ViewResolver | 逻辑视图名 → View 对象 |
7.3 常用注解速查
| 注解 | 用途 |
|---|---|
| @RestController | = @Controller + @ResponseBody(Spring 4.0+) |
| @GetMapping / @PostMapping / ... | @RequestMapping 的语义化简写(4.3+) |
| @RequestParam | 查询参数(默认必填,required=false 可选) |
| @PathVariable | 路径变量,配 {id} 占位符 |
| @RequestBody | 请求体 → 对象(走 HttpMessageConverter) |
| @RequestHeader / @CookieValue | 请求头 / Cookie |
| @ModelAttribute | 表单/查询参数绑定到对象 |
| @ControllerAdvice + @ExceptionHandler | 全局异常处理 |
less
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{id}")
public Order get(@PathVariable Long id) { // /api/orders/100
return orderService.findById(id);
}
@PostMapping
public Order create(@RequestBody CreateOrderRequest req) { // JSON 反序列化
return orderService.create(req);
}
}
// 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result handleBusiness(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}
}
7.4 Filter vs Interceptor(高频对比题)
| 维度 | Filter | HandlerInterceptor |
|---|---|---|
| 所属层次 | Servlet 容器层(Servlet 规范) | Spring MVC 层 |
| 执行时机 | 在 DispatcherServlet 之前 | 在 Handler 方法前后 |
| 能拿到什么 | HttpServletRequest/Response | HandlerMethod(方法、注解、参数) |
| 生命周期方法 | init / doFilter / destroy(一次调用) | preHandle / postHandle / afterCompletion(三段式) |
| 典型用途 | 编码、跨域(CORS)、压缩、通用日志 | 登录鉴权(能拿到方法上的注解)、权限校验、接口耗时统计 |
执行顺序:Filter → DispatcherServlet → Interceptor.preHandle → Controller → Interceptor.postHandle → 视图渲染 → Interceptor.afterCompletion。
实践提示:需要"看方法上有没有某个注解"这类需求必须用 Interceptor(Filter 拿不到 HandlerMethod);需要"在请求到达 MVC 之前就拦截"(如全局 CORS、防爬)用 Filter。
一句话带走 :一个
DispatcherServlet,八个组件,搞定请求的一生;Filter 在容器层拦,Interceptor 在 MVC 层拦。
8. Spring Boot 与自动配置
8.1 为什么有 Spring Boot
Spring 框架本身配置繁琐(XML/注解一堆样板),Spring Boot 的定位是约定优于配置(Convention over Configuration) :起步依赖(starter)+ 自动配置 + 内嵌 Web 服务器,让你"写一个 main 方法就能跑起一个 Web 服务"。
8.2 启动类与自动配置原理(必考)
typescript
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication = 三个注解的组合:
less
@SpringBootApplication
├── @SpringBootConfiguration = @Configuration(本身是配置类)
├── @EnableAutoConfiguration 开启自动配置 ← 核心
└── @ComponentScan 组件扫描(默认扫启动类所在包及子包)
自动配置机制(真实流程) :
@EnableAutoConfiguration通过AutoConfigurationImportSelector,读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(Spring Boot 2.7+ 的机制 ;2.7 之前是META-INF/spring.factories里的EnableAutoConfiguration键),拿到所有自动配置类全限定名;- 每个自动配置类用条件注解 判断是否生效------比如"类路径下有
DataSource才配置数据源"; - 生效的自动配置类通常用
@EnableConfigurationProperties绑定属性类(如ServerProperties绑定server.*),把你的application.yml配置映射到 Bean 上; - 自动配置类里大量使用
@ConditionalOnMissingBean------用户自己定义的 Bean 优先,自动配置只在"你没有定义"时才兜底。
常见条件注解:
| 注解 | 生效条件 |
|---|---|
| @ConditionalOnClass / @ConditionalOnMissingClass | 类路径上有没有某个类 |
| @ConditionalOnBean / @ConditionalOnMissingBean | 容器里有没有某个 Bean |
| @ConditionalOnProperty | 某个配置项是否存在/等于某值 |
| @ConditionalOnWebApplication / @ConditionalOnNotWebApplication | 是不是 Web 应用 |
| @ConditionalOnExpression | SpEL 表达式为 true |
面试答题模板 (背下来):"自动配置 = @EnableAutoConfiguration 读取 AutoConfiguration.imports 中的候选配置类 + 条件注解按需装配 + @ConditionalOnMissingBean 保证用户自定义优先 + 属性绑定读取配置文件。"
8.3 自定义 Starter(实践)
一个自定义 starter 四件套:
kotlin
my-starter/
├── MyAutoConfiguration 自动配置类(@Configuration + @ConditionalOnClass...)
├── MyProperties 属性类(@ConfigurationProperties(prefix = "my"))
└── META-INF/spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports
← 里面写一行 MyAutoConfiguration 的全限定名
加上 spring-boot-autoconfigure-processor 依赖生成配置元数据,让 IDE 有属性提示。
8.4 外部化配置优先级
Spring Boot 支持命令行参数、环境变量、application.yml 等多种配置来源。简化版优先级(高 → 低,完整版以官方文档为准):
ini
命令行参数 --server.port=8081
> Java System properties(-Dserver.port=8081)
> OS 环境变量(SERVER_PORT=8081)
> application-{profile}.yml(profile 环境配置)
> application.yml(默认配置)
> @PropertySource 引入的文件
> SpringApplication.setDefaultProperties 设置的默认值
实践提示:
- 命令行 > 配置文件,所以部署时用
--server.port=8081可以覆盖 yml; SPRING_APPLICATION_JSON环境变量可以注入一整个 JSON 配置;- 注意 Spring Boot 2.4 重构了配置加载机制(
ConfigDataEnvironment) ,老文章里的优先级顺序(profile 文件 vs 默认文件谁高)在 2.4 前后有差异,面试被问到就答"以官方文档 2.4+ 的顺序为准"。
8.5 Spring Boot 启动过程(一句话版)
SpringApplication.run() → 推断 Web 应用类型(Servlet / Reactive / 非 Web)→ 加载 ApplicationContextInitializer 和 ApplicationListener → 创建 ApplicationContext(Web 场景是 AnnotationConfigServletWebServerApplicationContext)→ 执行 refresh() 刷新容器(这里走完整个 IoC 生命周期)→ 启动内嵌 Web 服务器 (默认 Tomcat)→ 执行 ApplicationRunner / CommandLineRunner。
8.6 Fat Jar 结构
Spring Boot 可执行 Jar 的特殊结构:
bash
my-app.jar
├── BOOT-INF/classes/ ← 你的 class 和 resources
├── BOOT-INF/lib/ ← 所有依赖 jar
└── org/springframework/boot/loader/ ← 自定义类加载器(JarLauncher)
普通 java -jar 靠的是 BOOT-INF/lib 里嵌套 jar 的加载(由 JarLauncher 通过自定义类加载器完成),所以把 Boot 的 jar 直接解压出来按普通 jar 跑是起不来的------JVM 的 classpath 机制不认嵌套 jar。
一句话带走:约定优于配置------你写业务,框架猜配置;猜错了用条件注解兜底,你想覆盖就定义自己的 Bean。
9. 高频面试题与易错点清单
9.1 先记住一个公式
是什么 → 为什么 → 怎么做 → 有什么坑 / 怎么解决
以"循环依赖"为例:
- 是什么:A 依赖 B、B 依赖 A;
- 为什么能解决:三级缓存提前暴露;
- 怎么做:讲 6 步流程(第 4.4 节);
- 坑与解决:构造器不行、Boot 2.6 默认禁止、正解是重构 /
@Lazy。
按这个公式答,既完整又有深度,还能自然引出追问。
9.2 面试官连环追问:从 IoC 一路挖到源码(建议对着镜子演练)
Q1:说说你对 Spring IoC 的理解? A:IoC 是控制反转,创建对象和组装依赖的控制权从代码反转给容器;DI 是它的实现方式,有三种:构造器、setter、字段注入......(第 2 章)
Q2:那 Bean 的完整创建流程是怎样的? A:实例化 → 属性填充 → Aware 回调 → BeanPostProcessor 前置处理(含
@PostConstruct)→afterPropertiesSet→ init-method → BeanPostProcessor 后置处理(AOP 代理在这里生成)→ 使用 → 销毁。(第 3 章)Q3:AOP 代理具体是什么时候生成的? A:
postProcessAfterInitialization阶段,由AbstractAutoProxyCreator完成;没有循环依赖时不会提前代理。Q4:两个 Bean 互相依赖怎么办? A:三级缓存提前暴露。一级放成品、二级放提前暴露的半成品、三级放
ObjectFactory,只有别人来取时才调用工厂生成早期引用。(第 4 章)Q5:为什么是三级缓存而不是两级? A:为了配合 AOP 代理------如果只有两级,提前暴露的是原始对象,代理生成后 B 拿到的引用就失效了;三级缓存存工厂,在需要时通过
getEarlyBeanReference提前生成代理,保证 B 拿到的就是最终代理,且没有循环依赖的 Bean 不会提前代理。(第 4.5 节)Q6:构造器注入的循环依赖为什么解决不了? A:实例化阶段就需要依赖,而 Bean 进入三级缓存发生在构造器执行完之后,此时拿不到任何早期引用,直接抛
BeanCurrentlyInCreationException。Q7:你们项目里怎么避免循环依赖? A:重构拆类、
@Lazy延迟注入;Spring Boot 2.6+ 默认直接禁止循环依赖,报错会列出成环的 Bean。(第 4.6 节)
这套追问几乎是"深挖 Spring"的标准路径,能答到最后,说明你已经不是背答案,而是真懂。
9.3 概念题速查
Q1:Spring 中用了哪些设计模式?举出实例。
- 工厂模式:
BeanFactory创建 Bean; - 单例模式:Bean 默认 singleton(注意是容器级单例,不是 JVM 级);
- 代理模式:AOP 动态代理;
- 模板方法模式:
JdbcTemplate、RestTemplate、JmsTemplate等 Template 家族; - 观察者模式:
ApplicationEvent+ApplicationListener事件机制; - 适配器模式:
HandlerAdapter(适配不同 Handler)、AdvisorAdapter(适配不同通知); - 策略模式:
Resource资源访问、多种HandlerMapping实现; - 委派模式:
DispatcherServlet把请求委派给HandlerMapping; - 责任链模式:
HandlerInterceptor链、BeanPostProcessor链、HandlerExceptionResolver链。
Q2:@Component 和 @Bean 的区别?
@Component作用于类 ,靠@ComponentScan类路径扫描自动注册;@Bean作用于方法,显式声明,适合:第三方库的类(无法加注解)、需要复杂初始化逻辑、需要条件创建的场景。
Q3:@Configuration 和 @Component 里放 @Bean 有什么区别?(进阶)
@Configuration类是 full 模式 :被 CGLIB 代理,@Bean方法间调用返回同一个实例(保证单例语义);- 普通
@Component类是 lite 模式 :不被代理,@Bean方法间调用每次新建实例; - 所以要把
@Bean定义在@Configuration类里,否则可能拿到多个实例。
Q4:singleton Bean 线程安全吗?
- 容器只保证"单例唯一",不保证线程安全;
- 无状态的 service/dao 天然安全;有状态(成员变量可变)需要自己加同步、或用 prototype、或用 ThreadLocal。
Q5:@Value 和 @ConfigurationProperties 的区别?
@Value:单个属性注入,支持 SpEL(@Value("${a.b}")),用法灵活但散;@ConfigurationProperties:类型安全绑定 ,把一组配置绑定到对象(@ConfigurationProperties(prefix="my")+@EnableConfigurationProperties或@Component),支持校验(@Validated)、复杂类型(List/Map)、IDE 提示。推荐一组相关配置用后者。
Q6:@Lazy、@Scope、@Profile 各是什么?
@Lazy:懒加载(启动时不创建,首次使用时创建);@Scope:singleton(默认)/prototype/request/session/application/websocket;@Profile:按环境激活配置,激活方式spring.profiles.active=prod(或@ActiveProfiles测试用)。
Q7:Bean 是怎么被扫描和注册的? @ComponentScan → ClassPathScanningCandidateComponentProvider 扫描类路径 → 命中 @Component(含 @Service 等)→ 组装成 BeanDefinition → 注册进 BeanFactory → 后续按生命周期创建实例。BeanDefinition 是"Bean 的说明书" ,面试提到它很加分。
9.4 易错点清单(线上踩坑实录)
@Transactional没配rollbackFor,checked exception 静默提交 ------回滚一切异常的通用写法是rollbackFor = Exception.class;- 同类自调用让事务/异步/缓存注解全部失效------拆 Bean 或注入自身;
- Spring Boot 2.6+ 循环依赖直接启动失败 ------升级 Boot 后老项目常见问题,别急着开
allow-circular-references,先重构; - 字段注入的类没法脱离容器单测------用构造器注入;
@ConfigurationProperties属性没生效------检查前缀拼写、是否有 setter(绑定需要 setter)、类是否被 Spring 管理;@Async不生效 ------方法自调用、没有@EnableAsync、返回值必须是void或Future/CompletableFuture;@RequestBody报 415/400------没配 Jackson、请求 Content-Type 不是 JSON、DTO 字段类型与 JSON 不匹配;- 配置不生效但没报错 ------配置优先级问题(命令行/环境变量覆盖了 yml),用
spring-boot-actuator的/actuator/env排查最有效; - 启动报错
The dependencies of some of the beans in the application context form a cycle------Boot 2.6+ 默认禁止循环依赖,启动日志会列出是哪几个 Bean 成环,定位后按第 3 条处理; - 升级 Boot 3 / Spring 6 后大量编译错误 ------
javax.*全改成jakarta.*,第三方库也要换支持 Jakarta 的版本。
一句话带走:面试答题公式------是什么 → 为什么 → 怎么做 → 有什么坑;能答到"坑",你就赢了大多数人。
10. 学习建议:怎么把 Spring 真正吃透
10.1 按这条路径学(由浅入深)
- 会用:搭一个 Boot 项目,把本文 2/5/6/7 的代码都自己敲一遍并跑通;
- 会调:遇到问题会看堆栈、会看配置、会用 Actuator 排查;
- 懂原理:能不看文档画出 Bean 生命周期、MVC 流程、自动配置流程;
- 懂源码:按这条主线读源码(比泛读高效得多):
- IoC 入口:
AnnotationConfigApplicationContext→refresh()→AbstractApplicationContext#refresh; - Bean 创建:
AbstractAutowireCapableBeanFactory#doCreateBean(生命周期主战场); - 循环依赖:
DefaultSingletonBeanRegistry#getSingleton+ 三级缓存; - AOP:
AbstractAutoProxyCreator#wrapIfNecessary+getEarlyBeanReference; - 事务:
TransactionInterceptor#invoke→TransactionAspectSupport#invokeWithinTransaction; - MVC:
DispatcherServlet#doDispatch; - 自动配置:
AutoConfigurationImportSelector。
10.2 资源
- 一手资料:Spring 官方文档(spring.io/projects/sp...)永远是最高优先级,网上二手文章有错漏时以官方文档为准;
- 代码实践:用官方 Spring Initializr(start.spring.io)生成项目,边改边看;
- 源码阅读:本地 clone Spring Framework 仓库,用 IDE 跳转阅读,配合断点调试观察三级缓存的实时变化。
写在最后
Spring 已经二十多岁了。从 XML 到注解,从单体到微服务,从 JVM 到 GraalVM Native,技术栈换了一茬又一茬,但它的核心思想几乎没有变:用容器管好对象,用代理管好横切逻辑。
这也是学框架的正确姿势------不要追着 API 跑,先建立模型。模型是骨架,API 是血肉;骨架对了,血肉可以随时补。
如果你读完这篇文章,能向别人讲清楚"三级缓存为什么是三级""自调用为什么失效""事务默认为什么不回滚 checked exception",那这篇文章就算成功了。
最后回到开头那三个问题------现在,你都能答上来了吗?