最近这两个月,我陆续帮几位准备跳槽的朋友做了几次模拟面试,发现一个很有意思的现象:简历上写着“精通Spring”的候选人,真聊起来,能讲清楚“@Autowired为什么能直接注入”“Bean是什么时候变成单例的”的人其实不多。Spring面试题几乎是Java后端面试的必考区,从IoC、AOP到事务、自动配置,每个点都能往深里追问。这篇文章把我这些年带团队、面候选人时反复遇到的Spring核心问题整理了一遍,按“结论—原理—场景—追问”的顺序展开,适合正在准备校招、跳槽,或者想系统补一补框架底子的开发者直接拿来用。
1. 面试官问Spring时,到底在问什么
1.1 背下来的答案撑不过第二轮追问
先说一个真实场景。候选人对“什么是IoC”倒背如流:控制反转,把对象的创建和依赖交给Spring容器管理。我又追问了一句:“那容器启动时,一个单例Bean是立刻创建还是懒加载?默认情况下Spring是怎么处理的?”对方卡住了。这就是典型的“背题式准备”——记住了术语,但是没有把你正在运行的Spring Boot应用和这些术语对应起来。
面试官要验证的核心其实只有一件事:你有没有真正理解Spring的设计方法论。IoC解决的是什么问题?AOP在什么场景下非用不可?事务失效你实际遇到过没有?这些都不是靠背能够蒙混过关的,反而在追问两三轮之后,真实水平暴露得非常快。尤其是现在的面试节奏,很多公司一上来就是连环追问,从“Spring容器怎么启动”一路问到“三级缓存里到底存的是什么”,靠背题根本撑不过去。
我面过不少候选人,有些基础题答得很快,但一被追问细节就慌。最常见的情况是:知道@Transactional能开事务,不知道整个调用链要走代理对象;知道Spring Boot能自动配置,不知道AutoConfiguration.imports文件里写了什么。这背后其实不是记忆力的问题,而是没有把Spring当成一个“有设计逻辑的系统”来理解,只是把它当成了一堆注解的集合。
1.2 把知识体系分成四个层次来准备
我把Spring相关的知识梳理成四个层次,用来判断自己准备到了哪个位置:
- 会用层:注解、自动注入、starter依赖,能写出能跑的代码。
- 原理层:IoC容器思路、Bean生命周期、AOP代理机制、事务传播行为。
- 源码层:三级缓存、自动配置加载、MVC请求处理链路。
- 设计层:为什么Spring要这样设计,以及你在项目中如何基于这些机制做扩展。
大多数面试题卡在第一层和第二层之间。如果你的目标是中大厂,至少要把原理层和源码层的关键节点串起来。复习优先级上,我的建议是:IoC与Bean生命周期、循环依赖、AOP与代理、事务,这四个点先吃透,再去看Spring Boot自动配置和MVC流程,最后补一补Spring Security、分布式锁这类场景题。这个顺序不是随便排的——IoC是地基,Bean生命周期是理解容器运作的钥匙,循环依赖是考察你对容器机制理解深度的标尺,AOP和事务则是最常见的生产级应用。
不同职级和岗位的考察侧重也有差别。校招和初级岗更看重会用层和原理层的基础题,问得直接,比如“BeanFactory和ApplicationContext有什么区别”;中高级岗位则喜欢拿场景题来考,比如“一个事务方法里开子线程去操作数据库,事务还有效吗”。你准备的时候,最好能根据自己投递的岗位级别调整深度,别用一套固定内容应对所有面试。
2. IoC容器:从“控制反转”聊到Bean完整生命周期
2.1 控制反转到底“反”的是什么
不用Spring的时候,你创建一个UserService依赖UserMapper,代码里得自己new:
public class UserService { private final UserMapper userMapper = new UserMapper(); }这个依赖关系写死在类里面,想换一个实现,得改源码重新编译。用了Spring之后,你只需要声明字段,容器会在合适的时候把依赖给你:
@Service public class UserService { private final UserMapper userMapper; public UserService(UserMapper userMapper) { this.userMapper = userMapper; } }“反转”两个字,说人话就是从“我主动创建对象”变成“容器把对象给我”。对象的创建、组装、销毁这些事,原本由程序员管,现在交出去给容器管,这就是控制反转。依赖注入是实现IoC最常见的方式,Spring里主要有三类注入方式:构造器注入、setter注入、字段注入。面试时如果被问到推荐哪一种,我一般推荐构造器注入——依赖清晰、对象创建后不可变、方便写单元测试,还能顺便规避构造器注入场景下的循环依赖问题。字段注入最省事,但依赖是隐式的,测试的时候不好处理,也不利于依赖关系审查。
关于@Autowired和@Resource的区别,这个基础题也经常出现。@Autowired是Spring提供的,默认按类型注入;@Resource是JDK提供的,默认按名称注入,找不到名称再按类型。如果你用@Autowired注入一个接口有多个实现类,Spring会先按类型找,发现有多个实现之后再看字段名是否和某个Bean名字匹配,还不行就会报错。这个问题的价值在于考察你有没有在项目里实际面对过多实现注入的冲突,而不只是背两个注解的差异。
2.2 Bean的完整生命周期
“Bean生命周期”是Spring面试里的高频题,也是很多候选人答得最碎的一道题。完整链条大致是这样的:
- 解析配置得到BeanDefinition,Spring知道你要创建什么类、作用域是什么、是否懒加载。
- 实例化Bean,通过构造器或者工厂方法生成原始对象。
- 属性填充,处理@Autowired、@Resource等注解,把依赖塞进去。
- 执行Aware接口回调,比如BeanNameAware、ApplicationContextAware,让Bean感知到容器环境。
- 调用BeanPostProcessor前置处理,常见实现如ApplicationContextAwareProcessor就藏在这一步。
- 执行初始化逻辑,顺序是@PostConstruct、InitializingBean接口、自定义的init-method。
- 调用BeanPostProcessor后置处理,这一步非常关键,AOP动态代理多数情况下在这里生成。
- Bean进入容器单例池,等待被使用。
- 容器关闭时执行销毁逻辑:@PreDestroy、DisposableBean接口、destroy-method。
很多候选人能背出“实例化、属性填充、初始化、销毁”,但说不清“@PostConstruct和InitializingBean谁先执行”,也说不清“代理对象是在哪个阶段生成的”。记住上面这个顺序,尤其是第5步到第7步,面试官很容易在这一段往下追问。你还可以主动补充一句:BeanPostProcessor是Spring扩展机制里最重要的口子,像AOP、自定义注解扫描、属性注入,几乎都是靠它实现的。这句话一说,面试官就知道你不是只背了生命周期,而是看懂了整个容器扩展模型。
2.3 Bean的作用域与懒加载陷阱
Bean的常见作用域有singleton、prototype、request、session。默认是singleton,容器启动时大部分单例Bean就会创建,除非配置了@Lazy。这里有个典型的面试追问:“一个prototype Bean被注入到singleton Bean里,会是什么效果?”答案是:如果注入的是字段,prototype其实只生效一次,不是每次调用都拿新的。真正想每次调用都拿到新实例,要用ObjectFactory 或者@Lookup,这个点面试里能答出来就是加分项。
追问到这里还有个变体:“为什么@Lazy能解决循环依赖?”因为@Lazy会生成一个代理对象注入到依赖方,真正调用时再去找目标Bean,相当于把“需要真实依赖”的时刻延后了。理解了这个机制,你在项目里遇到奇怪的无依赖错误时,也能更快定位原因。
3. 循环依赖与三级缓存:这道题能刷掉一半候选人
3.1 先理清循环依赖的本质
循环依赖的场景很常见:A依赖B,B又依赖A。Spring默认管理的单例Bean如何让两个对象互相引用?答案藏在一、二、三级缓存里。源码就在DefaultSingletonBeanRegistry里:
// 一级缓存:成品Bean private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256); // 二级缓存:提前暴露的半成品Bean private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16); // 三级缓存:保存ObjectFactory,用于生成提前引用的对象 private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);三个缓存的名字网上有各种说法,但职责要记住:一级放的是创建完成的最终Bean;二级放的是“被提前引用出去的早期对象”;三级存的是ObjectFactory工厂,它决定了要不要生成一个代理对象。网上有些文章把一级缓存叫“单例池”,这个说法也可以,但面试时最好先说出源码中的变量名,再解释用途,显得你真的读过源码。
3.2 三级缓存的工作流程
看一个简单例子:A和B互相依赖,都用setter注入。
- A开始创建,实例化出原始A对象,此时A还没有属性。Spring把A的ObjectFactory放进三级缓存,这个工厂可以在需要时提前产出A。这一步叫“提前暴露”。
- A填充属性时发现依赖B,于是去创建B。
- B实例化完成,B填充属性时发现依赖A。此时A还没创建完,但是三级缓存里已经有A的ObjectFactory。B通过这个工厂拿到A的引用,同时这个引用被放入二级缓存。
- B继续走完属性填充和初始化流程,成为成品Bean,进入一级缓存。
- 回到A,B已经创建好,A完成属性填充。A继续初始化,成为成品进入一级缓存。
看起来二级缓存好像也没那么必要?为什么要三级而不是二级?这是绝大多数面试官的追问点。
关键原因在于:Spring在生成最终Bean时,可能要在后期通过BeanPostProcessor生成代理对象(比如@Transactional、@Async)。如果只有二级缓存,提前暴露出去的A就是原始对象;等到A真正完成创建时,却发现应该暴露的是代理对象,二者不一致,B那边持有的引用就错了。三级缓存里的ObjectFactory可以在“B需要引用A”的那一刻决定:直接返回原始A,还是生成一个代理A。如果需要代理,就提前把代理对象放到二级缓存;如果不需要代理,原始对象放二级缓存。这样保证了提前暴露给B的引用,和最终对外提供的对象是一致的。换句话说,三级缓存解决的是“代理对象生成时机不确定”的问题,这是二级缓存做不到的。
3.3 哪些循环依赖Spring救不了
面试里如果说“Spring能解决循环依赖”,一定要加限定条件:默认只解决单例Bean、setter注入/字段注入场景下的循环依赖。以下几种情况救不了:
- 构造器注入:实例化A之前就必须有B,实例化B之前又必须有A,没法提前暴露。
- prototype作用域:每次都是新的,三级缓存里根本不会保存工厂。
- 某些情况下Bean本身需要强制提前代理,比如@Async,也容易出现循环依赖异常。
- 异步场景还牵涉到提前暴露的引用不是同一对象的问题,排查起来很头疼。
解决手段一般有三个:改设计,把循环引用拆开;用@Lazy延迟注入其中一个依赖;或者用@DependsOn强制控制初始化顺序。还有一个背景知识要了解,Spring Boot 2.6开始默认禁止循环依赖,启动时直接报错,需要通过spring.main.allow-circular-references=true显式打开。所以面试里被问到“你们项目里怎么看待循环依赖”,不要只说“Spring能解决循环依赖”就完事,最好带一句“能避免就避免,项目里出现循环依赖大概率是设计有问题”。这句话才是面试官想听的——你不仅要懂机制,还要有正确的工程判断。
4. AOP与动态代理:别再只会说“横切关注点”
4.1 AOP解决什么
Spring AOP的核心价值是“横切逻辑复用”。典型场景是日志、权限、事务。比如你给每个Controller写日志,如果不用AOP,就是一个方法一个方法加代码,侵入性强还容易漏。用了AOP,定义一个切面,在方法执行前后自动记录日志,核心业务代码保持干净。Spring AOP的实现不是编译期改字节码,而是基于动态代理在运行时生成代理对象。你声明一个接口的实现类并配置切点,Spring容器最终返回给你的其实是代理对象,调用方法时会先经过代理逻辑。
这个“代理对象”的概念是整个AOP的基础,很多人只记住了“动态代理”四个字,却说不清楚容器里到底是JDK代理还是CGLIB代理,以及两者分别在什么条件下生效。面试官往往就是从这里开始分层的。
4.2 JDK动态代理与CGLIB怎么选
这是面试中出现频率非常高的对比题,建议用表格记清楚:
| 维度 | JDK动态代理 | CGLIB |
|---|---|---|
| 实现方式 | 基于接口 | 生成目标类的子类 |
| 目标类要求 | 必须有接口才能代理 | 目标类不能被final修饰,方法也不能是final |
| 创建与调用性能 | 创建速度快,反射调用有一定开销 | 创建字节码耗时稍多,调用性能较好 |
| 无法代理的情况 | 没有接口的类 | final类、final方法、static方法 |
| Spring Boot默认情况 | 传统Spring默认优先JDK | Spring Boot 2.x之后默认proxyTargetClass=true,直接用CGLIB |
追问环节经常出现:“当目标类有接口时,为什么Spring Boot还是倾向用CGLIB?”因为JDK代理只能代理接口里声明的方法,如果类里面新增了一个不在接口中的方法,事务和日志就拦截不到;而且CGLIB创建出的代理对象更贴近目标类,很多框架在桥接老代码时反而更省心。我在项目里确实遇到过这类问题:一个Service实现了接口,但有一部分方法不在接口里,用JDK代理时这些方法上的日志切面完全不生效,后来改成CGLIB才算彻底解决。
4.3 容易被追问的两处细节
第一处是@Configuration类代理。Spring的@Configuration类默认被CGLIB增强,目的是保证@Bean方法之间的调用走代理,确保单例效果。比如:
@Configuration public class MyConfig { @Bean public A a() { return new A(); } @Bean public B b() { return new B(a()); } }如果没有CGLIB代理,b()里的a()是直接new一个新的A,单例就被破坏了。Spring通过代理拦截了这次方法调用,返回容器里的同一个A。这个知识点如果能在面试里主动提出来,面试官会默认你对代理的理解不是停留在使用层面。顺带还可以提一句:Spring在解析@Configuration时,Lite Mode和Full Mode的区别,重点就在于这个类有没有被CGLIB增强。
第二处是通知类型的执行顺序。@Around的proceed是绕行,@Before在所有前置逻辑里最先准备,@After在方法结束之后执行,@AfterReturning和@AfterThrowing根据是否抛异常二选一。一个常见的执行顺序是:@Around前置逻辑 → @Before → 目标方法 → @AfterReturning/@AfterThrowing → @After → @Around后置逻辑。实务中如果想统一处理异常,一般会在最外层用@Around包住整个链路。面试时能把这个顺序画出来,基本就过关了。
5. Spring事务:@Transactional为什么会失效
5.1 事务注解的底层原理
直接说结论:@Transactional本身不实现事务,它靠的是Spring AOP。容器里你的Service实际上是一个代理对象,调用方法时事务拦截器会判断方法上有没有@Transactional,有就开启事务、执行方法、提交或回滚。所以事务生效的前提是:你的Bean被Spring容器管理,而且调用链路经过了代理对象。这两个点很多候选人答得出,但下面这个经典场景经常翻车。
面试官最常拿来试水的追问是:“那整个事务是围绕什么来实现的?”你如果能说出TransactionInterceptor和TransactionManager,说明你真看过源码。Spring执行事务的核心逻辑:通过事务拦截器拿到事务信息,包装成事务回调,再委托给PlatformTransactionManager完成commit或rollback。能把这条链路讲清,面试官基本不会纠结你对Spring事务是“背概念”还是“懂原理”。
5.2 事务失效的经典场景清单
面试官说“给我说几个@Transactional失效的场景”,大多数候选人能说出两三个。我把高频场景整理成清单,你准备的时候按这个来:
- 自调用。类内部方法A调用方法B,B上面有@Transactional。因为this调用不会经过代理对象,事务失效。
- 方法非public。@Transactional默认只对public方法生效,背后原因是Spring事务拦截器对非public方法不会主动增强,这点要注意。
- 异常被捕获后没有重新抛出。事务拦截器看不到异常,自然无法回滚。
- 抛出的异常是CheckedException,且没有配置rollbackFor。Spring的默认策略是运行时异常回滚,编译异常(如IOException)不会触发回滚。
- 类没有被Spring管理。没加@Service、@Component,整个类都不是代理Bean。
- 多线程调用。事务是绑定在线程上的,你在事务方法里另起一个线程操作数据库,那个线程里执行的是无事务逻辑。
- final方法被CGLIB代理不了。
- 传播行为设置不对,比如把方法设置成NOT_SUPPORTED。
5.3 从一个具体例子讲清楚自调用问题
自调用失效这个问题,我建议用一个例子来回答:
@Service public class OrderService { @Transactional(rollbackFor = Exception.class) public void createOrder() { saveOrder(); } @Transactional(propagation = Propagation.REQUIRES_NEW) public void saveOrder() { // 数据库操作 } }面试时如果只是说“两个方法在一个类里,事务不生效”,显得太单薄。你可以继续说:解决思路是把事务方法抽到另一个Service类里注入进来调用,或者通过ApplicationContext.getBean拿到代理对象再调用。其实Spring官方也是建议通过被代理的实例来调用,毕竟事务是基于代理的机制。这样回答,既亮了知识点,又体现出你踩过坑后有实际解决方案。
事务传播行为里,REQUIRED、REQUIRES_NEW、NESTED这三者是面试重点。REQUIRED表示加入当前事务,没有就新建,这是默认值;REQUIRES_NEW是挂起当前事务,开一个新事务;NESTED是嵌套事务,内层回滚可以只回滚自己并标记回滚点,外层决定整体是否提交。能区分这三者在“内层方法抛出异常,外层是否需要感知”上的差异,事务这块基本稳了。
6. Spring Boot自动配置与Spring MVC:拼图的最后两块
6.1 自动配置到底做了什么
我看过很多人的简历,写着“熟练掌握Spring Boot自动配置原理”,问他“Spring Boot是怎么知道要帮你配置DataSource的”就答不上来。自动配置的核心链路其实不复杂:@SpringBootApplication是一个组合注解,里面包含@EnableAutoConfiguration。这个注解通过导入AutoConfigurationImportSelector,扫描一个固定的配置文件,把所有候选自动配置类读进来。在Spring Boot 2.7之前是META-INF/spring.factories,从2.7开始改为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration候选配置类加载之后,Spring会根据条件注解决定到底启用哪些。比如DataSourceAutoConfiguration上有@ConditionalOnClass,只有classpath里存在javax.sql.DataSource相关类才会处理;@ConditionalOnMissingBean则确保你如果自定义了DataSource,Spring就不重复创建。
6.2 为什么说“自动配置”其实是“按需装配”
准确的说法是:Spring Boot不是把所有的配置都加载进来,而是“候选列表+条件判断”双重过滤。很多候选人一谈自动配置就只说“加载了配置文件”,少了条件判断这一层,回答深度立刻下来一截。条件注解里,@ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty是最常用的三个,前两个控制类和Bean层面的装配,第三个控制配置项层面的装配。能把这三个说清楚,面试官就不会觉得你是在只会用Spring Boot并不会“理解”它。
如果面试进一步问“怎么自己写一个starter”,动手思路是这样的:写一个自动配置类,在里面声明需要创建的Bean;用@ConditionalOnClass、@ConditionalOnProperty做条件控制;把类路径写到AutoConfiguration.imports文件里;最后用@ConfigurationProperties绑定配置属性。这个流程在微服务团队里经常会用到,比如自己做一套通用的Redis配置、通用的消息队列封装,本质上都是干这件事。能举出这类真实场景,比说一百句“自动配置很方便”都管用。
6.3 Spring MVC请求处理流程
Spring MVC处理一次请求,核心链路可以这样拆:
- DispatcherServlet接收请求。
- 通过HandlerMapping找到对应的Handler,以及拦截器链。
- 通过HandlerAdapter执行Handler方法,也就是真正调用Controller里的代码。
- Controller返回逻辑视图名,或者直接返回JSON数据。Spring Boot下用@RestController返回的其实是Jackson序列化之后的JSON。
- 如果返回的是视图名,ViewResolver负责找到对应的模板渲染。
- 整个链路走完后响应返回给客户端。
面试官围绕这个链路常问两个点。一是父子容器的理解:Spring MVC的Web层容器会以Root容器为父容器,Controller扫描配置在子容器里,Service、Repository扫在父容器里,子容器能看到父容器的Bean,反过来不行。二是@RestController和@Controller的区别,本质上就是@Controller加@ResponseBody,方法返回结果直接写进响应体,不再经过视图解析器。把这两点答清楚,Spring MVC部分基本不会被追到死角。
7. 让回答“活”起来:面试官真正想听到的答法
7.1 把每个题目当成一个小型讲解来准备
我面试完以后,经常会给候选人复盘:很多答案明明是对的,但听起来像在背课文。一个更好的组织方式是“结论—原理—例子”三段式。比如被问到“循环依赖”,你先说结论:Spring通过三级缓存解决单例Bean基于setter注入的循环依赖。然后讲原理:一级放成品、二级放提前暴露的对象、三级放ObjectFactory,关键在代理对象什么时候生成。最后给例子:A依赖B、B依赖A,把创建的几个步骤串一遍。一套下来大约两三分钟,信息密度高,又不是纯背诵。
7.2 准备一个能把多个知识点串起来的案例
个人强烈建议准备一个贯穿案例,把你熟悉的框架知识点串在同一个业务场景里。比如一个用户注册接口:请求进来,DispatcherServlet分发;UserServiceImpl被IoC托管,构造器注入Mapper;事务注解开启事务;密码安全校验走AOP切面;日志记录走统一切面。面试官问任何一环时,你随时能从这条链路里抽出对应环节展开,比一个个孤立的题答起来自然得多。准备的时候,最好还能在这个案例里嵌一两个“坑”,比如自调用导致事务失效,这样被追问时你也能马上切到实战视角。
7.3 面试前做两轮耐压测试
准备到后面,建议找一个不是Java技术栈很熟的朋友来模拟面试,因为外行提问更发散,反而能逼你把话说清楚。第一轮测试每个必考题的“结论—原理—例子”是否顺畅;第二轮专门练追问,比如把“为什么需要三级缓存”连续追问三层。我自己带人准备跳槽时最常用的套路,就是让候选人把每个核心问题讲给一个完全没听过的人听,讲得清楚才算真的学会。另外一个实用小技巧是:把每个必考题整理成一面卡片,正面写问题、背面写三个关键词,临面试前快速过一遍,比抱着整本八股文翻来覆去效率高得多。这个办法你面试前也可以试试,效果比闷头背题好得多。