Spring AOP 注解开发实战:无侵入式功能增强从入门到精通(Spring系列7)

一、AOP 简介

在学习 Spring 时,我们知道 Spring 有两个核心概念:IOC/DIAOP。在系统学习了 IOC/DI 之后,我们来深入学习另一个核心内容------AOP。

AOP 的核心定义是:AOP 是在不改变原有代码的前提下对其进行增强,本文将围绕这句话展开,带大家学习 AOP 核心概念与作用。

1.1 什么是 AOP?

  • AOP (Aspect Oriented Programming):面向切面编程,是一种编程范式,用于指导开发者如何组织程序结构。
  • OOP (Object Oriented Programming):面向对象编程。

OOP 是我们熟悉的编程思想,而 AOP 同样是一种编程思想,二者是不同的编程范式,解决不同维度的问题。

1.2 AOP 作用

AOP 的核心作用是:在不惊动原始设计(代码)的基础上,为其进行功能增强 ,底层通过代理模式实现。

1.3 AOP 核心概念

复制代码
@Repository
public class BookDaoImpl implements BookDao {
    public void save() {
        // 记录程序当前执行时间(开始时间)
        Long startTime = System.currentTimeMillis();
        // 业务执行万次
        for (int i = 0;i<10000;i++) {
            System.out.println("book dao save ...");
        }
        // 记录程序当前执行时间(结束时间)
        Long endTime = System.currentTimeMillis();
        // 计算时间差
        Long totalTime = endTime-startTime;
        // 输出信息
        System.out.println("执行万次消耗时间:" + totalTime + "ms");
    }
    public void update(){
        System.out.println("book dao update ...");
    }
    public void delete(){
        System.out.println("book dao delete ...");
    }
    public void select(){
        System.out.println("book dao select ...");
    }
}

在这个案例中,save() 方法有万次执行耗时统计,而 update()/delete()/select() 没有。我们希望在不修改 update()/delete() 代码的前提下,给它们也加上耗时统计,这就是 AOP 的典型应用场景。

我们来拆解 AOP 核心概念:

概念 定义 Spring AOP 中的理解
连接点 (JoinPoint) 程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等 理解为方法的执行 ,案例中 save()/update()/delete()/select() 都是连接点
切入点 (Pointcut) 匹配连接点的式子 用于描述「需要增强的方法」,一个切入点可以匹配一个或多个方法;切入点是连接点的子集,只有被增强的连接点才是切入点
通知 (Advice) 在切入点处执行的操作,也就是共性功能 案例中「统计万次执行耗时」就是通知,在 Spring AOP 中以方法的形式呈现
通知类 定义通知的类 存放通知方法的类
切面 (Aspect) 描述通知与切入点的对应关系 将通知绑定到切入点,告诉 Spring「哪个通知要增强哪个方法」

二、AOP 入门案例

2.1 需求分析

我们简化需求:使用 Spring AOP 注解方式,在方法执行前打印当前系统时间 ,在不修改 update() 方法的前提下,为其添加打印时间的功能。

2.2 思路分析

  1. 导入依赖(pom.xml)
  2. 制作连接点(原始操作,Dao 接口与实现类)
  3. 制作共性功能(通知类与通知)
  4. 定义切入点
  5. 绑定切入点与通知关系(切面)
  6. 将通知类配给容器并标识其为切面类
  7. 开启注解格式 AOP 功能
  8. 运行程序

2.3 环境准备

  1. 创建 Maven 项目

  2. pom.xml 添加 Spring 依赖

    <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> </dependencies>
  3. 添加 BookDao 接口与 BookDaoImpl 实现类

    public interface BookDao {
    public void save();
    public void update();
    }

    @Repository
    public class BookDaoImpl implements BookDao {
    public void save() {
    System.out.println(System.currentTimeMillis());
    System.out.println("book dao save ...");
    }
    public void update(){
    System.out.println("book dao update ...");
    }
    }

  4. 创建 Spring 配置类

    @Configuration
    @ComponentScan("com.itheima")
    public class SpringConfig {
    }

  5. 编写 App 运行类

    public class App {
    public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
    BookDao bookDao = ctx.getBean(BookDao.class);
    bookDao.save();
    }
    }

2.4 AOP 实现步骤

步骤1:添加依赖(已完成)

步骤2:定义接口与实现类(已完成)

步骤3-6:定义通知、切入点、切面并标识切面类(整合代码)

复制代码
// 让通知类被 Spring 容器扫描并管理
@Component
// 标识当前类为 AOP 切面类,用于承载通知和切入点
@Aspect
public class MyAdvice {

    // 定义切入点:匹配 com.itheima.dao.BookDao 接口的 update() 方法
    // 切入点依托一个无实际业务意义的私有方法 pt(),仅用于承载 @Pointcut 注解
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    // 绑定切入点 pt(),使用 @Before 注解指定:该通知在切入点方法【执行前】运行
    // 通知方法就是抽取的共性功能:打印当前系统时间
    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

步骤7:开启注解格式 AOP 功能

复制代码
@Configuration
@ComponentScan("com.itheima")
@EnableAspectJAutoProxy // 开启注解格式 AOP 功能,支持 @Aspect、@Pointcut 等注解解析
public class SpringConfig {
}

步骤8:运行程序

复制代码
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        bookDao.update();
    }
}

