← 学习门户 每日面试题 学习看板

2026-09-28

Spring 框架深度 —— IoC 容器 / AOP 代理 / 事务管理 到 设计模式落地

10 题 · 平均 3.3/5 · low 3.67 · medium 3.25 · high 3 · 第 1 轮

#1 什么是Spring的循环依赖问题? low java

题目

什么是Spring的循环依赖问题?

我的回答

Spring 的循环依赖指的是,由于 Spring 的 Bean 对象是由 Spring 自身管理的,这时候你要使用一个对象的时候,注入、管理、创建都是由 Spring 操作的。如果你创使用的 A 对象调用 B 对象,生成的时候会调用 B 对象,但是 B 对象生成的时候又调用 A 对象,这时候就产生了循环依赖。 嗯,对应的处理方式主要看你是怎么调用的。Spring 默认的处理的方式,循环依赖的方式是通过三级缓存去对单例对象进行循环依赖的处理。如果你是用的其他模式的对象创建模式,它是无法处理循环依赖的。另外,循环依赖的处理在 Spring 中默认只能处理 set 注入的那种模式的循环依赖。如果你是构造器模式的注入,这时候它是无法处理对应的循环依赖的。 而实质上,我们在开发过程中就不应该出现循环依赖的东西,它属于一种你设计过程中出现了问题,应该将循环依赖的部分摘出去,放到独立的位置单独引用。

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:成因讲对(A 要 B、B 要 A)、三级缓存是默认解法、构造器注入解不了的核心点抓到、架构视角收尾
不足:三级缓存三层结构没展开、没点出为什么需要第三级(AOP 代理)、没提 Spring Boot 2.6+ 默认禁止
补充:L1 成品/L2 半成品(可代理)/L3 ObjectFactory;触发链 A→L3→B→L3→getEarlyBeanReference→L2;规避:@Lazy / 拆 Bean / 事件解耦 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Spring IoC 容器 Bean 创建流程的深度理解,特别是循环依赖场景下的三级缓存机制。

**知识点拆解**:
1. **循环依赖的定义与判定**:两个或多个 Bean 在创建过程中直接或间接地互相引用,导致 Spring 无法在任何一方完全初始化前完成注入。常见形式:A→B→A、A→B→C→A、构造器循环依赖。
2. **三级缓存结构**:
- **singletonObjects(一级)**:已完全初始化、可直接注入的成品 Bean
- **earlySingletonObjects(二级)**:已实例化但未初始化完成、可能已包装成代理的半成品 Bean
- **singletonFactories(三级)**:ObjectFactory,用于按需在二级和三级之间按需生成代理对象
3. **解决流程(以 A↔B、@Autowired setter、需要 AOP 为例)**:
- A 实例化(调用无参构造器)后,先把 A 的 ObjectFactory 放入 L3
- A 进入属性填充阶段,注入 B → 触发 B 的创建
- B 实例化后,把 B 的 ObjectFactory 放入 L3,B 进入属性填充,注入 A
- Spring 发现 A 已在 L3,调用 ObjectFactory.getObject():如需 AOP,返回 A 的代理对象;否则返回 A 的原始对象。结果放入 L2,并从 L3 移除
- B 拿到 A 的代理,B 完成初始化 → 挪到 L1,清 B 的 L2/L3
- 回到 A 的属性填充:从 L2 拿到 A 的代理(若 AOP 未处理,此处可能是原始对象,会引发问题——Spring 通过提前把代理放 L2 解决),A 完成初始化 → 挪到 L1,清 A 的 L2/L3
4. **为什么需要第三级(ObjectFactory)**:核心是 AOP 场景——若只有两级,Spring 不知道 Bean 是否需要被代理;L3 用工厂延迟决定"暴露代理还是原始对象",避免提前创建代理对象
5. **Spring 的限制**:
- 仅支持 **singleton 作用域**,prototype / request 作用域下无法解决
- 仅支持 **setter / field 注入**,构造器循环依赖 Spring 会直接抛 BeanCurrentlyInCreationException
- Spring Boot 2.6+ 默认 `spring.main.allow-circular-references=false`,启动即失败
6. **最佳实践**:
- 循环依赖本身就是设计异味,应通过拆方法、拆 Bean、事件解耦、单向依赖等手段规避
- 无法避免时:`@Lazy` 注入代理对象、显式配置允许循环依赖、重构依赖方向

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:三级缓存的结构与 AOP 代理的耦合关系(为什么必须三级)
- **广度**:Bean 生命周期全流程、Spring AOP 代理机制、@Lazy 懒加载
- **应用**:Spring Boot 2.6+ 启动失败排查、生产环境循环依赖治理

**延伸递进面试题**:
1. **为什么三级缓存必须存在第三级?如果只用两级会发生什么?**
- 解答要点:两级情况下无法区分"是否需要代理";如果提前创建代理,可能生成错误的代理链(原始对象 vs 代理对象混用),且无法复用 ObjectFactory 的懒生成语义。AOP 场景下会出现"B 拿到 A 的原始对象,而 A 自身持有 B 后又变成代理"的引用不一致问题
2. **构造器循环依赖 Spring 一定解不了吗?有没有绕过方式?**
- 解答要点:构造器循环依赖 Spring 默认抛异常,因为构造器执行时对象还没实例化完成,无法提前暴露引用;可通过 `@Lazy` 让注入端拿代理对象绕过(本质是不真实调用 A 的构造器),或改用 setter 注入、或重构为事件/回调
3. **在 Spring Boot 3 中,遇到循环依赖应该先改什么?**
- 解答要点:首选重构——分析两个 Bean 的职责边界,抽出第三个 Bean 承接公共逻辑;次选 @Lazy;最后才是 `spring.main.allow-circular-references=true`(不推荐,掩盖设计问题)

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring 的循环依赖指的是两个或多个 Bean 在创建过程中互相需要对方——A 要注入 B,B 又要注入 A,Spring 在创建 A 时调用 B 的构造函数去创建 B,B 又反向需要 A,此时 A 还没初始化完成,就会卡死或者抛出异常。

Spring 官方是默认支持处理循环依赖的,核心方案是**三级缓存**:一级 `singletonObjects` 存放完全初始化好的成品 Bean,二级 `earlySingletonObjects` 存放已实例化但还没走完初始化的半成品,三级 `singletonFactories` 存放 ObjectFactory,用来按需生成半成品——这里的关键是 AOP 场景。当 A 实例化后,Spring 先把 A 的工厂放到三级缓存,然后开始填充属性时触发 B 的创建;B 在填充属性需要 A 时,从三级拿到 A 的 ObjectFactory,判断是否需要 AOP 代理,如果需要的话就提前生成一个代理对象,放到二级;B 拿到这个代理完成初始化后,回到 A 的填充流程,A 也是从二级拿到自己的代理对象继续走初始化流程,最后 A、B 都会进入一级缓存。

**为什么必须三级而不是两级?** 因为 AOP 代理的生成是延迟决策的——如果只用两级,Spring 就无法区分"要不要提前暴露代理",会导致 B 持有 A 的原始对象而 A 持有 B 的代理对象,两边引用不一致。

**Spring 的限制**:只支持 singleton 作用域、只支持 setter 或 field 注入,构造器注入的循环依赖 Spring 会直接抛出 BeanCurrentlyInCreationException,因为它在构造器还没执行完时就无法拿到对象引用去暴露。另外 Spring Boot 2.6 之后默认 `allow-circular-references=false`,遇到循环依赖会启动直接失败,这是官方在推工程实践的收紧。

实际开发中我的做法是**尽量不出现循环依赖**——循环依赖本身就说明两个 Bean 的职责边界没切好,我一般通过拆 Bean、抽第三方 Bean 承接公共逻辑、或者用事件解耦(ApplicationEvent)来打破循环。实在绕不过去的话用 @Lazy 让注入端先拿代理对象,本质是把真正的构造调用延后到使用阶段。

#2 Spring中@Service、@Component、@Repository等注解区别是什么? low java

题目

Spring中@Service、@Component、@Repository等注解区别是什么?

我的回答

本质上这些注解没有什么区别,它的核心注解都是一样的,都是用来进行对象注入。它的最大的区别其实是不同的业务属性标注,用 Service 标注业务层,Comment 标注一般的对象,repository 标注 dao 层对象,本质上没有区别,可以都用 component,只是看上去和进行一些切面操作时会有取结果的不同

点评

评分:⭐⭐⭐☆☆(3/5)
优点:抓到底层元注解都是 @Component、分层语义定位对
不足:@Repository 的核心差异(异常转换 PersistenceExceptionTranslationPostProcessor)没点明;@Controller/@RestController 忘了提;表述模糊
补充:@Repository 自动启用异常转换(SQLException→DataAccessException);@RestController = @Controller + @ResponseBody ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Spring 注解体系的理解深度——是否清楚"元注解"概念,是否能区分语义标注与运行时差异。

**知识点拆解**:
1. **元注解机制**:`@Service`、`@Repository`、`@Controller` 等注解的 `@Target` 上被 `@Component` 元标注,Spring 在 `ClassPathBeanDefinitionScanner` 扫描时通过 `isCandidateComponent` 递归判断类上是否含有 `@Component` 元注解,从而把这些类都注册为 bean
2. **@Component**:最基础的顶层注解,表示这个类交给 Spring 管理,无特定语义
3. **@Service**:语义上表示服务层,本质和 @Component 完全等价,Spring 不会对 @Service 做特殊处理——只是方便阅读和分层
4. **@Repository**:
- **唯一的运行时差异**:它触发 `PersistenceExceptionTranslationPostProcessor`(可通过 `@EnableTransactionManagement` 或 Spring Boot 自动配置启用)
- 该后置处理器会给所有 @Repository bean 添加异常转换拦截器,把底层持久层抛出的原生异常(`SQLException`、Hibernate 的 `HibernateException`、JPA 的 `PersistenceException`)转换成 Spring 抽象的 `DataAccessException` 体系
- 好处:上层业务代码不需要关心底层是 JDBC、JPA 还是 MyBatis,统一处理 DataAccessException 即可
5. **@Controller / @RestController**:
- `@Controller`:交给 Spring MVC DispatcherServlet 处理请求,方法返回值默认作为 view name 交给视图解析器
- `@RestController` = `@Controller` + `@ResponseBody`,方法返回值直接序列化写入 HTTP 响应体(JSON/XML)
6. **value 属性**:所有组件注解都支持 `value` 属性指定 bean 名,未指定则用简单类名(首字母小写)作为默认 bean 名
7. **@Transactional 落点**:虽然 `@Service` 和 `@Repository` 都能加 `@Transactional`,但规范做法是在 `@Service` 上加,因为事务边界应该划在业务层,而不是 DAO 层

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:元注解机制、@Repository 的异常转换原理
- **广度**:Spring MVC 的分派机制、Spring Data 的异常抽象
- **应用**:实际项目中分层注解选型、事务边界设计

