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 已经从"概念"变成你手里的"工具"了!

相关推荐
sugar__salt6 天前
网络编程套接字(二)——TCP
java·网络·网络协议·tcp/ip·java-ee·javaee
努力小周7 天前
基于STM32的智能台灯系统设计与实现
stm32·单片机·嵌入式硬件·c#·毕业设计·毕设·javaee
兮山与11 天前
JavaEE初阶10.0
javaee
兮山与11 天前
JavaEE初阶8.0
javaee
兮山与11 天前
JavaEE初阶9.0
javaee
爱学习的小可爱卢14 天前
JavaEE进阶——SpringMVC响应处理详解
spring boot·postman·javaee
带刺的坐椅16 天前
Solon 不依赖 Java EE 是其最有价值的设计!
java·spring·web·solon·javaee
想不明白的过度思考者20 天前
基于 Spring Boot 的 Web 三大核心交互案例精讲
前端·spring boot·后端·交互·javaee
朝新_24 天前
【实战】博客系统:项目公共模块 + 博客列表的实现
数据库·笔记·sql·mybatis·交互·javaee