JavaEE进阶——SpringAOP从入门到源码全解析

目录

[Spring AOP 超详细入门教程:从概念到源码](#Spring AOP 超详细入门教程:从概念到源码)

写给新手的话

[1. AOP基础概念(先理解思想)](#1. AOP基础概念(先理解思想))

[1.1 什么是AOP?(生活化理解)](#1.1 什么是AOP?(生活化理解))

[1.2 AOP核心术语(必须掌握)](#1.2 AOP核心术语(必须掌握))

[2. Spring AOP快速入门(第一个完整例子)](#2. Spring AOP快速入门(第一个完整例子))

[2.1 添加依赖(项目基础)](#2.1 添加依赖(项目基础))

[2.2 创建业务Controller(被增强的目标)](#2.2 创建业务Controller(被增强的目标))

[2.3 创建切面类(这是重点!)](#2.3 创建切面类(这是重点!))

[2.4 运行测试(看效果)](#2.4 运行测试(看效果))

[3. Spring AOP核心概念详解(代码级深扒)](#3. Spring AOP核心概念详解(代码级深扒))

[3.1 切点(Pointcut) - "规则定义器"](#3.1 切点(Pointcut) - "规则定义器")

[3.2 连接点(Join Point) - "符合条件的具体方法"](#3.2 连接点(Join Point) - "符合条件的具体方法")

[3.3 通知(Advice) - "增强的具体逻辑"](#3.3 通知(Advice) - "增强的具体逻辑")

[3.4 切面(Aspect) - "切点 + 通知的集合"](#3.4 切面(Aspect) - "切点 + 通知的集合")

[4. 通知执行顺序(代码验证结论)](#4. 通知执行顺序(代码验证结论))

[4.1 同一切面内的执行顺序](#4.1 同一切面内的执行顺序)

[4.2 多个切面的执行顺序(@Order注解)](#4.2 多个切面的执行顺序(@Order注解))

[5. 切点表达式详解(精确匹配)](#5. 切点表达式详解(精确匹配))

[5.1 execution表达式(最常用)](#5.1 execution表达式(最常用))

[5.2 @annotation表达式(注解匹配)](#5.2 @annotation表达式(注解匹配))

[6. Spring AOP实现原理(源码级理解)](#6. Spring AOP实现原理(源码级理解))

[6.1 代理模式基础(先理解设计模式)](#6.1 代理模式基础(先理解设计模式))

[6.1.1 静态代理(手动写代理类)](#6.1.1 静态代理(手动写代理类))

[6.1.2 动态代理(自动生成代理类)](#6.1.2 动态代理(自动生成代理类))

[6.2 Spring AOP如何选择代理方式?(源码逻辑)](#6.2 Spring AOP如何选择代理方式?(源码逻辑))

[6.3 代理执行流程(方法调用时发生了什么)](#6.3 代理执行流程(方法调用时发生了什么))

[7. 完整项目实战(整合所有知识点)](#7. 完整项目实战(整合所有知识点))

[7.1 项目结构](#7.1 项目结构)

[7.2 完整代码(带全套注释)](#7.2 完整代码(带全套注释))

[7.3 测试结果分析(结论与代码对应)](#7.3 测试结果分析(结论与代码对应))

[8. 常见问题与解决方案(代码级避坑)](#8. 常见问题与解决方案(代码级避坑))

[8.1 同一个类内方法调用,AOP失效](#8.1 同一个类内方法调用,AOP失效)

[8.2 通知中抛出异常,导致原方法无法执行](#8.2 通知中抛出异常,导致原方法无法执行)

[8.3 切点表达式过于宽泛,影响性能](#8.3 切点表达式过于宽泛,影响性能)

[9. 总结(知识地图)](#9. 总结(知识地图))

[9.1 核心概念关系图](#9.1 核心概念关系图)

[9.2 代理方式选择决策树](#9.2 代理方式选择决策树)

[9.3 最佳实践清单](#9.3 最佳实践清单)

[10. 最后的叮嘱(给新手的建议)](#10. 最后的叮嘱(给新手的建议))

[Spring Boot 3.x AOP 完整实战代码包](#Spring Boot 3.x AOP 完整实战代码包)

快速开始

[一、Maven 坐标 (pom.xml)](#一、Maven 坐标 (pom.xml))

二、配置文件 (application.yml)

[三、Java 源码](#三、Java 源码)

[1. 启动类 (DemoApplication.java)](#1. 启动类 (DemoApplication.java))

[2. 自定义注解 (LogExecutionTime.java)](#2. 自定义注解 (LogExecutionTime.java))

[3. 切面类 (Aspects)](#3. 切面类 (Aspects))

[3.1 日志切面 (LogAspect.java)](#3.1 日志切面 (LogAspect.java))

[3.2 耗时统计切面 (TimeAspect.java)](#3.2 耗时统计切面 (TimeAspect.java))

[3.3 安全/权限切面 (SecurityAspect.java)](#3.3 安全/权限切面 (SecurityAspect.java))

[4. 控制层 (Controllers)](#4. 控制层 (Controllers))

[4.1 用户控制器 (UserController.java)](#4.1 用户控制器 (UserController.java))

[4.2 订单控制器 (OrderController.java)](#4.2 订单控制器 (OrderController.java))

[4.3 测试控制器 (HelloController.java)](#4.3 测试控制器 (HelloController.java))

四、验证结果

测试用例

示例日志输出 (/user/add)

[Spring AOP 实战回顾:你其实已经掌握了什么?](#Spring AOP 实战回顾:你其实已经掌握了什么?)

[一、从日志里能直接验证的 6 个核心结论](#一、从日志里能直接验证的 6 个核心结论)

二、你已经到手的"技能点"

三、下一步可以玩什么(递进路线)

四、一句话总结


Spring AOP 超详细入门教程:从概念到源码

写给新手的话

你好!我是你的Java学习伙伴。这份教程将用最白话的语言、最详细的代码注释,带你彻底搞懂Spring AOP。每个概念我们都会先讲"是什么",再看"代码怎么写",最后分析"代码如何体现出这个概念"。

1. AOP基础概念(先理解思想)

1.1 什么是AOP?(生活化理解)

AOP = 面向切面编程,听起来很抽象对吧?我们来举几个生活中的例子:

例子1:餐厅服务员

  • 你点完菜后,服务员会:

    1. 前后都要说"您好"/"请慢用"(环绕通知)

    2. 上菜前检查餐具是否干净(前置通知)

    3. 上菜后主动询问是否需要加调料(后置通知)

    4. 如果菜品有问题,立即处理投诉(异常通知)

这些额外服务不干扰厨师做菜,但增强了用餐体验。这就是AOP思想!

例子2:手机壳

  • 手机(核心业务)能打电话、发短信

  • 手机壳(切面)提供防摔、美观功能

  • 你不需要改造手机内部电路,就能增强功能

1.2 AOP核心术语(必须掌握)

|---------------------|---------------|-----------------------------------------------|
| 术语 | 通俗解释 | 代码中的体现 |
| 切点(Pointcut) | "哪些方法要增强"的规则 | @Around("execution(* com.example.*.*(..))") |
| 连接点(Join Point) | 符合规则的具体方法 | BookController.addBook() |
| 通知(Advice) | "增强什么功能"的代码 | recordTime()方法里的计时逻辑 |
| 切面(Aspect) | 切点 + 通知的组合 | 整个TimeAspect类 |
| 织入(Weaving) | 把增强代码"织"到原方法上 | Spring自动完成的代理过程 |

2. Spring AOP快速入门(第一个完整例子)

2.1 添加依赖(项目基础)

XML 复制代码
<!-- pom.xml文件 -->
<!-- spring-boot-starter-aop:Spring Boot提供的AOP启动器,包含了所有AOP相关依赖 -->
<!-- 添加后无需任何配置,Spring Boot会自动开启AOP功能 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

依赖如何体现AOP思想?

  • 这个依赖就像"餐厅服务员培训手册",告诉Spring:"你要准备好提供额外服务的能力"

  • 它内部包含了spring-aopaspectjweaver等核心库,相当于服务员的工具包

2.2 创建业务Controller(被增强的目标)

java 复制代码
// BookController.java
// 这个类就是"厨师",只关心核心业务(做菜/处理图书)

import org.springframework.web.bind.annotation.RequestMapping;  // Spring MVC的请求映射注解,把HTTP请求映射到方法
import org.springframework.web.bind.annotation.RestController;  // 标识这是RESTful控制器,返回JSON数据

@RequestMapping("/book")  // 所有请求路径的前缀都是/book
@RestController  // 告诉Spring:这个类是控制器,所有方法返回的数据直接写给客户端(相当于@Controller + @ResponseBody)
public class BookController {

    // 处理添加图书的请求
    @RequestMapping("/addBook")  // 映射/book/addBook请求
    public String addBook() {
        // 模拟业务逻辑耗时
        try {
            Thread.sleep(100);  // 让当前线程暂停100毫秒,模拟数据库操作耗时
        } catch (InterruptedException e) {
            e.printStackTrace();  // 打印异常堆栈信息
        }
        return "添加成功";  // 返回给客户端的字符串
    }

    // 处理查询图书的请求
    @RequestMapping("/queryBookById")  // 映射/book/queryBookById请求
    public String queryBookById() {
        // 模拟业务逻辑耗时
        try {
            Thread.sleep(200);  // 模拟更复杂的数据库查询,耗时200毫秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "查询成功";
    }
}

代码如何体现"连接点"?

  • addBook()queryBookById()就是连接点,因为它们是具体的、可以被AOP控制的方法

  • 它们符合execution(* com.example.demo.controller.*.*(..))这个规则,所以会被增强

2.3 创建切面类(这是重点!)

java 复制代码
// TimeAspect.java
// 这个类就是"服务员",提供计时服务,不修改厨师的做菜逻辑

import lombok.extern.slf4j.Slf4j;  // Lombok提供的日志注解,自动生成log对象
import org.aspectj.lang.ProceedingJoinPoint;  // 连接点对象,封装了目标方法的信息和执行能力
import org.aspectj.lang.annotation.Around;  // 环绕通知注解,表示在目标方法前后都要执行
import org.aspectj.lang.annotation.Aspect;  // 标识这是一个切面类,Spring看到这个注解就知道要做AOP了
import org.springframework.stereotype.Component;  // Spring组件注解,让Spring管理这个Bean的生命周期

@Slf4j  // Lombok注解,自动生成private static final Logger log = LoggerFactory.getLogger(TimeAspect.class);
@Aspect  // 标识这是切面类(核心注解!没有它就不是切面)
@Component  // 把切面类交给Spring容器管理,这样Spring才能识别并启用它
public class TimeAspect {
    
    /**
     * 记录方法耗时(环绕通知)
     * * @param pjp 连接点对象,可以通过它:
     * - 获取方法名:pjp.getSignature().getName()
     * - 获取类名:pjp.getTarget().getClass().getName()
     * - 执行原方法:pjp.proceed()
     * - 获取方法参数:pjp.getArgs()
     * * @return 目标方法的执行结果(必须返回,否则调用者拿不到返回值)
     * @throws Throwable 可能抛出的异常(必须声明,因为pjp.proceed()可能抛出任何异常)
     */
    // @Around:环绕通知,像三明治一样包裹目标方法
    // "execution(* com.example.demo.controller.*.*(..))":切点表达式,匹配controller包下所有类的所有方法
    //       * :任意返回值
    //       com.example.demo.controller.*:controller包下的所有类
    //       .*:所有方法
    //       (..):任意参数
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        // ============ 目标方法执行前 ============
        long begin = System.currentTimeMillis();  // 记录开始时间(毫秒)
        
        // ============ 执行目标方法 ============
        // pjp.proceed():调用原始方法(如addBook())
        // 这个方法会暂停当前线程,直到目标方法执行完毕
        // 返回值就是目标方法的返回值(如"添加成功")
        Object result = pjp.proceed();  
        
        // ============ 目标方法执行后 ============
        long end = System.currentTimeMillis();  // 记录结束时间
        
        // 打印日志:方法签名 + 耗时
        // pjp.getSignature():获取方法签名(包含方法名、参数类型等)
        // end - begin:计算耗时
        log.info(pjp.getSignature() + "执行耗时:{}ms", end - begin);
        
        // 必须返回结果,否则调用方(如浏览器)收不到返回值
        return result;
    }
}

代码如何体现"切面"概念?

  • 整个TimeAspect类就是一个切面,因为它:

    1. @Aspect注解(标识身份)

    2. 包含了切点(@Around后面的字符串)

    3. 包含了通知(recordTime方法体)

  • 这三者的组合 = 切面!

代码如何体现"无侵入性"?

  • 注意BookController里完全没有关于计时的代码

  • 计时逻辑完全在TimeAspect

  • 这就是"不修改源代码就能增强功能",完美体现AOP的核心价值

2.4 运行测试(看效果)

bash 复制代码
// 启动Spring Boot应用后,访问:
// http://localhost:8080/book/addBook
// http://localhost:8080/book/queryBookById

// 控制台输出示例:
// 2023-10-05 14:30:22.123  INFO 12345 --- [nio-8080-exec-1] c.e.d.a.TimeAspect: String addBook() 执行耗时:105ms
// 2023-10-05 14:30:22.328  INFO 12345 --- [nio-8080-exec-2] c.e.d.a.TimeAspect: String queryBookById() 执行耗时:203ms

如何从输出验证AOP生效?

  1. c.e.d.a.TimeAspect:说明日志来自我们的切面类

  2. addBook():成功获取到方法名

  3. 105ms:记录了真实耗时(100ms sleep + 5ms代码执行)

  4. 没有修改BookController任何一行代码,却有了计时功能!

3. Spring AOP核心概念详解(代码级深扒)

3.1 切点(Pointcut) - "规则定义器"

概念:定义"哪些方法要被增强"的规则,使用AspectJ表达式描述。

java 复制代码
// 切点表达式详解:execution(* com.example.demo.controller.*.*(..))

// 语法结构:execution(访问修饰符 返回值 包名.类名.方法名(参数) 异常)
// 实际示例:execution(public String com.example.demo.controller.BookController.addBook(BookInfo))

// 通配符说明:
// * : 匹配任意单个元素(返回值、包名、类名、方法名)
// ..   : 匹配任意多个连续的任意符号(包层级、参数个数和类型)

// 更多示例:
@Around("execution(public * com.example.demo.controller.*.*(..))")  // 匹配public方法
@Around("execution(* com.example.demo.controller.BookController.*(..))")  // 只匹配BookController类
@Around("execution(* com..controller.*.*(..))")  // 匹配com包及其子包下的controller包
@Around("execution(* *.*(..))")  // 危险!匹配所有方法(性能差)

如何从代码中看出"切点"的作用?

  • @Around后面的字符串就是切点表达式

  • Spring启动时,会扫描所有Bean的方法,把符合这个表达式的方法标记为连接点

  • 比如BookController.addBook()符合规则,就被织入了计时逻辑

3.2 连接点(Join Point) - "符合条件的具体方法"

概念:切点表达式匹配到的每一个具体方法就是连接点。

java 复制代码
// 假设我们有以下类:
package com.example.demo.controller;

@RestController
public class UserController {
    @RequestMapping("/addUser")
    public String addUser() { /* ... */ }  // 这是连接点,因为匹配规则
    
    @RequestMapping("/delUser")
    private String delUser() { /* ... */ }  // 这不是连接点!private方法无法被代理
}

@RestController
public class OrderController {
    @RequestMapping("/addOrder")
    public String addOrder() { /* ... */ }  // 这是连接点
}

// 切点表达式:execution(* com.example.demo.controller.*.*(..))
// 匹配结果:
//  ✔ UserController.addUser()  - 匹配(public、在controller包下)
//  ✘ UserController.delUser()  - 不匹配(private修饰)
//  ✔ OrderController.addOrder() - 匹配

// 代码体现:在TimeAspect中,pjp.getSignature()能获取到的方法名,就是当前正在执行的连接点

如何从代码中验证"连接点"?

  • 运行后看日志:String addBook()String queryBookById()被打印出来

  • 这些就是具体的连接点,每一个都是切点表达式筛选出来的"候选人"

3.3 通知(Advice) - "增强的具体逻辑"

概念:真正要执行的增强代码,Spring提供了5种通知类型。

java 复制代码
// 完整展示5种通知的代码

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;  // 方法签名接口
import org.aspectj.lang.annotation.*;  // 导入所有AOP注解
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
public class AllAdviceDemo {

    // ============ 前置通知 ============
    // 在目标方法**之前**执行,无法阻止方法执行(除非抛异常)
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void doBefore(JoinPoint jp) {  // JoinPoint:连接点信息(比ProceedingJoinPoint少一个proceed())
        log.info("【前置通知】方法准备执行: {}", jp.getSignature().getName());
        // 可以在这里做:参数校验、权限检查、日志记录
    }

    // ============ 后置通知 ============
    // 在目标方法**之后**执行,无论方法是否成功或抛异常,**都会执行**(类似finally)
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void doAfter(JoinPoint jp) {
        log.info("【后置通知】方法执行完毕: {}", jp.getSignature().getName());
        // 可以在这里做:资源释放、清理工作
    }

    // ============ 返回后通知 ============
    // 在目标方法**成功返回后**执行,**方法抛出异常时不执行**
    @AfterReturning(value = "execution(* com.example.demo.controller.*.*(..))", 
                    returning = "result")  // returning指定接收返回值的参数名
    public void doAfterReturning(JoinPoint jp, Object result) {
        log.info("【返回后通知】方法成功返回,返回值: {}", result);
        // 可以在这里做:结果缓存、成功日志
    }

    // ============ 异常后通知 ============
    // 在目标方法**抛出异常后**执行,**方法成功返回时不执行**
    @AfterThrowing(value = "execution(* com.example.demo.controller.*.*(..))", 
                   throwing = "e")  // throwing指定接收异常的参数名
    public void doAfterThrowing(JoinPoint jp, Exception e) {
        log.info("【异常后通知】方法抛出异常: {}", e.getMessage());
        // 可以在这里做:异常记录、发送告警
    }

    // ============ 环绕通知 ============
    // **最强大**的通知,可以完全控制目标方法的执行
    // 在目标方法**前后**都执行,可以阻止方法执行、修改参数、修改返回值
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
        log.info("【环绕通知-前】方法准备执行: {}", pjp.getSignature().getName());
        
        // 可以在这里修改参数
        Object[] args = pjp.getArgs();
        // args[0] = "modified"; // 修改第一个参数
        
        Object result = null;
        try {
            result = pjp.proceed(args);  // 执行目标方法,可以传入修改后的参数
            log.info("【环绕通知-后】方法成功执行,准备返回");
            // 可以在这里修改返回值
            // result = "modified result";
        } catch (Throwable e) {
            log.info("【环绕通知-异常】方法执行出错");
            throw e;  // 必须抛出异常,否则异常被吞掉
        } finally {
            log.info("【环绕通知-最终】无论如何都会执行");
        }
        
        return result;  // 必须返回结果
    }
}

如何从代码中理解5种通知的区别?

java 复制代码
// 测试Controller
@RestController
public class TestController {
    @RequestMapping("/test1")
    public String test1() {
        return "success";  // 正常返回
    }
    
    @RequestMapping("/test2")
    public String test2() {
        int i = 1 / 0;  // 抛出ArithmeticException
        return "success";
    }
}

// 访问/test1的输出顺序:
// 【前置通知】方法准备执行: test1
// 【环绕通知-前】方法准备执行: test1
// 【环绕通知-后】方法成功执行,准备返回
// 【环绕通知-最终】无论如何都会执行
// 【返回后通知】方法成功返回,返回值: success
// 【后置通知】方法执行完毕: test1

// 访问/test2的输出顺序:
// 【前置通知】方法准备执行: test2
// 【环绕通知-前】方法准备执行: test2
// 【环绕通知-异常】方法执行出错
// 【环绕通知-最终】无论如何都会执行
// 【异常后通知】方法抛出异常: / by zero
// 【后置通知】方法执行完毕: test2
// 注意:【返回后通知】没有执行!因为方法抛异常了

// 结论体现:
// 1. @AfterReturning只在成功时执行 → 代码中test2抛异常,它没有打印
// 2. @AfterThrowing只在异常时执行 → 代码中test2抛异常,它打印了
// 3. @After无论成败都执行 → 两个测试都打印了
// 4. @Around最强大 → 代码中可以控制是否调用pjp.proceed()

3.4 切面(Aspect) - "切点 + 通知的集合"

概念:切面 = 切点(在哪里增强)+ 通知(增强什么),是一个完整的增强模块。

java 复制代码
// 一个完整的切面类

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect  // 标识这是一个切面类
public class LogAspect {
    
    // ============ 定义切点 ============
    // 把公共的切点表达式提取出来,避免重复写
    // 方法名pt()就是切点的ID,其他通知可以通过"pt()"引用
    @Pointcut("execution(* com.example.demo.service.*.*(..))")
    private void pt() {}  // 方法体为空,因为@Pointcut只需要表达式
    
    // ============ 定义通知 ============
    
    // 引用切点pt()
    @Before("pt()")
    public void beforeLog() {
        log.info("记录日志 - 方法开始");
    }
    
    @After("pt()")
    public void afterLog() {
        log.info("记录日志 - 方法结束");
    }
    
    // ============ 这个类整体就是一个切面 ============
    // 它包含了:
    // 1. 切点定义:pt()
    // 2. 通知定义:beforeLog()、afterLog()
    // 3. 类注解:@Aspect
}

如何从代码中看出"切面 = 切点 + 通知"?

  • @Pointcut定义了规则(在哪里)

  • @Before@After定义了逻辑(做什么)

  • 它们共同存在于一个@Aspect类中,形成了一个完整的增强模块

4. 通知执行顺序(代码验证结论)

4.1 同一切面内的执行顺序

java 复制代码
// 结论代码验证
@Aspect
@Component
public class OrderDemo {
    
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("Around-前");
        Object result = pjp.proceed();
        System.out.println("Around-后");
        return result;
    }
    
    @Before("pt()")
    public void before() {
        System.out.println("Before");
    }
    
    @After("pt()")
    public void after() {
        System.out.println("After");
    }
    
    @AfterReturning("pt()")
    public void afterReturning() {
        System.out.println("AfterReturning");
    }
}

// 调用目标方法后输出:
// Around-前
// Before
// 目标方法执行
// Around-后
// After
// AfterReturning

// 代码如何体现结论?
// 1. @Around先开始 → 代码中around()先打印"Around-前"
// 2. @Before在目标方法前 → 代码中before()在pjp.proceed()前打印
// 3. @Around后可以拦截返回值 → 代码中around()可以修改result变量
// 4. @After在@AfterReturning前 → 代码中after()打印在afterReturning()前面

4.2 多个切面的执行顺序(@Order注解)

java 复制代码
// 切面A
@Slf4j
@Aspect
@Component
@Order(1)  // 数字越小,优先级越高(最先执行)
public class AspectA {
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void beforeA() {
        log.info("【AspectA-前置】order=1");
    }
    
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void afterA() {
        log.info("【AspectA-后置】order=1");
    }
}

// 切面B
@Slf4j
@Aspect
@Component
@Order(2)  // 比AspectA晚执行
public class AspectB {
    @Before("execution(* com.example.demo.controller.*.*(..))")
    public void beforeB() {
        log.info("【AspectB-前置】order=2");
    }
    
    @After("execution(* com.example.demo.controller.*.*(..))")
    public void afterB() {
        log.info("【AspectB-后置】order=2");
    }
}

// 调用目标方法后输出:
// 【AspectA-前置】order=1
// 【AspectB-前置】order=2
// 目标方法执行
// 【AspectB-后置】order=2
// 【AspectA-后置】order=1

// 代码如何体现结论?
// 1. @Order(1)先执行 → 代码中AspectA的beforeA()先打印
// 2. @Order大的后执行 → 代码中AspectB的beforeB()后打印
// 3. 后置通知顺序相反 → 代码中afterB()在afterA()前打印(先进后出,像栈结构)
// 4. 为什么?因为@After是在finally中执行的,执行顺序与调用顺序相反

5. 切点表达式详解(精确匹配)

5.1 execution表达式(最常用)

java 复制代码
// 语法:execution(修饰符 返回值 包名.类名.方法名(参数) 异常)

// 示例1:精确匹配
@Pointcut("execution(public String com.example.demo.controller.BookController.addBook(BookInfo))")
private void exactMatch() {}  // 只匹配这一个方法

// 示例2:宽泛匹配
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
private void broadMatch() {}  // 匹配controller包下所有类的所有方法

// 示例3:匹配特定后缀的方法
@Pointcut("execution(* com.example.demo.service.*Service.*(..))")
private void serviceMatch() {}  // 匹配所有以Service结尾的类的方法

// 示例4:匹配特定前缀的方法
@Pointcut("execution(* com.example.demo.service.*.save*(..))")
private void saveMatch() {}  // 匹配所有以save开头的方法

// 示例5:匹配无参方法
@Pointcut("execution(* com.example.demo.controller.*.*())")
private void noArgsMatch() {}  // 注意:(..)变成了()

// 示例6:匹配第一个参数是String的方法
@Pointcut("execution(* com.example.demo.service.*.save(String, ..))")
private void firstArgMatch() {}  // 第一个参数必须是String,后面可以有任意参数

// 代码如何体现匹配粒度?
// 1. 精确匹配 → 代码中只写了addBook一个方法,其他方法不受影响
// 2. 宽泛匹配 → 代码中*通配符,多个类被匹配
// 3. 后缀匹配 → 代码中*Service,所有Service类都被匹配
// 4. 前缀匹配 → 代码中save*,所有save方法都被匹配
// 5. 参数匹配 → 代码中String, ..,精确控制参数类型

5.2 @annotation表达式(注解匹配)

java 复制代码
// 场景:有些方法需要增强,有些不需要,用注解标记

// 步骤1:自定义注解
import java.lang.annotation.ElementType;  // 注解作用目标类型(类、方法、字段等)
import java.lang.annotation.Retention;  // 注解保留策略(源码、字节码、运行时)
import java.lang.annotation.RetentionPolicy;  // 保留策略的枚举
import java.lang.annotation.Target;  // 指定注解的作用目标

// @Target:这个注解只能用在方法上
@Target(ElementType.METHOD)
// @Retention:这个注解在运行时有效(Spring AOP通过反射读取它)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
    // 可以添加配置属性,比如:String value() default "";
    // 这里简化,不写任何属性
}

// 代码如何体现注解的作用?
// 1. @Target限制使用位置 → 代码中只能用在方法上,不能用在类或字段上
// 2. @Retention(RUNTIME) → 代码中运行时Spring能读取到这个注解
// 3. @interface → 代码中定义了一个注解类型

// 步骤2:切面类使用@annotation
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class AnnotationAspect {
    
    // @annotation:匹配所有标注了@LogExecutionTime的方法
    // 括号里是注解的全限定类名
    @Around("@annotation(com.example.demo.aspect.LogExecutionTime)")
    public Object logTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long end = System.currentTimeMillis();
        System.out.println(pjp.getSignature().getName() + " 耗时: " + (end - start) + "ms");
        return result;
    }
}

// 步骤3:在需要增强的方法上添加注解
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ProductController {
    
    @RequestMapping("/addProduct")
    @LogExecutionTime  // 只有这个方法会被增强!
    public String addProduct() {
        return "商品添加成功";
    }
    
    @RequestMapping("/delProduct")
    // 没有@LogExecutionTime注解,不会被增强
    public String delProduct() {
        return "商品删除成功";
    }
}

// 代码如何体现注解的精确控制?
// 1. @LogExecutionTime像开关 → 代码中addProduct()有注解,被增强
// 2. delProduct()无注解 → 代码中没有被打印耗时
// 3. 对比execution:注解方式更精确,不需要写复杂的包路径

6. Spring AOP实现原理(源码级理解)

6.1 代理模式基础(先理解设计模式)

6.1.1 静态代理(手动写代理类)
java 复制代码
// 1. 定义接口(Subject)
public interface HouseService {
    void rentHouse();  // 出租房子的方法
}

// 2. 真实实现类(RealSubject)
public class RealHouseService implements HouseService {
    @Override
    public void rentHouse() {
        System.out.println("房东:签合同、收房租");  // 核心业务
    }
}

// 3. 代理类(Proxy)- 手动编写
public class HouseServiceProxy implements HouseService {
    
    // 持有真实对象的引用(组合关系)
    private HouseService realHouseService;
    
    // 通过构造器注入真实对象
    public HouseServiceProxy(HouseService realHouseService) {
        this.realHouseService = realHouseService;
    }
    
    @Override
    public void rentHouse() {
        // ============ 代理增强逻辑 ============
        System.out.println("中介:带看房、谈价格");  // 前置增强
        
        // 调用真实对象的方法(核心业务)
        realHouseService.rentHouse();
        
        // ============ 代理增强逻辑 ============
        System.out.println("中介:收中介费、帮维修");  // 后置增强
    }
}

// 4. 使用代理
public class StaticProxyDemo {
    public static void main(String[] args) {
        // 创建真实对象(房东)
        HouseService realHouseService = new RealHouseService();
        
        // 创建代理对象(中介),把真实对象传进去
        HouseService proxy = new HouseServiceProxy(realHouseService);
        
        // 通过代理调用(客户找中介租房)
        proxy.rentHouse();
        
        // 输出:
        // 中介:带看房、谈价格
        // 房东:签合同、收房租
        // 中介:收中介费、帮维修
    }
}

// 代码如何体现静态代理的缺点?
// 1. 代码重复 → 每个方法都要写一遍代理逻辑(rentHouse()、sellHouse()都要写)
// 2. 不灵活 → 如果增加新方法,代理类必须修改
// 3. 硬编码 → 增强逻辑写死在代理类里
6.1.2 动态代理(自动生成代理类)

JDK动态代理(目标必须实现接口)

java 复制代码
// 1. 接口和实现类(同上)
public interface HouseService { void rentHouse(); }
public class RealHouseService implements HouseService { 
    public void rentHouse() { System.out.println("房东出租房子"); }
}

// 2. 实现InvocationHandler(代理逻辑处理器)
import java.lang.reflect.InvocationHandler;  // JDK提供的代理处理器接口
import java.lang.reflect.Method;  // 反射的Method类,代表一个方法

public class HouseInvocationHandler implements InvocationHandler {
    
    // 目标对象(被代理的对象)
    private Object target;
    
    public HouseInvocationHandler(Object target) {
        this.target = target;
    }
    
    /**
     * 代理对象调用任何方法时,都会进入这个方法
     * @param proxy 代理对象本身(很少用)
     * @param method 被调用的方法对象(可以获取方法名、参数等)
     * @param args 方法调用时传入的参数数组
     * @return 方法执行结果
     * @throws Throwable 可能抛出的异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("中介:开始服务");  // 前置增强
        
        // 通过反射调用目标方法
        // method.invoke() = 调用target对象的method方法,参数是args
        Object result = method.invoke(target, args);
        
        System.out.println("中介:服务结束");  // 后置增强
        
        return result;
    }
}

// 3. 创建代理对象
import java.lang.reflect.Proxy;  // JDK的代理工具类

public class JdkProxyDemo {
    public static void main(String[] args) {
        // 创建真实对象
        HouseService target = new RealHouseService();
        
        // 创建代理对象
        // Proxy.newProxyInstance参数:
        // 1. classLoader:用目标类的类加载器
        // 2. interfaces:代理要实现哪些接口(这里实现HouseService接口)
        // 3. invocationHandler:代理逻辑处理器
        HouseService proxy = (HouseService) Proxy.newProxyInstance(
            target.getClass().getClassLoader(),  // 类加载器:加载代理类到JVM
            new Class[]{HouseService.class},  // 实现的接口数组
            new HouseInvocationHandler(target)  // 代理逻辑处理器
        );
        
        // 调用代理对象的方法
        proxy.rentHouse();  // 会自动进入invoke()方法
        
        // 输出:
        // 中介:开始服务
        // 房东出租房子
        // 中介:服务结束
    }
}

// 代码如何体现动态代理的优势?
// 1. 无需为每个类写代理类 → 代码中Proxy.newProxyInstance()自动生成代理
// 2. 通用性强 → 代码中target可以是任何实现了接口的对象
// 3. 灵活 → 代码中InvocationHandler可以复用给多个目标类

CGLIB动态代理(目标不需要实现接口)

java 复制代码
// 1. 真实类(没有实现接口)
public class RealHouseService {
    public void rentHouse() {
        System.out.println("房东出租房子(无接口)");
    }
}

// 2. 实现MethodInterceptor(方法拦截器)
import org.springframework.cglib.proxy.MethodInterceptor;  // CGLIB的方法拦截器接口
import org.springframework.cglib.proxy.MethodProxy;  // CGLIB的方法代理(比JDK的Method性能高)

public class HouseMethodInterceptor implements MethodInterceptor {
    
    private Object target;  // 目标对象
    
    public HouseMethodInterceptor(Object target) {
        this.target = target;
    }
    
    /**
     * 拦截目标方法调用
     * @param obj 代理对象本身
     * @param method 被调用的方法对象
     * @param args 方法参数
     * @param proxy CGLIB的方法代理对象(用于调用父类方法)
     * @return 方法执行结果
     * @throws Throwable
     */
    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("中介:开始服务(CGLIB)");
        
        // 调用父类(目标类)的方法
        // proxy.invokeSuper():调用被代理类的父类方法(即目标方法)
        // 比JDK的method.invoke()性能高,因为直接操作字节码
        Object result = proxy.invokeSuper(target, args);
        
        System.out.println("中介:服务结束(CGLIB)");
        
        return result;
    }
}

// 3. 创建代理对象
import org.springframework.cglib.proxy.Enhancer;  // CGLIB的增强器

public class CglibProxyDemo {
    public static void main(String[] args) {
        // 创建真实对象
        RealHouseService target = new RealHouseService();
        
        // 创建代理对象
        // Enhancer.create参数:
        // 1. 目标类的Class对象
        // 2. 方法拦截器
        RealHouseService proxy = (RealHouseService) Enhancer.create(
            target.getClass(),  // 目标类
            new HouseMethodInterceptor(target)  // 拦截器
        );
        
        // 调用代理对象
        proxy.rentHouse();
        
        // 输出:
        // 中介:开始服务(CGLIB)
        // 房东出租房子(无接口)
        // 中介:服务结束(CGLIB)
    }
}

// 代码如何体现CGLIB的特点?
// 1. 无需接口 → 代码中RealHouseService直接是类
// 2. 生成子类 → 代码中Enhancer.create()会生成RealHouseService的子类
// 3. 性能更高 → 代码中MethodProxy.invokeSuper()比反射快

6.2 Spring AOP如何选择代理方式?(源码逻辑)

java 复制代码
// Spring AOP代理选择规则(通过配置控制)

// 情况1:目标类实现了接口(默认使用JDK代理)
@Service
public class UserServiceImpl implements UserService { 
    // 实现了UserService接口
    // Spring默认用JDK代理
}

// 情况2:目标类没实现接口(只能用CGLIB)
@Service
public class ProductService { 
    // 没有实现接口
    // Spring强制用CGLIB代理
}

// 情况3:强制使用CGLIB(配置)
@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 关键配置!
public class MyApplication {
    // proxyTargetClass = true:强制使用CGLIB代理
    // 即使实现了接口,也用CGLIB
}

// 代码如何体现选择逻辑?
// 1. 没有配置 → 代码中Spring自动判断(有接口就用JDK,无接口用CGLIB)
// 2. 配置proxyTargetClass=true → 代码中所有类都用CGLIB代理
// 3. 为什么需要强制? → 代码中CGLIB可以代理类的方法,JDK只能代理接口方法

Spring AOP代理选择源码简化版

java 复制代码
// 这是Spring APO选择代理的核心逻辑(伪代码)
public class ProxyFactory {
    
    public AopProxy createAopProxy(Object target) {
        // 1. 如果配置了强制使用CGLIB
        if (proxyTargetClass) {
            return new CglibProxy(target);
        }
        
        // 2. 如果目标实现了接口
        if (target.getClass().getInterfaces().length > 0) {
            return new JdkProxy(target);  // 使用JDK动态代理
        }
        
        // 3. 否则使用CGLIB
        return new CglibProxy(target);
    }
}

// 代码如何体现选择逻辑?
// 1. 优先检查配置 → 代码中if(proxyTargetClass)优先判断
// 2. 次选接口 → 代码中else if检查getInterfaces()
// 3. 最后保底 → 代码中return new CglibProxy()

6.3 代理执行流程(方法调用时发生了什么)

java 复制代码
// 当你调用被代理的方法时,实际执行流程:

// 假设有:
@RestController
public class BookController {
    @RequestMapping("/add")
    public String add() { return "ok"; }
}

// 调用:bookController.add() 时

// ============ JDK代理执行流程 ============
// 1. 你调用的是**代理对象**的add()方法
// 2. 自动进入JdkDynamicAopProxy.invoke()方法
public Object invoke(Object proxy, Method method, Object[] args) {
    // 3. 获取所有增强器(通知)
    List<Advisor> advisors = getAdvisors();
    
    // 4. 创建MethodInvocation(方法调用链)
    MethodInvocation invocation = new ReflectiveMethodInvocation(
        proxy, target, method, args, targetClass, advisors
    );
    
    // 5. 执行拦截器链(依次执行所有通知)
    return invocation.proceed();  // 关键!所有通知在这里执行
}

// ============ CGLIB代理执行流程 ============
// 1. 你调用的是**代理子类**的add()方法
// 2. 自动进入CglibAopProxy.intercept()方法
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) {
    // 3. 获取所有增强器
    List<Advisor> advisors = getAdvisors();
    
    // 4. 创建MethodInvocation
    MethodInvocation invocation = new CglibMethodInvocation(
        proxy, target, method, args, targetClass, advisors, methodProxy
    );
    
    // 5. 执行拦截器链
    return invocation.proceed();
}

// 代码如何体现执行流程?
// 1. 代理对象 → 代码中proxy不是原始对象,而是Proxy.newProxyInstance()创建的
// 2. 自动拦截 → 代码中invoke()/intercept()由JDK/CGLIB自动调用
// 3. 拦截器链 → 代码中invocation.proceed()会遍历所有通知并执行

7. 完整项目实战(整合所有知识点)

7.1 项目结构

java 复制代码
com.example.demo
├── annotation
│   └── LogExecutionTime.java       // 自定义注解
├── aspect
│   ├── LogAspect.java              // 日志切面
│   ├── TimeAspect.java             // 耗时切面
│   └── SecurityAspect.java         // 安全切面
├── controller
│   ├── UserController.java         // 用户接口
│   └── OrderController.java        // 订单接口
└── DemoApplication.java            // 启动类

7.2 完整代码(带全套注释)

自定义注解

java 复制代码
// annotation/LogExecutionTime.java
import java.lang.annotation.ElementType;  // 注解作用目标
import java.lang.annotation.Retention;  // 注解保留策略
import java.lang.annotation.RetentionPolicy;  // 运行时保留
import java.lang.annotation.Target;  // 目标为METHOD

// 这个注解用于标记需要记录日志的方法
@Target(ElementType.METHOD)  // 只能用在方法上
@Retention(RetentionPolicy.RUNTIME)  // 运行时通过反射读取
public @interface LogExecutionTime {
    String value() default "";  // 可以添加描述信息
}

日志切面

java 复制代码
// aspect/LogAspect.java
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;  // 连接点信息
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;  // 方法签名
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;  // 顺序注解
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
@Order(1)  // 优先级最高,最先执行
public class LogAspect {
    
    // 定义切点:controller包下所有方法
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void controllerPointcut() {}
    
    // 定义切点:带有@LogExecutionTime注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.LogExecutionTime)")
    public void annotationPointcut() {}
    
    // 前置通知:记录方法开始
    @Before("controllerPointcut()")
    public void logBefore(JoinPoint jp) {
        Signature signature = jp.getSignature();  // 获取方法签名
        String className = jp.getTarget().getClass().getSimpleName();  // 获取类名
        String methodName = signature.getName();  // 获取方法名
        log.info("【日志】{}.{} 开始执行", className, methodName);
    }
    
    // 返回后通知:记录方法成功返回
    @AfterReturning(value = "controllerPointcut()", returning = "result")
    public void logAfterReturning(JoinPoint jp, Object result) {
        log.info("【日志】{} 返回结果: {}", jp.getSignature().getName(), result);
    }
    
    // 异常后通知:记录方法异常
    @AfterThrowing(value = "controllerPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint jp, Exception e) {
        log.error("【日志】{} 发生异常: {}", jp.getSignature().getName(), e.getMessage());
    }
}

耗时切面

java 复制代码
// aspect/TimeAspect.java
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
@Order(2)  // 优先级次之,在LogAspect之后执行
public class TimeAspect {
    
    // 环绕通知:计算耗时
    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object calculateTime(ProceedingJoinPoint pjp) throws Throwable {
        Signature signature = pjp.getSignature();
        String methodName = signature.getName();
        
        long startTime = System.currentTimeMillis();
        log.info("【计时】{} 开始", methodName);
        
        Object result = pjp.proceed();  // 执行目标方法
        
        long endTime = System.currentTimeMillis();
        long costTime = endTime - startTime;
        
        log.info("【计时】{} 结束,耗时: {}ms", methodName, costTime);
        
        return result;
    }
}

安全切面

java 复制代码
// aspect/SecurityAspect.java
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@Aspect
@Order(3)  // 优先级最低,最后执行
public class SecurityAspect {
    
    // 前置通知:权限检查
    @Before("@annotation(com.example.demo.annotation.LogExecutionTime)")
    public void checkPermission(JoinPoint jp) {
        Signature signature = jp.getSignature();
        String methodName = signature.getName();
        
        // 模拟权限检查
        log.info("【安全】检查 {} 的权限", methodName);
        // 实际应用:从Session获取用户角色,检查是否有权限
        // if (!hasPermission()) { throw new SecurityException("无权限"); }
    }
}

Controller

java 复制代码
// controller/UserController.java
import com.example.demo.annotation.LogExecutionTime;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
    
    @GetMapping("/add")
    @LogExecutionTime  // 添加注解,会被安全切面检查
    public String addUser() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "用户添加成功";
    }
    
    @GetMapping("/delete")
    public String deleteUser() {
        return "用户删除成功";
    }
}

// controller/OrderController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {
    
    @GetMapping("/create")
    public String createOrder() {
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return "订单创建成功";
    }
}

启动类

java 复制代码
// DemoApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)  // 启用CGLIB代理,强制使用CGLIB
public class DemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        // 启动后可以用context获取Bean进行测试
    }
}

7.3 测试结果分析(结论与代码对应)

测试1:访问 /user/add

java 复制代码
控制台输出顺序:
1. 【日志】UserController.addUser 开始执行        ← LogAspect前置通知
2. 【计时】addUser 开始                      ← TimeAspect环绕通知-前
3. 【安全】检查 addUser 的权限                ← SecurityAspect前置通知(注解匹配)
4. 【计时】addUser 结束,耗时: 105ms         ← TimeAspect环绕通知-后
5. 【日志】addUser 返回结果: 用户添加成功      ← LogAspect返回后通知

结论对应代码:
1. @Order(1)优先执行 → LogAspect的@Before先打印
2. @Order(2)次之 → TimeAspect的@Around在@Before之后执行
3. @annotation精确匹配 → SecurityAspect只对@LogExecutionTime生效
4. 环绕通知包裹 → TimeAspect的计时包含了SecurityAspect的权限检查时间
5. 返回后通知 → LogAspect的@AfterReturning拿到返回值

测试2:访问 /user/delete

java 复制代码
控制台输出:
1. 【日志】UserController.deleteUser 开始执行
2. 【计时】deleteUser 开始
3. 【计时】deleteUser 结束,耗时: 1ms
4. 【日志】deleteUser 返回结果: 用户删除成功

结论对应代码:
- 没有【安全】日志 → deleteUser()没有@LogExecutionTime注解
- SecurityAspect的切点只匹配注解,所以不生效

测试3:访问 /order/create

复制代码
控制台输出:
1. 【日志】OrderController.createOrder 开始执行
2. 【计时】createOrder 开始
3. 【计时】createOrder 结束,耗时: 202ms
4. 【日志】createOrder 返回结果: 订单创建成功

结论对应代码:
- execution(* controller.*.*(..))匹配所有controller
- LogAspect、TimeAspect对UserController和OrderController都生效
- SecurityAspect只对添加了@LogExecutionTime的方法生效

8. 常见问题与解决方案(代码级避坑)

8.1 同一个类内方法调用,AOP失效

java 复制代码
@Service
public class UserService {
    
    public void methodA() {
        System.out.println("执行methodA");
        methodB();  // 直接调用同类方法,不会触发AOP!
    }
    
    @LogExecutionTime  // 期望被增强
    public void methodB() {
        System.out.println("执行methodB");
    }
}

// 问题代码分析:
// methodA()调用methodB()时,调用的是this.methodB(),this是原始对象,不是代理对象
// Spring AOP只对代理对象的方法调用生效

// 解决方案1:注入自己(获取代理对象)
@Service
public class UserService {
    
    @Autowired
    private UserService self;  // 注入Spring管理的代理对象
    
    public void methodA() {
        System.out.println("执行methodA");
        self.methodB();  // 通过代理对象调用,AOP生效!
    }
    
    @LogExecutionTime
    public void methodB() {
        System.out.println("执行methodB");
    }
}

// 解决方案2:使用AopContext(需要配置expose-proxy=true)
@Service
public class UserService {
    
    public void methodA() {
        System.out.println("执行methodA");
        ((UserService) AopContext.currentProxy()).methodB();  // 获取当前代理对象
    }
    
    @LogExecutionTime
    public void methodB() {
        System.out.println("执行methodB");
    }
}

// 配置类
@EnableAspectJAutoProxy(exposeProxy = true)  // 暴露代理对象到AopContext

8.2 通知中抛出异常,导致原方法无法执行

java 复制代码
@Around("execution(* com.example.demo.service.*.*(..))")
public Object badAdvice(ProceedingJoinPoint pjp) throws Throwable {
    // 错误示例1:忘记调用pjp.proceed()
    System.out.println("前置逻辑");
    // 没有pjp.proceed(),原方法不会被执行!
    return null;  // 调用方拿到null,可能引发NPE
    
    // 错误示例2:吞掉异常
    try {
        return pjp.proceed();
    } catch (Exception e) {
        System.out.println("出错了");
        // 没有throw,异常被吞掉,调用方以为成功了!
        return null;
    }
    
    // 正确做法
    try {
        return pjp.proceed();
    } catch (Throwable e) {
        System.out.println("出错了: " + e.getMessage());
        throw e;  // 必须抛出,让调用方感知异常
    }
}

// 代码如何体现问题?
// 1. 忘记proceed() → 代码中目标方法根本没执行
// 2. 吞掉异常 → 代码中catch后没有throw,异常链断裂
// 3. 正确做法 → 代码中catch后必须throw,保持异常传播

8.3 切点表达式过于宽泛,影响性能

java 复制代码
// 错误示例:匹配所有方法
@Around("execution(* *.*(..))")  // 危险!会匹配所有类,包括Spring内置的Bean
public Object allMethods(ProceedingJoinPoint pjp) throws Throwable {
    // 这会拦截Spring自己的方法,如Bean初始化、AOP代理创建等
    // 导致:启动慢、性能差、不可预知的错误
    return pjp.proceed();
}

// 正确示例:精确匹配
@Around("execution(* com.example.demo.controller.*.*(..)) && " +
        "!execution(* com.example.demo.controller.HealthController.*(..))")  // 排除健康检查
public Object preciseMethods(ProceedingJoinPoint pjp) throws Throwable {
    return pjp.proceed();
}

// 代码如何体现性能问题?
// 1. 宽泛表达式 → 代码中*.*会匹配到Spring内部Bean的方法
// 2. 精确表达式 → 代码中明确指定包路径,并用!排除不需要的类
// 3. 启动时间 → 宽泛表达式会让Spring启动时处理大量不必要的代理

9. 总结(知识地图)

9.1 核心概念关系图

html 复制代码
┌─────────────────────────────────────────────────────────────┐
│                        切面 (Aspect)                        │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                      切点 (Pointcut)                    │ │
│  │  execution(* com.example.controller.*(..))              │ │
│  │  @annotation(com.example.annotation.Log)                │ │
│  └─────────────────────────────────────────────────────────┘ │
│                             +                                │
│                             |                                │
│                             |  匹配                          │
│                             v                                │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                    连接点 (Join Point)                   │ │
│  │  UserController.addUser()                               │ │
│  │  OrderController.createOrder()                          │ │
│  └─────────────────────────────────────────────────────────┘ │
│                             +                                │
│                             |                                │
│                             |  应用                          │
│                             v                                │
│  ┌─────────────────────────────────────────────────────────┐ │
│  │                      通知 (Advice)                       │ │
│  │  @Before: 前置逻辑                                      │ │
│  │  @Around: 环绕逻辑                                      │ │
│  │  @After: 后置逻辑                                       │ │
│  └─────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

9.2 代理方式选择决策树

java 复制代码
目标类是否实现了接口?
  ├─ 是 → 是否配置proxyTargetClass=true?
  │        ├─ 是 → 使用CGLIB代理
  │        └─ 否 → 使用JDK动态代理
  └─ 否 → 使用CGLIB代理(强制)

9.3 最佳实践清单

|----------|-----------------------|----------------------------------------------------|
| 场景 | 推荐做法 | 代码示例 |
| 精确匹配 | 使用@annotation或精确包路径 | @Around("@annotation(Log)") |
| 多个切面 | 用@Order控制顺序 | @Order(1) |
| 代理选择 | 默认即可,有特殊需求再配置CGLIB | @EnableAspectJAutoProxy(proxyTargetClass = true) |
| 内部调用 | 注入自己或使用AopContext | self.methodB() |
| 异常处理 | 环绕通知中必须throw异常 | catch(Throwable e) { throw e; } |

10. 最后的叮嘱(给新手的建议)

  1. 先跑起来 :不要纠结细节,先把第一个@Around例子跑通

  2. 再理解概念:对照代码和日志,理解切点、连接点、通知的关系

  3. 后玩复杂:熟练后再尝试多个切面、@annotation、@Order等高级玩法

  4. 看日志:日志是理解AOP执行顺序的最好工具

  5. 调试:在通知方法里打断点,看调用栈,能清晰看到代理链路

记住:AOP就像给代码穿外套,不穿外套的人(原始类)完全不知道外套的存在,但外套确实提供了额外的功能(日志、计时、安全)。这就是无侵入式编程的魅力!

恭喜你完成了Spring AOP的深度学习! 现在你应该能:

  • 写出带详细注释的切面代码

  • 解释每个注解的作用

  • 通过日志验证执行顺序

  • 选择合适的代理方式

  • 避开了常见的坑

如果还有疑问,随时回来翻代码和注释,它们是最诚实的老师!

Spring Boot 3.x AOP 完整实战代码包

这是一个可以直接运行的完整 Spring Boot 代码示例,展示了如何使用 Spring AOP(面向切面编程)实现日志记录方法耗时统计权限检查(模拟)。

项目特性:

  • 基于 Spring Boot 3.2.5 + JDK 17

  • 包含 AOP 切面 (@Aspect) 与 Lombok

  • 零数据库依赖(纯控制台日志演示)

  • 包含完整的 pom.xmlapplication.yml

快速开始

  1. 创建一个新的 Maven 项目。

  2. 将下方的文件复制到对应的目录中。

  3. Maven Reload 加载依赖。

  4. 运行 DemoApplication 启动类。

  5. 访问测试接口查看控制台日志。

一、Maven 坐标 (pom.xml)

位于项目根目录。

XML 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0)"
         xmlns:xsi="[http://www.w3.org/2001/XMLSchema-instance](http://www.w3.org/2001/XMLSchema-instance)"
         xsi:schemaLocation="[http://maven.apache.org/POM/4.0.0](http://maven.apache.org/POM/4.0.0)
                             [https://maven.apache.org/xsd/maven-4.0.0.xsd](https://maven.apache.org/xsd/maven-4.0.0.xsd)">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>demo-aop-complete</artifactId>
    <version>1.0.0</version>
    <name>demo-aop-complete</name>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <!-- Web 模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- AOP 切面模块 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- Lombok 工具库 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

二、配置文件 (application.yml)

位于 src/main/resources 目录。

java 复制代码
server:
  port: 8080

spring:
  application:
    name: demo-aop-complete

# 只打日志,不配数据库
logging:
  level:
    com.example.demo: info
  pattern:
    console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

三、Java 源码

所有代码均位于 com.example.demo 包及其子包下。

1. 启动类 (DemoApplication.java)

路径: src/main/java/com/example/demo/DemoApplication.java

java 复制代码
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@SpringBootApplication
@EnableAspectJAutoProxy(proxyTargetClass = true)   // 强制使用 CGLIB 代理
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

2. 自定义注解 (LogExecutionTime.java)

路径: src/main/java/com/example/demo/annotation/LogExecutionTime.java

java 复制代码
package com.example.demo.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 标记需要记录日志/检查权限的方法
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
    String value() default "";
}

3. 切面类 (Aspects)

路径: src/main/java/com/example/demo/aspect/

3.1 日志切面 (LogAspect.java)
java 复制代码
package com.example.demo.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
@Order(1) // 切面执行顺序:数字越小越先执行 Before
public class LogAspect {

    // 定义切点:扫描 controller 包下所有方法
    @Pointcut("execution(* com.example.demo.controller.*.*(..))")
    public void controllerPt() {}

    @Before("controllerPt()")
    public void logBefore(JoinPoint jp) {
        String className = jp.getTarget().getClass().getSimpleName();
        String methodName = jp.getSignature().getName();
        log.info("【LogAspect-前置】{}.{} 开始", className, methodName);
    }

    @AfterReturning(value = "controllerPt()", returning = "ret")
    public void logAfterReturning(JoinPoint jp, Object ret) {
        log.info("【LogAspect-返回】{} 返回值: {}", jp.getSignature().getName(), ret);
    }

    @AfterThrowing(value = "controllerPt()", throwing = "e")
    public void logAfterThrowing(JoinPoint jp, Exception e) {
        log.error("【LogAspect-异常】{} 异常信息: {}", jp.getSignature().getName(), e.getMessage());
    }
}
3.2 耗时统计切面 (TimeAspect.java)
java 复制代码
package com.example.demo.aspect;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
@Order(2)
public class TimeAspect {

    @Around("execution(* com.example.demo.controller.*.*(..))")
    public Object calculateTime(ProceedingJoinPoint pjp) throws Throwable {
        Signature signature = pjp.getSignature();
        String methodName = signature.getName();

        long start = System.currentTimeMillis();
        log.info("【TimeAspect-环绕前】{} 开始", methodName);

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

        long cost = System.currentTimeMillis() - start;
        log.info("【TimeAspect-环绕后】{} 结束,耗时: {} ms", methodName, cost);
        return result;
    }
}
3.3 安全/权限切面 (SecurityAspect.java)
java 复制代码
package com.example.demo.aspect;

import com.example.demo.annotation.LogExecutionTime;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

@Aspect
@Component
@Slf4j
@Order(3)
public class SecurityAspect {

    // 只拦截带有 @LogExecutionTime 注解的方法
    @Before("@annotation(logAnnotation)")
    public void checkPermission(JoinPoint jp, LogExecutionTime logAnnotation) {
        String methodName = jp.getSignature().getName();
        log.info("【SecurityAspect-注解前置】方法: {} , 注解value: {}", methodName, logAnnotation.value());
        // 实际开发中可在此处获取 Token 或 Session 进行鉴权
    }
}

4. 控制层 (Controllers)

路径: src/main/java/com/example/demo/controller/

4.1 用户控制器 (UserController.java)
java 复制代码
package com.example.demo.controller;

import com.example.demo.annotation.LogExecutionTime;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/add")
    @LogExecutionTime("新增用户") // 触发 SecurityAspect
    public String addUser() throws InterruptedException {
        Thread.sleep(100); // 模拟业务耗时
        return "用户添加成功";
    }

    @GetMapping("/delete")
    public String deleteUser() {
        return "用户删除成功";
    }
}
4.2 订单控制器 (OrderController.java)
java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping("/create")
    public String createOrder() throws InterruptedException {
        Thread.sleep(200);
        return "订单创建成功";
    }
}
4.3 测试控制器 (HelloController.java)
java 复制代码
package com.example.demo.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello aop";
    }
}

四、验证结果

项目启动后,通过浏览器或 Postman 访问以下链接,观察 IDE 控制台输出。

测试用例

  1. http://localhost:8080/hello

    • 预期 : 触发 LogAspectTimeAspect,但 触发 SecurityAspect (因为没有加注解)。
  2. http://localhost:8080/user/add

    • 预期: 触发所有三个切面(Log, Time, Security)。
  3. http://localhost:8080/order/create

    • 预期 : 触发 LogAspectTimeAspect

示例日志输出 (/user/add)

java 复制代码
14:53:12.121 [http-nio-8080-exec-1] INFO  c.e.d.aspect.LogAspect - 【LogAspect-前置】UserController.addUser 开始
14:53:12.122 [http-nio-8080-exec-1] INFO  c.e.d.aspect.TimeAspect - 【TimeAspect-环绕前】addUser 开始
14:53:12.123 [http-nio-8080-exec-1] INFO  c.e.d.aspect.SecurityAspect - 【SecurityAspect-注解前置】方法: addUser , 注解value: 新增用户
14:53:12.225 [http-nio-8080-exec-1] INFO  c.e.d.aspect.TimeAspect - 【TimeAspect-环绕后】addUser 结束,耗时: 103 ms
14:53:12.226 [http-nio-8080-exec-1] INFO  c.e.d.aspect.LogAspect - 【LogAspect-返回】addUser 返回值: 用户添加成功

Spring AOP 实战回顾:你其实已经掌握了什么?

你已经把项目跑通了,控制台也打出了整齐的切面日志------这一步看起来"只是跑了个 Demo",但其实你已经一次性把 Spring AOP 的完整生命周期 都体验了一遍。

下面给你一份"战利品清单",告诉你 从这段输出里到底能提炼出什么干货 ,以及 下一步可以怎么玩

一、从日志里能直接验证的 6 个核心结论

|-----------------------------|-------------------------|-------------------------------------------------|
| 日志片段 | 对应 AOP 概念 | 你得到的验证 |
| 【LogAspect-前置】... 开始执行 | @Before 通知 | 目标方法执行前 运行,且比 @Around 晚一步(Order 1 vs 2)。 |
| 【TimeAspect-环绕前】... 开始 | @Around 前半 | 环绕通知包裹整个调用链,时间统计包含后续所有切面。 |
| 【SecurityAspect-注解前置】... | @Before + @annotation | 只有标了 @LogExecutionTime 的方法才触发,精确匹配。 |
| 【TimeAspect-环绕后】... 耗时 | @Around 后半 | 拿到原返回值,并可无损追加耗时信息。 |
| 【LogAspect-返回】... 返回值 | @AfterReturning | 目标正常返回时才触发,异常时不打印。 |
| deleteUser 没有 Security 日志 | 切点排除 | 无注解的方法不会@annotation 切面拦截。 |

二、你已经到手的"技能点"

  1. 零配置开启 AOP

    只加 spring-boot-starter-aop + @EnableAspectJAutoProxy 即可,回忆一下你没写任何 XML。

  2. 会写 5 种通知模板

    把 LogAspect、TimeAspect 复制改个类名,就是生产级的日志、监控、权限骨架。

  3. 知道 Order 数字越小越先执行

    下次有"事务 > 日志 > 监控"三级需求时,直接 @Order(1/2/3) 即可,不用再踩顺序坑。

  4. 会用 @annotation 做"方法级开关"

    想给某个接口加限流/缓存/灰度,自定义一个注解 + 一个切面,业务代码零侵入。

  5. 亲眼见到 CGLIB 代理生效

    启动日志里 proxyTargetClass = true 已打开,你调试时可以在 UserController 里断点看到对象是 EnhancerBySpringCGLIB 字样,不再是原始类。

三、下一步可以玩什么(递进路线)

|--------|----------------------------------------------------------------------------|---------------------------|
| 难度 | 玩法 | 目的 |
| ⭐ | 把 TimeAspect 改造成 方法级性能告警 (例如:耗时 > 500 ms 发钉钉消息) | 生产监控 |
| ⭐⭐ | 自定义 @Cache(key="#id") 注解 结合 ConcurrentHashMap 做内存缓存 | 体验"注解即功能" |
| ⭐⭐⭐ | 多切面环境下,用 JoinPointProceedingJoinPoint 传递上下文(如 traceId) | 链路追踪 |
| ⭐⭐⭐⭐ | 引入 spring-boot-starter-validation@Before统一做参数校验 | 替换 Controller 里的 @Valid |
| ⭐⭐⭐⭐⭐ | 把 SecurityAspect 换成 SpEL 表达式 (如 @PreAuthorize("hasRole('ADMIN')")) | 与 Spring Security 打通 |

四、一句话总结

你现在拥有了一套可复制的"切面模板"

  • 日志 → 复制 LogAspect

  • 计时 → 复制 TimeAspect

  • 权限 → 复制 SecurityAspect

改包路径、改切点、改 Order 顺序 → 就能直接搬到生产环境。

恭喜你,AOP 已经从"概念"变成你手里的"工具"了!

相关推荐
XQ丶YTY1 天前
javaee程序设计 中南民族大学 复习
java·程序设计·javaee·期末·复习·速成·中南民族大学
兮山与21 天前
JavaEE初阶11.0
javaee
sugar__salt1 个月前
网络编程套接字(二)——TCP
java·网络·网络协议·tcp/ip·java-ee·javaee
努力小周1 个月前
基于STM32的智能台灯系统设计与实现
stm32·单片机·嵌入式硬件·c#·毕业设计·毕设·javaee
兮山与1 个月前
JavaEE初阶10.0
javaee
兮山与1 个月前
JavaEE初阶8.0
javaee
兮山与1 个月前
JavaEE初阶9.0
javaee
爱学习的小可爱卢1 个月前
JavaEE进阶——SpringMVC响应处理详解
spring boot·postman·javaee
带刺的坐椅1 个月前
Solon 不依赖 Java EE 是其最有价值的设计!
java·spring·web·solon·javaee