← 全部材料 Spring
SPRING · 15 个高频考点 · Boot 3.x

Spring 的题都指向同一个机制

IoC、AOP、事务失效、循环依赖,看起来是四个话题,其实都在回答同一个问题:调用有没有经过代理对象。把这条线抓住,一大半问题不用背。

01先看代理在不在任何"注解不生效"的问题,第一步都是确认拿到的是代理还是原始对象。
02用可跑的例子验证循环依赖和 AOP 失效这两节用纯 Java 模拟,输出就是答案。
03对照 Nest 迁移DI、拦截器、异常过滤器一一对应,你已经会的可以直接搬过来。
1

IoC 到底解决了什么问题★

你已经会了,只是名字不一样
IoC(控制反转)就是把"谁来 new 对象"从业务代码里拿出去。你不写 new UserService(),而是声明"我需要一个 UserService",容器负责创建、装配、管理生命周期。DI(依赖注入)是它的实现手段——构造器注入、字段注入、setter 注入。
面试怎么答IoC 是把对象的创建和装配控制权从业务代码转移到容器,DI 是实现 IoC 的手段。它解决三个具体问题:解耦,业务类只依赖接口;可测试,单测可以直接 new 传 mock;统一生命周期,事务和 AOP 这些横切逻辑需要容器在创建对象时插一脚。实践上推荐构造器注入,因为能保证对象创建出来就是完整的,字段还能声明 final,单构造器从 Spring 4.3 起可以省略 @Autowired。代价是构造器注入解决不了循环依赖,会直接启动失败,这时候应该重构代码消除环,而不是退回字段注入。要特别注意容器外的 new 出来的对象不是 Bean,注解一律不生效。
2

Bean 的一生:从实例化到销毁★

AOP 代理是在哪一步织进去的
一个 Bean 的生命周期是:实例化 → 属性填充 → Aware 回调 → BeanPostProcessor 前置 → 初始化方法 → BeanPostProcessor 后置 → 使用 → 销毁。AOP 代理发生在"后置处理"这一步,所以你在 @PostConstruct 里拿到的 this 还是原始对象,不是代理。
面试怎么答Bean 生命周期是实例化、属性填充、Aware 回调、BeanPostProcessor 前置处理、初始化方法、BeanPostProcessor 后置处理、使用、销毁。初始化有三个回调,顺序是 @PostConstruct、InitializingBean 的 afterPropertiesSet、initMethod。关键点是 AOP 代理在 BeanPostProcessor 的后置处理阶段生成,所以 @PostConstruct 里调用本类的 @Transactional 方法不生效,因为那时 this 还是原始对象。单例默认在启动时创建完,prototype 容器不管销毁。扩展点上,BeanPostProcessor 是 Spring 最重要的机制,Autowired、PostConstruct、AOP、Async 都建立在它之上。
3

循环依赖为什么要三级缓存★

两级也能跑,三级是为了延迟创建代理
循环依赖指 A 依赖 B、B 又依赖 A。Spring 靠提前暴露"半成品"解决:A 实例化后先把一个工厂放进三级缓存,B 需要 A 时通过工厂拿到 A 的早期引用,从而完成创建。为什么不用两级?因为工厂可以让"要不要创建代理"这个决定推迟到真正被需要时。
输出 a 开始创建 b 开始创建 a 命中三级缓存,调工厂拿到早期引用 b 完成,进入一级缓存 c 开始创建 a 命中二级缓存 c 完成,进入一级缓存 a 完成,进入一级缓存 b.a 和 a 是同一个对象: true c.a 和 a 是同一个对象: true a 的工厂被调用次数: 1
面试怎么答Spring 用三级缓存解决单例的循环依赖:一级 singletonObjects 存成品,二级 earlySingletonObjects 存提前暴露的早期引用,三级 singletonFactories 存对象工厂。流程是 A 实例化后把工厂放进三级缓存,B 需要 A 时通过工厂拿到 A 的早期引用完成创建,A 再回来填属性。二级缓存的作用是保证单例,工厂调用一次后结果放二级,后续都命中二级。三级缓存不能省的原因是它让"是否提前创建代理"这个决定推迟到真正需要时,避免所有 Bean 在实例化后就生成代理。构造器注入和 prototype 的循环依赖解决不了,Spring Boot 2.6 之后默认直接禁止循环依赖。
4

