Spring面试题这个东西,我在面试别人的时候见过太多“背题式”回答了。候选人能把Bean的生命周期八步背得滚瓜烂熟,但你一问“三级缓存到底是怎么处理循环依赖的”,或者“同一个类里两个方法互相调用,事务为什么失效”,立马卡壳。说白了,Spring面试题考查的不是记忆力,是你有没有真的把它当工具用明白过。这篇内容我不做那种“一百道题加答案”的清单,而是把高频考点背后的原理拆开,讲清楚每一个考点到底在考什么、面试官想听什么、怎么答才显得不像背答案。
1. 面试官为什么总从“Spring是什么”开始
1.1 控制反转,到底反转了什么
几乎所有Spring面试题开场都是“谈谈你对IoC的理解”。这个题看似基础,其实是个分水岭。能说出“控制反转就是把对象创建和依赖管理的控制权交给容器”的人很多,但能往下说透“反转前是什么样、反转后解决了什么问题”的人很少。
反转前的世界是这样的:你在Service里new一个Dao,在Controller里new一个Service,对象和对象之间硬编码耦合。想换一个实现类,得去改源码;想给某个对象加一个公共逻辑(日志、事务),得在每个new的地方重复写。这还不是最痛的,最痛的是当你需要单元测试时,发现被测对象依赖了一堆具体类,根本没法轻松替换成Mock。
反转后的逻辑也很直白:对象不再由自己创建依赖,而是声明“我需要什么”,容器负责把对应的Bean注入进来。你在类上写个@Autowired,容器启动时就扫描、实例化、按类型或名称匹配、把依赖塞进去。你可以把它理解成——以前是自己买菜做饭,现在是你只负责点菜,后厨(容器)把菜配好端上来。至于菜是从哪个供应链来的(Bean是单例还是原型,来自XML还是注解配置),你不需要关心。
面试时我会建议你在这个基础上再补一句:控制反转的本质不是“不用new了”,而是把“对象之间的协作关系”从代码里抽离出来,放到容器里统一管理。这句话一出来,面试官就知道你理解的是设计思想,不只是API用法。
1.2 Bean的生命周期怎么讲才算出彩
Bean生命周期是Spring面试题里怎么绕都绕不开的。很多背题清单会把步骤列成十几条,候选人背得痛苦,面试官听着也痛苦。我建议你换个讲法——只讲五个关键阶段,每阶段带一句“容器在做什么”:
- 实例化:容器通过构造器或工厂方法创建Bean的原始对象,此时属性还没赋值。
- 属性填充:容器执行依赖注入,把
@Autowired、@Value、XML里配置的property逐一塞进对象。 - 初始化前:执行
BeanPostProcessor的postProcessBeforeInitialization,各种*Aware回调(比如ApplicationContextAware)在这个阶段触发。 - 初始化:执行
@PostConstruct注解方法、InitializingBean的afterPropertiesSet、XML配置的init-method。 - 初始化后:执行
BeanPostProcessor的postProcessAfterInitialization,AOP代理生成就在这里发生——容器把原始对象包装成代理对象放进单例池。
这五个阶段背下来不难,真正的加分项是你主动提一句“初始化前和初始化后这两个阶段是Spring扩展能力的核心位置,像@Transactional、@Async这类功能,本质都是通过后置处理器在初始化后阶段把Bean包成代理”。这句话一出口,就把生命周期和后面的AOP、事务串起来了,面试官会认为你是真的理解,而不是考前突击。
1.3 想彻底理解容器,手写一个迷你IoC就够了
热词里有“手写spring”,这个不是让你真把框架重写一遍,而是有一个非常高效率的学习方法——用几百行代码写一个只能扫包、注册Bean、执行简单依赖注入的迷你容器。我当年就是这么干的,效果比看十遍源码注释都好。
迷你容器核心就三块:扫描注解收集Class、用反射实例化、遍历字段做注入。代码量不大,核心逻辑类似这样:
// 简化的迷你IoC容器核心逻辑 public class MiniApplicationContext { private Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); public void scan(String packageName) throws Exception { // 1. 扫描包下所有Class,过滤带@MiniComponent注解的类 // 2. 对每个类调用反射创建实例,放进singletonObjects // 3. 遍历所有Bean的字段,找带@MiniAutowired的字段,从容器里取出依赖注入 } public <T> T getBean(Class<T> clazz) { return clazz.cast(singletonObjects.get(clazz.getName())); } }写完你就明白一件事:Spring容器的工作远不止“反射创建对象”这么简单,它还要处理构造器参数解析、循环依赖、代理、作用域、事件发布、国际化……迷你容器帮你建立的是“容器大概长什么样”的直觉,后续再去看源码细节,心里就有了坐标。
2. 三级缓存解决循环依赖的完整现场推演
2.1 循环依赖长什么样,什么时候会触发
循环依赖的典型场景是两个Bean互相引用:Bean A的构造或属性里需要Bean B,Bean B的构造或属性里需要Bean A。如果两边都是构造器注入,Spring直接报错,因为实例化A需要先实例化B,实例化B又需要先实例化A,死锁了,没有解。如果两边都是属性注入(@Autowired字段或Setter),Spring就能通过三级缓存绕过去。
面试时建议你亲手画一下这个流程。假设先创建A:
- A被实例化——对象A刚new出来,属性还是空的,但这已经是个“半成品”对象了。
- A发现自己需要B,去容器里找B,发现B还没创建。
- 创建B,B实例化后发现自己需要A,在容器里找A——这时候A虽然还没有完成属性填充,但它的“早期引用”已经在三级缓存里了。
- B拿到A的早期引用,完成自己的创建,放进一级缓存。
- 回到A,把已经创建好的B注入进来,A继续走完属性填充和初始化,最终放进一级缓存。
整个过程里最关键的一句是:Spring用“提前暴露半成品对象”的方式,打破了互相等待的死局。
2.2 为什么三级缓存不能改成二级
这是Spring面试题里最狠的追问。很多人能背出三个Map的名字:singletonObjects(一级缓存,存完整成品)、earlySingletonObjects(二级缓存,存早期暴露的半成品或代理)、singletonFactories(三级缓存,存ObjectFactory工厂)。但问到“为什么非得三级”就沉默了。
我来把这个点讲透。三级缓存里的ObjectFactory,本质是一个延迟决策的“函数”。Spring在实例化A后,立刻把这个工厂塞进三级缓存,但它此时不知道A最终是否需要AOP代理——这个决定要等A跑完整个生命周期、经过BeanPostProcessor之后才知道。
如果只有二级缓存,Spring就必须在提前暴露的那一刻立刻决定:给依赖方的是原始对象还是代理对象。问题来了,万一此刻判断下来不需要代理,把原始对象暴露给了B,结果后续初始化阶段又冒出了需要代理的逻辑(比如某个后置处理器加了切面),那B持有的就是原始对象,和容器最终生成的代理对象不是同一个,AOP功能就悄悄失效了。
三级缓存等于把决定权往后拖:B说“我要A”,容器不是直接甩给它一个对象,而是给它一个工厂,工厂在执行的时候才去问“A到底要不要代理”,要就给代理,不要就给原始对象。这种“延迟决策”设计,既保证了循环依赖能解开,又保证了AOP代理不被破坏。你能把这个逻辑讲清楚,这道题基本就满分了。
2.3 哪些场景下三级缓存也救不了你
面试里紧接着的一个问题是“是不是所有循环依赖都能解决”。答案是显然不能,你应该主动列举三个典型场景:
- 构造器注入的循环依赖:A的构造器参数里有B,B的构造器参数里有A。实例化A的前提是拿到B,可B都还没出生,三级缓存里也没有A的早期引用(因为A还没被new出来),直接报
BeanCurrentlyInCreationException。 @Async注解的Bean循环依赖:异步代理的创建时机比较特殊,和普通AOP代理不在同一个节点上,强行走三级缓存很容易拿到半成品代理导致NPE。- 非单例Bean(原型作用域)循环依赖:原型Bean根本不在缓存体系里,每次获取都是新建,谈不上提前暴露,循环依赖必然失败。
答完这三个场景,你再补一句“所以设计依赖关系时先想清楚是不是真的需要互相引用,很多循环依赖其实是代码结构问题,拆开一个方向就好”,这句话会让面试官觉得你不仅懂原理,还有工程审美。
3. Spring Boot启动和自动配置的真正主流程
3.1 @SpringBootApplication背后藏着三个注解
Spring Boot面试题十有八九绕不开@SpringBootApplication。很多人知道它是一个复合注解,但不知道每一层的职责。拆开看,它是三个注解的组合:
@SpringBootConfiguration:本质上就是@Configuration,声明当前类是一个配置类。@ComponentScan:启动包扫描器,默认扫描启动类所在包及其子包下的所有@Component、@Service、@Repository、@Controller。@EnableAutoConfiguration:自动配置总开关,这是Spring Boot最核心的机制。
有一个很常见的坑你必须知道:@ComponentScan默认只扫启动类所在包及子包。很多人把Bean放在启动类的父包或兄弟包里,结果启动就报NoSuchBeanDefinitionException,排查半天以为是注入写错了。面试时主动提到这个坑,会显得你实际写过Spring Boot项目。
3.2 自动配置是靠什么“猜”出你的需求的
自动配置的底层逻辑,一句话概括:Spring Boot在启动时读取所有jar包里的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(Spring Boot 2.7之前的版本叫spring.factories),把里面列出的自动配置类全部加载进来,然后用一批条件注解决定“哪些配置类真正生效”。
条件注解是理解自动配置的钥匙,最常用的有几个:
@ConditionalOnClass:classpath里存在某个类时才生效,很多自动配置类靠它判断“你是不是引入了这个库”。@ConditionalOnMissingBean:容器里没有指定Bean时才生效,这就是你自定义配置为什么能覆盖默认配置的原因。@ConditionalOnProperty:配置文件里指定了某个属性值才生效,比如spring.redis.enabled类的开关。
我见过很多候选人把自动配置机制描述得神神秘秘,其实它就是“一堆配置类 + 一堆条件判断”。你能当场说出来“条件注解是Spring Boot的决策引擎,它让框架在默认配置和用户自定义之间做了优雅的调和”,面试官基本就满意了。
3.3 启动流程的关键里程碑
SpringApplication.run()这一行代码背后做了太多事,面试不需要背全流程,但你应该记住最核心的四个里程碑:
- 准备环境:读取
application.yml、命令行参数、系统环境变量,组装成Environment对象。 - 创建容器:根据应用类型(Web应用还是非Web应用)创建对应的
ApplicationContext。 - 刷新容器:这是最重的一步,内部执行BeanFactory的配置、BeanPostProcessor注册、事件发布、所有单例Bean的实例化,还有前面说的自动配置类加载。
- 启动完成:发布
ApplicationReadyEvent,启动内嵌Web服务器开启端口监听。
你可以用“做饭”来类比:环境准备是买菜和洗菜,创建容器是架好锅灶,刷新容器是把所有菜按顺序下锅,启动完成是端上桌。这个类比虽然朴素,但能帮面试官确认你不是死记硬背,而是理解了大方向。
4. Spring Security一次登录请求背后的过滤器链
4.1 从请求进来开始走的完整路径
Spring Security是Spring面试题里的热门分支,尤其是微服务项目,几乎必问认证与授权。最经典的问题是:“用户提交用户名密码到登录成功,中间到底发生了什么?”
完整的路径是这样的:请求先进入Servlet容器,经过FilterChainProxy——这是Spring Security过滤器链的统一入口。过滤链上挂着成串的委托过滤器,其中最有名的是UsernamePasswordAuthenticationFilter。这个过滤器专门处理表单登录请求,它从请求里提取用户名和密码,封装成一个UsernamePasswordAuthenticationToken。
注意,此时这个Token处于“未认证”状态。过滤器把它交给AuthenticationManager——它不干活,是个协调器,负责把任务分派给一组AuthenticationProvider。表单登录场景下轮到DaoAuthenticationProvider,它调用UserDetailsService.loadUserByUsername()从数据库里查用户,查到后用密码编码器(PasswordEncoder)比对密码。比对通过,Token被标记为已认证,里面还会带上用户的权限列表。
最后这个已认证的Token会被放入SecurityContext,再挂到SecurityContextHolder上——这个Holder用ThreadLocal存储,当前线程后续所有代码都能随时拿到登录用户信息。
4.2 SecurityContextHolder和异步线程的坑
SecurityContextHolder默认策略是ThreadLocal,这带来一个经典连环坑:你在一个请求线程里登录了,如果在代码里另开了一个新线程(比如把任务丢给线程池异步执行),新线程里取SecurityContextHolder.getContext(),拿到的永远是空的。
我见过不少项目因为这个线上出问题——登录状态下发了一条异步消息,消息处理里要取当前用户名,结果取出来是null,排查半天才发现是ThreadLocal不跨线程的问题。
正确的解决手段有三个方向:
- 配置
MODE_INHERITABLETHREADLOCAL,让子线程继承父线程的上下文,但对线程池不友好(线程复用会串数据)。 - 在提交任务时手动把Authentication对象传到子线程里。
- 用
DelegatingSecurityContextExecutor包装线程池,由框架自动传递上下文。
面试时能把这个坑和解决方案讲出来,说明你在真实项目里被Spring Security虐过,这比背十道概念题都管用。
4.3 无状态服务下的过滤器链调整
现在的项目大多做前后端分离,登录后用JWT而不是Session。这时候你要清楚一个关键调整:UsernamePasswordAuthenticationFilter是处理表单登录的,JWT模式下你不一定走这个过滤器,而是在前面加一个自定义的JWT解析过滤器。它负责从Authorization头里取出Token、解析出用户信息、手动塞进SecurityContextHolder。
真正的登录校验在认证服务里做一次,后续每个请求都只是“Token解析和上下文填充”。这个设计能支撑水平扩展,因为服务端不存Session,每个节点只认识Token本身。这种思路在Spring Cloud微服务体系里尤其重要,网关统一鉴权,下游服务只信任上游传过来的安全上下文。
5. @Transactional失效的场景,比背传播行为更重要
5.1 事务失效的四个经典场景
Spring事务相关面试题里,“什么情况下事务会失效”出现频率极高。我列举四个一定会考的:
第一,@Transactional注解加在了非public方法上。Spring默认用JDK动态代理或CGLIB代理来实现事务,代理只拦截public方法调用,非public方法直接穿透,事务完全不生效。虽然框架不一定报错,但这是最容易被忽略的坑。
第二,异常被方法内部catch掉了。数据访问抛出的异常在事务块内部被你捕获、记录日志、没有重新抛出,框架根本感知不到异常,自然就不会回滚。这是我见过最多的线上事故场景,很多团队排查半天,最后发现是catch吞掉了异常。
第三,自调用问题。同一个类里方法A调方法B,B上面标了@Transactional——事务不生效。原因很简单:Spring事务基于代理,A调用B时,调用的是this.B()而不是代理对象的B,事务逻辑根本没机会介入。
第四,默认只回滚RuntimeException和Error。如果业务方法抛出的是受检异常(比如IOException),事务默认不会回滚。想回滚受检异常,要用@Transactional(rollbackFor = Exception.class)。
5.2 自调用问题怎么优雅解决
自调用问题让很多人头疼过,但其实解法很朴素。最推荐的做法是拆分:把需要事务的方法挪到另一个独立的Service类里,让Spring来管理跨对象的代理调用。还有一个方案是在类内部注入自己的代理,比如:
@Service public class OrderService { @Autowired private OrderService self; public void createOrder() { // 事务方法B在另一个对象self上调用,代理生效 self.updateStock(); } @Transactional public void updateStock() { // ... } }注意这里不是循环依赖的问题,Spring允许你在单例Bean里注入自身代理,前提是配置了@EnableAspectJAutoProxy。面试时你能把“代理”两个字贯穿始终——事务、AOP、异步统统都是代理在起作用,面试官就知道你对Spring的认知体系是统一的。
5.3 传播行为只需要分清三种就够了
传播行为有七种,但面试时真正需要说清楚的是三种:REQUIRED、REQUIRES_NEW、NESTED。
REQUIRED是默认值,意思是如果当前已经有事务就加入,没有就新建,绝大多数场景都该用它。REQUIRES_NEW是挂起当前事务、新开一个独立事务,两个事务互不影响,适合“记录日志”这类即使主操作失败也必须成功的场景。NESTED是嵌套事务,它和REQUIRES_NEW最大的区别是——嵌套事务基于保存点(Savepoint),内层事务回滚时可以只回滚到自己开始前的状态,外层事务还能继续;而REQUIRES_NEW是彻底另起炉灶,内外完全独立。
一个很实用的记忆锚点:嵌套事务是“孙子犯了错,把孙子自己的事撤销,爷爷和爸爸的事不受影响”,新事务是“儿子直接搬出去住了,搬出去后发生的一切和原生家庭无关”。这个类比虽然不太严肃,但在面试现场能帮你有条理地组织语言。
6. Spring AI 接入 Agent:框架选型不是非此即彼
6.1 Spring团队为什么在1.0之后快速推进2.0
Spring AI是Spring生态面向大模型应用开发推出的官方框架,2025年发布了1.0正式版,随后很快演进到2.0。这个节奏本身就说明了一件事:企业级Java应用接入大模型的需求非常旺盛,而Spring团队发现,与其让每个项目自己封装OpenAI或通义的HTTP调用,不如直接把“模型对话、工具调用、向量检索、Agent编排”这层能力做成Spring标准的自动配置。
Spring AI对Java开发者最大的价值是:你不需要学习全新的框架和一套陌生的API,它沿用了Spring Boot的开发习惯,配置文件里写模型地址、API Key,代码里注入ChatClient就能发起对话。你可以把Spring AI理解成“JDBC之于数据库”的角色——统一了接入层,屏蔽了各家模型厂商的接口差异。
6.2 ChatClient和@Tool就是Agent最朴素的实现
面试被问到“Spring AI里Agent怎么实现”,千万别上来就扯复杂概念。Spring AI里Agent能力其实就两块核心。
第一块是ChatClient的链式调用,一个普通的对话请求长这样:
// Spring AI ChatClient 链式调用示例 ChatClient chatClient = ChatClient.builder(chatModel).build(); String answer = chatClient.prompt() .system("你是一个订单助手,必要时可以查询订单数据库") .user("查询订单10086的物流状态") .call() .content();第二块是工具调用能力,用@Tool注解把一个Java方法暴露给模型:
// 把Java方法注册为模型可调用的工具 @Tool("根据订单号查询物流状态") public String getLogisticsInfo(String orderId) { return logisticsService.query(orderId); }真正的执行流程是:用户提问 → 模型判断需要调用工具 → 框架把工具名和参数传给Java方法执行 → 执行结果反馈给模型 → 模型基于结果生成最终回答。这就构成了Agent的最基本闭环:模型负责决策“要不要调工具、调哪个、参数是什么”,Java代码负责真正的业务执行。你把这个闭环讲清楚,比背任何Agent定义都实在。
6.3 和LangGraph4j之间怎么选
热词里有一个很纠结点的问题:“现在到底用Spring AI还是LangGraph4j”。我的回答是:先搞清楚两者解决的问题层次。
Spring AI偏“接入层”和“基础能力层”,它解决的是模型接入、统一API、工具注册、向量存储这些基础问题,适合大多数CRUD系统加上一个智能对话/助手功能的场景。LangGraph4j偏“工作流编排层”,它的卖点是让多个模型节点按图的方式协作,适合客服机器人里“意图识别→多轮追问→调用业务系统→生成工单”这种复杂流程。
实际项目中两者完全可以共存:用Spring AI接模型和工具,用图编排把节点串成复杂工作流。不要被“二选一”的舆论带偏,选型的唯一标准是你的业务流程复杂度。这是我在多个项目里验证过的结论,你可以在面试里直接引用。
7. Spring Cloud Alibaba 停更风波后的选型与工程化避坑
7.1 “停更”传闻到底是怎么回事
热词里出现“spring cloud alibaba停更了”,这是个非常有讨论价值的话题。真实的背景是:Spring Cloud Alibaba在某个版本周期里发布节奏放缓,社区里一度出现“是不是不维护了”的猜测。但从长期观察来看,项目并没有彻底停更,后续相继发布了适配Spring Boot 3.x、Spring Cloud 2023.x的版本,并且完成了从孵化到毕业的过程。
面试里如果被问到,建议你把它当一次“关注开源项目健康状况”的话题来谈:选型时更要看重组件的社区活跃度、发布频率、维护者构成。Nacos做注册中心和配置中心,Sentinel做流量控制和熔断降级,Seata做分布式事务,这三个组件目前在国内微服务体系里占有率高,工程实践很成熟。
7.2 微服务体系里三个高频踩坑点
第一个坑是OpenFeign调用超时。Feign默认的读超时时间很短而且容易被人忽略,系统一忙,下游接口响应变慢,上游直接超时抛异常,随之而来的是重试。如果没有配好重试策略,瞬间在数据库上打满慢查询,最坏情况就是雪崩。
第二个坑是Nacos配置变更不生效。很多人改了Nacos里的配置,发现应用没反应。你得确认是不是开了配置自动刷新(@RefreshScope),或者配置文件的dataId和group是不是对得上,另外命名空间写错导致的“明明改了配置,程序读的还是旧的”是排查起来非常折腾的问题。
第三个坑是分布式事务方案过度设计。业务可以接受最终一致的地方,用本地消息表就能解决,非得上Seata AT模式,结果多了一个协调中心要运维,分布式事务的性能开销也上来了。面试时你能说出“事务方案要和业务一致性要求匹配”,这是非常加分的工程判断力。
7.3 Python服务融入Spring Cloud Alibaba的姿势
热词里有“python应用融入spring cloud alibaba微服务体系”,这个诉求在边缘计算或算法服务场景越来越常见。我的经验分享是三个字:走网关。
Python服务不必强行嵌入Java生态,它只需要满足几个条件:把自己的REST接口注册到Nacos(用nacos-sdk-python),保持健康检查端点能被探活,接入统一配置中心。Java服务调用Python服务时照常走OpenFeign,因为Feign本质是HTTP客户端,它不在乎对面是什么语言写的。网关层的路由配置、令牌校验、日志追踪都能对Python服务一视同仁。这套方案的好处是Java和Python团队不必互相理解对方框架,只要遵守协议就能协同。
7.4 Druid连接池配置片段里的隐藏陷阱
热词里有这样一段配置:spring: datasource: druid: remove-abandoned: true,这是阿里的Druid连接池里“移除超时未关闭连接”的开关。这个配置本意是防止连接泄露,但我见过团队把它开得过于激进,导致正常的长事务连接被误杀,业务跑着跑着突然报“connection has been closed”,排查两天才发现是removeAbandonedTimeout设得太短。
生产环境更稳妥的做法是同时配置好三个参数,配合使用:
spring: datasource: druid: remove-abandoned: true remove-abandoned-timeout: 300 log-abandoned: trueremove-abandoned-timeout按秒设置,给长事务留足余量;log-abandoned打开后,出现连接被移除时会在日志里打印调用栈,方便定位到底哪段代码占用连接超过阈值。合理阈值一般是180到300秒,如果你发现线上大量出现“abandoned connection”日志,优先排查的是代码里有没有连接未释放,而不是一味调大超时。
写在最后的经验总结
把Spring相关面试题当成一个系统来准备,我个人最大的体会是:不要按题号背,要按“一条链路”去串。IoC容器是一切的底座,Bean生命周期和三级缓存是这个底座上最精巧的两块积木,Spring Boot帮你自动搭好了积木,Spring Security在链路上加了安全关卡,事务和AOP用代理思想贯穿全局,Spring Cloud把单个应用吃大的问题拆到分布式层面,Spring AI又把大模型能力接入到了熟悉的世界里。任何一个考点,你只要能讲清楚“它解决什么问题、在哪一步生效、失效时会发生什么”,就已经超过大部分候选人了。最后给一个小建议:准备Spring面试题时找几篇源码类文章,把DefaultListableBeanFactory、AbstractApplicationContext.refresh()、FilterChainProxy这三个类的关键方法名记下来,不是为了装,而是当你面试时说出真实源码里的类名,整个回答的可信度会立刻提升一个档次。