编程导航SpringAOP话题讨论

SpringAOP

8 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

Spring进阶 - Spring事务理论+实战,一文吃透事务

本文系统性介绍Spring事务,一贯坚持知识关联性的叙述风格。 首先要了解 Spring 事务在 Spring 架构中的定位: ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/kVc6uW7H62deMiCb.webp) 可以看出来,Spring事务属于 Data Acess(数据访问模块) 的知识点。 Data Acess 是在**应用程序(Spring程序)**中,访问持久化数据存储(如关系型数据库、NoSQL、缓存)与业务/服务层之间进行解耦、简化样板代码、**统一事务**与异常处理的通用抽象层。形象理解 @Transactionnal 注解是无侵入的给业务代码加上了事务功能,这就是 Data Acess 模块带来的快乐之一,与外部中间件打交道进行了封装。 文本的目标是介绍 Data Acess 的 Transactions 。 fency: 从图中可以看出来,DataAccess 在 Core 模块之上,其实 Data Access 将 Core 模块作为底层能力支撑,而 Spring 事务是依赖于 `**spring-core**` 和 `**spring-beans**` 等核心模块,并与 `**spring-aop**` 模块紧密协作来实现 Spring 事务 ## 事务 ### 事务介绍 在计算机科学/数据管理系统中,一个“事务(transaction)”是指一系列操作组成的逻辑单元,它要么全部成功提交,要么全部失败回滚,从而保证系统状态从一个一致状态变到另一个一致状态。 举例:银行转账操作 —— 扣款和入账两个子操作构成一个事务。若其中一个失败,则两个操作都应回滚。 事务的四大特性(ACID):原子性、一致性、隔离性、持久性 - **原子性( Atomicity )**: 事务是一个不可分割的整体。事务中的所有操作要么全部成功,要么全部失败,不能只成功部分。若事务执行过程中某个操作失败,则要将之前已做的操作回滚,如同未执行过。 - **隔离性 (Isolation)**:当多个事务并发执行时(尤其在高并发系统中),一个事务不能看到其他事务未提交的中间状态;不同事务之间的执行效果应该与串行执行一致。这样可防止脏读、幻读、不可重复读等问题。 - **持久性 (Durability)**:一旦事务提交,其变更就应永久保存在系统中,即便系统崩溃、断电也不能丢失。通常通过日志、磁盘持久化机制实现。 - **一致性(Consistency)**:事务执行结束后,数据必须保持一致性状态。在事务执行期间,数据库中的数据可以处于中间状态,但在事务完成时必须保证数据的一致性。 落地技术: **原子性:**由 MySql 的 undolog 实现,undolog 可以在 发生错误/死锁中断 、 用户执行 ROLLBACK 时,将已做修改撤销,回滚操作就是由 undolog 实现的,因此可以**保证要么全成功,要么全失败**。 **持久性**:由 MySql 的 redolog 实现,注意 mysql 也是有数据缓冲区的,插入的数据放到数据缓冲区,定时或内存满了才会刷盘持久化(性能提升),如果还没持久化断电了,此时 redolog 就会发挥作用,redolog 记录了所有插入、更新、修改的操作,并在事务结束前刷盘 redolog , 确保redolog 必须持久化,从而保证了 MySQL 数据的持久化。 **隔离性:** 由 MySQL 的 MVCC 版本控制实现,有四种隔离级别,分别是读未提交、读已提交、可重复读、串行化,隔离级别决定了事务中间数据的脏读、不可重复读、幻读等问题。MySQL 默认隔离级别是 MVCC + 间隙锁能够解决幻读。 **一致性:**因为实现了以上三种特性,所以一定满足一致性,这是一种因果关系,数据一致性是事务的终极目标。 ### 事务类型:本地事务 VS 分布式事务 我们讨论的经常是本地事务,也就是单个数据源内执行的事务,这种事务由数据库自行管理,为开发者提供事务能力。 分布式事务涉及多个数据源(包括数据库、消息队列等),需要跨多个资源管理器(如数据库)进行协调,实现起来比较复杂,通常需要应用服务器(如Spring Cloud Gateway)或者JTA(Java Transcation API)支持 事务的概念不仅仅应用于 MySQL 数据,在以下系统和组件都有应用: | 系统/组件 | 事务支持 | 说明 | | ----------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | **关系型数据库** | **完全支持** ACID 特性。 | 如 MySQL、PostgreSQL、Oracle 等,是事务最经典的应用场景。 | | **消息队列** | 部分支持(如 RocketMQ 的事务消息)。 | 保证消息发送与本地事务的原子性,确保消息不丢失或重复消费。 | | **Redis** | 提供**有限的事务支持**(通过 `**MULTI**`/`**EXEC**`命令) 。 | **不保证原子性**(命令执行失败不会回滚)和**隔离性**(事务执行期间其他客户端命令可能插入)。通常用于简单批量操作,或结合 Lua 脚本实现原子操作 。 | | **Elasticsearch** | **不支持**传统意义上的 ACID 事务。 | 更侧重于最终一致性和文档级别的原子操作。 | ### 事务传播行为的概念 事务传播行为: 一个事务调用另一个事务如何处理(加入 , 挂起,禁止加入,嵌套) [维基百科](https://en.wikipedia.org/wiki/Database_transaction?utm_source=chatgpt.com)对事务的定义重点关注的是事务作为“独立单位”的概念,并没有牵扯事务传播行为,所以本质上事务传播行为不属于**标准事务**的知识领域,而是在应用层框架中运用的一种机制,在Spring中我们会看到“ 一个事务方法调用另一个事务方法,该子方法应如何参与或者不参与父事务 ”的情境。 注意区分,**事务传播行为是 Spring 事务的独有概念**,并不是数据库级事务定义的原生特性 。 ## Spring 事务 ### 介绍 Spring 事务是 **对底层事务 API(如 JDBC、JPA、Hibernate、JTA 等)的一层统一封装**。 通过它,开发者不需要关心底层数据库或框架的差异,只需用统一的编程方式来声明和管理事务。 **名词解释:** **JDBC**: JDBC 是 Java 提供给关系数据库访问的一个标准 API,定义了如何建立连接、发送 SQL、检索结果,仅仅定义了抽象接口,实现交给具体的关系数据库。 Spring 的事务管理机制能帮你管理 JDBC 的事务的提交、回滚,不必手写这些细节。 **JTA** : JTA 是 Java 规范中用于“分布式事务/多资源管理器事务” 的 API。 在一个事务内可能涉及多个资源(比如多个数据库、消息队列等)时,JTA 提供一个标准接口让事务管理器协调这些资源 。 **JPA** : JPA 是 Java 的一个对象–关系映射(ORM)规范/API,定义如何将 Java 对象映射为数据库表、如何做持久化、查询、事务等。 开发者能通过实体类(POJO)操作数据库,正是JPA发挥了作用。Spring 的事务适配 JPA 提供的事务模型 , 让你用 `@Transactional` 等方式控制 JPA 操作的事务边界 **Hibernate**:Hibernate 是一个最著名的 ORM 框架之一,在 Java 世界中用于将 Java 对象映射到关系数据库,并处理所需的 CRUD、查询、缓存、事务整合等。 Spring 的事务抽象也支持 Hibernate 的事务管理 **Spring 事务的优势** - **跨不同事务API的一致编程模型**:支持JTA、JDBC、Hibernate、JPA等 。 - **支持声明式事务管理**:通过AOP实现,极大简化事务管理 。 - **简化的编程式事务管理API**:比JTA等复杂API更易用 。 - **与Spring数据访问抽象完美集成**:无缝整合各种数据访问技术 。 ### Spring 事务的管理方式 Spring 提供了两种主要的事务管理方式:声明式事务管理,编程式事务管理。 #### 1、声明式事务管理(注解,xml) 基于注解或 XML 配置实现,无需在代码中显式编写事务逻辑, XML 声明事务过时了。 **常用注解:** ```java @Transactional public void transferMoney() { accountDao.debit("A", 100); accountDao.credit("B", 100); } ``` - 默认是 **运行时异常(RuntimeException)回滚** - 可以通过属性定制行为: ```java @Transactional( propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, timeout = 30, rollbackFor = Exception.class ) ``` @Transactional 更多属性: (按功能分类讲,表格后我还会带例子) | 属性 | 作用 | 默认值 | 说明 | | --------------- | -------------------- | ------------------------------------------ | ------------------------------------------------------ | | **propagation** | 事务传播行为 | `Propagation.REQUIRED` | 指定当前方法在事务中的传播方式(是否新建事务等)。 | | **isolation** | 事务隔离级别 | `Isolation.DEFAULT` | 控制并发事务间的隔离强度。 | | **timeout** | 超时时间(秒) | `-1`(不超时) | 超过时间未完成则回滚。 | | **readOnly** | 是否只读事务 | `false` | 可用于优化查询性能(提示数据库不用加锁)。 | | **rollbackFor** | 指定哪些异常触发回滚 | 无(仅对 `RuntimeException`/ `Error`回滚) | 支持多个异常类,如 `rollbackFor = {Exception.class}`。 | #### 2、编程式事务管理 通过 `**TransactionTemplate**` (**重点**)或 `PlatformTransactionManager` 手动控制事务。比声明式事务更灵活、可控,但是代码入侵高。 示例: ```java @Autowired private TransactionTemplate transactionTemplate; public void transferMoney() { transactionTemplate.execute(status -> { accountDao.debit("A", 100); accountDao.credit("B", 100); return null; }); } ``` 编程事务的 return 值一般不重要,用不到。 ### Spring 事务传播行为 事务传播行为定义了**当一个事务方法调用另一个事务方法时,事务如何在这些方法间传播,**传播行为是Spring独有的概念,并非事务通用概念。 **示例:** 以下面代码为例,createUserAndOrder() 方法本身有事务,方法内调用了另一个有事务的 OrderService() 方法,这就涉及到事务传播的知识范畴,事务传播有七种行为。 ```java @Transactional(propagation = Propagation.REQUIRED) public void createUserAndOrder() { // 1. 插入用户 insertUser(); // 2. 调用 OrderService orderService.createOrder(); } @Transactional(propagation = Propagation.REQUIRED) public void createOrder() { System.out.println("订单已创建"); // 这里如果抛异常,则事务回滚 // throw new RuntimeException("订单创建失败"); } ``` **propagation** 是事务传播行为的属性,可以指定以下七个值,代表了七种行为。 主要要站在内层事务的角度(createOrder方法)思考这七种行为,我将事务传播分为了四个类别:内层事务加入外层事务(当前事务),挂起当前事务,禁止当前事务,嵌套事务。 | 分类 | 传播行为 | 存在事务时行为 | 无事务时行为 | 核心特点与典型场景 | | ---------------- | ------------------- | ---------------------------------- | ---------------- | ------------------------------------------------------------ | | **加入当前事务** | `**REQUIRED**` | 加入当前事务 | 创建新事务 | **默认选择**。适用于绝大多数业务场景,保证操作在同一事务中。 | | | `**SUPPORTS**` | 加入当前事务 | 以非事务方式执行 | **可选支持**。适用于查询方法,可根据调用方决定是否启用事务。 | | | `**MANDATORY**` | 加入当前事务 | 抛出异常 | **强制要求**。确保方法必须在事务中运行,否则报错。用于内部核心方法,防止意外脱离事务。 | | **挂起当前事务** | `**REQUIRES_NEW**` | **挂起当前事务**,创建新事务 | 创建新事务 | **独立事务**。新事务与外层事务完全独立,必须单独提交/回滚。用于日志记录、发送通知等需与主事务隔离的操作。 | | | `**NOT_SUPPORTED**` | **挂起当前事务**,以非事务方式执行 | 以非事务方式执行 | **强制非事务**。明确不需要事务的操作,如复杂查询,避免事务开销。 | | **禁止当前事务** | `**NEVER**` | 抛出异常 | 以非事务方式执行 | **禁止事务**。绝对不允许在事务中运行,否则报错。用于纯计算或外部调用等场景。 | | **嵌套事务** | `**NESTED**` | 在**嵌套事务**中执行 | 创建新事务 | **部分回滚**。基于保存点机制,内层事务失败可单独回滚,不影响外层事务。适用于大操作中需独立回滚的部分步骤(如批量操作中的单条失败)。 | 面试官问到事务传播行为有哪些,你能说出四个种类来,也表现出你是理解的,每一个详细讲出来确实恶心。实际开发中,有意识的区分四个种类,也就够了。 ### Spring 事务隔离级别 事务隔离级别定义了**并发事务之间的隔离程度**,以防止数据不一致性问题(如脏读、不可重复读、幻读)Spring 支持以下 5 种隔离级别,与标准 JDBC 隔离级别对应: | 隔离级别 | 说明 | 可能引发的问题 | 数据库默认示例 | | ------------------------------------------- | ---------------------------------------------- | -------------------------- | --------------------------------- | | **ISOLATION_DEFAULT** **默认隔离级别** | 使用**底层数据库的默认隔离级别** 。 | 取决于数据库 | MySQL: `**REPEATABLE_READ**` | | **ISOLATION_READ_UNCOMMITTED** **读未提交** | 允许读取**未提交**的数据变更 。 | 脏读、不可重复读、幻读 | 较少使用 | | **ISOLATION_READ_COMMITTED** **读已提交** | 只允许读取**已提交**的数据变更 。 | 不可重复读、幻读 | Oracle、SQL Server 默认 | | **ISOLATION_REPEATABLE_READ** **可重复读** | 确保同一事务中多次读取同一数据的结果**一致**。 | 幻读 | MySQL 默认,但MySQL不会有幻读问题 | | **ISOLATION_SERIALIZABLE** **串行化** | 最高的隔离级别,**事务串行执行** 。 | 无并发问题,但**性能极低** | 金融等高一致性要求场景 | 当你在Spring应用层指定了事务的隔离级别,就相当于给数据库指定了隔离级别。 ### Spring 事务的原理 fency :对于程序技术而言,原理就是看程序的调用链路,看源码是怎么写的,分析背后的设计思想,不过这里看看调用链路,简单学一点原理就行了。 Spring 事务的原理核心是 **AOP(面向切面编程)** 和 **事务管理器** 的协同工作。它通过代理模式在方法执行前后插入事务控制逻辑,从而实现声明式事务管理。下面我用一张图帮你直观理解其整体流程,然后再分步解析。 **客户端**:理解成前端发送请求,访问到后端的一个事务方法,开始调用事务方法,你看的是调用了一个带有 @Transitionnal 注解的方法,实际上呢? **Spring AOP 代理**:Spring 容器在启动时,会检查所有 Bean,如果发现某个类(或其方法)上带有 `**@Transactional**` 注解,Spring 会通过 **AOP 框架** 为其创建一个**代理对象**(基于 JDK 动态代理或 CGLIB),这个代理对象包装了原始的目标对象(方法)。 所以实际上程序调用了一个 AOP 代理。 **TransactionManager** : AOP 代理会调用Spring事务管理器(理解成一个类就行),帮你在背后对接数据库,帮你开启事务,告知 Spring 最新的数据库事务状态。 所以 Spring 事务的能力是取决于数据库,如果数据库使用 MyISAM 存储引擎,那么 Spring 将丧失事务能力。 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/vqZZeMNl4EFAtXXo.svg) ## Spring 事务实战 技术栈选用:**Spring Boot 3.x** + MyBatis plus + MySQL 场景是经典转账场景,周杰伦给昆凌转账 ### 初始化项目 项目依赖: ```xml <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- MySQL Driver --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- MyBatis-Plus Starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.14</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> ``` 库表设计: ```plsql CREATE DATABASE IF NOT EXISTS spring_tx_demo; USE spring_tx_demo; CREATE TABLE users ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, balance DECIMAL(10,2) NOT NULL ); INSERT INTO users(name, balance) VALUES ('周杰伦', 1000.00), ('昆凌', 1000.00); ``` 配置文件: ```yaml spring: datasource: url: jdbc:mysql://localhost:3306/spring_tx_demo?useSSL=false&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # SQL 输出 global-config: db-config: id-type: auto table-underline: true ``` MyBatis X 插件生成代码: ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/sAbd6fZHMbdUbH2i.webp) ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/FBmbejt3835xWukT.webp) ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/3uvakBz1uphGFRqQ.png) 实体类: ```java @TableName(value ="users") @Data public class Users { /** * */ @TableId(type = IdType.AUTO) private Integer id; /** * */ private String name; /** * */ private BigDecimal balance; } ``` ### 事务基本功能示例 周杰伦给昆凌转账 , 我们模拟两次,一次是转账成功,一次是转账失败回滚,来验证Spring事务是正常工作的。 注意代码不用看的太仔细,一会跟我分析日志,日志是精髓,代码精髓就是一个 @Transactional ,代码逻辑无所谓。 Service 代码: ```java @Service public class UsersService { @Autowired private UsersMapper usersMapper; /** * 场景一:成功的转账 * @param fromName 转出人 * @param toName 转入人 * @param amount 金额 */ @Transactional // <-- 关键!开启事务 public void transferMoneySuccessfully(String fromName, String toName, BigDecimal amount) { System.out.println("--------- 开始成功转账 ---------"); // 1. 查询转出用户 Users fromUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", fromName)); // 使用 Users // 2. 查询转入用户 Users toUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", toName)); // 使用 Users // 3. 转出用户扣款 fromUser.setBalance(fromUser.getBalance().subtract(amount)); usersMapper.updateById(fromUser); System.out.println(fromName + " 扣款 " + amount + " 成功,余额: " + fromUser.getBalance()); // 4. 转入用户收款 toUser.setBalance(toUser.getBalance().add(amount)); usersMapper.updateById(toUser); System.out.println(toName + " 收款 " + amount + " 成功,余额: " + toUser.getBalance()); System.out.println("--------- 成功转账完成 ---------"); } /** * 场景二:失败的转账(模拟异常,触发回滚) * @param fromName 转出人 * @param toName 转入人 * @param amount 金额 */ @Transactional // <-- 关键!开启事务 public void transferMoneyWithException(String fromName, String toName, BigDecimal amount) { System.out.println("--------- 开始模拟异常转账 ---------"); // 1. 查询转出用户 Users fromUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", fromName)); // 使用 Users // 2. 查询转入用户 Users toUser = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", toName)); // 使用 Users // 3. 转出用户扣款 fromUser.setBalance(fromUser.getBalance().subtract(amount)); usersMapper.updateById(fromUser); System.out.println(fromName + " 扣款 " + amount + " 成功,余额: " + fromUser.getBalance()); // 4. 模拟系统在转账过程中突然发生异常! System.out.println("系统发生异常,转账中断..."); throw new RuntimeException("模拟转账过程中发生异常!"); // 下面的代码永远不会被执行 // toUser.setBalance(toUser.getBalance().add(amount)); // usersMapper.updateById(toUser); } } ``` 在启动类中直接调用了,不用 web 的 controller 调用。 ```java @SpringBootApplication @MapperScan("com.feng.springtransactiondemo.mapper") public class SpringTransactionDemoApplication implements CommandLineRunner { @Autowired private UsersService usersService; // 注入 UsersService @Autowired private UsersMapper usersMapper; // 注入 UsersMapper,用于查询最终结果 public static void main(String[] args) { SpringApplication.run(SpringTransactionDemoApplication.class, args); } @Override public void run(String... args) throws Exception { System.out.println("========================================"); System.out.println("Spring 事务实战演示开始"); System.out.println("========================================"); // --- 场景一:演示成功的事务 --- System.out.println("\n--- 场景一:Alice 给 Bob 转账 200 元 ---"); usersService.transferMoneySuccessfully("Alice", "Bob", new BigDecimal("200")); printAllUsersBalance(); // --- 场景二:演示失败的事务(回滚)--- System.out.println("\n--- 场景二:Alice 给 Bob 转账 300 元,但中途发生异常 ---"); try { usersService.transferMoneyWithException("Alice", "Bob", new BigDecimal("300")); } catch (Exception e) { System.err.println("捕获到异常: " + e.getMessage()); } printAllUsersBalance(); System.out.println("\n========================================"); System.out.println("Spring 事务实战演示结束"); System.out.println("========================================"); } private void printAllUsersBalance() { System.out.println("--- 查询当前所有用户余额 ---"); List<Users> users = usersMapper.selectList(null); // 使用 Users users.forEach(user -> System.out.println(user.getName() + " 的余额是: " + user.getBalance())); System.out.println("---------------------------"); } } ``` 最终得到日志就很有用了: ```bash ======================================== Spring 事务实战演示开始 ======================================== --- 场景一:周杰伦 给 昆凌 转账 200 元 --- 2025-10-28T18:50:28.216+08:00 INFO 29736 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... 2025-10-28T18:50:28.344+08:00 INFO 29736 --- [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@294f9d50 2025-10-28T18:50:28.345+08:00 INFO 29736 --- [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. --------- 开始成功转账 --------- Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] JDBC Connection [HikariProxyConnection@718057154 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will be managed by Spring ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 周杰伦(String) <== Columns: id, name, balance <== Row: 1, 周杰伦, 1000.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 昆凌(String) <== Columns: id, name, balance <== Row: 2, 昆凌, 1000.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction ==> Preparing: UPDATE users SET name=?, balance=? WHERE id=? ==> Parameters: 周杰伦(String), 800.00(BigDecimal), 1(Integer) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] 周杰伦 扣款 200 成功,余额: 800.00 Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] from current transaction ==> Preparing: UPDATE users SET name=?, balance=? WHERE id=? ==> Parameters: 昆凌(String), 1200.00(BigDecimal), 2(Integer) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] 昆凌 收款 200 成功,余额: 1200.00 --------- 成功转账完成 --------- Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6baf25d7] --- 查询当前所有用户余额 --- Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f25bf88] was not registered for synchronization because synchronization is not active JDBC Connection [HikariProxyConnection@2069678360 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will not be managed by Spring ==> Preparing: SELECT id,name,balance FROM users ==> Parameters: <== Columns: id, name, balance <== Row: 1, 周杰伦, 800.00 <== Row: 2, 昆凌, 1200.00 <== Total: 2 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6f25bf88] 周杰伦 的余额是: 800.00 昆凌 的余额是: 1200.00 --------------------------- --- 场景二:周杰伦 给 昆凌 转账 300 元,但中途发生异常 --- --------- 开始模拟异常转账 --------- Creating a new SqlSession Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] JDBC Connection [HikariProxyConnection@933293116 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will be managed by Spring ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 周杰伦(String) <== Columns: id, name, balance <== Row: 1, 周杰伦, 800.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] from current transaction ==> Preparing: SELECT id,name,balance FROM users WHERE (name = ?) ==> Parameters: 昆凌(String) <== Columns: id, name, balance <== Row: 2, 昆凌, 1200.00 <== Total: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] from current transaction ==> Preparing: UPDATE users SET name=?, balance=? WHERE id=? ==> Parameters: 周杰伦(String), 500.00(BigDecimal), 1(Integer) <== Updates: 1 Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] 周杰伦 扣款 300 成功,余额: 500.00 系统发生异常,转账中断... Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c719bd4] --- 查询当前所有用户余额 --- Creating a new SqlSession SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f7d9e] was not registered for synchronization because synchronization is not active JDBC Connection [HikariProxyConnection@1976788674 wrapping com.mysql.cj.jdbc.ConnectionImpl@294f9d50] will not be managed by Spring ==> Preparing: SELECT id,name,balance FROM users ==> Parameters: <== Columns: id, name, balance <== Row: 1, 周杰伦, 800.00 <== Row: 2, 昆凌, 1200.00 <== Total: 2 Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@456f7d9e] 周杰伦 的余额是: 800.00 昆凌 的余额是: 1200.00 --------------------------- ======================================== Spring 事务实战演示结束 ======================================== ``` 日志看完了,发现我们打上的注解确实有用的,在场景二中,我们执行了两个更新金额的操作,根据所学知识,这需要开启事务,所以我们用Spring事务快速开启 MySQL 的事务,而不用编写 sql 语句,一个注解搞定。最终也确实回滚了,转账失败。 ### 脏读示例 这里给出一个脏读示例,别的也是类似的。 在原本的service上加以下两个方法 上面的方法是默认的隔离级别(可重复读),下面的方法是`isolation = Isolation.READ_UNCOMMITTED`读未提交 ```java /** * 模拟周杰伦:增加余额,但最后会回滚(事务未成功提交) * @param name 用户名 * @param amount 增加的金额 */ @Transactional // 使用默认的隔离级别即可 public void addBalanceWithRollback(String name, BigDecimal amount) { System.out.println("\n[" + Thread.currentThread().getName() + "] " + name + " 开始事务,准备增加余额 " + amount); Users jay = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name)); jay.setBalance(jay.getBalance().add(amount)); usersMapper.updateById(jay); System.out.println("[" + Thread.currentThread().getName() + "] " + name + " 余额已更新为: " + jay.getBalance() + ",但事务尚未提交!"); try { // 模拟耗时操作,让其他事务有机会读取 Thread.sleep(3000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("[" + Thread.currentThread().getName() + "] " + name + " 的操作发生异常,事务即将回滚!"); throw new RuntimeException("模拟系统异常,导致回滚"); } /** * 模拟昆凌:使用 READ_UNCOMMITTED 隔离级别进行查询,可能发生脏读 * @param name 要查询的用户名 */ @Transactional(isolation = Isolation.READ_UNCOMMITTED) // 关键:设置隔离级别为读未提交 public void readWithDirtyRead(String name) { System.out.println("\n[" + Thread.currentThread().getName() + "] 昆凌开始查询 " + name + " 的余额..."); try { // 稍微等待一下,确保周杰伦已经更新了数据但还没回滚 Thread.sleep(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } Users jay = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name)); System.out.println("[" + Thread.currentThread().getName() + "] 昆凌第一次读取到 " + name + " 的余额是: " + jay.getBalance() + " (这可能是脏数据!)"); try { // 等待周杰伦的事务回滚 Thread.sleep(4000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 在同一个事务中再次读取 Users jayAgain = usersMapper.selectOne(new QueryWrapper<Users>().eq("name", name)); System.out.println("[" + Thread.currentThread().getName() + "] 昆凌第二次读取到 " + name + " 的余额是: " + jayAgain.getBalance() + " (周杰伦的事务已回滚)"); } ``` 启动类要注释刚刚的run方法相关代码,用以下的代码: ```java @Override public void run(String... args) throws Exception { System.out.println("========================================"); System.out.println("Spring 事务并发问题演示:脏读"); System.out.println("========================================"); // 为了演示效果,先重置周杰伦的余额 resetJayBalance(); // 创建一个包含2个线程的线程池 ExecutorService executor = Executors.newFixedThreadPool(2); System.out.println("\n--- 场景:昆凌在周杰伦未提交的事务中读取了脏数据 ---"); // 线程1:模拟周杰伦的操作(更新并回滚) executor.submit(() -> { try { usersService.addBalanceWithRollback("周杰伦", new BigDecimal("5000")); } catch (Exception e) { // 捕获异常,防止线程池报错 System.err.println("[" + Thread.currentThread().getName() + "] 捕获到异常: " + e.getMessage()); } }); // 线程2:模拟昆凌的查询(脏读) executor.submit(() -> { usersService.readWithDirtyRead("周杰伦"); }); // 关闭线程池 executor.shutdown(); } private void resetJayBalance() { Users jay = usersMapper.selectOne(new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper<Users>().eq("name", "周杰伦")); if (jay != null) { jay.setBalance(new BigDecimal("1000.00")); usersMapper.updateById(jay); System.out.println("周杰伦的余额已重置为: " + jay.getBalance()); } } ``` 最终得到日志是这样的,这一次简化给大家看: ```bash ======================================== Spring 事务并发问题演示:脏读 ======================================== 周杰伦的余额已重置为: 1000.00 --- 场景:昆凌在周杰伦未提交的事务中读取了脏数据 --- [pool-1-thread-1] 周杰伦 开始事务,准备增加余额 5000 [pool-1-thread-1] 周杰伦 余额已更新为: 6000.00,但事务尚未提交! [pool-1-thread-2] 昆凌开始查询 周杰伦的余额... [pool-1-thread-2] 昆凌第一次读取到 周杰伦 的余额是: 6000.00 (这可能是脏数据!) [pool-1-thread-1] 周杰伦 的操作发生异常,事务即将回滚! [pool-1-thread-1] 捕获到异常: 模拟系统异常,导致回滚 [pool-1-thread-2] 昆凌第二次读取到 周杰伦 的余额是: 1000.00 (周杰伦的事务已回滚) ``` 系统给周杰伦转 5000 块钱,周杰伦的事务还没提交,昆凌就开始查账 ,查到了6000 , 然后系统发生异常,正常回滚,周杰伦的金额回到了 1000, 昆凌再查一次账变成了 1000! 昆凌在一次事务中查询同样的数据,结果却不一致,这就是脏读,如果昆凌基于读取到的 6000 余额做了某些业务决策(比如批准了一笔大额贷款),那么当周杰伦的事务回滚后,这个决策就建立在了一个不存在的“假数据”之上,可能导致严重的业务问题。 ### 事务传播示例用法 我们演示两个传播行为:【**默认事务传播行为】** 和 【**挂起当前事务并开启新事务】**(REQUIRES_NEW) 场景周杰伦下单,下单包含多个步骤,每个步骤单独开启事务,便能演示一个事务调用另一个事务。 1. 创建订单 2. 扣减库存 3. 扣减用户余额 我们需要增加产品表和订单表: ```sql -- 商品表 CREATE TABLE product ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, stock INT NOT NULL ); -- 订单表 CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, product_id INT NOT NULL, amount DECIMAL(10, 2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 插入初始数据 INSERT INTO product(name, stock) VALUES ('iPhone 15', 10); -- 确保“周杰伦”的账户余额足够 UPDATE users SET balance = 8000.00 WHERE name = '周杰伦'; ``` 实体类: ```java @Data @TableName("orders") public class Order { @TableId(type = IdType.AUTO) private Long id; private Long userId; private Long productId; private BigDecimal amount; private LocalDateTime createTime; } @Data @TableName("product") @AllArgsConstructor public class Product { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer stock; } ``` mapper 类: ```java @Mapper public interface OrderMapper extends BaseMapper<Order> {} @Mapper public interface ProductMapper extends BaseMapper<Product> {} ``` 对应的mapper.xml ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.feng.springtransactiondemo.mapper.OrderMapper"> <resultMap id="BaseResultMap" type="com.feng.springtransactiondemo.domain.Order"> <id property="id" column="id" jdbcType="INTEGER"/> <result property="userId" column="user_id" jdbcType="INTEGER"/> <result property="productId" column="product_id" jdbcType="INTEGER"/> <result property="amount" column="amount" jdbcType="DECIMAL"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> </resultMap> <sql id="Base_Column_List"> id,user_id,product_id, amount,create_time </sql> </mapper> <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.feng.springtransactiondemo.mapper.ProductMapper"> <resultMap id="BaseResultMap" type="com.feng.springtransactiondemo.domain.Product"> <id property="id" column="id" jdbcType="INTEGER"/> <result property="name" column="name" jdbcType="VARCHAR"/> <result property="stock" column="stock" jdbcType="INTEGER"/> </resultMap> <sql id="Base_Column_List"> id,name,stock </sql> </mapper> ``` Service 类 ```java @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private UsersMapper usersMapper; // 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表 @Autowired private OrderMapper logMapper; // 假设这是一个独立的日志Mapper /** * 场景一:REQUIRED (默认值) * 外部方法事务,内部方法加入外部事务。任何一个失败,整体回滚。 */ @Transactional(propagation = Propagation.REQUIRED) public void placeOrder_REQUIRED(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景一:REQUIRED 传播 ---"); System.out.println("[外部事务] 开始下单..."); // 1. 创建订单 createOrder(userId, productId, amount); // 2. 扣减库存 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!"); } /** * 场景二:REQUIRES_NEW * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。 */ @Transactional(propagation = Propagation.REQUIRED) // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务 public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---"); System.out.println("[外部事务] 开始下单..."); try { // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 createOrder(userId, productId, amount); // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!"); } catch (Exception e) { System.err.println("[外部事务] 下单失败: " + e.getMessage()); // 无论成功失败,都要记录一条日志 recordLog(userId, productId, "下单失败"); } } // ============== 内部私有方法 ============== private void createOrder(Long userId, Long productId, BigDecimal amount) { Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(amount); orderMapper.insert(order); System.out.println("[内部方法] 创建订单成功,订单ID: " + order.getId()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void reduceStock(Long productId) { Product product = productMapper.selectById(productId); if (product.getStock() <= 0) { throw new RuntimeException("库存不足!"); } product.setStock(product.getStock() - 1); productMapper.updateById(product); System.out.println("[内部方法] 扣减库存成功,剩余库存: " + product.getStock()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void deductBalance(Long userId, BigDecimal amount) { Users user = usersMapper.selectById(userId); if (user.getBalance().compareTo(amount) < 0) { throw new RuntimeException("用户余额不足!"); } user.setBalance(user.getBalance().subtract(amount)); usersMapper.updateById(user); System.out.println("[内部方法] 扣减余额成功,用户余额: " + user.getBalance()); // 模拟一个系统异常,导致事务回滚 if (amount.compareTo(new BigDecimal("5000")) > 0) { throw new RuntimeException("模拟系统异常:大额订单需要人工审核!"); } } /** * 记录日志,使用 REQUIRES_NEW 开启一个独立的事务 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void recordLog(Long userId, Long productId, String status) { System.out.println("[独立事务] 开始记录日志..."); Order logEntry = new Order(); // 借用Order表做日志 logEntry.setUserId(userId); logEntry.setProductId(productId); // 在订单表中金额 -1 表示日志,执行一次你会在订单表中看到两条记录,一条是订单,另一条是日志 logEntry.setAmount(new BigDecimal("-1")); // 这里可以设置一个状态字段,为了简化,我们就不加了 logMapper.insert(logEntry); System.out.println("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。"); } } ``` 启动类: ```java package com.feng.springtransactiondemo.service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.feng.springtransactiondemo.domain.Order; import com.feng.springtransactiondemo.domain.Product; import com.feng.springtransactiondemo.domain.Users; import com.feng.springtransactiondemo.mapper.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; @Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Autowired private UsersMapper usersMapper; // 用于记录日志的Mapper,我们直接用OrderMapper来模拟一个日志表 @Autowired private OrderMapper logMapper; // 假设这是一个独立的日志Mapper // 使用 @Lazy 是一个好习惯,可以防止在某些Bean初始化顺序下可能出现的循环依赖问题 @Autowired @Lazy private OrderService self; // 注入的是被Spring AOP代理过的对象 /** * 场景一:REQUIRED (默认值) * 外部方法事务,内部方法加入外部事务。任何一个失败,整体回滚。 */ @Transactional(propagation = Propagation.REQUIRED) public void placeOrder_REQUIRED(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景一:REQUIRED 传播 ---"); System.out.println("[外部事务] 开始下单..."); // 1. 创建订单 createOrder(userId, productId, amount); // 2. 扣减库存 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!"); } /** * 场景二:REQUIRES_NEW * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。 */ @Transactional(propagation = Propagation.REQUIRED) // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务 public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---"); System.out.println("[外部事务] 开始下单..."); try { // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 createOrder(userId, productId, amount); // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) , 这里的异常会开启新的子事务,子事务吞掉了一场,外部异常就没有回滚了 deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!uuid"); } catch (Exception e) { System.err.println("[外部事务] 下单失败: " + e.getMessage()); // 这里一定使用 self , 否则在同一个对象内,AOP 代理不会触发,自然不会开启一个新事务。 // 只有走了 AOP 代理,才会让 @Transactional 注解生效,从而开启recordLog的子事务。 self.recordLog(userId, productId, "下单失败"); // 抛出异常,重新触发外部事务回滚 throw e; } } // ============== 内部私有方法 ============== private void createOrder(Long userId, Long productId, BigDecimal amount) { Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setAmount(amount); orderMapper.insert(order); System.out.println("[内部方法] 创建订单成功,订单ID: " + order.getId()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void reduceStock(Long productId) { Product product = productMapper.selectById(productId); if (product.getStock() <= 0) { throw new RuntimeException("库存不足!"); } product.setStock(product.getStock() - 1); productMapper.updateById(product); System.out.println("[内部方法] 扣减库存成功,剩余库存: " + product.getStock()); } @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void deductBalance(Long userId, BigDecimal amount) { Users user = usersMapper.selectById(userId); if (user.getBalance().compareTo(amount) < 0) { throw new RuntimeException("用户余额不足!"); } user.setBalance(user.getBalance().subtract(amount)); usersMapper.updateById(user); System.out.println("[内部方法] 扣减余额成功,用户余额: " + user.getBalance()); // 模拟一个系统异常,导致事务回滚 if (true) { throw new RuntimeException("模拟系统异常:大额订单需要人工审核!"); } } /** * 记录日志,使用 REQUIRES_NEW 开启一个独立的事务 */ @Transactional(propagation = Propagation.REQUIRES_NEW) public void recordLog(Long userId, Long productId, String status) { System.out.println("[独立事务] 开始记录日志..."); Order logEntry = new Order(); // 借用Order表做日志 logEntry.setUserId(userId); logEntry.setProductId(productId); // 在订单表中金额 -1 表示日志,执行一次你会在订单表中看到两条记录,一条是订单,另一条是日志 logEntry.setAmount(new BigDecimal("-1")); // 这里可以设置一个状态字段,为了简化,我们就不加了 logMapper.insert(logEntry); System.out.println("[独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。"); } } ``` 最终日志: REQUIRED 传播是默认传播行为,子事务加入当前事务。REQUIRES_NEW 是子事务不加入当前事务,开启新事务。 ```java ======================================== Spring 事务传播行为实战演示 ======================================== ========== 场景一:REQUIRED 传播 ========== --- 重置数据 --- 用户余额、商品库存、订单记录已重置。 --- 场景一:REQUIRED 传播 --- [外部事务] 开始下单... [内部方法] 创建订单成功,订单ID: 1 [内部方法] 扣减库存成功,剩余库存: 9 [内部方法] 扣减余额成功,用户余额: 2000.00 主程序捕获到异常: 模拟系统异常:大额订单需要人工审核! --- 查看最终状态 --- 用户余额: 8000.00 <-- 余额回滚了 商品库存: 10 <-- 库存回滚了 订单记录数量: 0 <-- 订单也回滚了 ------------------- ========== 场景二:REQUIRES_NEW 传播 ========== --- 重置数据 --- 用户余额、商品库存、订单记录已重置。 --- 场景二:REQUIRES_NEW 传播 --- [外部事务] 开始下单... [内部方法] 创建订单成功,订单ID: 2 [内部方法] 扣减库存成功,剩余库存: 9 [内部方法] 扣减余额成功,用户余额: 2000.00 [外部事务] 下单失败: 模拟系统异常:大额订单需要人工审核! [独立事务] 开始记录日志... [独立事务] 日志记录成功!这条日志不会因为外部事务回滚而消失。 主程序捕获到异常: 模拟系统异常:大额订单需要人工审核! --- 查看最终状态 --- 用户余额: 8000.00 <-- 余额回滚了 商品库存: 10 <-- 库存回滚了 订单记录数量: 1 <-- 注意!这里有一条记录,就是日志! 订单记录: [Order{id=3, ...}] ------------------- ``` #### 结果分析 - **场景一 (**`**REQUIRED**`**)**: - - `**placeOrder_REQUIRED**` 开启了一个外部事务。 - `**reduceStock**` 和 `**deductBalance**` 都加入了这个外部事务。 - 当 `**deductBalance**` 抛出异常时,整个外部事务被标记为回滚。 - **结果**:创建订单、扣减库存、扣减余额这三个操作**全部被回滚**。数据库恢复到下单前的状态。这保证了业务的原子性。 - **场景二 (**`**REQUIRES_NEW**`**)**: - - `**placeOrder_REQUIRES_NEW**` 开启了外部事务。 - 当 `**deductBalance**` 抛出异常,外部事务被标记为回滚。 - 在 `**catch**` 块中,调用了 `**recordLog**` 方法。因为它使用了 `**REQUIRES_NEW**`,它会**挂起**当前失败的外部事务,并**开启一个全新的、独立的事务**。 - `**recordLog**` 执行成功,**新事务被提交**。 - `**recordLog**` 执行完毕,被挂起的外部事务恢复,然后执行回滚。 - **结果**:下单相关的操作(订单、库存、余额)全部回滚,但 `**recordLog**` 的操作**被成功提交**。这对于需要记录失败日志的场景非常有用,**即使主业务失败被回滚了,子事务也能保留关键的错误信息。** ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/mqh66p9zbrfv7R2c.svg) #### 有意思的异常 在最后的例子中,场景二发生了异常交给 catch 处理,如果在catch中,你不抛出异常给外部,那么本次事务算成功!发生了异常事务还算做成功?这是为什么呢? (关注最后的 throw e; 如果没有它,主业务会正常提交,不会回滚,结果不符合预期) ```java /** * 场景二:REQUIRES_NEW * 内部方法开启一个新事务 REQUIRES_NEW,独立于外部事务。即使外部事务回滚,内部事务也提交。 */ @Transactional(propagation = Propagation.REQUIRED) // 外部事务设置为 REQUIRED,表示如果当前存在事务,就加入该事务 public void placeOrder_REQUIRES_NEW(Long userId, Long productId, BigDecimal amount) { System.out.println("\n--- 场景二:REQUIRES_NEW 传播 ---"); System.out.println("[外部事务] 开始下单..."); try { // 1. 创建订单,调用 createOrder 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 createOrder(userId, productId, amount); // 2. 扣减库存,调用 reduceStock 方法,该方法使用 REQUIRES_NEW 传播行为,会开启一个新事务 reduceStock(productId); // 3. 扣减余额 (这里会模拟失败) , 这里的异常会开启新的子事务,子事务吞掉了一场,外部异常就没有回滚了 deductBalance(userId, amount); System.out.println("[外部事务] 下单成功!uuid"); } catch (Exception e) { System.err.println("[外部事务] 下单失败: " + e.getMessage()); // 这里一定使用 self , 否则在同一个对象内,AOP 代理不会触发,自然不会开启一个新事务。 // 只有走了 AOP 代理,才会让 @Transactional 注解生效,从而开启recordLog的子事务。 self.recordLog(userId, productId, "下单失败"); // 抛出异常,重新触发外部事务回滚 throw e; } } ``` 这里要了解异常的生命周期了,如果异常被 catch 处理,那么异常就销毁了,程序又恢复正常健康的继续执行,只不过try里面的代码块一定没有全部执行完。 **分析结果** 1. **异常产生**:`deductBalance` 方法内抛出一个 `RuntimeException`,这个异常向上传播,被 `placeOrder_REQUIRES_NEW` 方法中的 `catch (Exception e)` 块捕获。 ```java @Transactional(propagation = Propagation.REQUIRED) // 加入到外部事务 void deductBalance(Long userId, BigDecimal amount) { // 其它代码 .. // 模拟一个系统异常,导致事务回滚 if (true) { throw new RuntimeException("模拟系统异常:大额订单需要人工审核!"); } } ``` 1. **异常被“处理”**:一旦异常进入 `catch` 块,从 JVM 和调用栈的角度来看,这个异常就已经被“处理”了。程序不会再因为这个异常而崩溃,而是会继续执行 `catch` 块内部的代码。 2. `**catch**` **块执行**:`System.err.println(...)` 和 `self.recordLog(...)` 被执行。注意,`self.recordLog` 方法本身执行是成功的,它没有抛出任何新的异常。 3. `**catch**` **块结束**:如果没有 `throw e;`,`catch` 块就会正常结束。 事务是AOP实现的,那么此时Spring 的事务管理器(AOP 代理)像一个监控员,它在 `placeOrder_REQUIRES_NEW` 方法的外部观察着: - 它只关心一件事:当 `placeOrder_REQUIRES_NEW` 方法执行完毕时,它是正常结束的,还是因为抛出异常而结束的。 **场景分析:如果没有** `**throw e;**` 1. 代理开启事务 T1。 2. 代理调用目标方法 `placeOrder_REQUIRES_NEW`。 3. 方法内部发生异常,但被 `catch` 块捕获了。 4. `self.recordLog` 在新事务 T2 中成功执行并提交。 5. `catch` 块执行完毕,`placeOrder_REQUIRES_NEW` 方法正常结束。 6. 代理看到方法正常结束,它认为:“太好了,一切顺利,没有发生任何错误!” 7. 于是,代理提交事务 T1。 8. 结果:订单、库存、余额的修改全部被保存,这与我们期望的“失败回滚”背道而驰。 **场景分析:如果有** `**throw e;**` 1. 代理开启事务 T1。 2. 代理调用目标方法 `placeOrder_REQUIRES_NEW`。 3. 方法内部发生异常,被 `catch` 块捕获。 4. `self.recordLog` 在新事务 T2 中成功执行并提交。 5. `catch` 块执行到 `throw e;`,将之前捕获的那个异常重新抛出。 6. `placeOrder_REQUIRES_NEW` 方法因为抛出异常而结束。 7. 代理看到方法因异常而结束,它认为:“糟糕,出问题了,需要执行回滚操作!” 8. 于是,代理回滚事务 T1。 9. 结果:T1 中的所有操作(订单、库存、余额)都被撤销,而 T2 中的日志记录因为已经提交而保留下来。这正是我们想要的结果。

