Spring 面试是 Java 后端绕不过去的坎,而“Spring-必问”这四个字背后,基本涵盖了面试官最常用来试探候选人功底的四大金刚:IoC 容器与 Bean 生命周期、循环依赖与三级缓存、事务管理与 AOP 代理、以及一堆“看似会了、一深问就露馅”的原理细节。
这篇博文我不打算按教科书顺序给你从头讲到尾,而是按我这些年面试别人、也被别人面试的实际经验,把那些高频考点按“是什么、为什么、怎么答、坑在哪”的节奏重新梳理一遍。内容会尽量贴合真实面试场景,适合正在准备跳槽的初中级 Java 开发阅读,也适合带新人的老手拿来当内部培训提纲。
1. 面试考点的整体分布逻辑
1.1 为什么 Spring 是 Java 面试的“必问重灾区”
Spring 在 Java 后端开发里的地位不需要我多解释。几乎每一家公司在招 Java 开发时,无论业务偏重什么方向,都会在面试里带上几个 Spring 相关的问题。这不是面试官偷懒,而是 Spring 已经成了 Java 服务端开发的“基础设施层”。一个候选人如果连 Spring 的核心机制都讲不清楚,那到了实际项目里,遇到 Bean 初始化失败、事务不生效、AOP 切面静默失效这类问题,基本就是两眼一抹黑。
从面试官的角度来看,Spring 问题是最容易“由浅入深”的。一个面试官可以从“你用过 Spring 吗”开始,一路问到“BeanFactory 和 ApplicationContext 有什么区别”、“三级缓存为什么能解决循环依赖”、“@Transactional 在什么情况下会失效”,每一个追问都对应着候选人实际动手能力和源码阅读深度的分层。换句话说,Spring 面试题是一个天然的“能力分级器”,所以它才会被反复问、必问。
1.2 高频考点的几个梯队划分
我梳理过大量真实的面试记录,也模拟过很多次技术面,Spring 相关的问题大致可以分成三个梯队。
第一梯队是“生存题”,问的是 IoC、AOP、Bean 生命周期、自动装配这些最基础的概念。这类问题考察的是候选人有没有真正用过 Spring,能不能把“控制反转”和“依赖注入”用大白话讲清楚。第二梯队是“进阶题”,集中在循环依赖、三级缓存、事务传播行为、动态代理选择机制这些源码级的话题上。这类问题能刷掉一大半停留在“会用注解”层面的候选人。第三梯队是“综合题”,一般结合项目场景出题,比如“你在项目里遇到过事务不生效吗?怎么排查的?”“一个 Bean 被代理之后,getClass() 返回的是什么?”这类问题没有标准答案,考的是候选人在真实工程里有没有踩过坑、有没有主动去查过原因。
接下来的内容,我会把每一梯队里的核心考点逐一拆开,结合源码机制和实际场景讲透。
2. IoC 容器与 Bean 生命周期:面试必问的第一座山头
2.1 控制反转到底反转了什么
很多候选人一上来就背定义:“控制反转就是把对象的创建和依赖关系的维护从代码里反转给容器管理。”这句话对不对?对,但太浅了。我在面试时通常会追问一句:“那反转前后,到底哪个‘控制’被反转了?”
说实话,这个追问能解释清楚的人并不多。用生活化的方式理解会容易很多:以前你自己开餐馆,买菜、洗菜、切菜、炒菜、上菜全是你自己管,这是“控制”在你自己手里。有了 Spring 容器之后,你只需要把菜单(配置)交给中央厨房,到点菜的时候中央厨房把做好的菜端上来,你只管吃。这里面“反转”的其实是对象创建和组装的控制权,从调用方手里移交给了容器。调用方不再负责 new 对象,也不再负责把依赖塞进去,而是声明“我需要什么”,由容器在合适的时机把依赖注入进来。
这个理解到位了之后,很多衍生问题就都能顺着推出来。比如“构造器注入和 setter 注入有什么区别”“为什么推荐构造器注入”“容器怎么知道要创建哪些 Bean”,这些问题本质上都是在考察你对“容器接管控制权”这件事理解得有多深。
2.2 Bean 的生命周期各阶段与扩展点
Bean 生命周期是 Spring 面试里的常客,而且面试官非常喜欢让你结合源码顺序来回答。常见的答法是:实例化、属性填充、初始化、销毁。但如果你想在这个问题上拿到不错的印象分,需要答出更细的扩展点。
我把 Bean 从创建到销毁的核心阶段整理成一张对照表,方便你记忆:
| 阶段 | 关键扩展点 | 说明 |
|---|---|---|
| BeanDefinition 解析 | BeanFactoryPostProcessor | 在 Bean 实例化之前,可以修改 BeanDefinition 的元信息 |
| 实例化 | 构造器 / 工厂方法 | 默认通过无参构造器创建实例 |
| 属性填充 | AutowiredAnnotationBeanPostProcessor | 处理 @Autowired、@Value 等注解的依赖注入 |
| 初始化前 | BeanPostProcessor.postProcessBeforeInitialization | 在 init 方法执行前调用,可以包装或修改 Bean |
| 初始化 | InitializingBean.afterPropertiesSet / init-method | 执行自定义初始化逻辑 |
| 初始化后 | BeanPostProcessor.postProcessAfterInitialization | 在 init 方法执行后调用,AOP 代理就是在这里完成的 |
| 销毁 | DisposableBean.destroy / destroy-method | 容器关闭时执行清理逻辑 |
这张表里有两个点经常被单独拎出来问。
第一个点,Spring 内部对 BeanPostProcessor 有优先级排序。实现了 PriorityOrdered 接口的 BPP 会最先执行,然后是 Ordered 接口的,最后才是没有排序的普通 BPP。这一点在你需要自定义扩展时非常重要,如果你写的 BPP 依赖了另一个 BPP 的处理结果,而你没有控制优先级,行为就完全不可预期。
第二个点,AOP 代理生成发生在 Bean 初始化之后的 postProcessAfterInitialization 阶段。这一点是理解 Spring 事务和 AOP 失效问题的钥匙。很多人疑惑“为什么 Spring 事务要基于代理实现”“为什么同类内部调用事务不生效”,根源就在这里:代理对象是在 Bean 初始化完成之后才生成的,而内部方法调用走的是 this 调用,根本没有经过代理对象。
2.3 BeanFactory 与 ApplicationContext 的差异
这个问题属于“面试官觉得你应该知道,但很多人说不透”的类型。我听到过最典型的错误说法是:“ApplicationContext 就是一个增强版的 BeanFactory。”
这么说不能算错,但不够准确。准确的差异点有三个层次。
第一个层次是功能增强。ApplicationContext 在 BeanFactory 的基础上增加了国际化(MessageSource)、资源加载(ResourceLoader)、事件发布(ApplicationEventPublisher)等能力,这是大部分人都能答出来的。
第二个层次是加载时机。BeanFactory 默认在 getBean 的时候才创建 Bean,是懒加载模式;ApplicationContext 在启动阶段就会预实例化所有单例 Bean,是饿汉式加载。这个差异在实际项目里会造成一个现象:如果有一段初始化逻辑写在了某个单例 Bean 的构造器或者 @PostConstruct 方法里,而它又不是懒加载的,那这段逻辑会在应用启动时执行,而不是第一次调用时执行。
第三个层次是内部组件的注册方式。ApplicationContext 在刷新容器(refresh)的流程里,会通过一系列内置的 BeanFactoryPostProcessor 来注册和增强核心组件,比如把 ConfigurationClassPostProcessor 注册进去,从而处理 @Configuration、@ComponentScan、@Import 等注解。BeanFactory 本身不具备这些能力,它只负责最基础的 Bean 创建和管理。
答这个问题的时候,我建议你顺着“容器刷新流程”来组织语言:从 AbstractApplicationContext.refresh() 开始,说 prepareBeanFactory、invokeBeanFactoryPostProcessors、registerBeanPostProcessors、finishBeanFactoryInitialization 这一串流程分别做了什么,面试官马上就知道你是真的读过源码。
3. 循环依赖与三级缓存:拉开差距的核心难题
3.1 循环依赖问题的本质
面试官问“Spring 如何解决循环依赖”时,他通常默认你已经知道了单例模式下 setter 注入的循环依赖能被解决,而构造器注入的循环依赖会报错。他要考察的其实是两个更深入的问题:第一,为什么三级缓存能解决循环依赖?第二,为什么必须是三级而不是两级?
先说问题的本质。循环依赖就是 A 依赖 B、B 又依赖 A,形成一个环。在没有容器的情况下,这个环几乎无解,因为 new A 需要先有 B,new B 又需要先有 A,谁先谁后都卡住。Spring 容器的解法很简单:允许先把 A 的“早期引用”暴露出来,让 B 在创建的过程中先拿到这个不完整的 A,等 B 创建完成后再把完整的 A 注入给 B,最后 A 自己也完成了属性填充和初始化。
这里的关键是“早期引用”。从字面上就能理解,A 的实例已经创建出来了,但是属性还没填充完,初始化方法也还没执行。这种情况下把 A 提前交给 B 使用,在绝大多数业务场景里是安全的,因为 B 在创建过程中一般不会立即调用 A 的业务方法。
3.2 三级缓存的各自职责
Spring 内部用三个 Map 来管理不同状态的 Bean,这就是所谓的“三级缓存”。我先把它们的职责列出来:
| 缓存 | 存储内容 | 作用 |
|---|---|---|
| singletonObjects | 完全创建好的单例 Bean | 一级缓存,getBean 时优先从这里取 |
| earlySingletonObjects | 提前暴露的早期引用(半成品 Bean) | 二级缓存,存放实例化但未完成初始化的 Bean |
| singletonFactories | ObjectFactory 工厂 | 三级缓存,用于生成早期引用的工厂 |
读代码的时候你会发现,核心逻辑集中在两个方法里:getSingleton 和 addSingletonFactory。当 A 创建时,实例化之后会调用 addSingletonFactory 往三级缓存里放一个 ObjectFactory。这个工厂的核心代码是一个 lambda 表达式,里面调用了 getEarlyBeanReference。这个 getEarlyBeanReference 会遍历所有 SmartInstantiationAwareBeanPostProcessor,让它们有机会对 A 的早期引用做处理,最典型的就是提前生成 AOP 代理。
当 B 在创建时需要注入 A,getSingleton("a", true) 会先查一级缓存,没有;查二级缓存,没有;查三级缓存,发现有 ObjectFactory,于是调用工厂的 getObject() 拿到 A 的早期引用,然后把这个引用放到二级缓存里,同时删除三级缓存中的工厂。这样一来,后续再有其他 Bean 依赖 A 时,就可以直接从二级缓存里拿同一个引用,保证整个容器里只有一个 A 实例。
3.3 为什么必须是三级缓存而不是两级
这是面试里最容易卡住人的问题。很多人知道三级缓存的流程,但说不出为什么不能砍掉第三级。
我的理解是:三级缓存存在的核心价值,是延迟 AOP 代理的创建时机。
设想一下,如果没有三级缓存,只有两级缓存(singletonObjects 和 earlySingletonObjects),那么 A 在实例化之后就必须立即完成代理对象的创建,然后放进二级缓存。这样一来,即使 A 最终没有发生循环依赖,A 也会被提前代理。这在大部分场景下可能没什么问题,但 Spring 的设计哲学是“按需代理”——只有真正发生循环依赖的时候,才需要在早期就生成代理对象;如果 A 只是一个普通的、没有任何 Bean 依赖它提前引用的单例,那它的代理应该在初始化完成之后的 postProcessAfterInitialization 阶段生成。
三级缓存里的 ObjectFactory 恰恰提供了这种“延迟决策”的能力。A 实例化完成后,只是先放一个工厂进去,工厂里具体生成原始 Bean 还是生成代理 Bean,取决于 getEarlyBeanReference 时有没有循环依赖发生。如果整个创建流程走完都没有其他 Bean 提前引用 A,这个工厂根本不会被调用,A 就会在初始化完成后走正常的代理生成流程。
也就是说,三级缓存相当于把“要不要生成早期代理”这个决策从“创建 A 时立刻做”推迟到“发现循环依赖时再做”。这既保证了代理的正确性,又避免了无谓的早期代理开销。
3.4 哪些循环依赖解决不了
三级缓存能解决大部分循环依赖,但有三类场景依然会失败,面试官很喜欢拿这些场景来考你。
第一类是构造器注入的循环依赖,这个必然失败。原因很简单:构造器注入要求在 new A 的瞬间就把 B 传进去,但此时 B 的实例还没创建出来,A 连“早期引用”都暴露不了,因为实例化这一步就被卡住了。三级缓存需要在实例化完成之后才能放工厂,构造器注入走不到这一步。
第二类是原型作用域的循环依赖,也必然失败。三级缓存是给单例 Bean 用的,原型 Bean 每次 getBean 都会新建实例,Spring 压根不会为它们缓存早期引用。所以原型 Bean 的循环依赖会直接抛出异常。
第三类是 @Async 注解的 Bean 出现循环依赖时,有可能在启动阶段就报错。这个坑比较隐蔽:当 A 和 B 互相依赖,且其中一个类标了 @Async 时,由于 @Async 会让 Spring 在早期就生成代理,有时会触发“BeanCurrentlyInCreationException”之类的异常。我在实际项目里遇到过,排查到最后发现是因为某个服务类上加了 @Async,同时又和别人互相依赖了。
如果你在面试中能把这几个失败场景都讲出来,顺带提一句“所以设计时最好的做法就是避免循环依赖,根本不要依赖 Spring 去解决它”,面试官基本就会认为你既有源码深度又有工程意识。
4. 事务管理与 AOP 代理:恶魔藏在细节里
4.1 Spring 事务的底层执行逻辑
Spring 事务是面试中的“场景题大户”,考察点主要集中在两个方向:事务的传播行为,以及事务失效的各类场景。
Spring 声明式事务的基础是 AOP。@Transactional 注解会被 Spring 的 BeanPostProcessor 识别,在目标 Bean 初始化完成后生成一个事务增强代理。这个代理会在方法调用前开启事务,在方法正常返回后提交事务,在方法抛出 RuntimeException 时回滚事务。
理解了这个底层逻辑,很多“为什么”就能自己推出来。比如事务注解为什么不能加在 private 方法上?因为 Spring 的代理机制只能拦截外部对 public 方法的调用,private 方法属于类内部实现细节,根本不会经过代理。再比如为什么同类内部方法调用事务会失效?因为内部调用走的是 this 引用,this 是原始对象,不是代理对象,事务拦截器根本感知不到。
4.2 事务失效的经典场景清单
我把这些年见过的、面试里高频出现的失效场景整理成了一份速查表,你可以直接拿来对照:
| 失效场景 | 原因 | 规避方式 |
|---|---|---|
| 同类内部方法调用 this.method() | 没有经过代理对象,事务拦截器不生效 | 注入自身代理,或拆分为不同 Bean |
| 方法声明为 private | 代理只能拦截 public 方法 | 改为 public |
| 抛出 checked 异常 | 默认只回滚 RuntimeException 和 Error | 配置 rollbackFor = Exception.class |
| 异常被 try-catch 吞掉 | 事务拦截器捕捉不到异常,无法回滚 | 在 catch 块中抛出运行时异常,或手动回滚 |
| 非 Spring 管理的类调用 | 没有经过 Bean 容器,压根没有代理 | 确保事务方法所在 Bean 由 Spring 管理 |
| 数据库引擎不支持事务 | MySQL 的 MyISAM 引擎不支持事务 | 使用 InnoDB |
| REQUIRES_NEW 传播行为使用不当 | 新事务和原事务不在同一连接上,回滚范围不符预期 | 仔细理解传播语义 |
这里我单独说一下异常被 catch 吞掉这个场景,因为它在真实项目里出现的频率非常高。有一段代码,在一个服务方法里调用了另一个 service 的更新方法,更新方法上标了 @Transactional,但调用方把异常 catch 住了,结果数据一直对不上,最后排查半天发现就是 catch 块把异常吃了,导致事务回滚逻辑压根没触发。
解决办法有两个思路:一个是不要在 catch 里吞异常,让它继续向上抛出;另一个是如果确实要处理异常,可以在 catch 块里手动调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(),强制把当前事务标记为回滚。后一种方式我在一些不能改动异常结构的老项目里用过,效果是可靠的。
4.3 事务传播行为怎么选
Spring 提供了七种事务传播行为,但真实业务里常用的就三种:REQUIRED、REQUIRES_NEW、NESTED。面试时至少要把这三种讲清楚,最好能结合业务场景来说明。
REQUIRED 是默认值:如果外层有事务,就加入外层事务;如果外层没有,就新建一个。大部分业务场景都适合用 REQUIRED,因为它符合“同一个业务操作共享同一个事务”的直觉。
REQUIRES_NEW 的特点是挂起外层事务,开启一个全新事务,新事务的提交和回滚都独立于外层。这个传播行为适合用在日志记录、异步通知、审计等场景,因为你不希望子操作失败后把主流程的事务也带崩。但它也有代价:如果外层事务已经锁了一些数据,REQUIRES_NEW 挂起外层事务后释放连接,容易引发并发问题。我之前在一个项目里曾经因为 REQUIRES_NEW 使用频繁导致连接池被耗尽,后来改成了异步任务才解决。
NESTED 是嵌套事务,利用数据库的 savepoint 实现部分回滚:外层回滚时内层也跟着回滚,但内层回滚不会影响外层。它和 REQUIRES_NEW 最大的区别就在这里:REQUIRES_NEW 内外是彻底隔离的两个事务,NESTED 还是外层事务的一部分,只是多了保存点。
4.4 事务在面试中的高分答法
面试一旦聊到事务,我的经验是不要直接背概念,而是用一条故事线来组织答案。
先说明 Spring 事务的本质是基于 AOP 的代理机制,然后指出代理生成的关键时机(Bean 初始化完成后),接着从两类视角展开:一类是正常情况下的执行流程(开启、提交、回滚),另一类是失效场景(内部调用、异常被吞、private 方法等)。最后一定要落到工程建议上:优先采用编程式事务或确保事务方法通过 proxy 调用;在类内部调用事务方法时,要么拆类,要么注入自身,要么使用 TransactionTemplate。这样答下来,面试官会认为你不是背了一堆结论,而是真的理解了事务治理的策略。
5. AOP 与动态代理:代理机制决定成败
5.1 JDK 代理和 CGLIB 的选择逻辑
AOP 是 Spring 的另一根支柱。面试中关于 AOP 的问题,我最常被问到的是:“Spring 默认用 JDK 代理还是 CGLIB?为什么?”
答案和 Spring Boot 版本相关。传统 Spring 框架时代,如果你的 Bean 实现了接口,默认用 JDK 动态代理;如果没有实现接口,则用 CGLIB。但 Spring Boot 2.0 之后,官方把默认策略改成了“优先使用 CGLIB”,也就是 proxyTargetClass 默认变为 true。这个策略调整的初衷是:JDK 代理要求目标类必须实现接口,而且只能代理接口中声明的方法,这在很多场景下限制太死。CGLIB 通过生成子类的方式代理,不需要接口约束,灵活得多。
当然,CGLIB 也有自己的限制。最典型的是:final 类不能被代理,final 方法不能被拦截,因为 CGLIB 靠继承生成子类,而 final 会阻断继承和重写。Spring 官方文档里也明确说过,如果目标类被 final 修饰,只能退回到 JDK 代理或者手动配置。
5.2 this 调用与代理失效的真正原因
AOP 失效的场景非常多,但根源只有一个:调用点没有被代理拦截。最典型的就是类内部的 this 调用。
举个例子,你有一个某个交易服务类,类上有 @Transactional,里面有两个方法:方法A被外部调用,方法A内部又调用了方法B。如果方法B上标了 @Transactional 而方法A没标,那么你从外部调用方法A时,方法B的事务是不生效的。原因很简单:方法A内部调用方法B,走的是 this.methodB(),this 是原始对象,不是代理对象,事务拦截器根本看不到这次调用。
解决这个问题的标准手法有几种:把方法B拆分到另一个 Spring Bean 中,通过注入的代理来调用;或者使用 AopContext.currentProxy() 获取当前代理对象,然后通过代理调用。我实际项目里最常用的是第一种,拆类,这样职责更清晰,也避免了 AopContext 依赖 ThreadLocal 可能带来的性能问题。
5.3 AOP 失效的几个冷门场景
除了 this 调用,还有几个冷门但真实存在的 AOP 失效场景,面试中如果聊到会很加分。
第一个是类没有被 Spring 管理。如果目标类既没加 @Component 也没有在 XML 或者 JavaConfig 中注册,那 Spring 根本不会对它做代理,AOP 自然无效。这个问题看起来简单,但经常会出现在新人写的工具类里。
第二个是切点表达式写错,切面静默失效。比如切点写的是 execution(public * com.xxx.service..*(..)),但目标方法实际上是 protected 或者包内可见,那这个切面就不会被触发。而且这种错误不会报错,只会让你觉得代码没走切面,排查起来特别耽误时间。
第三个是多个 AOP 生效时顺序不对。Spring 5 之后,AOP 的排序规则是:@Order 注解值越小优先级越高,优先级高的切面在外层。如果你有多个切面,比如事务切面和自定义日志切面,顺序混乱时可能导致事务提前提交或者日志记录时机不对。排查这类问题的方法是在日志里打印切面的执行顺序。
6. 面试追问场景与应答策略整理解读
6.1 从“背答案”到“拉通理解”的分水岭
面试官问 Spring 知识点,真正想听的往往不是那个标准答案,而是你的推导过程。我见过太多候选人把概念背得滚瓜烂熟,但一到“换个角度问”就卡壳。
比如面试官问你“你对 IoC 怎么理解”,你要是回答“通过容器管理对象创建,降低耦合”,这个答案满分只能拿 30%。但如果接着答:在传统开发中,A 依赖 B,B 依赖 C,A 的代码里要 new B,B 的代码里要 new C,这种硬编码依赖让单元测试寸步难行;引入容器后,我们只声明依赖关系,容器根据配置完成装配,模块间只面向接口编程,可测试性和可维护性都大大提升。这就是一个拉通理解的体现。
再比如循环依赖,面试官问“为什么用三级缓存”,核心不是让你背三级缓存的过程,而是考察你能不能理解“延迟暴露代理,按需创建”的设计意图。我建议平时准备 Spring 面试时,多问自己几个“为什么”,比如:
- 为什么 Spring 要搞这么多 BeanPostProcessor?
- 为什么 @Autowired 默认是 byType 而不是 byName?
- 为什么 Spring Boot 的自动配置要用 @ConditionalOnXxx 注解?
每个问题背后都有一条设计主线,拉通理解之后,你会发现 Spring 的知识结构其实非常自洽。
6.2 面试中的“引导式回答”技巧
在实际面试中,有一种回答方式很占便宜,叫“引导式回答”。就是你在回答当前问题时,顺手抛出一个与之强关联、且你非常擅长的子话题,引导面试官往下追问。
举个例子,面试官问“Spring 怎么解决循环依赖”。你在回答完三级缓存之后,可以顺带加一句:“其实循环依赖在工程里最好还是避免,使用构造器注入能从设计层面直接杜绝这个问题。”这时候面试官大概率会顺着问“那你平时怎么用构造器注入?”然后你就可以把话题引到你最熟悉的依赖注入风格上。
再比如面试官问“@Transactional 失效有哪些场景”。你答完自调用、异常被吞、private 方法之后,可以补一句:“我最近排查过一个 REQURIES_NEW 的坑,是连接池被耗尽的案例,最终发现是传播行为选型的问题。”这既能展示你的项目经验,也能把对话导向你有准备的领域。
当然,引导式回答的前提是你真的有料。技术面试最忌讳的就是抛出一个自己都没弄明白的概念去引面试官追问,那属于自掘坟墓。我的建议是:把自己的项目经验里踩过的坑、排查过的疑难杂症整理成 5 到 8 个“故事”,每个故事都要能和技术原理对上号,面试时灵活调用。
6.3 一份可直接复用的自检清单
在面试前,你可以拿下面这份清单自检一遍,看哪些点能脱口而出,哪些点需要再温习:
| 考点 | 我能讲清楚吗 | 关联的追问点 |
|---|---|---|
| IoC/DI | 能/否 | 构造器注入 vs setter 注入,为什么推荐前者 |
| Bean 生命周期 | 能/否 | BeanPostProcessor 和 Aware 接口的使用场景 |
| 循环依赖 | 能/否 | 为什么三级缓存,哪些场景无法解决 |
| 事务失效 | 能/否 | 传播行为选择,回滚异常类型判断 |
| AOP 代理 | 能/否 | JDK/CGLIB 区别,this 调用问题 |
| 自动装配 | 能/否 | @Autowired 和 @Resource 的区别,byType/byName |
| 配置类 | 能/否 | @Configuration 和 @Component 的差异,代理配置类的机制 |
我强烈建议你把每一项都往深入一层再准备。比如“@Autowired 和 @Resource 的区别”,不要只记住一个按类型、一个按名字,而要说到:@Autowired 是 Spring 注解,默认先按类型再按名字;@Resource 是 JSR-250 注解,默认先按名字再按类型,而且它只能用字段和 setter,不能用在构造器上。
6.4 项目场景里的“加分回答”
Spring 面试走到后半程,面试官通常会给一个综合场景题。比如:“你的项目里有一个批量处理任务,涉及大量数据库写操作,你怎么设计事务?”
这种题没有唯一答案,但有一个相对稳定且容易拿分的回答框架:先明确需求边界和异常容忍度,再选择事务策略。如果批量任务不要求严格原子性,建议拆批提交,比如每 500 条提交一次,避免长事务锁表和连接占用过久;如果要求严格一致性,则用大事务,但要注意控制数据量和低频执行;同时配合数据库层的幂等设计、重试机制和补偿任务,形成一个完整方案。
这个回答框架的好处在于:它展示了你对 Spring 事务机制的准确理解,同时把问题从“会不会用注解”上升到了“事务治理体系”的高度。面试官想要的从来不是一个孤立的 @Transactional,而是你在真实业务里对事务边界、性能开销、数据一致性做出的综合权衡。
7. 一些我踩过的坑和积累下来的经验
7.1 从源码读到一个行为之后,最好顺手写个 Demo 验证
读 Spring 源码这件事,大部分人都卡在“看懂了但记不住”的层面。我的经验是:每读透一个机制,就在一个新的测试工程里写一个最小化 Demo,把那个机制触发出来,看一眼真实的打印结果。
比如读三级缓存时,我写了一个 A 依赖 B、B 依赖 A 的 Demo,在全局加了后置处理器的打印日志,亲眼看到了 A 的早期引用从三级缓存里的工厂生成、然后放入二级缓存的过程。光是“眼见为实”这一步,就比纯看十遍源码更管用。
同理,测试事务失效场景时,我把 private 方法、同类调用、异常被吞三种情况各写了一个单元测试,直接看控制台日志里有没有“Transaction synchronization”相关的输出。通过这种方式确认“事务到底有没有生效”,比靠感觉猜要扎实得多。
7.2 面试被追问到底时,承认不清楚比硬编要体面得多
Spring 的知识边界非常宽,没有任何一个人能说自己所有细节都懂。面试中如果被追问到一个确实没研究过的点,比如某个冷门扩展点的内部排序规则,最好坦诚地说“这块我还没深入读过,但我可以推测它的设计意图是……”,然后给出一个建立在合理逻辑之上的推测。面试官其实很吃这一套,因为它展示的是思考能力,而不是背诵能力。
比这个更体面的做法,是主动把你知道的相关信息串联起来做“类比推演”。比如被问到“多线程环境下 AOP 代理的可见性”时,即使你没专门研究过,也可以从 ThreadLocal 传播和执行器框架两个角度来推,错了也没关系,只要逻辑链条清楚,面试官就会认可你的分析能力。
7.3 有些“工作不用但面试要会”的知识点,建议专门腾时间补
我见过不少候选人,工作多年确实没遇到过循环依赖的问题,项目里也没踩过事务失效的坑,但面试就是会问。这背后的逻辑是:面试官不是在考察你是否遇到过这些问题,而是考察你是否具备“遇到之后能快速定位、解决”的潜力。
所以我有一个朴素的建议:无论当前工作里用不用得到,都应该花两到三周的时间把 Spring 这几个核心机制彻底搞透。找一份靠谱的源码解析文章,配一个能跑起来的测试工程,把循环依赖、事务传播、AOP 代理三个核心场景完整地跑一遍、打断点、看调用栈、读日志。这套流程走完之后,你再回头看那些“必问”的面试题,会发现它们之间有了一条清晰的逻辑线,条理自然也就出来了。