AOP 的两种代理,怎么选

有接口用 JDK,没接口用 CGLIB
Spring AOP 只支持方法级别的拦截,底层两种代理:JDK 动态代理基于接口,生成实现类;CGLIB 生成目标类的子类。Spring Boot 2.x 起默认用 CGLIB,因为大量 Service 没有接口。
面试怎么答Spring AOP 是运行时代理,两种实现:JDK 动态代理要求目标类实现接口,生成实现同一接口的代理类;CGLIB 用 ASM 生成目标类的子类,覆盖方法织入通知,所以代理不了 final 类和 final 方法。Spring Boot 2.x 默认用 CGLIB,因为很多 Service 没有接口,而 JDK 代理有个硬限制是必须按接口类型注入,按实现类注入会报类型不匹配。判断用的哪种可以看类名有没有 EnhancerBySpringCGLIB。切面顺序靠 Order 控制,值越小越外层。要注意 Spring AOP 只能拦方法、只能作用于容器管理的 Bean,和 AspectJ 的字节码织入不是一回事。
5

AOP 为什么会失效(能跑的证据)★

this 调用不走代理,一句话解释清楚
AOP 靠代理对象拦截方法调用。同类内部用 this.method() 调用时,走的是原始对象,压根没经过代理,通知自然不执行。这就是"@Transactional 加在私有方法上不生效""同类方法互相调用事务失效"的唯一根因。
输出外部调用 proxy.outer(): [代理] 开启事务 -> outer [业务] outer [业务] inner ← 注意:这里没有打印"开启事务" [代理] 提交事务 外部调用 proxy.inner(): [代理] 开启事务 -> inner [业务] inner [代理] 提交事务
面试怎么答AOP 生效的前提是调用经过代理对象。同类内部用 this 调用时,执行的是原始对象的方法,不经过代理,所以通知不执行,这是 @Transactional 和 @Async 在同类方法互调时失效的根本原因。因为代理对象持有的是原始对象引用,它在调用目标方法时,目标方法内部的 this 就是原始对象本身。解决办法有三种:自注入代理、用 AopContext.currentProxy()、或者把方法拆到另一个 Bean。其他失效还包括方法私有、final、static、对象不是 Spring 管理的、切点没匹配上。排查时先打印 this 的类名,确认到底拿到的是代理还是原始对象。
6

@Transactional 失效的六种场景★

不是配置抄错了,是机制决定的
事务失效不是玄学,六种场景都有明确机制:调用没经过代理、方法不能被代理、异常不满足回滚条件、事务在不同线程、数据源不支持、传播行为不对。记住机制比背清单有用。
面试怎么答@Transactional 失效有六种典型场景。第一是同类 this 调用不经过代理,这是最常见的。第二是方法不是 public 或者被 final、static 修饰,代理覆盖不到。第三是异常被 catch 了没往外抛,代理感知不到,自然不回滚,这条最隐蔽。第四是抛的是受检异常,因为默认 rollbackFor 只包含 RuntimeException 和 Error,需要显式配 rollbackFor 等于 Exception。第五是事务跨线程,事务状态存在 ThreadLocal 里,子线程拿不到。第六是类没被 Spring 管理,或者是 MyISAM 这种不支持事务的引擎。判断标准始终是异常有没有穿过代理边界。
7

七种事务传播行为怎么记

