Spring面试题

Ioc

说一说什么是 IoC?

IoC(Inversion of Control)控制反转,不是什么技术,而是一种设计思想。将你设计好的对象交给容器控制,而不是传统的在你的对象内部直接控制。

  • 谁控制谁,控制什么?

    传统Java SE程序设计,我们直接在对象内部通过new进行创建对象,是程序主动去创建依赖对象;而IoC是有专门一个容器来创建这些对象,即由Ioc容器来控制对象的创建;谁控制谁?当然是IoC 容器控制了对象;控制什么?那就是主要控制了外部资源获取(不只是对象包括比如文件等)。

  • 为何是反转,哪些方面反转了?

    有反转就有正转,传统应用程序是由我们自己在对象中主动控制去直接获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转;哪些方面反转了?依赖对象的获取被反转了。

传统程序设计下,都是主动去创建相关对象然后再组合起来:

image-20250702235216799

当有了IoC/DI的容器后,在客户端类中不再主动去创建这些对象了,如图

image-20250702235236601

说一说什么是DI?

DI (Dependency Injection) 依赖注入,是实现 IOC 的具体方式,比如说利用注入机制(如构造器注入、Setter 注入或接口注入)将依赖传递给目标对象。

  • 优点
    • 在平时的 Java 开发中,如果我们要实现某一个功能,可能至少需要两个以上的对象来协助完成,在没有 Spring 之前,每个对象在需要它的合作对象时,需要自己 new 一个,比如说 A 要使用 B,A 就对 B 产生了依赖,也就是 A 和 B 之间存在了一种耦合关系。
    • 有了 Spring 之后,就不一样了,创建 B 的工作交给了 Spring 来完成,Spring 创建好了 B 对象后就放到容器中,A 告诉 Spring 我需要 B,Spring 就从容器中取出 B 交给 A 来使用。
    • 至于 B 是怎么来的,A 就不再关心了,Spring 容器想通过 new 创建 B 还是 new 创建 B,无所谓。
    • 这就是 IoC 的好处,它降低了对象之间的耦合度,使得程序更加灵活,更加易于维护。

Spring的依赖注入有哪些实现方式?

  • 构造函数注入:通过类的构造函数来注入依赖项。
  • Setter 注入:通过类的 Setter 方法来注入依赖项。
  • Field(字段) 注入:直接在类的字段上使用注解(如 @Autowired 或 @Resource)来注入依赖项。

@Autowired@Resource@Inject 的区别

特性 @Autowired (Spring) @Resource (JSR-250) @Inject (JSR-330)
来源规范 Spring 专属 Java EE 标准 (JSR-250) Java DI 标准 (JSR-330)
包路径 org.springframework.beans.factory.annotation javax.annotation javax.inject
默认注入方式 按类型 (byType) 按名称 (byName) 按类型 (byType)
名称指定 需配合 @Qualifier 直接使用 name 属性 需配合 @Named
必需依赖 支持 required=false 总是必需 总是必需
构造器注入 ✅ 支持 ❌ 不支持 ✅ 支持
方法/字段 ✅ 支持 ✅ 支持 ✅ 支持
参数注入 ✅ 支持 ❌ 不支持 ✅ 支持
自定义限定符 ✅ 支持 ❌ 不支持 ✅ 支持

什么是 Spring Bean?

Bean 是指由 Spring 容器管理的对象,它的生命周期由容器控制,包括创建、初始化、使用和销毁。以通过三种方式声明:注解方式XML 配置Java 配置

①、使用 @Component@Service@Repository@Controller 等注解定义,主流。

②、基于 XML 配置,Spring Boot 项目已经不怎么用了。

③、使用 Java 配置类创建 Bean:

1
2
3
4
5
6
7
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService();
}
}

@Component 和 @Bean 的区别

@Component 是 Spring 提供的一个类级别注解,由 Spring 自动扫描并注册到 Spring 容器中。

@Bean 是一个方法级别的注解,用于显式地声明一个 Bean,当我们需要第三方库或者无法使用 @Component 注解类时,可以使用 @Bean 来将其实例注册到容器中。

说一下 Bean 的生命周期

实例化 → 属性注入 →(Aware 回调 → BeanPostProcessor 前置 → 初始化方法 → BeanPostProcessor 后置)→ 使用 →(销毁方法 → 资源释放)

  • 实例化:Spring 首先使用构造方法或者工厂方法创建一个 Bean 的实例。在这个阶段,Bean 只是一个空的 Java 对象,还未设置任何属性。
  • 属性注入:Spring 将配置文件中的属性值或依赖的 Bean 注入到该 Bean 中。这个过程称为依赖注入,确保 Bean 所需的所有依赖都被注入。
  • 初始化:对注入属性后的 Bean 进行 “初始化”(如初始化连接、加载资源等),使其达到可用状态。
    1. Aware 接口回调(可选):
      若 Bean 实现了 Aware 系列接口(如 BeanNameAwareApplicationContextAware),Spring 会回调接口方法,将容器相关信息传递给 Bean。
      • BeanNameAware:获取当前 Bean 在容器中的名称(setBeanName);
      • BeanFactoryAware:获取当前 Bean 所属的容器(setBeanFactory);
      • ApplicationContextAware:获取 Spring 应用上下文(setApplicationContext)。
    2. BeanPostProcessor 前置处理(可选):
      若容器中注册了 BeanPostProcessor(Bean 后置处理器),会调用其 postProcessBeforeInitialization 方法,对 Bean 进行初始化前的自定义处理(如修改属性、增强功能)。
      注:BeanPostProcessor 对容器中所有 Bean 生效,是 Spring 扩展的核心机制之一。
    3. 初始化方法执行(可选):
      执行用户定义的初始化逻辑,按以下优先级执行:
      • @PostConstruct 注解的方法(JSR-250 标准,在依赖注入完成后执行);
      • 实现 InitializingBean 接口的 afterPropertiesSet 方法(Spring 内置接口,注入完成后回调);
      • 自定义 init-method(在 XML 中通过 init-method 指定,或 @Bean(initMethod = "..."))。
    4. BeanPostProcessor 后置处理(可选):
      调用 BeanPostProcessorpostProcessAfterInitialization 方法,对初始化后的 Bean 进行最终处理(如 AOP 代理就是通过此步骤生成代理对象)。
  • 使用中:Bean 进入可用状态,供应用程序调用。此时 Bean 已完成初始化,可通过容器的 getBean 方法获取,或被其他 Bean 依赖使用。
  • 销毁:在容器关闭时,Spring 会调用 destroy 方法,释放 Bean 占用的资源(如关闭连接、释放缓存)。

Bean 的作用域有哪些?

在 Spring 中,Bean 默认是单例的,即在整个 Spring 容器中,每个 Bean 只有一个实例。

可以通过在配置中指定 scope 属性,将 Bean 改为多例(Prototype)模式,这样每次获取的都是新的实例。

在Spring配置文件中,可以通过标签的scope属性来指定Bean的作用域。例如:

1
<bean id="myBean" class="com.example.MyBean" scope="singleton"/>