**延伸递进面试题**:
1. **@Repository 的异常转换具体是怎么实现的?如果不用 @Repository 而用 @Component 标注 MyBatis 的 Mapper,会有什么后果?**
- 解答要点:Spring 通过 `PersistenceExceptionTranslationPostProcessor` 这个 BeanPostProcessor 拦截 bean 初始化后的对象,如果 bean 类上有 @Repository 且存在 `@Transactional` 或类似事务/数据访问切面,就为其生成代理,代理方法执行完捕获原生异常,通过 `PersistenceExceptionTranslator` 转换成 `DataAccessException`。如果不用 @Repository,异常会以原生形式(如 `SQLException`、MyBatis 的 `PersistenceException`)抛到业务层,业务代码难以统一处理
2. **为什么 Spring 官方推荐在 @Service 上放 @Transactional 而不是 @Repository?**
- 解答要点:事务边界应该和业务操作对齐(一个 Service 方法通常对应一个业务事务),DAO 层被多个 Service 共享,若在 DAO 上加事务会导致事务粒度过细、无法跨 DAO 保持一致性;而且 Spring 通过 AOP 代理实现事务,Service 层已经持有代理,跨 DAO 调用也能共享同一事务上下文
3. **@RestController 和 @Controller 内部有什么区别?如果我要返回 JSON 但用的是 @Controller,有哪些方式?**
- 解答要点:@RestController = @Controller + @ResponseBody,等价于在类上统一标注 @ResponseBody;用 @Controller 时要返回 JSON,可以在方法上单独加 @ResponseBody、返回 ResponseEntity、或返回值加 @JsonRawValue 等注解

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,这几个注解在 Spring 中本质上都属于 `@Component` 家族,也就是"组件注解"。Spring 在做组件扫描的时候,只要类上含有 `@Component` 元注解——不管直接标注还是通过 `@Service`、`@Repository`、`@Controller` 间接标注——都会被注册为 Spring 容器管理的 bean。所以从这个意义上讲,它们的底层机制是一致的。

但它们有几个关键差异:

**第一,@Component 是最基础的顶层注解**,只是"把这个类交给 Spring 管理",没有任何额外语义。

**第二,@Service 本质上和 @Component 等价**,纯粹是语义标注,用来标记"这是业务层",方便代码阅读和分层。Spring 运行时不会对 @Service 做特殊处理。

**第三,@Repository 有唯一的运行时差异**——它会触发 Spring 的 `PersistenceExceptionTranslationPostProcessor`,这个后置处理器会把所有 @Repository bean 包装一层代理,自动把底层持久层的原生异常(比如 `SQLException`、Hibernate 的 `HibernateException`、MyBatis 的 `PersistenceException`)转换成 Spring 统一抽象的 `DataAccessException`。这样上层业务代码只需要关心 DataAccessException 这一族异常,不用管底层用的是 JDBC、JPA 还是 MyBatis。这是 @Repository 存在的最大意义。

**第四,@Controller 和 @RestController 属于 Spring MVC 范畴**。@Controller 交给 DispatcherServlet 分派请求,方法返回值默认作为 view name 交给视图解析器渲染;@RestController 相当于 @Controller 加了 @ResponseBody,方法返回值直接序列化写入 HTTP 响应体,通常返回 JSON。

实际使用中,我会按分层来选:DAO 层用 @Repository 拿到异常转换能力,业务层用 @Service 且在这里加 @Transactional(事务边界应该在业务层),控制器层 REST 接口用 @RestController。所有注解都支持 value 属性自定义 bean 名,不指定就用类名首字母小写作为默认名。

#3 SpringBoot和Spring的区别是什么? low java

题目

SpringBoot和Spring的区别是什么?

我的回答

Spring Boot 是 Spring 的一个封装,它把大量的 Spring 的衍生功能、常用的功能都封装起来,让你可以即时的启用,直接即拿即用。通过简单的配置就可以启用对应的功能,快速的启动服务,简单的开发,提高研发的效率。并且它把项目的架构基本都设计好了,在它内部有比较规范的设定,按它的规范填充业务,可以很快速的完成业务开发。它的核心在于约定大于配置,很多东西它是规约好了,你不需要配置,你想改你就可以改,但是不改的情况下,它会有一个自己行,有行业内的默认配置去帮助你使用。另外就是他把大量的功能封装成了一个个 star,方便大家使用,直接引入就可以进行正常的使用。

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:核心定位对(Spring 封装/快速启动)、约定大于配置抓精髓、Starter 依赖抓到
不足:核心机制(@SpringBootApplication 拆解、AutoConfiguration + 条件注解)没展开、内嵌容器、Actuator、配置外部化都忘了
补充:@SpringBootApplication = @EnableAutoConfiguration + @ComponentScan + @Configuration;spring.factories(2.x)/AutoConfiguration.imports(3.x);内嵌 Tomcat/Jetty/Undertow ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Spring Boot 设计哲学的理解,以及是否清楚其内部自动配置的机制。

**知识点拆解**:
1. **本质定位**:Spring Boot 不是新框架,而是基于 Spring 的快速启动/开箱即用工具,核心依然是 Spring 的 IoC、AOP、MVC 等
2. **@SpringBootApplication 拆解**:
- `@SpringBootConfiguration`:标识这是一个 Spring 配置类(本质是 @Configuration)
- `@EnableAutoConfiguration`:启用自动配置,触发 AutoConfigurationImportSelector 扫描 starter 中的自动配置类
- `@ComponentScan`:扫描启动类所在包及其子包下的组件
3. **自动配置机制**:
- Spring Boot 2.x:读取 `META-INF/spring.factories` 中的 `EnableAutoConfiguration` 键,加载列出的自动配置类
- Spring Boot 3.x:改用 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 文件
- 每个自动配置类通过 `@Conditional` 系列注解(`@ConditionalOnClass`、`@ConditionalOnMissingBean`、`@ConditionalOnProperty`)决定是否生效——这就是"约定大于配置"的底层实现
4. **Starter 机制**:
- Spring Boot 2.x 时代:starter = 一组依赖的聚合 pom(如 spring-boot-starter-web 聚合了 spring-webmvc、内嵌 tomcat、jackson 等)
- Spring Boot 3.x 时代:引入了 BOM 依赖管理 + `@AutoConfiguration` 注解,配合 `AutoConfiguration.imports` 做延迟加载
5. **内嵌容器**:Tomcat、Jetty、Undertow 被打包进应用 JAR/WAR,`java -jar xxx.jar` 直接启动,无需部署到外部 Servlet 容器;这是 Spring Boot 生产部署最大便利
6. **配置外部化**:`application.yml` → `application-{profile}.yml` → 命令行参数 → 环境变量 → 配置中心(Nacos/Apollo),按优先级覆盖
7. **Actuator**:提供 `/actuator/health`、`/actuator/info`、`/actuator/metrics`、`/actuator/env` 等生产级端点,支持健康检查、指标暴露、优雅停机
8. **生态延伸**:Spring Cloud(微服务治理)建立在 Spring Boot 之上,Spring Security / Spring Data / Spring Task 等也是 Spring Boot 常见集成

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:自动配置的实现原理、条件注解
- **广度**:Spring Cloud、Actuator、配置外部化、Starter 自定义
- **应用**:Spring Boot 3 与 2 的差异、自定义 starter、生产环境监控接入

**延伸递进面试题**:
1. **自动配置是怎么"生效又不过度装配"的?请以 @ConditionalOnMissingBean 为例说明**
- 解答要点:Spring Boot 自动配置类通常先 @ConditionalOnClass 判断相关类是否在项目 classpath 中,若用户在配置类中显式 @Bean 声明了同类,@ConditionalOnMissingBean 会让自动配置的默认实现失效——这就是"用户配置优先"的机制,也是"约定大于配置但可覆盖"的关键
2. **Spring Boot 3 相比 2 有哪些核心变化?为什么移除了 spring.factories?**
- 解答要点:3.x 移除了 spring.factories(改用 AutoConfiguration.imports)是为了更好的性能(延迟加载);引入 GraalVM 原生镜像支持、Jakarta EE 9(javax.* → jakarta.* 包名迁移)、虚拟线程支持(@EnableVirtualThreads)、Observability 模块、Micrometer Tracing;最低 JDK 版本要求 17
3. **如何在自己的项目里做一个自定义 Starter?**
- 解答要点:拆两个模块——xxx-spring-boot-autoconfigure(写自动配置类 + 属性类 + @AutoConfiguration)+ xxx-spring-boot-starter(聚合 pom,引入 autoconfigure 和其他依赖);在 autoconfigure 模块的 resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 中注册自动配置类的全限定名;通过 @ConfigurationProperties 提供外部化配置;提供 @ConditionalOnMissingBean 让用户能自定义实现

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring Boot 本身不是一个独立框架,它是基于 Spring 做的一整套"快速启动 + 开箱即用"的工具链。它的核心价值是把 Spring 传统开发中繁琐的 XML/JavaConfig 配置、依赖版本协调、容器部署等负担都收敛掉了。

