相关问题
一、Spring IoC 与 Bean 管理(8 题)
- Spring 框架主要解决了什么问题?什么是 IoC 和 DI?
- Spring IoC 容器是什么?BeanFactory 和 ApplicationContext 有什么区别?
- Spring 容器是如何发现、注册和管理 Bean 的?BeanDefinition 起什么作用?
- Spring Bean 的完整生命周期是什么?
- Spring 如何实现依赖注入?构造器注入、Setter 注入和字段注入有什么区别?
- Spring Bean 有哪些作用域?默认单例 Bean 是线程安全的吗?
- 什么是循环依赖?Spring 为什么需要三级缓存来解决循环依赖?
- BeanPostProcessor 是什么?它在 Bean 生命周期中起什么作用?
二、Spring AOP 与动态代理(5 题)
- 什么是 AOP?Spring 为什么需要 AOP?它和 IoC 有什么关系?
- 什么是动态代理?JDK 动态代理和 CGLIB 有什么区别?
- Spring AOP 的核心概念有哪些?切面、切点、通知分别是什么?
- Spring AOP 的底层实现原理是什么?代理对象是如何创建和执行的?
- Spring AOP 在什么情况下会失效?为什么同类内部方法调用可能导致 AOP 失效?
Spring IoC
在传统的 Java 开发中,对象之间往往存在依赖关系。例如,一个 UserService 需要调用 UserDao 来完成数据库操作,最直接的方式就是在 UserService 内部通过 new 创建一个 UserDao 对象。
|
|
这种方式在简单场景下没有问题,但随着项目规模不断扩大,对象之间的依赖关系也会越来越复杂。如果每个对象都负责创建自己需要的其他对象,就会带来两个主要问题。
首先是对象之间的耦合度过高。例如,UserService 不仅需要知道如何调用 UserDao,还需要知道应该创建哪个具体的 UserDao 实现。如果后续需要替换数据访问层的实现,就可能需要修改 UserService 的代码,使得业务逻辑和对象创建逻辑耦合在一起。
其次是对象的创建和管理越来越复杂。实际项目中,一个对象可能依赖多个对象,而这些对象又依赖其他对象,最终形成复杂的依赖关系。如果全部由开发者手动创建,就需要自行处理对象的创建顺序、依赖关系、初始化以及销毁等问题,增加了开发和维护成本。
因此,一个自然的思路是:能不能把对象的创建和依赖关系管理从业务代码中抽离出来,交给一个统一的容器负责,让业务对象只关注自身的业务逻辑?
这就是 IoC(Inversion of Control,控制反转) 的核心思想。
所谓控制反转,就是将对象创建和依赖管理的控制权,从业务对象自身转移到外部容器。在传统方式中,UserService 主动创建自己依赖的 UserDao;而在 Spring 中,UserService 不再负责创建依赖对象,而是由 Spring IoC 容器统一创建和管理这些对象,并在需要时将依赖提供给它。
例如:
|
|
在这个例子中,UserService 不再通过 new 创建 UserDao,而是通过构造方法接收依赖。在正常的 Spring 组件扫描配置下,Spring 会将这两个类注册为 Bean,并在创建 UserService 时自动提供对应的 UserDao 对象。
这种由外部容器向对象提供依赖的方式,就叫作 DI(Dependency Injection,依赖注入)。
因此,IoC 和 DI 其实是在描述同一套机制的不同方面:IoC 强调的是控制权的转移,即对象的创建和依赖管理由容器负责;DI 强调的是依赖关系如何建立,即容器将对象所需要的依赖注入进去。 DI 是实现 IoC 的一种主要方式。
通过这种设计,业务对象不再需要关心依赖对象的具体创建过程,而只需要声明自己需要哪些依赖。对象的创建、装配和生命周期管理则统一交给 Spring 容器完成,从而降低对象之间的耦合,提高代码的可维护性、可扩展性和可测试性。
Spring IoC 容器
前面我们提到,IoC 的核心思想是将对象的创建和依赖关系管理交给外部容器。那么,Spring 容器到底是什么?它又是如何知道需要创建哪些对象,以及这些对象之间存在什么依赖关系的?
首先,Spring 容器并不是什么特殊的运行环境,本质上就是 Spring 提供的一套用于创建、管理和装配 Java 对象的程序组件。这些由 Spring 容器管理的对象被称为 Bean。容器负责根据配置信息创建 Bean、建立 Bean 之间的依赖关系,并管理它们的初始化、作用域以及生命周期。
不过,容器要管理对象,首先就需要知道应该创建什么对象,以及应该怎样创建。例如,对于一个 UserService,Spring 至少需要知道它对应哪个 Java 类、是否需要创建单例对象、依赖哪些其他 Bean 等信息。
因此,Spring 引入了 BeanDefinition,可以将它理解为 Bean 的定义信息。它并不是实际创建出来的对象,而是 Spring 用来描述 Bean 的一份元数据,其中包含 Bean 的类型、作用域、依赖信息、初始化方法等配置。Spring 会通过组件扫描、@Bean 注解或 XML 配置等方式获取这些信息,并将其转换为 BeanDefinition,注册到容器内部。
这样,Spring 就可以将 Bean 的定义和 Bean 的实例分离开来:BeanDefinition 负责描述应该创建什么样的对象,而容器负责根据这些定义真正创建和管理 Bean。
有了 BeanDefinition 之后,容器就可以按照定义实例化对象,并处理对象之间的依赖关系。例如,Spring 在创建 UserService 时,发现它的构造方法需要一个 UserDao,就会从容器中寻找符合条件的 Bean,并将其注入到 UserService 中。如果对应的单例 Bean 尚未创建,容器也可以根据其 BeanDefinition 先创建这个依赖对象。
当然,Spring 容器并不只是负责执行 new。在对象实例化之后,它还可能进行属性填充、执行初始化回调、调用 BeanPostProcessor 等操作,最终将一个完成初始化的 Bean 提供给应用程序使用。这些步骤共同构成了后面需要讨论的 Bean 生命周期。
那么,Spring 是通过什么接口来提供这些容器能力的?
Spring IoC 容器的基础接口是 BeanFactory。它定义了获取 Bean 等基础能力,例如可以通过 getBean() 从容器中取得一个对象。BeanFactory 的具体实现负责管理 BeanDefinition,并在需要时创建、装配和返回 Bean。因此,BeanFactory 可以理解为 Spring IoC 容器最基础的抽象。
但是,实际开发中我们通常使用的并不是直接面向 BeanFactory 的容器,而是 ApplicationContext。
ApplicationContext 在 BeanFactory 的基础上提供了更完整的应用级功能,例如事件发布、国际化资源访问、环境与配置管理等。同时,常见的 ApplicationContext 实现通常会在容器启动过程中预先创建非懒加载的单例 Bean,而不是等到第一次调用 getBean() 时才创建。因此,ApplicationContext 更适合作为实际 Spring 应用的容器入口。
需要注意,BeanFactory 和 ApplicationContext 并不是两个完全独立的容器体系。ApplicationContext 本身继承了 BeanFactory 接口,在提供基础 Bean 管理能力的同时扩展了更多功能。而具体的 ApplicationContext 实现通常会借助内部的 BeanFactory 来完成 BeanDefinition 注册和 Bean 创建等底层工作。
因此,可以将 Spring IoC 容器的基本工作过程理解为:
|
|
这里描述的是一个简化后的整体流程,实际 Spring 容器还涉及 BeanFactoryPostProcessor、BeanPostProcessor、Bean 作用域、循环依赖等机制,而且不同 Bean 的创建时机也可能不同。
所以,Spring IoC 容器的本质,是一个以 BeanDefinition 为基础,负责 Bean 创建、依赖注入和生命周期管理的对象管理系统。BeanFactory 定义了容器最基础的能力,而 ApplicationContext 在此基础上提供了更完整的应用级功能。
Bean 的生命周期
前面我们已经知道,Spring IoC 容器会根据 BeanDefinition 创建和管理 Bean,并负责处理对象之间的依赖关系。但这里还有一个问题:Spring 创建 Bean,为什么不直接通过 new 得到对象,而是需要经过一套完整的生命周期?
因为在实际开发中,创建一个对象并不意味着这个对象已经可以正常使用。例如,一个 UserService 可能需要依赖 UserDao,还可能需要在创建后完成一些初始化操作,甚至需要被 Spring AOP 包装成代理对象。如果只是简单地执行 new UserService(),就无法自动完成这些工作。
因此,Spring 将 Bean 从创建到销毁的过程划分为多个阶段,在不同阶段完成不同的工作,这就是 Bean 的生命周期。
对于一个普通的单例 Bean,可以将它的生命周期概括为:
|
|
首先是实例化(Instantiation)。Spring 根据 BeanDefinition 中的定义,选择合适的构造方法,通过反射等机制创建 Bean 对象。这个阶段主要解决的是“对象如何被创建”的问题。需要注意,实例化只是得到了一个 Java 对象,并不代表所有依赖都已经注入,也不代表初始化已经完成。
接下来是属性填充(Populate Properties)。如果 Bean 需要通过字段或 Setter 注入其他对象,Spring 就会解析这些依赖,并从容器中找到对应的 Bean 完成注入。例如,UserService 需要 UserDao,Spring 就会找到对应的 UserDao Bean,并将其注入到 UserService 中。对于构造器注入,依赖则需要在实例化之前先解析并作为构造参数传入,因此不能简单认为所有依赖注入都发生在实例化之后。
完成依赖注入之后,Spring 还会处理一些容器相关的回调。例如,如果 Bean 实现了 BeanNameAware、BeanFactoryAware 等接口,Spring 就会把 Bean 名称、BeanFactory 等容器信息传递给它。这类接口统称为 Aware 接口,主要用于让 Bean 感知自己所处的 Spring 容器环境。
随后就进入了 Bean 生命周期中非常重要的一个扩展机制:BeanPostProcessor(Bean 后置处理器)。
BeanPostProcessor 可以理解为 Spring 提供的一种扩展接口,允许开发者或框架在 Bean 初始化前后执行额外操作。它主要定义了两个方法:
postProcessBeforeInitialization():在 Bean 初始化回调之前执行。postProcessAfterInitialization():在 Bean 初始化回调之后执行。
为什么需要这样的机制?因为 Spring 不仅需要管理普通 Java 对象,还需要在对象创建过程中加入一些框架能力。例如,Spring AOP 就会利用 BeanPostProcessor 体系,在合适的阶段判断一个 Bean 是否需要代理,并可能返回一个代理对象来替代原始对象。
在执行前置处理之后,Spring 会进入**初始化(Initialization)**阶段。初始化和实例化不是一回事:实例化负责创建对象,而初始化负责让已经创建并完成必要依赖注入的对象执行额外的准备工作。
Spring 常见的初始化方式包括:
- 使用
@PostConstruct标注初始化方法。 - 实现
InitializingBean接口的afterPropertiesSet()方法。 - 通过
@Bean(initMethod = "...")或其他配置方式指定初始化方法。
这些方式可以用于执行配置检查、资源准备等操作。对于一个普通 Bean,@PostConstruct 通常由相关 BeanPostProcessor 在前置处理阶段调用;随后 Spring 执行 afterPropertiesSet(),再执行配置的自定义初始化方法。因此,虽然它们都属于广义上的初始化过程,但实际执行时机并不完全相同。
初始化完成之后,Spring 会调用 BeanPostProcessor 的后置处理方法。在常见的 Spring AOP 场景中,如果当前 Bean 需要被增强,就可能在这一阶段生成代理对象,并让容器后续使用这个代理对象,而不是直接使用原始对象。不过,代理创建也存在提前暴露等特殊路径,并不总是在这个阶段才发生。
当这些处理完成后,Bean 就进入了正常使用阶段。对于默认的 singleton 作用域,Spring 通常会将创建完成的单例 Bean 保存起来,后续需要时直接返回已有对象,而不是重新创建。
最后是销毁(Destruction)。当 Spring 容器正常关闭时,会对其负责管理销毁过程的 Bean 执行相应的销毁回调,用于释放连接、线程池等资源。常见的销毁方式包括 @PreDestroy、DisposableBean 的 destroy() 方法,以及配置的自定义销毁方法。
需要注意,上面描述的是常见的单例 Bean 生命周期。对于 prototype 作用域的 Bean,Spring 通常只负责创建、依赖注入和初始化,不会在容器关闭时自动执行它的销毁回调,销毁工作需要由使用者自行负责。
因此,Bean 生命周期的本质,就是 Spring 将对象创建拆分成实例化、依赖注入、初始化、扩展处理和销毁等阶段,在不同阶段完成对象管理和框架功能的整合。
其中有两个阶段尤其值得关注:一是属性填充,因为 Spring 在处理 Bean 之间的依赖关系时,可能遇到循环依赖;二是 BeanPostProcessor,因为它为 Spring AOP 等功能提供了重要的扩展入口。
循环依赖与三级缓存
前面我们介绍 Bean 生命周期时提到,Spring 创建 Bean 并不是简单地执行 new,而是需要经过实例化、属性填充、初始化等多个阶段。其中,属性填充的主要作用就是为 Bean 注入它所依赖的其他对象。
但如果两个 Bean 相互依赖,会发生什么?
例如,Bean A 需要依赖 Bean B,而 Bean B 又需要依赖 Bean A:
|
|
当 Spring 创建 A 时,首先会实例化 A,然后进入属性填充阶段,发现 A 需要依赖 B,于是开始创建 B。但在创建 B 的过程中,又发现 B 需要依赖 A,此时 A 虽然已经实例化,却还没有完成属性填充和初始化。如果 Spring 必须等一个 Bean 完全创建好才能将它注入其他 Bean,就会出现 A 等待 B、B 又等待 A 的情况,最终导致 Bean 创建失败。
这种多个 Bean 之间形成闭环依赖关系的情况,就称为循环依赖(Circular Dependency)。
那么,Spring 能不能在 A 尚未完全初始化时,就先把 A 的引用提供给 B,从而打破这种循环?
这里就需要利用前面提到的 Bean 生命周期:实例化和初始化是两个不同的阶段。对于字段注入、Setter 注入的情况,Spring 可以先实例化 A,得到一个真实存在的 Java 对象,然后在它尚未完成依赖注入和初始化时,提前暴露这个对象的引用。
这样,当创建 B 时发现它需要 A,就可以先获取 A 的早期引用,完成 B 的创建,再将 B 注入 A,最终完成 A 的初始化。
整个过程可以简单理解为:
|
|
因此,解决这种循环依赖的关键并不是反复创建对象,而是让其他 Bean 能够提前获取一个已经实例化、但尚未完成初始化的 Bean 引用。
不过,这里还有一个问题:Spring 应该把这些提前暴露的对象保存在哪里?
为了管理单例 Bean,Spring 内部使用了缓存机制。对于常见的单例 Bean 创建流程,可以重点理解以下三个缓存:
| 缓存 | 存储内容 | 主要作用 |
|---|---|---|
一级缓存 singletonObjects |
完成创建的单例 Bean | 保存最终可使用的单例对象 |
二级缓存 earlySingletonObjects |
提前暴露的 Bean 引用 | 保存尚未完成初始化的早期对象 |
三级缓存 singletonFactories |
创建早期引用的 ObjectFactory |
在需要时获取早期 Bean 引用,并提供提前生成代理的机会 |
正常情况下,一个 Bean 完成创建后,就会被放入一级缓存,后续获取时直接从一级缓存返回。
但如果一个 Bean 尚未完成创建,Spring 就需要考虑能否从其他缓存中获取它的早期引用。
例如,在创建 A 时,Spring 会在实例化 A 之后、属性填充之前,将一个能够获取 A 早期引用的 ObjectFactory 放入三级缓存。当创建 B 时,如果 B 需要 A,Spring 会先检查一级缓存,发现 A 尚未创建完成;再检查二级缓存,如果也不存在,就尝试通过三级缓存中的 ObjectFactory 获取 A 的早期引用。
获取成功后,Spring 会将这个早期引用放入二级缓存,并移除对应的三级缓存工厂。这样,后续再次需要 A 的早期引用时,就可以直接从二级缓存获取,而不必重复调用工厂。
等到 A 完成依赖注入和初始化之后,Spring 再将最终的单例对象放入一级缓存,并清理与它对应的二级、三级缓存记录。
因此,三级缓存的查找顺序可以简单理解为:
|
|
到这里,其实已经能够通过提前暴露对象的方式解决普通单例 Bean 的部分循环依赖问题了。
但是仔细思考会发现:如果只是为了解决循环依赖,为什么不直接使用二级缓存保存提前实例化的对象,还需要额外增加一个三级缓存?
这就涉及 Spring AOP。
前面介绍 BeanPostProcessor 时提到,Spring 可以在 Bean 创建过程中对对象进行额外处理,而 AOP 就是其中一个典型场景。
AOP(Aspect-Oriented Programming,面向切面编程) 的主要作用,是在不直接修改业务代码的情况下,为业务方法增加日志、事务、权限校验等通用功能。Spring AOP 通常通过动态代理实现:容器不一定直接将原始对象提供给其他组件,而是可能创建一个代理对象,让方法调用先经过代理,再执行原始业务逻辑及相应的增强操作。
例如,A 是一个需要进行 AOP 增强的 Bean。如果 Spring 在创建 B 时,直接将尚未初始化的原始 A 注入 B,而 A 后续完成初始化后又被包装成代理对象,就可能出现一个问题:
B 持有的是原始 A,而容器最终对外提供的是代理 A。
这样,B 直接调用原始 A 的方法时,就可能绕过代理,导致原本应该执行的 AOP 增强逻辑没有生效。
因此,Spring 不仅需要解决“如何提前获取对象”的问题,还需要考虑“提前提供的对象,是否应该是最终使用的代理对象”。
这就是三级缓存中 ObjectFactory 的重要意义。
三级缓存并不是简单地保存原始 Bean,而是保存一个能够按需获取早期引用的工厂。在需要提前暴露对象时,Spring 可以通过 getEarlyBeanReference() 等扩展机制,让相关的 AOP 自动代理创建器判断是否需要返回代理对象。
如果 A 不需要代理,就可以返回原始对象;如果 A 需要代理,就有机会提前创建并返回代理对象。这样,B 注入的就可以是 A 的代理引用,而不是未经增强的原始对象。
同时,Spring 还需要在后续初始化阶段协调代理处理,避免不必要的重复代理,并尽量保证容器最终使用的对象与其他 Bean 已经注入的早期引用保持一致。
因此,三级缓存的核心价值,并不只是缓存尚未初始化的 Bean,而是通过 ObjectFactory 延迟获取早期引用,为 AOP 代理对象的提前暴露提供扩展能力。
不过,这并不意味着 Spring 能够解决所有循环依赖。
首先,构造器之间形成的直接循环依赖通常无法通过提前暴露对象解决。例如 A 的构造方法需要 B,而 B 的构造方法又需要 A。由于 Spring 必须先获得构造参数才能实例化对象,此时 A 和 B 都无法完成实例化,自然也就没有对象可以提前暴露。
其次,Spring 的三级缓存机制主要针对符合条件的单例 Bean。prototype 作用域的循环依赖通常无法通过这种方式解决。另外,即使是单例 Bean,也需要容器允许循环引用,并满足相应的创建条件;涉及特殊 BeanPostProcessor 或复杂代理的场景仍然可能失败。例如,Spring Boot 2.6 起默认禁止循环引用,需要显式调整配置才可能允许符合条件的循环依赖。
因此,Spring 三级缓存并不是万能的循环依赖解决方案,而是利用单例 Bean 实例化与初始化之间的时间差,通过提前暴露引用解决部分循环依赖,并借助 ObjectFactory 兼容 AOP 代理对象的提前创建。
从设计角度来说,循环依赖通常也意味着两个组件之间存在较强的双向耦合。因此,虽然 Spring 在一定条件下能够处理循环依赖,但实际开发中仍然应该优先通过职责拆分、调整依赖关系等方式避免循环依赖,而不是把三级缓存当作常规的依赖设计手段。
Spring AOP
前面我们介绍 IoC 时提到,Spring 容器不仅负责创建和管理 Bean,还可以在 Bean 创建过程中通过 BeanPostProcessor 等扩展机制对对象进行额外处理。其中一个非常重要的应用就是 AOP(Aspect-Oriented Programming,面向切面编程)。
在实际开发中,除了业务逻辑本身,我们经常还需要为方法添加一些通用功能,例如日志记录、权限校验、事务管理等。如果直接将这些逻辑写入每一个业务方法,就会出现大量重复代码,而且业务逻辑与通用功能耦合在一起,后续修改也比较麻烦。
例如,一个 UserService 的 saveUser() 方法除了保存用户,还需要在执行前后记录日志。如果直接将日志代码写入方法内部,那么其他业务方法也需要重复编写类似的代码。
因此,一个自然的思路是:能不能在不修改原有业务代码的情况下,统一为符合条件的方法添加额外功能?
这就是 AOP 要解决的问题。AOP 的核心思想是将日志、事务等横切关注点从业务逻辑中抽离出来,统一定义,并在指定的方法执行位置织入相应的增强逻辑。
动态代理
那么,Spring 如何在不修改原始业务方法的情况下,实现方法增强?
一种直接的思路是:既然不能修改原来的业务方法,那能不能让其他对象先接收方法调用,在执行一些额外逻辑之后,再调用真正的业务对象?
这就是代理模式。
假设原本需要调用 UserService.saveUser(),现在可以创建一个代理对象,让调用方先访问代理对象。代理对象负责执行日志记录,再调用真正的 UserService 完成业务操作。
|
|
通过这种方式,业务对象本身不需要知道日志等增强逻辑的存在,而调用方也只需要调用代理对象即可。
不过,如果每增加一个需要增强的业务类,都要手动编写对应的代理类,就会产生大量重复代码。因此,Spring 进一步使用动态代理,在运行时根据目标对象生成代理对象,而不需要开发者为每个业务类手动编写代理类。
Spring AOP 主要支持两种动态代理实现:JDK 动态代理和 CGLIB 动态代理。
JDK 动态代理是 Java 自带的代理机制,主要基于接口实现。它可以在运行时生成一个实现目标接口的代理类,当调用代理对象的方法时,方法调用会进入 InvocationHandler 的 invoke() 方法,由它决定执行哪些增强逻辑以及何时调用目标对象。
例如:
|
|
可以通过 JDK 动态代理生成代理对象:
|
|
这里,调用 proxy.saveUser() 时,并不是直接进入 UserServiceImpl 的方法,而是先执行 InvocationHandler 中的逻辑,再通过反射调用真正的目标方法。
不过,JDK 动态代理依赖接口,如果业务类没有实现接口,就不能直接通过这种方式创建其业务接口代理。
因此,Spring 还支持 CGLIB 动态代理。
CGLIB 的基本思想不是生成接口实现类,而是在运行时生成目标类的子类,通过重写可被覆盖的方法来实现增强。当调用代理对象的方法时,可以先进入拦截逻辑,再根据需要调用目标业务方法。
两者的核心区别在于:
| 对比 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 实现方式 | 生成接口的实现类 | 生成目标类的子类 |
| 是否需要业务接口 | 需要 | 不需要 |
| 方法拦截机制 | InvocationHandler | MethodInterceptor |
| 主要限制 | 只能通过代理接口暴露的方法进行调用 | 无法增强 final、private 等不可重写的方法 |
需要注意,CGLIB 依赖继承,因此 final 类无法通过 CGLIB 创建子类代理,final 方法也无法通过重写实现增强。
Spring Framework 的代理选择可以通过配置控制。通常在未强制使用类代理时,有合适接口的目标对象可以使用 JDK 动态代理,否则使用 CGLIB;而 Spring Boot 的常见自动配置可能默认启用基于类的代理,因此不能简单地认为所有 Spring Boot 项目都是“有接口就一定使用 JDK 动态代理”。
Spring AOP 的核心概念
有了动态代理之后,Spring 已经能够在目标方法前后增加额外逻辑。但这里又产生了一个问题:一个应用中可能存在大量 Bean 和方法,Spring 怎么知道哪些方法需要增强,以及应该执行什么增强逻辑?
为了解决这个问题,Spring AOP 引入了一套用于描述增强规则的概念。
首先是 Join Point(连接点),表示程序执行过程中可以被增强的位置。在 Spring AOP 中,连接点主要对应方法执行。
但并不是所有方法都需要增强,因此还需要 Pointcut(切点),用于定义哪些连接点应该被增强。例如,可以指定某个包下的所有 Service 方法,或者某个类中的特定方法。
确定了需要增强的方法之后,还需要知道应该执行什么逻辑,这就是 Advice(通知)。通知表示具体的增强操作,常见的有前置通知、后置通知、返回通知、异常通知和环绕通知。其中,环绕通知可以在目标方法执行前后添加逻辑,还能够决定是否以及何时继续执行后续调用。
最后,Spring 将切点与通知等内容组织起来,形成 Aspect(切面)。一个切面可以理解为一组横切功能的定义,描述哪些方法需要被增强,以及应该执行哪些增强逻辑。
例如:
|
|
这里,@Aspect 表示一个切面,@Around 表示环绕通知,execution(...) 表达式用于匹配需要增强的方法,而 joinPoint.proceed() 则负责继续执行后续拦截逻辑,并最终调用目标方法。
因此,可以简单理解为:Pointcut 决定增强哪些方法,Advice 决定执行什么增强逻辑,Aspect 则负责将这些增强规则组织起来。
Spring AOP 的底层实现流程
理解了动态代理和 AOP 的核心概念之后,就可以进一步思考:Spring 是如何把这些切面规则应用到实际 Bean 上,并生成代理对象的?
前面介绍 Bean 生命周期时提到,Spring 在 Bean 初始化前后会调用 BeanPostProcessor,而 Spring AOP 正是利用这一扩展机制,将代理对象的创建过程整合进 IoC 容器。
Spring 内部存在一类自动代理创建器,例如 AnnotationAwareAspectJAutoProxyCreator。它属于 BeanPostProcessor 体系,可以在 Bean 创建过程中判断当前 Bean 是否需要被 AOP 增强。
对于常见的代理创建流程,可以简单理解为以下几个步骤。
首先,Spring 会解析切面配置,识别其中的切点和通知,并将这些信息组织成可以用于匹配和执行的增强规则。在底层,这类规则通常会通过 Advisor 进行组织,Advisor 可以理解为将切点匹配条件与通知关联起来的对象。
接下来,当 Spring 创建某个 Bean 时,自动代理创建器会判断是否存在适用于这个 Bean 的 Advisor。
如果没有匹配的增强规则,通常就直接使用原始 Bean;如果存在匹配规则,则会根据代理配置和目标类的情况,使用 JDK 动态代理或 CGLIB 创建代理对象。
最终,Spring 容器对外提供的就可能不再是原始业务对象,而是能够执行 AOP 增强逻辑的代理对象。
整个过程可以概括为:
|
|
需要注意,这只是普通 Bean 初始化后创建代理的典型流程。前面讨论循环依赖时提到,如果某个 Bean 需要提前暴露引用,Spring 也可能通过 getEarlyBeanReference() 提前获取它的代理对象,而不是一定等到初始化完成之后才创建代理。
不过,代理对象创建完成,并不意味着增强逻辑已经执行。创建代理和执行增强是两个不同的阶段。
当其他组件调用代理对象的方法时,Spring 才会根据当前方法匹配相应的拦截器,并将这些拦截器组织成一条拦截器链(Interceptor Chain)。
例如,一个方法同时需要进行权限校验、日志记录和事务管理,那么它的执行过程可以理解为:
|
|
这里的顺序只是示意,实际拦截器的执行顺序由 Spring 的排序规则以及相关配置决定。
Spring AOP 的拦截器链通常采用类似责任链的机制。每个拦截器负责执行自己的增强逻辑,并通过 proceed() 将调用传递给下一个拦截器,直到最终执行目标方法。
因此,Spring AOP 的底层实现可以分成两个核心过程:Bean 创建阶段通过自动代理创建器判断是否需要增强,并生成代理对象;方法调用阶段由代理对象组织和执行拦截器链,最终调用目标方法。
这样,Spring 就可以将日志、权限、事务等通用逻辑从业务代码中分离出来,统一管理,并在不直接修改原始业务方法的情况下实现功能增强。
不过,Spring AOP 基于代理对象实现,也意味着它存在一定限制。
例如,如果一个 Bean 内部通过 this 调用自己的另一个方法,那么这次调用可能不会经过 Spring 代理对象,而是直接发生在目标对象内部,因此对应的 AOP 增强可能不会执行。
同样,在基于 CGLIB 的代理中,final 方法无法被重写,private 方法也不能通过子类重写实现拦截,因此这类方法不能通过常规的 Spring AOP 代理机制进行增强。
所以,理解 Spring AOP 最重要的并不是记住几个注解,而是理解它的核心机制:通过动态代理接管外部方法调用,再利用切点匹配和拦截器链,在目标方法执行前后加入通用功能。
而这个机制在 Spring 中一个非常重要的实际应用,就是声明式事务。我们平时使用的 @Transactional,本质上就是利用 Spring AOP 的代理和事务拦截机制,在业务方法执行前开启或加入事务,并根据方法执行结果决定提交还是回滚。