简介经典Spring面试题文档收录25个高频问答面向准备Java后端岗位面试的开发者及需要系统梳理Spring核心知识的初学者。内容从框架基础概念讲起覆盖IoCDI、BeanFactory与ApplicationContext区别、Bean生命周期与作用域、注解配置、自动写入、构造器注入与setter注入对比、事件机制及设计模式等常见考点每个问题均配有简明答案与分析思路便于快速记忆和查漏补缺。资源包共1个docx文件大小88KB排版清晰适合直接阅读或打印复习。目前已有403人学习适合在面试冲刺阶段作为速查手册也可作为日常复习提纲帮助加深对Spring框架整体脉络的理解。对已有一定基础的开发者而言这份清单还能帮助检验知识盲区并提示面试官常追问的细节方向。1. 经典Spring面试题背完100道题还挂在IoC上问题出在哪面试圈里有个很常见的场景候选人把Spring的八股文背得滚瓜烂熟一问说说Bean的生命周期就卡壳再追问一句你怎么验证你的答案干脆冷场。我参与过不少模拟面试也帮人改过简历发现一个规律刷完上百道经典Spring面试题的人往往挂在最基础的两个字上——为什么。经典Spring面试题考察的从来不是背诵而是四件事概念能不能说清、代码能不能跑通、边界能不能答出、踩过的坑能不能讲出取舍。单靠答案列表你只能撑过前三个问题撑不过追问。这篇笔记把经典Spring面试题按四个主题重新过一遍IoC与Bean生命周期、AOP与动态代理、事务、高频易错点并给每道题配一个能直接运行的验证代码。适合准备面试的Java工程师也适合拿来做团队知识自测。目标只有一个下次被问到你能从一个具体现象讲到根因而不是从书目录背到书目录。2. IoC与Bean生命周期两段代码跑通容器的管与Bean的活先看两道高频题BeanFactory和ApplicationContext有什么区别、Bean的生命周期是什么样的这两道题的共同点是只背结论不验证的人必挂。下面用两段短代码把IoC容器怎么管对象和Bean一生怎么走跑一遍。2.1 从new对象到依赖注入IoC容器到底替你做了什么很多人说IoC就是控制反转但面试官下一句通常是反转了什么答案是反转了对象获取依赖的控制权。没有IoC时依赖关系写死在调用方代码里public class OrderService { private PaymentService paymentService; public OrderService() { // 没有IoC每次都要自己new依赖换实现就得改这里 this.paymentService new PaymentService(); } }这段代码的问题不是不能跑而是改不动换一个支付实现要改构造函数要给OrderService加缓存得先找到所有new它的地方。引入容器后代码变成下面这样Component public class OrderService { private final PaymentService paymentService; // 构造器注入依赖由容器提供OrderService自己不管创建 public OrderService(PaymentService paymentService) { this.paymentService paymentService; } }逻辑说明第一段是主动找依赖第二段是被动等注入。容器扫描到OrderService构造器需要PaymentService就先把PaymentService创建好再通过构造器传进去。这就是依赖注入DI它是IoC的一种实现方式也是大多数Spring项目里最常见的Bean装配方式。参数说明构造器注入有两个好处。一是依赖不可变字段加final少了状态被改的隐患二是可测试测试里new一个假支付服务就能构造OrderService不用启动容器。相比之下字段注入Autowired直接打在字段上写起来快但依赖对测试是黑匣子反射才能换所以不推荐作为默认方案。接口关系也是这道题的延伸。BeanFactory是最底层容器只负责Bean的创建和获取懒加载ApplicationContext是它的扩展多了事件发布、国际化、资源加载还会自动注册BeanPostProcessor和BeanFactoryPostProcessor默认启动时就实例化单例Bean。常见实现是DefaultListableBeanFactory配AnnotationConfigApplicationContext。答这道题的加分句是日常开发用的ApplicationContext不是替代BeanFactory而是在它之上加了面向容器的扩展能力。2.2 Bean的一生用BeanPostProcessor把生命周期顺序跑出来生命周期这道题背12个步骤不如跑一遍。我准备了一个专门用来打印顺序的Bean实现InitializingBean和DisposableBean两个接口再用Bean指定自定义init/destroy方法public class LifecycleBean implements InitializingBean, DisposableBean { private String appName; public LifecycleBean() { System.out.println([1] 实例化构造函数执行); } // Value打在setter上会在属性填充阶段被调用 Value(${app.name:demo}) public void setAppName(String appName) { this.appName appName; System.out.println([2] 属性填充setter注入 appName); } PostConstruct public void postConstruct() { System.out.println([4] 初始化回调PostConstruct); } Override public void afterPropertiesSet() throws Exception { System.out.println([5] 初始化回调InitializingBean.afterPropertiesSet); } public void customInit() { System.out.println([6] 初始化回调Bean(initMethod)); } PreDestroy public void preDestroy() { System.out.println([8] 销毁回调PreDestroy); } Override public void destroy() throws Exception { System.out.println([9] 销毁回调DisposableBean.destroy); } public void customDestroy() { System.out.println([10] 销毁回调Bean(destroyMethod)); } }注意[2]和[3]之间其实还有一层Aware回调BeanNameAware、BeanFactoryAware等在真实生命周期里位于属性填充之后、初始化前处理之前这里没单独打印。再写一个BeanPostProcessor它才是生命周期题里最值钱的考点Component public class TracePostProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) { if (bean instanceof LifecycleBean) { System.out.println([3] 初始化前处理postProcessBeforeInitialization); } return bean; } Override public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean instanceof LifecycleBean) { System.out.println([7] 初始化后处理postProcessAfterInitialization); } return bean; } }最后用配置类把LifecycleBean注册进去Configuration public class LifecycleConfig { Bean(initMethod customInit, destroyMethod customDestroy) public LifecycleBean lifecycleBean() { return new LifecycleBean(); } }逻辑说明运行后控制台会按1→2→3→4→5→6→7→8→9→10的顺序打印。这个顺序就是面试官要的生命周期。[7]这一步尤其关键Spring的AOP代理就是在这里通过BeanPostProcessor把原始对象包成了代理对象再放进容器。所以你回答生命周期时补一句AOP发生在初始化后的BeanPostProcessor阶段面试官通常会顺着去问动态代理。参数说明PostConstruct和PreDestroy是JSR规范注解工程里如果用Spring Boot 3.x需要jakarta.annotation包如果出现编译找不到注解多半是依赖没带齐。Bean(initMethod)只对配置类里显式注册的Bean生效对Component扫描出来的Bean不生效——这也是很多人反复打印看不到customInit的原因。提示验证销毁阶段时容器要正常关闭。用AnnotationConfigApplicationContext时记得调用close()或者用try-with-resources包裹否则只输出到[7]就结束了。这个跑一遍的习惯比背步骤管用得多。下次被问生命周期你直接说我验证过顺序是实例化、属性填充、初始化前、三个初始化回调、初始化后、销毁面试官就知道你真的动手了。3. AOP与动态代理从手写JDK代理到注解切面落地AOP几乎是Spring面试的必考区两道题最有代表性JDK动态代理和CGLIB有什么区别五个通知类型怎么用第一道考原理第二道考落地。这章把两道题都落到代码上。3.1 JDK动态代理还是CGLIB一句话判断Spring走哪条路JDK动态代理基于接口运行时用Proxy.newProxyInstance生成一个实现了目标接口的代理对象CGLIB基于继承运行时生成目标类的子类重写父类方法。判断路线的口诀只有一句Spring Framework老逻辑是有接口走JDK没接口走CGLIB而Spring Boot从2.x开始默认proxy-target-classtrue一律CGLIB。面试问到你项目里用的是哪个默认答CGLIB即可但要能说出判断依据。手写JDK动态代理是经典面试题代码很短建议直接背下来// 目标接口与实现 public interface UserService { void findUser(String userId); } public class UserServiceImpl implements UserService { Override public void findUser(String userId) { System.out.println(查询用户: userId); } }UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 参数1类加载器 target.getClass().getInterfaces(), // 参数2要代理的接口列表 (proxyInstance, method, args) - { // 参数3InvocationHandler System.out.println(调用前记录日志: method.getName()); Object result method.invoke(target, args); System.out.println(调用后记录日志: method.getName()); return result; }); proxy.findUser(1001);逻辑说明三个参数缺一不可。类加载器负责生成代理类接口列表决定了代理对象能响应哪些方法InvocationHandler里的invoke方法是每次代理方法被调用时的统一入口method.invoke(target, args)这行才是真正调用原目标对象。参数说明JDK代理的局限很清楚——目标类必须实现了接口。如果UserServiceImpl没有接口这段代码直接报错。CGLIB就是为这个场景设计的它通过字节码生成子类来重写方法所以final类、final方法都无法被它代理。Spring 4之后CGLIB的类被打包进了spring-core工程里不用额外引cglib依赖。验证Spring到底用了哪种代理有个土办法打印代理对象的类名。CGLIB代理的类名里带$$EnhancerBySpringCGLIB$$JDK代理带$Proxy0。面试时能说出这个验证手段比背十遍区别都有说服力。3.2 最小注解版AOP切面切点表达式与通知类型怎么配合原理讲完动手写一个最小切面。先准备一个要被增强的业务类Component public class OrderService { public void createOrder() { System.out.println(创建订单); } public void deleteOrder(String orderId) { System.out.println(删除订单: orderId); } }再写切面Aspect Component public class OrderAspect { // 切点命中OrderService下所有public方法 Pointcut(execution(public * com.example.service.OrderService.*(..))) public void orderMethods() { } Before(orderMethods()) public void before(JoinPoint jp) { System.out.println(前置通知: jp.getSignature().getName()); } AfterReturning(pointcut orderMethods(), returning result) public void afterReturning(JoinPoint jp, Object result) { System.out.println(返回通知, 结果: result); } AfterThrowing(pointcut orderMethods(), throwing ex) public void afterThrowing(JoinPoint jp, Exception ex) { System.out.println(异常通知: ex.getMessage()); } Around(orderMethods()) public Object around(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); System.out.println(环绕通知, 耗时: (System.currentTimeMillis() - start) ms); return result; } }逻辑说明Aspect声明这是个切面Component把它交给容器。Pointcut定义切点Before、AfterReturning、AfterThrowing、Around是通知类型。还少一个After它等价于finally不管有没有异常都会执行。执行顺序是Around先拦然后Before方法返回后After和AfterReturning收尾抛异常时走AfterThrowing。参数说明切点表达式是重点。execution(public * com.example.service.OrderService.(..))拆开看public是修饰符是返回类型通配com.example.service.OrderService是目标类(..)是任意方法任意参数。常用变体有execution(com.example..service..(..))匹配service包及子包within(com.example.service.OrderService)匹配类型内全部方法annotation(com.example.Log)匹配标注了某注解的方法。切点写错最坑的是不报错只是不生效。提示注解版AOP需要EnableAspectJAutoProxy开启或直接用Spring Boot它会在启动时自动开启。Spring Boot项目里发现切面不生效先检查切面类有没有被扫描到再看切点表达式是不是把包名写错了。这里还有一个高频追问同类自调用时AOP不生效。OrderService里createOrder调this.deleteOrder()deleteOrder上的切点不会触发因为调用走的是原始对象而不是代理。这个问题在事务里更典型下一章会实际复现它。4. Spring事务七种传播行为一张表两个失效场景现场复现事务这一块每年面试都会出现Spring事务传播行为有哪几种事务为什么失效这两道题。传播行为考记忆失效场景考排查。先给记忆表再上复现代码。4.1 传播行为与隔离级别记忆表和三种实用选型七种传播行为一张表说清传播行为含义典型场景REQUIRED默认有事务就加入没有就新建绝大多数业务方法REQUIRES_NEW挂起当前事务新开一个写操作日志独立提交NESTED基于保存点嵌套事务批量处理单项失败只回滚当前项SUPPORTS有事务就加入没有就以非事务执行查询方法NOT_SUPPORTED挂起当前事务以非事务执行减少锁持有时间MANDATORY必须已有事务否则抛异常内部约定必须走事务的方法NEVER必须无事务否则抛异常测试外部服务记忆方法别硬背七个把REQUIRED、REQUIRES_NEW、NESTED三个吃透就够用。REQUIRED是默认几乎天天用REQUIRES_NEW用于这个操作必须独立提交不能跟着主事务回滚典型是审计日志NESTED用于批量里单条出错只回滚这一条靠JDBC的保存点实现。剩下四个看名字就能猜出语义。隔离级别也要能背下来隔离级别脏读不可重复读幻读READ_UNCOMMITTED存在存在存在READ_COMMITTED避免存在存在REPEATABLE_READ避免避免存在SERIALIZABLE避免避免避免参数说明Spring事务默认隔离级别是DEFAULT即跟随数据库设置。常见开源数据库默认REPEATABLE_READ而且InnoDB引擎下通过MVCC和间隙锁多数场景幻读也能被挡住某些数据库则默认READ_COMMITTED。面试时不要只说四种隔离级别要补一句Spring默认用DEFAULT跟随数据库默认值这比盲目配置REPEATABLE_READ更显功力。选型建议读多写少的项目传播行为保持REQUIRED隔离级别用默认需要独立记录操作日志时用REQUIRES_NEW批量导入用NESTED避免一行脏数据把整批都滚回去。长事务是另一个隐藏坑一个大事务跑几十秒会让连接池和锁拖垮系统改法是把大事务拆小而不是调高隔离级别。4.2 事务失效现场自调用与吞异常用代码复现事务失效的经典现场有两个都是真实项目里查了半天才定位到的。现场一自调用。现象是方法内部调用同类另一个带Transactional的方法传播行为完全失效。复现代码如下Service public class OrderService { Transactional public void createOrder() { System.out.println(插入订单头); // 自调用同一个类里直接调用不走代理 this.insertOrderItem(); } Transactional(propagation Propagation.REQUIRES_NEW) public void insertOrderItem() { System.out.println(插入订单明细这里设置独立事务); } }原因外部调用createOrder时先经过事务代理但createOrder内部this.insertOrderItem()调用的是原始对象的方法代理管不着REQUIRES_NEW自然不生效。解决最干净的是把insertOrderItem拆到另一个Service里由代理重新入口也可以用ObjectProvider拿到自身代理再调用或者放弃注解改用TransactionTemplate做编程式事务。现场二吞异常。现象是事务代码里try-catch把异常吃掉事务照样提交扣款成功了但通知失败业务上必须回滚却回滚不了Transactional public void pay() { try { deduction(); // 扣款SQL执行成功 notifyUser(); // 发通知这里抛了异常 } catch (Exception e) { // 异常被吞掉Spring根本不知道出错了 System.out.println(通知失败继续往下走); } }原因Spring默认通过异常触发回滚异常没抛出去事务管理器收不到信号最终提交。解决让通知失败抛出异常触发回滚如果通知确实不能影响扣款就把通知改成REQUIRES_NEW独立事务再配合补偿机制。还有一个备用手段手动调用TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()标记回滚。顺带一个异常类型坑Spring默认只对RuntimeException和Error回滚checked异常不会回滚。所以业务方法上常见做法是显式写Transactional(rollbackFor Exception.class) public void syncData() throws Exception { // checked异常也要触发回滚 }参数说明rollbackForException.class的意思是任何异常都回滚。这个参数要结合业务判断不是所有方法都该无脑加只有明确要求出错必须整体回滚时再写。注意MySQL的InnoDB引擎才支持事务MyISAM引擎下Transactional静默失效。这个坑不容易排查因为代码和配置都对就是数据不回滚。5. 经典Spring面试题的避坑清单5个答错率最高的点这章不是新主题而是把前面题目里最容易答错、最容易被追问的点单独拎出来按现象→原因→解决写。每一条都是我在模拟面试里反复见到的翻车现场。5.1 概念题里容易说半句就停的坑坑一Spring单例Bean是线程安全的吗。现象张口就答安全因为单例只有一个实例或者反过来答不安全但没有下文。原因把单例和线程安全混为一谈了。容器只保证Bean实例唯一不保证状态安全。一个带可变字段的service如果多个线程同时写那个字段照样出并发问题。解决默认把Bean设计成无状态把状态放在方法参数里确实要共享状态时用ThreadLocal、原子类或加锁。答题模板是容器保证唯一实例不保证线程安全是否安全取决于Bean内有没有可变共享状态。坑二Component和Bean到底怎么分。现象背了Component加在类上Bean加在方法上就停了面试官追问那我有一个第三方类用哪个时卡住。原因没有讲清适用边界。Component靠扫描只能处理源码在自己工程里的类第三方类改不了源码没法加Component。解决一句话区分自己的类用Component第三方类或需要复杂初始化逻辑的用Bean写在Configuration方法里。Bean还能配合ConditionalOnProperty等条件装配这是Component做不到的。坑三BeanFactory和ApplicationContext。现象只答ApplicationContext继承BeanFactory然后没有然后。原因面试官要的不是接口继承关系这个结论而是你平时到底用的是什么它比底层多了什么。解决按三层回答第一层说接口关系第二层说ApplicationContext多了事件发布、国际化、资源加载、自动注册后置处理器而且默认启动时就实例化单例第三层给一个常见实现类比如AnnotationConfigApplicationContext配DefaultListableBeanFactory。5.2 配置和代码里容易翻车的坑坑四启动报BeanCurrentlyInCreationException循环依赖。现象A依赖B、B依赖A项目一启动就抛异常把其中一个改成构造器注入后必现。原因Spring靠三级缓存解决循环依赖是有前提的。构造器注入发生时对象还没实例化完缓存里拿不到早期引用所以构造器循环依赖无解。setter或字段注入能绕过但那只是绕不是解。解决正确的做法是重构把互相依赖的逻辑拆开或加一层中间对象别在设计上绕圈。如果老项目短期改不动可以设置spring.main.allow-circular-referencestrue这是后悔药不是常规方案Spring Boot从2.6开始默认关掉了循环依赖不配置就继续报错。坑五Transactional加在非public方法上。现象方法不报错数据就是不回滚日志里也看不到事务异常排查小半天。原因事务代理基于CGLIB子类能重写的是public方法非public方法上的Transactional不会进入事务切面的处理范围注解白写了。解决把方法改成public并且确保它是从外部被调用的入口。如果是同类自调用recheck第4章的自调用问题方法再public也没用必须换入口。6. 面试前夜的验证三招迷你容器、条件断点、倒读接口清单面试前一晚刷题没有意义做这三件事更有用把容器写一遍、用断点看一次真实运行、把源码的接口骨架过一遍。每一招都能直接验证前面几章的概念。6.1 写一个能跑的迷你IoC容器// 极简IoC容器注册→实例化→按字段注入依赖 public class MiniContainer { private MapString, Object beans new HashMap(); // 第一遍把所有类实例化 public void register(Class? clazz) throws Exception { String name clazz.getSimpleName(); Object instance clazz.getDeclaredConstructor().newInstance(); beans.put(name, instance); } // 第二遍遍历所有Bean把带Autowired的字段注入进去 public void inject() throws Exception { for (Object bean : beans.values()) { for (Field field : bean.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); Object dep beans.get(field.getType().getSimpleName()); field.set(bean, dep); } } } } SuppressWarnings(unchecked) public T T get(String name) { return (T) beans.get(name); } }逻辑说明这个版本不支持接口注入、不支持循环依赖、不处理作用域但它把容器最核心的三件事讲透了创建实例、注入依赖、按名字提供对象。写完这段代码前面容器替你做了什么就不再是抽象概念了。6.2 用条件断点看AOP代理在哪一步生成在自定义的BeanPostProcessor里打一行日志还不够更直接的验证是把断点打在postProcessAfterInitialization里条件设置为beanName等于你的业务Bean名执行后会一路看到这个Bean是怎么被检查、被包裹的。观察bean.getClass()的输出如果类名里带$$EnhancerBySpringCGLIB$$说明AOP代理就是在这里替换了原始对象。这一招能顺带回答Spring什么时候生成的代理这个追问。6.3 从接口倒读源码的三步法别从第1行读源码那会让你一夜白读。按三步走先列接口清单BeanFactory有哪些方法、ApplicationContext扩展了哪些能力再找默认实现也就是DefaultListableBeanFactory和AnnotationConfigApplicationContext的类继承关系最后沿着refresh()方法里的方法名读节奏不要求逐行看懂能说出invokeBeanFactoryPostProcessors、finishBeanFactoryInitialization、finishRefresh这三步的大致顺序就够了。很多经典Spring面试题的答案其实都藏在refresh()这十几个方法名里。我自己的习惯是面试前一天不刷题就做这三件事。背答案只能撑三分钟能动手演示的才是你的。把这个每个答案都能用代码证明的习惯带进项目里下次再被问到说说你对Spring的理解你会发现自己不是在背笔记而是在讲你用过的方案。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?