从**核心机制**上讲,Spring Boot 提供了 @SpringBootApplication 这个顶层注解,它内部相当于三个组合:@SpringBootConfiguration(本质是 @Configuration)+ @EnableAutoConfiguration(启用自动配置)+ @ComponentScan(扫描启动类所在包)。自动配置是它最精髓的部分——启动时通过 AutoConfigurationImportSelector 扫描所有 starter jar 里的自动配置类,然后用 @ConditionalOnClass、@ConditionalOnMissingBean、@ConditionalOnProperty 这些条件注解决定是否生效。也就是说,Spring Boot 会"猜你想用":classpath 里有 spring-webmvc 它就自动装配 DispatcherServlet;你自己在配置类里显式 @Bean 声明了 DataSource,它就用你的、不用默认的。这就是"约定大于配置但可覆盖"的底层实现。

从**工程实践**上讲,Spring Boot 有几个杀手级能力:
第一,**Starter 依赖**,比如 spring-boot-starter-web 一个依赖就打包了 webmvc、内嵌 tomcat、jackson、spring-web 等一整套,配合 BOM 管理版本,彻底解决了 Maven 依赖版本冲突的历史痛点。
第二,**内嵌容器**,Tomcat/Jetty/Undertow 被打包进 JAR,直接 java -jar 就能跑,不需要部署到外部 Tomcat,这是生产部署最大的便利。
第三,**配置外部化**,application.yml 支持 profile 分环境、命令行参数、环境变量、配置中心(Nacos/Apollo)层层覆盖。
第四,**Actuator 监控**,内置 /actuator/health、/metrics、/info 等生产端点,配合 Spring Boot Admin 就能搭建一套完整的服务监控体系。

到 Spring Boot 3 又有一些新变化:包名从 javax.* 迁移到 jakarta.*、支持 GraalVM 原生镜像、支持虚拟线程(@EnableVirtualThreads)、spring.factories 被 AutoConfiguration.imports 取代以支持延迟加载。总之,Spring Boot 不是替代 Spring,而是把 Spring 的搭建过程标准化了,让我们能专注于业务而不是配置。

#4 Spring Bean的生命周期是怎么样的? medium java

题目

Spring Bean的生命周期是怎么样的?

我的回答

Spring Bean 的生命周期,初始化、实例化、常驻使用中、在缓存中复用、最终销毁回收,基本上就是它整个生命周期了。按照佛教的说法就是成住坏空; 这也是常规对象的所有生命周期,而 Spring 中不同的就在于这个整个生命周期都在 Spring 的自己的管理之下。你的对象创建、对象的实例化都是由 Spring 进行的,包括它的缓存、它的销毁,都是 Spring 来进行操作的。 Spring 先去根据注解找到要注入的对象,比如,@Component、@Service,这些被标注的对象,Spring 会将其实例化,并把对象缓存到对应的缓存中。实例完后会缓存到一级缓存,创建过程中可能会在二级或者三级缓存中,二级缓存中存留一会,三级缓存中存的是工厂。完成了实例化之后,开始通过缓存去进行使用调用。要使用的时候就通过 对应的注释读取对应的 bean,进行使用,对象销毁时,如果要进行一些处理,可以在对象的销毁方法中进行自己想要实现的业务逻辑,在销毁时会进行调用执行。

点评

评分:⭐⭐⭐☆☆(3/5)
优点:宏观阶段抓住(实例化→初始化→使用→销毁)、提到三级缓存
不足:**完全没展开具体回调方法**——三类核心钩子(BeanPostProcessor / Aware / InitializingBean)都缺;实例化步骤(BeanDefinition→实例化→属性填充→初始化)没提
补充:核心骨架 = 实例化→放L3→属性填充→Aware→beforeInit→@PostConstruct→afterPropertiesSet→initMethod→afterInit(AOP代理)→放L1→使用→@PreDestroy→destroy→destroyMethod ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Spring IoC 容器 Bean 完整创建流程的深度理解,特别是三类核心回调接口(BeanPostProcessor / Aware / InitializingBean)的介入时机。

**知识点拆解**:

1. **五个阶段总览**:
- 加载配置(BeanDefinition 元数据解析)
- 实例化(调用构造器 createBeanInstance)
- 属性填充(populateBean,处理依赖注入)
- 初始化(Aware 回调 + BeanPostProcessor 前后钩子 + 初始化方法)
- 使用与销毁

2. **三个核心抽象接口**:
- **BeanDefinitionRegistryPostProcessor**:修改 BeanDefinition(在实例化前),如 @EnableAspectJAutoProxy 注册切面
- **BeanPostProcessor**:修改 Bean 实例(在初始化前后),Spring 里 90% 高级功能都靠它:AOP、@Autowired、@Resource、@PostConstruct、@EventListener、@Value
- **Aware 接口族**:让 Bean 感知容器上下文(BeanNameAware、BeanFactoryAware、ApplicationContextAware 等)

3. **三级缓存在生命周期中的位置**:
- **实例化后立即放 L3**(addSingletonFactory),让循环依赖能提前拿到代理对象
- 属性填充阶段可能从 L2/L3 获取代理对象
- 完全初始化后放 L1,清理 L2/L3

4. **初始化三兄弟严格顺序**(必背):
- @PostConstruct(CommonAnnotationBeanPostProcessor 处理)
- InitializingBean.afterPropertiesSet()
- 自定义 init-method(@Bean(initMethod="x") 或 XML init-method="x")

5. **销毁三兄弟顺序**(与初始化反向):
- @PreDestroy
- DisposableBean.destroy()
- 自定义 destroy-method

6. **不同作用域的差异**:
- singleton:容器启动时创建一次,容器关闭时销毁一次
- prototype:每次 getBean 都新建,Spring 不管理其销毁
- request/session:随 Web 请求/会话生灭

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:BeanPostProcessor 机制、三级缓存介入时机
- **广度**:AOP 代理生成时机、@PostConstruct 与 init-method 的关系
- **应用**:自定义 BeanPostProcessor、生产环境初始化资源加载

**延伸递进面试题**:
1. **@Autowired 什么时候被注入?本质是什么?**
- 解答要点:属性填充阶段(populateBean)由 AutowiredAnnotationBeanPostProcessor 处理,本质是 BeanPostProcessor;@Resource 也是同类机制
2. **AOP 代理对象什么时候生成?如果发生循环依赖呢?**
- 解答要点:正常流程在 postProcessAfterInitialization(AbstractAutoProxyCreator);循环依赖场景下代理会在更早的 L3 ObjectFactory 调用时提前生成,保证 B 拿到 A 的代理而非原始对象
3. **@PostConstruct 和 init-method 谁先执行?为什么这样设计?**
- 解答要点:@PostConstruct 先。三者顺序:@PostConstruct → afterPropertiesSet → init-method。设计初衷:注解优先于接口(更解耦、无需实现接口)、接口优先于 XML 配置(更类型安全)

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring Bean 的完整生命周期我一般会分**五个阶段**来讲。

**第一阶段是加载配置**:Spring 通过 XML、@ComponentScan 或 Java Config 扫描出类,解析成 BeanDefinition 元数据。这个阶段 BeanFactoryPostProcessor 会介入,比如处理 ${xxx} 占位符替换。

**第二阶段是实例化**:调用构造器生成 bean 实例,此时对象刚创建但还没初始化属性。关键点:**实例化后 Spring 会立刻把这个 bean 的 ObjectFactory 放进三级缓存**,为后面的循环依赖做准备。

**第三阶段是属性填充**:处理 @Autowired、@Resource、setter 注入,本质都是 AutowiredAnnotationBeanPostProcessor 这个 BeanPostProcessor 在工作。

**第四阶段是初始化**,这是最复杂的:
- 先走 Aware 接口(BeanNameAware、ApplicationContextAware 等),让 bean 感知容器
- 然后 BeanPostProcessor.postProcessBeforeInitialization 被调用,CommonAnnotationBeanPostProcessor 在这里处理 @PostConstruct
- 接着按严格顺序执行三兄弟:**@PostConstruct → InitializingBean.afterPropertiesSet() → 自定义 init-method**
- 最后 BeanPostProcessor.postProcessAfterInitialization 被调用,**AOP 代理就在这个环节由 AbstractAutoProxyCreator 创建**

**第五阶段是缓存使用**:完全初始化后 bean 被放入一级缓存 singletonObjects,singleton 作用域的 bean 会被容器复用直到关闭。

**销毁阶段**反过来走三步:@PreDestroy → DisposableBean.destroy() → 自定义 destroy-method。

**几个关键点补充**:
- 90% 的 Spring 高级功能(AOP、@Autowired、@PostConstruct、@EventListener)都是通过 BeanPostProcessor 机制实现的
- 三级缓存介入的生命周期位置在属性填充之前,目的是让 AOP 场景下的循环依赖能提前暴露代理对象
- prototype 作用域的 bean 每次 getBean 都新建,且 Spring 不管其销毁,循环依赖也完全无法解决

实际开发中我最常用的自定义时机是 InitializingBean.afterPropertiesSet,比如初始化一些静态资源、注册事件监听器;销毁时用 @PreDestroy 关闭资源。

#5 介绍一下Spring的AOP medium java

题目

介绍一下Spring的AOP

我的回答

Spring AOP,AOP 就是基于代理的切面编程增强。它的核心逻辑就是在创建对象的时候,创建的是一个代理对象。在你的对象的前后,对应标注的方法,增加了对应的代理方法实现。在调用你的方法前,先调用代理内的前置方法、后置方法或者环绕方法、前后环绕方法,最终实现对你原有的方法的增强。我们常规用来使用的 AOP 会很多,比如健全,大量方法的健全使用一个注解,然后在健全,在进入方法前提前进行校验健全就可以了。另外就是日志,调用的使用环绕的 AOP 切面,在调用前调用后记录对应的日志。这是 AOP 的常规使用方式,另外还有一些打点可能会用到 AOP。

点评