AOP 切面编程

什么是 AOP ? ========= `AOP`: * `Aspect Oriented Programming`(面向切面编程、面向方面编程),其实就是面向特定方法编程。 实现: * 动态代理是面向切面编程最主流的实现。而`SpringAOP`是`Spring`框架的高级技术,旨在管理`bean`对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。 AOP 核心概念 ======== * 连接点: `JoinPoint`, 可以被`AOP`控制的方法(暗含方法执行时的相关信息) * 通知: `Advice`, 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法) * 切入点: `PointCut`, 匹配连接点的条件,通知仅会在切入点方法执行时被应用 * 切面: `Aspect`, 描述通知与切入点的对应关系(通知+切入点) * 目标对象: `Target`, 通知所应用的对象 场景说明 ---- 例如现有一个场景:定位执行耗时较长的业务方法,统计各个业务层方法的执行耗时 ```java @Component @Aspect // 切面类 @Slf4j public class TimeAspect { @Around ("execution (* com.itheima.service.impl.DeptServiceImpl.list ())") // 切面表达式 public Object recordTime (ProceedingJoinPoint joinPoint) throws Throwable { long begin = System.currentTimeMillis (); // 调用原始操作 Object result = joinPoint.proceed (); long end = System.currentTimeMillis (); log.info("执行耗时 : {} ms", (end-begin)); return result; } } // 目标对象 @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper; // region-begin 连接点 @Override public List<Dept> list() { List<Dept> deptList = deptMapper.list(); // 切入点 return deptList; } @Override public void delete(Integer id) { deptMapper.delete(id); } @Override public void save(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now()); deptMapper.save(dept); } // region-end 连接点 } ``` 对于`aop`的五大核心概念,我们可以使用更加通俗易懂的类比来说明: 可以用 "学校检查卫生" 来类比: * **连接点**:学校里所有可能被检查的班级(每个班级都是一个潜在的检查点) * **通知**:检查卫生的具体操作流程(比如看地面是否干净、桌椅是否整齐,这是一套固定重复的动作) * **切入点**:筛选要检查的班级的条件(比如 "只查一年级的班级" 或 "只查偶数号的班级") * **切面**:检查计划(把 "检查流程" 和 "筛选条件" 结合起来,比如 "用标准流程检查所有一年级班级") * **目标对象**:最终被检查的那些班级(符合筛选条件,实际接受检查的对象) 简单来说就是:学校(`AOP`)要检查卫生(`通知`),所有的班级都可能被抽查到(`连接点`),但是只会查到一年级的(`切入点`),"用标准流程查一年级班级" 这个整体安排就是切面,而被查到的那些一年级班级就是目标对象。 ### 特别注意:连接点 * 在Spring中用`JoinPoint`抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。 * 对于 `@Around` 通知,获取连接点信息只能使用 `ProceedingJoinPoint` * 对于其他四种通知,获取连接点信息只能使用 `JoinPoint`,它是 `ProceedingJoinPoint` 的父类型 通知类型 ==== 通知类型 ---- 1. `**@Around**`**:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(常用)** 2. `@Before`:前置通知,此注解标注的通知方法在目标方法前被执行 3. `@After`:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行 4. `@AfterReturning`:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行 5. `@AfterThrowing`:异常后通知,此注解标注的通知方法发生异常后执行 注意事项 ---- * `@Around`环绕通知需要自己调用 `ProceedingJoinPoint.proceed()` 来让原始方法执行,其他通知不需要考虑目标方法执行 * `@Around`环绕通知方法的返回值,必须指定为`Object`,来接收原始方法的返回值。 通知顺序 ---- 当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。 ### 执行顺序 1. 不同切面类中,默认按照切面类的类名字母排序: * 目标方法前的通知方法:字母排名靠前的先执行 * 目标方法后的通知方法:字母排名靠前的后执行 1. 用 `@Order(数字)` 加在切面类上来控制顺序 * 目标方法前的通知方法:数字小的先执行 * 目标方法后的通知方法:数字小的后执行 `@PointCut` =========== 该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可。 ```java @Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") public void pt(){} @Around("pt()") public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable { // 方法体内容 } ``` 切入点表达式 ====== execution --------- `execution` 主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为: ```java execution(访问修饰符? 返回值 包名.类名.?方法名(方法参数) throws 异常?) ``` * 可以使用通配符描述切入点 * `*`:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分 `execution(* com.**.service.**.update*(*))` * `..`:多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数 `execution(* com.itheima..DeptService.*(..))` 切入点表达式-@annotation ------------------ * `@annotation`切入点表达式,用于匹配标识有特定注解的方法。 `@annotation(com.itheima.anno.Log)` ```java @Before("@annotation(com.itheima.anno.Log)") public void before(){ log.info("before ...."); } ``` Spring 实战代码演示 ============= 依赖导入 ---- 引入`aop`依赖 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 日志响应拦截 ------ 主要作用:对`controller`上的 ```java @Around("execution(* com.lantzuc.lanucbackend.controller.*.*(..))") public Object doInterceptor(ProceedingJoinPoint joinPoint) throws Throwable { // 开始计时 StopWatch stopWatch = new StopWatch(); stopWatch.start(); // 获取请求路径 RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); // 生成唯一请求id String requestId = UUID.randomUUID().toString(); String url = servletRequest.getRequestURI(); // 获取请求参数 Object[] args = joinPoint.getArgs(); String reqParams = "[" + StringUtils.join(args, ", ") + "]"; // 输出请求日志 log.info("request start,id: {}, path: {}, ip: {}, params: {}", requestId, url, servletRequest.getRemoteHost(), reqParams); // 执行原方法 Object result = joinPoint.proceed(); // 输出原日志 stopWatch.stop(); long totalTimeMillis = stopWatch.getTotalTimeMillis(); log.info("request end, id: {}, cost: {}ms", requestId, totalTimeMillis); return result; } ``` 结果显示 ```java 2025-10-15 17:13:48.649 INFO 23396 --- [nio-8080-exec-5] c.l.lanucbackend.aop.LogInterceptor : request start,id: ad4061ee-d5ed-47f3-bf2e-e0567867fabc, path: /api/user/login, ip: 0:0:0:0:0:0:0:1, params: [UseLoginRequest(userAccount=Lantz, userPassword=12345678), org.apache.catalina.connector.RequestFacade@6ce7aed7] 2025-10-15 17:13:49.265 INFO 23396 --- [nio-8080-exec-5] c.l.lanucbackend.aop.LogInterceptor : request end, id: ad4061ee-d5ed-47f3-bf2e-e0567867fabc, cost: 623ms ``` 相比原来没有添加日志拦截的,我们可以更加清晰地看到对某一路径发送请求的状态,比如请求路径,请求参数,IP 地址等等信息,而且我们还可以获悉到某一请求的执行时间是多少,可以在后续有针对的目的优化 权限响应拦截 ------ 首先要创建一个注解: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface AuthCheck { /** * 必须有某一个角色(默认无) * @return */ String mustRole() default ""; } ``` 然后再编写权限校验拦截代码: ```java @Around("@annotation(authCheck)") public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable { String mustRole = authCheck.mustRole(); RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes(); HttpServletRequest httpServletRequest = ((ServletRequestAttributes) requestAttributes).getRequest(); // 当前登录用户 User loginUer = userService.getLoginUer(httpServletRequest); UserRoleEnum mustRoleEnum = UserRoleEnum.getEnumByValue(mustRole); // 不需要权限,放行 if (mustRoleEnum == null) { return joinPoint.proceed(); } // 必须有该权限才放行 UserRoleEnum userRoleEnum = UserRoleEnum.getEnumByValue(loginUer.getUserRole()); if (userRoleEnum == null) { throw new BusinessException(ErrorCode.NO_AUTH); } // 如果被封号,直接拒绝 if (UserRoleEnum.BAN.equals(userRoleEnum)) { throw new BusinessException(ErrorCode.NO_AUTH); } // 必需有管理员权限 if (UserRoleEnum.ADMIN.equals(mustRoleEnum)) { // 用户没有管理员权限,拒绝 if (!UserRoleEnum.ADMIN.equals(userRoleEnum)) { throw new BusinessException(ErrorCode.NO_AUTH); } } // 通过管理权限,放行 return joinPoint.proceed(); } ``` 在`controller`中使用: ```java @GetMapping("/search") @AuthCheck(mustRole = ADMIN_ROLE) // 需要管理员权限 public List<User> searchUser(String userName, HttpServletRequest request){ QueryWrapper<User> queryWrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(userName)) { queryWrapper.like("userName", userName); } List<User> userList = userService.list(queryWrapper); return userList.stream().map(user -> userService.getSafetyUser(user)).collect(Collectors.toList()); } ``` 测试结果: 如果登录用户为管理员则正常通过,如果不是,则会报错: ![](https://pic.code-nav.cn/post_picture/1872161376428277761/CV9v0hGM6yeqKYV9.webp) `Postman Body`显示: ```json { "code": 40101, "data": null, "message": "无权限", "description": "" } ``` 记得如果使用了`jwt`鉴权,在`Postman`中测试的时候记得选择`Bearer Token`然后粘贴进去登录时候产生的`Token`

