Spring 框架深度 —— IoC 容器 / AOP 代理 / 事务管理 到 设计模式落地
10 题 · 平均 3.3/5 · low 3.67 · medium 3.25 · high 3 · 第 1 轮
**核心考点定位**:考察对 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`(不推荐,掩盖设计问题)
面试官您好,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 让注入端先拿代理对象,本质是把真正的构造调用延后到使用阶段。
**核心考点定位**:考察对 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 等注解
面试官您好,这几个注解在 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 名,不指定就用类名首字母小写作为默认名。
**核心考点定位**:考察对 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 让用户能自定义实现
面试官您好,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 的搭建过程标准化了,让我们能专注于业务而不是配置。
**核心考点定位**:考察对 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 配置(更类型安全)
面试官您好,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 关闭资源。
**核心考点定位**:考察对 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 的判断
面试官您好,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 的编译期或类加载期织入。
**核心考点定位**:考察对 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 调用能共享同一事务上下文
面试官您好,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 嵌套在同一物理事务内,适合批处理中可回滚的子单元。
**核心考点定位**:考察对 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)),让事务拦截器自然处理
面试官您好,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 挂到事务提交之后
**核心考点定位**:考察对 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。适合"事务提交后再发通知/发消息"的场景,避免事务回滚后消息已经发出去的问题
面试官您好,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 类里。
**核心考点定位**:考察对面向对象设计原则的系统掌握,特别是 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
面试官您好,设计模式的 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)在实际重构中用得最多。
**核心考点定位**:考察对设计模式实际工程价值的理解,特别是"可维护性"这一设计目标的落地方式。
**知识点拆解**:
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. **责任链模式和策略模式有什么区别?什么场景选哪个?**
- 解答要点:策略是"多个候选选一个执行"(如不同支付方式选一个),责任链是"多个节点依次执行"(如参数校验链、权限校验链、风控规则链)。选策略:一次调用只走一个实现;选责任链:一次调用要走多个节点
面试官您好,可维护性的核心是**降低变更成本**——每次需求变化时,改动的代码越少、影响范围越小、回归成本越低,可维护性就越高。我从三个维度选模式:
**第一维度是减少变更范围**,对应开闭原则 OCP。最常用的是**策略模式**,业务分支多的场景(不同支付方式、不同通知渠道、不同优惠计算规则)特别适合——加新策略不改老代码,比 if-else 好维护太多。其次**代理模式**(Spring AOP 核心)把横切关注点(日志、事务、权限、限流)从业务代码剥离,业务代码保持纯粹。第三个是**观察者模式**,Spring 里用 ApplicationEvent + @EventListener,跨模块协作(订单支付完成后触发积分、通知、库存回滚)用事件驱动,新增监听器完全不动发送端,这是解耦利器。
**第二维度是消除重复代码**,对应 DRY 原则。**模板方法**把"参数校验 → 业务执行 → 后置日志"这种重复骨架抽成模板,子类只实现差异部分,Spring 的 JdbcTemplate、RestTemplate 都是典型。**工厂模式**和**建造者模式**把复杂对象创建收敛,比到处 new + 初始化好读太多。
**第三维度是简化调用点**,对应 KISS 原则。**外观模式**把复杂子系统收敛为几个 API(如 RestTemplate 收敛了 HttpURLConnection 的复杂操作)。**责任链模式**特别适合规则判定场景(参数校验链、权限校验链、风控规则链、支付回调链),加新规则节点即可,不改主流程,这是我认为**最容易落地的可维护性模式**。
**装饰器模式**是动态增强,运行时组合功能不用继承爆炸,IO 流、代理增强都是典型。
**实际项目里用得最多的三个**:策略模式(业务分支多、if-else 泛滥时)、观察者模式(跨模块协作、需要解耦时)、责任链模式(判定规则、校验链、回调处理)。
**但克制使用很重要**——简单业务不要为了用模式而用模式,一行 if-else 比抽象策略类更易读。判断标准:如果一年内这个分支大概率要改 3 次以上,才值得抽象成模式。反过度设计的标志:简单 CRUD 强用策略模式、只有两个实现也上策略接口、大量工厂+策略+建造者堆叠形成"过度抽象",这些都会让代码更难维护。