评分:⭐⭐⭐☆☆(3/5)
优点:AOP 本质抓对(基于代理的增强)、通知类型提到、实战场景到位
不足:**AOP 七大核心概念完全没讲**(JoinPoint/Pointcut/Advice/Aspect/Target/Introduction/Weaving);Spring 代理选型(JDK vs CGLIB)没讲;切点表达式没提;**AOP 五大失效场景没提**(尤其是类内部自调用);织入方式(Spring AOP 是运行时织入 vs AspectJ 编译期织入)没提
补充:Spring AOP 默认有接口用 JDK、无接口用 CGLIB;Spring Boot 2.x 默认 spring.aop.proxy-target-class=true 强制 CGLIB;切点表达式 execution/@within/@annotation/@args ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 AOP 设计思想、Spring AOP 实现机制以及工程实践中失效场景的深度理解。

**知识点拆解**:

1. **AOP 思想本源**:
- 关注点分离(Separation of Concerns):把横切逻辑(日志、事务、权限、监控)从业务代码中剥离
- 横切关注点(Cross-cutting Concern):分散在多个方法中但功能类似的逻辑
- 相比继承(编译期强耦合)和直接编码(散乱),AOP 是运行时的动态代理增强

2. **七大核心概念**:
- **JoinPoint(连接点)**:程序执行中能插入增强的点,Spring AOP 只支持**方法执行**
- **Pointcut(切点)**:定义哪些连接点需要被增强,通过表达式匹配
- **Advice(通知)**:在连接点做什么,五种:Before / After / AfterReturning / AfterThrowing / Around
- **Aspect(切面)**:切点 + 通知 = 切面,通常是一个 @Component 类
- **Target(目标对象)**:被代理的原始对象
- **Introduction(引入)**:让目标类在运行时"实现"一个原本没实现的接口
- **Weaving(织入)**:把切面织入到连接点,编译期 / 类加载期 / 运行时

3. **Spring AOP 实现机制**:
- Spring AOP = **基于动态代理的运行时织入**
- 底层是 BeanPostProcessor(AbstractAutoProxyCreator)在 Bean 初始化后(postProcessAfterInitialization)判断是否需要代理,需要就包装成代理对象
- **代理选型**:
- JDK 动态代理:基于接口,目标类必须有接口
- CGLIB:基于继承,生成目标类的子类覆写方法
- Spring 5+ 默认:**目标类有接口用 JDK,无接口用 CGLIB**
- Spring Boot 2.x 默认:`spring.aop.proxy-target-class=true` 强制 CGLIB(避免自调用时的 this 类型不一致)

4. **切点表达式(Pointcut Expression)**:
- `execution(modifiers? ret-type? name(param) throws?)`:最常用,匹配方法执行
- 例:`execution(* com.x.service.*.*(..))` 匹配所有 service 包下的方法
- `@within`:匹配被特定注解标注的类中的所有方法
- `@annotation`:匹配被特定注解标注的方法
- `@args`:匹配参数带有特定注解的方法
- `this()` / `target()`:匹配代理对象 / 目标对象的类型
- 支持 `&&` `||` `!` 组合

5. **织入方式对比**:
- **编译期织入(AspectJ CTW)**:编译时修改字节码,性能最好但依赖 AspectJ 编译器
- **类加载期织入(AspectJ LTW)**:JVM 启动时通过 agent 拦截,性能次之
- **运行时织入(Spring AOP)**:动态代理,性能稍差但零侵入
- Spring AOP **不是**真正的字节码织入,只做方法级切点,无法切字段访问、构造器、私有方法

6. **AOP 五大失效场景**(工程必看):
- **类内部自调用**:this.aMethod() 直接调原方法,绕过代理 → 事务失效、日志不生效
- **方法 private / final / static**:代理无法覆写 final/static,CGLIB 不能拦截 private
- **非 public 方法**:JDK 动态代理只支持 public 接口方法
- **目标类未注册为 Spring Bean**:无代理对象
- **切面 bean 没被 Spring 扫描**:切点无匹配对象

7. **自调用失效的四种规避方案**:
- 从其他 Bean 中调用(最干净)
- `AopContext.currentProxy()` 获取当前代理对象(需 @EnableAspectJAutoProxy(exposeProxy=true))
- `@Autowired @Lazy private MyService self;` 自注入,然后 self.xxx()
- 拆成两个 Bean(架构最佳)

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Spring AOP 与 AspectJ 的差异、代理选型策略
- **广度**:切点表达式语法、失效场景的工程排查
- **应用**:日志切面、事务切面、限流切面、脱敏切面

**延伸递进面试题**:
1. **Spring AOP 和 AspectJ 有什么本质区别?为什么 Spring 不用 AspectJ 做织入?**
- 解答要点:Spring AOP 是运行时动态代理,只支持方法级切点,性能略差但零依赖;AspectJ 是编译期/类加载期真正的字节码织入,能切字段、构造器、静态初始化,功能完整但复杂度高。Spring 选择了"够用即可"的工程折中
2. **Spring Boot 2 为什么默认强制 CGLIB?有什么坑?**
- 解答要点:解决 JDK 代理下 this 指向代理对象而非目标类导致的类型不一致问题;坑点是 final 类 / final 方法无法代理(CGLIB 不能继承),会启动报错;解决办法是避免 final,或用 @SpringBootApplication 排除强制 CGLIB
3. **如何写一个"脱敏"切面,在方法返回前把敏感字段打码?**
- 解答要点:用 @Around 环绕通知,调用 proceed() 拿到返回值,反射遍历字段或基于注解(@Sensitive)匹配敏感字段,用 StringUtils.mask 处理后重新 set 回对象;注意返回类型可能是基本类型 / 集合 / null 的判断

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring AOP 本质上是**基于动态代理的切面编程增强**。它的核心思想是**关注点分离**——把日志、事务、权限、监控这些横切关注点从业务代码中剥离出来,通过代理对象在方法执行前后插入增强逻辑。

**核心概念**有七个:**JoinPoint(连接点)**是程序执行中能插入增强的点,Spring AOP 只支持方法执行;**Pointcut(切点)**定义哪些连接点需要被增强,通常用 @Pointcut 注解配表达式;**Advice(通知)**是在连接点做什么,有五种:Before、After、AfterReturning、AfterThrowing、Around,其中 Around 最灵活,能拿到 ProceedingJoinPoint 控制是否执行原方法;**Aspect(切面)**是切点 + 通知的组合,通常是一个 @Component 类;**Target**是被代理的原始对象;**Introduction**让目标类运行时实现新接口;**Weaving(织入)**是把切面织入连接点。

**Spring AOP 的实现机制**:底层通过 BeanPostProcessor(AbstractAutoProxyCreator)在 Bean 初始化后判断是否需要代理,需要就包装成代理对象。代理选型有两种:**JDK 动态代理**基于接口,目标类必须有接口;**CGLIB**基于继承生成子类覆写方法。Spring 5+ 默认策略是**目标类有接口用 JDK、无接口用 CGLIB**;Spring Boot 2.x 更进一步,通过 spring.aop.proxy-target-class=true 强制使用 CGLIB,主要是为了解决 JDK 代理下 this 指向代理对象而非目标类导致的类型不一致问题。

**切点表达式**主要有几种:`execution(* com.x.service.*.*(..))` 匹配所有 service 包下的方法(最常用)、`@within` 匹配被特定注解标注的类、`@annotation` 匹配被特定注解标注的方法、`@args` 匹配参数带注解的方法,可以用 && / || / ! 组合。

**工程中最容易踩的坑是切面失效**,主要是五种场景:
1. **类内部自调用**:this.aMethod() 直接调原方法绕过代理,导致事务、日志、脱敏切面全部失效。规避方案是从其他 Bean 调用、用 AopContext.currentProxy()、@Lazy 自注入 self 字段、或拆成两个 Bean
2. **方法 private / final / static**:代理无法覆写 final 和 static,CGLIB 不能拦截 private
3. **方法非 public**:JDK 动态代理只支持 public 接口方法
4. **目标类不是 Spring Bean**:无代理对象
5. **切面 bean 没被扫描**:切点无匹配

**实际场景**中我用 AOP 主要做三类事:**日志切面**用 @Around 记录方法入参、耗时、返回值;**权限校验切面**用 @Before 校验方法上的自定义 @RequirePermission 注解;**脱敏切面**用 @Around 遍历返回值对象,把标了 @Sensitive 的字段打码。

需要澄清的一点:**Spring AOP 不是真正的字节码织入**,它只支持方法级切点,无法切字段访问、构造器、私有方法。如果需要这些能力,得用 AspectJ 的编译期或类加载期织入。

#6 Spring中@Transactional事务的实现原理 medium java

题目

Spring中@Transactional事务的实现原理

我的回答

Spring 中事务注解的实现原理,本质上就是在事务方法上增加注解,增加注解的目的就是把事务的切面增加进去,在调用方法前先开启事务,调用方法后进行 commit,最终实现整个事务的实现。 从这可以看出,它的事务的实现原理是基于两个层面。首先数据库要支持事务,它支持开启事务跟提交事务。另外,当前的对象需要是 AOP 的代理对象,才能使用到 Spring 中的这个事务注解,否则事务注解是没有意义的。 比如你在一个方法中,一个类的方法中调用类中的另一个方法。其中 A 方法是调用方,B 方法是被调用方。如果 B 方法有事务注解,A 方法没有,你方在类中直接调用,你的事务注解是无效的,并不能产生作用,因为并没有经历过代理,没有相关的提交跟启动注解,启动事务。 另外事务注解有它的一些自定义的配置,包括在什么状态下失效,哪些异常状态下捕获才能失效。它默认的是 runtime exception 吧。正常是需要你捕获一下,确定你要哪些状态下才失效你的事务,进行回滚,back exception。另外你需要制定你的事务的作用方式,是每次启动一个新事务,还是有事务时使用事务,没事务时候就不使用,只是接受外部的,还是说你跟外部使用同一个事务,这需要你自己指定。

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:核心抓对(AOP 切面)、两大前提识别(数据库支持 + 代理对象)、**主动点出自调用失效**、异常控制抓住关键(默认只对 RuntimeException 回滚)、传播机制提到
不足:异常规则说反(不是"你捕获",而是"注解声明哪些要回滚");传播机制三个描述重复混乱;底层实现类没提(AbstractPlatformTransactionManager / DataSourceTransactionManager / TransactionInterceptor)
补充:三层结构 = @Transactional 注解 + TransactionInterceptor + PlatformTransactionManager;7 种传播机制;rollbackFor/noRollbackFor;异常被 catch 吞掉事务不回滚 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Spring 事务的实现机制、传播机制、失效场景的深度理解。