Spring进阶 - Spring AOP实现原理(三)JDK代理实现

上文我们学习了SpringAOP Cglib动态代理的实现,本文主要是SpringAOP JDK动态代理的案例和实现部分。 本文是AOP实现原理的最后一篇。 ## 什么是 JDK 代理? JDK动态代理是有JDK提供的工具类Proxy实现的,动态代理类是在运行时生成指定接口的代理类,每个代理实例(实现需要代理的接口)都有一个关联的调用处理程序对象,此对象实现了InvocationHandler,最终的业务逻辑是在InvocationHandler实现类的invoke方法上。 简单来说,对目标对象 UserService 接口(有一个实现类UserServiceImpl)使用 JDK 动态代理,那么会自动生成一个代理类对象,这个代理对象实现了 InvocationHandler。调用UserService 方法,业务逻辑走的是 InvocationHandler 的 invoke 方法,因为代理对象关联上目标对象了。 实战演示就懂了 ## JDK 实战 创建一个maven 项目,什么依赖都不用。 1. User 类 ```java public class User { private String name; private Integer age; public User() { } public User(String name, Integer age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } } ``` 1. 目标对象和实现类 ```java // 目标接口 public interface UserService { void addUser(); List<User> findUserList(); } // 目标对象实现 public class UserServiceImpl implements UserService { @Override public void addUser() { // do something } @Override public List<User> findUserList() { return Collections.singletonList(new User("fency", 18)); } } ``` 1. 创建一个代理,getLoggingProxy方法有一个 InvocationHandler 实现,代理的秘密都在 InvocationHandler 中实现。 ```java public class UserLogProxy { // 目标对象 /** * proxy target */ private UserServiceImpl target; /** * init. * * @param target target */ public UserLogProxy(UserServiceImpl target) { super(); this.target = target; } public UserService getLoggingProxy() { UserService proxy; ClassLoader loader = target.getClass().getClassLoader(); Class[] interfaces = new Class[]{UserService.class}; InvocationHandler h = new InvocationHandler() { /** * proxy: 代理对象。 一般不使用该对象 method: 正在被调用的方法 args: 调用方法传入的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); // log - before method System.out.println("[before] execute method: " + methodName); // call method Object result = null; try { // 前置通知 result = method.invoke(target, args); // 返回通知, 可以访问到方法的返回值 } catch (NullPointerException e) { e.printStackTrace(); // 异常通知, 可以访问到方法出现的异常 } // 后置通知. 因为方法可以能会出异常, 所以访问不到方法的返回值 // log - after method System.out.println("[after] execute method: " + methodName + ", return value: " + result); return result; } }; /** * loader: 代理对象使用的类加载器. * interfaces: 指定代理对象的类型. 即代理代理对象中可以有哪些方法. * h: 当具体调用代理对象的方法时, 应该如何进行响应, 实际上就是调用 InvocationHandler 的 invoke 方法 */ proxy = (UserService) Proxy.newProxyInstance(loader, interfaces, h); return proxy; } } ``` 1. 调用 ```java public class ProxyDemo { /** * main interface. * * @param args args */ public static void main(String[] args) { // proxy UserService userService = new UserLogProxy(new UserServiceImpl()).getLoggingProxy(); // call methods userService.findUserList(); userService.addUser(); } } ``` 测试结果 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/WWakPu9uyEleTzkw.webp) ## JDK代理的流程 JDK代理自动生成的class是由sun.misc.ProxyGenerator来生成的。 sun.misc.ProxyGenerator是什么?刚刚的代码没看懂这个类啊? `**sun.misc.ProxyGenerator**` **确实存在JDK中,但它不属于 Java 的公开 API。仅java 内部的 API 可以调用这个类,这是为了为了防止开发者误用它们。** 我们来解读 **ProxyGenerator** 源码**。** ### ProxyGenerator生成代码 我们看下sun.misc.ProxyGenerator生成代码的逻辑: ```java /** * Generate a proxy class given a name and a list of proxy interfaces. * * @param name the class name of the proxy class * @param interfaces proxy interfaces * @param accessFlags access flags of the proxy class */ public static byte[] generateProxyClass(final String name, Class<?>[] interfaces, int accessFlags) { ProxyGenerator gen = new ProxyGenerator(name, interfaces, accessFlags); final byte[] classFile = gen.generateClassFile(); ... } ``` generateClassFile方法如下。 代码很长,无非就是三步(把大象放冰箱分几步?): - 第一步:(把冰箱门打开)准备工作,将所有方法包装成ProxyMethod对象,包括Object类中hashCode、equals、toString方法,以及被代理的接口中的方法 - 第二步:(把大象装进去)为代理类组装字段,构造函数,方法,static初始化块等 - 第三步:(把冰箱门带上)写入class文件 ```java /** * Generate a class file for the proxy class. This method drives the * class file generation process. */ private byte[] generateClassFile() { /* 第一步:将所有方法包装成ProxyMethod对象 */ // 将Object类中hashCode、equals、toString方法包装成ProxyMethod对象 addProxyMethod(hashCodeMethod, Object.class); addProxyMethod(equalsMethod, Object.class); addProxyMethod(toStringMethod, Object.class); // 将代理类接口方法包装成ProxyMethod对象 for (Class<?> intf : interfaces) { for (Method m : intf.getMethods()) { addProxyMethod(m, intf); } } // 校验返回类型 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { checkReturnTypes(sigmethods); } /* 第二步:为代理类组装字段,构造函数,方法,static初始化块等 */ try { // 添加构造函数,参数是InvocationHandler methods.add(generateConstructor()); // 代理方法 for (List<ProxyMethod> sigmethods : proxyMethods.values()) { for (ProxyMethod pm : sigmethods) { // 字段 fields.add(new FieldInfo(pm.methodFieldName, "Ljava/lang/reflect/Method;", ACC_PRIVATE | ACC_STATIC)); // 上述ProxyMethod中的方法 methods.add(pm.generateMethod()); } } // static初始化块 methods.add(generateStaticInitializer()); } catch (IOException e) { throw new InternalError("unexpected I/O Exception", e); } if (methods.size() > 65535) { throw new IllegalArgumentException("method limit exceeded"); } if (fields.size() > 65535) { throw new IllegalArgumentException("field limit exceeded"); } /* 第三步:写入class文件 */ /* * Make sure that constant pool indexes are reserved for the * following items before starting to write the final class file. */ cp.getClass(dotToSlash(className)); cp.getClass(superclassName); for (Class<?> intf: interfaces) { cp.getClass(dotToSlash(intf.getName())); } /* * Disallow new constant pool additions beyond this point, since * we are about to write the final constant pool table. */ cp.setReadOnly(); ByteArrayOutputStream bout = new ByteArrayOutputStream(); DataOutputStream dout = new DataOutputStream(bout); try { /* * Write all the items of the "ClassFile" structure. * See JVMS section 4.1. */ // u4 magic; dout.writeInt(0xCAFEBABE); // u2 minor_version; dout.writeShort(CLASSFILE_MINOR_VERSION); // u2 major_version; dout.writeShort(CLASSFILE_MAJOR_VERSION); cp.write(dout); // (write constant pool) // u2 access_flags; dout.writeShort(accessFlags); // u2 this_class; dout.writeShort(cp.getClass(dotToSlash(className))); // u2 super_class; dout.writeShort(cp.getClass(superclassName)); // u2 interfaces_count; dout.writeShort(interfaces.length); // u2 interfaces[interfaces_count]; for (Class<?> intf : interfaces) { dout.writeShort(cp.getClass( dotToSlash(intf.getName()))); } // u2 fields_count; dout.writeShort(fields.size()); // field_info fields[fields_count]; for (FieldInfo f : fields) { f.write(dout); } // u2 methods_count; dout.writeShort(methods.size()); // method_info methods[methods_count]; for (MethodInfo m : methods) { m.write(dout); } // u2 attributes_count; dout.writeShort(0); // (no ClassFile attributes for proxy classes) } catch (IOException e) { throw new InternalError("unexpected I/O Exception", e); } return bout.toByteArray(); } ``` ### 从生成的Proxy代码看执行流程 从上述sun.misc.ProxyGenerator类中可以看到,这个类里面有一个配置参数`sun.misc.ProxyGenerator.saveGeneratedFiles`,可以通过这个参数将生成的Proxy类保存在本地,比如设置为true 执行后,生成的文件如下: ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/x2Ey3OTXwxdncSVX.webp)我们看下生成后的代码: ```java // // Source code recreated from a .class file by IntelliJ IDEA // (powered by FernFlower decompiler) // package com.sun.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.lang.reflect.UndeclaredThrowableException; import java.util.List; import com.fency.springframework.service.IUserService; // 所有类和方法都是final类型的 public final class $Proxy0 extends Proxy implements UserService { private static Method m1; private static Method m3; private static Method m2; private static Method m0; private static Method m4; // 构造函数注入 InvocationHandler public $Proxy0(InvocationHandler var1) throws { super(var1); } public final boolean equals(Object var1) throws { try { return (Boolean)super.h.invoke(this, m1, new Object[]{var1}); } catch (RuntimeException | Error var3) { throw var3; } catch (Throwable var4) { throw new UndeclaredThrowableException(var4); } } public final List findUserList() throws { try { return (List)super.h.invoke(this, m3, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final String toString() throws { try { return (String)super.h.invoke(this, m2, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final int hashCode() throws { try { return (Integer)super.h.invoke(this, m0, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } public final void addUser() throws { try { super.h.invoke(this, m4, (Object[])null); } catch (RuntimeException | Error var2) { throw var2; } catch (Throwable var3) { throw new UndeclaredThrowableException(var3); } } static { try { // 初始化 methods, 2个UserService接口中的方法,3个Object中的接口 m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object")); m3 = Class.forName("com.fency.springframework.service.UserService").getMethod("findUserList"); m2 = Class.forName("java.lang.Object").getMethod("toString"); m0 = Class.forName("java.lang.Object").getMethod("hashCode"); m4 = Class.forName("com.fency.springframework.service.UserService").getMethod("addUser"); } catch (NoSuchMethodException var2) { throw new NoSuchMethodError(var2.getMessage()); } catch (ClassNotFoundException var3) { throw new NoClassDefFoundError(var3.getMessage()); } } } ``` 这个代理类没干任何实事,它的唯一使命就是 **“转发”,**我来描述 App 调用 proxy.findUserList() 方法时,整体的工作流程,让你理解这个 proxy0 的职责。 1. 你调用 `**proxy.findUserList()**` 2. `**$Proxy0**` 的 `**findUserList()**` 方法被触发。 3. 它什么也不做,直接把调用请求(我是谁?调了啥方法?参数是啥?)打包,扔给它的“大脑”——`**InvocationHandler**` 的 `**invoke**` 方法。 4. 你的 `**InvocationHandler**` 的 `**invoke**` 方法开始执行。你可以在这里做任何事:打印日志、检查权限、开启事务、**调用真实对象的方法**等等。 5. 你的 `**invoke**` 方法执行完毕后,返回一个结果。 6. `**$Proxy0**` 的 `**findUserList()**` 方法接收到这个结果,把它 `**return**` 出去,最终回到你的手里。 很清晰的看见 **$Proxy0** 这个代理类就是个中转站**,**把 app 调用消息给 `**InvocationHandler**` ,`**InvocationHandler**` 结果给回 App. 至此,我们明白了 JDK 代理实现原理,循序渐进,来看 Spring AOP 中 JDK 代理的实现原理。 ## SpringAOP中JDK代理的实现 SpringAOP扮演的是JDK代理的创建和调用两个角色,我们通过这两个方向来看下SpringAOP的代码(JdkDynamicAopProxy类) ### SpringAOP Jdk代理的创建 代理的创建比较简单,调用getProxy方法,然后直接调用JDK中Proxy.newProxyInstance()方法将classloader和被代理的接口方法传入即可。这就是Spring AOP 框架的代理创建入口,这里进去就接上了本文前面提到的 ”JDK 代理的流程“。 这一个部分的源码我们在 “**AOP实现原理(二)“**文章中 **”创建代理的入口方法“** 扒过一次源码的**。** ```java @Override public Object getProxy() { return getProxy(ClassUtils.getDefaultClassLoader()); } @Override public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); } return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this); } ``` 这里就生成一个 $Proxy0 的代理对象,然后就要调用 invoke 执行代理。 ### SpringAOP Jdk代理的执行 在spring aop 框架中我们不用手写一个invoke,框架内部有一个 invoke 给$Proxy0 的代理对象调用,invoke 最终逻辑是开发者编写的切面编程的通知逻辑。 至此,SpringAOP 和 JDK 动态代理的执行流程打通了! ```java /** * Implementation of {@code InvocationHandler.invoke}. * <p>Callers will see exactly the exception thrown by the target, * unless a hook method throws an exception. */ @Override @Nullable public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; TargetSource targetSource = this.advised.targetSource; Object target = null; try { // 执行的是equal方法 if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { // The target does not implement the equals(Object) method itself. return equals(args[0]); } // 执行的是hashcode方法 else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { // The target does not implement the hashCode() method itself. return hashCode(); } // 如果是包装类,则dispatch to proxy config else if (method.getDeclaringClass() == DecoratingProxy.class) { // There is only getDecoratedClass() declared -> dispatch to proxy config. return AopProxyUtils.ultimateTargetClass(this.advised); } // 用反射方式来执行切点 else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && method.getDeclaringClass().isAssignableFrom(Advised.class)) { // Service invocations on ProxyConfig with the proxy config... return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); } Object retVal; if (this.advised.exposeProxy) { // Make invocation available if necessary. oldProxy = AopContext.setCurrentProxy(proxy); setProxyContext = true; } // Get as late as possible to minimize the time we "own" the target, // in case it comes from a pool. target = targetSource.getTarget(); Class<?> targetClass = (target != null ? target.getClass() : null); // 获取拦截链 List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); // Check whether we have any advice. If we don't, we can fallback on direct // reflective invocation of the target, and avoid creating a MethodInvocation. if (chain.isEmpty()) { // We can skip creating a MethodInvocation: just invoke the target directly // Note that the final invoker must be an InvokerInterceptor so we know it does // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { // We need to create a method invocation... MethodInvocation invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); // Proceed to the joinpoint through the interceptor chain. retVal = invocation.proceed(); } // Massage return value if necessary. Class<?> returnType = method.getReturnType(); if (retVal != null && retVal == target && returnType != Object.class && returnType.isInstance(proxy) && !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { // Special case: it returned "this" and the return type of the method // is type-compatible. Note that we can't help if the target sets // a reference to itself in another returned object. retVal = proxy; } else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { throw new AopInvocationException( "Null return value from advice does not match primitive return type for: " + method); } return retVal; } finally { if (target != null && !targetSource.isStatic()) { // Must have come from TargetSource. targetSource.releaseTarget(target); } if (setProxyContext) { // Restore old proxy. AopContext.setCurrentProxy(oldProxy); } } } ``` ### 小结 本文因为有一个代理对象和invoke方法,所有调用链比前面的文章理解起来稍微有点困难,这东西其实不用深入理解,作为开发者,仅需要知道 Spring AOP 的注解和几个术语就可以开发切面编程。稍微了解切面编程底层是 JDK 动态代理和Cglib 代理的技术原理即可。