运行结果:

复制代码
1618998676535
book dao update ...

2.5 核心注解详解

注解 类型 位置 作用
@EnableAspectJAutoProxy 配置注解 配置类上方 开启注解格式 AOP 功能
@Aspect 类注解 切面类定义上方 设置当前类为 AOP 切面类
@Pointcut 方法注解 切入点方法定义上方 设置切入点方法,value 为切入点表达式
@Before 方法注解 通知方法定义上方 设置通知在切入点方法执行前运行

三、AOP 工作流程

AOP 基于 Spring 容器管理的 Bean 做增强,完整工作流程从 Spring 加载 Bean 开始:

3.1 AOP 工作流程

流程1:Spring 容器启动

容器启动时,会加载所有需要被增强的类(如 BookDaoImpl)和通知类(如 MyAdvice),此时 Bean 对象尚未创建完成。

流程2:读取所有切面配置中的切入点

复制代码
@Component
@Aspect
public class MyAdvice {
    // 未被使用的切入点,不会被读取
    @Pointcut("execution(void com.itheima.dao.BookDao.save())")
    private void ptx(){}

    // 被使用的切入点,会被读取
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

流程3:初始化 Bean

  • 匹配失败 :创建原始对象,无需增强,直接调用原始方法。
  • 匹配成功 :创建原始对象 + 代理对象,最终运行的是代理对象的方法。

流程4:获取 Bean 执行方法

  • 获取的 Bean 是原始对象:直接调用方法执行。
  • 获取的 Bean 是代理对象:运行原始方法 + 增强的通知。

3.2 验证:容器中是原始对象还是代理对象

我们通过代码验证结论:方法被增强 → 容器中是代理对象;方法不被增强 → 容器中是原始对象本身。

验证步骤 1:不增强时(切入点匹配失败)

修改 MyAdvice 的切入点,让 update() 方法不被匹配:

复制代码
@Component
@Aspect
public class MyAdvice {
    // 切入点改为 update1(),与 BookDao 的 update() 不匹配
    @Pointcut("execution(void com.itheima.dao.BookDao.update1())")
    private void pt(){}

    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

修改 App 类,打印对象类型:

复制代码
public class App {
    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
        BookDao bookDao = ctx.getBean(BookDao.class);
        System.out.println(bookDao);
        System.out.println(bookDao.getClass());
    }
}

运行结果:

复制代码
com.itheima.dao.impl.BookDaoImpl@279fedbd
class com.itheima.dao.impl.BookDaoImpl

此时容器中是原始对象本身

验证步骤 2:增强时(切入点匹配成功)

修改 MyAdvice 的切入点,让 update() 方法被匹配:

复制代码
@Component
@Aspect
public class MyAdvice {
    @Pointcut("execution(void com.itheima.dao.BookDao.update())")
    private void pt(){}

    @Before("pt()")
    public void method(){
        System.out.println(System.currentTimeMillis());
    }
}

运行结果:

复制代码
com.itheima.dao.impl.BookDaoImpl@4e50c791
class com.sun.proxy.$Proxy19

此时容器中是代理对象,验证了我们的结论。

3.3 核心总结

  • Spring AOP 的本质是代理模式,底层通过动态代理实现。
  • 只有被切入点匹配到的方法,才会被创建代理对象并增强;未匹配的方法直接使用原始对象。
  • AOP 实现了「无侵入式增强」,在不修改原始业务代码的前提下,灵活添加共性功能。
相关推荐
她说..3 小时前
Java 注解核心面试题
java·spring boot·spring·spring cloud·自定义注解
gelald3 小时前
Spring - 循环依赖
java·后端·spring
小旭95274 小时前
Spring Data Redis 从入门到实战:简化 Redis 操作全解析
java·开发语言·spring boot·redis·spring
希望永不加班4 小时前
SpringBoot 多数据源配置(读写分离基础)
java·spring boot·后端·spring
Java成神之路-4 小时前
Spring AOP 核心进阶:切入点表达式 + 通知类型 + 环绕通知避坑指南(Spring系列8)
java·后端·spring
廋到被风吹走5 小时前
【AI】Codex + 后端框架实战:Spring/Express/Django 业务逻辑全自动生成
人工智能·spring·express
计算机学姐6 小时前
基于SpringBoot的宠物店管理系统
java·vue.js·spring boot·后端·spring·java-ee·intellij-idea
无心水6 小时前
22、Java开发避坑指南:日期时间、Spring核心与接口设计的最佳实践
java·开发语言·后端·python·spring·java.time·java时间处理
REDcker6 小时前
C++ 多线程内存模型与 memory_order 详解
java·c++·spring