**知识点拆解**:

1. **实现原理(三层结构)**:
- **@Transactional 注解**:声明式配置,本质是标记"这个 bean 的方法需要事务代理"
- **AOP 拦截器(TransactionInterceptor)**:真正的事务切面,在方法调用前后开启/提交/回滚事务
- **PlatformTransactionManager**:抽象事务管理器接口,具体实现如 DataSourceTransactionManager(管理 JDBC Connection 的事务)

2. **完整调用链**:
- `@EnableTransactionManagement` 启动时注册 `BeanFactoryTransactionAttributeSourceAdvisor`
- 该 Advisor 继承 `AbstractAdvisingBeanPostProcessor`,在 `postProcessAfterInitialization` 阶段判断 bean 是否需要事务代理
- 若需要(即有方法/类上标注 @Transactional),通过 AOP 创建代理对象
- 调用 `@Transactional` 方法时,代理对象先执行 `TransactionInterceptor.invoke()`
- 拦截器通过 `TransactionAttributeSource` 解析注解(获取传播机制、隔离级别、超时、只读、rollbackFor、noRollbackFor 等属性)
- 委托 `PlatformTransactionManager.getTransaction()` 开启事务(底层调 `Connection.setAutoCommit(false)`)
- 反射调用目标方法,成功则 `commit()`,异常则判断是否符合回滚规则 → `rollback()`

3. **事务三要素**:
- **事务管理器**:`PlatformTransactionManager` 及其实现
- **事务属性**:传播机制、隔离级别、超时、只读、rollbackFor / noRollbackFor
- **事务注解**:@Transactional 及其配置属性

4. **事务传播机制 7 种**:
- REQUIRED(默认):有则加入、无则新建
- REQUIRES_NEW:挂起外层,开新事务
- SUPPORTS:有则加入、无则非事务
- NOT_SUPPORTED:挂起外层,非事务执行
- MANDATORY:必须外层有事务,否则抛异常
- NESTED:有则嵌套(保存点)、无则新建
- NEVER:必须无外层事务,否则抛异常

5. **异常回滚规则**:
- **默认只对 RuntimeException 和 Error 回滚**
- 受检异常(checked exception)默认不回滚
- 通过 `@Transactional(rollbackFor = Exception.class)` 让所有异常都回滚
- 通过 `@Transactional(noRollbackFor = BusinessException.class)` 排除特定异常
- **重要**:异常被 catch 吞掉,事务不会回滚(拦截器看不到异常)

6. **失效场景**:
- 类内部自调用(this.aMethod()):不走代理,事务失效
- 方法 private / final / static:代理无法覆写
- 方法非 public:JDK 代理只支持 public
- 目标类未注册为 Spring Bean:无代理
- 异常被 catch 吞掉:事务拦截器看不到
- 数据库不支持事务(如 MySQL MyISAM 引擎):底层无事务能力
- 传播机制选错(如 REQUIRES_NEW 导致外层事务看不到内层)

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:三层实现结构、事务管理器底层操作
- **广度**:传播机制的选型、分布式事务演进
- **应用**:事务边界设计、异常处理策略、失效场景排查

**延伸递进面试题**:
1. **@Transactional 的三要素是什么?它们分别是什么类/接口?**
- 解答要点:三要素是"事务管理器 + 事务属性 + 事务注解"。事务管理器是 PlatformTransactionManager 及其实现(如 DataSourceTransactionManager);事务属性是 TransactionDefinition 定义的配置(传播、隔离、超时、只读);事务注解是 @Transactional(本质是一个 TransactionAttribute,通过 TransactionAttributeSource 解析)
2. **REQUIRES_NEW 和 NESTED 有什么区别?各自适用场景?**
- 解答要点:REQUIRES_NEW 挂起外层事务、开完全独立的新事务,内层回滚不影响外层,外层回滚内层已提交不回滚;NESTED 是在同一物理事务内用 savepoint 嵌套,内层回滚到 savepoint 但不影响外层其他部分,外层回滚则全部回滚。适用:REQUIRES_NEW 用于日志、审计等必须提交的操作;NESTED 用于批处理中的可回滚子单元
3. **什么情况下应该把 @Transactional 放在 Service 层还是 DAO 层?为什么?**
- 解答要点:应该放 Service 层。理由:事务边界应该和业务操作对齐(一个 Service 方法通常对应一个业务事务);DAO 层被多个 Service 共享,若在 DAO 上加事务会导致事务粒度过细;Spring 通过 AOP 代理实现事务,Service 层已持有代理,跨 DAO 调用能共享同一事务上下文

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring 的 @Transactional 本质上是**基于 AOP 切面的声明式事务**。它的实现我一般从**三层结构**来讲:

**第一层是 @Transactional 注解本身**:它只是一个声明式标记,告诉 Spring "这个 bean 的方法需要事务代理"。它内部包含一系列事务属性,比如传播机制、隔离级别、超时时间、只读标记、rollbackFor、noRollbackFor 等。

**第二层是 AOP 拦截器(TransactionInterceptor)**:@EnableTransactionManagement 启动时会注册 BeanFactoryTransactionAttributeSourceAdvisor,它在 Bean 初始化后(postProcessAfterInitialization)判断这个 bean 是否需要事务代理,需要就通过 AOP 创建代理对象。调用 @Transactional 方法时,代理对象先执行 TransactionInterceptor.invoke():
- 通过 TransactionAttributeSource 解析注解拿到事务属性
- 委托 PlatformTransactionManager.getTransaction() 开启事务(底层调 Connection.setAutoCommit(false))
- 反射调用目标方法
- 方法成功则 commit(),方法抛异常则判断是否符合回滚规则 → rollback()

**第三层是 PlatformTransactionManager**:抽象的事务管理器接口,JDBC 场景下由 DataSourceTransactionManager 实现,它持有 DataSource,通过 Connection 对象操作事务。

**两大前提**:一是数据库本身支持事务(比如 MySQL InnoDB 支持,MyISAM 就不支持),二是当前对象必须是 AOP 代理对象,否则 @Transactional 完全无效。

**最经典的失效场景是自调用**:同一个类里的 A 方法调 B 方法,B 上标了 @Transactional,通过 this.bMethod() 调用时不走代理,事务注解无效。规避方案是从其他 Bean 调用、用 AopContext.currentProxy()、@Lazy 自注入 self 字段、或拆成两个 Bean。

**异常回滚规则**是关键:默认只对 RuntimeException 和 Error 回滚,受检异常(checked exception)默认不回滚。如果业务需要让 checked exception 也回滚,要显式写 @Transactional(rollbackFor = Exception.class);如果不想让某些特定异常回滚,用 noRollbackFor 排除。另外还有一个坑:如果异常被 catch 吞掉了,事务拦截器看不到异常,事务也不会回滚。

**传播机制 7 种**最常见的是 REQUIRED(默认)—— 有事务就加入、没有就新建;REQUIRES_NEW 会挂起外层事务、开完全独立的新事务,适合审计、日志这种必须提交的操作;NESTED 用 savepoint 嵌套在同一物理事务内,适合批处理中可回滚的子单元。

#7 Spring事务失效可能是哪些原因? medium java

题目

Spring事务失效可能是哪些原因?

我的回答

Spring 事务失效。首先 Spring 事务是它自身不提供事务,它是依赖于所属数据库支持事务才能使用事务。另外,Spring 的事务它是基于自身代理实现的,通过事务注解标注,然后通过代理对象来实现事务的。在事务代理的实现过程中,它需要首先关闭自动提交,然后在启动方法前进行启动事务,启动方法后进行提交事务,这么一整个流程。这里就限定了。首先第一点,你这个对象这里边就隐含了一些它失败的原因。首先你这个对象是要在 Spring Bean 的管理管理范围内呢,它需要是个代理。另外,它需要执行代理的方法才能执行事务。如果你是通过直接 new 或者是在本方法内去调用这个方,事务方法,它是无法进行事务支撑的。

点评

评分:⭐⭐⭐☆☆(3/5)
优点:两大前提抓到(数据库支持 + 代理机制)、直接 new 和类内部自调用两个失效场景点到
不足:(严重):清单严重不全,只给了 3 条。漏了 7 条:方法可见性(private/final/static)、非 public、**异常被 catch 吞掉**(工程最常见"静默失效")、传播机制选错、@EnableTransactionManagement 未开启、数据库引擎不支持、@Transactional 加在字段/构造器
补充:完整 10 条清单(注册Bean / new / 自调用 / 方法可见性 / 非public / catch吞异常 / 传播机制 / 数据库引擎 / @Enable / 注解位置) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Spring 事务失效场景的系统性掌握和工程排查能力。

**知识点拆解**:

1. **失效的两大根本原因**:
- **前提不成立**:数据库层不支持事务、@EnableTransactionManagement 未开启
- **代理机制失效**:所有其他失效场景(10 条清单里 8 条)都源于"调用没有走代理对象"

2. **完整失效清单(10 条)**:
1. 类未注册为 Spring Bean:没有 @Service/@Component 或被 @Configuration 扫描
2. 通过 new 直接实例化:必须通过容器 @Autowired 注入
3. 类内部自调用(this.method()):走 this 不走代理,最常见
4. 方法 private / final / static:代理无法覆写
5. 方法非 public:JDK 动态代理限制
6. 异常被 catch 吞掉:事务拦截器看不到异常
7. 传播机制不匹配(如 REQUIRES_NEW):业务期望与实际传播行为不符
8. 数据库本身不支持事务:MySQL 用 MyISAM
9. @EnableTransactionManagement 未开启:Spring 原生项目需要,Spring Boot 自动配置
10. 注解加在字段/构造器上:只能加在类或方法上

