Spring(3-AOP)

AOP简介

Spring框架有两大核心:IOC/DI (控制反转/依赖注入)和AOP(面向切面编程)。

我们已经系统学习了IOC/DI,现在来学习AOP。AOP的核心思想是:在不修改原有代码的前提下,对程序功能进行增强

AOP(Aspect Oriented Programming,面向切面编程)是一种编程范式,指导开发者如何组织程序结构

  • OOP(Object Oriented Programming,面向对象编程)也是一种编程范式
  • 两者是不同的编程思想,分别从不同角度解决软件设计问题

AOP的作用:在不修改原始代码的基础上,为程序添加新的功能。

实际上,我们之前学过的代理模式就可以实现这样的功能,Spring的AOP底层正是基于代理模式实现的。

AOP核心概念详解

为了更好地理解AOP,我们先看一个例子:

java 复制代码
@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        //记录程序当前执行执行(开始时间)
        Long startTime = System.currentTimeMillis();
        //业务执行万次
        for (int i = 0;i

**观察与思考**:

*   只有`save()`方法有计算执行时间的代码
*   但运行后发现,`update()`和`delete()`方法也具备了计算时间的功能
*   `select()`方法却没有这个功能

这就是AOP的魔力:在不改动原有代码的情况下,选择性地为某些方法添加新功能!

**Spring是如何实现的**:\ <img src="https://p3-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/d67eb945a0b643b3915d10af9e5e1088~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg6YOh5p2w:q75.awebp?rk3s=f64ab15b&x-expires=1765985902&x-signature=C9ZBbkcu2HchOCCkBePTgtqtdp0%3D" alt="image.png" width="100%">

**AOP核心概念**

1.  **连接点**

*   定义:程序执行过程中**可以插入额外功能的点**
*   实质:**可被增强的方法**
*   理解与例子:供插入的点,`BookDaoImpl`中的`save()`、`update()`、`delete()`、`select()`方法都是连接点

2.  **切入点**

*   定义:实际**被增强了功能的连接点**
*   实质:**已被增强的方法**
*   与连接点关系:**切入点⊆连接点**(切入点一定是连接点,但连接点不一定是切入点)
*   理解与例子:已插入的点,`update()`和`delete()`方法被增强了,它们就是切入点;`select()`方法没有被增强,它只是连接点

3.  **通知**

*   定义:要添加的**额外功能**
*   实质:**存放增强功能的方法**
*   例子:计算方法执行时间的功能就是一个通知

4.  **通知类/切面类**

*   定义:存放通知方法的类
*   特点:通知方法不能独立存在,必须放在某个类中

5.  **切面**

*   定义:描述通知和切入点之间的对应关系,建立&#34;什么地方&#34;和&#34;做什么事&#34;的映射关系
*   实质:描述哪些方法增强了哪些功能

# AOP入门案例

## 需求分析

**场景设定**:我们需要测量接口方法的执行效率,但为了入门学习,我们先简化需求:在方法执行前输出当前系统时间。

**技术选择**:Spring AOP支持XML配置和注解两种方式,由于注解方式更简洁、现代,我们选择使用**注解方式**开发。

**最终目标**:使用Spring AOP注解,在不修改原有方法代码的前提下,在方法执行前打印当前系统时间。

## 实现思路

让我们将整个实现过程分解为清晰的步骤:

1.  **导入依赖** - 在pom.xml中添加必要的Spring AOP依赖
2.  **准备连接点** - 创建Dao接口和实现类(原始业务代码)
3.  **制作通知** - 创建通知类和通知方法(增强功能)
4.  **定义切入点** - 指定哪些方法需要被增强
5.  **绑定关系** - 通过切面将通知和切入点关联起来

## 环境准备

**第一步:创建Maven项目并添加依赖**

```xml

    
    
        org.springframework
        spring-context
        5.2.10.RELEASE
    
    
    
    
        org.springframework
        spring-aspects
        5.2.10.RELEASE
    

第二步:创建业务接口和实现类(连接点)

BookDao接口:

java 复制代码
public interface BookDao {
    void save();
    void update();
}

BookDaoImpl实现类:

java 复制代码
@Repository
public class BookDaoImpl implements BookDao {
    
    public void save() {
        // 原始业务逻辑
        System.out.println(&#34;book dao save ...&#34;);
    }
    
    public void update() {
        // 原始业务逻辑  
        System.out.println(&#34;book dao update ...&#34;);
    }
}

第三步:创建Spring配置类

java 复制代码
@Configuration
@ComponentScan(&#34;com.itheima&#34;)
@EnableAspectJAutoProxy  // 开启AOP注解支持
public class SpringConfig {
}

第四步:创建测试运行类

java 复制代码
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        
        // 测试方法调用
        bookDao.save();
        bookDao.update();
    }
}

最终创建好的项目结构如下: \

当前状态分析

当前运行效果:

  • save()方法:只输出"book dao save ..."
  • update()方法:只输出"book dao update ..."

我们的目标:

  • 在不修改BookDaoImpl代码的前提下
  • 让这两个方法在执行前都输出系统时间
  • 实现"无侵入式"的功能增强

AOP实现步骤

步骤1:添加AOP依赖

在pom.xml中添加AspectJ依赖:

xml 复制代码
    
    
        org.springframework
        spring-context
        5.2.10.RELEASE
    
    
    
    
        org.aspectj
        aspectjweaver
        1.9.4
    

重要说明

  • spring-context已经包含了spring-aop,无需重复导入
  • 我们使用AspectJ作为AOP的具体实现,因为它功能更强大、使用更简单

步骤2:确认业务类(连接点)

我们的BookDaoImpl已经准备好,包含两个方法:

java 复制代码
@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        System.out.println(&#34;book dao save ...&#34;);
    }
    
    public void update() {
        System.out.println(&#34;book dao update ...&#34;);
    }
}

步骤3:创建通知类和通知方法

创建通知类,定义要添加的增强功能(即定义通知):

java 复制代码
public class MyAdvice {
    // 通知方法:打印当前系统时间
    public void printCurrentTime() {
        System.out.println(&#34;当前时间:&#34; + System.currentTimeMillis());
    }
}

步骤4:在通知类中定义切入点

使用@Pointcut注解定义切入点(指定要增强的方法):

java 复制代码
public class MyAdvice {
    // 定义切入点:匹配BookDao的update方法
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void updateMethodPointcut() {}
    
    public void printCurrentTime() {
        System.out.println(&#34;当前时间:&#34; + System.currentTimeMillis());
    }
}

切入点说明

  • execution(void com.itheima.dao.BookDao.update())表示匹配:
    • 返回类型:void
    • 完整方法名包名.实现类名.接口名() ):com.itheima.dao.BookDao.update()
  • 切入点方法 updateMethodPointcut()仅用于标记,本身没有实际逻辑

@Pointcut注解的意义

java 复制代码
@Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
private void updateMethodPointcut() {}
  1. 最简单的理解:@Pointcut定义 "监控哪个方法"

    text 复制代码
    &#34;我要监控 com.itheima.dao.BookDao.update() 这个方法&#34;
  2. 各部分分解:

  • execution = "执行时监控"
  • void = "返回类型是void的方法"
  • com.itheima.dao.BookDao.update() = "具体监控哪个方法"
  • updateMethodPointcut = "给这个监控点起个名字"
  • 完整意思 :当 以返回类型为void的方式,调用com.itheima.dao.BookDao类中的update方法时,就会触发后续的通知。
  1. 为什么需要一个空方法?

    这个空方法updateMethodPointcut()的作用就是:
    给监控规则及其方法起个名字,方便其他代码引用,相当于"取别名"、"贴标签"

    • 如果没有这个名字,每次都要写完整的监控规则:

      java 复制代码
      @Before(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
      @After(&#34;execution(void com.itheima.dao.BookDao.update())&#34;) 
      @Around(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    • 有了名字,就简单多了:

      java 复制代码
      @Before(&#34;updateMethodPointcut()&#34;)
      @After(&#34;updateMethodPointcut()&#34;)
      @Around(&#34;updateMethodPointcut()&#34;)

步骤5:在通知类中制作切面(绑定通知和切入点)

使用@Before注解将通知方法与切入点绑定:

java 复制代码
public class MyAdvice {
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void updateMethodPointcut() {}
    
    // 在切入点监控的方法执行前执行通知
    @Before(&#34;updateMethodPointcut()&#34;)
    public void printCurrentTime() {
        System.out.println(&#34;当前时间:&#34; + System.currentTimeMillis());
    }
}

定义通知的执行时机

  • @Before:在切入点方法监控的方法执行之前执行通知方法
  • 还有其他时机:@After(切入点之后)、@Around(环绕)等

步骤6:配置通知类为切面类

添加注解,让Spring识别这是一个切面类:

java 复制代码
@Component  // 交给Spring容器管理
@Aspect     // 标识这是一个切面类
public class MyAdvice {
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void updateMethodPointcut() {}
    
    @Before(&#34;updateMethodPointcut()&#34;)
    public void printCurrentTime() {
        System.out.println(&#34;当前时间:&#34; + System.currentTimeMillis());
    }
}

步骤7:开启AOP注解支持

在Spring配置类中启用AOP功能:

java 复制代码
@Configuration
@ComponentScan(&#34;com.itheima&#34;)
@EnableAspectJAutoProxy  // 开启AOP注解自动代理
public class SpringConfig {
}

步骤8:测试运行

运行程序查看AOP效果:

java 复制代码
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        
        // 调用update方法,会触发AOP增强
        bookDao.update();
        
        // 调用save方法,不会触发AOP增强
        bookDao.save();
    }
}

预期运行结果

注解汇总

名称 @EnableAspectJAutoProxy
类型 配置类注解
位置 配置类定义上方
作用 开启注解格式AOP功能
名称 @Aspect
类型 类注解
位置 切面类定义上方
作用 设置当前类为AOP切面类
名称 @Pointcut
类型 方法注解
位置 切入点方法定义上方
作用 设置切入点方法
属性 value(默认):切入点表达式
名称 @Before
类型 方法注解
位置 通知方法定义上方
作用 设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行

AOP工作流程

AOP工作流程

为了更好地理解AOP的工作流程,我们可以把它想象成一个"智能安保系统"的工作过程:

流程1:Spring容器启动 - 系统初始化

  • Spring容器启动时,扫描并识别所有需要管理的类
  • 识别两类重要角色:
    • 目标类 :需要被增强的类(如:BookServiceImpl
    • 通知类 :包含增强功能的类(如:MyAdvice
  • 注意:此时只是识别了这些类,还没有创建具体的对象实例

流程2:读取切面配置 - 制定监控规则

  • Spring读取所有@Aspect切面类中的 实际被使用的 @Pointcut切入点(有通知方法引用的)配置
  • 未被引用的切入点不会被处理

流程3:创建Bean并判断是否需要增强 - 对象检查

当Spring创建每个bean实例时,会进行重要判断:

  1. 情况A:匹配失败 → 创建原始对象
  • 如果类中的方法没有匹配 任何切入点,Spring直接创建原始对象,后续调用方法时直接执行,无任何拦截
  • 例子:UserDao类中的方法都不需要增强
  1. 情况B:匹配成功 → 创建代理对象
  • 如果类中的方法匹配到 切入点规则,Spring创建代理对象(而不是直接使用原始对象),后续所有方法调用都通过代理对象进行
  • 目标对象:原始的、需要被增强的对象
  • 代理对象:Spring生成的、包含增强逻辑的"替身"

流程4:获取Bean执行方法 - 实际行为监控

获取到原始对象时:

java 复制代码
// 直接调用方法,无任何拦截
userDao.someMethod();  // 直接执行原始逻辑

获取到代理对象时:

java 复制代码
// 通过代理对象调用方法
bookDao.update();
// 实际执行过程:
// 1. 代理对象拦截方法调用
// 2. 执行前置增强(如:打印时间)
// 3. 调用原始对象的update()方法
// 4. 返回结果

AOP代理对象验证

验证Spring AOP的工作机制:

  • 当方法需要增强时,容器中存储的是代理对象
  • 当方法不需要增强时,容器中存储的是原始对象本身

验证步骤

步骤1:修改测试类,获取对象类型信息

java 复制代码
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        
        // 打印对象引用和具体类型
        System.out.println(&#34;对象引用: &#34; + bookDao);
        System.out.println(&#34;实际类型: &#34; + bookDao.getClass());
    }
}

步骤2:场景一:方法不被增强(不匹配切入点)

修改通知类,使切入点不匹配update方法:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    // 切入点指向不存在的update1方法,update方法不会被增强
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update1())&#34;)
    private void pt(){}
    
    @Before(&#34;pt()&#34;)
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

运行结果:

kotlin 复制代码
对象引用: com.itheima.dao.impl.BookDaoImpl@xxxxxx
实际类型: class com.itheima.dao.impl.BookDaoImpl

分析:

  • 显示的是原始类名BookDaoImpl
  • 说明容器中存储的是原始对象本身
  • 因为update方法不匹配切入点,不需要增强

步骤3:场景二:方法被增强(匹配切入点)

修改通知类,使切入点匹配update方法:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    // 切入点正确指向update方法,该方法会被增强
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void pt(){}
    
    @Before(&#34;pt()&#34;)
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

运行结果:

kotlin 复制代码
对象引用: com.itheima.dao.impl.BookDaoImpl@xxxxxx
实际类型: class com.sun.proxy.$ProxyXX

分析:

  • 显示的是代理类名$ProxyXX(具体编号可能不同)
  • 说明容器中存储的是代理对象
  • 因为update方法匹配切入点,需要增强

关于toString()方法的注意事项

两次运行都打印了相同的对象引用信息,这是因为:

  • 代理对象重写了toString()方法,使其显示与原始对象相同
  • 直接打印对象无法区分是否为代理对象
  • 必须通过getClass()方法才能看到真实的类型信息

AOP核心概念

目标对象 (Target Object)

定义:

  • 是(未增强)原始业务类的对象
  • 在AOP中,这是需要被增强的对象,不包含增强功能

准确理解:

  • 目标对象是完整的、可以独立运行的对象
  • 它包含了设计时预期的所有业务功能
  • 只是在AOP的视角下,它"缺少"某些横切关注点功能(如日志、事务等)

示例说明:

java 复制代码
// BookDaoImpl 实例就是目标对象
// 它本身完全可以正常工作,执行save()、update()等业务方法
BookDao bookDao = new BookDaoImpl();  // 目标对象
bookDao.update();  // 可以正常执行

代理对象 (Proxy Object)

定义:

  • Spring为需要增强的目标对象创建的替代对象
  • 在目标对象的基础上已添加了额外功能

准确理解/代理模式的本质:

  • 代理对象不修改目标对象,而是创建新对象包装原始对象
  • 代理对象内部持有目标对象的引用
  • 当调用代理对象的方法时,会执行增强逻辑 与 目标对象的原始方法,执行顺序取决于使用的通知类型

逻辑实现:

java 复制代码
// Spring内部大致这样工作:
public class ProxyBookDao implements BookDao {
    private BookDao target;  // 持有目标对象引用
    public void update() {
        // 执行前置增强(如:打印时间)
        System.out.println(System.currentTimeMillis());
        // 调用目标对象的原始方法
        target.update();
    }
}

AOP配置管理

AOP切入点表达式

切入点:需要进行功能增强的具体方法

切入点表达式:用来描述和匹配切入点的表达式语言

切入点描述的两种方式

对于切入点的描述,我们其实是有两种方式的,先来看下前面的例子:

方式一:基于接口描述

java 复制代码
execution(void com.itheima.dao.BookDao.update())

描述

  • 执行com.itheima.dao包下的BookDao接口中的无参数update方法
  • 匹配所有实现了BookDao接口的类的update()方法

方式二:基于实现类描述

java 复制代码
execution(void com.itheima.dao.impl.BookDaoImpl.update())

描述

  • 执行com.itheima.dao.impl包下的BookDaoImpl类中的无参数update方法
  • 只匹配BookDaoImpl这一个具体类的update()方法

两种方式区别与选择

  • 接口方式:适用于所有实现该接口的类,更通用,面向抽象,推荐在生产代码中使用
  • 实现类方式:只针对特定的实现类,更具体,面向具体,适合特定场景
  • 两种方式都能匹配到实际的方法调用

语法格式

切入点表达式的标准语法格式:

scss 复制代码
动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)

语法分解说明

以具体例子说明:

java 复制代码
execution(public User com.itheima.service.UserService.findById(int))
组成部分 说明 说明
动作关键字 描述切入点的行为 execution(执行到指定切入点)
访问修饰符 方法的访问权限 public(还可以是public、private等,省略默认public
返回值 方法的返回类型 User,写返回值类型
包名 类的完整包路径 com.itheima.service,多级包使用点连接
类/接口名 具体的类或接口 UserService
方法名 具体的方法名称 findById
参数 方法的参数类型 int,多个类型用逗号隔开
异常名 方法抛出的异常 (可省略)

通配符使用

为了简化切入点表达式的编写,一次匹配一类需要增强的方法,可以使用通配符:

  1. * 通配符
  • 相当于单个任意元素

  • 可以独立出现,也可以作为名的前缀部分或者后缀部分出现

  • 可用于包名、类名、方法名等位置

    java 复制代码
    // 匹配com.itheima包下任意子包中的UserService类中所有find开头的方法
    execution(public * com.itheima.*.UserService.find*(*))
  1. .. 通配符
  • 相当于多个连续的任意元素

  • 常用于包路径和参数列表

    java 复制代码
    // 匹配com包下任意多级包中的UserService类的findById方法,参数任意
    execution(public User com..UserService.findById(..))
  1. + 通配符
  • +写在接口名后面,接口名+ 表示该接口的所有后代类型

  • +号的作用是向下展开整个继承与实现树(包括实现类的子类、子接口的实现类)。它不匹配该类型本身,而是匹配其所有后代类型。

  • 使用频率较低

    java 复制代码
    // 匹配所有以Service结尾的接口的子类,*Service+表示所有以Service结尾的接口的子类
    execution(* *..*Service+.*(..))

常用表达式示例

java 复制代码
// 精确匹配示例
// 匹配BookDao接口的update方法
execution(void com.itheima.dao.BookDao.update())
// 匹配BookDaoImpl类的update方法  
execution(void com.itheima.dao.impl.BookDaoImpl.update())

// 通配符匹配示例
// 匹配任意返回值的update方法
execution(* com.itheima.dao.impl.BookDaoImpl.update())
// 匹配com包下任意三层包中的任意类的update方法
execution(void com.*.*.*.update())
// 匹配任意包下方法名为update的任意方法
execution(void *..update())
// 匹配项目中所有类的所有方法(不推荐)
execution(* *..*(..))
// 匹配所有以u开头的方法
execution(* *..u*(..))
// 匹配所有以e结尾的方法
execution(* *..*e(..))

// 实用业务场景示例
// 匹配所有业务层中以find开头的方法
execution(* com.itheima.*.*Service.find*(..))
// 匹配所有业务层中以save开头的方法  
execution(* com.itheima.*.*Service.save*(..))

书写技巧

  1. 所有代码务遵循统一的命名规范,按照标准规范开发
  2. 描述切入点通常描述接口,而不描述实现类,如果描述到实现类,就出现紧耦合了。
  3. 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)。
  4. 返回值类型对于增删改类使用精准类型 加速匹配,对于查询类使用*通配快速描述。
  5. 包名 书写尽量不使用..通配符 ,效率过低,常*做单个包描述匹配,或精准匹配。
  6. 接口名/类名书写 与模块相关的采用*匹配 ,例如UserService书写成*Service,绑定所有业务层接口。
  7. 方法名书写,当方法执行的是同一类操作,但操作对象不同时,动词采用精准匹配,名词采用*匹配,例如getById写成getBy*匹配所有"getByXxx"格式的方法,selectAll书写成select*匹配所有所有选择相关方法。
  8. 参数规则较为复杂,根据业务方法灵活调整。
  9. 通常不使用异常作为匹配规则。

AOP通知类型

类型介绍

AOP通知类型

  1. 前置通知(Before):在切入点方法执行之前执行
  2. 后置通知(After):在切入点方法执行之后执行,无论方法是否抛出异常都会执行
  3. 环绕通知(Around):在切入点方法执行前后执行,可以控制方法执行时机,并可以处理返回值和异常
  4. 返回后通知(AfterReturning):在切入点方法正常执行完毕后执行,如果方法抛出异常则不执行
  5. 抛出异常后通知(AfterThrowing):在切入点方法抛出异常后执行

逻辑理解 :\

环境准备

第一步:创建项目并添加依赖

在pom.xml文件中配置必要的依赖:

xml 复制代码
    
    
        org.springframework
        spring-context
        5.2.10.RELEASE
    
    
    
    
        org.aspectj
        aspectjweaver
        1.9.4
    

第二步:创建业务接口和实现类

BookDao接口:

java 复制代码
public interface BookDao {
    void update();    // 无返回值方法
    int select();     // 有返回值方法
}

BookDaoImpl实现类:

java 复制代码
@Repository
public class BookDaoImpl implements BookDao {
    
    public void update() {
        System.out.println(&#34;book dao update ...&#34;);
    }
    
    public int select() {
        System.out.println(&#34;book dao select is running ...&#34;);
        return 100;  // 返回固定值100
    }
}

第三步:配置Spring容器

SpringConfig配置类:

java 复制代码
@Configuration          // 标识这是配置类
@ComponentScan(&#34;com.itheima&#34;)  // 扫描指定包下的组件
@EnableAspectJAutoProxy // 启用AOP注解支持
public class SpringConfig {
}

第四步:创建通知类(暂未绑定)

MyAdvice通知类:

java 复制代码
@Component  // 让Spring管理这个类
@Aspect     // 标识这是切面类
public class MyAdvice {
    
    // 定义切入点:匹配BookDao的update方法
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void pt(){}  // 切入点方法,仅作为标记
    
    // 各种通知方法(暂未绑定到切入点)
    public void before() {
        System.out.println(&#34;before advice ...&#34;);
    }
    
    public void after() {
        System.out.println(&#34;after advice ...&#34;);
    }
    
    public void around() {
        System.out.println(&#34;around before advice ...&#34;);
        System.out.println(&#34;around after advice ...&#34;);
    }
    
    public void afterReturning() {
        System.out.println(&#34;afterReturning advice ...&#34;);
    }
    
    public void afterThrowing() {
        System.out.println(&#34;afterThrowing advice ...&#34;);
    }
}

第五步:创建测试运行类

App测试类:

java 复制代码
public class App {
    public static void main(String[] args) {
        // 创建Spring容器
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        
        // 从容器获取BookDao实例
        BookDao bookDao = ctx.getBean(BookDao.class);
        
        // 调用update方法
        bookDao.update();
    }
}

最终创建好的项目结构如下:\

通知类型的使用

前置通知 @Before

在通知方法上添加@Before注解

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void pt(){}
    
    // 使用@Before注解绑定到切入点
    @Before(&#34;pt()&#34;)
    public void before() {
        System.out.println(&#34;before advice ...&#34;);
    }
}

当调用bookDao.update()方法时,执行顺序为:

less 复制代码
1. 执行 before() 方法 → 输出 &#34;before advice ...&#34;
2. 执行 update() 方法 → 输出 &#34;book dao update ...&#34;

@Before(&#34;pt()&#34;)也可以写成 @Before(&#34;MyAdvice.pt()&#34;),但不推荐,因为更冗长

后置通知 @After

在通知方法上添加@After注解

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void pt(){}
    
    @Before(&#34;pt()&#34;)
    public void before() {
        System.out.println(&#34;before advice ...&#34;);
    }
    
    // 使用@After注解绑定到切入点
    @After(&#34;pt()&#34;)
    public void after() {
        System.out.println(&#34;after advice ...&#34;);
    }
}

当调用bookDao.update()方法时,执行顺序为:

less 复制代码
1. 执行 before() 方法 → 输出 &#34;before advice ...&#34;
2. 执行 update() 方法 → 输出 &#34;book dao update ...&#34;
3. 执行 after() 方法 → 输出 &#34;after advice ...&#34;

后置通知无论目标方法是否抛出异常都会执行

环绕通知 @Around
基本使用

注意 :环绕通知方法中必须显式调用原始方法,否则原始方法不会执行。

正确实现

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(void com.itheima.dao.BookDao.update())&#34;)
    private void pt(){}
    
    @Around(&#34;pt()&#34;)
    public void 环绕方法名(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println(&#34;around before advice ...&#34;);  //前置逻辑
        // 调用原始方法
        pjp.proceed();
        System.out.println(&#34;around after advice ...&#34;);  //后置逻辑
    }
}

运行结果

erlang 复制代码
around before advice ...
book dao update ...
around after advice ...

关键点

  1. ProceedingJoinPoint pjppjp.proceed() 是环绕通知的固定组成部分,任何环绕通知都必须这样写、调用。
  2. 必须声明抛出Throwable异常。

环绕通知方法必须声明throws Throwable,因为这是Spring框架的强制要求,确保能够处理原始方法可能抛出的所有类型的异常。

有返回值的方法的环绕通知处理

有返回值的方法的环绕通知必须

  1. 必须设置返回类型为Object
  2. 必须接收pjp.proceed()的返回值
  3. 必须返回原始方法的返回值(或修改后的值)

正确实现

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(int com.itheima.dao.BookDao.select())&#34;)
    private void pt2(){}
    
    @Around(&#34;pt2()&#34;)
    public Object aroundSelect(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println(&#34;around before advice ...&#34;);
        // 接收原始方法的返回值
        Object ret = pjp.proceed();
        System.out.println(&#34;around after advice ...&#34;);
        // 返回原始方法的返回值
        return ret;
    }
}

测试代码:

java 复制代码
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        int num = bookDao.select();
        System.out.println(num);  // 输出: 100
    }
}

运行结果:

erlang 复制代码
around before advice ...
book dao select is running ...
around after advice ...
100

无论是否有返回值,仍然建议所有环绕通知都必须遵循这个固定模式:

java 复制代码
@Around(&#34;...&#34;)
public Object 方法名(ProceedingJoinPoint pjp) throws Throwable {
    // 前置逻辑
    Object result = pjp.proceed();
    // 后置逻辑
    return result;
}
返回后通知 @AfterReturning

定义 :在目标方法正常执行完成后被触发执行的通知。

java 复制代码
@AfterReturning(&#34;pt2()&#34;)
public void afterReturning() {
    System.out.println(&#34;afterReturning advice ...&#34;);
}

执行条件

  • 目标方法必须正常执行完毕(没有抛出异常)
  • 如果目标方法执行过程中出现异常,此通知将不会执行

应用场景:记录方法成功执行的日志、处理方法的返回值等。

异常后通知 @AfterThrowing

定义 :在目标方法抛出异常时被触发执行的通知。

java 复制代码
@AfterThrowing(&#34;pt2()&#34;)  // 注意:原示例中注解名称有误,应为@AfterThrowing
public void afterThrowing() {
    System.out.println(&#34;afterThrowing advice ...&#34;);
}

执行条件

  • 目标方法抛出异常时
  • 如果目标方法正常执行,此通知将不会执行

应用场景:异常处理、错误日志记录、异常监控等。

环绕通知的实现原理

环绕通知(@Around)通过控制目标方法的调用时机,可以模拟其他所有通知类型的功能:

java 复制代码
@Around(&#34;pt()&#34;)
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    Object ret;
    try {
        // 前置通知位置
        System.out.println(&#34;前置通知&#34;);
        
        // 调用目标方法
        ret = pjp.proceed();
        
        // 返回后通知位置
        System.out.println(&#34;返回后通知&#34;);
        
    } catch (Throwable e) {
        // 异常后通知位置
        System.out.println(&#34;异常后通知&#34;);
        throw e;
    } finally {
        // 后置通知位置
        System.out.println(&#34;后置通知&#34;);
    }
    return ret;
}

环绕通知的灵活性

  • 前置通知 :在proceed()调用之前
  • 返回后通知 :在proceed()调用之后且无异常时
  • 异常后通知:在catch块中
  • 后置通知:在finally块中

这种设计使得环绕通知成为功能最强大的通知类型,可以完全控制目标方法的执行流程。

业务层接口执行效率的监控

需求理解

核心目标:监控每个业务层方法的执行时间,找出性能瓶颈以便优化。

实现思路

  • 方法执行前记录开始时间
  • 方法执行后记录结束时间
  • 计算时间差得到执行时长
  • 要在方法执行的前后添加业务,采用环绕通知`。
  • 由于单次执行时间可能太短,需要执行多次来计算平均时间

环境准备

pom.xml中添加必要的依赖

xml 复制代码
    
    
        org.springframework
        spring-context
        5.2.10.RELEASE
    
    
    
    
        org.springframework
        spring-jdbc
        5.2.10.RELEASE
    
    
    
    
        org.springframework
        spring-test
        5.2.10.RELEASE
    
    
    
    
        org.aspectj
        aspectjweaver
        1.9.4
    
    
    
    
        mysql
        mysql-connector-java
        5.1.47
    
    
        com.alibaba
        druid
        1.1.16
    
    
    
    
        org.mybatis
        mybatis
        3.5.6
    
    
        org.mybatis
        mybatis-spring
        1.3.0
    
    
    
    
        junit
        junit
        4.12
        test
    

数据模型与业务接口

账户实体类(Account类):

java 复制代码
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Double money;
    
    //setter...getter...toString方法省略
}