Spring进阶 - Spring AOP实现原理(三)Cglib代理实现

我们在前文中已经介绍了SpringAOP的切面实现和创建动态代理的过程,那么动态代理是如何工作的呢?本文主要介绍Cglib动态代理的案例和SpringAOP实现的原理。 要了解动态代理是如何工作的,首先需要了解 - 什么是代理模式? - 什么是动态代理? - 什么是Cglib? - SpringAOP和Cglib是什么关系? ### 动态代理要解决什么问题? #### 什么是代理? **代理模式**(Proxy pattern): 为另一个对象提供一个替身或占位符以控制对这个对象的访问。从代码为例,就是我们调用的是目标对象的方法,但实际执行的是代理对象的方法,很奇怪,明明没调用代理对象,它非要执行。这是因为目标对象被代理了,一定会这样执行。 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/nNZjD1IlGtpVIV5C.png) 再举一个生活中的例子,你(调用方)去买房,你根本分不清他是中介(代理对象)还是房东(真实对象)。 #### 什么是动态代理? 动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。 在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,有一种技术可以动态拦截目标对象,在目标对象前后增加其它逻辑,这种技术就是动态代理技术。 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/eyCUT62ba5tw0xeV.webp) ### 什么是Cglib? SpringAOP和Cglib是什么关系? Cglib是一个强大的、高性能的代码生成包,它广泛被许多AOP框架使用,为他们提供方法的拦截。 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/KhD5iwzE7lprjo5A.webp) - 最底层是字节码 - ASM是操作字节码的工具 - cglib基于ASM字节码工具操作字节码(即动态生成代理,对方法进行增强) - SpringAOP基于cglib进行封装,实现cglib方式的动态代理 ## Cglib代理的案例 不借助 Spring AOP 框架,用 Cglib 一样可以实现代理,只是没有切面编程范式,就没有切入点、通知等功能,很不灵活,实现如下: ### pom包依赖 引入cglib的依赖包 ```xml <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>com-fency-spring-demos</artifactId> <groupId>com.fency</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>spring-framework-demo-aop-proxy-cglib</artifactId> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/cglib/cglib --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency> </dependencies> </project> ``` ### 定义实体 User ```java public class User { /** * user's name. */ private String name; /** * user's age. */ private int age; /** * init. * * @param name name * @param age age */ public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } } ``` ### 被代理的类 即目标类, 对被代理的类中的方法进行增强 ```java public class UserServiceImpl { /** * find user list. * * @return user list */ public List<User> findUserList() { return Collections.singletonList(new User("fency", 18)); } /** * add user */ public void addUser() { // do something } } ``` ### cglib代理 cglib代理类,需要实现MethodInterceptor接口,并指定代理目标类target ```java public class UserLogProxy implements MethodInterceptor { /** * 业务类对象,供代理方法中进行真正的业务方法调用 */ private Object target; public Object getUserLogProxy(Object target) { //给业务对象赋值 this.target = target; //创建加强器,用来创建动态代理类 Enhancer enhancer = new Enhancer(); //为加强器指定要代理的业务类(即:为下面生成的代理类指定父类) enhancer.setSuperclass(this.target.getClass()); //设置回调:对于代理类上所有方法的调用,都会调用CallBack,而Callback则需要实现intercept()方法进行拦截 enhancer.setCallback(this); // 创建动态代理类对象并返回 return enhancer.create(); } // 实现回调方法 @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // log - before method System.out.println("[before] execute method: " + method.getName()); // call method Object result = proxy.invokeSuper(obj, args); // log - after method System.out.println("[after] execute method: " + method.getName() + ", return value: " + result); return null; } } ``` ### 使用代理 启动类中指定代理目标并执行。 ```java public class ProxyDemo { /** * main interface. * * @param args args */ public static void main(String[] args) { // proxy UserServiceImpl userService = (UserServiceImpl) new UserLogProxy().getUserLogProxy(new UserServiceImpl()); // call methods userService.findUserList(); userService.addUser(); } } ``` ### 简单测试 我们启动上述类main 函数,执行的结果如下: ```bash [before] execute method: findUserList [after] execute method: findUserList, return value: [User{name='fency', age=18}] [before] execute method: addUser [after] execute method: addUser, return value: null ``` ## Cglib代理的流程 我们把上述Demo的主要流程画出来,你便能很快理解 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/lrnTfV9BdtGyVuwz.webp) ## SpringAOP中Cglib代理的实现 SpringAOP封装了cglib,通过其进行动态代理的创建。 CglibAopProxy 是 Spring 框架的一个工具类,作用是**统一管理和执行CGLIB代理逻辑**。 Spring 6.x 没有 CglibAopProxy ,取而代之的是 DefaultAopProxyFactory ,本文仅学习 CglibAopProxy 源码。 我们看下CglibAopProxy的getProxy方法 ```java @Override public Object getProxy() { return getProxy(null); } @Override public Object getProxy(@Nullable ClassLoader classLoader) { if (logger.isTraceEnabled()) { logger.trace("Creating CGLIB proxy: " + this.advised.getTargetSource()); } try { Class<?> rootClass = this.advised.getTargetClass(); Assert.state(rootClass != null, "Target class must be available for creating a CGLIB proxy"); // 上面流程图中的目标类 Class<?> proxySuperClass = rootClass; if (rootClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) { proxySuperClass = rootClass.getSuperclass(); Class<?>[] additionalInterfaces = rootClass.getInterfaces(); for (Class<?> additionalInterface : additionalInterfaces) { this.advised.addInterface(additionalInterface); } } // Validate the class, writing log messages as necessary. validateClassIfNecessary(proxySuperClass, classLoader); // 重点看这里,就是上图的enhancer,设置各种参数来构建 Enhancer enhancer = createEnhancer(); if (classLoader != null) { enhancer.setClassLoader(classLoader); if (classLoader instanceof SmartClassLoader && ((SmartClassLoader) classLoader).isClassReloadable(proxySuperClass)) { enhancer.setUseCache(false); } } enhancer.setSuperclass(proxySuperClass); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(classLoader)); // 设置callback回调接口,即方法的增强点 Callback[] callbacks = getCallbacks(rootClass); Class<?>[] types = new Class<?>[callbacks.length]; for (int x = 0; x < types.length; x++) { types[x] = callbacks[x].getClass(); } // 上节说到的filter enhancer.setCallbackFilter(new ProxyCallbackFilter( this.advised.getConfigurationOnlyCopy(), this.fixedInterceptorMap, this.fixedInterceptorOffset)); enhancer.setCallbackTypes(types); // 重点:创建proxy和其实例 return createProxyClassAndInstance(enhancer, callbacks); } catch (CodeGenerationException | IllegalArgumentException ex) { throw new AopConfigException("Could not generate CGLIB subclass of " + this.advised.getTargetClass() + ": Common causes of this problem include using a final class or a non-visible class", ex); } catch (Throwable ex) { // TargetSource.getTarget() failed throw new AopConfigException("Unexpected AOP exception", ex); } } ``` 重点关注 getCallbacks(rootClass) 方法,下面给出源码。 提几个下面这段源码的关键点: - `rootClass`: 即目标代理类 - `advised`: 包含上文中我们获取到的advisor增强器的集合 - `exposeProxy`: 在xml配置文件中配置的,作用就是如果在事务A中使用了代理,事务A调用了目标类的方法a,在方法a中又调用目标类的方法b,方法a,b同时都是要被增强的方法,如果不配置exposeProxy属性,方法b的增强将会失效,如果配置exposeProxy,方法b在方法a的执行中也会被增强了 - `DynamicAdvisedInterceptor`: 拦截器将advised(包含上文中我们获取到的advisor增强器)构建配置的AOP的callback(第一个callback) - `targetInterceptor`: xml配置的optimize属性使用的(第二个callback) - 最后连同其它5个默认的Interceptor 返回作为cglib的拦截器链,之后通过CallbackFilter的accpet方法返回的索引从这个集合中返回对应的拦截增强器执行增强操作 ```java private Callback[] getCallbacks(Class<?> rootClass) throws Exception { // Parameters used for optimization choices... boolean exposeProxy = this.advised.isExposeProxy(); boolean isFrozen = this.advised.isFrozen(); boolean isStatic = this.advised.getTargetSource().isStatic(); // Choose an "aop" interceptor (used for AOP calls). Callback aopInterceptor = new DynamicAdvisedInterceptor(this.advised); // Choose a "straight to target" interceptor. (used for calls that are // unadvised but can return this). May be required to expose the proxy. Callback targetInterceptor; if (exposeProxy) { targetInterceptor = (isStatic ? new StaticUnadvisedExposedInterceptor(this.advised.getTargetSource().getTarget()) : new DynamicUnadvisedExposedInterceptor(this.advised.getTargetSource())); } else { targetInterceptor = (isStatic ? new StaticUnadvisedInterceptor(this.advised.getTargetSource().getTarget()) : new DynamicUnadvisedInterceptor(this.advised.getTargetSource())); } // Choose a "direct to target" dispatcher (used for // unadvised calls to static targets that cannot return this). Callback targetDispatcher = (isStatic ? new StaticDispatcher(this.advised.getTargetSource().getTarget()) : new SerializableNoOp()); Callback[] mainCallbacks = new Callback[] { aopInterceptor, // targetInterceptor, // invoke target without considering advice, if optimized new SerializableNoOp(), // no override for methods mapped to this targetDispatcher, this.advisedDispatcher, new EqualsInterceptor(this.advised), new HashCodeInterceptor(this.advised) }; Callback[] callbacks; // If the target is a static one and the advice chain is frozen, // then we can make some optimizations by sending the AOP calls // direct to the target using the fixed chain for that method. if (isStatic && isFrozen) { Method[] methods = rootClass.getMethods(); Callback[] fixedCallbacks = new Callback[methods.length]; this.fixedInterceptorMap = CollectionUtils.newHashMap(methods.length); // TODO: small memory optimization here (can skip creation for methods with no advice) for (int x = 0; x < methods.length; x++) { Method method = methods[x]; List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, rootClass); fixedCallbacks[x] = new FixedChainStaticTargetInterceptor( chain, this.advised.getTargetSource().getTarget(), this.advised.getTargetClass()); this.fixedInterceptorMap.put(method, x); } // Now copy both the callbacks from mainCallbacks // and fixedCallbacks into the callbacks array. callbacks = new Callback[mainCallbacks.length + fixedCallbacks.length]; System.arraycopy(mainCallbacks, 0, callbacks, 0, mainCallbacks.length); System.arraycopy(fixedCallbacks, 0, callbacks, mainCallbacks.length, fixedCallbacks.length); this.fixedInterceptorOffset = mainCallbacks.length; } else { callbacks = mainCallbacks; } return callbacks; } ``` 可以结合调试,方便理解 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/00DOLHMaxeAR0idS.webp)

Spring进阶 - Spring AOP实现原理(二)AOP代理的创建