3. **常见自调用规避方案**:
- 从其他 Bean 中调用
- AopContext.currentProxy() 获取当前代理对象
- @Autowired @Lazy private MyService self 自注入,然后 self.xxx()
- 拆成两个 Bean(架构最佳)

4. **异常处理的两大坑**:
- **默认只回滚 RuntimeException 和 Error**,受检异常不回滚
- **异常被 catch 吞掉**:拦截器在 finally 之前看不到异常,事务按成功提交
- 建议:@Transactional(rollbackFor = Exception.class) + 不要静默 catch

5. **传播机制选错的典型场景**:
- REQUIRES_NEW:内层事务和外层事务独立提交,外层回滚不影响内层
- NESTED:使用 savepoint 嵌套,外层回滚会回滚所有嵌套
- REQUIRED:默认值,加入外层事务,但可能出现"外层已提交,内层回滚导致数据不一致"

6. **加分项**:
- @Transactional 加在类上会应用到所有 public 方法,可能带来性能损耗
- @Transactional(readOnly = true) 让数据库连接用只读、跳过脏检查
- 传播机制组合:REQUIRED + REQUIRES_NEW 实现"外层大事务 + 内层独立提交",用于审计日志等必须提交场景

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:代理机制原理、AOP 织入时机
- **广度**:数据库事务支持、传播机制、异常传播链
- **应用**:生产环境事务失效排查、日志追踪

**延伸递进面试题**:
1. **生产上如何快速定位 @Transactional 是否真的生效了?**
- 解答要点:三种方式——① 在方法内调 Thread.currentThread().getStackTrace() 检查调用栈是否经过 TransactionInterceptor 代理对象;② 打印目标对象的 class 名,看是否包含 $$EnhancedBySpringCGLIB 或 $$ 后缀(CGLIB 代理标志);③ 打开 Spring 事务调试日志(logging.level.org.springframework.transaction=DEBUG)观察事务开启/提交日志
2. **如何用事务事件(TransactionalEventListener)把非事务代码也挂到事务的生命周期?**
- 解答要点:用 @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT / AFTER_COMMIT / AFTER_ROLLBACK) 监听事务生命周期,在发布端通过 ApplicationEventPublisher.publishEvent() 发送事件;适合"事务提交后再执行非事务操作"的场景,如发消息、发邮件、更新缓存
3. **@Transactional 里 catch 异常然后手动回滚怎么做?抛事务异常和 catch 后调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 有什么区别?**
- 解答要点:直接抛异常最干净,事务拦截器自动 rollback;catch 后手动 setRollbackOnly 只是标记回滚,但业务逻辑可能还继续执行了后续代码,容易漏出脏数据;最佳实践是 catch 后 rethrow(throw new RuntimeException(e)),让事务拦截器自然处理

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring 事务失效的场景我一般分成**两大类**来记:

**第一类是前提不成立**:
- 数据库本身不支持事务(比如 MySQL 用 MyISAM 引擎)
- @EnableTransactionManagement 没开启(Spring 原生项目常见,Spring Boot 自动配置所以不常踩)
- @Transactional 注解加在字段或构造器上(只能加在类或方法上)

**第二类是代理机制失效**,这是 90% 的失效场景都在这类里。Spring 事务基于 AOP 代理实现,所以**只要调用没走代理对象,事务就一定失效**。具体的场景我数一下有 8 条:

1. **类没注册成 Spring Bean**:没有 @Service/@Component 标注,或 @ComponentScan 没扫到,Spring 不会给这类建代理
2. **通过 new 直接实例化**:绕过 Spring 容器,拿到的肯定是原始对象
3. **类内部自调用(this.method())**:这是最经典的坑。this 是原始对象,不走代理,事务直接失效。规避方案有四种:从其他 Bean 调用、AopContext.currentProxy()、@Lazy 自注入 self 字段、或拆成两个 Bean
4. **方法 private / final / static**:CGLIB 无法覆写 final 和 static,也无法拦截 private 方法
5. **方法非 public**:JDK 动态代理只支持 public 接口方法
6. **异常被 catch 吞掉**:事务拦截器在 finally 之前看不到异常,事务按成功提交,这是最隐蔽的"静默失效"
7. **传播机制选错**:比如 REQUIRES_NEW 会挂起外层事务,内层回滚不影响外层,业务以为整个都回滚了但其实内层已提交
8. **数据库引擎不支持事务**:这个虽然归在前提类,但线上真会遇到"MySQL 建表没指定引擎默认用 MyISAM"

**几个补充建议**:
- @Transactional 加在类上会应用到所有 public 方法,性能有损耗,能精确到方法就精确到方法
- 建议显式加 `@Transactional(rollbackFor = Exception.class)` 避免受检异常不回滚
- 建议 `@Transactional(readOnly = true)` 用在查询方法上,数据库连接会走只读模式跳过脏检查
- 生产上定位事务是否生效可以用三个方法:看目标对象 class 名是否包含 `$$EnhancedBySpringCGLIB`、用 Thread.currentThread().getStackTrace() 检查调用栈、或打开 `logging.level.org.springframework.transaction=DEBUG` 日志

**实际工程中的最佳实践**:
- 事务边界划在 Service 层,不要在 DAO 层加 @Transactional
- 不要把事务方法设计得太大,事务内不要做 RPC 调用、发消息、发邮件这些非事务操作
- 必须的非事务副作用用 @TransactionalEventListener 挂到事务提交之后

#8 Spring中用到了哪些设计模式 high java

题目

Spring中用到了哪些设计模式

我的回答

Spring 中最常规的首先是代理,它的大部分功能都是基于代理的实现的,AOP。另外就是工厂,工厂方法,它的创建对象都是基于工厂方法去实现的。再之后是单例模式,它的管理 bean 的模式大部分是单例的。还有就是享元模式。再有就是模板模式,它的很多功能是模板化的,比如 Resent Template Latter,JDBC Template Latter 都是模板方法性质的。然后是策略模式,其他的暂时想不起来了。

点评

评分:⭐⭐⭐☆☆(3/5)
优点:核心模式答对(代理/工厂/单例/模板/策略)、每类都有具体 Spring 实现
不足:**"享元模式"是错误答案**——Spring 里没有明显享元实现(可能把单例记混了);漏了多个经典模式:观察者(@EventListener)、适配器、装饰器、责任链(HandlerInterceptor)、建造者(@Bean)、中介者(ApplicationContext)、抽象工厂(FactoryBean);Spring 单例 vs GoF 单例的区别没点出
补充:Spring 设计模式分三类:创建型(单例/工厂/抽象工厂/建造者)、结构型(代理/装饰器/适配器/外观)、行为型(模板/观察者/策略/责任链/中介者/状态) ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对 Spring 源码架构的理解深度,以及能否把设计模式与 Spring 的实际实现一一对应。

**知识点拆解**:

1. **创建型模式**:
- **单例模式**:Bean 默认作用域是 singleton,容器保证每个 bean 定义只有一个实例。**注意区分 Spring 单例 vs GoF 单例**:Spring 是容器级单例(可多容器并存),GoF 是全局唯一(静态实例 + 私有构造器)
- **工厂方法**:BeanFactory.getBean() 按名称/类型获取 bean,本质是工厂方法模式
- **抽象工厂**:FactoryBean 是"生产其他 bean 的工厂",Spring 在容器启动时调用 FactoryBean.getObject() 得到真正的业务 bean
- **建造者**:@Configuration 类中的 @Bean 方法内部多行初始化代码,本质是建造者模式构建复杂对象

2. **结构型模式**:
- **代理模式**(Spring 最大特色):AOP 通过 JDK 动态代理(基于接口)或 CGLIB(基于继承)在运行时包装目标对象
- **装饰器模式**:代理增强、HttpServletRequestWrapper 等包装类
- **适配器模式**:AbstractCachingErrorHandler、HandlerMethodReturnValueHandler 系列把不同格式的返回值适配到统一响应
- **外观模式(Facade)**:JdbcTemplate、RestTemplate 把 JDBC/HTTP 客户端的复杂操作收敛为几个简单方法

3. **行为型模式**:
- **模板方法**:JdbcTemplate.execute()、RestTemplate.exchange() 定义了"操作 - 预处理 - 后置处理"的算法骨架,子类/lambda 实现具体细节
- **观察者模式**:ApplicationEvent + ApplicationListener + @EventListener 实现事件驱动,@TransactionalEventListener 更进一步结合事务
- **策略模式**:PlatformTransactionManager 有多种实现(DataSourceTransactionManager / JtaTransactionManager),ResourceLoader 有多种实现(ClassLoaderResourceLoader / DefaultResourceLoader),运行时选不同实现
- **责任链模式**:SpringMVC 的 HandlerInterceptor 链、Spring Security 的 FilterChain
- **中介者模式**:ApplicationContext 协调 Bean 之间的通信
- **状态模式**:TransactionSynchronizationManager 管理事务同步状态

4. **几个容易搞混的对比**:
- **代理 vs 装饰器**:代理是控制访问(AOP、远程代理、保护代理),装饰器是动态添加功能(IO 流装饰)
- **工厂 vs 建造者**:工厂一次性创建,建造者分步构建复杂对象
- **观察者 vs 发布 - 订阅**:观察者是同步通知,发布 - 订阅通常引入消息中间件

5. **享元模式的常见误用**(本题失分点):
- 享元模式是共享细粒度对象以减少内存开销,如 Integer 缓存、字符串常量池、Character 缓存
- Spring 中并没有明显的享元模式实现,容易和"单例"混淆

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:Spring 源码中对设计模式的具体实现类
- **广度**:设计模式之间的对比和适用边界
- **应用**:业务开发中如何借鉴 Spring 的设计思路