数据访问层接口(AccountDao类):

java 复制代码
public interface AccountDao {
    @Insert(&#34;insert into tbl_account(name,money)values(#{name},#{money})&#34;)
    void save(Account account);

    @Delete(&#34;delete from tbl_account where id = #{id}&#34;)
    void delete(Integer id);

    @Update(&#34;update tbl_account set name = #{name}, money = #{money} where id = #{id}&#34;)
    void update(Account account);

    @Select(&#34;select * from tbl_account&#34;)
    List findAll();

    @Select(&#34;select * from tbl_account where id = #{id}&#34;)
    Account findById(Integer id);
}

业务层接口与实现(AccountService接口、AccountServiceImpl类):

java 复制代码
// 业务接口
public interface AccountService {
    void save(Account account);
    void delete(Integer id);
    void update(Account account);
    List findAll();
    Account findById(Integer id);
}

// 业务实现
@Service
public class AccountServiceImpl implements AccountService {
    
    @Autowired
    private AccountDao accountDao;

    public void save(Account account) {
        accountDao.save(account);
    }

    public void update(Account account) {
        accountDao.update(account);
    }

    public void delete(Integer id) {
        accountDao.delete(id);
    }

    public Account findById(Integer id) {
        return accountDao.findById(id);
    }

    public List findAll() {
        return accountDao.findAll();
    }
}

配置文件

数据库连接配置(resources下的jdbc.properties):

properties 复制代码
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=root

Spring主配置类:

java 复制代码
@Configuration
@ComponentScan(&#34;com.itheima&#34;)
@PropertySource(&#34;classpath:jdbc.properties&#34;)
@Import({JdbcConfig.class, MybatisConfig.class})
@EnableAspectJAutoProxy  // 启用AOP自动代理
public class SpringConfig {
}

JDBC配置类:

java 复制代码
public class JdbcConfig {
    @Value(&#34;${jdbc.driver}&#34;)
    private String driver;
    @Value(&#34;${jdbc.url}&#34;)
    private String url;
    @Value(&#34;${jdbc.username}&#34;)
    private String userName;
    @Value(&#34;${jdbc.password}&#34;)
    private String password;

    @Bean
    public DataSource dataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setDriverClassName(driver);
        ds.setUrl(url);
        ds.setUsername(userName);
        ds.setPassword(password);
        return ds;
    }
}

MyBatis配置类:

java 复制代码
public class MybatisConfig {
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean ssfb = new SqlSessionFactoryBean();
        ssfb.setTypeAliasesPackage(&#34;com.itheima.domain&#34;);
        ssfb.setDataSource(dataSource);
        return ssfb;
    }

    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer msc = new MapperScannerConfigurer();
        msc.setBasePackage(&#34;com.itheima.dao&#34;);
        return msc;
    }
}

编写Spring整合Junit的测试类

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
    @Autowired
    private AccountService accountService;

    @Test
    public void testFindById(){
        Account ac = accountService.findById(2);
    }

    @Test
    public void testFindAll(){
        List all = accountService.findAll();
    }
}

最终创建好的项目结构如下:

功能开发

1. 开启Spring AOP注解支持

在Spring的主配置文件SpringConfig类中添加注解以启用AOP功能:

java 复制代码
@Configuration
@ComponentScan(&#34;com.itheima&#34;)
@EnableAspectJAutoProxy  // 关键:添加注解以启用AOP注解支持
public class SpringConfig {
}

作用:告诉Spring框架要处理AOP相关的注解,让切面类能够正常工作。

2. 创建AOP切面类

切面类,即@Aspect注解的整个Java类,包含:切入点定义(要拦截哪些方法)、通知定义(拦截后要做什么)、其他AOP相关配置

添加注解

  • 该类要被Spring管理,需要添加@Component
  • 要标识该类是一个AOP的切面类,需要添加@Aspect
  • 配置切入点表达式,需要添加一个方法,并添加@Pointcut
java 复制代码
@Component      // 让Spring管理这个类
@Aspect         // 声明这是一个切面类
public class ProjectAdvice {
    
    // 定义切入点:拦截所有业务层方法
    @Pointcut(&#34;execution(* com.itheima.service.*Service.*(..))&#34;)  //匹配com.itheima.service包下所有以Service结尾的类中的所有方法。
    private void servicePt(){}
    
    // 环绕通知方法(待完善)
    public void runSpeed(){
        
    }
}

3. 实现环绕通知

基础环绕通知

在runSpeed()方法上添加@Around

java 复制代码
@Around(&#34;servicePt()&#34;)  // 引用上面定义的切入点
public Object runSpeed(ProceedingJoinPoint pjp) throws Throwable {
    Object ret = pjp.proceed();  // 调用原始方法
    return ret;
}

说明:此时只是调用了原始方法,还没有任何增强功能。

添加性能监控逻辑( 记录万次执行的时间)

java 复制代码
@Around(&#34;servicePt()&#34;)
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();  // 记录开始时间
    
    // 执行10000次来计算平均时间
    for (int i = 0; i < 10000; i++) {
        pjp.proceed();
    }
    
    long end = System.currentTimeMillis();    // 记录结束时间
    System.out.println(&#34;业务层接口万次执行时间: &#34; + (end - start) + &#34;ms&#34;);
}

为什么执行10000次

  • 单次方法执行时间可能太短(几毫秒甚至更少)
  • 执行多次取平均值,结果更准确可靠
  • 尽可能避免因系统波动导致的测量误差

4. 优化输出信息

存在的问题

当前输出:业务层接口万次执行时间: 156ms

  • 不知道是哪个类的方法
  • 不知道是哪个具体方法
  • 多个方法测试时无法区分

解决方案:获取方法签名信息

java 复制代码
@Around(&#34;servicePt()&#34;)
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
    // 获取方法签名信息
    Signature signature = pjp.getSignature();
    String className = signature.getDeclaringTypeName();  // 获取声明类型的名称(通常是接口名)
    String methodName = signature.getName();              // 获取方法名
    
    long start = System.currentTimeMillis();
    
    for (int i = 0; i < 10000; i++) {
        pjp.proceed();
    }
    
    long end = System.currentTimeMillis();
    System.out.println(&#34;万次执行:&#34; + className + &#34;.&#34; + methodName + &#34; ---> &#34; + (end - start) + &#34;ms&#34;);
}

签名信息详解

  • ProceedingJoinPoint pjp:连接点对象,包含被拦截方法的所有信息
  • signature.getDeclaringTypeName() :获取声明该方法的类型 的完整名称
    • 在Spring默认的JDK动态代理中,获取的是接口名
    • 如果使用CGLIB代理,获取的是实现类名
  • signature.getName() :获取被拦截的方法名称

输出示例

lua 复制代码
万次执行:com.itheima.service.AccountService.findById ---> 234ms
万次执行:com.itheima.service.AccountService.findAll ---> 567ms

5. 完整代码实现

java 复制代码
@Component
@Aspect
public class ProjectAdvice {
    
    /**
     * 切入点配置:拦截service包下所有Service结尾的类的所有方法
     */
    @Pointcut(&#34;execution(* com.itheima.service.*Service.*(..))&#34;)
    private void servicePt(){}
    
    /**
     * 环绕通知:性能监控
     */
    @Around(&#34;servicePt()&#34;)
    public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        // 获取方法签名信息
        Signature signature = pjp.getSignature();
        String className = signature.getDeclaringTypeName();
        String methodName = signature.getName();
        
        // 记录开始时间
        long start = System.currentTimeMillis();
        
        // 执行10000次目标方法
        for (int i = 0; i < 10000; i++) {
            pjp.proceed();
        }
        
        // 记录结束时间并计算耗时
        long end = System.currentTimeMillis();
        long costTime = end - start;
        
        // 输出性能监控结果
        System.out.println(&#34;万次执行:&#34; + className + &#34;.&#34; + methodName + &#34; ---> &#34; + costTime + &#34;ms&#34;);
    }
}

6. 测试与结果分析

执行相应的单元测试,控制台会输出类似信息:

lua 复制代码
万次执行:com.itheima.service.AccountService.findById ---> 156ms
万次执行:com.itheima.service.AccountService.findAll ---> 423ms
万次执行:com.itheima.service.UserService.save ---> 89ms

结果说明

  • 这些时间是理论值,不是实际业务场景下的执行时间
  • 实际性能受数据库连接、网络、数据量等多种因素影响
  • 本案例主要目的是学习AOP的使用,不是精确的性能测试

AOP通知获取数据

目前我们写AOP仅仅是在原始方法前后追加一些操作,接下来我们要说说AOP中数据相关的内容。

在AOP中,我们可以从切入点方法中获取三种重要数据:

  1. 参数 - 方法调用时传入的值
  2. 返回值 - 方法执行后返回的结果
  3. 异常 - 方法执行过程中抛出的异常信息

各通知类型的数据获取能力

数据获取能力总览

通知类型 获取参数 获取返回值 获取异常
前置通知(@Before) ✅ 可以 ❌ 不可以 ❌ 不可以
后置通知(@After) ✅ 可以 ❌ 不可以 ❌ 不可以
返回后通知(@AfterReturning) ✅ 可以 ✅ 可以 ❌ 不可以
异常后通知(@AfterThrowing) ✅ 可以 ❌ 不可以 ✅ 可以
环绕通知(@Around) ✅ 可以 ✅ 可以 ✅ 可以

详细说明

1. 获取参数的能力

  • 所有通知类型都可以获取方法的参数
  • 使用方式:
    JoinPoint对象:适用于前置、后置、返回后、异常后通知
    ProceedingJoinPoint对象:适用于环绕通知(它是JoinPoint的子类)

2. 获取返回值的能力

  • 只有两种通知可以获取返回值:返回后通知(@AfterReturning)、环绕通知(@Around)

3. 获取异常信息的能力

  • 只有两种通知可以获取异常信息:异常后通知(@AfterThrowing)、环绕通知(@Around)

环境准备

Maven依赖配置

pom.xml中添加必要的依赖:

xml 复制代码
    
    
        org.springframework
        spring-context
        5.2.10.RELEASE
    
    
    
    
        org.aspectj
        aspectjweaver
        1.9.4
    

依赖说明:

  • spring-context:Spring核心容器,提供IoC和AOP基础功能
  • aspectjweaver:AspectJ切面编织器,让Spring能够处理AOP注解

添加数据访问层组件

接口

java 复制代码
public interface BookDao {
    public String findName(int id);
}

实现类

java 复制代码
@Repository  // 标识为数据访问层组件,由Spring管理
public class BookDaoImpl implements BookDao {
    
    public String findName(int id) {
        System.out.println(&#34;id:&#34; + id);  // 打印传入的参数
        return &#34;itcast&#34;;                 // 返回固定字符串
    }
}

组件说明:

  • @Repository:Spring注解,标记该类为数据访问层组件
  • 简单的findName方法:接收一个id参数,返回字符串"itcast"

创建Spring配置类

java 复制代码
@Configuration      // 声明这是Spring配置类
@ComponentScan(&#34;com.itheima&#34;)  // 扫描指定包下的组件
@EnableAspectJAutoProxy        // 启用AOP自动代理功能
public class SpringConfig {
}

配置说明:

  • @Configuration:替代XML配置文件,用Java代码配置Spring
  • @ComponentScan:自动扫描并注册指定包中的Spring组件
  • @EnableAspectJAutoProxy:关键配置,启用AOP注解支持

编写AOP切面类

java 复制代码
@Component  // 让Spring管理这个切面类
@Aspect     // 声明这是一个切面类
public class MyAdvice {
    
    // 定义切入点:拦截BookDao的findName方法
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}
    
    // 前置通知 - 在目标方法执行前执行   
    @Before(&#34;pt()&#34;)
    public void before() {
        System.out.println(&#34;before advice ...&#34;);
    }
    
    // 后置通知 - 在目标方法执行后执行(无论成功或异常)
    @After(&#34;pt()&#34;)
    public void after() {
        System.out.println(&#34;after advice ...&#34;);
    }
    
    // 环绕通知 - 最强大的通知类型,可以控制目标方法执行
    @Around(&#34;pt()&#34;)
    public Object around(ProceedingJoinPoint pjp) throws Throwable { 
        Object ret = pjp.proceed();  // 执行目标方法
        return ret;
    }
    
    // 返回后通知 - 在目标方法成功返回后执行
    @AfterReturning(&#34;pt()&#34;)
    public void afterReturning() {
        System.out.println(&#34;afterReturning advice ...&#34;);
    }
    
    // 异常后通知 - 在目标方法抛出异常后执行
    @AfterThrowing(&#34;pt()&#34;)
    public void afterThrowing() {
        System.out.println(&#34;afterThrowing advice ...&#34;);
    }
}

应用程序入口App运行类

java 复制代码
public class App {
    public static void main(String[] args) {
        // 1. 创建Spring容器,加载配置类
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        // 2. 从容器中获取BookDao实例(已经是代理对象)
        BookDao bookDao = ctx.getBean(BookDao.class);
        // 3. 调用方法,观察AOP通知的执行
        String name = bookDao.findName(100);
        // 4. 打印方法返回值
        System.out.println(name);
    }
}

获取参数

在AOP中,获取方法参数是指在通知方法中访问要传入被拦截方法的参数值。这让我们能够在方法执行前后检查和操作参数。

非环绕通知获取参数

使用JoinPoint对象来获取参数,适用于前置、后置、返回后、异常后通知:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}

    @Before(&#34;pt()&#34;)
    public void before(JoinPoint jp) {
        // 获取方法的所有参数
        Object[] args = jp.getArgs();
        System.out.println(&#34;方法参数: &#34; + Arrays.toString(args));
        System.out.println(&#34;before advice ...&#34;);
    }
}

多参数示例

因为参数的个数是不固定的,所以使用数组更通配些。如果将参数改成两个会是什么效果呢:

修改BookDao接口和实现类:

java 复制代码
public interface BookDao {
    public String findName(int id, String password);  // 添加第二个参数
}

@Repository
public class BookDaoImpl implements BookDao {
    public String findName(int id, String password) {
        System.out.println(&#34;id:&#34; + id + &#34;, password:&#34; + password);
        return &#34;itcast&#34;;
    }
}

修改App测试类:

java 复制代码
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        String name = bookDao.findName(100, &#34;itheima&#34;);  // 传入两个参数
        System.out.println(name);
    }
}

运行结果:

makefile 复制代码
方法参数: [100, itheima]
before advice ...
id:100, password:itheima
itcast

关键说明

  • 必须将 JoinPoint jp 作为通知方法的参数
  • jp.getArgs() 方法:返回Object[]数组,包含所有参数值
  • 为什么用数组:因为方法参数个数不固定,数组可以通用地处理任意数量的参数
环绕通知获取参数

基本获取方式

环绕通知使用ProceedingJoinPoint类型参数(它是JoinPoint的子类):

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}

    @Around(&#34;pt()&#34;)
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        // 获取参数
        Object[] args = pjp.getArgs();
        System.out.println(&#34;环绕通知获取参数: &#34; + Arrays.toString(args));
        
        // 执行原始方法
        Object ret = pjp.proceed();
        
        return ret;
    }
}
修改参数

环绕通知不仅可以获取参数,还可以修改参数值

java 复制代码
@Around(&#34;pt()&#34;)
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    // 1. 获取原始参数
    Object[] args = pjp.getArgs();
    System.out.println(&#34;原始参数: &#34; + Arrays.toString(args));
    
    // 2. 修改参数(例如将第一个参数改为666)
    if (args.length > 0) {
        args[0] = 666;  // 修改第一个参数的值
    }
    
    // 3. 使用修改后的参数执行方法
    Object ret = pjp.proceed(args);  // 注意:这里传入修改后的参数数组
    
    return ret;
}

执行结果

bash 复制代码
原始参数: [100, itheima]
id:666, password:itheima  // 注意:id变成了666
itcast
proceed()方法的两种用法

无参数调用:当原始方法有参数,会在调用的过程中自动传入参数,适用于不需要修改参数的情况

java 复制代码
Object ret = pjp.proceed();  // 自动使用原始参数

带参数调用:当需要修改原始方法的参数时,就只能采用带有参数的方法,适用于需要修改或过滤参数的情况

java 复制代码
Object ret = pjp.proceed(modifiedArgs);  // 使用修改后的参数

修改参数注意事项

  • 修改参数时要确保参数类型和数量与原始方法匹配
  • 参数数组索引从0开始,对应方法的第一个参数
  • 在处理参数前最好进行空值检查和类型检查
  • 修改参数要谨慎,避免破坏原始业务逻辑

获取返回值

在AOP中,获取返回值是指在通知方法中访问 被拦截方法的执行结果。只有两种通知类型具备这种能力,因为:

  • 环绕通知(@Around):完全控制方法执行,自然可以获取返回值
  • 返回后通知(@AfterReturning):专门为获取返回值设计
  • 其他通知类型在方法执行时点无法或不应该获取返回值
环绕通知获取返回值

环绕通知通过ProceedingJoinPoint.proceed()的返回值直接获取:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}

    @Around(&#34;pt()&#34;)
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = pjp.proceed(args);  // 执行目标方法并获取返回值
        return ret;  // 返回获取到的值
    }
}
环绕通知修改返回值

环绕通知的强大之处在于可以修改返回值:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}

    @Around(&#34;pt()&#34;)
    public Object around(ProceedingJoinPoint pjp) throws Throwable{
        Object[] args = pjp.getArgs();
        System.out.println(&#34;原始参数: &#34; + Arrays.toString(args));
        args[0] = 666;
        // 1. 执行原始方法并获取返回值
        Object ret = pjp.proceed(args);  // 假设原始方法返回&#34;itcast&#34;
        // 2. 在环绕通知中修改返回值
        System.out.println(&#34;原始返回值: &#34; + ret);
        // 3. 修改返回值(例如添加前缀)
        if (ret != null) {
            //ret.toString()将ret对象转换为字符串,.toUpperCase()将字符串转换为大写字母
            ret = &#34;修改后的返回值: &#34; + ret.toString().toUpperCase();
        }
        
        System.out.println(&#34;修改后的返回值: &#34; + ret);
        return ret;  // 返回修改后的值
    }
}
返回后通知获取返回值

返回后通知使用@AfterReturning注解的returning属性来绑定返回值:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}

    // returning属性指定参数名,该参数将接收返回值
    @AfterReturning(value = &#34;pt()&#34;, returning = &#34;ret&#34;)
    public void afterReturning(Object ret) {
        System.out.println(&#34;afterReturning获取返回值: &#34; + ret);
    }
}

AfterReturning的三个重要细节

  • 参数名必须匹配
    @AfterReturning中的returning值必须与通知方法的参数名一致:

  • 参数类型建议使用Object

    java 复制代码
    // 建议:使用Object类型,可以接收任何类型的返回值
    @AfterReturning(value = &#34;pt()&#34;, returning = &#34;ret&#34;)
    public void afterReturning(Object ret) {
        System.out.println(&#34;返回值: &#34; + ret);
    }
    
    // 也可以指定具体类型,但只能接收该类型或子类
    public void afterReturning(String ret) {  // 只能处理返回String类型的方法}
  • 如果通知方法有多个参数,JoinPoint参数必须在第一位:

两种方式的对比

特性 环绕通知(@Around) 返回后通知(@AfterReturning)
获取返回值 Object ret = pjp.proceed() 使用returning属性绑定
修改返回值 ✅ 可以修改返回值 ❌ 只能读取,不能修改
控制方法执行 ✅ 可以控制是否及何时执行原始方法 ❌ 不能控制原始方法执行
参数要求 必须有ProceedingJoinPoint参数 可选JoinPoint参数,必须有返回值参数
异常处理 可以在try-catch中处理异常 方法正常返回才会执行

获取异常

只有两种通知类型可以获取方法的异常信息:

  • 环绕通知(@Around):通过try-catch捕获异常
  • 异常后通知(@AfterThrowing) :通过throwing属性绑定异常
环绕通知获取异常

基本实现方式

环绕通知使用try-catch块来捕获和处理异常:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}

    @Around(&#34;pt()&#34;)
    public Object around(ProceedingJoinPoint pjp) {
        Object[] args = pjp.getArgs();
        System.out.println(Arrays.toString(args));
        args[0] = 666;
        Object ret = null;
        
        try {
            // 尝试执行原始方法
            ret = pjp.proceed(args);
        } catch (Throwable throwable) {
            // 捕获到异常,可以获取异常信息
            throwable.printStackTrace();  // 打印异常堆栈信息
        }
        
        return ret;
    }
}

关键要点

  1. try-catch结构 :将pjp.proceed()调用放在try块中
  2. 异常捕获 :catch块可以捕获Throwable(所有异常的父类)
  3. 异常处理:捕获异常后可以进行日志记录、异常转换等操作
异常后通知获取异常

异常后通知使用@AfterThrowing注解的throwing属性来绑定异常:

java 复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut(&#34;execution(* com.itheima.dao.BookDao.findName(..))&#34;)
    private void pt(){}

    @AfterThrowing(value = &#34;pt()&#34;, throwing = &#34;t&#34;)
    public void afterThrowing(Throwable t) {
        System.out.println(&#34;afterThrowing advice ...&#34; + t);
    }
}

AfterThrowing的三个重要细节

  • 参数名必须匹配
    @AfterThrowing注解中的throwing值必须与通知方法的参数名一致:\
  • 参数类型建议使用Throwable,可以捕获所有类型的异常,也可以指定具体异常类型(如NullPointerException
  • 如果有JoinPoint参数,必须放在异常参数之前:

两种方式的对比

特性 环绕通知(@Around) 异常后通知(@AfterThrowing)
获取异常方式 通过try-catch捕获 通过throwing属性绑定
能否阻止异常传播 ✅ 可以(catch后不重新抛出) ❌ 不能(执行后异常仍会传播)
能否继续执行 ✅ 可以在catch后继续执行其他逻辑 ❌ 只是执行通知,异常仍会抛出
执行时机 在目标方法执行时捕获异常 在目标方法抛出异常后执行
能否获取参数 ✅ 可以 ✅ 可以(通过JoinPoint)

百度网盘密码数据兼容处理

需求分析

问题背景 :接收方复制提取网盘密码时,可能无意间复制到多余空格(如:"1234 "或" 1234"),多输入一个空格可能会导致项目的功能无法正常使用,此时我们就想能不能将输入的参数先帮用户去掉空格再操作呢?

需求 :对百度网盘分享链接输入密码时,自动去除密码字符串前后的空格,避免因格式问题导致的访问失败。 \

技术实现

  1. 选择AOP(面向切面编程) :因为多个业务方法可能需要相同的处理,使用AOP可以避免代码重复,实现统一处理。
  2. 使用环绕通知:因为需要在方法执行前修改参数,然后使用修改后的参数调用原始方法,环绕通知可以满足这一需求。

实现步骤

  1. 定义切面,拦截目标方法。
  2. 在环绕通知中,在方法执行前,获取原始方法的参数。
  3. 遍历参数,对每个字符串类型的参数执行trim()操作( string.trim():移除字符串string开头和结尾的所有空白字符)。
  4. 在环绕通知中,使用处理后的参数调用原始方法并返回其结果。

环境准备

  1. 创建一个Maven项目

  2. pom.xml中添加必要的依赖:

    xml 复制代码
        
        
            org.springframework
            spring-context
            5.2.10.RELEASE
        
    
        
        
            org.aspectj
            aspectjweaver
            1.9.4
        
  3. 添加ResourcesService,ResourcesServiceImpl,ResourcesDao和ResourcesDaoImpl
    数据访问层(DAO)
    ResourcesDao:

    java 复制代码
    public interface ResourcesDao {
        /**
         * 验证资源访问权限
         * @param url 资源链接
         * @param password 提取密码
         * @return 验证结果
         */
        boolean readResources(String url, String password);
    }

    ResourcesDaoImpl:

    java 复制代码
    @Repository
    public class ResourcesDaoImpl implements ResourcesDao {
        public boolean readResources(String url, String password) {
            // 模拟数据库存储的正确密码
            String correctPassword = &#34;root&#34;;
            // 直接比较密码
            return password.equals(correctPassword);
        }
    }

    业务逻辑层(Service)
    ResourcesService:

    java 复制代码
    public interface ResourcesService {
        /**
         * 打开资源链接
         * @param url 资源链接
         * @param password 用户输入的密码
         * @return 是否成功访问
         */
        boolean openURL(String url, String password);
    }

    ResourcesServiceImpl:

    java 复制代码
    @Service
    public class ResourcesServiceImpl implements ResourcesService {
        @Autowired
        private ResourcesDao resourcesDao;
    
        public boolean openURL(String url, String password) {
            // 调用DAO层验证密码
            return resourcesDao.readResources(url, password);
        }
    }
  4. 创建Spring的配置类

    java 复制代码
    @Configuration
    @ComponentScan(&#34;com.example&#34;)  // 扫描包路径下的组件
    public class SpringConfig {
        // 基础配置,暂时不需要额外配置
    }
  5. 编写App运行类

    java 复制代码
    public class App {
        public static void main(String[] args) {
            // 1. 加载Spring配置类,创建Spring容器
            ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    
            // 2. 从容器中获取ResourcesService类型的Bean
            ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
    
            // 3. 调用业务方法:尝试打开指定URL,使用密码&#34;root&#34;
            boolean flag = resourcesService.openURL(&#34;http://pan.baidu.com/haha&#34;, &#34;root&#34;);
    
            // 4. 打印验证结果
            System.out.println(flag);
        }
    }

最终创建好的项目结构如下:

现在项目的效果是,当输入密码为"root"控制台打印为true,如果密码改为"root "控制台打印的是false

我们需要在不修改现有业务代码的前提下,通过AOP解决这个问题。

具体实现

  1. 在Spring配置类中启用AOP注解功能:

    java 复制代码
    @Configuration
    @ComponentScan(&#34;com.example&#34;)  // 扫描项目包
    @EnableAspectJAutoProxy        // 开启AOP注解支持
    public class SpringConfig {
        // 配置类内容
    }
  2. 创建AOP切面类

    java 复制代码
    @Component
    @Aspect
    public class DataAdvice {
    
        // 定义切入点:com.example.service包下,所有Service结尾的类的所有恰有两个参数方法
        // 方法返回类型为boolean
        @Pointcut(&#34;execution(boolean com.example.service.*Service.*(*,*))&#34;)
        private void servicePt() {
            // 方法体为空,仅用于定义切入点
        }
    
    }
  3. 编写环绕通知框架

    java 复制代码
    @Component
    @Aspect
    public class DataAdvice {
        @Pointcut(&#34;execution(boolean com.example.service.*Service.*(*,*))&#34;)
        private void servicePt() {}
    
        // 环绕通知:在切入点方法执行前后都可以执行逻辑
        @Around(&#34;servicePt()&#34;)  // @Around(&#34;DataAdvice.servicePt()&#34;)这两种写法都对
        public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
            // 调用原始方法,暂不做任何处理
            Object result = pjp.proceed();
            return result;
        }
    
    }
  4. 完成核心业务,实现参数去空格功能

    java 复制代码
    @Component
    @Aspect
    public class DataAdvice {
    
        @Pointcut(&#34;execution(boolean com.example.service.*Service.*(*,*))&#34;)
        private void servicePt() {}
    
        @Around(&#34;servicePt()&#34;)
        public Object trimStr(ProceedingJoinPoint pjp) throws Throwable {
            // 1. 获取原始方法的参数数组
            Object[] args = pjp.getArgs();
    
            // 2. 遍历所有参数
            for (int i = 0; i < args.length; i++) {
                // 3. 判断参数是否为字符串类型
                if(args[i].getClass().equals(String.class)){
                    args[i] = args[i].toString().trim();
                }
            }
    
            // 5. 使用处理后的参数调用原始方法
            Object result = pjp.proceed(args);
            // 6. 返回原始方法的执行结果
            return result;
        }
    
    }
  • args[i].toString():将Object类型的args[i]转换为字符串类型
  1. 验证效果

    运行App主程序,测试不同密码,不管密码root前后是否加空格,最终控制台打印的都是true

    验证方式2:添加日志验证

    修改DAO层实现,打印密码长度以验证AOP生效:

    java 复制代码
    @Repository
    public class ResourcesDaoImpl implements ResourcesDao {
        public boolean readResources(String url, String password) {
            // 打印接收到的密码和长度
            System.out.println(&#34;接收密码: '&#34; + password + &#34;'&#34;);
            System.out.println(&#34;密码长度: &#34; + password.length());
    
            // 模拟校验
            return password.equals(&#34;root&#34;);
        }
    }

    运行结果(不管密码root前后是否加空格):

    arduino 复制代码
    接收密码: 'root'
    密码长度: 4
    true

AOP事务管理

Spring事务简介

核心概念与作用

事务的核心作用 :确保一系列相关的数据库操作(例如多条SQL语句)作为一个不可分割的单元来执行。这些操作要么全部成功 (提交),要么在遇到问题时全部撤销(回滚),以此保障数据的一致性和完整性。

Spring事务的扩展作用 :将上述"同成功同失败"的保障能力,从传统的数据层 (如DAO层),扩展并提升到业务层(Service层)。这是Spring框架在事务管理上提供的关键价值。

事务需要提升到业务层的原因

数据层的事务边界(通常是一个独立的方法调用)无法覆盖"跨多个数据库操作的完整业务单元"。一个典型的业务场景可以清晰地说明这个问题。

转账案例 - 需求分析

以银行转账业务为例:

  1. 业务逻辑:从A账户转100元到B账户。
  2. 对应的数据层操作
    • 操作1 :从A账户的余额中 减100
    • 操作2 :向B账户的余额中 加100

假设事务仅在数据层管理:

  • 减100 操作会对A账户开启并提交(或回滚)一个独立的事务 Tx1
  • 加100 操作会对B账户开启并提交(或回滚)另一个独立的事务 Tx2
  • 风险 :如果 Tx1 成功提交后,系统在执行 Tx2 前发生故障(如网络中断、服务器宕机),结果将是A账户的钱已扣除,但B账户的钱未到账,导致数据严重不一致。

结论 :必须将 减100加100 这两个数据层操作,捆绑在同一个业务层事务中。Spring事务管理正是为了解决这一问题,它能确保一个业务方法内部所有的数据库操作,处于同一个事务管辖之下。

转账案例 - 环境搭建

1. 数据库准备

首先创建数据库和表结构:

sql 复制代码
-- 创建数据库
create database spring_db character set utf8;

-- 使用数据库
use spring_db;

-- 创建账户表
create table tbl_account(
    id int primary key auto_increment,
    name varchar(35),
    money double
);

-- 插入测试数据
insert into tbl_account values(1,'Tom',1000);
insert into tbl_account values(2,'Jerry',1000);

说明:创建了两个账户,初始余额各1000元,用于模拟转账操作。

2. 项目依赖配置(pom.xml)

项目使用Maven构建,需要添加以下依赖:

xml 复制代码
    org.springframework
    spring-context
    5.2.10.RELEASE




    com.alibaba
    druid
    1.1.16




    org.mybatis
    mybatis
    3.5.6




    mysql
    mysql-connector-java
    5.1.47




    org.springframework
    spring-jdbc
    5.2.10.RELEASE




    org.mybatis
    mybatis-spring
    1.3.0




    junit
    junit
    4.12
    test




    org.springframework
    spring-test
    5.2.10.RELEASE

3. 实体类(Account.java)

对应数据库表结构:

java 复制代码
public class Account implements Serializable {
    private Integer id;
    private String name;
    private Double money;
    
    // 构造方法
    public Account() {}
    
    public Account(Integer id, String name, Double money) {
        this.id = id;
        this.name = name;
        this.money = money;
    }
    
    // Getter和Setter方法
    public Integer getId() {
        return id;
    }
    
    public void setId(Integer id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public Double getMoney() {
        return money;
    }
    
    public void setMoney(Double money) {
        this.money = money;
    }
    
    // toString方法
    @Override
    public String toString() {
        return &#34;Account{&#34; +
                &#34;id=&#34; + id +
                &#34;, name='&#34; + name + '\'' +
                &#34;, money=&#34; + money +
                '}';
    }
}

4. 数据访问层(DAO)

定义账户操作接口:

java 复制代码
public interface AccountDao {
    // 使用MyBatis的注解方式
    /**
     * 转入操作(增加金额)
     * @param name 账户名称
     * @param money 转账金额
     */
    @Update(&#34;UPDATE tbl_account SET money = money + #{money} WHERE name = #{name}&#34;)
    void inMoney(@Param(&#34;name&#34;) String name, @Param(&#34;money&#34;) Double money);

    /**
     * 转出操作(减少金额)
     * @param name 账户名称
     * @param money 转账金额
     */
    @Update(&#34;UPDATE tbl_account SET money = money - #{money} WHERE name = #{name}&#34;)
    void outMoney(@Param(&#34;name&#34;) String name, @Param(&#34;money&#34;) Double money);
}

5. 业务逻辑层(Service)

定义转账业务接口和实现:

java 复制代码
public interface AccountService {
    /**
     * 转账操作
     * @param out 转出账户名
     * @param in 转入账户名
     * @param money 转账金额
     */
    void transfer(String out, String in, Double money);
}
java 复制代码
@Service
public class AccountServiceImpl implements AccountService {
    
    @Autowired
    private AccountDao accountDao;
    
    @Override
    public void transfer(String out, String in, Double money) {
        // 第一步:从转出账户扣除金额
        accountDao.outMoney(out, money);
        
        // 第二步:向转入账户增加金额
        accountDao.inMoney(in, money);
    }
}

注意:当前的实现存在事务问题!如果在两个操作之间发生异常,会导致数据不一致。

6. 数据库连接配置(jdbc.properties)

properties 复制代码
# 数据库驱动
jdbc.driver=com.mysql.jdbc.Driver

# 数据库连接URL
# useSSL=false表示禁用SSL连接
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false

# 数据库用户名
jdbc.username=root

# 数据库密码
jdbc.password=root

7. JDBC配置类(JdbcConfig.java)

java 复制代码
@PropertySource(&#34;classpath:jdbc.properties&#34;)
public class JdbcConfig {
    
    @Value(&#34;${jdbc.driver}&#34;)
    private String driver;
    
    @Value(&#34;${jdbc.url}&#34;)
    private String url;
    
    @Value(&#34;${jdbc.username}&#34;)
    private String userName;
    
    @Value(&#34;${jdbc.password}&#34;)
    private String password;
    
    /**
     * 创建数据源Bean
     * @return 数据源对象
     */
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        return dataSource;
    }
}

8. MyBatis配置类(MybatisConfig.java)

java 复制代码
public class MybatisConfig {
    
    /**
     * 配置SqlSessionFactory
     * @param dataSource 数据源
     * @return SqlSessionFactoryBean
     */
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
        // 设置实体类所在的包
        factoryBean.setTypeAliasesPackage(&#34;com.itheima.domain&#34;);
        // 设置数据源
        factoryBean.setDataSource(dataSource);
        return factoryBean;
    }
    
    /**
     * 配置Mapper接口扫描
     * @return MapperScannerConfigurer
     */
    @Bean
    public MapperScannerConfigurer mapperScannerConfigurer() {
        MapperScannerConfigurer configurer = new MapperScannerConfigurer();
        // 设置DAO接口所在的包
        configurer.setBasePackage(&#34;com.itheima.dao&#34;);
        return configurer;
    }
}

9. Spring主配置类(SpringConfig.java)

java 复制代码
@Configuration  // 标记为配置类
@ComponentScan(&#34;com.itheima&#34;)  // 扫描组件
@PropertySource(&#34;classpath:jdbc.properties&#34;)  // 加载属性文件
@Import({JdbcConfig.class, MybatisConfig.class})  // 导入其他配置类
public class SpringConfig {
    // 主配置类,整合所有配置
}

10. 测试类(AccountServiceTest.java)

java 复制代码
@RunWith(SpringJUnit4ClassRunner.class)  // 使用Spring测试运行器
@ContextConfiguration(classes = SpringConfig.class)  // 指定配置类
public class AccountServiceTest {
    
    @Autowired
    private AccountService accountService;
    
    @Test
    public void testTransfer() {
        // 测试转账:从Tom账户转100元到Jerry账户
        accountService.transfer(&#34;Tom&#34;, &#34;Jerry&#34;, 100D);
        
        // 预期结果:
        // Tom账户:1000 - 100 = 900元
        // Jerry账户:1000 + 100 = 1100元
    }
}

最终创建好的项目结构如下:

存在的问题

当前的实现存在一个重要问题:缺乏事务管理 以确保两个操作(扣款和收款)要么全部成功,要么全部失败,否则在转账过程中出现异常,可能导致数据不一致。

目前,我们搭建了基本的Spring+MyBatis整合环境,后面将通过Spring事务管理来解决这个问题。

转账案例 - 事务管理

1. 在需要被事务管理的方法上添加事务注解

1.1 业务接口(无改动)

java 复制代码
public interface AccountService {
    /**
     * 转账操作
     * @param out 转出账户名
     * @param in 转入账户名
     * @param money 转账金额
     */
    // 此处不需要@Transactional注解,建议在实现类上添加
    void transfer(String out, String in, Double money);
}

1.2 业务实现类(添加事务管理)

java 复制代码
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    
     // 方法上添加@Transactional注解,表示该方法需要事务管理
    @Transactional
    @Override     // 注解实现的接口方法
    public void transfer(String out, String in, Double money) {
        // 第一步:从转出账户扣除金额
        accountDao.outMoney(out, money);
        
        // 模拟异常:这里会抛出算数异常ArithmeticException
        int i = 1 / 0;
        
        // 第二步:向转入账户增加金额(如果上面出现异常,这行代码不会执行)
        accountDao.inMoney(in, money);
    }
}
@Transactional注解详解

作用位置

  1. 接口类上:该接口的所有实现类的所有方法都会有事务

    java 复制代码
    @Transactional
    public interface AccountService {
        void transfer(String out, String in, Double money);
    }
  2. 接口方法上:该接口的所有实现类的该方法都会有事务

    java 复制代码
    public interface AccountService {
        @Transactional
        void transfer(String out, String in, Double money);
    }
  3. 实现类上:该类中的所有方法都会有事务

    java 复制代码
    @Service
    @Transactional
    public class AccountServiceImpl implements AccountService {
        // 类中所有方法都会自动获得事务管理
    }
  4. 实现类方法上:只有该方法有事务

    java 复制代码
    @Service
    public class AccountServiceImpl implements AccountService {
        @Transactional
        public void transfer(String out, String in, Double money) {
            // 只有这个方法有事务
        }
    }

最佳实践建议

  • @Transactional注解添加在实现类或实现类的方法上
  • 原因:Spring的事务管理是通过AOP实现的,注解在接口上可能在某些情况下不生效
2. 配置事务管理器

JdbcConfig配置类中添加事务管理器配置:

java 复制代码
public class JdbcConfig {
    @Value(&#34;${jdbc.driver}&#34;)
    private String driver;
    @Value(&#34;${jdbc.url}&#34;)
    private String url;
    @Value(&#34;${jdbc.username}&#34;)
    private String userName;
    @Value(&#34;${jdbc.password}&#34;)
    private String password;

    /**
     * 配置数据源
     * @return 数据源对象
     */
    @Bean
    public DataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(userName);
        dataSource.setPassword(password);
        return dataSource;
    }

    /**
     * 配置事务管理器
     * MyBatis使用的是JDBC事务,JDBC事务管理器需用DataSourceTransactionManager创建
     * @param dataSource 数据源对象,Spring会自动注入
     * @return 事务管理器
     */
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        // 创建JDBC事务管理器
        DataSourceTransactionManager transactionManager = new DataSourceTransactionManager();
        
        // 设置数据源,事务管理器需要知道管理哪个数据库的事务
        transactionManager.setDataSource(dataSource);
        
        return transactionManager;
    }
}

为什么需要事务管理器?

Spring本身不直接管理事务,而是通过事务管理器来协调各种持久层技术(JDBC、Hibernate等)的事务。不同的技术需要不同的事务管理器实现。

不同的持久层技术使用不同的事务管理器

  • JDBC/MyBatis:DataSourceTransactionManager
  • JPA:JpaTransactionManager
  • JTA(分布式事务):JtaTransactionManager

public PlatformTransactionManager transactionManager(DataSource dataSource)方法详解:

  • 方法的作用:这个方法用于定义一个Spring Bean,这个Bean是PlatformTransactionManager类型。Spring容器会调用这个方法,并将返回的对象作为一个Bean注册到容器中。事务管理器是Spring事务管理的核心,它负责事务的开启、提交、回滚等操作。
  • 参数说明:方法有一个参数:DataSource dataSource。Spring容器会自动根据类型注入一个DataSource Bean。这个DataSource就是我们在JdbcConfig中定义的dataSource()方法返回的数据源。事务管理器需要操作数据库,因此它需要知道数据源。
  • 为什么需要这样做:
    在Spring中,如果我们想要使用声明式事务(即通过@Transactional注解),我们必须配置一个事务管理器。因为Spring事务管理是基于AOP的,它会在运行时为带有@Transactional注解的方法创建代理,代理中会调用事务管理器来管理事务。
    具体来说,当我们调用一个被@Transactional注解的方法时,Spring会通过事务管理器开启一个事务,然后执行方法,如果方法执行成功,则提交事务,如果方法抛出异常,则回滚事务。
  • 注意:在Spring Boot中,如果我们引入了相应的依赖(如spring-boot-starter-jdbc),Spring Boot会自动配置一个事务管理器。但在我们现在的Spring项目中,我们需要手动配置。
3. 开启事务注解驱动

在Spring主配置类上添加@EnableTransactionManagement注解:

java 复制代码
@Configuration                 // 标记为配置类
@ComponentScan(&#34;com.itheima&#34;)  // 扫描组件,包括@Service
@PropertySource(&#34;classpath:jdbc.properties&#34;)  // 加载数据库配置
@Import({JdbcConfig.class, MybatisConfig.class})  // 导入其他配置
@EnableTransactionManagement  // 关键:开启注解式事务管理
public class SpringConfig {
    // 主配置类,整合所有配置
}

@EnableTransactionManagement的作用

  1. 启用Spring的注解驱动事务管理功能
  2. 告诉Spring容器扫描@Transactional注解,为标记@Transactional的方法创建AOP代理对象
  3. 代理对象会在方法执行时自动管理事务的开启、提交和回滚
4. 运行测试类

测试执行流程:

  1. 开始执行transfer()方法
  2. Tom账户扣款100元成功
  3. 遇到int i = 1/0异常,程序中断
  4. Spring检测到异常,自动回滚事务
  5. Tom账户扣款的SQL操作被撤销
  6. Jerry账户加款操作未执行

最终结果:

  • 两个账户的金额都保持原样(各1000元)
  • 数据一致性得到保障

事务管理工作原理图解

sql 复制代码
   [调用者]
       |
       | 调用transfer()方法
       ↓
+----------------------+
| Spring AOP 代理对象  | ← ① Spring为Service创建代理
+----------------------+
       |
       | ② 代理对象开始事务
       ↓
+----------------------+
|  开启数据库事务      |
|  设置自动提交为false  |
+----------------------+
       |
       | ③ 执行业务方法
       ↓
+----------------------+
|  accountDao.outMoney | ← 执行SQL1:Tom扣款
+----------------------+
       |
       | ④ 继续执行业务
       ↓
+----------------------+
|  accountDao.inMoney  | ← 执行SQL2:Jerry加款
+----------------------+
       |
       | ⑤ 业务执行完成
       ↓
+----------------------+
|  提交事务           | ← 所有SQL成功,提交事务
|  (commit)           |
+----------------------+
       OR
+----------------------+
|  回滚事务           | ← 出现异常,回滚事务
|  (rollback)         |    撤销所有SQL操作
+----------------------+

Spring事务角色

核心概念解析

事务管理员(Transaction Administrator)

  • 定义 :发起和控制事务的一方,通常是业务层(Service层)
  • 职责
    • 开启一个新的事务
    • 定义事务的边界(从何处开始,到何处结束)
    • 决定事务的提交或回滚
  • 实现方式 :通过在业务方法上添加@Transactional注解

事务协调员(Transaction Coordinator)

  • 定义 :加入并参与现有事务的一方,通常是数据层(DAO层),也可以是业务层方法
  • 职责
    • 加入已经存在的事务
    • 在事务中执行具体的数据库操作
    • 不独立控制事务的提交或回滚
  • 实现方式:通过Spring的事务管理机制自动参与事务

场景分析

无Spring事务管理的场景分析:两个数据库操作分别在两个独立事务中,无法保障数据一致性

有Spring事务管理的场景分析

  1. transfer上添加了@Transactional注解,在该方法上就会有一个事务T
  2. AccountDao的outMoney方法的事务T1加入到transfer的事务T中
  3. AccountDao的inMoney方法的事务T2加入到transfer的事务T中

这样就保证他们在同一个事务中,当业务层中出现异常,整个事务就会回滚,保证数据的准确性。

数据源一致性要求

java 复制代码
@Configuration
public class JdbcConfig {
    
    @Bean
    public DataSource dataSource() {
        // 创建数据源
        return new DruidDataSource();
    }
    
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        // 事务管理器使用同一个数据源
        return new DataSourceTransactionManager(dataSource);
    }
    
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        // MyBatis使用同一个数据源
        SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        return factory;
    }
}

为什么事务管理器、MyBatis必须引用同一个DataSource Bean实例?

  • 事务管理器在开启事务时,需要数据源实例来获取数据库连接,并将连接与当前线程绑定,以管理事务。
    MyBatis执行SQL时也需要从数据源实例获取连接。
  • 如果事务管理器和MyBatis使用的是同一个数据源实例DataSource Bean,那么Spring就能确保它们使用同一个数据库连接,从而使得事务能够正确管理。
  • 反之,如果事务管理器和MyBatis使用了不同的数据源实例(即使这两个数据源的配置完全相同,指向同一个数据库),那么它们将使用两个独立的数据库连接,事务管理器管理的连接和MyBatis执行SQL使用的连接不是同一个,事务就无法正常工作。

上述为了简化演示将数据源、事务管理器和MyBatis配置全部放在JdbcConfig类中,实际开发建议为数据源、MyBatis、事务管理分别创建配置类

Spring事务属性

事务配置

在Spring中,@Transactional注解提供了丰富的事务配置选项,利用这些属性可以精确控制事务的行为。

核心事务属性详解

1. readOnly 属性

作用:

  • true:声明为只读事务,适用于查询操作
  • false:声明为读写事务,适用于增删改操作
java 复制代码
// 查询方法使用只读事务
@Transactional(readOnly = true)
public Account findById(Integer id) {
    return accountDao.findById(id);
}

// 增删改方法使用读写事务
@Transactional(readOnly = false)
public void updateAccount(Account account) {
    accountDao.update(account);
}

2. timeout 属性

作用:

  • 设置事务的完成时间,单位:秒
  • 事务在规定时间内未完成则自动回滚
  • 默认值:-1(不设置超时,使用数据库的默认超时设置,不同数据库的默认超时设置不同)
java 复制代码
// 设置10秒超时
@Transactional(timeout = 10)
public void batchProcess() {
    // 复杂的批量处理逻辑
}

3. rollbackFor 和 noRollbackFor 属性

Spring事务默认只对以下异常进行回滚:

  • Error异常:JVM严重错误
  • RuntimeException异常及其子类:运行时异常

检查型异常(Checked Exception)默认不会触发事务回滚。

业务异常属于哪一类,完全取决于开发者如何设计,继承RuntimeException就属于RuntimeException异常(大多数),继承Exception就属于检查型异常。

rollbackFor 和 noRollbackFor的优先级:Spring会优先匹配noRollbackFor。如果一个异常同时匹配rollbackFor和noRollbackFor,则以noRollbackFor为准(不回滚)。

自定义回滚规则

指定特定异常触发回滚:

  • @Transactional(rollbackFor/noRollbackFor = 异常类名.class)
  • @Transactional(rollbackFor/noRollbackFor = {异常类名1.class, 异常名类2.class})(当指定多个异常类时,使用数组形式)
java 复制代码
// 指定特定异常触发回滚
@Transactional(rollbackFor = {IOException.class, SQLException.class})
public void transfer(String out, String in, Double money) throws IOException {
    accountDao.outMoney(out, money);
    throw new IOException(&#34;模拟异常&#34;);
    // 现在IOException会触发事务回滚
}

// 指定特定异常不触发回滚
@Transactional(noRollbackFor = {BusinessException.class})
public void processBusiness() {
    // BusinessException发生时,事务不会回滚
    throw new BusinessException(&#34;业务异常,但不回滚事务&#34;);
}

字符串&#34;&#34;配置方式:

  • rollbackForClassName等同于rollbackFor,但属性为异常的类全名字符串
  • noRollbackForClassName等同于noRollbackFor,但属性为异常的类全名字符串
  • @Transactional(rollbackForClassName/noRollbackForClassName = &#34;异常的类全名字符串&#34;) (当指定多个异常类时,使用数组形式)
java 复制代码
// 使用异常类全名(字符串形式)
@Transactional(
    rollbackForClassName = {&#34;java.io.IOException&#34;, &#34;java.sql.SQLException&#34;},
    noRollbackForClassName = &#34;com.example.BusinessException&#34;
)

写法总结一下

  • 一个@Transactional()注解可以同时包含rollbackFornoRollbackForrollbackForClassNamenoRollbackForClassName四个异常配置属性(以及其他非异常属性),这些属性之间用逗号分隔。
  • 每个属性内部可以指定一个或多个异常类的Class对象或全限定名字符串&#34;&#34;
  • 如果是多个异常类,则用逗号分隔并用大括号{}包裹(数组形式)。

Java标准库的核心异常类

text 复制代码
Throwable (所有异常/错误的基类)
├── Error (错误)
│   ├── VirtualMachineError (虚拟机错误)
│   │   ├── OutOfMemoryError (内存溢出)
│   │   ├── StackOverflowError (栈溢出)
│   │   └── InternalError (内部错误)
│   ├── LinkageError (链接错误)
│   │   ├── NoClassDefFoundError (类定义未找到)
│   │   └── ClassFormatError (类格式错误)
│   └── AssertionError (断言错误)
└── Exception (异常)
    ├── RuntimeException (运行时异常)
    │   ├── NullPointerException (空指针)
    │   ├── IllegalArgumentException (非法参数)
    │   ├── IndexOutOfBoundsException (索引越界)
    │   ├── ClassCastException (类型转换)
    │   └── ArithmeticException (算术异常)
    └── Checked Exception (检查型异常)
        ├── IOException (IO异常)
        ├── SQLException (SQL异常)
        ├── ParseException (解析异常)
        └── ClassNotFoundException (类未找到异常)
  • 业务异常都是自定义的异常

4. isolation 属性

作用:设置事务的隔离级别。

写法:@Transactional(isolation = Isolation.隔离级别)

隔离级别从低到高:

  1. DEFAULT(默认)
  • 使用底层数据库的默认隔离级别
  • MySQL默认:REPEATABLE_READ(可重复读)
  • Oracle默认:READ_COMMITTED(读已提交)
  1. READ_UNCOMMITTED(读未提交)
  • 优点:性能最高
  • 缺点:可能读取到其他事务未提交的数据(脏读)
  • 使用场景:对数据一致性要求极低,追求性能的场景
  1. READ_COMMITTED(读已提交,Oracle默认级别)
  • 优点:解决脏读问题
  • 缺点:不可重复读(同一事务内两次读取结果可能不同)
  • 使用场景:大多数业务场景
  1. REPEATABLE_READ(可重复读,MySQL默认级别)
  • 优点:解决脏读、不可重复读问题
  • 缺点:幻读(同一查询条件,多次查询返回的行数不同)
  • 使用场景:对数据一致性要求较高的场景
  1. SERIALIZABLE(串行化)
  • 优点:解决所有并发问题
  • 缺点:性能最低(完全串行执行)
  • 使用场景:对数据一致性要求极高,可接受性能损失的场景
java 复制代码
// 使用READ_COMMITTED隔离级别
@Transactional(isolation = Isolation.READ_COMMITTED)
public void updateAccount(Account account) {
    // 在高并发环境下,确保读取已提交的数据
    accountDao.update(account);
}

// 使用SERIALIZABLE隔离级别处理敏感操作
@Transactional(isolation = Isolation.SERIALIZABLE)
public void processFinancialTransaction() {
    // 财务交易,需要最高级别的一致性保证
}

转账业务追加日志案例

需求分析

在原有的银行转账功能基础上,新增日志记录功能,要求:

  1. 每次转账操作都要在数据库中留下记录
  2. 无论转账是否成功,都必须记录日志
  3. 日志内容应包含:转出账户、转入账户、转账金额、操作时间
环境准备

该环境是基于转账环境来完成的,所以环境的准备可以参考AOP事务管理 - Spring事务简介 - 转账案例-环境搭建,在其基础上,我们继续往下写

1. 数据库表结构

创建日志表,用于记录转账操作:

sql 复制代码
create table tbl_log(
   id int primary key auto_increment,
   info varchar(255),
   createDate datetime
);

2. 数据访问层(DAO)

添加日志数据访问接口LogDao:

java 复制代码
public interface LogDao {
    // 插入日志记录,使用当前时间
    @Insert(&#34;insert into tbl_log (info,createDate) values(#{info},now())&#34;)
    void log(String info);
}

3. 业务逻辑层(Service)

添加日志服务接口LogService及其实现类:

java 复制代码
// 日志服务接口
public interface LogService {
    void log(String out, String in, Double money);
}

// 日志服务实现类
@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
    
    // 日志记录方法,也有事务管理
    @Transactional
    public void log(String out, String in, Double money) {
        // 拼接日志信息
        String logInfo = &#34;转账操作由&#34; + out + &#34;到&#34; + in + &#34;,金额:&#34; + money;
        logDao.log(logInfo);
    }
}

在转账的业务中添加记录日志:

java 复制代码
// 转账服务接口
public interface AccountService {
    /**
     * 转账操作
     * @param out 转出账户
     * @param in 转入账户
     * @param money 转账金额
     */
    void transfer(String out, String in, Double money) throws IOException;
}

// 转账服务实现类(问题版本)
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    
    @Autowired
    private LogService logService;
    
    // 转账方法,有事务管理
    @Transactional
    public void transfer(String out, String in, Double money) {
        try {
            // 执行转账操作
            accountDao.outMoney(out, money);  // 转出
            // int i = 1 / 0;                 // 模拟异常
            accountDao.inMoney(in, money);    // 转入
            
        } finally {
            // 期望无论转账是否成功,都记录日志
            logService.log(out, in, money);
        }
    }
}
java 复制代码
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;
    
     // 方法上添加@Transactional注解,表示该方法需要事务管理
    @Transactional
    @Override     // 注解实现的接口方法
    public void transfer(String out, String in, Double money) {
        // 第一步:从转出账户扣除金额
        accountDao.outMoney(out, money);
        
        // 模拟异常:这里会抛出算数异常ArithmeticException
        int i = 1 / 0;
        
        // 第二步:向转入账户增加金额(如果上面出现异常,这行代码不会执行)
        accountDao.inMoney(in, money);
    }
}

运行程序

  • 当程序正常运行,tbl_account表中转账成功,tbl_log表中日志记录成功
  • 当转账业务之间出现异常(int i =1/0)时,转账失败,tbl_account表成功回滚,但是tbl_log表未添加数据,结果失败
    失败原因:日志的记录与转账操作隶属同一个事务,同成功同失败
    目标效果:无论转账操作是否成功,日志必须保留

事务传播行为

对于上述案例的分析:

  1. transfer()方法开启事务T,包含inMoney()方法和outMoney()方法
  2. log()方法因为加了@Transactional注解,开启了事务T2。log()方法默认传播行为是REQUIRED,会加入现有事务T
  3. 当转账失败时,事务T回滚,导致log()方法的操作也被回滚
  4. 最终结果:转账失败时日志没有被记录

要想解决这个问题,就需要用到事务传播行为

事务传播行为:指的是事务协调员(被调用的方法)对事务管理员(调用方 方法)所携带事务的处理态度。

在这个案例中的通俗理解:

  • 当事务管理员(如transfer()方法)调用事务协调员(如log()方法)时
  • 事务协调员需要决定:是加入调用方的事务,还是自己开启新事务,或是不在事务中执行
  • 这个决定就是事务传播行为
修改logService以改变事务的传播行为

log()方法的事务传播行为改为REQUIRES_NEW

java 复制代码
@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
    
    // 设置传播行为为REQUIRES_NEW,始终开启新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void log(String out, String in, Double money) {
        String logInfo = &#34;转账操作由&#34; + out + &#34;到&#34; + in + &#34;,金额:&#34; + money;
        logDao.log(logInfo);
    }
}

修改后的执行流程

转账成功的场景:

r 复制代码
1. transfer()方法开启事务T
2. 执行accountDao.outMoney()(在事务T中)
3. 执行accountDao.inMoney()(在事务T中)
4. 调用logService.log()
5. 由于log()方法的传播行为是REQUIRES_NEW,挂起事务T,开启新事务T1
6. 在事务T1中执行日志记录
7. 提交事务T1(日志被记录)
8. 恢复事务T
9. 提交事务T(转账成功)

转账失败的场景:

r 复制代码
1. transfer()方法开启事务T
2. 执行accountDao.outMoney()(在事务T中)
3. 执行int i = 1/0(抛出异常)
4. 调用logService.log()(在finally块中)
5. 由于log()方法的传播行为是REQUIRES_NEW,挂起事务T,开启新事务T1
6. 在事务T1中执行日志记录
7. 提交事务T1(日志被记录)
8. 恢复事务T
9. 事务T因异常而回滚(转账失败)

效果:修改后,无论转账是否成功,日志都会被记录。log()方法在自己的独立事务中执行,不受transfer()方法事务的影响。

事务传播行为的可选值

Spring提供了7种事务传播行为,每种行为都有特定的用途:

  1. REQUIRED(默认值)
  • 含义:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新事务
  • 使用场景:大多数业务方法适用,确保操作在事务中执行
java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public void updateAccount(Account account) {
    // 默认行为,无需显式指定
}
  1. REQUIRES_NEW
  • 含义:始终创建一个新事务,如果当前存在事务,则挂起当前事务
  • 使用场景:需要独立提交的操作,如日志记录、审计跟踪
java 复制代码
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void auditLog(String operation) {
    // 独立事务,不受调用方事务影响
}
  1. SUPPORTS
  • 含义:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行
  • 使用场景:查询方法,可以在事务中执行,也可以不在事务中执行
java 复制代码
@Transactional(propagation = Propagation.SUPPORTS)
public List findAll() {
    // 查询方法,可以支持事务但不是必须
}
  1. NOT_SUPPORTED
  • 含义:以非事务方式执行操作,如果当前存在事务,则挂起当前事务
  • 使用场景:不支持事务的操作,如发送邮件、调用外部API
java 复制代码
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void sendEmail(String to, String content) {
    // 非事务执行,即使调用方有事务也会被挂起
}
  1. MANDATORY
  • 含义:必须在事务中执行,如果当前没有事务,则抛出异常
  • 使用场景:强制要求调用方必须开启事务的方法
java 复制代码
@Transactional(propagation = Propagation.MANDATORY)
public void financialTransaction(Transaction tx) {
    // 必须在事务中调用,否则抛出异常
}
  1. NEVER
  • 含义:必须在非事务状态下执行,如果当前存在事务,则抛出异常
  • 使用场景:明确禁止在事务中执行的方法
java 复制代码
@Transactional(propagation = Propagation.NEVER)
public void generateReport() {
    // 不能在事务中调用
}
  1. NESTED
  • 含义:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则创建新事务
  • 使用场景:需要部分回滚的场景,嵌套事务可以独立回滚而不影响外部事务
  • 注意:并非所有数据库都支持嵌套事务,MySQL的InnoDB引擎通过保存点(savepoint)实现
java 复制代码
@Transactional(propagation = Propagation.NESTED)
public void processItem(Item item) {
    // 嵌套事务,可以独立回滚
}
判断方法是事务管理员还是事务协调员,或者都不是
text 复制代码
判断方法的事务角色
│
├─ 判断:方法是否有@Transactional注解?
│   │
│   ├─ 否 → 不是事务协调员,也不是事务管理员,只是普通方法
│   │
│   └─ 是 → 是事务相关方法
│       │
│       └─ 传播行为决定角色
│           │
│           ├─ 情况1:被调用时调用方无事务
│           │   │
│           │   ├─ REQUIRED/MANDATORY/NESTED: 开启新事务 → 事务管理员
│           │   │
│           │   ├─ REQUIRES_NEW: 开启新事务 → 事务管理员
│           │   │
│           │   └─ SUPPORTS/NOT_SUPPORTED/NEVER: 无事务执行 → 都不是(普通方法)
│           │
│           └─ 情况2:被调用时调用方有事务
│               │
│               ├─ REQUIRED/MANDATORY/SUPPORTS: 加入现有事务 → 事务协调员
│               │
│               ├─ REQUIRES_NEW: 开启新事务 → 事务管理员
│               │
│               ├─ NOT_SUPPORTED/NEVER: 无事务执行 → 都不是(普通方法)
│               │
│               └─ NESTED: 创建嵌套事务 → 特殊角色(既是协调员又是管理员)
相关推荐
okseekw17 小时前
File类:你与文件的"爱恨情仇"——Java文件操作的趣味指南
java·后端
海上彼尚17 小时前
Go之路 - 2.go的常量变量[完整版]
开发语言·后端·golang
闲人编程17 小时前
Flask-SQLAlchemy高级用法:关系建模与复杂查询
后端·python·flask·一对多·多对多·一对一·自引用
Li_76953217 小时前
Spring Cloud —— SkyWalking(五)
java·后端·spring·spring cloud·skywalking
武子康17 小时前
大数据-180 Elasticsearch 近实时搜索:Segment、Refresh、Flush、Translog 全流程解析
大数据·后端·elasticsearch
武子康17 小时前
Java-189 Guava Cache 源码剖析:LocalCache、Segment 与 LoadingCache 工作原理全解析
java·redis·后端·spring·缓存·guava·guava cache
踏浪无痕17 小时前
彻底搞懂微服务 TraceId 传递:ThreadLocal、TTL 与全链路日志追踪实战
后端·微服务·面试
程序员小假17 小时前
我们来说一说 Redis 主从复制的原理及作用
java·后端
海上彼尚17 小时前
Go之路 - 1.gomod指令
开发语言·后端·golang