Spring核心原理深剖:IOC、三级缓存、AOP与事务失效面试题解析
又到了面试季Spring系列面试题整理的第二篇来了。上一篇发出去之后很多读者反馈说想看更深入的原理篇特别是控制反转、Bean生命周期、循环依赖、事务失效这些高频考点。所以这一篇直接围绕Spring核心原理展开把这些面试官最喜欢连环追问的知识点掰开揉碎讲清楚。内容涵盖IOC、三级缓存、AOP、Spring事务、Spring Boot自动配置以及微服务集成场景适合正在准备Java面试的开发者也适合已经工作一段时间但想系统性查漏补缺的朋友。不同经验层次都可以从里面找到对应的价值尤其是那些“以为自己知道一问就卡壳”的细节。1. 面试官到底在考什么——Spring面试题背后的核心逻辑1.1 Spring知识在Java面试中的权重不管你是校招还是社招Spring几乎是Java后端岗位的一面必考内容。很多候选人简历上写着“熟练使用Spring Boot”面试官就会从最简单的“什么是IOC”开始问然后一步步深入到源码层面。表面上看是考察知识点实际上是考察三个维度基础概念是否准确、源码理解是否到位、遇到问题时能否结合实战解决。我见过不少候选人能背出“IOC是控制反转DI是依赖注入”但一追问“Spring容器是怎么管理Bean的Bean什么时候被创建默认单例还是多例”就卡住了。也遇到过能把三级缓存流程图默写出来但问“二级缓存能不能解决循环依赖”就支支吾吾的人。这两种情况其实都不是真正的掌握了。从面试官的视角看Spring面试题不是要你背八股文而是通过问题链条来判断候选人平时有没有读过源码、有没有在项目里独立解决过Spring相关的问题。所以准备面试时一定要把每个知识点拆成“是什么、为什么、怎么用、有什么坑”四层光记结论是撑不住追问的。1.2 回答Spring面试题的正确姿势回答原理类题目时推荐采用“概念解释—底层机制—实战经验”的三段式结构。比如问“Spring如何解决循环依赖”不要只回答“三级缓存”而是先说清楚循环依赖是什么再说三级缓存是哪三级每级存的是什么为什么需要第三级最后补充一句“构造器注入的循环依赖是解决不了的实际项目中可以用Lazy打破初始化顺序”。这样的回答方式会让面试官觉得你是有自己思考的而不是临时背的。另外如果项目里确实用到过相关技术一定要主动关联项目场景。比如面试官问“事务为什么失效”你可以说“我之前在一个下单流程里因为同类内部方法自调用导致事务不生效后来改成通过代理对象调用或者拆分Bean就解决了”。有真实案例支撑远比讲十点理论更有说服力。还有一个很重要的点遇到不确定的问题不要瞎编。面试官一般都能分辨出你在编。与其硬凑答案不如坦诚说“这块我只是了解没有深入源码我理解的大概是这样”然后给出思路。面试是沟通不是背书。2. 控制反转与依赖注入从基础概念到连环追问2.1 IOC和DI到底解决什么问题控制反转Inversion of Control这个词听起来很抽象其实核心就一句话对象创建和依赖管理的控制权从程序内部交到了外部容器手中。传统开发中你需要在代码里手动new依赖对象用了Spring之后Bean的创建、组装、销毁全部交给IOC容器统一管理。依赖注入Dependency Injection是实现控制反转的手段容器在创建Bean时主动把依赖“塞”给目标对象。面试中问“IOC和DI的关系”时可以说IOC是一种设计原则DI是它的具体实现方式之一。Spring采用的是运行时通过反射进行依赖注入支持构造器注入、Setter注入、字段注入三种方式。字段注入最简洁但容易打破依赖清晰度而且依赖外部容器构造器注入可以保证依赖不可变写起来更啰嗦Setter注入则允许运行时重新注入。现在比较好的实践是优先构造器注入。追问“为什么需要IOC”时可以从解耦和生命周期管理展开。没有IOC的时候UserService自己new UserDao如果UserDao后面需要连数据库、连缓存、加代理UserService的代码就要跟着改。引入IOC后UserService只需要声明依赖容器负责把合适的实现给它模块之间通过接口交互天然满足开闭原则。另外容器创建的Bean默认是单例如果Bean无状态单例能节省大量内存这也解释了为什么Spring默认Scope是singleton。2.2 单例Bean注入原型Bean面试高频的坑面试官很喜欢问一个问题“如果我在单例Bean中注入了原型Bean每次获取到的原型Bean是同一个吗”很多候选人回答“是新的”但实际结果是同一个。原因很简单Spring容器启动时创建单例Bean注入依赖后这个Bean就被缓存了之后不会再次执行属性填充。尽管原型Bean本身确实是每次都新建但被注入的位置已经固定了。要解决这个问题有几种常见方案。最简单粗暴的是不要注入原型Bean改为每次通过方法参数传入或者使用持有ApplicationContext通过getBean获取。Spring官方推荐使用ObjectProvider 或者Lookup注解。Lookup标注在抽象方法上每次调用方法时容器就会返回一个新的原型Bean。面试时如果能主动提到这个坑会加分不少。可以这么说“我在一个支付服务里遇到过类似问题定时任务每次执行需要不同的随机数策略但因为注入的是原型Bean结果前后台拿到的其实是同一个实例。后来改用ObjectProvider从容器中获取问题就解决了。”这种实战细节才是面试官想听到的。2.3 从new对象到Spring容器代码层面的对比为了加深理解可以看一个简单例子。传统方式这样写public class UserService { private UserDao userDao new UserDao(); }这样的代码一旦UserDao需要连接多个数据源或者要加缓存逻辑就必须改动UserService。Spring方式下是这样Service public class UserService { private final UserDao userDao; public UserService(UserDao userDao) { this.userDao userDao; } }UserService不知道UserDao怎么创建的只有一个构造器参数。整个应用的装配都在配置类里完成Configuration public class AppConfig { Bean public UserDao userDao() { return new UserDao(); } Bean public UserService userService(UserDao userDao) { return new UserService(userDao); } }很多初学者会觉得这不过是把new搬到了配置类里但在复杂系统里依赖关系成百上千时统一装配的价值才会真正体现出来。面试中讲到这里不妨延伸一句Spring的IOC容器本质上是一个大工厂加注册表通过BeanDefinition描述每个对象怎么创建、依赖什么再通过反射实例化和注入所以即使面试官问“这个对象是哪来的”你也可以从源码层面回答。3. Bean生命周期与三级缓存必考重灾区3.1 Bean的完整生命周期流程Bean生命周期几乎是Spring面试必考的内容而且经常和BeanFactory、ApplicationContext一起出现。面试官希望你能按顺序说清楚一个Bean从创建到销毁经历了哪些步骤。完整的生命周期大致包括BeanDefinition加载和解析、实例化构造器创建对象、属性填充依赖注入、Aware接口回调BeanNameAware、BeanFactoryAware、ApplicationContextAware、BeanPostProcessor的beforeInitialization、初始化初始化方法PostConstruct、InitializingBean、自定义init-method、BeanPostProcessor的afterInitialization此处通常生成AOP代理、使用中、销毁PreDestroy、DisposableBean、destroy-method。这里有几个容易混淆的细节。PostConstruct的顺序是构造器 - Autowired - PostConstruct。如果把初始化工作放在构造器里可能导致依赖还未注入而出现空指针。BeanPostProcessor的afterInitialization是Spring AOP代理创建的关键时机如果Bean被切面命中返回的实际是代理对象。Aware回调顺序也是面试点Spring容器创建Bean时会先回调BeanNameAware、BeanFactoryAware如果是ApplicationContext环境下还会回调ApplicationContextAware。准备这部分时强烈建议自己动手写一个实现Aware、BeanPostProcessor和InitializingBean的测试类通过控制台打印日志验证顺序。试过之后你会发现背一遍源码输出比看十遍概念都管用。3.2 三级缓存为什么是三级一张图讲透循环依赖问题在Spring中是个经典场景。当A依赖B、B依赖A时如果按常规流程创建A需要属性填充B但B又依赖A此时A还没创建完成B无法拿到A就产生了死循环。Spring为了解决单例Bean的循环依赖在DefaultSingletonBeanRegistry里设计了三级缓存一级缓存singletonObjects存放已经完整创建好的成品Bean。二级缓存earlySingletonObjects存放提前暴露的早期Bean此时Bean已实例化但属性尚未完全填充。三级缓存singletonFactories存放ObjectFactory对象这个工厂能在需要时生成早期Bean的引用或者代理对象。创建A时实例化后立刻将A包装成ObjectFactory放入三级缓存然后继续填充属性。当需要注入B时发现B没有就去创建B。B填充属性时发现需要A在三级缓存中通过ObjectFactory拿到了A的早期引用这个引用被注入给BB完成创建后放入一级缓存。此时A再继续完成后续的初始化和代理增强最后替换掉早期引用。很多面试题会问“二级缓存不够吗为什么一定要三级”关键点在于AOP代理。如果Bean需要被AOP增强那么在实例化后、属性填充前是无法确定最终代理对象的因为代理创建通常发生在初始化之后。三级缓存存储的是ObjectFactory可以在获取早期引用时才判断是否需要生成代理。如果是二级缓存直接存实例那么一旦提前暴露了原始对象后续就无法再插入代理逻辑了。所以第三级缓存本质上解决了“在初始化前暴露代理对象”的时机问题。面试中能说出这一层基本就过了。如果被追问“那二级缓存存ObjectFactory行不行”其实也可以但设计上把工厂放在第三级使得只有循环依赖真正发生时才执行工厂逻辑对性能影响最小。3.3 循环依赖相关的连环追问与避坑指南围绕循环依赖面试官还会追问几个变体。第一个是“构造器注入能解决循环依赖吗”答案是不能。因为构造器注入的循环发生在实例化阶段此时对象还没有创建出来连提前暴露的资格都没有。Spring会直接报BeanCurrentlyInCreationException。第二个是“原型Bean的循环依赖能解决吗”也不能。原型Bean每次获取都会新建根本不走三级缓存一旦出现循环依赖就会立刻抛异常。第三个是“循环依赖有什么坏处”虽然能创建成功但多次代理可能导致某些切面逻辑执行两次或者因为循环依赖让Bean初始化顺序变得脆弱所以从根本上应该尽量避免。实际项目中处理循环依赖的姿势也很重要。推荐的方法是重新设计类结构将A、B相互依赖的部分抽取到独立对象中。如果只是想快速打破初始化顺序可以在构造函数参数上加Lazy让其中一个Bean延迟实例化。另外很多团队会把字段注入当成常规操作但字段注入更容易隐藏循环依赖因为编译期根本看不出来。改成构造器注入后循环依赖会在启动时就暴露反而更容易修复。4. AOP与Spring事务原理和场景缺一不可4.1 AOP核心概念与实现机制AOP面向切面编程是Spring的另一大支柱。面试中会问一些基础术语切面Aspect是横切逻辑的封装切点Pointcut是匹配连接点的表达式通知Advice是切面上的具体动作如前置通知、后置通知、环绕通知连接点JoinPoint是程序执行的具体位置。Spring AOP是基于动态代理实现的目标对象有接口时使用JDK动态代理没有接口时使用CGLIB代理。一个经典面试题是“Spring AOP和AspectJ有什么区别”。Spring AOP运行时织入基于代理只能支持方法级别而AspectJ是编译期/加载期织入支持字段、构造器级别。实际应用中95%的需求Spring AOP都够用。还有一个常问的是“JDK动态代理和CGLIB代理的区别”。JDK动态代理要求目标类实现接口通过反射调用目标方法CGLIB通过生成目标类的子类覆盖方法来实现增强因此不能代理final类和方法。现在Spring Boot 2.x默认开启了CGLIB代理哪怕是接口类也会走CGLIB面试时可以提这个细节。AOP在项目中最常见的场景就是日志、权限校验、性能监控和数据库事务。比如用环绕通知统计一个方法的执行耗时通过Around和ProceedingJoinPoint实现。面试官问到AOP时你可以主动说“事务本质上也是通过AOP实现的Spring在代理对象上包裹了事务拦截器”。这样很自然就能衔接下一个事务考点。4.2 Spring事务失效十大场景盘点含案例事务失效是面试中的超高频考题也是项目里踩坑最多的点。我总结了最常见的几个场景每个都最好能说清原因和解决方案。方法自调用。同类内部一个方法调用另一个标注Transactional的方法因为调用走的是this对象而不是代理对象事务拦截器自然不生效。解决方式是拆分Bean或者用AopContext.currentProxy()获取代理再调用方法。方法不是public。Spring事务默认基于Spring AOP代理而代理机制要求方法必须能被代理覆盖public方法才能被CGLIB正常代理。private方法调用会导致事务失效protected也有风险。检查时优先看方法是否是public。异常被内部捕获。事务方法里自己try-catch了异常导致异常没有抛给事务拦截器事务不会回滚。正确的做法是不捕获异常或者捕获后标记回滚并重新抛出。抛出的异常类型不对。Spring默认只回滚RuntimeException和Error。如果方法抛的是checked异常如Exception默认不会回滚。解决方案是在Transactional里指定rollbackFor Exception.class这也是很多新手没做的。数据库引擎不支持事务。比如MySQL的MyISAM引擎不支持事务表怎么操作都不会回滚。排查时要确认用的是InnoDB。事务传播属性配置错误。比如把传播行为配成了NOT_SUPPORTED事务就挂了。这个下面专门讲。多线程调用。事务是绑定在线程上的如果方法内部新开了子线程子线程里发生异常不会影响主线程事务子线程自己的操作也不会被主事务统一管理。Bean没有交给Spring管理。比如自己new了一个Service类上的事务注解完全无效因为事务拦截器是通过容器创建代理Bean才生效的。这个比较简单但容易忽略。说到项目案例时我印象最深的是有一次在消息队列消费方法里调用本地注册方法因为自调用导致事务失效数据落库了但后续状态更新失败。后来把本地注册方法拆到另一个Service里通过注入的方式调用问题立刻解决。这种案例面试官很愿意听。4.3 事务传播行为速记与面试话术Spring事务传播行为指的是方法之间调用时事务如何传播。面试常考的是REQUIRED、REQUIRES_NEW、NESTED三者容易混淆。REQUIRED是默认行为。如果当前没有事务就新建一个事务如果当前已有事务则加入当前事务。比如ServiceA的methodA启动了事务内部调用methodBREQUIREDmethodB会使用同一个事务methodA抛出异常整个事务回滚。REQUIRES_NEW则是挂起当前事务创建新事务。methodA和methodB的事务是独立的methodB回滚不影响methodA的事务methodA回滚也不会影响methodB。常用于日志记录、发送消息等不希望被主事务回滚影响的场景但要注意这样可能导致主事务已经写了部分数据而子事务提交成功数据不一致要自己权衡。NESTED是嵌套事务内部使用JDBC保存点。内层事务可以单独回滚到保存点外层事务可以继续提交。但NESTED依赖数据库保存点能力实现上基于JDBC 3.0注意MySQL AppEngine...实际上MySQL InnoDB支持。面试话术可以这样说“我一般优先用REQUIRED保证一致性独立子任务用REQUIRES_NEW隔离异常对于需要柔性控制的场景考虑NESTED但NESTED使用得有清晰的认识因为代理机制和保存点都会有额外开销。”话术里带上使用场景和取舍比背定义强。5. Spring Boot与Spring Cloud从框架到微服务5.1 Spring Boot自动配置原理现在的面试题经常直接问“Spring Boot为什么能自动装配”。核心点在于SpringBootApplication注解它由三个注解组成SpringBootConfiguration标记配置类ComponentScan扫描组件EnableAutoConfiguration开启自动配置。自动配置的核心机制是SpringFactoriesLoader。SpringBoot在META-INF/spring.factories文件中定义了大量AutoConfiguration类容器启动时会加载这些类然后通过条件注解筛选生效的配置。条件注解包括ConditionalOnClass、ConditionalOnMissingBean、ConditionalOnProperty等。比如RedisAutoConfiguration会判断类路径下是否存在RedisTemplate类如果在pom中引入了spring-boot-starter-data-redis这个自动配置就会生效默认创建一个RedisTemplate实例。面试官常问“自动配置会覆盖我们自己的配置吗”需要分情况。条件注解中ConditionalOnMissingBean表示如果容器中已经存在用户定义的Bean则自动配置的Bean不会创建。但有些自动配置类只是在属性值上给默认值用户用配置文件覆盖了相关属性就能覆盖默认行为。因此回答时要强调“约定优于配置默认值可以被显式覆盖”。准备这一块建议自己去阅读spring-boot-autoconfigure源码挑一个熟悉的Redis或者MyBatis的自动配置类看它上的条件注解和配置属性类。面试时如果能说出具体类名和方法名比如DataSourceAutoConfiguration、MybatisAutoConfiguration会让面试官对你的源码阅读能力更认可。5.2 Spring Cloud核心组件与常见问题微服务架构下Spring Cloud是绕不开的话题。面试题通常围绕注册中心、配置中心、网关、容错组件、链路追踪展开。比较常问的有Nacos和Eureka有什么区别Feign和Ribbon的关系Sentinel和Hystrix的区别网关选型等。Nacos在Spring Cloud Alibaba体系中是核心组件它同时承担服务注册与配置中心的职责。Eureka主要是注册中心只有服务发现功能。Nacos相比Eureka有更丰富的健康检查机制、配置动态刷新功能还支持临时和持久实例。Spring Cloud Alibaba关于Nacos的问题在2024、2025年热度非常高基本可以默认面试会问。Feign和LoadBalancer/Ribbon的关系也常考。Feign是一个声明式HTTP客户端负责接口定义和请求转发底层服务发现和负载均衡需要配合Ribbon或Spring Cloud LoadBalancer。Ribbon在Spring Cloud 2021版本后停止维护新项目更推荐使用LoadBalancer。回答时可以提一句“Feign自带负载均衡集成通过FeignClient可以自动替换服务名”。容错方面Sentinel相比Hystrix提供了更细粒度的流量控制、熔断降级、系统保护和热点防护实践上更适用于高并发场景。如果面试官问“熔断和限流有什么区别”可以这样回答熔断是依赖方的一种自我保护当下游调用失败率达到阈值时直接短路调用限流是对请求入口或方法入口的流量控制防止系统被打爆。两者目标不同但经常配合使用。5.3 集成场景题WebSocket、日志、分布式锁现在面试题往往不只是基础原理还会结合具体集成场景。根据近期的热门关键词WebSocket集成、Spring Boot日志、分布式锁是三个出现频率很高的方向。WebSocket在Spring Boot中的配置核心就是两个点一是添加依赖二是编写握手拦截器与配置类。yml主要配置端口、上下文路径、允许的源等。如果面试官问到你可以说WebSocket的握手前通常需要用拦截器校验身份把用户信息存入session配置类需要实现WebSocketConfigurer注册Handler和拦截器还需要考虑跨域。这套流程只要说出关键类名WebSocketHandler、HandshakeInterceptor再结合自己的项目细节就行。Spring Boot日志问题经常从“日志框架原理”和“配置实践”两个角度考。框架层面要能说清Slf4j作为门面底层绑定Logback或Log4j2。配置方面需要关注application.yml里的logging.level和logback-spring.xml比如怎么设置某个包下日志异步输出、按大小和时间滚动切割。面试中常见问题“日志打印会影响性能吗”正确回答是减少无谓日志、用占位符代替字符串拼接、生产环境合理调整日志级别。分布式锁是微服务场景的重要考点。实现方式比较常见的有三种基于Redis的SETNX实现基于Zookeeper的临时顺序节点以及基于数据库的唯一约束。面试时最常被问的是“Redis分布式锁如何保证原子性和释放安全性”。回答时要说清楚使用Redisson的思路加锁时执行lua脚本同时设置过期时间释放时用lua脚本校验客户端唯一标识避免误删其他客户端的锁。如果只是简单SETNX加expire中间存在异常导致锁永久不释放的风险。此外还要考虑锁续期否则业务时间超出过期时间会提前释放锁。6. 高频面试题速查表与备考心得6.1 高频面试题速查表准备面试的人往往需要在最后冲刺阶段反复翻看一些核心结论我整理了一个速查表把Spring面试中最高频的十几道题的关键答案浓缩进来。面试题核心要点回答关键IOC和DI的区别控制反转是原则DI是实现方式容器负责对象创建与依赖管理Bean默认作用域singleton无状态Bean用单例节省资源单例注入原型问题注入的是同一原型实例使用ObjectProvider或Lookup解决Bean初始化顺序构造器 - Autowired - PostConstructBeanPostProcessor在初始化前后执行三级缓存为什么是三级第三级存ObjectFactory为了处理AOP代理时机二级无法在提前暴露时确定代理对象构造器注入能解决循环依赖吗不能实例化阶段无法提前暴露Spring AOP代理方式JDK动态代理和CGLIBSpring Boot 2.x默认CGLIB事务默认回滚异常RuntimeException和Errorchecked异常需rollbackFor自调用事务失效this调用不经过代理注入代理或拆分BeanREQUIRED与REQUIRES_NEW加入当前事务 vs 新建独立事务NESTED基于保存点Spring Boot自动配置原理SpringFactoriesLoader 条件注解ConditionalOnClass等分布式锁实现Redis Lua脚本、Zookeeper临时节点Redisson是成熟方案Feign负载均衡服务发现 客户端负载均衡新项目用LoadBalancerSentinel和Hystrix区别流控、降级、热点防护更适合高并发场景这张表格是我自己在面试前一周会反复看的内容你可以把它当成考前背诵版。但背诵只是第一步最重要的还是回到源码和项目里验证否则面试官一个“为什么”就能扎破纸面。6.2 面试准备的经验与常见误区准备Spring面试题时我见过最多的误区是“只看结论不看源码”。比如问到AOP底层能说出JDK动态代理和CGLIB但进一步问他代理是什么时候创建的、同一个Bean被多个切面增强时顺序怎么确定回答就开始模糊。这些问题在源码里其实都有答案代理是在Bean初始化后通过AnnotationAwareAspectJAutoProxyCreator创建的切面执行顺序由Order控制默认按Order值排序Order值小的先执行。另一个常见误区是“忽视版本差异”。Spring 5和Spring Boot 2.x、3.x之间的变化不少比如javax到jakarta包名的变化Spring Boot 3要求Java 17以上。如果面试官问“Spring Boot 3和2有什么区别”你最好能答出来。不用背全部但至少要掌握大方向。最后面试前强烈建议把Spring源码工程导入IDE里直接对着DefaultSingletonBeanRegistry和AbstractAutowireCapableBeanFactory调试。不需要把整个容器的代码都读透只要跟一遍单例Bean的创建流程把三级缓存、生命周期、代理创建的代码路径走一遍很多困扰你的问题就迎刃而解了。纸上得来终觉浅Spring这种实战型框架源代码本身就是最好的学习资料。我个人在实际准备过程中的体会是Spring面试题不是靠刷量刷出来的更需要你对容器运行的整个过程有画面感。面试官问任何一个知识点你都能快速在脑海里定位到源码中的某个阶段、某个类、某行关键逻辑这才是面试中真正的底气。如果你也会像我一样读完源码后自己写一堆小Demo去验证疑问那你距离拿下Spring面试题就真的不远了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →