一文吃透 Spring 框架:原理、实践与面试全解析

先问三个问题,测测你对 Spring 的掌握程度:

  1. 面试官问"Spring 怎么解决循环依赖",你能讲清楚为什么必须是三级缓存、而不是两级吗?
  2. @Transactional 加了却没回滚,你能一口气说出至少 4 种可能的原因吗?
  3. 如果让你给一个新人讲"Spring 到底做了什么",你能用两句话讲明白吗?

只要有一题卡壳,这篇文章就是为你写的。

这篇文章的目标不是让你背 API,而是先给你两个能解释 Spring 90% 行为的心智模型 ,再带着这两个模型,把 IoC、Bean 生命周期、循环依赖、AOP、事务、MVC、自动配置全部过一遍------每个知识点都标注了面试考点线上踩坑,不编造、不注水,引用的类名、注解、默认行为都对应 Spring 源码中真实存在的实现。

怎么读这篇文章(按你的层次对号入座)

  • 初学者:从头读到尾。先把两个心智模型(见 1.2 节)记牢,后面每一章都是它们的展开;
  • 面试者:重点看每章的「面试考点」,以及第 9 章的「面试官连环追问」------建议对着镜子演练;
  • 老手:直接跳第 9 章「易错点清单」查漏补缺,欢迎对照你自己的踩坑记录。

目录

  1. 开篇:Spring 到底是什么
  2. IoC 容器与依赖注入
  3. Bean 的生命周期
  4. 循环依赖与三级缓存
  5. AOP 与动态代理
  6. 声明式事务
  7. Spring MVC 请求全流程
  8. Spring Boot 与自动配置
  9. 高频面试题与易错点清单
  10. 学习建议:怎么把 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.servletjavax.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):

  1. 先按类型找候选 Bean;
  2. 找到多个 候选时依次尝试: ① 标了 @Primary 的优先; ② 按 @Priority(JSR-330)值排序取最小; ③ 与字段名/参数名匹配;
  3. 都不行 → 抛 NoUniqueBeanDefinitionException

@Autowired vs @Resource(高频面试):

@Autowired @Resource
来源 Spring 注解 JSR-250 标准注解(Java 自带)
默认策略 按类型 先按名称,再按类型(默认按字段名找,找不到按类型)
指定名称 @Qualifier name 属性

2.4 BeanFactory vs ApplicationContext

  • BeanFactory :IoC 容器的最顶层接口,懒加载getBean 时才创建实例),功能只有最基本的 Bean 管理。
  • ApplicationContextBeanFactory子接口,是实际开发中用的容器,增强能力:
    • 国际化(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 方法

必背结论

  • 初始化回调顺序:@PostConstructafterPropertiesSet()init-method
  • 销毁回调顺序:@PreDestroydestroy()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 代理)、善终(销毁);回调顺序 @PostConstructafterPropertiesSetinit-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 为例:

  1. 创建 A:实例化(构造器执行完)后,把 singletonFactories 里放入 A 的 ObjectFactory(此时还没填充属性);
  2. A 填充属性,发现需要 B → 触发创建 B;
  3. B 实例化,填充属性时发现需要 A;
  4. getSingleton("a"):一级没有 → 二级没有 → 三级有 ,取出 A 的 ObjectFactory 生成"早期引用"(如果 A 需要代理,这里就提前生成代理),放入二级缓存并移除三级缓存
  5. B 拿到 A 的引用,完成创建,进入一级缓存
  6. 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 本身默认允许)。官方态度很明确:循环依赖是设计问题,不是特性,三级缓存只是兜底。

正确的解决姿势

  1. 重构:拆出独立的依赖关系(最根本);
  2. @Lazy:延迟注入其中一方,打破启动期依赖;
  3. 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+ 默认一律 CGLIBspring.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 不回滚!

因为默认的 rollbackForRuntimeException.class, Error.classIOExceptionSQLException 这类 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_READMVCC + 间隙锁(Next-Key Lock) 实际避免了幻读,所以生产环境用默认级别即可。

6.4 事务失效场景大全(背下来)

  1. 自调用this.method() 不走代理(同 AOP 失效);
  2. 方法非 public@Transactional 只对 public 方法生效(官方文档明确,代理无法拦截 private);
  3. 异常被 catch 吞掉:事务感知不到异常,自然不回滚;
  4. checked exception 没配 rollbackFor:见 6.1;
  5. Bean 不是 Spring 管理的new 出来的、没被扫描到的类,注解不生效;
  6. 数据库引擎不支持事务:MySQL 的 MyISAM 引擎不支持事务;
  7. 多线程:Spring 事务与线程绑定(连接保存在 ThreadLocal),子线程里的事务是独立的,父线程异常回滚不了子线程的写操作;
  8. 传播行为导致提前提交 :内层方法用了 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             组件扫描(默认扫启动类所在包及子包)