基于Java的配置中,可以通过@Scope注解来指定Bean的作用域。例如:

1
2
3
4
5
@Bean
@Scope("prototype") // 每次获取都是新的实例
public MyBean myBean() {
return new MyBean();
}

Spring支持几种不同的作用域,以满足不同的应用场景需求。以下是一些主要的Bean作用域:

  • Singleton(单例):在整个应用程序中只存在一个 Bean 实例。默认作用域,Spring 容器中只会创建一个 Bean 实例,并在容器的整个生命周期中共享该实例。
  • Prototype(原型):每次请求时都会创建一个新的 Bean 实例。次从容器中获取该 Bean 时都会创建一个新实例,适用于状态非常瞬时的 Bean。
  • Request(请求):每个 HTTP 请求都会创建一个新的 Bean 实例。仅在 Spring Web 应用程序中有效,每个 HTTP 请求都会创建一个新的 Bean 实例,适用于 Web 应用中需求局部性的 Bean。
  • Session(会话):Session 范围内只会创建一个 Bean 实例。该 Bean 实例在用户会话范围内共享,仅在 Spring Web 应用程序中有效,适用于与用户会话相关的 Bean。
  • Application:当前 ServletContext 中只存在一个 Bean 实例。仅在 Spring Web 应用程序中有效,该 Bean 实例在整个
  • ServletContext 范围内共享,适用于应用程序范围内共享的 Bean。
  • WebSocket(Web套接字):在 WebSocket 范围内只存在一个 Bean 实例。仅在支持 WebSocket 的应用程序中有效,该 Bean 实例在 WebSocket 会话范围内共享,适用于 WebSocket 会话范围内共享的 Bean。
    Custom scopes(自定义作用域):Spring 允许开发者定义自定义的作用域,通过实现 Scope 接口来创建新的 Bean 作用域。

Spring中的单例Bean会存在线程安全问题吗?

在 Spring 中,单例 Bean 本身并不保证线程安全,其线程安全性取决于 Bean 的实现方式。单例 Bean 在容器中仅存在一个实例,当多个线程同时访问该实例时,若 Bean 包含可变状态(如成员变量),则可能出现线程安全问题;若 Bean 是无状态不可变的,则线程安全。

  • 单例 Bean 的线程安全问题

    1. 可变状态的单例 Bean

      若 Bean 中包含可被多个线程修改的成员变量(如计数器、集合),则存在线程安全隐患。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Component
      public class CounterService {
      private int count = 0; // 可变状态,多线程访问可能冲突

      public void increment() {
      count++; // 非原子操作,存在竞态条件
      }

      public int getCount() {
      return count;
      }
      }

      问题:多个线程同时调用 increment() 时,由于 count++ 不是原子操作,可能导致计数错误。

    2. 有状态 Bean 的方法共享资源

      若 Bean 的方法操作共享资源(如文件、数据库连接),且未进行同步控制,则可能出现线程安全问题。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Component
      public class FileService {
      private FileWriter writer; // 共享资源

      @PostConstruct
      public void init() throws IOException {
      writer = new FileWriter("data.txt");
      }

      public void write(String data) throws IOException {
      writer.write(data); // 多线程写入可能导致数据错乱
      }
      }

      问题:多个线程同时调用 write() 可能导致文件内容混乱。

  • 线程安全的单例 Bean 实现方式

    1. 无状态 Bean

      Bean 不包含任何成员变量(仅提供方法逻辑),所有操作基于方法参数和局部变量。

      1
      2
      3
      4
      5
      6
      @Component
      public class CalculatorService {
      public int add(int a, int b) {
      return a + b; // 仅依赖方法参数,无共享状态
      }
      }

      线程安全原因:方法内的局部变量在每个线程的栈内存中独立存在,互不干扰。

    2. 不可变 Bean

      Bean 的成员变量被声明为 final,且对象创建后状态不可变。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Component
      public class ConfigService {
      private final String apiKey;

      @Autowired
      public ConfigService(@Value("${api.key}") String apiKey) {
      this.apiKey = apiKey; // 构造器注入后不可变
      }

      public String getApiKey() {
      return apiKey;
      }
      }

      线程安全原因:不可变对象的状态无法被修改,多线程访问时不存在竞态条件。

    3. 同步机制

      在有状态 Bean 中使用 synchronizedLock 或原子类(如 AtomicInteger)保证线程安全。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Component
      public class SafeCounterService {
      private final AtomicInteger count = new AtomicInteger(0); // 原子类保证线程安全

      public void increment() {
      count.incrementAndGet(); // 原子操作
      }

      public int getCount() {
      return count.get();
      }
      }

      线程安全原因AtomicInteger 使用 CAS(Compare-and-Swap)保证原子性,避免锁竞争。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Component
      public class CounterService {
      private int count = 0; // 可变状态,多线程访问可能冲突

      //使用 synchronized 确保线程安全
      public synchronized void increment() {
      count++; // 非原子操作,存在竞态条件
      }

      public int getCount() {
      return count;
      }
      }

      使用 synchronized 确保线程安全

    4. ThreadLocal 隔离线程状态

      使用 ThreadLocal 为每个线程提供独立的变量副本。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      @Component
      public class RequestContextHolder {
      private final ThreadLocal<String> requestId = new ThreadLocal<>();

      public void setRequestId(String id) {
      requestId.set(id);
      }

      public String getRequestId() {
      return requestId.get();
      }

      public void clear() {
      requestId.remove(); // 避免内存泄漏
      }
      }

      线程安全原因:每个线程独立维护自己的 ThreadLocal 副本,互不干扰。

    5. 改为 prototype 作用域

      如果 Bean 确实需要维护状态,可以考虑将其改为 prototype 作用域,这样每次注入都会创建一个新的实例,避免了多线程共享同一个实例的问题。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Service
      @Scope("prototype") // 每次注入都创建新实例
      public class StatefulService {
      private String state; // 现在每个实例都有独立状态

      public void setState(String state) {
      this.state = state;
      }
      }

FactoryBean 与 BeanFactory 的区别

  • FactoryBean:它是一个工厂 Bean,实现了FactoryBean接口,主要用于创建特定类型的 Bean 实例。
  • BeanFactory:它是 Spring 容器的核心接口,负责管理和获取 Bean 实例,是容器的基础。

什么是循环依赖?

A 依赖 B,B 依赖 A,或者 C 依赖 C,就成了循环依赖。

1
2
3
4
5
6
7
8
9
class ServiceA {
@Autowired
private ServiceB serviceB; // A依赖B
}

class ServiceB {
@Autowired
private ServiceA serviceA; // B依赖A
}

原因很简单,AB 循环依赖,A 实例化的时候,发现依赖 B,创建 B 实例,创建 B 的时候发现需要 A,创建 A1 实例……无限套娃。。。。

Spring中的循环依赖如何处理?

Spring的解决方案:核心是三级缓存机制,通过提前暴露刚实例化(但未初始化)的 Bean 的早期引用(可能被包装成代理),并利用属性注入(Setter/Field) 来打破 Singleton Bean 的循环依赖。