只记两个常用的,其余知道存在
传播行为回答的是:一个已经有事务的方法,调用了另一个带事务的方法,第二个该怎么办。真正需要记住的只有两个:REQUIRED(默认,有就加入、没有就新建)和 REQUIRES_NEW(不管有没有,都新建并挂起外层)。
面试怎么答传播行为解决的是事务方法嵌套调用时内层怎么办。默认的 REQUIRED 是有事务就加入、没有就新建,所以内外共用一个事务,内层异常会导致整体回滚,包括外层的写操作。REQUIRES_NEW 是挂起外层、开独立事务,适合日志这种必须留下的操作,但代价是占两个连接,而且如果操作外层已锁定的同一行会自己锁自己。NESTED 是同一物理事务里的保存点,只有一次提交,外层最终回滚的话内层也白搭。剩下四个 SUPPORTS、NOT_SUPPORTED、MANDATORY、NEVER 主要用于表达约束,实际业务里用得少。
8

一个请求在 Spring MVC 里走了哪些地方

十步链路,每步都能插东西
请求进入后:Filter → DispatcherServlet → HandlerMapping 找方法 → HandlerAdapter 调方法(期间做参数绑定、校验、拦截器前置)→ 返回值处理器选消息转换器 → 写出响应 → 拦截器后置。搞清这条链路,才知道某个功能该写在哪一层。
面试怎么答请求先经过 Servlet 容器的 Filter 链,进入 DispatcherServlet 的 doDispatch,先由 HandlerMapping 按 URL 找到 HandlerMethod,再由 HandlerAdapter 负责调用,期间完成参数解析、数据校验、拦截器 preHandle,然后执行 Controller 方法,返回值由 HandlerAdapter 处理后交给 HttpMessageConverter 序列化,最后走拦截器 postHandle 和 afterCompletion,再回到 Filter 的后半段。异常由 HandlerExceptionResolver 处理,对应 @ControllerAdvice。要能说出三类参数异常的区分:请求体解析失败、校验失败、类型转换失败。对照 Nest 的话,Guard 对应 Spring Security 的 Filter,Pipe 对应参数解析加校验,ExceptionFilter 对应 ControllerAdvice。
9

Filter、Interceptor、AOP 差在哪

三层拦截,能力和时机都不同
三者的归属不同:Filter 属于 Servlet 容器,拿得到原始 ServletRequest;Interceptor 属于 Spring MVC,知道"要调哪个 Controller 方法";AOP 属于业务 Bean,知道方法参数和返回值。越靠外层能拦的东西越少但越早,越靠内层拿到信息越多但越晚。
面试怎么答Filter 是 Servlet 规范的一部分,在 DispatcherServlet 之前执行,能拿原始 request 但拿不到 Controller 方法信息,适合做编码、CORS、TraceId。Interceptor 是 Spring MVC 的,能拿到 HandlerMethod,适合按注解做鉴权和权限。AOP 作用于业务 Bean,能拿到方法参数和返回值,事务和缓存都在这层。执行顺序是 Filter 前半段、Interceptor preHandle、AOP 前置、方法、AOP 后置、postHandle、afterCompletion、Filter 后半段。几个实践要点:Filter 的顺序要用 FilterRegistrationBean 的 setOrder 控制,不是 @Order;CORS 要放在鉴权之前否则预检请求会被拦;Filter 里读请求体必须用 ContentCachingRequestWrapper 否则 Controller 读不到。
10

自动装配到底是怎么装上的★

一个注解背后三次跳转
@SpringBootApplication 是三个注解的组合,其中 @EnableAutoConfiguration 通过 @Import 引入一个选择器,选择器去读 classpath 上的候选配置清单文件,把里面几百个自动配置类按条件注解筛一遍,留下该生效的注册成 Bean。
面试怎么答@SpringBootApplication 等于 @SpringBootConfiguration 加 @ComponentScan 加 @EnableAutoConfiguration。自动装配的核心是 @EnableAutoConfiguration 通过 @Import 引入 AutoConfigurationImportSelector,它去读 classpath 上的自动配置清单文件,Boot 2.7 之前是 spring.factories,之后换成了 AutoConfiguration.imports 这个新文件名,然后几百个候选配置类逐个过条件注解筛选,留下的注册成 Bean。最常用的条件是 @ConditionalOnClass 和 @ConditionalOnMissingBean,后者是自定义覆盖默认配置的机制,而且它不是覆盖而是根本不让默认的注册。排查没生效就加 --debug 看条件评估报告。
11