**延伸递进面试题**:
1. **Spring 的三级缓存用了什么设计模式?如果不用工厂模式(ObjectFactory)会怎样?**
- 解答要点:三级缓存本质是抽象工厂模式(L3 存的是 ObjectFactory,延迟决定"要不要代理")。如果不用工厂模式,只能用两级缓存,但会导致 AOP 场景下"B 持有 A 原始对象而 A 自身持有代理对象"的引用分裂问题。三级缓存的核心价值是让"是否代理"的决策延迟到运行时按需生成
2. **Spring 里有哪些地方用到了责任链?如果让你自定义一条拦截链怎么做?**
- 解答要点:SpringMVC 的 HandlerInterceptor(preHandle/afterCompletion/postHandle)、Spring Security 的 FilterChain、Tomcat 的 Pipeline、Spring AOP 的 AdviceChain 都用责任链。自定义方式:定义接口 Handler { boolean handle(Request ctx, Handler next); },构造一个 List<Handler>,运行时依次调用 handler.handle(ctx, next),最后一个 handler 处理业务逻辑,异常向上传播
3. **Spring 的观察者模式如何结合事务使用?@TransactionalEventListener 的 phase 有哪些?**
- 解答要点:@TransactionalEventListener 结合事务使用,支持 BEFORE_COMMIT、AFTER_COMMIT、AFTER_ROLLBACK、BEFORE_COMPLETION 四种 phase,默认是 AFTER_COMMIT。适合"事务提交后再发通知/发消息"的场景,避免事务回滚后消息已经发出去的问题

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,Spring 是设计模式的集大成者,我按**创建型、结构型、行为型**三大类来讲。

**创建型模式**:
- **单例模式**:Bean 默认作用域是 singleton,容器保证每个 bean 定义只有一个实例。这里有个关键点:Spring 的"单例"是**容器级单例**(可多容器并存),和 GoF 的"全局唯一单例"(静态实例 + 私有构造器)不完全一样
- **工厂方法**:BeanFactory.getBean() 按名称/类型获取 bean,本质是工厂方法模式
- **抽象工厂**:FactoryBean 是"生产其他 bean 的工厂",Spring 在容器启动时调用 FactoryBean.getObject() 得到真正的业务 bean
- **建造者**:@Configuration 类中的 @Bean 方法内部多行初始化代码,本质是建造者模式

**结构型模式**:
- **代理模式**(Spring 最大特色):AOP 通过 JDK 动态代理(基于接口)或 CGLIB(基于继承)在运行时包装目标对象
- **装饰器模式**:代理增强、HttpServletRequestWrapper 等包装类
- **适配器模式**:AbstractCachingErrorHandler、HandlerMethodReturnValueHandler 系列把不同格式的返回值适配到统一响应
- **外观模式(Facade)**:JdbcTemplate、RestTemplate 把 JDBC/HTTP 客户端的复杂操作收敛为几个简单方法

**行为型模式**:
- **模板方法**:JdbcTemplate.execute()、RestTemplate.exchange() 定义了"操作 - 预处理 - 后置处理"的算法骨架,子类/lambda 实现具体细节
- **观察者模式**:ApplicationEvent + ApplicationListener + @EventListener 实现事件驱动,@TransactionalEventListener 更进一步结合事务
- **策略模式**:PlatformTransactionManager 有多种实现(DataSourceTransactionManager / JtaTransactionManager),ResourceLoader 有多种实现(ClassLoaderResourceLoader / DefaultResourceLoader),运行时选不同实现
- **责任链模式**:SpringMVC 的 HandlerInterceptor 链、Spring Security 的 FilterChain
- **中介者模式**:ApplicationContext 协调 Bean 之间的通信
- **状态模式**:TransactionSynchronizationManager 管理事务同步状态

**几个容易搞混的对比**:
- **代理 vs 装饰器**:代理是控制访问(AOP、远程代理),装饰器是动态添加功能(IO 流装饰)
- **工厂 vs 建造者**:工厂一次性创建,建造者分步构建复杂对象

**关于享元模式的一个澄清**:享元模式是共享细粒度对象以减少内存开销(如 Integer 缓存、字符串常量池),Spring 中并没有明显的享元模式实现,容易和"单例"混淆。

**实际工程借鉴**:我在写业务代码时会借鉴 Spring 的设计思路——比如用模板方法把"参数校验 - 业务执行 - 后置日志"的重复代码抽象成 Template;用观察者模式通过 Spring 事件解耦业务模块(订单支付完成后触发积分、通知、库存回滚);用工厂模式把复杂的 Bean 构建封装在 @Configuration 类里。

#9 设计模式的7大基本原则有哪些? high system-design

题目

设计模式的7大基本原则有哪些?

我的回答

设计模式的七大基本原则,开闭原则。想不起来了,我就记得对修改关闭,对继承开放开闭原则,然后是还有啥呀?

点评

评分:⭐⭐☆☆☆(2/5)
优点:记得开闭原则 OCP 概念
不足:(严重):**开闭原则定义说错**——"对继承开放" ❌ 应该是 "对扩展开放";**7 大原则只记得 1 个**——SOLID 五大原则(SRP/OCP/LSP/ISP/DIP)+ CRP + LOD 是骨架,这 7 个必须完整背诵
补充:- SRP 单一职责:一个类只做一件事 - OCP 开闭:对修改关闭,对扩展开放 - LSP 里氏替换:子类能替换父类 - DIP 依赖倒置:面向接口编程 - ISP 接口隔离:客户端不依赖不用的接口 - CRP 合成复用:优先组合而非继承 - LOD 迪米特法则:最少知识原则 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对面向对象设计原则的系统掌握,特别是 SOLID 五大原则的完整理解和工程应用。

**知识点拆解**:

1. **SOLID 五大原则(核心中的核心)**:
- **S**ingle Responsibility(单一职责原则 SRP):一个类只做一件事(一个类只承担一个引起变化的原因)
- **O**pen/Closed(开闭原则 OCP):对修改关闭,对扩展开放(新增功能不改老代码)
- **L**iskov Substitution(里氏替换原则 LSP):子类能替换父类出现且程序逻辑不变(子类不能违反父类契约)
- **I**nterface Segregation(接口隔离原则 ISP):客户端不应被迫依赖不用的接口(接口要小而专)
- **D**ependency Inversion(依赖倒置原则 DIP):面向接口编程,高层不依赖低层,都依赖抽象

2. **另外两个补充原则**:
- **Composite Reuse Principle(合成复用原则 CRP)**:优先组合,而不是继承("合成优于继承")
- **Law of Demeter(迪米特法则 LOD)**:最少知识原则("只和直接的朋友谈",减少对象间的耦合)

3. **每个原则的典型违反案例**:
- SRP:一个类里既有 CRUD 又有导出 Excel 又有权限校验 —— 应该拆
- OCP:加个"信用卡支付"要改 Pay 方法里的 if-else —— 应该用策略模式
- LSP:Rectangle 继承 Shape,子类覆写方法时抛异常 —— 子类应该能替换父类
- DIP:UserService 直接 new MySqlUserDao —— 应该 @Autowired UserDao
- ISP:OrderService 接口同时有 create/cancel/export/notify —— 应该拆小
- CRP:用继承实现"可序列化用户" —— 应该用组合 Serializer serializer
- LOD:a.getB().getC().doSomething() 链条太长 —— 应该封装

4. **"扩展"到底是什么**(回答 OCP 时要能答):
- 用继承扩展:子类新增方法
- 用接口 + 策略扩展:新实现类注册到策略容器
- 用注解 + 反射扩展:定义规范(注解),运行时扫描匹配
- 用模板方法扩展:子类覆写模板钩子
- 用装饰器扩展:装饰对象动态增强

5. **原则之间的关系**:
- SRP 是基础(单一职责),其他原则建立在它之上
- OCP 是目标(对修改关闭),需要 LSP/DIP/ISP/CRP 来支撑
- LSP 保证继承的健壮性
- DIP 是 OCP 的工程手段(面向接口才能扩展)
- ISP 让 DIP 更纯粹(小接口更好用)
- CRP 是实现 OCP 的替代方案(组合优于继承)

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:每个原则的边界条件、反例识别
- **广度**:原则之间的关联与冲突
- **应用**:实际项目中的重构经验

**延伸递进面试题**:
1. **开闭原则的"扩展"具体指哪些方式?策略模式和模板方法哪种更符合开闭原则?**
- 解答要点:扩展指不改老代码而增加功能,包括继承子类新增方法、接口 + 策略新增实现类、注解 + 反射匹配、模板方法覆写钩子、装饰器动态增强。模板方法更适合"算法骨架固定、细节可变化"的场景(如 JdbcTemplate.execute),策略模式更适合"算法整体可替换"的场景(如不同支付方式的 Pay 接口)。模板方法扩展更受限,策略模式扩展更灵活
2. **里氏替换原则和接口隔离原则经常冲突,如何解决?**
- 解答要点:LSP 要求"子类能替换父类",但 ISP 要求"接口要小而专"。解决方式是先按 ISP 拆小接口,每个小接口内部再按 LSP 继承扩展。例如 Order 相关接口拆成 OrderCreateService、OrderQueryService、OrderExportService,每个接口单独扩展而不互相耦合
3. **单一职责和迪米特法则如何在实践中落地?有什么反例?**
- 解答要点:SRP 落地方法是给类起个"能一句话说清的职责名",如果名字里出现"和"(如 User 和 Order Service),大概率违反 SRP。LOD 落地方法是控制对象间调用链不超过 2 层(a.getB().doX() 已经算极限)。反例:一个 Service 里既有业务逻辑又有 Excel 导出又调用了 3 个下游的下游 API

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,设计模式的 7 大基本原则我一般分成两组来记,**核心是 SOLID 五大原则**,另外补充**合成复用原则**和**迪米特法则**。

**SOLID 五大原则**:

第一,**单一职责原则 SRP**(Single Responsibility):一个类只做一件事,一个类只承担一个引起变化的原因。反例是一个类里既有 CRUD 又有导出 Excel 又有权限校验,应该拆。实际落地方法是给类起个"能一句话说清的职责名",如果名字里出现"和"字(如 User 和 Order Service),大概率违反 SRP。

