1. 面试前的整体规划:Spring考察的底层逻辑
我做了这么多年技术面试官,也陪跑过不少候选人准备面试,先说一个最核心的观察:Spring相关的题,表面上考的是“知识点”,实际上考的是“有没有真的用Spring写过东西”。背答案的人,遇到稍微变通的问题就会露馅;真正写过代码的人,哪怕记不住源码细节,也能说出个所以然来。所以这篇分享,我会把Spring面试里最高频、最容易区分水平的考点串起来讲,并且告诉你每一类题目面试官究竟想听什么。
Spring面试题覆盖的范围很广,从IOC、AOP、事务,到Spring Boot自动配置、Spring Cloud微服务,再到Spring Security、Spring AI这类新方向,每一个都能展开聊很久。但绝大多数公司的面试不会每个方向都问一遍,而是根据岗位职级和项目经历,聚焦在若干个核心模块。我梳理了近两年的面试反馈和数据,发现考察频率最高的几个模块是:IOC与Bean生命周期、循环依赖与三级缓存、AOP与动态代理、事务机制、Spring Boot自动配置原理。这五个模块大约覆盖了Spring相关面试问题的七成以上,所以我们先从它们开始,逐个拆透。
这套内容适合谁?如果你正在准备Java后端岗位面试,不管是校招还是社招,这篇都有参考价值;如果你是刚学完Spring想查漏补缺的开发者,里面每个章节也都能当成自测清单用。我会尽量把每个核心问题的“标准答案框架”“面试官追问方向”“加分回答姿势”都讲清楚,因为面试的本质不是背答案,而是让对方相信你理解了这个东西。
2. IOC与Bean生命周期:面试官最爱问的地基问题
2.1 控制反转到底反转了什么
IOC这个话题几乎每一场Java面试都会碰到,但很多人回答得特别空。最常见的说法是“IOC就是把对象的创建交给Spring容器管理”,这句话没错,但只说了一半。面试官真正想听的,是你理解了这个设计解决的是什么问题。
传统开发里,对象之间的依赖关系是通过new关键字硬编码在代码里的。比如OrderService要用UserService,就在OrderService里面直接new UserService()。这听起来没什么问题,但一旦系统变大,这种写法会带来两个麻烦:一是对象创建逻辑散落在各处,改一个依赖的构造方式,所有相关位置都要跟着动;二是耦合度高,想替换实现类做测试非常麻烦,因为代码里写死了具体的类名。
IOC反转的就是这个“控制权”。本来由业务代码决定“我要创建谁”,反转之后变成了“我需要什么,容器给我什么”。你只需要在配置里声明依赖关系,容器负责把对象创建好、组装好,在合适的时机交给你。控制权从代码转移到了容器,所以叫控制反转。而DI依赖注入,是IOC的一种实现手段——容器通过构造器、Setter或者字段注入,把依赖传递进来,而不是让对象自己去创建依赖。
在面试中回答这个问题,我建议你先说设计思想,再举一个具体的例子说明硬编码依赖的痛点,最后点一下Spring中IOC容器的落地形态(BeanFactory和ApplicationContext)。这样既有理论高度,又有实战感,面试官想继续追问你也能接得住。
2.2 手写一个mini版IOC,理解就到位了
很多候选人背熟了IOC定义,但一问到“如果让你自己实现一个Spring容器,你会怎么做”就卡住了。其实这个问题的答案没那么玄乎,核心就三步:扫描类、存定义、反射创建。
第一步,扫描指定包下的所有类,把带有@Component这类注解的类找出来;第二步,把这些类的元信息存到一个Map里,key是beanName,value是类的Class对象或者BeanDefinition;第三步,根据类的构造器,用反射newInstance创建实例,然后扫描字段上的@Autowired或@Resource注解,递归注入依赖。这个过程就是最简版IOC容器。
我把核心逻辑写出来,你一看就明白:
public class SimpleIocContainer { private Map<String, Object> singletonObjects = new ConcurrentHashMap<>(); private Map<String, Class<?>> beanDefinitions = new ConcurrentHashMap<>(); public void register(String beanName, Class<?> clazz) { beanDefinitions.put(beanName, clazz); } public Object getBean(String beanName) throws Exception { Object bean = singletonObjects.get(beanName); if (bean != null) { return bean; } Class<?> clazz = beanDefinitions.get(beanName); if (clazz == null) { throw new IllegalArgumentException("No bean defined: " + beanName); } // 第1步:反射创建实例 bean = clazz.getDeclaredConstructor().newInstance(); // 第2步:处理依赖注入 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); Object dependency = getBean(field.getName()); // 模拟按名称查找 field.set(bean, dependency); } } singletonObjects.put(beanName, bean); return bean; } }这段代码虽然简陋,但已经把Spring容器最核心的两个机制体现出来了:单例缓存和依赖注入。真实Spring容器比这个复杂得多,因为还要处理BeanDefinition的解析、属性填充、初始化回调、代理生成、作用域管理、销毁逻辑等等,但底层思路是一致的。面试时如果能用这样的代码框架讲清楚IOC的本质,比单纯背概念有说服力得多。
2.3 Bean完整生命周期:从定义到销毁的全程拆解
Bean生命周期是Spring面试中的重头戏,几乎十个面试官里有八个会问。有些人能背出“实例化、属性填充、初始化、销毁”四个阶段,但真要问“BeanPostProcessor在哪个阶段起作用”“循环依赖发生在哪一步”,就又含糊了。
我建议你在面试中把生命周期拆成四个大阶段来讲:实例化阶段、属性填充阶段、初始化阶段、销毁阶段。每个阶段内部还有细分,我用一条时间线给你捋清楚。
实例化阶段:容器根据BeanDefinition,通过反射调用构造器创建Bean的实例。这里有个细节,Spring默认使用无参构造器,如果类里只有有参构造器,而你又没有通过配置指定构造参数,Spring会报错——除非它找到了可以匹配的候选Bean做自动装配(比如只有一个构造器时Spring会自动用它的参数去容器里找依赖)。
属性填充阶段:实例创建完之后,Spring会处理各种依赖注入。@Autowired、@Resource、@Value这些注解的解析,以及XML里<property>标签的配置,都在这个阶段完成。这是循环依赖发生的关键阶段:当A填充依赖时需要B,而B填充依赖时又需要A,就形成了循环引用。
初始化阶段:属性填充完毕,Spring会调用一系列初始化回调。顺序是这样的:先是BeanNameAware、BeanClassLoaderAware、BeanFactoryAware这些Aware接口回调,然后是BeanPostProcessor的postProcessBeforeInitialization方法,接着是@PostConstruct标注的方法,再接着是InitializingBean接口的afterPropertiesSet方法,最后是配置的init-method方法。这里最容易记混的是@PostConstruct和afterPropertiesSet的执行顺序,记住一个口诀:Aware最早,Before前置处理器其次,PostConstruct第三,afterPropertiesSet第四,init-method最后。
销毁阶段:容器关闭时,单例Bean会依次执行@PreDestroy标注方法、DisposableBean接口的destroy方法,以及配置的destroy-method方法。这里要特别注意,@PreDestroy只对单例Bean有效,原型作用域的Bean默认不会被容器销毁管理。
除了这四个阶段,还要提一下BeanPostProcessor的妙用。Spring的AOP自动代理就是通过AbstractAutoProxyCreator这个BeanPostProcessor在初始化阶段介入的——Bean初始化完之后,这个处理器会判断是否满足切点条件,如果满足就生成一个代理对象返回。所以你在容器里拿到的可能不是原始Bean,而是它的代理。
我建议你把下面这张时序列表记下来,面试的时候按顺序讲,基本就是标准满分答案:
| 阶段 | 关键动作 | 扩展点 |
|---|---|---|
| 实例化 | 反射创建对象(构造器) | 构造器选择、实例化策略 |
| 属性填充 | 依赖注入、属性赋值 | @Autowired、@Value、@Resource |
| 初始化-前置 | 各种回调准备 | Aware接口、BeanPostProcessor.before |
| 初始化-核心 | 业务初始化逻辑 | @PostConstruct、InitializingBean、init-method |
| 初始化-后置 | 代理生成等扩展 | BeanPostProcessor.after |
| 销毁 | 资源释放 | @PreDestroy、DisposableBean、destroy-method |
2.4 一个容易踩坑的细节:原型Bean与单例Bean的差异
很多人在面试时会提到Bean的作用域,但往往只背了singleton和prototype的名字,并没有真正理解它们在容器中的行为差异。这里我展开聊一下,因为面试官很喜欢在这个话题上做文章。
单例Bean是Spring默认的作用域,整个容器里同一个Bean定义只创建一个实例,缓存在singletonObjects这个Map里。所以如果你在单例Bean里放了一个可变的状态字段,并发访问时就要自己考虑线程安全问题——容器不负责多线程同步。
原型Bean则每次获取都创建一个新实例,不进单例缓存。但有个大坑:如果某个单例Bean依赖了一个原型Bean,那这个依赖在容器启动时就被注入进单例Bean了,之后不管从容器里获取多少次,拿到的都是同一个原型Bean实例。也就是说,原型作用域只对“每次从容器直接获取Bean”生效,对“单例Bean里注入的属性字段”是失效的。
如果面试官追到这里,你还可以补充一个解决方案:通过ObjectFactory或者ApplicationContext.getBean()来延迟获取原型实例,或者用@Lookup注解标注一个方法,让Spring在每次调用时动态生成原型Bean的返回值。能说到这个层次,说明你不是光背理论,而是真的在项目里遇到过这种场景。
3. 循环依赖与三级缓存:Spring源码的必考科目
3.1 循环依赖是什么,为什么会发生
循环依赖指的是两个或多个Bean互相持有对方的引用,形成一个闭环。最简单的例子:A依赖B,B依赖A。在Spring容器加载时,创建A时需要注入B,创建B时需要注入A,如果没有特殊机制,就会无限循环下去,最终抛出一个BeanCurrentlyInCreationException。
有个问题是:用构造器注入的循环依赖是解决不了的,只有基于属性或Setter的循环依赖才能被Spring处理。原因是构造器注入要求对象在创建时就完成依赖传递,而这时候A和B都还没实例化完成,没有一个可以提前暴露的半成品对象来做中间过渡。但是属性注入不一样——A可以先用无参构造器创建一个实例,再把A的“早期引用”暴露出去,让B先拿到这个半成品,等B创建完成后再把属性值补回A。
这个区别面试时一定要主动讲出来,它往往也是面试官设下的陷阱题:他可能先问“Spring如何解决循环依赖”,你回答完三级缓存之后,他再问“那构造器循环依赖呢?”,如果你之前没讲,这时候就很容易卡壳。先发制人把这个坑点透露出来,会显得你考虑问题很全面。
3.2 三级缓存的设计与执行时序
Spring容器里为了解决属性注入循环依赖,设计了三个缓存Map,源码中定义在DefaultSingletonBeanRegistry里:
Map<String, Object> singletonObjects; // 一级缓存:完整成品 Map<String, Object> earlySingletonObjects; // 二级缓存:早期暴露的引用 Map<String, ObjectFactory<?>> singletonFactories; // 三级缓存:对象工厂一级缓存放的是创建完成、属性填充完毕、初始化结束的成品Bean;二级缓存放的是已经实例化但还没完成属性填充和初始化的早期Bean;三级缓存放的是ObjectFactory,它是一个工厂接口,调用它的getObject()方法可以生成一个早期Bean对象。
为什么要分三级,而不是直接用二级缓存?这是面试里最核心的追问点。答案是为了处理AOP代理的场景。
想象一下:如果最终需要的是一个代理对象,而循环依赖发生时A只是半成品,这时候如果直接把原始A对象放进二级缓存,B拿到的就是原始类实例,后续AOP生成的代理对象就不会替代它——B持有的引用和容器最终暴露的引用不一致。三级缓存里的ObjectFactory可以在早期引用阶段就调用getEarlyBeanReference()方法,提前判断这个Bean是否需要代理,如果需要就生成代理对象暴露出去。这样一来,B拿到的就已经是最终的代理形态,之后容器里的成品也是同一个代理,引用一致,逻辑正确。
时序可以这么理:创建A -> 实例化完成 -> 把A的ObjectFactory放入三级缓存 -> 填充A属性发现需要B -> 去容器找B -> B不存在则创建B -> B实例化完成 -> 填充B属性发现需要A -> 从一级缓存找不到A -> 从二级缓存找不到A -> 从三级缓存拿到A的ObjectFactory -> 调用getObject()得到早期A引用 -> 放入二级缓存 -> B拿到A引用完成属性填充 -> B继续初始化和后续流程 -> B创建完成后回到A -> A从缓存中拿到B引用完成属性填充 -> A执行初始化 -> 最后A也放进一级缓存,同时清除二三级缓存中的A。
面试时如果能把这个流程流畅地讲完,基本就把这个考点吃透了。不过我要提醒一句,Java多线程环境下三级缓存还存在一个细微的可见性问题,Spring是通过synchronized关键字配合双重检查锁来保证缓存的原子性的。这个话题太细,面试中谈不谈到都不影响大局,但有精力的话可以作为加分点。
3.3 面试追问:三级缓存都能被二级缓存替代吗
这是近几年非常流行的一个追问点,面试官想考察你是不是真的理解了三级的必要性,而不是只记住了“三级缓存”这个名字。
网上有不少说法是“为了AOP才需要三级”,但从Spring源码的角度更精确地讲,三级缓存存在的核心理由之一,是让AOP代理能够在早期引用阶段就暴露正确的类型。如果没有AOP,只有普通的属性注入循环依赖,那两级缓存是足够的——实例化完成后直接把半成品放到暴露Map里,后续填充属性时直接取用即可。
但如果引入了AOP,事情就复杂了。在循环依赖发生的时候,A可能是一个需要生成代理的Bean。如果只有二级缓存,我们在A实例化完成后,只能把原始对象放进去。等到A初始化完,Spring在initializeBean步骤之后调用AbstractAutoProxyCreator生成代理时,B那边持有的还是原始对象的引用。这个B拿到的A和最终容器提供的A就不是同一个引用,行为会出现偏差。
三级缓存的ObjectFactory就是为了解决“代理需要尽早暴露”的问题。它提供一个延迟计算的入口:在A还没完成初始化时,如果有其他Bean提前引用A,可以先通过工厂触发getEarlyBeanReference(),此时提前创建代理;如果没有循环依赖,那工厂不会被提前调用,一切照旧,直到常规的后置处理阶段才生成代理。这种“延迟到需要时才决定是否代理”的设计,才是三级缓存的精髓。
3.4 实际业务中怎么避免循环依赖
虽然Spring有三级缓存兜底,但在我实际的项目经验里,循环依赖通常不是什么好味道,它往往意味着模块间职责划分不够清晰。所以面试里如果被问到“你们项目里有循环依赖吗,怎么解决的”,不要只答“靠三级缓存”,更要说出设计层面的规避手法。
常用的方案有三个:重构拆分——把A和B互相依赖的逻辑抽到第三个类C里,让A和B都只依赖C;改构造器注入——Spring对构造器循环依赖不支持,所以如果你改成构造器注入,编译期就能暴露循环问题,强制你重构;使用延迟加载——在字段上配合@Lazy注解,让Spring在注入时生成一个代理对象,真正使用时才从容器中获取目标Bean的引用。这三种方案里,@Lazy最快但只是权宜之计,重构才是长久之策。
我见过一些项目搞到后来,Bean之间的依赖网纠缠得跟蜘蛛网似的,连Spring容器启动都要花很长时间。原因就是早期图方便,到处用@Autowired注入互相引用的Bean,没有维护好依赖方向。面试时如果能从“设计合理性”的视角来讲循环依赖问题,而不是仅仅停留在“Spring能解决它”,会显得你对架构问题有自己的判断力。
4. AOP与动态代理:看透代理生成的底层逻辑
4.1 AOP解决什么问题,核心概念怎么串起来
AOP面向切面编程,解决的是业务代码中横切关注点(日志、权限、事务、性能统计等)难以复用、容易侵入业务逻辑的问题。比如你有一个订单Service,里面十几个方法都要打日志、都要开启事务,如果每个方法里手动写,代码重复度极高,还容易漏写。
AOP的做法是把这些横切逻辑抽到“切面”里,通过“切点”描述“在哪些方法上生效”,通过“通知”描述“在这些方法执行的哪个时机做什么事”。常见的通知类型有五种:@Before(方法执行前)、@AfterReturning((正常返回后)、@AfterThrowing(异常抛出后)、@After(最终执行后)、@Around(环绕通知,最灵活,可以控制目标方法是否执行)。
面试里描述这些概念时,不要只念定义,最好用一个例子串起来。比如权限校验场景:切点是execution(* com.example.service.*.*(..)),切面里用@Before通知检查当前用户是否登录;又比如事务场景:切点是@Transactional标注的方法,切面在@Around通知中开启事务、执行目标方法、提交或回滚。这样回答既完整又贴近实际。
我还想提醒一点:AOP的本质是动态代理。Spring AOP默认在目标类实现了接口时使用JDK动态代理,如果目标类没有实现接口,则使用CGLIB生成子类代理。很多候选人只记得“JDK代理要接口,CGLIB不用”,但不知道Spring Boot 2.x之后默认的行为是什么。实际上Spring Boot 2.x开始默认优先使用CGLIB动态代理,不再强迫依赖接口。这一点经常在面试中作为细节题被问到。
4.2 JDK动态代理与CGLIB的差异对比
深入讲一下两种代理的区别,这是AOP面试题的延伸细节点。
JDK动态代理基于Java的反射机制,核心类是java.lang.reflect.Proxy和InvocationHandler。它要求目标类必须实现至少一个接口,代理对象和目标对象实现相同的接口,调用方看到的是一个接口类型的对象。因为额外引入了一层接口调用开销,性能上没有CGLIB直接操作字节码快,但它的创建过程不依赖额外的字节码库,天生更“轻”。从维护角度看,基于接口编程也更容易保持整洁。
CGLIB则是通过生成目标类的子类来实现代理,所以要求目标类不能被final修饰,被代理的方法也不能是final的,否则无法重写。CGLIB生成代理时直接操作字节码,动态生成一个目标类的子类,重写非final的方法,在方法调用前后织入增强逻辑。它的调用效率和灵活性在某些场景下更高,代价是最终类无法代理、方法重写要满足访问控制条件。
在Spring Boot 2.x+中,默认的proxy-target-class已经被设为true,也就是优先用CGLIB。所以你在项目中会发现,哪怕Service实现了接口,默认注入的也是CGLIB代理。如果面试官问起“为什么Spring Boot默认用CGLIB”,你可以回答:避免因为没实现接口而无法生成代理的坑;同时CGLIB的性能在现代JVM和字节码优化下已经很接近JDK代理,默认选择它更省心。
4.3 代理失效的经典场景:自调用问题
AOP面试里还有一道非常经典的高频追问:同一个类内部调用另一个方法,AOP会不会生效?
答案是不会。原因是Spring AOP的代理机制建立在“调用方持有的是代理对象”这个前提下。当你从容器里拿到Service时,实际上拿到的是代理对象;但代理对象的内部,this指向的还是原始对象。如果你在methodA()里直接this.methodB(),这个调用走的是原始对象的方法地址,绕过了代理,AOP当然不会拦截。
这在事务场景中最常见。比如一个Service类里有方法A和方法B,方法A没有事务,方法B标注了@Transactional,A内部调用了B。看起来B被事务保护了,实际上Spring生成的代理只会对容器外的调用生效,A调B时并没有经过代理对象,所以B上的事务注解完全不会起作用。第一次遇到这个坑的人排查事务不生效问题时,往往会耗费很多时间。
推荐的解决方案有三个:一是把B抽到另一个Service类中,让A通过注入的Bean来调用B,这样走的就是代理对象;二是把事务切点放到方法A上,让A直接开启事务,让B作为内部调用的普通方法执行;三是在A内部通过ApplicationContext.getBean(Class)拿到代理对象再调B,或者用AopContext.currentProxy()获取当前代理,前提是要开启exposeProxy属性。我个人最推荐第一种,职责拆分清晰,也符合领域设计。
遇到这种问题时,面试官想考察的是一个开发者在实际调参过程中对代理机制的理解程度。你如果能直接说出“内部方法调用不会走代理”的核心原因,并举出事务失效的例子,就非常加分。
5. Spring事务管理:从隔离级别到失效场景全解
5.1 声明式事务的原理:代理+AOP
Spring的事务管理分为编程式事务和声明式事务。编程式事务就是自己用TransactionTemplate或者PlatformTransactionManager的API,在代码里显式地begin、commit、rollback。声明式事务则是用@Transactional注解,让Spring通过AOP在目标方法执行前后自动处理事务的开闭回滚。
面试中一般不会要求你手写编程式事务代码,但一定要理解声明式事务的原理:Spring通过TransactionInterceptor这个AOP增强器,在@Transactional标注的方法上生成代理。方法调用前开启事务,方法执行正常返回则提交事务,方法抛出运行时异常或者特定受检异常(可通过rollbackFor指定)则回滚事务。这个事务增强器就是AOP的一个具体应用场景,所以回答这个问题的思路和AOP那部分是相通的。
有个容易混淆的细节是@Transactional只对RuntimeException和Error默认回滚,对受检异常默认不回滚。很多新人写代码时在方法里抛了一个IOException或自定义受检异常,结果事务提交了都不知道为什么。记住解决办法:在@Transactional注解上显式写rollbackFor = Exception.class,让它对所有异常都回滚。
5.2 事务传播行为:什么时候用哪个
事务传播行为解决的是“当方法B在方法A的事务中被调用时,B应该怎么处理事务”的问题。Spring定义了七种传播行为,但面试频率最高的只有三种:REQUIRED、REQUIRES_NEW、NESTED。
REQUIRED是默认值:如果当前没有事务,就新建一个事务;如果当前已经有事务,就加入当前事务。这是最常用的行为,比如一个业务编排方法调用多个Service方法,期望它们都在同一个事务里,任意一个失败整体回滚。
REQUIRES_NEW:不管当前有没有事务,都开启一个新事务。如果当前已有事务,先挂起外层事务,新事务执行完后恢复外层事务。这个适合的场景是“内部操作必须独立成功或失败,不能被外部事务牵连”。比如下单后要记录一条独立的审计日志,即使订单失败了,日志也要留下,这时日志记录方法用REQUIRES_NEW。
NESTED:如果当前有事务,在嵌套事务中执行,可以独立回滚到保存点,但最终提交还要看外层事务。它和REQUIRES_NEW的区别在于:NESTED不是真正独立的事务,它依赖于外层事务,只是内部出错时可以回滚内部已经执行的SQL,不影响外层的整体方向。面试中能把这个差异说清楚,绝对是一个亮点。
此外还有SUPPORTS(支持当前事务,没有就以非事务方式执行)、MANDATORY(强制要求已有事务)、NOT_SUPPORTED(以非事务方式执行,挂起当前事务)、NEVER(以非事务方式执行,如果存在事务则抛异常)。这些了解即可,实际用得很少。
5.3 事务失效的十大坑点
事务失效是真实项目里最容易踩的坑,面试官也很喜欢拿这些场景来考察候选人的项目经验。我整理了几个最高频的失效场景,你可以对照自查。
@Transactional注解加在非public方法上。Spring的代理机制决定了它只能拦截public方法,如果标注在protected、default或者private方法上,注解会被忽略,且不会报错。这属于静默失败,特别难排查。
同类内部方法自调用。前面讲AOP自调用问题时已经提到,方法A调用同类方法B时,B上的@Transactional不会生效。解决办法要么把B拆到别的Bean里,要么把事务加到A上。
方法被final修饰。CGLIB代理通过生成子类来覆盖方法,如果方法是final的,子类无法重写,代理自然无法织入事务逻辑。这种情况即便使用接口的JDK代理也不太行,因为增强逻辑仍然依赖代理链。
方法抛出的异常被内部catch掉了。事务代理只能在异常抛到切面时才触发回滚,如果方法内部把异常吞掉,Spring根本感知不到,自然不会回滚。这也是最常见的“事务没生效”背后的真实原因。
方法抛出受检异常且rollbackFor没设置。Spring默认只对RuntimeException和Error回滚,受检异常需要显式指定rollbackFor。实际情况中很多人用自定义业务异常,但继承的是Exception,直接把默认行为踩中。
数据库存储引擎不支持事务。比如MySQL的MyISAM引擎不支持事务,无论你怎么配置都没用。排查问题时先确认表引擎是InnoDB。这个坑在老旧项目里会碰到。
事务管理器没有正确配置。Spring Boot默认基于DataSource自动配置事务管理器,但如果你用了多数据源,默认的事务管理器可能绑定到了错误的DataSource上,导致事务不生效或者管错了库。
Bean没有被Spring管理。如果类没有加@Service、@Component之类的注解,或者通过new关键字直接创建了对象,Spring根本不认识它,@Transactional自然也不会有任何效果。
@Transactional加到了接口或实现类上但方法签名不匹配。Spring支持注解加在接口方法或实现类方法上,但要注意如果只在接口上标注,而实现类又重写了方法且没有加注解,部分情况下注解会失效或产生混淆。建议直接标注在实现类方法上,最直观。
使用了TransactionTemplate又把业务逻辑写在回调之外。少数人混用编程式事务和声明式事务,导致事务边界混乱。这种情况不常见,但真出现了排查起来也很头疼。
上面这十条并不是让你在面试时全部背出来,而是挑两三个结合自己的项目经历讲,效果最好。比如你说“之前我们线上遇到过一次事务不生效,排查发现是方法内部自己catch了异常,后来我们统一规范了异常处理”,这种带有真实故事感的回答,比罗列知识点强得多。
5.4 隔离级别与锁的配合
事务隔离级别在面试中出现的频率也不低,但相比失效场景,这个问题更偏数据库理论。Spring通过@Transactional的isolation属性可以指定五种隔离级别:DEFAULT(用数据库默认)、READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。
面试时除了要能说出四种隔离级别对应的脏读、不可重复读、幻读问题,最好还能结合实际项目说一下为什么MySQL默认用REPEATABLE_READ,而很多互联网公司却在业务层选用READ_COMMITTED。这个问题不要求标准答案,只要你能说出两种方案的取舍就可以:MySQL默认的REPEATABLE_READ是基于MVCC实现的,对读多写少的业务很友好;但READ_COMMITTED可以降低某些间隙锁引发的死锁概率,在更新频繁的高并发场景中反而更稳。
事务和锁是密不可分的,面试中还可能被问到“悲观锁和乐观锁各适合什么场景”。悲观锁适合并发冲突概率高的场景,比如库存扣减;乐观锁通过版本号或CAS实现,适合并发冲突概率低、追求性能的场景。能把这个话题和事务隔离级别串起来讲,又比单纯背定义有层次感。
6. Spring Boot自动配置与启动流程:从“约定大于配置”说起
6.1 为什么Spring Boot能省掉那么多配置
Spring Boot最大的卖点是“约定大于配置”。传统Spring项目要配置一大堆XML,Spring Boot让大部分配置自动化完成。让我解释它的实现原理:@SpringBootApplication注解由三个注解组合而来——@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan。其中@EnableAutoConfiguration是灵魂,它内部通过@Import引入了AutoConfigurationImportSelector,这个Selector会扫描整个classpath下所有jar包里的META-INF/spring.factories文件,把所有标注了自动配置的类加载进来,再根据条件注解判断哪些配置真正生效。
所谓“根据条件注解判断”,指的是@ConditionalOnClass(classpath里有没有某个类)、@ConditionalOnMissingBean(容器里是否缺少某个Bean)、@ConditionalOnProperty(配置项是否匹配)等。比如你引入了Redis的starter,classpath里有了RedisTemplate相关的类,Spring就自动帮你创建RedisTemplate的Bean;你引入了Web的starter,内置的Tomcat和DispatcherServlet就自动就位。这套机制让“引入依赖即获得能力”成为可能。
面试里如果只答到这里,算及格;但如果你想讲得更深,可以补充一句:spring.factories在Spring Boot 2.7开始部分被AutoConfiguration.imports文件替代,在Spring Boot 3.x中自动配置的注册改用了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。这个细节能让面试官意识到你关注过新版本的变化,而非停留在古老的教程上。
6.2 自动配置的条件注解与顺序问题
条件注解是自动配置的核心,面试官很喜欢问“你想让某个自动配置只在特定条件下生效,应该怎么做”。这类题考的其实就是对@Conditional系列注解的掌握。
常用的条件注解有:
@ConditionalOnClass:classpath中存在指定的类才生效。@ConditionalOnMissingClass:classpath中不存在指定的类才生效。@ConditionalOnBean:容器中存在指定的Bean才生效。@ConditionalOnMissingBean:容器中不存在指定Bean才生效,常用于允许用户覆盖默认配置。@ConditionalOnProperty:配置文件中存在指定的属性且值匹配才生效。@ConditionalOnWebApplication:当前应用是Web应用才生效。
这里有个面试问题经常出现:当@ConditionalOnBean和@ConditionalOnMissingBean出现时,为什么有时候自动配置的顺序会影响结果?答案在于条件注解的评估时机。部分条件是在容器已经注册了大量Bean之后才评估的,所以自动配置类的特定顺序(通过@AutoConfigureBefore、@AutoConfigureAfter、@AutoConfigureOrder控制)会影响最终Bean是否创建。这条知识比较深,但确实属于“源码级理解”的加分点。
6.3 手写一个自定义Starter的思路
面试中偶尔会冒出一个题:如何写一个自定义的Spring Boot Starter。这道题考的是对自动配置机制的综合理解,如果你有相关经验,讲起来会非常加分。
一个Starter本质上就是一个普通的Maven模块,里面包含两部分:一是自动配置类,二是AutoConfiguration.imports文件(或者老版本的spring.factories)。自动配置类里定义各种@Bean方法,用条件注解控制生效范围。此外,还要提供一个属性类,比如@ConfigurationProperties(prefix = "my.starter")来绑定配置项,让使用方可以在application.yml里自定义行为。
我举一个最简的例子。假设你要做一个短信发送Starter,核心类大概是:
@AutoConfiguration @EnableConfigurationProperties(SmsProperties.class) @ConditionalOnClass(SmsSender.class) public class SmsAutoConfiguration { @Bean @ConditionalOnMissingBean public SmsSender smsSender(SmsProperties properties) { return new SmsSender(properties.getAk(), properties.getSk()); } }然后把这个类注册到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件里,写上类的全限定名即可。使用者引入starter坐标,再在application.yml里配置my.starter.ak和my.starter.sk,就自动拥有了发送短信的能力。
能把这个讲清楚,说明你对Spring Boot的自动配置有第一手的实操经验,而不是只会在网上看别人写的总结。
6.4 Spring Boot启动流程的完整时间线
另一个高频面试题是“Spring Boot启动时都做了什么”,这题考察的是对ApplicationContext启动过程的理解,回答时可以按启动顺序逐步拆解。
整体流程可以分成三个阶段:准备阶段、刷新阶段、收尾阶段。
准备阶段:SpringApplication.run()被调用后,先确定应用类型(Servlet Web、Reactive Web、非Web),然后加载所有ApplicationContextInitializer和ApplicationListener,准备环境变量Environment,最后创建ApplicationContext(默认是AnnotationConfigServletWebServerApplicationContext)。
刷新阶段:调用context.refresh(),这个方法是Spring容器启动的核心,里面包含十几个步骤。大家常听到的invokeBeanFactoryPostProcessors(处理BeanFactoryPostProcessor)、registerBeanPostProcessors(注册BeanPostProcessor)、initMessageSource(初始化国际化资源)、onRefresh(创建内嵌Web服务器并启动)都在这阶段执行。
收尾阶段:容器刷新完成之后,Spring Boot会触发ApplicationRunner和CommandLineRunner(可以做数据初始化),然后对外发布ApplicationReadyEvent,标志应用启动成功。
面试时可以加一句自己曾经排查过启动慢问题,用到--debug模式查看自动配置报告,这种经验细节会让回答更生动。
7. Spring Cloud Alibaba微服务体系:不会也要知道方向
7.1 从大厂需求看微服务的考察趋势
现在很多Java岗位的JD里会写着熟悉Spring Cloud,而国内公司多数采用Spring Cloud Alibaba体系,所以Spring Cloud Alibaba的面试题占比也在上升。代表性的组件包括:Nacos(注册中心与配置中心)、OpenFeign(声明式HTTP客户端)、Sentinel(流量治理)、Seata(分布式事务)、Gateway(API网关)、RocketMQ(消息队列)等。
如果你没有真实落地经验,但被问到相关话题,可以重点讲对几个核心问题的理解:注册中心是什么、为什么需要(服务发现与配置管理)、服务之间如何调用(Feign)、熔断降级怎么做(Sentinel)、分布式事务怎么处理(Seata)。这些属于微服务领域的公共知识,不会因为没踩过坑就完全不能聊。
7.2 Nacos和Feign的高频考点
Nacos往往被问到两个点:一是和Eureka、Consul等注册中心的对比,二是配置中心的使用场景与动态刷新机制。动态刷新这块可以讲@RefreshScope刷新Bean的作用原理:它会将Bean缓存清除,并在下一次获取时重新创建,从PropertySource中读取最新的配置。
Feign则是“声明式服务调用”的代表,考的是它的工作流程:通过动态代理生成接口的实现,把方法调用转换成HTTP请求,再由底层HTTP客户端(如OkHttp或HttpClient)发送出去,对接服务端负载均衡(内置Ribbon或者Spring Cloud LoadBalancer)。面试中常问的一个细节是Feign的日志级别配置与超时设置,这些操作可以直接照着配置文档写出来。
7.3 从单体到微服务的演进思考
面试官有时会问“你们项目为什么拆微服务,拆了之后遇到什么问题”。这类开放题没有标准答案,但你要能表述清楚拆分的动机和代价。
比如原来的单体架构维护成本上升,团队协作互相阻塞,发布频率受限,引入微服务后每个团队可以独立开发部署,技术栈选型更自由。但代价是分布式带来的新问题:网络延迟、服务间调用失败、数据一致性、链路追踪、配置管理复杂化。如果你的项目恰好经历过这种演进,把真实踩过的坑讲出来最好;如果没有,就讲理论知识框架,态度要诚恳,不要编造项目经历。
8. Spring Security与Spring AI:面试中的加分方向
8.1 Spring Security的常见问题
Spring Security在面试中不算必考,但如果你简历里写了认证授权相关的功能,那一问一个准。常考的点是认证流程:登录请求经过UsernamePasswordAuthenticationFilter,提取用户名密码,封装成Authentication对象,交给AuthenticationManager,由AuthenticationProvider做校验(通常会调用UserDetailsService加载用户信息,用PasswordEncoder比对密码),成功后生成认证信息,写入SecurityContextHolder,后续请求通过过滤器链识别认证状态。
授权相关的考点集中在方法级别的@PreAuthorize和@Secured注解,以及常见的配置主题。在Spring Boot 3里,基于SecurityFilterChain的Lambda配置风格已经取代了旧的WebSecurityConfigurerAdapter,如果你还在用老教程里的写法,面试时容易露怯。
8.2 Spring AI和AI Agent方向该怎么答
随着AI应用爆发,Spring AI在面试中也越来越常见,相关热搜词里就能看到大量关于spring ai的内容。
Spring AI的目标是把AI能力集成到Java应用里,提供统一的接口对接各类大模型供应商,帮助你快速开发RAG、Agent等应用。面试中常见的问题包括:Spring AI的核心概念(Model、EmbeddingModel、VectorStore、ChatClient等)、RAG(检索增强生成)的实现思路、如何用Spring AI开发简单的Agent、以及Spring AI Alibaba这个国内分支的进展。
如果你面试岗位明确要求AI相关能力,至少要能讲清楚RAG的架构:文档切分(Splitter)-> 向量化(EmbeddingModel)-> 存储与检索(VectorStore)-> 提示词组装(Prompt)-> 模型生成(ChatClient),并回答如何缓解大模型幻觉问题:通过检索增强、Prompt约束、答案引用来源等方式。在Spring Boot项目中引入Spring AI的starter,然后配置模型Key,5分钟就能跑起一个最小的对话接口,上手成本比想象中低。能现场演示这种最小实现,面试官通常会很满意。
8.3 我建议准备的AI方向面试话术
如果你确实没做过Spring AI,不要硬装。可以把话题引向自己熟悉的领域,比如“我了解Spring AI的基本思路,也看过官方示例,目前在项目中主要负责某块核心链路,对AI侧的集成可以快速上手”。这种坦诚加学习能力的表达,往往比背几个名词更可信。面试官更看重的是你的基础功底和快速学习能力,AI方向本身迭代极快,岗位匹配度往往可以通过短期冲刺弥补。
9. 面试实战问答速查表
我把Spring面试中最常见的问题连同标准回答框架整理成一个速查表,方便你临考前快速过一遍。注意,这不是让你背答案,是让你当作“自测清单”,看到问题先自己在心里回答,再看我给的要点。
| 问题 | 回答要点 | 加分细节 |
|---|---|---|
| 什么是IOC | 控制权反转,容器管理对象的创建与依赖 | 对比硬编码new的痛点,说明DI是IOC实现方式 |
| Bean生命周期 | 实例化、属性填充、初始化、销毁 | 按顺序说出Aware、BeanPostProcessor、@PostConstruct、InitializingBean |
| 循环依赖如何解决 | 三级缓存 | 能区分构造器注入与属性注入;能解释三级缓存和AOP的关系 |
| AOP底层原理 | JDK动态代理与CGLIB | 能说明Spring Boot 2.x默认CGLIB;能讲代理失效场景 |
| 事务为何失效 | 代理失效、异常被吞、rollbackFor未配置等 | 结合真实项目例子讲,别光背书 |
| 事务传播行为 | REQUIRED、REQUIRES_NEW、NESTED | 每个传播行为配合一个业务场景例子 |
| Spring Boot自动配置原理 | @EnableAutoConfiguration + 条件注解 | 了解Spring Boot 3.x中AutoConfiguration.imports机制 |
| 如何自定义Starter | 自动配置类 + 注册文件 | 能现场说清楚核心代码结构 |
| Spring Security认证流程 | 过滤器链 + AuthenticationManager | 能说出Spring Boot 3迁移要点 |
| Spring AI怎么用 | 统一模型接口 + RAG + Agent | 能讲清楚RAG流程和向量库选型 |
10. 终极建议:面试是“讲道理”,不是“背答案”
Spring面试题准备到最后,我想强调一个核心心法:面试官考核的不是你背诵了多少知识点,而是你在遇到一个具体问题时,能不能讲清楚它背后的原理,并给出对应的处理思路。
所以我的建议是:每个高频考点,都试着用“是什么 -> 为什么需要 -> 底层怎么实现 -> 实际项目中怎么用 -> 遇到问题怎么排查”这个五段式结构去组织你的回答。这比单纯背一堆定义更有说服力,也更符合“资深从业者”的身份画像。面试的时候,安安心心把常规问题吃透,比押偏门怪题更稳妥。这里没有捷径,就是一遍遍对着问题训练自己的组织能力。
最后再分享一个小技巧:面试前可以自己开一个空项目,把提到的核心机制(比如三级缓存、动态代理、事务传播行为)用代码去验证。我这几年来带过的人里,凡是亲手验证过这些机制的,面试表现普遍比只看文档的人扎实得多。毕竟Spring这类框架,看懂源码是技术,能讲明白才是本事。