配置从哪来,谁覆盖谁

优先级记错,线上就会读到错误的值
Spring Boot 的配置有十几层来源,优先级从高到低大致是:命令行参数 > Java 系统属性 > 操作系统环境变量 > profile 专属配置文件 > 默认配置文件。同一个 key 出现在多处时,高优先级的赢,而且不会合并。
面试怎么答Spring Boot 配置优先级从高到低是命令行参数、系统属性、操作系统环境变量、jar 包外的 profile 配置文件、jar 包外的默认配置、jar 包内的 profile 配置、jar 包内的默认配置、PropertySource、代码里的默认值。环境变量会自动把点转成下划线并大写,所以容器部署覆盖配置很方便。profile 之间后激活的覆盖先激活的。实践上要注意两点:一组配置项用 @ConfigurationProperties 而不是 @Value,因为它支持松散绑定、嵌套和校验;配置中心的优先级通常高于本地文件,所以本地改了不生效要先查这个。
12

全局异常处理和参数校验

别让前端拿到 500 和堆栈
@ControllerAdvice + @ExceptionHandler 把异常统一转成结构化响应。参数校验用 @Valid(JSR-303),嵌套对象要层层加 @Valid 才会级联校验,否则内层字段不校验也不报错。
面试怎么答全局异常处理用 @RestControllerAdvice 配合 @ExceptionHandler,至少要接四类:业务异常、@Valid 校验失败抛的 MethodArgumentNotValidException、请求体解析失败抛的 HttpMessageNotReadableException、以及 Exception 兜底。校验这块有几个高频坑:嵌套对象和集合元素必须在字段上加 @Valid 才会级联校验,不加就静默不校验;@RequestParam 上的约束抛的是 ConstraintViolationException,需要在类上加 @Validated,和 @RequestBody 抛的不是同一个;新增和修改的校验规则不同就用分组校验。兜底 handler 一定不能把堆栈返回给前端。
13

Spring 的事件机制与异步

解耦和踩坑都在这两个注解上
ApplicationEventPublisher.publishEvent() 发事件,@EventListener 收事件。默认是同步的——发布方法会等所有监听器执行完才返回。要异步加 @Async,但要先 @EnableAsync 并配好线程池,否则用的是每次新建线程的执行器。
面试怎么答Spring 的事件机制是发布订阅模式,发布方调 publishEvent,监听方用 @EventListener 接收。默认是同步的,发布方法会等所有监听器执行完才返回,而且监听器和发布方在同一个事务里,监听器抛异常会导致发布方回滚。所以做发消息、清缓存这类操作要用 @TransactionalEventListener 的 AFTER_COMMIT 阶段,保证事务提交后才执行,避免消息发出去了但数据回滚。要注意 AFTER_COMMIT 之后事务已经结束,里面的操作不再被原事务保护。@Async 有几个坑:不开 @EnableAsync 就完全同步、默认执行器每次新建线程、返回 void 时异常被吞、ThreadLocal 里的 TraceId 会丢。
14

定时任务和线程池的默认值陷阱