自动配置机制(真实流程)

  1. @EnableAutoConfiguration 通过 AutoConfigurationImportSelector,读取 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件(Spring Boot 2.7+ 的机制 ;2.7 之前是 META-INF/spring.factories 里的 EnableAutoConfiguration 键),拿到所有自动配置类全限定名;
  2. 每个自动配置类用条件注解 判断是否生效------比如"类路径下有 DataSource 才配置数据源";
  3. 生效的自动配置类通常用 @EnableConfigurationProperties 绑定属性类(如 ServerProperties 绑定 server.*),把你的 application.yml 配置映射到 Bean 上;
  4. 自动配置类里大量使用 @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)→ 加载 ApplicationContextInitializerApplicationListener → 创建 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 动态代理;
  • 模板方法模式:JdbcTemplateRestTemplateJmsTemplate 等 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:懒加载(启动时不创建,首次使用时创建);
  • @Scopesingleton(默认)/ prototype / request / session / application / websocket
  • @Profile:按环境激活配置,激活方式 spring.profiles.active=prod(或 @ActiveProfiles 测试用)。

Q7:Bean 是怎么被扫描和注册的? @ComponentScanClassPathScanningCandidateComponentProvider 扫描类路径 → 命中 @Component(含 @Service 等)→ 组装成 BeanDefinition → 注册进 BeanFactory → 后续按生命周期创建实例。BeanDefinition 是"Bean 的说明书" ,面试提到它很加分。

9.4 易错点清单(线上踩坑实录)

  1. @Transactional 没配 rollbackFor,checked exception 静默提交 ------回滚一切异常的通用写法是 rollbackFor = Exception.class
  2. 同类自调用让事务/异步/缓存注解全部失效------拆 Bean 或注入自身;
  3. Spring Boot 2.6+ 循环依赖直接启动失败 ------升级 Boot 后老项目常见问题,别急着开 allow-circular-references,先重构;
  4. 字段注入的类没法脱离容器单测------用构造器注入;
  5. @ConfigurationProperties 属性没生效------检查前缀拼写、是否有 setter(绑定需要 setter)、类是否被 Spring 管理;
  6. @Async 不生效 ------方法自调用、没有 @EnableAsync、返回值必须是 voidFuture/CompletableFuture
  7. @RequestBody 报 415/400------没配 Jackson、请求 Content-Type 不是 JSON、DTO 字段类型与 JSON 不匹配;
  8. 配置不生效但没报错 ------配置优先级问题(命令行/环境变量覆盖了 yml),用 spring-boot-actuator/actuator/env 排查最有效;
  9. 启动报错 The dependencies of some of the beans in the application context form a cycle------Boot 2.6+ 默认禁止循环依赖,启动日志会列出是哪几个 Bean 成环,定位后按第 3 条处理;
  10. 升级 Boot 3 / Spring 6 后大量编译错误 ------javax.* 全改成 jakarta.*,第三方库也要换支持 Jakarta 的版本。

一句话带走:面试答题公式------是什么 → 为什么 → 怎么做 → 有什么坑;能答到"坑",你就赢了大多数人。


10. 学习建议:怎么把 Spring 真正吃透

10.1 按这条路径学(由浅入深)

  1. 会用:搭一个 Boot 项目,把本文 2/5/6/7 的代码都自己敲一遍并跑通;
  2. 会调:遇到问题会看堆栈、会看配置、会用 Actuator 排查;
  3. 懂原理:能不看文档画出 Bean 生命周期、MVC 流程、自动配置流程;
  4. 懂源码:按这条主线读源码(比泛读高效得多):
  • IoC 入口:AnnotationConfigApplicationContextrefresh()AbstractApplicationContext#refresh
  • Bean 创建:AbstractAutowireCapableBeanFactory#doCreateBean(生命周期主战场);
  • 循环依赖:DefaultSingletonBeanRegistry#getSingleton + 三级缓存;
  • AOP:AbstractAutoProxyCreator#wrapIfNecessary + getEarlyBeanReference
  • 事务:TransactionInterceptor#invokeTransactionAspectSupport#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",那这篇文章就算成功了。

最后回到开头那三个问题------现在,你都能答上来了吗?

相关推荐
站大爷IP2 小时前
Python 的切片把我坑惨了,原来 `[:]` 是浅拷贝,而 `copy.deepcopy` 才是我的救命稻草
后端
神奇小汤圆2 小时前
Java 万字长文:从零基础到高级应用的完整教程——把面向对象讲透
后端
孓最求完美2 小时前
实战经验:JT808/JT809 车联网高并发服务端性能优化指南
后端
shengjk13 小时前
深度拆解:从 LC-3 汇编到 Java/Rust,数组越界与硬件中断的底层真相
后端
Gorway3 小时前
理解 Spring 依赖注入:从构造器注入到集合与条件 Bean
java·后端
颜进强3 小时前
Claude Code - 26 效率三件套:cc-switch 切模型 · codeburn 算成本 · claude-hud 看状态
前端·后端·ai编程
掘金者阿豪3 小时前
时序数据库选型被写入峰值带偏了?金仓超表让我少加了不少班
后端
大黄评测3 小时前
jQuery 遍历方法 each 实战:循环处理列表数据
后端