上一篇文章熟悉后,创建代理对象的源码5分钟能看完。 上一篇文章我们通过阅读源码学会切面实现原理,将切面类的所有切面方法根据使用的注解生成对应Advice,并将Advice连同切入点匹配器和切面类等信息一并封装到Advisor。本文在此基础上继续介绍,代理(cglib代理和JDK代理)的创建过程。 ## 引言 AOP 切面实现后才是代理的创建,先快速回顾切面实现原理。 **阶段一 : 标签解析** 1. Spring 启动,IOC 容器从加载标签 XML 标签开始,XML 标签声明了多个自定义实例和 AOP 切面实例,IOC 容器加载所有的 XML 。 2. 在 AopNamespaceHandler 中有一个 init()方法,我们发现每一个 aop 标签都有一个解析器,所以当你在 xml 使用 aop 这些标签,会被解析成 AOP 代理。 3. config 配置标签的解析,初始化 ConfigBeanDefinitionParser 类,执行内部 parse 方法。 4. aspect-autoproxy 标签解析,初始一个解析类,内部有 parse 方法,但是一直调用下去会发现 AOP的创建工作是交给 AnnotationAwareAspectJAutoProxyCreator 类完成 **阶段二:**`**postProcessBeforeInstantiation**` **方法** 1. 方法内有两个重要的判断,判断是否给这个实例加 aop 的代理能力。 1. 1. 是否是 AOP 基础类?是基础类就不加 aop 能力。 2. 是否跳过?跳过就不加 aop能力 1. 如果实例是基础类,且要被跳过,那么它是 aop 切面类,把切面转成 Advisor 对象,内部会调用一个 getAdvisor() 的方法来转换。 2. 在 getAdvisor() 方法又会调用 getPointcut() 方法获取表达式切点。最后转成出来一个 advisor 对象。 3. advisor 对象是 advice 的包装类,所以我们要获取 advice 信息,封装到 advisor 内部。 大费周章得到的 advisor 是什么? advisor 约等于通知(Advice)和 切入点(Pointcut),一个 advisor 是 AOP 设计思想的两个术语的具体实现。一个切面类有多个通知,就有多个切点表达式,那么就有多个 advisor. **AOP 代理实现逻辑在 postProcessAfterInstantiation 方法内,通过这个方法看**Spring AOP的代理(cglib或jdk)的创建过程。 ## 文字描述阅读源码流程 1. 一个Bean实例化之后会调用 postProcessAfterInitialization 方法 2. wrapIfNecessary方法主要用于判断是否需要创建代理,如果Bean能够获取到advisor才需要创建代理 3. 获取所有 advisor 对象,但只取够应用于beanClass的Advisor 4. 如果有advisor,则说明需要增强,即需要创建代理对象。 5. 最后执行 DefaultAopProxyFactory.createAopProxy() 创建代理对象。 ## 代理的创建 一个Bean实例化之后会调用 postProcessAfterInitialization 方法,这个方法会判断这个 Bean 是否需要代理,如果要那么就会创建代理对象,接下来详细看源码实现。 ```java /** * Create a proxy with the configured interceptors if the bean is * identified as one to proxy by the subclass. * @see #getAdvicesAndAdvisorsForBean */ @Override public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); // 如果不是提前暴露的代理,这个判断用来处理循环依赖 if (this.earlyProxyReferences.remove(cacheKey) != bean) { // 核心逻辑:如果需要,就包装(代理)这个Bean return wrapIfNecessary(bean, beanName, cacheKey); } } return bean; } ``` wrapIfNecessary方法主要用于判断是否需要创建代理,如果Bean能够获取到advisor才需要创建代理 ```java /** * Wrap the given bean if necessary, i.e. if it is eligible for being proxied. * @param bean the raw bean instance * @param beanName the name of the bean * @param cacheKey the cache key for metadata access * @return a proxy wrapping the bean, or the raw bean instance as-is */ protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { // 如果bean是通过TargetSource接口获取 if (beanName != null && this.targetSourcedBeans.contains(beanName)) { return bean; } // 如果bean是切面类 if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; } // 如果是aop基础类?是否跳过? if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } // 重点:获取所有advisor,如果没有获取到,那说明不要进行增强,也就不需要代理了。 Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); if (specificInterceptors != DO_NOT_PROXY) { this.advisedBeans.put(cacheKey, Boolean.TRUE); // 重点:创建代理 Object proxy = createProxy( bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; } ``` ### 获取所有的Advisor 我们看下获取所有advisor的方法getAdvicesAndAdvisorsForBean ```java @Override @Nullable protected Object[] getAdvicesAndAdvisorsForBean( Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) { List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName); if (advisors.isEmpty()) { return DO_NOT_PROXY; } return advisors.toArray(); } ``` 通过findEligibleAdvisors方法获取advisor, 如果获取不到返回DO_NOT_PROXY(不需要创建代理),findEligibleAdvisors方法如下 ```java /** * Find all eligible Advisors for auto-proxying this class. * @param beanClass the clazz to find advisors for * @param beanName the name of the currently proxied bean * @return the empty List, not {@code null}, * if there are no pointcuts or interceptors * @see #findCandidateAdvisors * @see #sortAdvisors * @see #extendAdvisors */ protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { // 和上文一样,获取所有切面类的切面方法生成Advisor List<Advisor> candidateAdvisors = findCandidateAdvisors(); // 找到这些Advisor中能够应用于beanClass的Advisor List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); // 如果需要,交给子类拓展 extendAdvisors(eligibleAdvisors); // 对Advisor排序 if (!eligibleAdvisors.isEmpty()) { eligibleAdvisors = sortAdvisors(eligibleAdvisors); } return eligibleAdvisors; } ``` 这里内部有一个获取所有切面类的切面方法,生成Advisor ```java /** * Find all candidate Advisors to use in auto-proxying. * @return the List of candidate Advisors */ protected List<Advisor> findCandidateAdvisors() { Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); return this.advisorRetrievalHelper.findAdvisorBeans(); } ``` 找到这些Advisor中能够应用于beanClass的Advisor , 这一步代码长,但没啥好说的,就是返回符合要求的 advisor。 ```java /** * Determine the sublist of the {@code candidateAdvisors} list * that is applicable to the given class. * @param candidateAdvisors the Advisors to evaluate * @param clazz the target class * @return sublist of Advisors that can apply to an object of the given class * (may be the incoming List as-is) */ public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) { if (candidateAdvisors.isEmpty()) { return candidateAdvisors; } List<Advisor> eligibleAdvisors = new ArrayList<>(); for (Advisor candidate : candidateAdvisors) { // 通过Introduction实现的advice if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { eligibleAdvisors.add(candidate); } } boolean hasIntroductions = !eligibleAdvisors.isEmpty(); for (Advisor candidate : candidateAdvisors) { if (candidate instanceof IntroductionAdvisor) { // already processed continue; } // 是否能够应用于clazz的Advice if (canApply(candidate, clazz, hasIntroductions)) { eligibleAdvisors.add(candidate); } } return eligibleAdvisors; } ``` 然后代码流程就走到了创建代理对象了。 ### 创建代理的入口方法 获取所有advisor后,如果有advisor,则说明需要增强,即需要创建代理,创建代理的方法如下: ```java /** * Create an AOP proxy for the given bean. * @param beanClass the class of the bean * @param beanName the name of the bean * @param specificInterceptors the set of interceptors that is * specific to this bean (may be empty, but not null) * @param targetSource the TargetSource for the proxy, * already pre-configured to access the bean * @return the AOP proxy for the bean * @see #buildAdvisors */ protected Object createProxy(Class<?> beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { if (this.beanFactory instanceof ConfigurableListableBeanFactory) { AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); } ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.copyFrom(this); if (proxyFactory.isProxyTargetClass()) { // Explicit handling of JDK proxy targets (for introduction advice scenarios) if (Proxy.isProxyClass(beanClass)) { // Must allow for introductions; can't just set interfaces to the proxy's interfaces only. for (Class<?> ifc : beanClass.getInterfaces()) { proxyFactory.addInterface(ifc); } } } else { // No proxyTargetClass flag enforced, let's apply our default checks... if (shouldProxyTargetClass(beanClass, beanName)) { proxyFactory.setProxyTargetClass(true); } else { evaluateProxyInterfaces(beanClass, proxyFactory); } } Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); proxyFactory.addAdvisors(advisors); proxyFactory.setTargetSource(targetSource); customizeProxyFactory(proxyFactory); proxyFactory.setFrozen(this.freezeProxy); if (advisorsPreFiltered()) { proxyFactory.setPreFiltered(true); } // Use original ClassLoader if bean class not locally loaded in overriding class loader ClassLoader classLoader = getProxyClassLoader(); if (classLoader instanceof SmartClassLoader && classLoader != beanClass.getClassLoader()) { classLoader = ((SmartClassLoader) classLoader).getOriginalClassLoader(); } return proxyFactory.getProxy(classLoader); } ``` 这里最终是调用了 proxyFactory 对象,看一眼 proxyFactory 的继承图,没啥特殊的,看看有没有你熟悉的组件 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/jsIgotP5AcjbygcP.webp) 最后一行的 getProxy() 方法源码: ```java /** * Create a new proxy according to the settings in this factory. * <p>Can be called repeatedly. Effect will vary if we've added * or removed interfaces. Can add and remove interceptors. * <p>Uses the given class loader (if necessary for proxy creation). * @param classLoader the class loader to create the proxy with * (or {@code null} for the low-level proxy facility's default) * @return the proxy object */ public Object getProxy(@Nullable ClassLoader classLoader) { return createAopProxy().getProxy(classLoader); } ``` 这里的 createAopProxy()方法是先执行,它是真正的创建代理对象的方法,DefaultAopProxyFactory.createAopProxy。 ### 依据条件创建代理(jdk或cglib) DefaultAopProxyFactory.createAopProxy: ```java @Override public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (!NativeDetector.inNativeImage() && (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config))) { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: " + "Either an interface or a target is required for proxy creation."); } // 目标类本身是一个接口 || 目标类已经是一个JDK代理对象(这种情况发生在代理嵌套时) if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { // 返回 JDK 动态代理对象 return new JdkDynamicAopProxy(config); } // 否则返回 Cglib 动态代理 return new ObjenesisCglibAopProxy(config); } else { return new JdkDynamicAopProxy(config); } } ``` 到这就明白了八股文里面的 Spring Boot 优先返回哪个代理对象的问题。 在 Spring Framework 6.x 我的项目截取出来的源码目前是这样的: ```java public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { if (NativeDetector.inNativeImage() || !config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) { return new JdkDynamicAopProxy(config); } else { Class<?> targetClass = config.getTargetClass(); if (targetClass == null) { throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation."); } else { return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config)); } } } ``` 第一个 if 有四个判断条件, if 成立则返回 JDK 代理,说明Spring 是优先使用 JDK 代理。条件含义是: - 检测当前应用是否运行在 **GraalVM Native Image** 环境中。GraalVM是一个将Java代码提前编译成本地可执行文件的技术。在这种环境下,CGLIB这种运行时生成字节码的技术会受到很大限制甚至不可用。因此,如果检测到是Native Image环境,此条件true , Spring会强制使用JDK动态代理。 - 检查是否**没有**开启“优化”模式。优化配置如:`**@EnableAspectJAutoProxy(proxyTargetClass = false, optimize = true)**` ,那么此条件 true。它告诉Spring:“请尽可能地对代理进行优化”。 - 检查是否**没有**强制要求代理目标类。对应配置项对应 `**@EnableAspectJAutoProxy(proxyTargetClass = false)**`。 表示没有强制要求,此条件 true,Spring会优先考虑使用JDK动态代理。 - **是否没有任何用户提供的接口**,如果有,那么此条件 true , 优先考虑 JDK动态代理。 如果第一个 if 不满足,进入else , else 中如果目标对象实现了接口,也是优先考虑 JDK 。 到这里我们就把 AOP 的切面实现原理和AOP的创建代理对象原理讲清楚了。

Spring进阶 - Spring AOP实现原理(一)AOP切面实现原理