默认配置几乎总是错的
@Scheduled 的默认调度线程池只有一个线程,多个定时任务会串行执行,一个卡住全部卡住。Spring Boot 里 @Async 的默认执行器每次新建线程。这两处默认值都不能用于生产。
面试怎么答@Scheduled 默认的调度线程池只有一个线程,多个任务会串行执行,一个卡住全部停摆而且不报错,所以生产必须用 SchedulingConfigurer 配置 ThreadPoolTaskScheduler。另外 cron 用 JVM 时区,容器是 UTC 的话执行时间会偏移 8 小时;多实例部署时每个实例都会跑,需要分布式锁或者用 XXL-JOB 这类调度框架。@Async 的默认执行器是 SimpleAsyncTaskExecutor,每次新建线程不复用,也必须自定义 ThreadPoolTaskExecutor。线程池参数上要注意队列太长会让拒绝策略永远不触发,把快速失败变成缓慢积压。线程名一定要起,排查时能直接定位。
15

Spring Boot 3 变了什么★

升级踩坑清单
Boot 3 的三条硬变化:JDK 基线提到 17、javax 包名全部换成 jakarta、自动配置清单文件换位置。前两条会导致编译不过(这是好事),第三条不报错只是不生效(这是坑)。
面试怎么答Spring Boot 3 的关键变化有六条。最主要的三条是:JDK 基线提到 17;javax 命名空间整体换成 jakarta,涉及 servlet、persistence、validation、annotation 四大块,而且依赖的第三方库也必须提供 jakarta 版本;自动配置清单文件从 spring.factories 换成了 AutoConfiguration.imports。前两条编译期会报错,第三条不报错只静默失效,是最坑的,因为 Boot 2.7 同时兼容两种格式,很多项目停在 2.7 没暴露问题。另外 Spring Security 6 移除了 WebSecurityConfigurerAdapter、匹配器 API 改名、默认授权更严格,升级后容易突然 403。还有 Micrometer Tracing 取代 Sleuth,以及支持 GraalVM 原生镜像但需要注册运行时提示。
16

速答清单