第二,**开闭原则 OCP**(Open/Closed):对修改关闭,对扩展开放,新增功能不改老代码。这个"扩展"具体有几种方式:继承子类新增方法、接口 + 策略新增实现类、注解 + 反射匹配、模板方法覆写钩子、装饰器动态增强。反例是加个"信用卡支付"要改 Pay 方法里的 if-else,应该用策略模式,加个 CreditCardPay implements Pay。

第三,**里氏替换原则 LSP**(Liskov Substitution):子类能替换父类出现且程序逻辑不变,子类不能违反父类契约。反例是 Rectangle 继承 Shape,但子类覆写方法时抛异常,破坏了父类契约。这个原则是继承的健康保障。

第四,**接口隔离原则 ISP**(Interface Segregation):客户端不应被迫依赖不用的接口,接口要小而专。反例是 OrderService 接口同时有 create/cancel/export/notify,业务方只需要 create,应该拆成 OrderCreateService。这个原则和 LSP 有时会冲突,解决办法是先按 ISP 拆小接口,每个小接口内部再按 LSP 继承扩展。

第五,**依赖倒置原则 DIP**(Dependency Inversion):面向接口编程,高层不依赖低层,都依赖抽象。反例是 UserService 直接 new MySqlUserDao,应该 @Autowired UserDao。这个原则是开闭原则的工程手段——只有面向接口编程,才能通过"新增实现类"来扩展。

**另外两个补充原则**:

第六,**合成复用原则 CRP**(Composite Reuse):优先组合,而不是继承,也叫"合成优于继承"。反例是用继承实现"可序列化用户",应该用组合 User 里注入 Serializer。这个原则是实现开闭原则的替代方案,组合比继承更灵活。

第七,**迪米特法则 LOD**(Law of Demeter):最少知识原则,"只和直接的朋友谈",减少对象间的耦合。反例是 a.getB().getC().doSomething() 链条太长,应该封装成 a.doX()。落地方法是控制对象间调用链不超过 2 层。

**原则之间的关系**:SRP 是基础(单一职责),其他原则建立在它之上;OCP 是目标(对修改关闭),需要 LSP/DIP/ISP/CRP 来支撑;DIP 是 OCP 的工程手段(面向接口才能扩展);ISP 让 DIP 更纯粹(小接口更好用)。

**实际工程中**我一般会先用 SOLID 检查设计,再用设计模式落地——比如用策略模式实现 OCP、用工厂模式配合 DIP、用模板方法保证 LSP。这三大问题(SRP + OCP + DIP)在实际重构中用得最多。

#10 使用哪种设计模式可以提高代码可维护性? high system-design

题目

使用哪种设计模式可以提高代码可维护性?

我的回答

首先就是 Spring 中最核心的代理模式,通过代理对象对,在方法不改动的情况下对方法实行扩展,提高了代码的可维护性。 其次是我觉得是策略模式,策略模式中通过对于策略的扩展,然后提升整个代码的扩展能力,而不是每次去 if else 的去进行修改,这也是我用的比较多的一种模式。 再有就是创建者或者说工厂一类的,它把创建的创建对象简化了,提供了更简洁的能力,也更利于抽象化。 还有责任链模式。在一些判定的过程中,我会使用责任链模式,比较方便的扩展判定规则,更容易进行减少方法的变更,更容易进行修改,更容易进行扩展。

点评

评分:⭐⭐⭐⭐☆(4/5)
优点:4 个主流模式覆盖(代理/策略/工厂/责任链)、每模式都点出核心收益、有实战痕迹("我用得比较多")、责任链场景选择有工程视角(判定规则)
不足:每个模式只给 1 句概括,没展开"为什么"和"怎么落地";漏了 3 个对可维护性非常关键的模式(观察者/模板方法/外观);**没提"反过度设计"的边界**——这是 senior 加分项
补充:可维护性模式分三维度——减少变更(策略/代理/观察者)、消除重复(模板/工厂/建造者)、简化调用点(外观/责任链);克制使用是判断标准:一年内大概率要改 3 次以上才抽象 ---

完整讲解

第一部分:知识重点剖析

**核心考点定位**:考察对设计模式实际工程价值的理解,特别是"可维护性"这一设计目标的落地方式。

**知识点拆解**:

1. **可维护性的本质**:**降低变更成本**—— 每次需求变化时,改动的代码越少、影响范围越小、回归成本越低,可维护性就越高。

2. **减少变更范围的模式**(OCP 落地):
- **策略模式**:if-else 泛滥的场景(不同支付方式、不同通知渠道、不同优惠计算规则),加新策略不改老代码
- **代理模式**:横切关注点(日志、事务、权限、限流)通过 AOP 剥离,业务代码保持纯粹
- **观察者模式**:跨模块协作(订单支付完成后触发积分、通知、库存回滚),新增监听器完全不动发送端

3. **消除重复代码的模式**(DRY 原则):
- **模板方法**:把"参数校验 → 业务执行 → 后置日志"这种重复骨架抽成模板,子类只实现差异部分(如 JdbcTemplate)
- **工厂模式**:封装复杂对象创建过程,避免到处 new + 初始化
- **建造者模式**:复杂对象分步构建,比构造函数更好读

4. **简化调用点的模式**(KISS 原则):
- **外观模式**:把复杂子系统收敛为几个 API(如 RestTemplate、JdbcTemplate)
- **责任链模式**:新增规则节点即可,不改主流程(参数校验链、权限校验链、支付回调链)

5. **动态增强的模式**:
- **装饰器模式**:运行时组合功能,不用继承爆炸(如 IO 流装饰器、代理增强)

6. **回答框架(senior 加分项)**:
- 从三个维度选模式:减少改动范围(策略/代理/观察者)、消除重复(模板/工厂/建造者)、简化调用点(外观/责任链)
- 实际项目用得最多的三个:策略模式、观察者模式、责任链模式
- **克制使用**:简单业务不要为了用模式而用模式,一行 if-else 比抽象策略类更易读。判断标准:如果一年内这个分支大概率要改 3 次以上,才值得抽象成模式

7. **反过度设计的标志**:
- 简单 CRUD 强用策略模式,加一层抽象反而绕
- 只有两个实现也上策略接口,未来扩展性"预期"存在
- 大量用工厂+策略+建造者堆叠,形成"过度抽象"

第二部分:考察方向与延伸递进

**考察方向**:
- **深度**:模式的实际收益与反例
- **广度**:模式之间的选型与组合
- **应用**:实际项目中的落地经验和反思

**延伸递进面试题**:
1. **策略模式和 if-else 的分界线在哪?什么情况该抽象成策略?**
- 解答要点:如果一年内这个分支大概率要改 3 次以上,才值得抽象成策略。判断维度:① 分支是否会持续扩展(业务增长);② 分支逻辑是否高度相似(可以抽象公共接口);③ 分支是否会被多处复用(策略类能注入到其他模块);④ 分支逻辑是否频繁变更(策略类能独立测试)
2. **观察者模式在 Spring 中如何落地?如何避免"事件风暴"?**
- 解答要点:Spring 用 ApplicationEvent + ApplicationListener + @EventListener 实现观察者,@TransactionalEventListener 结合事务。避免事件风暴:① 事件命名规范(按领域分类);② 一个事件只表达一个领域事实;③ 用类型参数而不是消息体(避免事件过大);④ 监听器之间不产生级联事件(避免循环)
3. **责任链模式和策略模式有什么区别?什么场景选哪个?**
- 解答要点:策略是"多个候选选一个执行"(如不同支付方式选一个),责任链是"多个节点依次执行"(如参数校验链、权限校验链、风控规则链)。选策略:一次调用只走一个实现;选责任链:一次调用要走多个节点

第三部分:面试者参考回答(300-600 字口语化)

面试官您好,可维护性的核心是**降低变更成本**——每次需求变化时,改动的代码越少、影响范围越小、回归成本越低,可维护性就越高。我从三个维度选模式:

**第一维度是减少变更范围**,对应开闭原则 OCP。最常用的是**策略模式**,业务分支多的场景(不同支付方式、不同通知渠道、不同优惠计算规则)特别适合——加新策略不改老代码,比 if-else 好维护太多。其次**代理模式**(Spring AOP 核心)把横切关注点(日志、事务、权限、限流)从业务代码剥离,业务代码保持纯粹。第三个是**观察者模式**,Spring 里用 ApplicationEvent + @EventListener,跨模块协作(订单支付完成后触发积分、通知、库存回滚)用事件驱动,新增监听器完全不动发送端,这是解耦利器。

**第二维度是消除重复代码**,对应 DRY 原则。**模板方法**把"参数校验 → 业务执行 → 后置日志"这种重复骨架抽成模板,子类只实现差异部分,Spring 的 JdbcTemplate、RestTemplate 都是典型。**工厂模式**和**建造者模式**把复杂对象创建收敛,比到处 new + 初始化好读太多。

**第三维度是简化调用点**,对应 KISS 原则。**外观模式**把复杂子系统收敛为几个 API(如 RestTemplate 收敛了 HttpURLConnection 的复杂操作)。**责任链模式**特别适合规则判定场景(参数校验链、权限校验链、风控规则链、支付回调链),加新规则节点即可,不改主流程,这是我认为**最容易落地的可维护性模式**。

**装饰器模式**是动态增强,运行时组合功能不用继承爆炸,IO 流、代理增强都是典型。

**实际项目里用得最多的三个**:策略模式(业务分支多、if-else 泛滥时)、观察者模式(跨模块协作、需要解耦时)、责任链模式(判定规则、校验链、回调处理)。

**但克制使用很重要**——简单业务不要为了用模式而用模式,一行 if-else 比抽象策略类更易读。判断标准:如果一年内这个分支大概率要改 3 次以上,才值得抽象成模式。反过度设计的标志:简单 CRUD 强用策略模式、只有两个实现也上策略接口、大量工厂+策略+建造者堆叠形成"过度抽象",这些都会让代码更难维护。

📚 查看全部面试记录