紧跟前文,前面我们学习了 AOP 基础,并进阶学习 IOC 初始化过程和 Bean 的生命周期等,而Spring AOP也是基于IOC的Bean加载来实现的。 本文介绍Spring AOP 原理解析,切面的实现过程。将切面类的所有切面方法根据使用的注解生成对应Advice,并将Advice连同切入点匹配器和切面类等信息一并封装到Advisor,为后续交给代理增强实现做准备的过程。 本文主要以阅读源码为主,与 IOC 容器初始化文章的源码阅读一样,本文也描述了源码阅读的流程,类似于地图,阅读源码迷路时看一下文字流程。 ## 引言 我们应该从哪里入手看Spring AOP源码?和我们上文分析的IOC源码实现有什么关系呢? 前文聊到Spring AOP配置方式有 XML 和注解两种方式,XML核心配置如下: ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/X7hlgZCHjvc7lLt8.webp) 可以看出AOP配置在Bean下,表明AOP是基于IOC容器加载来完成,这便是我们探索AOP的主要入口。 启动应用,初始化。 然后我们就能找到如下**初始化的流程和aop对应的handler**类,即parseCustomElement方法找到解析 `aop:aspectj-autoproxy`的handler(org.springframework.aop.config.AopNamespaceHandler) ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/xzyM5HFJ5j6ssrz2.webp) 其实你会发现,最重要的是知识点的关联关系,而不是知识点本身,单个知识点学一会谁学不会,前面的知识学好了,后续知识点不就是跑跑程序打个断点慢慢看的事了么? 入口找到,开始本文学习之旅。 ### 上面的示例代码完整提供 初始化 Spring 项目,引入依赖 pom.xml , 最好是 java8 版本 ```xml <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectjweaver.version}</version> </dependency> </dependencies> ``` User 实体类 ```java public class User { /** * user's name. */ private String name; /** * user's age. */ private int age; /** * init. * * @param name name * @param age age */ public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } ``` dao 层代码 ```java @Repository public class UserDaoImpl { /** * init. */ public UserDaoImpl() { } /** * mocked to find user list. * * @return user list */ public List<User> findUserList() { return Collections.singletonList(new User("fency", 18)); } } ``` Service 类 ```java @Service public class UserServiceImpl { /** * user dao impl. */ @Autowired private UserDaoImpl userDao; /** * init. */ public UserServiceImpl() { } /** * find user list. * * @return user list */ public List<User> findUserList() { return this.userDao.findUserList(); } /** * set dao. * * @param userDao user dao */ public void setUserDao(UserDaoImpl userDao) { this.userDao = userDao; } } ``` 切面类 ```java package com.fency.springframework.aspect; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.reflect.MethodSignature; import java.lang.reflect.Method; public class LogAspect { /** * aspect for every methods under service package. */ public Object businessService(ProceedingJoinPoint pjp) throws Throwable { // get attribute through annotation Method method = ((MethodSignature) pjp.getSignature()).getMethod(); System.out.println("execute method: " + method.getName()); // continue to process return pjp.proceed(); } } ``` XML 配置 aspect.xml ```xml <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 配置LogAspect切面 --> <bean id="logAspect" class="com.fency.springframework.aspect.LogAspect"/> <!-- 配置 AOP --> <aop:config> <!-- 定义切面 --> <aop:aspect id="logAspect" ref="logAspect"> <!-- 定义切入点 --> <aop:pointcut id="userServiceMethods" expression="execution(* com.fency.springframework.service.*.*(..))"/> <!-- 配置环绕通知 --> <aop:around method="businessService" pointcut-ref="userServiceMethods"/> </aop:aspect> </aop:config> </beans> ``` daos.xml ```java <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 启用注解扫描 --> <context:component-scan base-package="com.fency.springframework.dao"/> <!-- 配置UserDaoImpl bean --> <bean id="userDao" class="com.fency.springframework.dao.UserDaoImpl"/> </beans> ``` services.xml ```java <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 启用注解扫描 --> <context:component-scan base-package="com.fency.springframework.service"/> <!-- 配置UserServiceImpl bean,注入userDao依赖 --> <bean id="userService" class="com.fency.springframework.service.UserServiceImpl"> <property name="userDao" ref="userDao"/> </bean> </beans> ``` APP 启动类 ```java public class App { /** * main interfaces. * * @param args args */ public static void main(String[] args) { // create and configure beans ApplicationContext context = new FileSystemXmlApplicationContext("aspects.xml", "daos.xml", "services.xml"); // retrieve configured instance UserServiceImpl service = context.getBean("userService", UserServiceImpl.class); // use configured instance List<User> userList = service.findUserList(); // print info from beans userList.forEach(a -> System.out.println(a.getName() + "," + a.getAge())); } } ``` ## 文字描述阅读源码流程(XML为例) 为了避免阅读源码时迷路,提前给大家提供地图,这是接下来我们要做的事情,现在看不懂是正常的,迷路了别忘记回来看这里。 **阶段一 : 标签解析** 1. Spring 启动,IOC 容器从加载标签 XML 标签开始,XML 标签声明了多个自定义实例和 AOP 切面实例,IOC 容器加载所有的 XML 。 2. 在 AopNamespaceHandler 中有一个 init()方法,我们发现每一个 aop 标签都有一个解析器,所以当你在 xml 使用 aop 这些标签,会被解析成 AOP 代理。 3. config 配置标签的解析,初始化 ConfigBeanDefinitionParser 类,执行内部 parse 方法。 4. aspect-autoproxy 标签解析,初始一个解析类,内部有 parse 方法,但是一直调用下去会发现 AOP的创建工作是交给 AnnotationAwareAspectJAutoProxyCreator 类完成 `AnnotationAwareAspectJAutoProxyCreator` 有两个重要的方法: `**postProcessBeforeInstantiation**` **(本文详细讲解)**和 **postProcessAfterInstantiation(下一篇文章讲解)。**到这里就**挖到宝了**。 **阶段二:**`**postProcessBeforeInstantiation**` **方法** 1. 方法内有两个重要的判断,判断是否给这个实例加 aop 的代理能力。 1. 1. 是否是 AOP 基础类?是基础类就不加 aop 能力。 2. 是否跳过?跳过就不加 aop能力 1. 如果实例是基础类,且要被跳过,那么它是 aop 切面类,把切面转成 Advisor 对象,内部会调用一个 getAdvisor() 的方法来转换。 2. 在 getAdvisor() 方法又会调用 getPointcut() 方法获取表达式切点。最后转成出来一个 advisor 对象。 3. advisor 对象是 advice 的包装类,所以我们要获取 advice 信息,封装到 advisor 内部。 大费周章得到的 advisor 是什么? advisor 约等于通知(Advice)和 切入点(Pointcut),一个 advisor 是 AOP 设计思想的两个术语的具体实现。一个切面类有多个通知,就有多个切点表达式,那么就有多个 advisor. 这就是`**postProcessBeforeInstantiation**` 方法做的事情,第一步的判断确保不会给 aop 切面类实例加 aop 代理能力,避免无限递归。后面的步骤是把 aop 切面实例转成一个 advisor 对象。 **总结流程:** 解析标签 -> 解析时调用 `postProcessBeforeInstantiation` 方法 -> 得到一个 advisor 对象。 有了Adisor, 注入到合适的位置并交给代理(cglib和jdk)实现了。这里是在 **postProcessAfterInstantiation** 执行,也就是下一篇文章讲解的部分。 接下来按照这个流程,看源码。 ## aop配置标签的解析 我们找到了AopNamespaceHandler,其实就是注册BeanDefinition的解析器BeanDefinitionParser,将`aop:xxxxxx`配置标签交给指定的parser来处理。 看一下 AopNamespaceHandler 的源码,将XML的aop:xxx配置标签交给Parser来处理。 ```java public class AopNamespaceHandler extends NamespaceHandlerSupport { /** * Register the {@link BeanDefinitionParser BeanDefinitionParsers} for the * '{@code config}', '{@code spring-configured}', '{@code aspectj-autoproxy}' * and '{@code scoped-proxy}' tags. */ @Override public void init() { // In 2.0 XSD as well as in 2.5+ XSDs // 注册解析<aop:config> 配置 registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser()); // 注册解析<aop:aspectj-autoproxy> 配置 registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator()); // Only in 2.0 XSD: moved to context namespace in 2.5+ registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); } } ``` 我们来对 config配置标签和aspectj-autoproxy标签解析。 ### config配置标签的解析 `<aop:config/>`由ConfigBeanDefinitionParser这个类处理(java8才有这个类),作为parser类最重要的就是parse方法,源码: ```java @Override @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); parserContext.pushContainingComponent(compositeDef); configureAutoProxyCreator(parserContext, element); List<Element> childElts = DomUtils.getChildElements(element); for (Element elt: childElts) { String localName = parserContext.getDelegate().getLocalName(elt); if (POINTCUT.equals(localName)) { parsePointcut(elt, parserContext); } else if (ADVISOR.equals(localName)) { parseAdvisor(elt, parserContext); } else if (ASPECT.equals(localName)) { parseAspect(elt, parserContext); } } parserContext.popAndRegisterContainingComponent(); return null; } ``` 打个断点看看 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/JmhgNjH5Me6EPcg3.webp) parseAspect的方法如下, 处理方法不难,我这里就不展开了 ```java private void parseAspect(Element aspectElement, ParserContext parserContext) { String aspectId = aspectElement.getAttribute(ID); String aspectName = aspectElement.getAttribute(REF); try { this.parseState.push(new AspectEntry(aspectId, aspectName)); List<BeanDefinition> beanDefinitions = new ArrayList<>(); List<BeanReference> beanReferences = new ArrayList<>(); List<Element> declareParents = DomUtils.getChildElementsByTagName(aspectElement, DECLARE_PARENTS); for (int i = METHOD_INDEX; i < declareParents.size(); i++) { Element declareParentsElement = declareParents.get(i); beanDefinitions.add(parseDeclareParents(declareParentsElement, parserContext)); } // We have to parse "advice" and all the advice kinds in one loop, to get the // ordering semantics right. NodeList nodeList = aspectElement.getChildNodes(); boolean adviceFoundAlready = false; for (int i = 0; i < nodeList.getLength(); i++) { Node node = nodeList.item(i); if (isAdviceNode(node, parserContext)) { if (!adviceFoundAlready) { adviceFoundAlready = true; if (!StringUtils.hasText(aspectName)) { parserContext.getReaderContext().error( "<aspect> tag needs aspect bean reference via 'ref' attribute when declaring advices.", aspectElement, this.parseState.snapshot()); return; } beanReferences.add(new RuntimeBeanReference(aspectName)); } AbstractBeanDefinition advisorDefinition = parseAdvice( aspectName, i, aspectElement, (Element) node, parserContext, beanDefinitions, beanReferences); beanDefinitions.add(advisorDefinition); } } AspectComponentDefinition aspectComponentDefinition = createAspectComponentDefinition( aspectElement, aspectId, beanDefinitions, beanReferences, parserContext); parserContext.pushContainingComponent(aspectComponentDefinition); List<Element> pointcuts = DomUtils.getChildElementsByTagName(aspectElement, POINTCUT); for (Element pointcutElement : pointcuts) { parsePointcut(pointcutElement, parserContext); } parserContext.popAndRegisterContainingComponent(); } finally { this.parseState.pop(); } } ``` ### aspectj-autoproxy配置标签的解析 这个部分仅仅是看调用关系,比较简单。 `<aop:aspectj-autoproxy/>`则由AspectJAutoProxyBeanDefinitionParser这个类处理的,我们看下parse 方法 ```java @Override @Nullable public BeanDefinition parse(Element element, ParserContext parserContext) { // 注册AspectJAnnotationAutoProxyCreator AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); // 拓展BeanDefinition extendBeanDefinition(element, parserContext); return null; } ``` AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary方法对应如下 ```java public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( ParserContext parserContext, Element sourceElement) { BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary( parserContext.getRegistry(), parserContext.extractSource(sourceElement)); useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); registerComponentIfNecessary(beanDefinition, parserContext); } ``` AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary对应如下 ```java @Nullable public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary( BeanDefinitionRegistry registry, @Nullable Object source) { return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); } ``` 到这里,我们发现**AOP的创建工作是交给AnnotationAwareAspectJAutoProxyCreator来完成的。** ## 注解切面代理创建类(AnnotationAwareAspectJAutoProxyCreator) AnnotationAwareAspectJAutoProxyCreator是如何工作的呢?这时候我们就要看AnnotationAwareAspectJAutoProxyCreator类结构关系了。 如下是类结构关系 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/uqroLeAzFO0GuU1a.webp) 它实现了两类接口: - BeanFactoryAware属于**Bean级生命周期接口方法** - InstantiationAwareBeanPostProcessor 和 BeanPostProcessor 这两个接口实现,一般称它们的实现类为“后处理器”,是**容器级生命周期接口方法**; Bean级生命周期与容器级生命周期前文讲解过,没啥特殊的: - **Bean 级**:Bean 实例化 -> 销毁的全过程 - **容器级**:容器准备 -> Bean级生命周期 -> 容器关闭 全过程。简化步骤就是如此。 结合前文Spring Bean生命周期的流程,我们就可以定位到核心的初始化方法肯定在postProcessBeforeInstantiation和postProcessAfterInitialization中。 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/DIJtz9lzHVcX2MXh.webp) ### postProcessBeforeInstantiation 如下是上述类结构中postProcessBeforeInstantiation的方法,读者在自己看代码的时候建议打个断点看,可以方便理解 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/h7eCJvl5aFeUuL2p.webp) ```java @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) { Object cacheKey = getCacheKey(beanClass, beanName); if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) { // 如果已经在缓存中,则忽略 if (this.advisedBeans.containsKey(cacheKey)) { return null; } // 是否是aop基础类?是否跳过? if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return null; } } // Create proxy here if we have a custom TargetSource. // Suppresses unnecessary default instantiation of the target bean: // The TargetSource will handle target instances in a custom fashion. TargetSource targetSource = getCustomTargetSource(beanClass, beanName); if (targetSource != null) { if (StringUtils.hasLength(beanName)) { this.targetSourcedBeans.add(beanName); } Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource); Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource); this.proxyTypes.put(cacheKey, proxy.getClass()); return proxy; } return null; } ``` #### 判断是否是aop基础类 **postProcessBeforeInstantiation** 方法内有一个步骤是 **isInfrastructureClass(beanClass),**判断是否是aop基础类,如果是基础类那么不能**被AOP代理,**源码: 简单理解,除了【业务目标对象】,其它【AOP框架自身组件】和【切面】都属于基础类,不应该被AOP代理。 ```java @Override protected boolean isInfrastructureClass(Class<?> beanClass) { // Previously we setProxyTargetClass(true) in the constructor, but that has too // broad an impact. Instead we now override isInfrastructureClass to avoid proxying // aspects. I'm not entirely happy with that as there is no good reason not // to advise aspects, except that it causes advice invocation to go through a // proxy, and if the aspect implements e.g the Ordered interface it will be // proxied by that interface and fail at runtime as the advice method is not // defined on the interface. We could potentially relax the restriction about // not advising aspects in the future. // 父类判断它是aop基础类 or 使用@Aspect注解 return (super.isInfrastructureClass(beanClass) || (this.aspectJAdvisorFactory != null && this.aspectJAdvisorFactory.isAspect(beanClass))); } ``` 父类判断它是否是aop基础类的方法 super.isInfrastructureClass(beanClass), 本质上就是判断该类是否实现了Advice, Pointcut, Advisor或者AopInfrastructureBean接口。 ```java protected boolean isInfrastructureClass(Class<?> beanClass) { // 该类是否实现了Advice, Pointcut, Advisor或者AopInfrastructureBean接口 boolean retVal = Advice.class.isAssignableFrom(beanClass) || Pointcut.class.isAssignableFrom(beanClass) || Advisor.class.isAssignableFrom(beanClass) || AopInfrastructureBean.class.isAssignableFrom(beanClass); if (retVal && logger.isTraceEnabled()) { logger.trace("Did not attempt to auto-proxy infrastructure class [" + beanClass.getName() + "]"); } return retVal; } ``` #### 是否应该跳过shouldSkip **postProcessBeforeInstantiation** 方法内有一个步骤是 **shouldSkip(beanClass, beanName),**通过断点辅助,candidateAdvisors与xml配置的通知是对应的。 java 8 才能跑出来这个效果,java11 源码改了。 这一步的意义是:防止切面自己被自己通知,从而**避免无限递归调用**和潜在的循环依赖问题, 并且把**切面转成 advisor** 也是从这里开始。 当循环到 UserServiceImpl 时,这是一个需要 AOP 切面的Bean, 那么我们debug 可以看出要给 Bean 添加的候选通知。 ![img](https://pic.code-nav.cn/post_picture/1625164612255182850/8Kn8VPgyxkVr8uj6.webp) ```java @Override protected boolean shouldSkip(Class<?> beanClass, String beanName) { // TODO: Consider optimization by caching the list of the aspect names List<Advisor> candidateAdvisors = findCandidateAdvisors(); for (Advisor advisor : candidateAdvisors) { if (advisor instanceof AspectJPointcutAdvisor && ((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) { return true; } } return super.shouldSkip(beanClass, beanName); } ``` #### 切面方法转成Advisor 这里不做剖析,如果感兴趣想深入研究切面方法怎么转成 Advisor, 这里提供java8源码。 findCandidateAdvisors方法如下: ```java @Override protected List<Advisor> findCandidateAdvisors() { // 在父类中找到所有的advisor:基于xml配置的<aop:before/>生成的 List<Advisor> advisors = super.findCandidateAdvisors(); // 为bean Factory中AspectJ切面构建advistor:通过AspectJ注解的方式生成Advisor类 if (this.aspectJAdvisorsBuilder != null) { advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); } return advisors; } ``` 在当前的bean Factory中通过AspectJ注解的方式生成Advisor类,buildAspectJAdvisors方法如下 这方法很长,本质上做的事情:用DCL双重锁的单例实现方式,拿到切面类里的切面方法,将其转换成advisor(并放入缓存中)。 ```java /** * Look for AspectJ-annotated aspect beans in the current bean factory, * and return to a list of Spring AOP Advisors representing them. * <p>Creates a Spring Advisor for each AspectJ advice method. * @return the list of {@link org.springframework.aop.Advisor} beans * @see #isEligibleBean */ public List<Advisor> buildAspectJAdvisors() { List<String> aspectNames = this.aspectBeanNames; if (aspectNames == null) { synchronized (this) { aspectNames = this.aspectBeanNames; if (aspectNames == null) { List<Advisor> advisors = new ArrayList<>(); aspectNames = new ArrayList<>(); String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( this.beanFactory, Object.class, true, false); for (String beanName : beanNames) { if (!isEligibleBean(beanName)) { continue; } // We must be careful not to instantiate beans eagerly as in this case they // would be cached by the Spring container but would not have been weaved. Class<?> beanType = this.beanFactory.getType(beanName, false); if (beanType == null) { continue; } if (this.advisorFactory.isAspect(beanType)) { aspectNames.add(beanName); AspectMetadata amd = new AspectMetadata(beanType, beanName); if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { MetadataAwareAspectInstanceFactory factory = new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory); // 单例加到advisorsCache, 非单例加到aspectFactoryCache if (this.beanFactory.isSingleton(beanName)) { this.advisorsCache.put(beanName, classAdvisors); } else { this.aspectFactoryCache.put(beanName, factory); } advisors.addAll(classAdvisors); } else { // Per target or per this. if (this.beanFactory.isSingleton(beanName)) { throw new IllegalArgumentException("Bean with name '" + beanName + "' is a singleton, but aspect instantiation model is not singleton"); } MetadataAwareAspectInstanceFactory factory = new PrototypeAspectInstanceFactory(this.beanFactory, beanName); this.aspectFactoryCache.put(beanName, factory); // advisorFactory工厂获取advisors advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } } this.aspectBeanNames = aspectNames; return advisors; } } } if (aspectNames.isEmpty()) { return Collections.emptyList(); } List<Advisor> advisors = new ArrayList<>(); for (String aspectName : aspectNames) { List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName); if (cachedAdvisors != null) { advisors.addAll(cachedAdvisors); } else { MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } return advisors; } ``` 转换的成advisor的方法是:this.advisorFactory.getAdvisors,看getAdvisors源码 ```java @Override public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); validate(aspectClass); // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator // so that it will only instantiate once. MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); List<Advisor> advisors = new ArrayList<>(); for (Method method : getAdvisorMethods(aspectClass)) { // Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect // to getAdvisor(...) to represent the "current position" in the declared methods list. // However, since Java 7 the "current position" is not valid since the JDK no longer // returns declared methods in the order in which they are declared in the source code. // Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods // discovered via reflection in order to support reliable advice ordering across JVM launches. // Specifically, a value of 0 aligns with the default value used in // AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor). Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName); if (advisor != null) { advisors.add(advisor); } } // If it's a per target aspect, emit the dummy instantiating aspect. if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); advisors.add(0, instantiationAdvisor); } // Find introduction fields. for (Field field : aspectClass.getDeclaredFields()) { Advisor advisor = getDeclareParentsAdvisor(field); if (advisor != null) { advisors.add(advisor); } } return advisors; } ``` getAdvisor方法如下 ```java @Override @Nullable public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) { validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); AspectJExpressionPointcut expressionPointcut = getPointcut( candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass()); if (expressionPointcut == null) { return null; } // 封装成advisor return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod, this, aspectInstanceFactory, declarationOrderInAspect, aspectName); } ``` 最后这里有一个重点 getPointcut() 方法,这是获取表达式的切点,详细看看 #### 获取表达式的切点 切面类包含了切点,切面类转成 advisor 了,那么切点自然也是要转的。 获取表达式的切点的方法getPointcut如下: ```java @Nullable private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) { AspectJAnnotation<?> aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { return null; } AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]); ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); if (this.beanFactory != null) { ajexp.setBeanFactory(this.beanFactory); } return ajexp; } ``` AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod的方法如下 ```java private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] { Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class}; /** * Find and return the first AspectJ annotation on the given method * (there <i>should</i> only be one anyway...). */ @SuppressWarnings("unchecked") @Nullable protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) { for (Class<?> clazz : ASPECTJ_ANNOTATION_CLASSES) { AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) clazz); if (foundAnnotation != null) { return foundAnnotation; } } return null; } ``` findAnnotation方法如下 ```java @Nullable private static <A extends Annotation> AspectJAnnotation<A> findAnnotation(Method method, Class<A> toLookFor) { A result = AnnotationUtils.findAnnotation(method, toLookFor); if (result != null) { return new AspectJAnnotation<>(result); } else { return null; } } ``` AnnotationUtils.findAnnotation 获取注解方法如下 ```java /** * Find a single {@link Annotation} of {@code annotationType} on the supplied * {@link Method}, traversing its super methods (i.e. from superclasses and * interfaces) if the annotation is not <em>directly present</em> on the given * method itself. * <p>Correctly handles bridge {@link Method Methods} generated by the compiler. * <p>Meta-annotations will be searched if the annotation is not * <em>directly present</em> on the method. * <p>Annotations on methods are not inherited by default, so we need to handle * this explicitly. * @param method the method to look for annotations on * @param annotationType the annotation type to look for * @return the first matching annotation, or {@code null} if not found * @see #getAnnotation(Method, Class) */ @Nullable public static <A extends Annotation> A findAnnotation(Method method, @Nullable Class<A> annotationType) { if (annotationType == null) { return null; } // Shortcut: directly present on the element, with no merging needed? if (AnnotationFilter.PLAIN.matches(annotationType) || AnnotationsScanner.hasPlainJavaAnnotationsOnly(method)) { return method.getDeclaredAnnotation(annotationType); } // Exhaustive retrieval of merged annotations... return MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, RepeatableContainers.none()) .get(annotationType).withNonMergedAttributes() .synthesize(MergedAnnotation::isPresent).orElse(null); } ``` #### 封装成Advisor 注:Advisor 是 advice的包装器,包含了advice及其它信息 由InstantiationModelAwarePointcutAdvisorImpl构造完成 ```java public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut, Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { this.declaredPointcut = declaredPointcut; this.declaringClass = aspectJAdviceMethod.getDeclaringClass(); this.methodName = aspectJAdviceMethod.getName(); this.parameterTypes = aspectJAdviceMethod.getParameterTypes(); this.aspectJAdviceMethod = aspectJAdviceMethod; this.aspectJAdvisorFactory = aspectJAdvisorFactory; this.aspectInstanceFactory = aspectInstanceFactory; this.declarationOrder = declarationOrder; this.aspectName = aspectName; if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { // Static part of the pointcut is a lazy type. Pointcut preInstantiationPointcut = Pointcuts.union( aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut); // Make it dynamic: must mutate from pre-instantiation to post-instantiation state. // If it's not a dynamic pointcut, it may be optimized out // by the Spring AOP infrastructure after the first evaluation. this.pointcut = new PerTargetInstantiationModelPointcut( this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory); this.lazy = true; } else { // A singleton aspect. this.pointcut = this.declaredPointcut; this.lazy = false; this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); } } ``` 通过pointcut获取advice ```java private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) { Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut, this.aspectInstanceFactory, this.declarationOrder, this.aspectName); return (advice != null ? advice : EMPTY_ADVICE); } ``` 交给aspectJAdvisorFactory获取 ```java @Override @Nullable public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { // 获取切面类 Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); validate(candidateAspectClass); // 获取切面注解 AspectJAnnotation<?> aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { return null; } // If we get here, we know we have an AspectJ method. // Check that it's an AspectJ-annotated class if (!isAspect(candidateAspectClass)) { throw new AopConfigException("Advice must be declared inside an aspect type: " + "Offending method '" + candidateAdviceMethod + "' in class [" + candidateAspectClass.getName() + "]"); } if (logger.isDebugEnabled()) { logger.debug("Found AspectJ method: " + candidateAdviceMethod); } // 切面注解转换成advice AbstractAspectJAdvice springAdvice; switch (aspectJAnnotation.getAnnotationType()) { case AtPointcut: // AtPointcut忽略 if (logger.isDebugEnabled()) { logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); } return null; case AtAround: springAdvice = new AspectJAroundAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtBefore: springAdvice = new AspectJMethodBeforeAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtAfter: springAdvice = new AspectJAfterAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); break; case AtAfterReturning: springAdvice = new AspectJAfterReturningAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterReturningAnnotation.returning())) { springAdvice.setReturningName(afterReturningAnnotation.returning()); } break; case AtAfterThrowing: springAdvice = new AspectJAfterThrowingAdvice( candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); } break; default: throw new UnsupportedOperationException( "Unsupported advice type on method: " + candidateAdviceMethod); } // 最后将其它切面信息配置到advice springAdvice.setAspectName(aspectName); springAdvice.setDeclarationOrder(declarationOrder); String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); if (argNames != null) { springAdvice.setArgumentNamesFromStringArray(argNames); } springAdvice.calculateArgumentBindings(); return springAdvice; } ``` #### 小结 回头看,主要是处理使用了@Aspect注解的切面类,然后将切面类的所有切面方法根据使用的注解生成对应Advice,并将Advice连同切入点匹配器和切面类等信息一并封装到Advisor的过程。 advisor 有了,再加上源码的流程,其实到这里切面就已经实现了。 回顾一下你在写切面编程的时候,你需要定义 @Aspect, @Around , 切点表达式(Pointcut) 对吧,那就对应上源码了,@Aspect声明的了切面类实例,作用是源码中判断是否属于 aop 基础类,@Around , 切点表达式 对应为 advisor, 切面实现了。 下一篇文章学 advisor 的用法。

Spring 核心 - AOP 面向切面编程入门, 醒醒国庆假期第二天了