面试前一小时只看这页
01IoC 和 DI 是什么关系?
IoC 是思想:对象的创建和装配交给容器。DI 是实现手段:构造器注入、setter 注入、字段注入。
02为什么推荐构造器注入?
保证对象创建出来就是完整的,字段能声明 final,单测可以直接 new 传 mock。代价是解决不了循环依赖。
03Bean 的生命周期?
实例化 → 属性填充 → Aware → BeanPostProcessor 前置 → 初始化(@PostConstruct、afterPropertiesSet、initMethod)→ 后置 → 使用 → 销毁。
04AOP 代理是在哪一步生成的?
BeanPostProcessor 的后置处理阶段。所以 @PostConstruct 里调本类的 @Transactional 方法不生效。
05三级缓存分别存什么?
一级 singletonObjects 存成品,二级 earlySingletonObjects 存早期引用,三级 singletonFactories 存对象工厂。
06为什么需要三级缓存,两级不够吗?
两级也能解决循环依赖,但会迫使实例化后就决定要不要创建代理。三级存工厂,把决定推迟到真正被需要时,避免给无循环依赖的 Bean 提前创建代理。
07哪些循环依赖解决不了?
构造器注入的、prototype 作用域的、@Async 导致的。Boot 2.6 之后默认直接禁止循环依赖。
08JDK 动态代理和 CGLIB 的区别?
JDK 基于接口生成实现类,CGLIB 生成子类覆盖方法。CGLIB 代理不了 final 类和 final 方法。Boot 2.x 默认 CGLIB。
09AOP 为什么会失效?
同类 this 调用不经过代理。还有 private、final、static 方法,对象不是 Bean,切点没匹配上。
10@Transactional 失效的六种场景?
this 调用、非 public 或 final、异常被 catch、抛受检异常、跨线程、类没被 Spring 管理或引擎不支持事务。
11@Transactional 默认对哪些异常回滚?
只有 RuntimeException 和 Error。受检异常默认不回滚,要配 rollbackFor = Exception.class。
12事务传播行为默认是哪个?
REQUIRED。有事务就加入,没有就新建,内外共用一个。REQUIRES_NEW 是独立事务并挂起外层。
13REQUIRES_NEW 有什么隐藏成本?
占两个数据库连接;如果内层操作外层已锁定的同一行,会自己锁自己导致死锁或超时。
14NESTED 和 REQUIRES_NEW 的区别?
NESTED 是同一物理事务里的保存点,只有一次提交,外层回滚内层也白搭;REQUIRES_NEW 是两个独立事务。
15一个请求在 Spring MVC 里的链路?
Filter → DispatcherServlet → HandlerMapping → HandlerAdapter(参数解析、校验、preHandle)→ Controller → 返回值处理 → postHandle → afterCompletion → Filter 后半段。
16Filter 和 Interceptor 的区别?
Filter 属于 Servlet 容器,能拿原始 request 但不知道 Controller 方法;Interceptor 属于 Spring MVC,能拿到 HandlerMethod。
17为什么 CORS 配置会拦截 OPTIONS?
预检请求不带 Authorization 头,如果鉴权 Filter 在 CORS 之前就会拦掉。要让 CORS 优先级最高或放行 OPTIONS。
18Filter 里读了请求体,Controller 为什么读不到?
输入流只能读一次。要用 ContentCachingRequestWrapper 包一层。
19自动装配的原理?
@EnableAutoConfiguration 通过 @Import 引入选择器,读自动配置清单文件,按条件注解筛选后注册 Bean。
20@ConditionalOnMissingBean 有什么用?
容器里没有这个 Bean 才生效。这是自定义配置覆盖默认配置的机制,本质是不让默认的注册,不是后加载覆盖。
21Boot 3 的自动配置清单文件在哪?
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,2.7 之前是 spring.factories。
22自动配置没生效怎么排查?
启动加 --debug,看 CONDITIONS EVALUATION REPORT,会列出每个自动配置被哪个条件否掉。
23@Value 和 @ConfigurationProperties 怎么选?
一组配置项用 @ConfigurationProperties,支持松散绑定、嵌套对象、校验和 IDE 提示;单个值可以用 @Value。
24配置优先级最高的是什么?
命令行参数。然后是系统属性、环境变量、jar 外的 profile 配置、jar 外的默认配置。
25环境变量怎么映射到配置项?
点转下划线并全大写,server.port 对应 SERVER_PORT。
26全局异常处理要接哪几类异常?
业务异常、MethodArgumentNotValidException、HttpMessageNotReadableException、Exception 兜底。少一个前端就拿到框架默认响应。
27嵌套对象的校验为什么不生效?
字段上没加 @Valid,不会级联。集合元素同理,而且不报错,属于静默失效。
28Spring 事件默认是同步还是异步?
同步,发布方法会等所有监听器执行完,而且监听器和发布方在同一个事务里。
29@TransactionalEventListener 什么时候用?
需要"事务提交后才执行"时,比如发 MQ、清缓存。避免消息发出去了但数据回滚。
30@Async 的常见坑?
不开 @EnableAsync 就完全同步;默认执行器每次新建线程;同类调用无效;返回 void 时异常被吞;ThreadLocal 的 TraceId 会丢。
31@Scheduled 默认几个线程?
一个。多个任务串行执行,一个卡住全部停摆且不报错。必须配置 ThreadPoolTaskScheduler。
32多实例部署时定时任务会重复吗?
会。@Scheduled 是单机调度,每个实例都会跑。要加分布式锁或用 XXL-JOB 这类调度框架。
33线程池队列太长有什么问题?
任务积压到超时,拒绝策略永远不触发,把快速失败变成缓慢烂掉。
34Boot 3 的三条硬变化?
JDK 基线 17、javax 换 jakarta、自动配置清单换位置。前两条编译报错,第三条静默失效。
35升级 Boot 3 为什么建议先到 2.7?
2.7 同时兼容两种清单文件格式,可以先改好自定义 starter,再升 3.x 就不会突然失效。