ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring面试不背题:核心原理与高频考点全拆解

Spring面试不背题:核心原理与高频考点全拆解 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的postProcessAfterInitializationAOP代理生成就在这里发生——容器把原始对象包装成代理对象放进单例池。这五个阶段背下来不难真正的加分项是你主动提一句“初始化前和初始化后这两个阶段是Spring扩展能力的核心位置像Transactional、Async这类功能本质都是通过后置处理器在初始化后阶段把Bean包成代理”。这句话一出口就把生命周期和后面的AOP、事务串起来了面试官会认为你是真的理解而不是考前突击。1.3 想彻底理解容器手写一个迷你IoC就够了热词里有“手写spring”这个不是让你真把框架重写一遍而是有一个非常高效率的学习方法——用几百行代码写一个只能扫包、注册Bean、执行简单依赖注入的迷你容器。我当年就是这么干的效果比看十遍源码注释都好。迷你容器核心就三块扫描注解收集Class、用反射实例化、遍历字段做注入。代码量不大核心逻辑类似这样// 简化的迷你IoC容器核心逻辑 public class MiniApplicationContext { private MapString, Object singletonObjects new ConcurrentHashMap(); public void scan(String packageName) throws Exception { // 1. 扫描包下所有Class过滤带MiniComponent注解的类 // 2. 对每个类调用反射创建实例放进singletonObjects // 3. 遍历所有Bean的字段找带MiniAutowired的字段从容器里取出依赖注入 } public T T getBean(ClassT clazz) { return clazz.cast(singletonObjects.get(clazz.getName())); } }写完你就明白一件事Spring容器的工作远不止“反射创建对象”这么简单它还要处理构造器参数解析、循环依赖、代理、作用域、事件发布、国际化……迷你容器帮你建立的是“容器大概长什么样”的直觉后续再去看源码细节心里就有了坐标。2. 三级缓存解决循环依赖的完整现场推演2.1 循环依赖长什么样什么时候会触发循环依赖的典型场景是两个Bean互相引用Bean A的构造或属性里需要Bean BBean B的构造或属性里需要Bean A。如果两边都是构造器注入Spring直接报错因为实例化A需要先实例化B实例化B又需要先实例化A死锁了没有解。如果两边都是属性注入Autowired字段或SetterSpring就能通过三级缓存绕过去。面试时建议你亲手画一下这个流程。假设先创建AA被实例化——对象A刚new出来属性还是空的但这已经是个“半成品”对象了。A发现自己需要B去容器里找B发现B还没创建。创建BB实例化后发现自己需要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的构造器参数里有BB的构造器参数里有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把里面列出的自动配置类全部加载进来然后用一批条件注解决定“哪些配置类真正生效”。条件注解是理解自动配置的钥匙最常用的有几个ConditionalOnClassclasspath里存在某个类时才生效很多自动配置类靠它判断“你是不是引入了这个库”。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调方法BB上面标了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.0Spring 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这三个类的关键方法名记下来不是为了装而是当你面试时说出真实源码里的类名整个回答的可信度会立刻提升一个档次。
返回列表