graph TD
    subgraph Spring容器
        A[一级缓存] -->|完全初始化的Bean| singletonObjects
        B[二级缓存] -->|早期暴露的Bean| earlySingletonObjects
        C[三级缓存] -->|Bean工厂| singletonFactories
    end
  1. singletonObjects (一级缓存):
    • 存放内容: 完全初始化好的、可用的成品单例 Bean
    • 访问时机: 当需要获取一个 Bean 时,首先检查这里。
    • 状态: Bean 已实例化、属性已填充、初始化方法(如 @PostConstructInitializingBeaninit-method)已执行。
  2. earlySingletonObjects (二级缓存):
    • 存放内容: 提前暴露的、早期的单例 Bean 引用。这些 Bean 已经实例化,但属性尚未填充(可能依赖其他 Bean),初始化方法也未执行
    • 作用: 解决循环依赖的关键。当一个 Bean 还在创建过程中(刚实例化完,还没填充属性),就提前把它放入这个缓存,供其他依赖它的 Bean 引用,从而打破循环。
    • 状态: Bean 已实例化(调用了构造方法),但属性未填充,未初始化。
  3. singletonFactories (三级缓存):
    • 存放内容: 单例对象工厂(ObjectFactory。每个单例 Bean 在实例化后(调用构造方法后),都会生成一个对应的 ObjectFactory 并放入此缓存。
    • ObjectFactory 的作用: 当需要获取该 Bean 的早期引用时(比如在解决循环依赖时),会调用这个工厂的 getObject() 方法。这个方法最终会调用 getEarlyBeanReference()
    • getEarlyBeanReference() 的重要性:
      • 如果 Bean 不需要被 AOP 代理(如没有切面匹配它),这个方法直接返回原始的 Bean 实例。
      • 如果 Bean 需要被 AOP 代理,这个方法会提前生成并返回代理对象。这是保证注入到其他 Bean 中的是代理对象(而不是原始对象)的关键,避免了后续注入不一致的问题。

🔄 解决循环依赖的流程(以 ServiceA 和 ServiceB 为例)

  1. 开始创建 ServiceA
    • 容器发现需要创建 ServiceA
    • 调用 ServiceA 的构造方法,实例化 ServiceA 对象(此时 serviceB 属性还是 null)。
    • 将创建 ServiceAObjectFactory 放入 **三级缓存 (singletonFactories)**。
  2. 填充 ServiceA 的属性:
    • 容器准备为 serviceB 属性赋值。
    • 发现需要注入 ServiceB,于是去获取 ServiceB
  3. 开始创建 ServiceB
    • 容器发现需要创建 ServiceB
    • 调用 ServiceB 的构造方法,实例化 ServiceB 对象(此时 serviceA 属性还是 null)。
    • 将创建 ServiceBObjectFactory 放入 **三级缓存 (singletonFactories)**。
  4. 填充 ServiceB 的属性:
    • 容器准备为 serviceA 属性赋值。
    • 发现需要注入 ServiceA,于是去获取 ServiceA
  5. 获取 ServiceA(关键步骤):
    • singletonObjects (一级缓存) 中未找到完全初始化的 ServiceA
    • earlySingletonObjects (二级缓存) 中未找到 ServiceA 的早期引用。
    • singletonFactories (三级缓存) 中找到了 ServiceAObjectFactory
    • 调用 ObjectFactory.getObject() -> 实际调用 getEarlyBeanReference()
      • 如果 ServiceA 需要 AOP 代理,则在此刻生成代理对象并返回。
      • 如果不需要,则返回原始 ServiceA 对象。
    • 将这个(可能是代理的)早期对象放入 **二级缓存 (earlySingletonObjects)**,并从三级缓存中移除对应的 ObjectFactory
    • 将这个早期对象注入到 ServiceBserviceA 属性中。至此,ServiceB 的属性填充完成。
  6. 完成 ServiceB 的初始化:
    • 执行 ServiceB 的初始化方法(@PostConstruct 等)。
    • 将完全初始化好的 ServiceB 放入 **一级缓存 (singletonObjects)**,并从二级和三级缓存中移除。
  7. 回到填充 ServiceA 的属性:
    • 容器现在获取到了完全初始化好的 ServiceB(它已经在步骤 6 中被放入一级缓存)。
    • ServiceB 注入到 ServiceAserviceB 属性中。至此,ServiceA 的属性填充完成。
  8. 完成 ServiceA 的初始化:
    • 执行 ServiceA 的初始化方法(@PostConstruct 等)。
    • 将完全初始化好的 ServiceA 放入 **一级缓存 (singletonObjects)**,并从二级缓存中移除(它之前在步骤 5 被放入了二级缓存)。

至此,循环依赖成功解决! ServiceA 持有完整的 ServiceBServiceB 持有完整的 ServiceA(或 ServiceA 的代理对象)。

解决条件与限制

Spring只能解决特定条件下的循环依赖:

  • 必须是单例Bean:原型(prototype)作用域的Bean无法解决
  • 不能是构造器注入:仅支持属性注入或setter注入
  • 需要Spring管理:不能是new创建的对象

当遇到构造器注入的循环依赖时,Spring会直接抛出BeanCurrentlyInCreationException异常。

为什么要二级缓存, 一级缓存能实现吗?

理论上可以的, 只要在设计一级缓存的过程中能准确的标识当前 bean 是处于完成状态还是半成品状态即可; 但是如果通过二级缓存, 可以简化代码开发, 并且能提高每次的判断执行效率, 所以引入二级缓存

为什么用三级缓存解决循环依赖问题?用二级缓存不行吗?

如果不引入三级缓存的话会造成一下问题, 如果 B 通过二级缓存将 A 的值进行填充了,那么 B 中 A 的对象就是 A 的原始 bean 对象; 因为 bean 的生命周期中 bean 的字段填充是发生在初始化之前的, 所以当 A 进行后续操作中被代理了功能得到增强了, 那么 B 中的 A 字段是无法被更新和感知的; 所以引入三级缓存的概念, 如果 A 被代理了, 那么在 B 在进行赋值的时候就可以将代理提前。

举个栗子

  • 假设只有两级缓存(仅保留一级和二级缓存)

    1. 实例化 BeanA → 直接放入二级缓存earlySingletonObjects
    2. 填充 BeanA 属性时发现需要 BeanB
    3. 实例化 BeanB → 直接放入二级缓存
    4. 填充 BeanB 属性时发现需要 BeanA → 从二级缓存取出 BeanA 的原始对象
    5. 将 BeanA 注入 BeanB
    6. 完成 BeanB 初始化 → 移入一级缓存
    7. 继续填充 BeanA → 注入已完成的 BeanB
    8. 完成 BeanA 初始化 → 移入一级缓存

    表面看似乎可行?但遇到 AOP 代理时会暴露致命缺陷!

    如果BeanA需要被动态代理(比如加了 @Transactional),如果没有三级缓存,当填充BeanB的属性BeanA时,拿到的是 A 的原始对象。Spring 需要在初始化阶段之后生成代理对象A ;会导致:

    sequenceDiagram
        participant C as Spring容器
        participant A as BeanA原始对象
        participant P as BeanA代理对象
    
        C->>A: 1. 实例化BeanA(原始对象)
        C->>C: 2. 将原始BeanA存入二级缓存
        C->>A: 3. 填充属性(需要BeanB)...
        ## 创建BeanB并注入原始BeanA ...
    
        C->>A: 4. 执行BeanA初始化
        C->>P: 5. 【问题点】此时才生成代理对象
        C->>C: 6. 将代理对象放入一级缓存
    • 代理对象失效:循环依赖时,其他 BeanB 注入的是原始 BeanA 实例,而非代理对象,AOP 增强逻辑无法生效。

    • 代理时机错误:代理对象应在 BeanA初始化完成后生成,若提前存入二级缓存,会破坏初始化流程。

  • 三级缓存的解决方案:

    三级缓存存储 ObjectFactory,其 getObject() 方法可延迟生成代理对象(调用 getEarlyBeanReference() 方法)。当发生循环依赖时,通过工厂生成代理对象并放入二级缓存,确保其他 Bean 注入的是代理后的实例。

@Lazy 能解决循环依赖吗?

  • @Lazy的作用原理

    @Lazy注解的核心作用是延迟加载 Bean 的实例化过程,即当真正需要用到该Bean的时候才去初始化。当应用于循环依赖场景时:

    • 延迟依赖注入:将依赖对象包装为Proxy(代理对象),在实际使用时才创建真实实例。
    • 打破初始化死锁:避免两个 Bean 在初始化阶段互相等待对方实例化完成。
  • @Lazy能解决的场景

    1. 构造器注入的循环依赖(Spring 默认无法解决)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      @Service
      public class A {
      private final B b;

      @Autowired
      public A(@Lazy B b) { // 使用@Lazy延迟加载B
      this.b = b;
      }
      }

      @Service
      public class B {
      private final A a;

      @Autowired
      public B(@Lazy A a) { // 使用@Lazy延迟加载A
      this.a = a;
      }
      }

      原理:注入时实际保存的是B的代理对象(而非真实实例),A可以顺利完成初始化。当A首次使用B时,才会触发B的真实实例化。

    2. 单例与原型 Bean 的循环依赖

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      @Service
      @Scope("prototype")
      public class A {
      @Autowired
      private B b;
      }

      @Service
      public class B {
      @Autowired
      private A a;
      }

      问题:原型 Bean 不支持三级缓存,默认无法解决循环依赖。

      解决方法

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Service
      @Scope("prototype")
      public class A {
      @Autowired
      private B b;
      }

      @Service
      public class B {
      @Autowired
      @Lazy
      private A a;
      }
  • 能解决:通过延迟加载和代理对象,@Lazy 可以解决构造器注入的循环依赖,以及非单例 Bean 的循环依赖。

  • 不能解决:如果循环依赖的 Bean 在初始化阶段就需要真实依赖对象(而非代理),@Lazy 无法解决问题,此时必须重构代码。

AOP

说说什么是 AOP?

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,AOP 就是把一些业务逻辑中的相同代码抽取到一个独立的模块中,让业务逻辑更加清爽。简单的把 AOP 理解为贯穿于方法之中,在方法执行前、执行时、执行后、返回值后、异常后要执行的操作。

它的核心是 “将程序中跨多个模块的通用功能(比如日志记录、事务管理、权限校验等)从业务逻辑中抽离出来,单独维护”,实现 “业务逻辑与通用功能的解耦”。

核心概念

  • 切面(Aspect)

    • 被抽离出来的通用功能模块,包含 “要增强的逻辑” 和 “在哪里增强”。可以认为是切入点(Pointcut)+通知(Advice)。

    • 示例:日志记录、权限校验、事务管理等。

    • @Aspect
      public class LoggingAspect {
        @Before("execution(* com.example.service.*.*(..))")
        public void logBefore() {
            System.out.println("Logging before method execution");
        }
      }
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17

      - **连接点(Join Point)**

      - 程序执行过程中**可插入切面的点**,例如方法调用、异常处理等,AOP允许在这些点上插入切面逻辑。在 Spring AOP 中,仅支持方法级别的连接点。
      - **示例**:某个 Service 方法被调用时、Controller 返回响应前。

      - **切入点(Pointcut)**

      - 定义 “**哪些连接点需要被增强**”,筛选出真正需要应用切面的位置。通常使用 @Pointcut 注解来定义切点表达式。

      - 举个栗子:

      ```java
      // 匹配所有UserService类的方法
      @Pointcut("execution(* com.example.service.UserService.*(..))")
      // 匹配所有带@Log注解的方法
      @Pointcut("@annotation(com.example.annotation.Log)")
  • 通知(Advice)

    指拦截到目标对象的连接点之后要做的事情,也可以称作切面中具体的增强逻辑

    • 前置通知(Before):目标方法执行前。
    • 后置通知(After):目标方法执行后(无论是否异常)。
    • 返回通知(AfterReturning):目标方法正常返回后。
    • 异常通知(AfterThrowing):目标方法抛出异常时。
    • 环绕通知(Around):包裹目标方法,可自定义执行时机。
  • 目标对象(Target)

    就是被代理的对象,或者叫被切面增强的对象(即包含核心业务逻辑的类)。

  • 代理(Proxy)

    AOP 通过动态代理技术生成 “增强后的对象”。

    • 两种代理方式:
      • JDK 动态代理:基于接口,代理对象实现目标接口。
      • CGLIB 代理:基于继承,代理对象继承目标类。
  • 织入(Weaving)

切面逻辑插入目标对象,生成代理对象的过程。

  • 发生时机:
    • 编译时:如 AspectJ 编译时织入。
    • 类加载时:如使用 LoadTimeWeaver。
    • 运行时:如 Spring AOP 的动态代理。

AOP的使用场景有哪些?

AOP 的使用场景有很多,比如说日志记录、事务管理、权限控制、性能监控等。

  • 日志记录

    1. 添加 Maven 依赖

      1
      2
      3
      4
      <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
      </dependency>
    2. 定义日志注解(用于标记需要记录的方法)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      import java.lang.annotation.*;

      @Target(ElementType.METHOD)
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      public @interface Loggable {
      String value() default ""; // 自定义操作描述
      boolean recordParams() default true; // 是否记录参数
      boolean recordResult() default true; // 是否记录返回值
      }
    3. 实现日志切面

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      import org.aspectj.lang.JoinPoint;
      import org.aspectj.lang.ProceedingJoinPoint;
      import org.aspectj.lang.annotation.*;
      import org.aspectj.lang.reflect.MethodSignature;
      import org.slf4j.Logger;
      import org.slf4j.LoggerFactory;
      import org.springframework.stereotype.Component;
      import java.util.Arrays;

      @Aspect
      @Component
      public class LoggingAspect {
      private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

      // 切入点:所有带有自定义@Loggable注解的方法为切点
      @Pointcut("@annotation(com.example.demo.Loggable)")
      public void loggableMethods() {}

      // 环绕通知(最灵活,可控制方法执行)
      @Around("loggableMethods()")
      public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
      MethodSignature signature = (MethodSignature) joinPoint.getSignature();
      Loggable loggable = signature.getMethod().getAnnotation(Loggable.class);

      // 方法开始日志
      long startTime = System.currentTimeMillis();
      logMethodStart(joinPoint, loggable);

      try {
      // 执行目标方法
      Object result = joinPoint.proceed();

      // 方法结束日志
      long executionTime = System.currentTimeMillis() - startTime;
      logMethodEnd(joinPoint, loggable, result, executionTime);

      return result;
      } catch (Exception e) {
      // 异常日志
      long executionTime = System.currentTimeMillis() - startTime;
      logException(joinPoint, loggable, e, executionTime);
      throw e;
      }
      }

      private void logMethodStart(JoinPoint joinPoint, Loggable loggable) {
      String methodName = joinPoint.getSignature().toShortString();
      String description = loggable.value().isEmpty() ? methodName : loggable.value();

      StringBuilder logMessage = new StringBuilder();
      logMessage.append("▶️ 开始执行: ").append(description);

      if (loggable.recordParams()) {
      Object[] args = joinPoint.getArgs();
      logMessage.append(" | 参数: ").append(Arrays.toString(args));
      }

      logger.info(logMessage.toString());
      }

      private void logMethodEnd(JoinPoint joinPoint, Loggable loggable, Object result, long executionTime) {
      String methodName = joinPoint.getSignature().toShortString();
      String description = loggable.value().isEmpty() ? methodName : loggable.value();

      StringBuilder logMessage = new StringBuilder();
      logMessage.append("✅ 执行完成: ").append(description)
      .append(" | 耗时: ").append(executionTime).append("ms");

      if (loggable.recordResult() && result != null) {
      logMessage.append(" | 结果: ").append(result.toString());
      }

      logger.info(logMessage.toString());
      }

      private void logException(JoinPoint joinPoint, Loggable loggable, Exception e, long executionTime) {
      String methodName = joinPoint.getSignature().toShortString();
      String description = loggable.value().isEmpty() ? methodName : loggable.value();

      String errorMsg = String.format("❌ 执行异常: %s | 耗时: %dms | 异常: %s - %s",
      description, executionTime, e.getClass().getSimpleName(), e.getMessage());

      logger.error(errorMsg, e);
      }
      }
    4. 在使用的地方加上自定义注解

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      @Service
      public class UserService {

      @Loggable(value = "创建用户", recordParams = true, recordResult = true)
      public User createUser(String name, String email) {
      // 业务逻辑
      return new User(name, email);
      }

      @Loggable("获取用户列表")
      public List<User> getUsers() {
      // 业务逻辑
      return Arrays.asList(new User("Alice", "alice@example.com"));
      }

      @Loggable(value = "删除用户", recordResult = false)
      public void deleteUser(Long id) {
      // 业务逻辑
      if (id <= 0) throw new IllegalArgumentException("无效的用户ID");
      }
      }
    5. 日志输出示例

      • 正常执行日志

        1
        2
        ▶️ 开始执行: 创建用户 | 参数: [John Doe, john@example.com]
        ✅ 执行完成: 创建用户 | 耗时: 42ms | 结果: User{name='John Doe', email='john@example.com'}
      • 异常执行日志

        1
        2
        ▶️ 开始执行: 删除用户 | 参数: [-1]
        ❌ 执行异常: 删除用户 | 耗时: 5ms | 异常: IllegalArgumentException - 无效的用户ID
  • 事务管理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    package com.example.aop.transaction;

    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.AfterThrowing;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;

    /**
    * 事务切面,使用AOP控制事务边界
    */
    @Aspect
    @Component
    public class TransactionAspect {

    @Autowired
    //负责事务的底层操作,包括获取连接、开启事务、提交 / 回滚事务等。
    private TransactionManager transactionManager;

    // 定义切入点:匹配所有Service包下的方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceMethods() {}

    // 环绕通知:控制事务的开启、提交和回滚
    @Around("serviceMethods()")
    public Object around(JoinPoint joinPoint) throws Throwable {
    try {
    // 开启事务
    transactionManager.beginTransaction();

    // 执行目标方法
    Object result = joinPoint.proceed();

    // 提交事务
    transactionManager.commit();

    return result;
    } catch (Exception e) {
    // 回滚事务
    transactionManager.rollback();
    throw e;
    }
    }

    // 异常通知:当方法抛出异常时回滚事务
    @AfterThrowing(pointcut = "serviceMethods()", throwing = "ex")
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
    transactionManager.rollback();
    }
    }

说说 JDK 动态代理和 CGLIB 代理?

AOP 是通过动态代理实现的,代理方式有两种:JDK 动态代理和 CGLIB 代理。

Spring AOP 默认优先使用 JDK 动态代理,当目标类 没有实现接口 时,会自动切换为 CGLIB 代理。如果希望强制使用 CGLIB 代理(即使目标类有接口),可以通过配置开启(如 @EnableAspectJAutoProxy(proxyTargetClass = true))。

特性 JDK 动态代理 CGLIB 动态代理
依赖接口 必须实现接口 无需接口,基于继承
代理方式 生成实现接口的代理类 生成继承目标类的子类
性能 创建快,调用慢(反射) 创建慢,调用快(字节码)
适用场景 接口导向的框架(如 Spring AOP) 无接口的类(如 Spring 的 @Service)

说说 Spring AOP 和 AspectJ 的 区别?

  1. 定位不同
    • Spring AOP:Spring 框架的轻量级 AOP 实现,依赖 Spring IoC 容器,仅支持方法级别的增强。
    • AspectJ:独立的、功能完整的 AOP 框架,支持 方法调用、字段访问、构造函数、静态初始化 等各种连接点(Join Point),提供更强大的切面能力。
  2. 实现原理不同
    • Spring AOP运行时基于代理(Proxy),通过 JDK 动态代理(接口代理)或 CGLIB(类代理)生成代理对象,在方法调用时插入切面逻辑。
    • AspectJ编译期或类加载期织入字节码,直接修改目标类的字节码,将切面逻辑嵌入到目标方法中,无需代理对象。
  3. 功能范围不同
    • Spring AOP:仅支持 5 种通知类型(Before、After、AfterReturning、AfterThrowing、Around)和简单的切入点表达式(如 execution@annotation)。
    • AspectJ:支持完整的 AspectJ 语法,包括 引介增强(@DeclareParents)、各种切入点类型(如 callgetset)及更灵活的切面实例化模型(如 perthispertarget)。
  4. 性能不同
    • Spring AOP:运行时代理生成有一定开销,但多次调用后趋于稳定,适合大多数业务场景。
    • AspectJ:编译期织入无运行时开销,性能更优,适合对性能敏感的高频调用场景。
  5. 应用场景不同
    • Spring AOP:适用于 Spring 项目中的简单切面需求(如日志、事务、权限校验),与 Spring 生态无缝集成。
    • AspectJ:适用于 复杂切面需求(如拦截字段访问、静态方法)或 非 Spring 项目(如独立 Java 应用、Android 开发)。

Spring MVC

什么是Spring MVC?

Spring MVC 是 Spring 框架的一个模块,用于构建基于 Java 的 Web 应用程序。它遵循 MVC(Model-View-Controller)设计模式,将 Web 应用划分为三个核心组件:模型(Model)视图(View)控制器(Controller),以实现关注点分离,提高代码可维护性和可测试性。

Spring MVC的核心组件

  1. DispatcherServlet(前端控制器):作为请求的统一入口,接收所有 HTTP 请求并分发给相应的处理组件进行处理,并根据处理结果将响应返回给客户端。

  2. HandlerMapping(处理器映射器):确定哪个 Controller (或 Handler) 应该处理传入的请求。

    它维护了一个请求 URL(或请求方法、参数等条件)到具体 Controller 方法的映射关系。DispatcherServlet 查询 HandlerMapping 来找到匹配当前请求的 Handler(通常是一个 Controller 的方法)和可选的拦截器链。常见的实现如 RequestMappingHandlerMapping (基于 @RequestMapping 注解)。

  3. HandlerAdapter(处理器适配器):负责实际调用找到的 Handler (通常是 Controller 方法) 来处理请求,并且适配不同类型的处理器。。

    因为 Handler 的实现方式可以多种多样(如基于 @Controller 注解的类、实现了 Controller 接口的旧式类、HttpRequestHandler 等),DispatcherServlet 需要一种统一的方式来调用它们。HandlerAdapter 屏蔽了不同 Handler 实现细节的差异,提供了统一的调用接口。常见的实现如 RequestMappingHandlerAdapter (用于调用 @RequestMapping 注解的方法)。

  4. **Controller / Handler (处理器/控制器)**:处理具体的请求。它接收 DispatcherServlet 通过 HandlerAdapter 转发的请求,处理用户输入(解析参数、表单、请求体等),调用服务层进行业务处理,并返回一个包含模型数据和逻辑视图名的 ModelAndView 对象(或其它类型的返回值,如 ResponseEntity, String 视图名等)。通常使用 @Controller@RestController 注解标记。

  5. ModelAndView(模型与视图):封装处理结果,包含模型数据(Model)和视图名称(View)。

    装载了模型数据和视图信息,作为 Handler 的处理结果,返回给 DispatcherServlet。

  6. ViewResolver(视图解析器): 将 Controller 返回的逻辑视图名解析为实际的 View 对象。

    Controller 通常返回一个字符串(如 "userList""redirect:/success"),这只是一个逻辑标识。ViewResolver 根据配置的规则(如视图前缀、后缀)将这个逻辑名映射到具体的视图技术实现(如 JSP 文件 /WEB-INF/views/userList.jsp, Thymeleaf 模板, FreeMarker 模板等)。常见的实现如 InternalResourceViewResolver (用于 JSP/JSTL)。

  7. View(视图):负责渲染最终的响应内容(通常是 HTML)。

    它使用 Model 中的数据,结合特定的视图技术(JSP, Thymeleaf, FreeMarker, PDF, JSON 等)来生成具体的输出(HTML, JSON, XML, PDF 等),并将其写入 HTTP 响应流。DispatcherServlet 将渲染任务委托给 View 对象。

  8. HandlerInterceptor(拦截器):在请求处理前后执行自定义逻辑(如日志记录、权限验证)。

  9. ExceptionHandler(异常处理器):统一处理控制器抛出的异常。

  10. MultipartResolver(文件上传解析器):处理文件上传请求。

Spring MVC 的工作原理

image-20250729140513051
  1. 请求到达:用户发送 HTTP 请求到 Web 服务器。
  2. DispatcherServlet 接管:请求被配置好的 DispatcherServlet 捕获。
  3. 查找 HandlerDispatcherServlet 查询 HandlerMapping,根据请求 URL 等信息找到对应的 Handler (Controller 方法) 和拦截器链。
  4. 执行拦截器前置处理:如果配置了拦截器 (HandlerInterceptor),执行其 preHandle 方法。
  5. 适配并执行 HandlerDispatcherServlet 通过合适的 HandlerAdapter 调用找到的 Handler (Controller 方法)。
  6. Controller 处理: Controller 执行业务逻辑:
    • 处理请求参数、表单数据、请求体等。
    • 调用 Service 层。
    • 将结果数据放入 Model
    • 返回一个结果(逻辑视图名 String, ModelAndView, ResponseEntity 等)。
  7. 执行拦截器后置处理:如果配置了拦截器,执行其 postHandle 方法(此时 Model 已填充)。
  8. 解析视图:DispatcherServletController 返回的逻辑视图名交给 ViewResolver 解析,得到具体的 View 对象。
  9. 渲染视图:DispatcherServlet 调用 View 对象的 render() 方法,传入 Model 数据。View 使用模型数据和特定技术生成响应内容(如 HTML)。
  10. 执行拦截器最终处理:如果配置了拦截器,执行其 afterCompletion 方法(无论成功或异常)。
  11. 返回响应: 生成的响应内容通过 DispatcherServlet 返回给 Web 服务器,最终发送给客户端。

事务

Spring 事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,Spring 是无法提供事务功能的。Spring 只提供统一事务管理接口,具体实现都是由各数据库自己实现,数据库事务的提交和回滚是通过数据库自己的事务机制实现。

Spring 管理事务的方式有几种?

Spring 管理事务的方式主要分为两大类:声明式事务编程式事务声明式事务首选和主流的方式,因为它更简洁、非侵入性且符合 Spring 的 AOP 理念。编程式事务则提供了更细粒度的控制,但代码侵入性强。

  1. 声明式事务(Declarative Transaction Management)

    通过 AOP 实现事务管理,将事务逻辑与业务代码分离,降低耦合度。其本质是通过 AOP 功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在目标方法执行完之后根据执行情况提交或者回滚事务。

    实现方式

    • 基于 @Transactional 注解(最常用)

      • 原理:利用 Spring AOP 动态代理在方法执行前后添加事务逻辑。

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        @Configuration
        @EnableTransactionManagement // 启用事务注解支持
        public class AppConfig {
        @Bean
        public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource); // 配置事务管理器
        }
        }

        @Service
        public class UserService {
        @Transactional(
        propagation = Propagation.REQUIRED, // 事务传播行为
        isolation = Isolation.DEFAULT, // 隔离级别
        rollbackFor = Exception.class, // 触发回滚的异常
        timeout = 5 // 超时时间(秒)
        )
        public void updateUser(User user) {
        // 业务逻辑(自动被事务包裹)
        }
        }
    • 基于 XML 配置(旧项目常用)

      • 在 XML 中定义事务规则,无需修改源码:

      • <!-- 配置事务管理器 -->
        <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"/>
        </bean>
        
        <!-- 定义事务规则 -->
        <tx:advice id="txAdvice" transaction-manager="txManager">
            <tx:attributes>
                <tx:method name="update*" propagation="REQUIRED" rollback-for="Exception"/>
                <tx:method name="get*" read-only="true"/>
            </tx:attributes>
        </tx:advice>
        
        <!-- 通过 AOP 应用事务 -->
        <aop:config>
            <aop:advisor advice-ref="txAdvice" 
                pointcut="execution(* com.example.service.*.*(..))"/>
        </aop:config>
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        30



        2. 编程式事务(Programmatic Transaction Management)

        通过编写代码直接管理事务的生命周期,灵活性高但代码侵入性强。编程式事务可以使用 TransactionTemplate 和 PlatformTransactionManager 来实现,需要显式执行事务。允许我们在代码中直接控制事务的边界,通过编程方式明确指定事务的开始、提交和回滚。

        实现方式

        - **使用 `TransactionTemplate`(推荐编程式方案)**

        ```java
        @Service
        public class OrderService {
        @Autowired
        private TransactionTemplate transactionTemplate;

        public void createOrder(Order order) {
        transactionTemplate.execute(status -> {
        try {
        orderDao.save(order);
        inventoryService.deductStock(order.getProductId());
        return null; // 返回结果(可自定义)
        } catch (Exception e) {
        status.setRollbackOnly(); // 标记回滚
        throw e;
        }
        });
        }
        }
    • 直接使用 PlatformTransactionManager(底层 API)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      @Service
      public class PaymentService {
      @Autowired
      private PlatformTransactionManager txManager;

      public void processPayment() {
      TransactionDefinition def = new DefaultTransactionDefinition();
      TransactionStatus status = txManager.getTransaction(def);
      try {
      paymentDao.executePayment();
      txManager.commit(status); // 提交事务
      } catch (Exception e) {
      txManager.rollback(status); // 回滚事务
      throw e;
      }
      }
      }

Spring 事务的隔离级别

Spring 事务的隔离级别(Isolation Level)是数据库事务处理中的核心概念,用于控制多个并发事务对同一数据的访问规则,解决脏读、不可重复读、幻读等问题。Spring 通过 @Transactional 注解的 isolation 属性支持标准的事务隔离级别,其底层依赖于数据库的实现。

1
@Transactional(isolation = Isolation.READ_COMMITTED)
隔离级别 脏读 (Dirty Read) 不可重复读 (Non-Repeatable Read) 幻读 (Phantom Read) 适用场景 性能影响
READ_UNCOMMITTED (读未提交) ❌ 可能发生 ❌ 可能发生 ❌ 可能发生 极低并发场景(几乎不用 最高
READ_COMMITTED (读已提交) ✅ 禁止 ❌ 可能发生 ❌ 可能发生 多数数据库默认级别(如 Oracle) 较高
REPEATABLE_READ (可重复读) ✅ 禁止 ✅ 禁止 ❌ 可能发生 MySQL InnoDB 的默认级别 中等
SERIALIZABLE (串行化) ✅ 禁止 ✅ 禁止 ✅ 禁止 高一致性要求场景(如金融交易) 最低
DEFAULT (默认) - - - 使用数据库的默认隔离级别(推荐 依赖数据库
  1. READ_UNCOMMITTED (读未提交)

    • 问题:事务能读取其他事务未提交的数据(脏读)。

    • 场景

      1
      2
      3
      4
      5
      -- 事务A
      UPDATE account SET balance = 1000 WHERE id = 1; -- 未提交

      -- 事务B(READ_UNCOMMITTED)
      SELECT balance FROM account WHERE id = 1; -- 读到1000(脏数据)
    • 风险:事务A若回滚,事务B读到的数据就是无效的。

  2. READ_COMMITTED (读已提交)

    • 解决:禁止脏读(只读已提交数据)。

    • 遗留问题:不可重复读(同一事务内多次读同一数据结果不同)。

    • 场景

      1
      2
      3
      4
      5
      6
      7
      8
      9
      -- 事务B(第一次查询)
      SELECT balance FROM account WHERE id = 1; -- 返回1000

      -- 事务A提交更新
      UPDATE account SET balance = 900 WHERE id = 1;
      COMMIT;

      -- 事务B(第二次查询)
      SELECT balance FROM account WHERE id = 1; -- 返回900(两次读取结果不一致!)
  3. REPEATABLE_READ (可重复读)

    • 解决:禁止脏读、不可重复读。

    • 遗留问题:幻读(同一查询条件返回的行数变化)。

    • 场景(MySQL InnoDB 通过 MVCC 解决了幻读):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      -- 事务B(第一次查询)
      SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 返回5条

      -- 事务A插入新订单并提交
      INSERT INTO orders(user_id) VALUES (1);
      COMMIT;

      -- 事务B(第二次查询)
      SELECT COUNT(*) FROM orders WHERE user_id = 1; -- 仍返回5条(避免幻读)
  4. SERIALIZABLE (串行化)

    • 解决:所有并发问题(通过强制事务串行执行)。

    • 代价:性能严重下降(类似表级锁)。

    • 场景

      1
      2
      3
      4
      5
      -- 事务B执行查询(自动加共享锁)
      SELECT * FROM account WHERE id = 1;

      -- 事务A尝试更新(被阻塞,直到事务B结束)
      UPDATE account SET balance = 800 WHERE id = 1;

Spring 事务的传播机制

Spring 事务的传播机制定义了 当一个事务方法被另一个事务方法调用时,如何管理事务的创建、嵌套和合并。这是 Spring 事务管理中最复杂且强大的特性之一,通过 @Transactional(propagation = Propagation.XXX) 枚举类提供 7 种传播行为。

传播行为 是否创建新事务 当前无事务时 异常影响范围 应用场景
REQUIRED 否(加入现有) 创建新事务 整个事务回滚 常规业务逻辑
SUPPORTS 非事务执行 无事务,异常不触发回滚 查询操作
MANDATORY 抛出异常 整个事务回滚 强制要求事务的敏感操作
REQUIRES_NEW 创建新事务 仅影响当前事务 日志记录、独立子操作
NOT_SUPPORTED 非事务执行 无事务,异常不触发回滚 非事务的性能敏感操作
NEVER 非事务执行 存在事务时抛出异常 禁止事务的操作
NESTED 否(嵌套事务) 创建新事务(同 REQUIRED 嵌套事务回滚不影响外层,外层回滚嵌套也回滚 子操作可独立回滚的场景
  1. REQUIRED(默认)

    • 规则

      • 若当前存在事务,则加入该事务;
      • 若不存在事务,则新建一个事务
    • 场景

      1
      2
      3
      4
      5
      6
      7
      @Transactional(propagation = Propagation.REQUIRED)
      public void methodA() {
      methodB(); // 加入 methodA 的事务
      }

      @Transactional(propagation = Propagation.REQUIRED)
      public void methodB() { ... }
    • 用途最常用的传播行为(如订单创建+库存扣减需在同一事务)。

  2. SUPPORTS

    • 规则

      • 若当前存在事务,则加入;
      • 若不存在事务,则以非事务方式执行
    • 场景:查询方法(支持事务但不强制)。

      1
      2
      @Transactional(propagation = Propagation.SUPPORTS)
      public User getUserById(Long id) { ... }
    • 应用场景:查询方法(无需事务,但可参与外层事务)。

  3. MANDATORY

    • 规则

      • 若当前存在事务,则加入;
      • 若不存在事务,则抛出异常IllegalTransactionStateException)。
    • 场景:强制要求调用方必须开启事务(如资金操作)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Transactional
      public void methodA() {
      methodB(); // 正确:B 加入 A 的事务
      }

      public void methodC() {
      methodB(); // 错误:抛出异常,因为 C 无事务
      }

      @Transactional(propagation = Propagation.MANDATORY)
      public void methodB() {
      // 必须在事务中执行
      }
  4. REQUIRES_NEW

    • 规则

      • 无论当前是否存在事务,都新建一个独立事务
      • 原事务(若有)被挂起,新事务执行完毕后再恢复。
    • 场景:日志记录(即使主事务失败,日志仍需提交)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Transactional
      public void methodA() {
      try {
      methodB(); // B 创建新事务,A 的事务挂起
      } catch (Exception e) {
      // B 的异常不影响 A 的事务
      }
      }

      @Transactional(propagation = Propagation.REQUIRES_NEW)
      public void methodB() {
      // 独立事务
      }
  5. NOT_SUPPORTED

    • 规则

      • 以非事务方式执行
      • 若当前存在事务,则挂起该事务。
    • 场景:执行耗时操作(如文件处理),避免长事务阻塞。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      @Transactional
      public void methodA() {
      methodB(); // A 的事务挂起,B 以非事务方式执行
      }

      @Transactional(propagation = Propagation.NOT_SUPPORTED)
      public void methodB() {
      // 非事务执行
      }
  6. NEVER

    • 规则

      • 强制要求不在事务中执行
      • 若当前存在事务,则抛出异常。
    • 场景:与事务不兼容的操作(如某些外部 API 调用)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      public void methodA() {
      methodB(); // 正确:A 无事务
      }

      @Transactional
      public void methodC() {
      methodB(); // 错误:抛出异常,因为 C 有事务
      }

      @Transactional(propagation = Propagation.NEVER)
      public void methodB() {
      // 禁止在事务中执行
      }
  7. NESTED

    • 规则

      • 若当前存在事务,则在嵌套事务中执行(使用保存点 Savepoint);
      • 若不存在事务,则同 REQUIRED(新建事务)。
    • 特点

      • 嵌套事务回滚不影响主事务(除非主事务提交);
      • 主事务回滚会导致嵌套事务一起回滚。
    • 场景:复杂业务中的子操作(如订单拆分子订单,子订单失败不影响主订单)。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      @Transactional
      public void methodA() {
      try {
      methodB(); // 嵌套事务
      } catch (Exception e) {
      // B 回滚,但 A 可以继续执行或提交
      }
      }

      @Transactional(propagation = Propagation.NESTED)
      public void methodB() {
      // 嵌套事务,使用保存点
      }

spring 声明式事务在哪些情况下会失效?

  1. 方法的访问权限问题

    若事务方法的访问权限是 privateprotected 或者默认的(包级私有),或者 final/static方法,事务就会失效。因为Spring 默认使用基于 JDK 的动态代理(当接口存在时)或基于 CGLIB 的代理(当只有类时)来实现事务。

  2. 同类内部方法调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Service
    public class UserService {

    public void saveUser(User user) {
    // 非事务方法调用事务方法
    this.updateUser(user);
    }

    @Transactional
    public void updateUser(User user) {
    // 数据库操作
    }
    }
    • 原因:Spring 事务基于 AOP 代理,内部调用不走代理。

    • 解决办法

      • 借助 ApplicationContext 来获取代理对象:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        @Service
        public class UserService {

        @Autowired
        private ApplicationContext context;

        public void saveUser(User user) {
        UserService proxy = context.getBean(UserService.class);
        proxy.updateUser(user);
        }

        @Transactional
        public void updateUser(User user) {
        // 数据库操作
        }
        }
      • 通过构造函数注入自身代理:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        @Service
        public class UserService {

        private final UserService self;

        @Autowired
        public UserService(UserService self) {
        this.self = self;
        }

        public void saveUser(User user) {
        self.updateUser(user);
        }

        @Transactional
        public void updateUser(User user) {
        // 数据库操作
        }
        }
  3. 未被 Spring 容器管理

    1
    2
    3
    4
    5
    // ❌ 未加 @Service 注解
    public class ExternalService {
    @Transactional
    public void process() { ... }
    }

    原因:非 Spring Bean 无法被代理。
    解决:确保类被注解标记(@Component, @Service 等)。

  4. 注解未被正确扫描

    @Transactional 注解所在的类没有被 Spring 组件扫描到,或者没有启用事务支持,事务就会失效。

    解决办法
    确保配置了 @EnableTransactionManagement 注解:

    1
    2
    3
    4
    5
    6
    @Configuration
    @EnableTransactionManagement
    @ComponentScan("com.example.service")
    public class AppConfig {
    // 配置类
    }
  5. 异常类型不匹配

    1
    2
    3
    4
    5
    @Transactional
    public void update() throws IOException {
    jdbcTemplate.update(...); // 抛 SQLException(RuntimeException 子类)
    throw new IOException(); // 非 RuntimeException
    }

    原因:若事务方法抛出的异常不在 @Transactional 注解的 rollbackFor( 用来指定能够触发事务回滚的异常类型 ) 范围内,事务将不会回滚。Spring 默认抛出未检查 unchecked 异常(继承自 RuntimeException 的异常)或者 Error 才回滚事务,其他异常不会触发回滚事务。

    解决:显式指定回滚异常

    1
    @Transactional(rollbackFor = Exception.class)
  6. 异常被捕获未抛出

    1
    2
    3
    4
    5
    6
    7
    8
    @Transactional
    public void update() {
    try {
    jdbcTemplate.update(...); // 抛异常
    } catch (DataAccessException e) {
    // 吞掉异常 → 事务不会回滚!
    }
    }

    解决:在 catch 中抛出 RuntimeException 或手动回滚:

    1
    2
    3
    4
    catch (Exception e) {
    TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    throw new BusinessException(e);
    }
  7. 事务传播行为配置有误

    要是事务传播行为配置成 PROPAGATION_SUPPORTSPROPAGATION_NOT_SUPPORTED 或者 PROPAGATION_NEVER,在特定条件下事务会失效。

    1
    2
    3
    4
    @Transactional(propagation = Propagation.NOT_SUPPORTED)
    public void doSomething() {
    // 此方法不会在事务中执行
    }

    解决办法
    依据业务需求,合理选择传播行为,像 PROPAGATION_REQUIRED 就是常用的选择。

    1
    2
    3
    4
    @Transactional(propagation = Propagation.REQUIRED)
    public void doSomething() {
    // 会加入当前事务或者创建新事务
    }
  8. 多线程环境问题

    在多线程环境中,由于每个线程拥有独立的事务上下文,子线程的事务不会影响主线程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Transactional
    public void parentMethod() {
    new Thread(() -> {
    // 子线程中的事务与主线程无关
    childMethod();
    }).start();
    }

    @Transactional
    public void childMethod() {
    // 数据库操作
    }

    解决办法: 尽量避免在多线程中使用事务,或者采用其他方式来保证数据一致性。