撰写本文目的只有一个,让你畅快阅读 AOP 知识,并搞定以下几个问题。 + AOP 面向切面编程到底是什么? + AOP 术语:连接点,切入点,通知,切面,织入是什么东西? + @Aspect 注解为啥用的是 aspectj 包,不是 AOP 包?它们啥关系? + Spring 框架是如何实现 AOP ? **代理技术**。 不急解释 AOP 的概念, 我们先来看 AOP 解决了什么问题。 ## 引言 在写项目的时,你是否遇到过随着系统体积增大,有一些新的需求——比如日志、用户鉴权、指标上报、redis 缓存填充/失效、事务等——需要对系统的多个类、方法改动,这种大量**人工改动**代码的行为容易遗漏、逻辑改动大,侵入风险高。可见,在这种场景下,手动改写代码必然是下下策。 很明显本文讲的是 AOP , AOP 必然可以解决此类问题,做到**不侵入**原有代码新增日志、用户鉴权等功能。 + 举一个例子,让场景更形象化。 ```java public class UserServiceImpl implements UserService { /** * find user list. * * @return user list */ @Override public List<User> findUserList() { System.out.println("执行方法: findUserList()"); return Collections.singletonList(new User("fency", 18)); } /** * add user */ @Override public void addUser() { System.out.println("执行方法: addUser()"); // do something } @Override public void deleteUser() { System.out.println("执行方法: deleteUser()"); } } ``` 要想对这三个方法加日志和鉴权,逐个添加是下策。如图: ![](https://pic.code-nav.cn/post_picture/1625164612255182850/yfXSSrfsG0lyf3kh.png) 下面有请主角登场。 ## AOP ### 介绍 AOP 为 Aspect Oriented Programming 的缩写,意为:面向切面编程。 AOP 最早是 AOP 联盟的组织提出的,指定的一套规范,Spring 将 AOP 的思想引入框架之中,通过**预编译方式**和**运行期间动态代理**实现程序的统一维护的一种技术。 AOP 是设计思想,Spring AOP 、AspectJ 才是技术实现, AOP 思想**本质的作用解耦**。我们将记录日志、鉴权功能解耦为切面,进而引出 AOP 理念:将分散在各个业务逻辑代码中相同的代码,通过**横向切割**的方式抽取到一个独立的模块中。用图表示: ![](https://pic.code-nav.cn/post_picture/1625164612255182850/HTpwC6fQQEvgpX2o.png) 意思是写一次日志代码,即可让三个方法实现日志记录功能。 下面用 Spring AOP 实战演示统一加日志。 ### 实战 - 统一添加日志 创建一个 spring boot 项目,引一个 lombok 依赖。 1)引入 aop 依赖包 ```xml <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 2)新建 UserServiceImpl 类, 我们要对它进行统一添加日志。 ```java @Service public class UserServiceImpl implements UserService { /** * find user list. * * @return user list */ @Override public List<User> findUserList() { System.out.println("执行方法: findUserList()"); return Collections.singletonList(new User("fency", 18)); } /** * add user */ @Override public void addUser() { System.out.println("执行方法: addUser()"); // do something } @Override public void deleteUser() { System.out.println("执行方法: deleteUser()"); } } ``` 单元测试执行 ```java @SpringBootTest class UserServiceImplTest { @Resource private UserService userService; @Test void invokeMethod() { userService.addUser(); userService.findUserList(); userService.deleteUser(); } } ``` 效果如图,没有日志。 ![](https://pic.code-nav.cn/post_picture/1625164612255182850/JCB5aoVuIhb7flUh.png) 3)添加 Aspect 切面类,统一为 UserService 的三个方法添加日志。 ```java @Slf4j @Aspect @Component public class LogAspect { /** * 切点:拦截所有 controller 包下的公共方法 */ @Around("execution(* com.codebear.springboothelloword.service..*(..))") public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().toShortString(); Object[] args = joinPoint.getArgs(); log.info("Entering {} with args: {}", methodName, args); try { Object result = joinPoint.proceed(); // 执行业务方法 log.info("Exiting {} with result: {}", methodName, result); return result; } catch (Throwable ex) { log.error("Exception in {}: {}", methodName, ex.getMessage(), ex); throw ex; } } } ``` 再次执行,可以看到每个方法前后都有日志,解决了最初的痛点,避免大量手动修改代码。 ![](https://pic.code-nav.cn/post_picture/1625164612255182850/qsv5OZxjcfFP4V5T.webp) 初学 AOP 可能会觉得不会灵活,所有的方法都有日志了,如果只想两个方法有日志怎么办? 探索一下切点表达式,便有答案。拿示例的切点表达式来说: ```java // 第一个 * 代表匹配任意方法的返回类型 // service..* 代表匹配这个包下的所有类都是切点 // (..) 代表任意方法的参数类型、个数都匹配 @Around("execution(* com.codebear.springboothelloword.service..*(..))") ``` ### AOP 术语 开始念经:**连接点、切入点、通知、切面、引入、目标对象、织入、AOP代理**,第一次看到这一堆还是会恶心一下吧。 其实这些术语不是 Spring AOP 独有的, 而是 AOP 设计思想的组成,所以这些术语也会比较抽象。 先说明,作为开发者,我认为不需要掌握全部的 AOP 思想,理解切面、通知、切入点、连接点就能写出 `LogAspect` 切面编程。 下面介绍一下,并且把术语代入到 Spring AOP 中。 **连接点(JoinPoint):**上述示例中这里有三个方法,任意**单个**方法就叫一个**连接点**。官方一点的解释,表示需要在程序中插入横切关注点的扩展点,**连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等,**在AOP中表示为**在哪里干**。 **** **切入点(Pointcut):**上述示例中,service包下的所有子包、类和方法都是切入点,因为切入点表达式编写成这样,切入点说白了就是表达式规则。官方解释:选择一组相关连接点,即可以认为连接点的集合,在AOP中表示为**在哪里干的集合**。 **通知(Advice)**:真正要织入的代码逻辑,比如上述例子,我们要给切入点织入日志记录逻辑,这段日志逻辑便是通知,为什么叫通知?那么抽象,因为有前置通知(before advice)、后置通知(after advice)、环绕通知(around advice),允许你在原有方法执行前、后、环绕等加入日志逻辑。在AOP中表示为**干什么**。 **切面(Aspect)**: 面向切面编程AOP的主角就是这位,以上述例子来讲,打上 @Aspect 注解的 `LogAspect` 类就是切面,目前这个类中只有一个环绕通知 `logAround()` , 当然可以增加通知,所以这一整个类**编程范式**就是切面编程,那么切面就是 `LogAspect`。 **引入(Introduction)**: 允许一个切面**为目标对象(被通知的对象)声明新的接口,并提供一个默认实现**。作用是**让一个原本没有实现某个接口的类,在运行时“假装”实现了这个接口,从而可以调用这个接口中的方法。** **目标对象(Target Object):**需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring AOP 通过代理模式实现,从而这个对象永远是被代理对象。在AOP中表示为**对谁干**。 **织入(Weaving)**:把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring 和其他纯 Java AOP 框架一样,在运行时完成织入,运行时生成动态代理代码的这个过程叫做织入。在 AOP 中表示为**怎么实现的**。 **AOP 代理(AOP)**: AOP 框架使用**代理模式**创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在 Spring 中,AOP 代理可以用 JDK 动态代理或 CGLIB 代理实现,而通过拦截器模型应用切面。在AOP中表示为**怎么实现的一种典型方式**; ### AOP 术语难点解释 ```java // 1. 定义一个我们想要“赋予”的能力 public interface IsModified { boolean isModified(); } // 实现类,给一个最简单的实现,调用方法只能回复 false public class DefaultIsModifiedImpl implements IsModified { @Override public boolean isModified() { // 它总是返回一个固定的值,比如 false return false; } } // 2. 定义切面,使用引入 @Aspect public class IsModifiedAspect { // @DeclareParents 就是引入的核心注解 // value: 指定要为哪些目标类赋予新能力 // defaultImpl: 指定新能力的默认实现 @DeclareParents( value = "com.example.domain.*+", // 匹配 domain 包下的所有类 defaultImpl = DefaultIsModifiedImpl.class ) public static IsModified isModified; // 字段类型就是要赋予的能力(接口) } // 3. 在业务代码中使用 @Service public class SomeService { @Autowired private UserRepository userRepository; public void processUser() { User user = userRepository.findById(1L); // 关键:User类本身没有isModified方法 // 但通过引入,我们可以把它当作IsModified接口来使用 IsModified modifiedUser = (IsModified) user; System.out.println("Is user modified? " + modifiedUser.isModified()); } } ``` 可以看到 user 可以强转成 IsModified 接口并使用 IsModified 接口的方法,而 User 类并没有实现 IsModified 接口,这便是引入的作用:**让一个原本没有实现某个接口的类,在运行时“假装”实现了这个接口,从而可以调用这个接口中的方法。** 织入和代理应该有点难分清吧? 织入是生成代理对象的过程,有了代理对象,通知功能得以实现。可以看出 Spring AOP 本质上是通过动态代理实现。 一图胜千言 ![](https://pic.code-nav.cn/post_picture/1625164612255182850/PS2snNjiY9QBvUzE.png) 如果坚持读到这里,我相信你搞明白了 AOP 和 Spring AOP 的概念,一个设计思想,一个是具体实现,另外AspectJ 也是实现 AOP 的一门技术。 细心的你已经发现了上述示例 @Aspect 注解来源于 aspectj 包,并不是 aop 包的注解。 ![](https://pic.code-nav.cn/post_picture/1625164612255182850/OkX8rsPmbceoHoOV.webp) 如下图所示,我们引入的 starter - aop 依赖包含了 aspectj 和 aop 两个技术,说明 spring 开发小组直接使用了 aspectj 包作为 Spring AOP 实现的一部分。 ![](https://pic.code-nav.cn/post_picture/1625164612255182850/UEH8WDA0QFxjBfKA.png) 那必须要讲一讲 Spring AOP 和 AspectJ 之间的瓜葛。 ### Spring AOP 和 AspectJ 的渊源 **Aspect 是什么呢?** AspectJ 是一个 java 语言实现的 AOP 框架,它能够对java代码进行AOP编译(一般在编译期进行),让java代码具有 AspectJ 的 AOP 功能(当然需要特殊的编译器)。 可以这样说 AspectJ 是目前实现 AOP 框架中最成熟,功能最丰富的语言,更幸运的是,AspectJ 与 java 程序完全兼容,几乎是无缝关联,因此对于有 java 编程基础的工程师,上手和使用都非常容易。 **Spring AOP 和 AspectJ 是什么关系**? 1)AspectJ 是更强的 AOP 框架,是实际意义的 **AOP 标准**; 2)Spring 为何不写类似 AspectJ 的框架? Spring AOP 使用纯 Java 实现, 它不需要专门的编译过程, 它一个**重要的原则就是无侵入性(non-invasiveness)**; Spring 小组完全有能力写类似的框架,只是 Spring AOP 从来没有打算通过提供一种全面的 AOP 解决方案来与 AspectJ 竞争。Spring 的开发小组相信无论是基于代理(proxy-based)的框架如 Spring AOP 或者是成熟的框架如 AspectJ 都是很有价值的,他们之间应该是**互补而不是竞争的关系**。 3) Spring 小组喜欢 @AspectJ 注解风格更胜于 Spring XML 配置; 所以**在 Spring 2.0 使用了和 AspectJ 5 一样的注解,并使用 AspectJ 来做切入点解析和匹配**。**但是,AOP 在运行时仍旧是纯的 Spring AOP,并不依赖于AspectJ 的编译器或者织入器(weaver)**, 但是注解和切点解析用的都是 AspectJ 的技术。 4)Spring 2.5 对 AspectJ 的支持:在一些环境下,增加了对 AspectJ 的装载时编织支持,同时提供了一个新的bean切入点。 **看样子 AspectJ 哪哪都好呀,更强大,更全面,完全按照 AOP 标准来实现,为啥 Spring 小组还要开发 Spring AOP 呢?** 以下Spring官方的回答:(总结来说就是 **Spring AOP更易用,AspectJ更强大**)。 + Spring AOP 比完全使用 AspectJ 更加简单, 因为它不需要引入 AspectJ 的编译器/织入器到你开发和构建过程中。 如果你**仅仅需要在 Spring bean 上通知执行操作,那么 Spring AOP 是合适的选择**。 + 如果你需要通知 domain 对象或其它没有在 Spring 容器中管理的任意对象,那么你需要使用 AspectJ。 + 如果你想通知除了简单的方法执行之外的连接点(如:调用连接点、字段get或set的连接点等等), 也需要使用AspectJ。 他们的关系稍微了解即可,回归 Spring AOP 的学习。 ## Spring AOP 的配置方式 Spring AOP 支持对 **XML 模式**和基于 **@AspectJ 注解**的两种配置方式。 本文例子使用的是 **@AspectJ 注解式**开发切面编程**,**XML 就是写一堆 XML 配置,用的很少,知道可以 XML 配置即可,不做实战演示。 Spring 使用了 @AspectJ 框架为 AOP 的实现提供了一套注解,下面归纳一下。 | 注解名称 | 解释 | | --- | --- | | @Aspect | 用来定义一个切面。 | | @pointcut | 用于定义**切入点表达式**。在使用时还需要定义一个包含名字和任意参数的方法签名来表示切入点名称,这个方法签名就是一个返回值为void,且方法体为空的普通方法。 | | @Before | 用于定义**前置通知**,相当于BeforeAdvice。在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。 | | @AfterReturning | 用于定义**后置通知**,相当于AfterReturningAdvice。在使用时可以指定pointcut / value和returning属性,其中pointcut / value这两个属性的作用一样,都用于指定切入点表达式。 | | @Around | 用于定义**环绕通知**,相当于MethodInterceptor。在使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 | | @After-Throwing | 用于定义**异常通知**来处理程序中未处理的异常,相当于ThrowAdvice。在使用时可指定pointcut / value和throwing属性。其中pointcut/value用于指定切入点表达式,而throwing属性值用于指定一个形参名来表示Advice方法中可定义与此同名的形参,该形参可用于访问目标方法抛出的异常。 | | @After | 用于定义**最终final 通知**,不管是否异常,该通知都会执行。使用时需要指定一个value属性,该属性用于指定该通知被植入的切入点。 | | @DeclareParents | 用于定义**引介通知**,相当于IntroductionInterceptor (不要求掌握)。 | ## Spring AOP 的实现方式 Spring AOP 的实现方式是动态织入,动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的;**如 Java JDK的动态代理( Proxy,底层通过反射实现)或者 CGLIB 的动态代理(底层通过继承实现)**,Spring AOP 采用的就是基于运行时增强的代理技术。 所以我们看下如下的两个例子 + 基于 JDK 代理例子 + 基于 Cglib 代理例子 > 经典面试题,接口使用 JDK 动态代理,类使用 Cglib 动态代理。Spring Boot 2.x 开始默认优先使用 Cglib 动态代理技术,无论你的类是否实现了接口,Spring Boot 默认都会尝试使用 CGLIB 来创建代理对象。 > 参考文章:[https://www.cnblogs.com/lyh233/p/16008251.html#_label1_1](https://www.cnblogs.com/lyh233/p/16008251.html#_label1_1) spring boot 2.0 前默认优先使用 JDK 代理,2.0 后默认优先使用 Cglib 代理,Spring AOP 的底层实现方式是“同时支持 JDK 动态代理和 CGLIB,并根据配置和目标对象的情况,智能选择其中一种来工作”,不改配置默认就是 Cglib 。 再来一道经典面试题。 ## JDK 动态代理和 Cglib 代理动态代理的区别? 一张表格看懂 | 特性 | JDK 动态代理 | CGLIB 动态代理 | | --- | --- | --- | | **核心原理** | **基于接口** | **基于继承** | | **代理对象类型** | 生成目标接口的**新类** (`**com.sun.proxy.$Proxy**`) | 生成目标类的**子类** (`**...$$EnhancerBySpringCGLIB$$...**`) | | **对目标类的要求** | **必须实现至少一个接口** | **可以是任何类**(但不能是 `**final**` 类) | | **对方法的要求** | 只能代理接口中的方法 | 可以代理类中的 `**public**`/`**protected**`方法,**不能代理 **`**final**`** 或 **`**static**`** 方法** | | **依赖** | JDK 原生支持,无需外部库 | 需要引入 `**cglib**`(或 Spring 内置的 `**spring-core**`) | 下面我们抛开 Spring,用最原生的代码来分别实现这两种代理,你会立刻明白它们的工作方式。 先从 Cglib 开始。 ### Cglib 动态代理技术原理 **CGLIB 动态代理**:它在运行时为你创建一个**目标类的子类**。这个子类会重写父类(你的原始类)的所有非 `**final**` 方法,并在重写的方法中加入额外的逻辑(切面代码)。**它和你的原始类是父子关系。** **** **场景预设** 我们想在调用 `OrderService` 的 `createOrder` 方法前后,打印日志。 创建一个包 cglib ,一顿复制。 1) 添加 CGLIB 依赖,Spring boot 项目不用导入依赖。 ```xml <!-- Maven --> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency> ``` 2)创建一个目标类(无需接口) ```java public class OrderService { public void createOrder(String productName) { System.out.println("【核心业务】正在创建订单: " + productName); } } ``` 3)**创建 **`**MethodInterceptor**`**(核心逻辑拦截器)**,下一步发挥作用。 ```java public class LogMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { // 1. 前置增强(日志) System.out.println("[CGLIB代理] - 准备执行方法: " + method.getName()); // 2. 调用父类(目标对象)的原方法 // 注意:这里用的是 proxy.invokeSuper,而不是 method.invoke Object result = proxy.invokeSuper(obj, args); // 3. 后置增强(日志) System.out.println("[CGLIB代理] - 方法执行完毕"); return result; } } ``` 4)创建代理对象并使用 ```java public class CglibProxyDemo { public static void main(String[] args) { // 1. 创建 Enhancer 对象,类似于 JDK 中的 Proxy 类 Enhancer enhancer = new Enhancer(); // 2. 设置父类(目标类) enhancer.setSuperclass(OrderService.class); // 3. 设置回调(拦截器) enhancer.setCallback(new LogMethodInterceptor()); // 4. 创建代理对象 OrderService proxyInstance = (OrderService) enhancer.create(); // 5. 使用代理对象调用方法 System.out.println("代理对象的类型: " + proxyInstance.getClass().getName()); proxyInstance.createOrder("iPhone 15"); } } ``` 运行结果: ![](https://pic.code-nav.cn/post_picture/1625164612255182850/eYop3NRktGNjFXLp.webp) ### JDK 动态代理技术原理 **JDK 动态代理**:它不关心你的类是什么,只关心你实现了哪些接口。它在运行时**通过反射**为你创建一个**全新的类**,这个新类实现了你指定的所有接口,并把所有方法调用都转发到一个 `**InvocationHandler**` 上。**它和你的原始类没有父子关系。** **** **场景预设** 我们想在调用 `UserService` 的 `addUser` 方法前后,打印日志。 1)定义一个接口 ```java public interface UserService { void addUser(String username); } ``` 2)创建接口的实现类(目标对象) ```java public class UserServiceImpl implements UserService { @Override public void addUser(String username) { System.out.println("【核心业务】正在添加用户: " + username); } } ``` 3)**创建 **`**InvocationHandler**`**(核心逻辑处理器)** ```java import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class LogInvocationHandler implements InvocationHandler { // 1. 持有目标对象的引用 private Object target; public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 2. 前置增强(日志) System.out.println("[JDK代理] - 准备执行方法: " + method.getName()); // 3. 调用目标对象的原方法 Object result = method.invoke(target, args); // 4. 后置增强(日志) System.out.println("[JDK代理] - 方法执行完毕"); return result; } } ``` 4)创建代理对象并使用,可以看到 JDK 是反射技术实现代理。 ```java import java.lang.reflect.Proxy; public class JdkProxyDemo { public static void main(String[] args) { // 1. 创建目标对象 UserServiceImpl target = new UserServiceImpl(); // 2. 创建代理对象 UserService proxyInstance = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), // 类加载器 target.getClass().getInterfaces(), // 目标对象实现的接口 new LogInvocationHandler(target) // 事件处理器 ); // 3. 使用代理对象调用方法 System.out.println("代理对象的类型: " + proxyInstance.getClass().getName()); proxyInstance.addUser("张三"); } } ``` 执行结果: ![](https://pic.code-nav.cn/post_picture/1625164612255182850/9qwqEJvmQrwbEJeb.webp)

SpringAOP入门(一)

