
Spring框架从2002年诞生到现在二十多年过去了依然是Java后端开发的事实标准。不管你是刚准备校招的应届生还是工作了三五年想跳槽的进阶选手面试时大概率逃不过Spring这一关。网上关于Spring的八股文满天飞但多数是零散的问答背完就忘根本串不起来。我结合自己这些年看源码、带团队、面试候选人的经验把Spring框架的核心知识点重新梳理了一遍不整虚的全是硬货。这篇文章适合所有想系统掌握Spring核心原理的Java开发者不管是应付面试还是夯实基础都能派上用场。1. 从容器说起IoC的核心思路与设计哲学1.1 控制反转到底反转了什么很多新手刚接触Spring时最懵的就是IoCInversion of Control控制反转这个概念。教科书上的定义绕来绕去什么“将对象的创建和依赖关系的维护交给容器管理”听起来很高深其实用大白话讲就一句话以前你自己new对象现在你不用new了容器把对象给你送过来。传统开发模式里Service层要使用DAO层直接new UserDao()就完事了。这样做的问题是层与层之间耦合太紧你换一个DAO实现Service代码就得改动。IoC的思路是Service不自己创建DAO了而是声明“我需要一个UserDao”由容器在运行时把实现类注入进来。这样一来Service只依赖接口不依赖具体实现换实现类只要改配置就行。真正理解IoC要抓住三个关键点对象由谁创建传统是调用者创建IoC是容器创建。就像你去餐厅吃饭传统模式是你自己下厨IoC模式是你只需要点菜厨师容器把菜做好端上来。依赖由谁维护传统是对象内部自己维护依赖IoC是容器负责把依赖关系组装好。对象本身不需要关心依赖从哪里来只管用。生命周期的管理容器不只负责创建对象还负责对象何时初始化、何时销毁。单例对象在容器启动时就创建原型对象在使用时才会被实例化。IoC带来的好处往小了说是解耦往大了说是改变了整个项目的架构方式。以前你要做单元测试Service依赖于具体的DAO实现测试时得连数据库。用了IoC之后测试时注入一个Mock对象就行了开发效率高很多。1.2 DI的几种注入方式与适用场景DIDependency Injection依赖注入是IoC的具体实现方式。我们最常用的注入方式有三种构造器注入、Setter注入、字段注入。构造器注入是我最推荐的方式。它是Spring官方文档里明确推荐的做法好处在于依赖不可变、能保证依赖完整性——对象被创建时依赖就已经注入完成不会出现空指针。缺点是当依赖太多时构造器参数会很长说明这个类可能违反了单一职责原则需要拆分。代码这样写Service public class UserServiceImpl implements UserService { private final UserDao userDao; private final PasswordEncoder passwordEncoder; // 构造器注入final字段保证不可变 public UserServiceImpl(UserDao userDao, PasswordEncoder passwordEncoder) { this.userDao userDao; this.passwordEncoder passwordEncoder; } }Setter注入适合可选依赖的场景就是那种“有这个依赖更好没有也能跑”的情况。Spring早期的XML配置时代Setter注入非常流行因为配置灵活想注入哪个就注入哪个。随着构造器注入的普及Setter注入在业务代码里用得越来越少了但在一些框架代码、配置类里还能看到。字段注入用Autowired直接打在字段上代码最简洁但这是一种“看起来很美”的写法。问题在于测试不方便、容易产生循环依赖、依赖关系不透明。代码里看不出对象什么时候需要什么依赖违反了不少设计原则。我在带团队时明确要求代码Commit信息里禁止出现Autowired用在字段上除非是极特殊情况比如某些框架回调场景。1.3 BeanFactory与ApplicationContext的区别从容器实现来看Spring有两个核心接口BeanFactory和ApplicationContext。BeanFactory是Spring容器的根接口提供了最基础的IoC功能只有在调用getBean()时才会实例化对象属于懒加载模式。它通常是面向Spring框架自身的编程模型。ApplicationContext是BeanFactory的子接口功能更完整增加了事件发布、国际化消息支持、资源加载、自动注册BeanPostProcessor、自动注册ApplicationListener等功能。而且ApplicationContext在容器启动时就预实例化所有单例Bean属于饿汉模式能在启动阶段就发现配置错误。实际开发中我们基本不会直接用BeanFactory而是用ApplicationContext的各种实现类。比如ClassPathXmlApplicationContext用于读取classpath下的XML配置AnnotationConfigApplicationContext用于读取Java配置类WebApplicationContext用于Web环境。这里有个面试常问的点ApplicationContext是在容器启动时就把所有单例Bean创建出来吗答案是不完全对。默认情况下确实是启动时创建但如果你给Bean配置了Lazy注解Spring就会延迟到第一次使用时才创建该Bean。Lazy的底层原理是创建了一个代理对象真正调用方法时才触发真正的Bean创建。2. 造物逻辑Bean生命周期与三级缓存2.1 一个Bean从出生到死亡的全过程Bean的生命周期是Spring面试题里出镜率最高的考点之一也是很多工作多年的程序员容易记混的地方。完整流程我用一个执行顺序串起来实例化通过构造器或工厂方法创建Bean实例。属性填充Spring会把当前Bean依赖的其他属性、对象通过Autowired、Resource、Value等注解或者XML配置进行填充。Aware接口回调如果Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware等接口Spring会调用对应的setter方法让Bean感知到自己的名字、所属容器等信息。BeanPostProcessor前置处理调用postProcessBeforeInitialization方法。初始化方法先执行PostConstruct标注的方法再执行InitializingBean接口的afterPropertiesSet()方法最后执行XML或Bean(initMethod )指定的自定义初始化方法。BeanPostProcessor后置处理调用postProcessAfterInitialization方法。这一步产生的对象才是最终暴露给外部使用的Bean代理对象就是在这个阶段生成的。使用Bean容器开始对外提供服务。销毁前处理先执行PreDestroy标注的方法再执行DisposableBean接口的destroy()方法最后执行自定义的销毁方法。我用一张表格帮你理清初始化三个方法的执行优先级方法类型执行顺序说明PostConstruct注解方法第1个执行JSR-250规范定义最常用InitializingBean.afterPropertiesSet()第2个执行Spring框架接口不推荐直接用Bean(initMethod customInit)第3个执行配置类里指定的方法三个方法都执行顺序固定你需要清楚这个顺序才能在生产环境debug时准确定位问题。我遇到过一个线上故障就是有人在PostConstruct里做了耗时很久的预热操作导致容器启动超时。排查了很久最后是靠日志里的执行顺序才定位到问题。2.2 循环依赖与三级缓存原理循环依赖是指A依赖BB又依赖A。如果使用构造器注入Spring启动时会直接抛异常BeanCurrentlyInCreationException因为构造器注入在实例化阶段就强制要求依赖对象已创建无法折中。但如果是字段注入或Setter注入Spring通过三级缓存就能破解这个死局。三级缓存对应三个Map// 一级缓存成品对象所有已经完全初始化好的单例Bean MapString, Object singletonObjects; // 二级缓存半成品对象已经实例化但还没完成属性填充的Bean MapString, Object earlySingletonObjects; // 三级缓存对象工厂缓存存放ObjectFactory用于生成早期暴露的代理对象 MapString, ObjectFactory? singletonFactories;循环依赖的解决过程是这样的A创建时发现自己需要B先从缓存里找B找不到就创建B。B创建时发现自己需要A于是去找A。此时A虽然还没完全创建好但A已经把自身暴露到了三级缓存里。B从三级缓存中拿到A的ObjectFactory调用getEarlyBeanReference()拿到A的早期引用半成品或代理对象也就A的早期依赖。B拿到A之后继续完成自己的创建然后把自己放进一级缓存。A再继续从一级缓存里拿到B的成品引用此时A拿到的是同一个BBean完全创建结束。整个机制最核心的设计在第三级缓存也就是ObjectFactory。第三级缓存的意义不只是解决循环依赖而是解决一个更复杂的问题如果A需要AOP代理那么在循环依赖场景下B拿到的A必须是代理对象而不是普通对象。Spring通过ObjectFactory在调用时提前判断是否需要生成代理从而保证B拿到的是正确的代理对象。这里有个经典的面试衍生题为什么不用两级缓存如果只需要解决循环依赖二级缓存就够了。第三级缓存是为了代理的时机问题。如果A不需要AOP代理二级缓存就能解决循环依赖但如果A需要AOP代理B去拿A时必须拿到代理对象而代理对象的创建时机在postProcessAfterInitialization太晚了。所以Spring把代理逻辑封装在ObjectFactory里B需要的A时再决定生成裸Bean还是代理Bean。2.3 单例与原型作用域的选择Spring Bean默认是单例Singleton作用域同一容器内同一个Bean只创建一个实例。这是Spring推荐的方式因为Spring本身是无状态的框架单例状态下不用考虑并发问题。但如果你在单例Bean里用了可变状态那就要特别注意线程安全性了。有个常见的坑在单例Bean里声明了一个非静态成员变量来存储请求级别的数据这在多线程环境下会互相污染。我之前接手过一个项目有人在Service里加了一个private String currentUser结果并发请求下用户A的数据串到了用户B的订单里排查了半天才发现是这个字段在作怪。正确的做法是用ThreadLocal或者直接通过方法参数传递。原型Prototype作用域下每次获取Bean都会创建一个新实例。但注意原型Bean里如果依赖了单例Bean这是合法的反过来单例Bean依赖原型Bean时每次注入的是同一个原型Bean实例就失去了原型的意义。解决办法是用Scope(value prototype, proxyMode ScopedProxyMode.TARGET_CLASS)通过代理来保证每次都拿到新的原型Bean。关于作用域的选择我的建议很明确默认用单例只有非要用到状态隔离时才考虑原型或者自己new。原型Bean的创建和销毁由容器管理如果原型Bean依赖了DisposableBean接口Spring并不会自动调用其销毁方法因为容器不知道原型Bean的生命周期何时结束。这一点在工作多年的人里也有不少踩坑的。3. 织入横切逻辑AOP原理与实战手记3.1 AOP的核心概念与内在联系AOPAspect Oriented Programming面向切面编程解决的核心问题是如何在不修改业务代码的前提下将日志记录、权限校验、性能监控等横切逻辑统一放到业务代码之外。理解AOP要把这些概念串起来切面Aspect横切关注点的封装比如日志切面、事务切面。用Aspect注解声明。连接点JoinPoint程序执行的某个特定位置比如方法调用前、方法调用后、抛出异常时。Spring AOP只支持方法级别的连接点。通知Advice切面在某个连接点执行的具体动作。Spring定义了五种通知类型前置通知Before、后置通知AfterReturning、异常通知AfterThrowing、最终通知After、环绕通知Around。切入点Pointcut定义匹配通知织入哪些连接点的规则通过表达式来匹配比如execution(* com.example.service.*.*(..))表示匹配service包下所有类的所有方法。联系在于切入点决定了在哪些连接点上执行通知切面是通知和切入点的集合。打个生活化的比方连接点就是火车站的各个站台切入点是编好了的列车运行到哪个站通知就是在对应站台执行的操作比如播音报站、检票。3.2 静态代理、JDK动态代理与CGLIB代理的选择Spring AOP的底层实现依赖动态代理分为JDK动态代理和CGLIB代理两种。JDK动态代理要求目标类实现至少一个接口。它通过java.lang.reflect.Proxy和InvocationHandler在运行时生成一个代理类代理类实现相同的接口在调用方法时拦截并织入增强逻辑。代码大致是这样public class JdkProxyHandler implements InvocationHandler { private final Object target; public JdkProxyHandler(Object target) { this.target target; } Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println(前置处理记录日志); Object result method.invoke(target, args); System.out.println(后置处理清理工作); return result; } }CGLIB是通过继承目标类生成子类对目标类的方法进行重写来实现增强。因为是基于继承所以目标类的方法不能是final否则无法被重写。Spring Boot 2.x版本开始默认使用CGLIB代理即使目标类实现了接口也会优先用CGLIB因为CGLIB不需要目标类实现接口更灵活代理对象在多态应用下更自然。这里有一个大坑也是面试题常客JDK动态代理产生的代理对象是com.sun.proxy.$Proxy类型而CGLIB生成的代理类是目标类的子类。如果你在代码里强制类型转换目标类的具体实现类CGLIB代理没问题JDK代理却会报ClassCastException。更隐蔽的是CGLIB代理对象在调用方法时如果方法内部通过this调用另一个被拦截的方法这个调用不会经过代理所以内部的增强逻辑不会被触发。这就是经典的this自调用失效问题也是事务注解经常在这个场景下失效的原因之一。3.3 Pointcut表达式与切面顺序编排Pointcut表达式是AOP的搜索定位系统最常见的表达式类型是execution格式如下execution(修饰符? 返回类型 包名.类名.方法名(参数列表) 异常?)完整示例Pointcut(execution(public * com.example.order.service.*.*(..))) public void servicePointcut() {}第一个*表示返回类型不限。com.example.order.service.*表示service包下所有类。第二个*表示所有方法。(..)表示参数不限。除了execution还有within限定包下所有方法、annotation匹配标注了特定注解的方法、bean按Bean名字匹配等。切面的执行顺序在多个切面时很关键Spring按Order注解的值从小到大排列值越小优先级越高。在方法执行前优先级高的先执行在方法执行后优先级低的先执行。举个实际例子事务切面和日志切面同时存在你希望日志先记录还是事务先开启如果日志优先级高它先执行Before然后事务开启最终方法执行完事务先提交还是日志先记录这个顺序要靠Order来控制。默认情况下同一件事务切面Spring的TransactionInterceptor优先级比较低所以日志一般能在事务外层包住。4. 事务与数据访问从麻烦到省心4.1 声明式事务的原理与失效场景排查Spring事务管理分为编程式事务和声明式事务。编程式事务是手动写transactionTemplate.execute()这样的模板代码声明式事务是加一个Transactional注解就完事底层靠AOP拦截器实现。声明式事务的执行逻辑是当某个方法标了TransactionalSpring会生成一个代理对象调用该方法时会先获取数据库连接设置setAutoCommit(false)然后执行业务逻辑如果没有异常就提交如果有运行时异常就回滚。这里有个关键点Spring默认只对RuntimeException和Error回滚对受检异常CheckedException不回滚。生产环境里最常见的失效场景我整理成了一张速查表失效场景根本原因解决办法方法被private修饰CGLIB无法代理私有方法改为public类内部自调用this调用不经过代理注入自身代理或拆分到另一个Bean抛出受检异常不回滚Spring默认回滚策略Transactional(rollbackFor Exception.class)数据库引擎不支持事务MyISAM引擎不支持改成InnoDB多线程下事务不生效Spring事务绑定到当前线程把方法调用放到主线程或使用编程式事务被捕获了异常导致不抛出事务无法感知异常在catch块中手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()这当中有个坑比较隐蔽基于接口的动态代理如果注解加在接口方法上那么只能使用JDK动态代理的场景才生效。Spring Boot 2.x默认CGLIB对类方法做代理不再依赖接口所以较少遇到了但还是建议把Transactional加在实现类的方法上更稳妥。4.2 传播行为与隔离级别事务传播行为定义了事务与调用链的关系一共七种面试最常问的是REQUIRED、REQUIRES_NEW、NESTED。REQUIRED默认如果当前存在事务则加入当前事务如果当前没有事务则创建新事务。两个方法都在同一个事务里一个异常会影响另一个。REQUIRES_NEW不管当前有没有事务都创建一个新事务。如果已有事务挂起当前事务。适合记录操作日志这种业务场景日志失败不能影响主业务提交。NESTED嵌套事务。如果当前没有事务行为同REQUIRED如果当前有事务则在当前事务内创建一个保存点Savepoint嵌套的子事务可以回滚到这个保存点而不影响外层事务。我需要特别澄清NESTED和REQUIRES_NEW的区别REQUIRES_NEW是完全独立的新事务和外部事务互不干扰而NESTED是嵌套的内部事务外部事务提交时一起提交外部事务回滚时内部事务也跟着回滚只是内部事务发生异常时可以单独回滚到保存点不影响外部事务继续执行。隔离级别有四种READ_UNCOMMITTED、READ_COMMITTED、REPEATABLE_READ、SERIALIZABLE。MySQL默认是REPEATABLE_READ。Spring的Transactional(isolation Isolation.DEFAULT)默认使用数据库默认的隔离级别一般不显式改它因为高隔离级别能避免的并发问题更多但性能会随之下降。4.3 动态数据源与mybatis集成要点Spring Boot MyBatis是目前国内最主流的数据访问组合集成的核心思路是SqlSessionFactory由DataSource、Mapper扫描路径、TypeHandler等配置组装。多数据源场景下每套数据源都要有独立的SqlSessionFactory、DataSourceTransactionManager和SqlSessionTemplate。动态数据源的核心原理是实现Spring的AbstractRoutingDataSource抽象类在运行时通过ThreadLocal记录当前使用的数据源标识在determineCurrentLookupKey()中返回这个标识就能实现读写分离。MyBatis替换了Spring JDBC的JdbcTemplate但在实际使用中需要注意几件事。第一Mapper接口和XML文件的namespace要完全一致否则启动时报错。第二MyBatis-Spring的MapperScannerConfigurer会把所有接口代理化并注册到Spring容器所以Mapper接口不能标Service之类注解否则会有冲突。第三#{}预编译和${}字符串拼接的边界要分清楚#{}能防止SQL注入${}用于动态表名、排序字段等无法预编译的场景但一定要做白名单校验。5. 拆箱即用还是服务治理从Spring Boot到Spring Cloud5.1 自动配置的底层原理Spring Boot火遍全球的核心原因是“约定大于配置”这个口号落地的关键就是自动配置。自动配置的入口是SpringBootApplication它组合了EnableAutoConfiguration、SpringBootConfiguration和ComponentScan。EnableAutoConfiguration通过Import(AutoConfigurationImportSelector.class)导入一个选择器这个选择器会在classpath下加载META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件旧版本是spring.factories里面列出了所有的自动配置类。每个自动配置类通常都有条件注解比如ConditionalOnClass类路径存在某个类才生效、ConditionalOnMissingBean用户没有手动声明该Bean才生效、ConditionalOnProperty配置文件中存在某个属性才生效。面试官喜欢问“Spring Boot是怎么知道你配了哪些组件的”答案就在于这些条件注解。比如你在pom.xml里引入了spring-boot-starter-web类路径下就有了DispatcherServlet于是DispatcherServletAutoConfiguration生效Tomcat嵌入容器启动Spring MVC的整套配置自动装配完成。我踩过的一个深入坑自定义一个starter时自动配置类的加载要放在META-INF/spring/目录下名字必须叫AutoConfiguration.imports。但如果你只写这个文件而不加AutoConfiguration注解Spring Boot 2.7版本以后会提示失效。正确做法是AutoConfiguration public class MyAutoConfiguration { // 配置内容 }5.2 核心组件与分布式架构落地Spring Cloud是一整套分布式系统解决方案的集合核心组件包括注册中心Eureka经典、Consul、Nacos。服务启动时把自己注册到注册中心调用方从注册中心拉取服务列表。Nacos是目前国内最主流的因为它是阿里巴巴体系功能完善且支持配置管理。负载均衡Spring Cloud LoadBalancerSpring Cloud 2020版本后替代Ribbon。它可以在服务提供方有多个实例时从注册中心拿到实例列表通过轮询、随机等策略选中一个实例发起服务调用。服务调用OpenFeign声明式的HTTP客户端写一个接口加注解就能像调用本地方法一样调用远程服务。熔断降级Sentinel或Resilience4j。当被调服务异常率升高时熔断器打开后续请求快速失败不再打到下游服务避免雪崩。网关Spring Cloud Gateway基于WebFlux构建的响应式网关负责路由转发、权限过滤、跨域处理。配置中心Spring Cloud Config或Nacos Config把各个微服务的配置集中管理支持动态刷新。一个典型的微服务调用链路是这样的请求进来先经过API网关网关完成鉴权、限流后根据路由规则转发到对应的微服务微服务之间通过OpenFeign互相调用服务实例信息从注册中心Nacos获取调用过程中的熔断降级由Sentinel兜底每个服务实例中的配置动态从配置中心拉取。整条链路跑下来分布式系统的三大核心诉求——服务发现、配置管理、容错处理——都得到了解决。5.3 分布式事务的选型思路分布式事务是微服务架构里痛点最多的部分。如果一个用户下单操作同时涉及订单服务、库存服务、积分服务这三个服务各持有一个数据库传统本地事务就没法保证一致性。常用的方案有三种2PC两阶段提交有事务协调者先询问所有参与者能否提交都同意后再真正提交。强一致性但协调者单点风险高性能差适用场景有限。TCCTry-Confirm-Cancel每个事务都要实现Try预留资源、Confirm确认执行、Cancel取消回滚三个方法。最终一致性对业务侵入大需要每个服务都要实现三套逻辑。消息事务/本地消息表利用消息队列如RocketMQ的分布式事务消息事务参与方通过MQ异步通知实现最终一致性。目前最常用对业务侵入小、性能较好。用不用分布式事务用什么方案要结合业务场景具体分析。能用最终一致性解决的不要强上强一致性的2PC因为强一致性在分布式系统里的代价极高。我的原则是优先压缩分布式操作范围能合并到同一个服务的尽量合并实在拆不开再用消息队列平滑降级。6. 面试高频追问与进阶观察除了八股还能聊什么6.1 那些绕不开的面试题串烧面试官问Spring很少会只问一个孤立的知识点。他会不断往下钻从一个问题引出另一个问题。我给你串一遍最常见的追问链追问链一Bean的生命周期 → Spring如何解决循环依赖 → 三级缓存为什么是三级 → 代理对象什么时候创建 → JDK代理和CGLIB区别这条链路考察的是你对Bean创建过程的整体理解。能完整讲清楚从“实例化”到“属性填充”到“初始化”到“后置处理”的全过程然后落脚到三级缓存的调度逻辑上基本就是高分答案。很多候选人死记硬背概念一被问“为什么需要三级缓存”就答不上来这是最大的败笔。追问链二什么是AOP → 底层实现原理 → 动态代理和CGLIB区别 → 切入点表达式怎么写 → 事务失效的场景这条链路重点考察实战能力。AOP概念好背但能准确说出CGLIB怎么生成子类、为什么private方法无法被代理、为什么this调用会跳过通知这就要靠真实的排障经验了。追问链三Spring Boot自动配置 → 条件注解有哪些 → 自定义starter怎么编写 → starter在项目里的实际应用这条链路考察工程化落地的能力。如果候选人能讲清楚自己写过starter的实际过程包括AutoConfiguration.imports文件的位置、ConditionalOnMissingBean防止覆盖、ConfigurationProperties加载配置等基本可以判定这个人是真写过代码。追问链四Spring Security的认证授权流程 → FilterChain如何工作 → JWT怎么和Spring Security结合 → OAuth2.0四种授权模式Security是Spring生态里最复杂的一块。核心要理解SecurityFilterChain的执行顺序请求进来先经过一堆FilterAuthenticationFilter负责拉起认证流程AuthorizationFilter负责鉴权最终的入口是ExceptionTranslationFilter把异常转成对应的HTTP状态码。OAuth2.0的四种授权模式最常用的是授权码模式典型的公司内部登录流程用的就是这个模式。6.2 Spring AI新方向的新机会最近的Spring生态里一个绕不开的新方向是Spring AI。这是Spring官方推出的AI应用开发框架目标是把AI能力像SQL、Redis一样集成到Java应用里。Spring AI目前的核心能力包括ChatClient对话服务统一多种大模型通义千问、OpenAI、Ollama本地模型等的调用接口。你写一个PromptTemplate框架负责和模型交互、处理历史会话上下文。Embedding与向量检索把文本转成向量配合向量数据库如Elasticsearch、Redis做语义检索实现RAG检索增强生成。Structured Output结构化输出让大模型按指定JSON Schema输出实体类通过BeanOutputConverter定义类结构解决“模型返回乱格式”的痛点。AI Agent智能体在Spring AI Alibaba中有Graph和Agent框架可以实现复杂的多步骤任务编排。我自己在本地电脑上通过Ollama跑过Qwen模型和Spring AI集成后不到半小时就搭好了一个简单的知识库问答接口。整体感受是Java本身对大模型的SDK生态一直不友好Spring AI把这个短板补齐了。但它目前迭代速度极快API变化也比较多做技术选型时要注意版本兼容尤其是Spring Boot的版本对应关系。6.3 从八股到实战我的一些体会说了这么多知识点最后分享几个实操心得帮你把这些八股真正变成自己的东西。第一研究Spring源码要追“设计动机”不要说背代码。Spring大量使用模板方法模式、策略模式、观察者模式。这些问题想清楚面试时遇上任何变体题目都不会慌。第二写代码时主动制造异常并观察Spring的行为。比如故意写一个private调用Transactional方法观察事务回滚故意构造循环依赖并思考是什么条件导致可以解决、什么条件导致启动失败。这种实验比任何博客都直观。第三面试时聊Spring不能只停留在“知道它好用”更要能说出“它为什么这样设计”。比如“为什么默认用CGLIB代理而不是JDK代理”“为什么三级缓存里的ObjectFactory能解决代理问题”这类追问才是区分“背八股”和“真会”的分水岭。不管你是准备面试的求职者还是想把基础打得更扎实的在职开发Spring这套东西值得反复撸。每次重新看都能从里面发现新的设计细节。这也是它能活二十多年的原因——Spring本质上是在教你如何写出更松耦合、更好扩展的代码。把这个思想吃透了工具早晚会更新但思路不过时。