<html> <head></head> <body> <div class="content ql-editor"> <p>Spring学习新笔记,原文链接:https://blog.csdn.net/m0_66570338/article/details/129869192</p> <h2><strong style="font-size: 18px; color: rgb(236, 40, 52);"> 内容如下:</strong></h2> <h1>一.什么是AOP</h1> <p><br></p> <p>Spring有<strong>两个核心</strong>的概念,一个是<code>IOC/DI,一个是AOP</code> 。</p> <p><br></p> <p>我们都知道<code>OOP是一种编程思想,而AOP也是一种编程思想,编程思想主要的内容就是指导程序员该如何编写程序,所以它们两个是不同的编程范式</code> 。</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>AOP(<strong>A</strong>spect <strong>O</strong>riented <strong>P</strong>rogramming)<strong>面向切面</strong>编程(面向特定方法编程),一种编程范式,指导开发者如何组织程序结构。</li> </ol> <p><br></p> <p>那么它对我们编程<strong>有什么作用</strong>呢?</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>它能帮助我们在<code>不改动原有代码的前提下对其进行功能增强</code> 。</li> </ol> <p><br></p> <h1>二.作用</h1> <p><br></p> <p>为了能更好的理解AOP的相关概念以及作用,我们通过一些使用场景来学习它~</p> <p><br></p> <h2>(1) 引入案例</h2> <p><br></p> <p>假如我们有一个项目,上线后发现项目执行效率比较低,我们准备优化一些执行比较耗时的业务功能,此时就需要统计每一个业务方法的执行耗时。但是我们的项目中方法的数量令人头疼.....</p> <p>!<a href="https://img-blog.csdnimg.cn/a92c3c58861049399f9a30b585145888.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <p>想必没人愿重复做若干遍相同的操作,就算你做了,假如我们优化完了,这些测试就比较多余了,再让你一个个去掉.......不知有何感想~</p> <p><br></p> <p>而我们使用AOP面向切面编程便可以很轻松的完成这个工作,做到在不改变原始方法的情况下,对原始方法进行编程(增强或改变)。</p> <p>!<a href="https://img-blog.csdnimg.cn/497c8d40b89247269102029e24beb49a.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <p>我们可以定义一个模板方法,将通用的逻辑定义在模板方法中,再在模板方法中调用原始方法。当我们调用原始方法时,将不会直接执行原始方法,而是转而执行模板方法并在其中开始原始方法的执行。</p> <p><br></p> <h2>(2) 案例实现</h2> <p><br></p> <p>实现过程我们暂时可以不用关注,只需看我们在不修改原始方法的情况下,给下面四个方法进行万次执行耗时测试的效果。</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> // 原始方法类 </div> <div class="ql-code-block"> @Repository </div> <div class="ql-code-block"> public class BookDaoImpl implements BookDao { </div> <div class="ql-code-block"> System.out.println("book dao save ..."); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> public void update(){ </div> <div class="ql-code-block"> System.out.println("book dao update ..."); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> public void delete(){ </div> <div class="ql-code-block"> System.out.println("book dao delete ..."); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> public void select(){ </div> <div class="ql-code-block"> System.out.println("book dao select ..."); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <p>我们来定义模板方法测试他们,语法暂时不用关心~</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> @Component </div> <div class="ql-code-block"> @Aspect </div> <div class="ql-code-block"> public class MyAdvice { </div> <div class="ql-code-block"> @Pointcut("execution(* com.guanzhi.dao.BookDao.*(..))") </div> <div class="ql-code-block"> private void pt(){} </div> <div class="ql-code-block"><br> </div> <div class="ql-code-block"> // 测试方法万次执行耗时 </div> <div class="ql-code-block"> @Around("pt()") </div> <div class="ql-code-block"> public Object around(ProceedingJoinPoint pjp) throws Throwable { </div> <div class="ql-code-block"> System.out.println("------------------------------"); </div> <div class="ql-code-block"> Long startTime = System.currentTimeMillis(); </div> <div class="ql-code-block"> for (int i = 0 ; i&lt;10000 ; i++) { </div> <div class="ql-code-block"> //调用原始操作 </div> <div class="ql-code-block"> pjp.proceed(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> Long endTime = System.currentTimeMillis(); </div> <div class="ql-code-block"> Long totalTime = endTime-startTime; </div> <div class="ql-code-block"> System.out.println("执行万次消耗时间:" + totalTime + "ms"); </div> <div class="ql-code-block"> return null; </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <p>我们分别执行上述<code>save,delete,update和select</code> 方法后会有如下的打印结果:</p> <p>!<a href="https://img-blog.csdnimg.cn/3b2613eaf0254aa1b440d47d595dd6f6.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <p>可以看到我们未对上述原始方法进行任何的修改,却成功的给他们都加上了耗时测试,一劳永逸是不是很爽呀?</p> <p><br></p> <h2>(3) 小结</h2> <p><br></p> <p>除此之外,AOP还可以帮助我们很快很方便的实现众多功能,例如:</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>统一记录操作日志</li> <li data-list="bullet"><span class="ql-ui"></span>统一进行权限控制</li> <li data-list="bullet"><span class="ql-ui"></span>事务管理</li> <li data-list="bullet"><span class="ql-ui"></span>......</li> </ol> <p><br></p> <p>它有着许多的优势使我们开发体验更好:</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>代码无侵入,增加功能不影响原始代码</li> <li data-list="bullet"><span class="ql-ui"></span>减少众多重复代码的编写</li> <li data-list="bullet"><span class="ql-ui"></span>维护方便,需要发生更改时我们只要修改模板方法即可</li> <li data-list="bullet"><span class="ql-ui"></span>.....</li> </ol> <p><br></p> <h1>三.核心概念</h1> <p><br></p> <h2>(1) 引入</h2> <p><br></p> <p>说了这么多,Spring到底是如何实现的呢?接下来我们一起了解一下AOP的相关概念。</p> <p>!<a href="https://img-blog.csdnimg.cn/681c469c4ed346338c40b0dd8997978b.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>前面一直在强调,Spring的AOP是对一个类的方法在不进行任何修改的前提下实现增强。对于上面的案例中BookServiceImpl中有<code>save,update,delete和select方法,这些方法我们给起了一个名字叫连接点</code></li> </ol> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>这四个方法中都只有打印而没有计算万次执行消耗时间的功能,但是在<strong>运行的时候已经有该功能</strong>,那也就是说这几个方法都已经被增强,所以对于<strong>需要增强的方法</strong>我们给起了一个名字叫<code>切入点</code></li> </ol> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>在执行这四个方法的时候都被添加了一个计算万次执行消耗时间的功能,将这个功能抽取到一个方法中,换句话说就是<strong>存放共性功能的方法</strong>,我们给起了个名字叫<code>通知</code></li> </ol> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>通知是要增强的内容,会有多个,切入点是需要被增强的方法,也会有多个,那哪个切入点需要添加哪个通知,就需要提前将它们之间的关系描述清楚,那么对于<strong>通知和切入点之间的关系描述</strong>,我们给起了个名字叫<code>切面</code></li> </ol> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>通知是一个方法,方法不能独立存<strong>在需要被写在一个类中</strong>,这个类我们也给起了个名字叫<code>通知类</code></li> </ol> <p><br></p> <p>概括来说就是:首先我们先找到程序中的共性功能抽取出来,写一个通知类,在通知类中定义一个方法,这个方法就叫做通知,我们再把需要执行通知的方法找出来,也就是切入点,最后我们再把他们之间进行绑定也就是切面。</p> <p><br></p> <h2>(2) 总结</h2> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><code>连接点(JoinPoint)</code> :程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等</li> <li data-list="bullet" class="ql-indent-2"><span class="ql-ui"></span>在SpringAOP中,理解为<strong>方法的执行</strong></li> <li data-list="bullet"><span class="ql-ui"></span><code>切入点(Pointcut)</code> :匹配连接点的式子</li> <li data-list="bullet" class="ql-indent-2"><span class="ql-ui"></span>在SpringAOP中,一个切入点可以描述<strong>一个具体方法,也可也匹配多个方法</strong></li> <li data-list="bullet" class="ql-indent-4"><span class="ql-ui"></span><strong>一个具体的方法</strong>:如com.guanzhi.dao包下的BookDao接口中的无形参无返回值的save方法</li> <li data-list="bullet" class="ql-indent-4"><span class="ql-ui"></span><strong>匹配多个方法</strong>:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法</li> <li data-list="bullet" class="ql-indent-2"><span class="ql-ui"></span> 连接点范围要比切入点范围大,是切入点的方法也一定是连接点,但是是连接点的方法就不一定要被增强,所以可能不是切入点。</li> <li data-list="bullet"><span class="ql-ui"></span><code>通知(Advice)</code> :在切入点处执行的操作,也就是共性功能</li> <li data-list="bullet" class="ql-indent-2"><span class="ql-ui"></span>在SpringAOP中,功能最终<strong>以方法的形式呈现</strong></li> <li data-list="bullet"><span class="ql-ui"></span><code>通知类</code> :定义通知的类</li> <li data-list="bullet"><span class="ql-ui"></span><code>切面(Aspect)</code> :描述通知与切入点的对应关系。</li> </ol> <p><br></p> <h1>四.入门案例</h1> <p><br></p> <p>了解了AOP的作用与概念之后,接下来让我们一步一步的来使用一下AOP面向切面编程~</p> <p><br></p> <h2>(1) 引入</h2> <p><br></p> <p>上述测试接口执行效率案例稍微有点繁琐,为了方便我们初学AOP的使用,我们对其进行简化,<strong>只在方法执行前输出当前系统时间。</strong></p> <p><br></p> <p>此外对于SpringAOP的开发也<strong>有两种方式</strong>,<strong>XML</strong> 和 <strong>注解</strong>,由于现在注解使用的比较多,所以我们就采用注解完成案例。</p> <p><br></p> <p>需求明确后,具体该如何实现,都有哪些步骤,我们先来分析下:</p> <p><br></p> <ol> <li data-list="ordered"><span class="ql-ui"></span>导入相关坐标(pom.xml)</li> </ol> <p><br></p> <ol> <li data-list="ordered"><span class="ql-ui"></span>制作连接点(原始操作,Dao接口与实现类)</li> </ol> <p><br></p> <ol> <li data-list="ordered"><span class="ql-ui"></span>制作共性功能(通知类与通知)</li> </ol> <p><br></p> <ol> <li data-list="ordered"><span class="ql-ui"></span>定义切入点</li> </ol> <p><br></p> <ol> <li data-list="ordered"><span class="ql-ui"></span>绑定切入点与通知关系(切面)</li> </ol> <p><br></p> <h2>(2) 环境准备</h2> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>创建一个Maven项目,在pom.xml添加spring-aop相关依赖</li> </ol> <p><br></p> <p>```xml</p> <p>&lt;dependencies&gt;</p> <p>&lt;dependency&gt;</p> <p>&lt;groupId&gt;org.springframework&lt;/groupId&gt;</p> <p>&lt;artifactId&gt;spring-context&lt;/artifactId&gt;</p> <p>&lt;version&gt;5.2.10.RELEASE&lt;/version&gt;</p> <p>&lt;/dependency&gt;</p> <p></p> <p>&lt;dependency&gt;</p> <p>&lt;groupId&gt;org.aspectj&lt;/groupId&gt;</p> <p>&lt;artifactId&gt;aspectjweaver&lt;/artifactId&gt;</p> <p>&lt;version&gt;1.9.4&lt;/version&gt;</p> <p>&lt;/dependency&gt;</p> <p>&lt;/dependencies&gt;</p> <p>```</p> <p><br></p> <p>因为<code>spring-context中已经导入了spring-aop,所以不需要再单独导入spring-aop</code></p> <p>!<a href="https://img-blog.csdnimg.cn/8ca0b6a5086b43da9976b7424a302571.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <p>AspectJ是AOP思想的一个具体实现,Spring有自己的AOP实现,但是相比于AspectJ来说比较麻烦,所以我们直接采用Spring整合ApsectJ的方式进行AOP开发。</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>制作BookDao和BookDaoImpl类</li> </ol> <p><br></p> <p>```java</p> <p>public interface BookDao {</p> <p>public void save();</p> <p>public void update();</p> <p>}</p> <p></p> <p>@Repository</p> <p>public class BookDaoImpl implements BookDao {</p> <p></p> <p>public void save() {</p> <p>System.out.println("book dao save ...");</p> <p>}</p> <p></p> <p>public void update(){</p> <p>System.out.println("book dao update ...");</p> <p>}</p> <p>}</p> <p>```</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>创建Spring的配置类</li> </ol> <p><br></p> <p>```java</p> <p>@Configuration</p> <p>@ComponentScan("com.guanzhi")</p> <p>public class SpringConfig {</p> <p>}</p> <p>```</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>编写App运行类,用于调用测试功能</li> </ol> <p><br></p> <p>```java</p> <p>public class App {</p> <p>public static void main(String[] args) {</p> <p>ApplicationContext ctx = new</p> <p>AnnotationConfigApplicationContext(SpringConfig.class);</p> <p>BookDao bookDao = ctx.getBean(BookDao.class);</p> <p>bookDao.save();</p> <p>}</p> <p>}</p> <p>```</p> <p><br></p> <p>最终创建好的项目结构如下:</p> <p>!<a href="https://img-blog.csdnimg.cn/818fb45c816d4d72a0f2d837c81c6b44.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <p>再说明一下我们的目的:使用SpringAOP的方式在不改变方法的前提下让其具有打印系统时间的功能。</p> <p><br></p> <h2>(3) AOP实现步骤</h2> <p><br></p> <h3>(3.1) 定义通知类和通知</h3> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>通知就是将共性功能抽取出来后形成的方法,共性功能指的就是当前系统时间的打印。</li> </ol> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> // 通知类 </div> <div class="ql-code-block"> public class MyAdvice { </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> // 通知 </div> <div class="ql-code-block"> // 打印当前系统时间的 </div> <div class="ql-code-block"> public void method(){ </div> <div class="ql-code-block"> System.out.println(System.currentTimeMillis()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <p>类名和方法名没有要求,可以任意。</p> <p><br></p> <h3>(3.2) 定义切入点</h3> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>BookDaoImpl中有两个方法,分别是save和update,我们现在<strong>只增强</strong>的是update方法,该如何定义呢?</li> </ol> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> // 通知类 </div> <div class="ql-code-block"> public class MyAdvice { </div> <div class="ql-code-block"> // 定义切入点 </div> <div class="ql-code-block"> @Pointcut("execution(void com.guanzhi.dao.BookDao.update())") </div> <div class="ql-code-block"> private void pt(){} </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> // 通知 </div> <div class="ql-code-block"> public void method(){ </div> <div class="ql-code-block"> System.out.println(System.currentTimeMillis()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <p><strong>说明:</strong></p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>切入点定义依托一个不具有实际意义的方法进行,即无参数、无返回值、方法体无实际逻辑,还有另一种写法后面会讲。</li> <li data-list="bullet"><span class="ql-ui"></span>execution及后面编写的内容,先不必关心,后面我们也会讲到。</li> </ol> <p><br></p> <h3>(3.3) 制作切面</h3> <p><br></p> <p>切面是用来描述通知和切入点之间的关系,如何进行关系的绑定?</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> // 通知类 </div> <div class="ql-code-block"> public class MyAdvice { </div> <div class="ql-code-block"> // 定义切入点 </div> <div class="ql-code-block"> @Pointcut("execution(void com.guanzhi.dao.BookDao.update())") </div> <div class="ql-code-block"> private void pt(){} </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> // 通知 </div> <div class="ql-code-block"> // 绑定切入点与通知关系,并指定通知添加到原始连接点的具体执行位置(Before) </div> <div class="ql-code-block"> @Before("pt()") </div> <div class="ql-code-block"> public void method(){ </div> <div class="ql-code-block"> System.out.println(System.currentTimeMillis()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <p><strong>说明:</strong>@Before翻译过来是之前,也就是说通知会在切入点方法执行之前执行,除此之前还有其他四种类型,后面会讲。</p> <p><br></p> <h3>(3.4) 标识切面类</h3> <p><br></p> <p>目前我们的Spring容器并不知道它的存在,更不知道它是通知类,因此我们还需要给他加上相应注解。</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> // 交给Spring管理 </div> <div class="ql-code-block"> @Component </div> <div class="ql-code-block"> // 定义为切面类 </div> <div class="ql-code-block"> @Aspect </div> <div class="ql-code-block"> public class MyAdvice { </div> <div class="ql-code-block"> @Pointcut("execution(void com.guanzhi.dao.BookDao.update())") </div> <div class="ql-code-block"> private void pt(){} </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> @Before("pt()") </div> <div class="ql-code-block"> public void method(){ </div> <div class="ql-code-block"> System.out.println(System.currentTimeMillis()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <h3>(3.5) 开启注解格式AOP功能</h3> <p><br></p> <p>我们开始说到了使用注解开发,由此需要开启相应功能告知Spring加载相关功能</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> @Configuration </div> <div class="ql-code-block"> @ComponentScan("com.guanzhi") </div> <div class="ql-code-block"> @EnableAspectJAutoProxy </div> <div class="ql-code-block"> public class SpringConfig { </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <h3>(3.6) 运行程序</h3> <p><br></p> <p>好了,我们终于可以测试我们的程序了~</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> public class App { </div> <div class="ql-code-block"> public static void main(String[] args) { </div> <div class="ql-code-block"> ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); </div> <div class="ql-code-block"> BookDao bookDao = ctx.getBean(BookDao.class); </div> <div class="ql-code-block"> bookDao.update(); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <p>运行看到在执行update方法之前打印了系统时间戳,说明对原始方法进行了增强,AOP编程成功。</p> <p>!<a href="https://img-blog.csdnimg.cn/1debc92099e74728807293c86898113f.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <h2>(4) 相关注解说明</h2> <p><br></p> <h3>@EnableAspectJAutoProxy</h3> <p><br></p> <p>| 名称 | @EnableAspectJAutoProxy |</p> <p>| ---- | ----------------------- |</p> <p>| 类型 | 配置类注解 |</p> <p>| 位置 | 配置类定义上方 |</p> <p>| 作用 | 开启注解格式AOP功能 |</p> <p><br></p> <h3>@Aspect</h3> <p><br></p> <p>| 名称 | @Aspect |</p> <p>| ---- | --------------------- |</p> <p>| 类型 | 类注解 |</p> <p>| 位置 | 切面类定义上方 |</p> <p>| 作用 | 设置当前类为AOP切面类 |</p> <p><br></p> <h3>@Pointcut</h3> <p><br></p> <p>| 名称 | @Pointcut |</p> <p>| ---- | --------------------------- |</p> <p>| 类型 | 方法注解 |</p> <p>| 位置 | 切入点方法定义上方 |</p> <p>| 作用 | 设置切入点方法 |</p> <p>| 属性 | value(默认):切入点表达式 |</p> <p><br></p> <h3>@Before</h3> <p><br></p> <p>| 名称 | @Before |</p> <p>| ---- | ------------------------------------------------------------ |</p> <p>| 类型 | 方法注解 |</p> <p>| 位置 | 通知方法定义上方 |</p> <p>| 作用 | 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行 |</p> <p><br></p> <p>通知类型还有几种我们后期会一一学习到~</p> <p><br></p> <h1>五.工作流程</h1> <p><br></p> <p>我们已经完成了AOP的入门案例,接下来我们一起对于刚才案例的执行过程进行分析。</p> <p><br></p> <p>由于AOP是基于Spring容器管理的bean做的增强,所以整个工作过程需要从Spring加载bean说起</p> <p><br></p> <h2>(1) Spring容器启动</h2> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>容器启动就需要去加载bean,上述案例中哪些类需要被加载呢?</li> <li data-list="bullet"><span class="ql-ui"></span>需要被增强的类,如:BookServiceImpl</li> <li data-list="bullet"><span class="ql-ui"></span>通知类,如:MyAdvice</li> <li data-list="bullet"><span class="ql-ui"></span>注意此时bean对象还没有创建成功</li> </ol> <p><br></p> <h2>(2) 读取所有切面配置中的切入点</h2> <p>!<a href="https://img-blog.csdnimg.cn/23b098be478f4a4b92b0d2333ef956ae.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>上面这个例子中有两个切入点的配置,但是第一个<code>ptx()</code> 并没有被使用,所以不会被读取。</li> </ol> <p><br></p> <h2>(3) 初始化bean</h2> <p><br></p> <p>判定bean对应的类中的方法是否匹配到任意切入点</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>注意第1步在容器启动的时候,bean对象还没有被创建成功。</li> </ol> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>让要被实例化bean对象的类中的方法和切入点进行匹配</li> </ol> <p>!<a href="https://img-blog.csdnimg.cn/0d3dd1f3d5234640a4039718b77cb25a.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <ol> <li data-list="bullet" class="ql-indent-2"><span class="ql-ui"></span>匹配失败,创建原始对象,如<code>UserDao</code></li> <li data-list="bullet" class="ql-indent-4"><span class="ql-ui"></span>匹配失败说明不需要增强,直接调用原始对象的方法即可。</li> <li data-list="bullet" class="ql-indent-2"><span class="ql-ui"></span>匹配成功,创建原始对象(<strong>目标对象</strong>)的<strong>代理对象</strong>,如:<code>BookDao</code></li> <li data-list="bullet" class="ql-indent-4"><span class="ql-ui"></span>匹配成功说明需要对其进行增强</li> <li data-list="bullet" class="ql-indent-4"><span class="ql-ui"></span><strong>对哪个类做增强,这个类对应的对象就叫做目标对象</strong></li> <li data-list="bullet" class="ql-indent-4"><span class="ql-ui"></span><strong>因为要对目标对象进行功能增强,而采用的技术是动态代理,所以会为其创建一个代理对象</strong></li> <li data-list="bullet" class="ql-indent-4"><span class="ql-ui"></span><strong>最终运行的是代理对象的方法</strong>,在该方法中会对原始方法进行功能增强</li> </ol> <p><br></p> <h2>(4) 获取bean执行方法</h2> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>获取的bean是<code>原始对象</code> 时,调用相应方法并执行,完成操作</li> <li data-list="bullet"><span class="ql-ui"></span>获取的bean是<code>代理对象</code> 时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作</li> </ol> <p><br></p> <h2>(5) 验证容器中是否为代理对象</h2> <p><br></p> <p>为了验证IOC容器中创建的对象和我们刚才所说的结论是否一致,首先把结论理出来:</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>如果目标对象中的方法会被增强,那么容器中将存入的是目标对象的代理对象</li> <li data-list="bullet"><span class="ql-ui"></span>如果目标对象中的方法不被增强,那么容器中将存入的是目标对象本身。</li> </ol> <p><br></p> <h3>(5.1) 验证思路</h3> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>先让要执行的方法,不被定义的切入点包含,即不增强,打印当前类的getClass()方法</li> </ol> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>再让要执行的方法,被定义的切入点包含,即增强,打印出当前类的getClass()方法</li> </ol> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span>观察两次打印的结果</li> </ol> <p><br></p> <h3>(5.2) 修改App类,获取类的类型</h3> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> public class App { </div> <div class="ql-code-block"> public static void main(String[] args) { </div> <div class="ql-code-block"> ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class); </div> <div class="ql-code-block"> BookDao bookDao = ctx.getBean(BookDao.class); </div> <div class="ql-code-block"> System.out.println(bookDao); </div> <div class="ql-code-block"> System.out.println(bookDao.getClass()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <h3>(5.3) 修改MyAdvice类,不增强</h3> <p><br></p> <p>因为定义的切入点中,被修改成<code>update1</code> ,所以BookDao中的update方法在执行的时候,就不会被增强,</p> <p><br></p> <p>所以容器中的对象应该是目标对象本身。</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> @Component </div> <div class="ql-code-block"> @Aspect </div> <div class="ql-code-block"> public class MyAdvice { </div> <div class="ql-code-block"> @Pointcut("execution(void com.itheima.dao.BookDao.update1())") </div> <div class="ql-code-block"> private void pt(){} </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> @Before("pt()") </div> <div class="ql-code-block"> public void method(){ </div> <div class="ql-code-block"> System.out.println(System.currentTimeMillis()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <h3>(5.4) 运行程序</h3> <p>!<a href="https://img-blog.csdnimg.cn/ee9fbacc74e14ab0a6a6dc8416a2d590.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <h3>(5.5) 修改MyAdvice类,增强</h3> <p><br></p> <p>因为定义的切入点中,被修改成<code>update</code> ,所以BookDao中的update方法在执行的时候,就会被增强,</p> <p><br></p> <p>所以容器中的对象应该是目标对象的代理对象</p> <p><br></p> <div class="ql-code-block-container"> <div class="ql-code-block"> ava </div> <div class="ql-code-block"> @Component </div> <div class="ql-code-block"> @Aspect </div> <div class="ql-code-block"> public class MyAdvice { </div> <div class="ql-code-block"> @Pointcut("execution(void com.itheima.dao.BookDao.update())") </div> <div class="ql-code-block"> private void pt(){} </div> <div class="ql-code-block"> </div> <div class="ql-code-block"> @Before("pt()") </div> <div class="ql-code-block"> public void method(){ </div> <div class="ql-code-block"> System.out.println(System.currentTimeMillis()); </div> <div class="ql-code-block"> } </div> <div class="ql-code-block"> } </div> </div> <p><br></p> <p><br></p> <h3>(5.6) 运行程序</h3> <p>!<a href="https://img-blog.csdnimg.cn/a35597d89189432c8d58d1908ac184b8.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <p>至此对于刚才的结论,我们就得到了验证,这块大家需要注意的是:</p> <p><br></p> <p>不能直接打印对象,从上面两次结果中可以看出,直接打印对象走的是对象的toString方法,不管是不是代理对象打印的结果都是一样的,原因是内部对toString方法进行了重写。</p> <p>!<a href="https://img-blog.csdnimg.cn/d6113f085ad448ccb5fa069819116e31.png" target="_blank">在这里插入图片描述</a></p> <p><br></p> <p><br></p> <h2>(6) 核心概念补充</h2> <p><br></p> <p>在上面介绍AOP的工作流程中,我们提到了两个核心概念,分别是:</p> <p><br></p> <ol> <li data-list="bullet"><span class="ql-ui"></span><strong>目标对象(Target)</strong>:原始功能去掉共性功能对应的类产生的对象,这种对象是无法直接完成最终工作的</li> <li data-list="bullet"><span class="ql-ui"></span><strong>代理对象(Proxy)</strong>:目标对象无法直接完成工作,需要对其进行功能回填,通过原始对象的代理对象实现</li> </ol> <p><br></p> <p>上面这两个概念比较抽象,简单来说,</p> <p><br></p> <p>目标对象就是要增强的类[如:BookServiceImpl类]对应的对象,也叫原始对象,不能说它不能运行,只是它的运行效果不能够令我们满意。</p> <p><br></p> <p>SpringAOP是在不改变原有设计(代码)的前提下对其进行增强的,它的底层采用的是代理模式实现的,所以要对原始对象进行增强,就需要对原始对象创建代理对象,在代理对象中的方法把通知 [如:MyAdvice中的method方法] 内容加进去,就实现了增强,这就是我们所说的代理(Proxy)。</p> </div> </body> </html>

下载 APP