Spring AOP

Spring AOP

代理模式

代理模式是 GoF 提出的 23 种设计模式中最为经典的模式之一,属于对象的结构模式。它的核心思想是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。简单来说,代理对象可以承担比原对象更多的职责,当需要为原对象添加横切关注功能时,代理对象就能发挥作用。

生活中也有很多代理模式的应用场景,比如我们打开 Office 系列的 Word 文档时,若文档包含插图,刚加载文档时,插图仅以虚框占位符的形式呈现,直到用户翻到对应页面需要查看该图片时,才会真正加载图片。这里代替真正图片的虚框就是一个典型的虚拟代理。

代理模式的核心流程

代理模式的实现需要经过两个关键步骤,结合以下结构示意图可清晰理解:

客户端 -> 代理对象 -> 目标对象
  1. 代理对象和真实对象(目标对象)建立代理关系;
  2. 实现代理对象的代理逻辑方法。

代理模式的分类

代理可分为静态代理和动态代理,以下通过“找枪手代考”的示例详细演示两种代理模式的使用。

静态代理

静态代理的核心是代理类和目标类实现相同的接口,通过代理类封装目标类的逻辑并扩展额外功能。

步骤 1:定义抽象主题角色(接口)

抽象主题角色声明了真实主题和代理主题的共同接口,确保在任何可使用真实主题的地方都能使用代理主题。

/**
 * 参考人员接口(抽象主题角色)
 */
public interface Candidate {
    /**
     * 答题
     */
    public void answerTheQuestions();
}
步骤 2:定义真实主题角色(目标类)

真实主题角色是代理角色所代表的真实对象,实现了抽象主题接口的核心功能。

/**
 * 学渣(真实主题角色)
 */
public class SlackerStudent implements Candidate {
    private String name;        // 姓名

    public SlackerStudent(String name) {
        this.name = name;
    }

    @Override
    public void answerTheQuestions() {
        // 学渣只能写出自己的名字不会答题
        System.out.println("姓名: " + name);
    }
}
步骤 3:定义代理主题角色(代理类)

代理主题角色内部包含对真实主题的引用,提供与真实主题相同的接口,可在调用真实主题方法前后执行额外操作,而非单纯传递调用。

/**
 * 枪手(代理主题角色)
 */
public class Gunman implements Candidate {
    private Candidate target;   // 被代理对象

    public Gunman(Candidate target) {
        this.target = target;
    }

    @Override
    public void answerTheQuestions() {
        // 枪手要写上代考的学生的姓名(调用真实对象方法)
        target.answerTheQuestions();
        // 枪手帮助懒学生答题并交卷(扩展功能)
        System.out.println("奋笔疾书正确答案");
        System.out.println("交卷");
    }
}
步骤 4:测试静态代理
public class ProxyTest{
    public static void main(String[] args) {
        // 创建代理对象,传入真实对象(学渣peppa)
        var c = new Gunman(new SlackerStudent ("peppa"));
        // 调用代理对象方法,间接执行真实对象逻辑并扩展功能
        c.answerTheQuestions();
    }
}
静态代理的优缺点
  • 优点:实现客户端与目标类的解耦,客户端无需知道目标类的具体实现,只需与代理类交互。
  • 缺点
    1. 代码冗余:代理类和目标类实现相同接口,若接口新增方法,所有实现类和代理类都需同步实现,增加维护成本;
    2. 灵活性差:代理类仅服务于一种类型的对象,若需代理多种类型对象,需为每种对象单独创建代理类,无法适应大规模程序。

动态代理

从 JDK 1.3 开始,Java 提供了动态代理技术,允许开发者在运行时动态创建接口的代理实例,无需手动编写代理类。Java 中常用的动态代理技术有两种:JDK 动态代理(JDK 自带)和 CGLib 动态代理(第三方技术)。Spring 常用这两种技术,MyBatis 还额外使用了 Javassist 技术,它们的核心理念一致。例如 MyBatis 中的 Mapper 接口无实现类,其内部功能就是通过动态代理实现的。

JDK 动态代理

JDK 动态代理的核心是实现 java.lang.reflect.InvocationHandler 接口,通过 Proxy.newProxyInstance() 方法在运行时生成代理对象。注意:JDK 动态代理要求目标类必须实现接口。

步骤 1:实现 InvocationHandler 接口(代理逻辑类)
public class JDKProxyFactory implements InvocationHandler {
    // 目标对象
    private Object target;

    /**
     * 建立代理对象和目标对象的代理关系,并返回代理对象
     * @param target 目标对象
     * @return 代理对象
     */
    public Object createTargetProxyInstance(Object target) {
        this.target = target;
        /*
         * 第1个参数:使用加载目标对象的类加载器加载代理对象(采用 target 本身的类加载器)
         * 第2个参数:将生成的动态代理对象下挂到目标对象实现的所有接口下
         * 第3个参数:实现方法逻辑的代理类(当前对象,需实现 InvocationHandler 的 invoke 方法)
         */
        return Proxy.newProxyInstance(
                this.target.getClass().getClassLoader(),
                this.target.getClass().getInterfaces(),
                this
        );
    }

    /**
     * 代理对象对目标对象拦截后进行的代理逻辑回调
     * @param proxy 代理对象
     * @param method 当前调度方法(拦截的方法)
     * @param args 当前方法参数
     * @return 代理结果返回
     * @throws Throwable 异常
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object ret = null;
        System.out.println("进入代理逻辑方法,在调度真实对象之前的服务,比如记录日志");
        // 调用目标对象的方法
        ret = method.invoke(target, args);
        System.out.println("调度真实对象之后的服务");
        return ret;
    }
}
CGLib 动态代理

CGLib 采用底层字节码生成技术,通过为目标类创建子类的方式生成代理对象,弥补了 JDK 动态代理“目标类必须实现接口”的局限性。Spring 中对于未实现接口的类,会使用 CGLib 动态代理。

步骤 1:实现 MethodInterceptor 接口(代理逻辑类)
public class CGLibProxyFactory implements MethodInterceptor {
    // 目标对象
    private Object target;

    /**
     * 生成 CGLib 代理对象
     * @param target 目标对象
     * @return 目标对象的 CGLib 代理对象
     */
    public Object createProxyInstance(Object target) {
        this.target = target;
        // 增强类对象(原理:通过继承目标对象创建代理对象)
        var enhancer = new Enhancer();
        // 设置增强类型(指定父类为目标对象的类)
        enhancer.setSuperclass(target.getClass());
        // 定义代理逻辑对象为当前对象(需实现 MethodInterceptor 接口)
        enhancer.setCallback(this);
        // 生成并返回代理对象
        return enhancer.create();
    }

    /**
     * 代理对象对目标对象的方法拦截后进行的代理逻辑回调
     * @param proxy 代理对象
     * @param method 方法
     * @param args 方法参数
     * @param methodProxy 方法代理
     * @return 代理逻辑返回
     * @throws Throwable 异常
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object ret = null;
        // 环绕通知(可通过权限判断决定是否调用目标方法)
        try {
            System.out.println("开启事务"); // 前置通知
            // 反射调用真实方法
            ret = method.invoke(target, args);
            System.out.println("提交事务"); // 后置通知
        } catch (Exception e) {
            System.out.println("回滚事务"); // 例外通知
        } finally {
            System.out.println("释放资源,最终操作"); // 最终通知
        }
        return ret;
    }
}
注意事项

JDK 8 中反射相关功能在 JDK 9 及以上版本被限制,为兼容旧版本,运行项目时需添加以下 JVM 参数:

--add-opens java.base/java.lang=ALL-UNNAMED

Spring AOP 核心概念

要熟练使用 Spring AOP,需先理解其核心概念,各概念间的关系可概括为:切面 = 切点 + 增强(引介),织入是将切面应用到目标对象的过程。

概念 定义与说明
连接点 程序执行的特定位置(如方法调用前、调用后、抛出异常后)。Spring 仅支持方法级别的连接点。
切点 连接点的筛选条件(类似数据库查询条件),一个切点可匹配多个连接点。Spring AOP 的规则解析引擎通过切点条件找到对应的连接点。
增强 织入到目标类连接点上的一段程序代码(即横切逻辑)。Spring 提供的增强接口带方位名,如 BeforeAdvice(前置增强)、AfterReturningAdvice(后置增强)等。部分资料将“增强”译为“通知”,易造成误解,建议以“增强”为准。
通知 切面开启后执行的方法,根据与目标方法的执行顺序和逻辑分为 5 类(详见下文)。
引介 特殊的增强,为类动态添加属性和方法。即使业务类未实现某个接口,通过引介可动态添加该接口的实现逻辑,使业务类成为接口的实现类。
织入 将增强添加到目标类具体连接点的过程,AOP 有 3 种织入方式(详见下文)。
切面 由切点和增强(或引介)组成,包含横切关注功能的定义和连接点的定义,是 AOP 的核心组件。
横切关注点 系统中被多个核心业务模块共同依赖的功能(如日志、安全、事务管理等),这些功能分散在核心业务代码中,通过 AOP 可将其抽取为独立切面,实现复用和解耦。

通知的分类

通知是切面的具体执行逻辑,Spring AOP 支持 5 种类型的通知,覆盖目标方法执行的全生命周期:

  • 前置通知(Before):在连接点(目标方法)执行前执行;
  • 后置通知(AfterReturning):在目标方法正常执行完成后执行(异常时不执行);
  • 最终通知(After):无论目标方法是否异常,执行后都会触发(类似 finally 块);
  • 异常通知(AfterThrowing):在目标方法抛出异常后执行;
  • 环绕通知(Around):包围目标方法,可在目标方法执行前、后执行逻辑,还能决定是否调用目标方法(如权限拦截)、修改目标方法的返回值。

织入的三种方式

织入方式 说明
编译期织入 需特殊 Java 编译器(如 AspectJ),在编译目标类时将切面织入字节码。
类装载期织入 需特殊类加载器,在目标类加载到 JVM 时织入切面。
动态代理织入 运行时为目标类生成代理对象,通过代理对象执行切面逻辑(Spring 采用此方式)。

Spring AOP 实现方式

AOP 并非 Spring 特有,Spring 是支持 AOP 编程的框架之一,且仅支持方法拦截的 AOP。Spring 提供 4 种 AOP 实现方式:

  1. 使用 ProxyFactoryBean 和对应接口实现 AOP;
  2. 使用 XML 配置 AOP(仅在遗留项目中出现,极少使用);
  3. 使用 @AspectJ 注解驱动切面(主流方式);
  4. 使用 AspectJ 注入切面。

下文重点讲解 @AspectJ 注解驱动切面的实现(有时 XML 配置会用在一些遗留项目中或者起辅助作用,而其它两种方式基本不会使用)。

使用 @AspectJ 注解开发 Spring AOP

@AspectJ 是 AspectJ 框架提供的注解,Spring 对其进行了整合,可通过注解快速定义切面,是目前最常用的 Spring AOP 开发方式。

步骤 1:环境配置(依赖 + 配置文件)

添加依赖

需引入 AspectJ 相关依赖(以 Maven/Gradle 为例):

  • Gradle:
implementation 'org.aspectj:aspectjweaver:1.9.25.1'
implementation 'org.aspectj:aspectjrt:1.9.25.1'
  • Maven:
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.25.1</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.9.25.1</version>
</dependency>
Spring 配置文件(开启注解扫描和 AOP 自动代理)

创建 spring-beans.xml 文件,配置组件扫描和 AOP 自动代理:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop
        https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 自动扫描指定包及子包下的 Bean(替换为实际包路径) -->
    <context:component-scan base-package="edu.uestc.avatar"/>

    <!-- 开启 @AspectJ 注解驱动的 AOP 自动代理 -->
    <aop:aspectj-autoproxy/>
</beans>

步骤 2:定义业务接口和实现类(目标对象)

Spring AOP 是方法级别的 AOP 框架,需先定义业务接口和实现类,以业务方法作为连接点。

业务接口(BookService
package edu.uestc.avatar.service;

import edu.uestc.avatar.domain.Book;
import java.util.List;

public interface BookService {
    Integer save(Book book);
    void removeById(Integer id);
    void update(Book book);
    Book findById(Integer id);
    List<Book> list();
}
业务实现类(BookServiceImpl
package edu.uestc.avatar.service.impl;

import edu.uestc.avatar.dao.BookDao;
import edu.uestc.avatar.domain.Book;
import edu.uestc.avatar.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

@Service // 标记为 Spring 服务 Bean
public class BookServiceImpl implements BookService {
    private AtomicInteger idGenerator = new AtomicInteger();

    @Autowired // 自动注入 BookDao(需提前定义)
    private BookDao dao;

    @Override
    public Integer save(Book book) {
        System.out.println("save()执行");
        var id = idGenerator.incrementAndGet();
        book.setId(id);
        dao.save(book);
        return id;
    }

    @Override
    public void removeById(Integer id) {
        System.out.println("removeById()执行");
        if(id == 0) // 测试异常通知(主动抛出异常)
            throw new IllegalArgumentException("id不能为0");
        dao.removeById(id);
    }

    @Override
    public void update(Book book) {
        System.out.println("update()执行");
        dao.update(book);
    }

    @Override
    public Book findById(Integer id) {
        System.out.println("findById()执行");
        return dao.findById(id);
    }

    @Override
    public List<Book> list() {
        System.out.println("list()执行");
        return dao.list();
    }
}

步骤 3:创建切面类(@Aspect 注解)

切面类是 AOP 的核心,需用 @Aspect 注解标记(同时需用 @Component 注解让 Spring 扫描到),内部包含切点定义通知方法

package edu.uestc.avatar.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * 定义切面:由切点和增强(引介)组成,包含横切关注功能和连接点定义
 * 用 @Aspect 注解标记类,Spring IoC 容器会识别为切面类
 */
@Component // 标记为 Spring Bean
@Aspect // 标记为切面类
public class ServiceAspect {
    /**
     * 定义切点 @Pointcut:指定哪些方法需要被拦截(连接点筛选条件)
     * 表达式:execution(* edu.uestc.avatar.service.*.*(..))
     * 解析:
     * - execution:触发时机为方法执行时
     * - *:任意返回类型
     * - edu.uestc.avatar.service.*.*:edu.uestc.avatar.service 包下所有类(或接口)的所有方法
     *   (第一个 * 表示任意类/接口,第二个 * 表示任意方法)
     * - (..):任意参数列表
     */
    @Pointcut("execution(* edu.uestc.avatar.service.*.*(..))")
    public void pointcut() {}

    /**
     * 前置通知:在目标方法执行前调用
     */
    @Before("pointcut()")
    public void before() {
        System.out.println("前置通知:开启事务");
    }

    /**
     * 后置通知:在目标方法正常执行完成后调用
     */
    @AfterReturning("pointcut()")
    public void afterReturning() {
        System.out.println("后置通知:提交事务");
    }

    /**
     * 异常通知:在目标方法抛出异常后调用
     * throwing = "th":将目标方法抛出的异常对象注入到参数 th 中
     */
    @AfterThrowing(pointcut = "pointcut()", throwing = "th")
    public void afterThrowing(Throwable th) {
        System.out.println("例外通知:记录异常信息==>" + th);
    }

    /**
     * 最终通知:无论目标方法是否异常,执行后都会调用
     */
    @After("pointcut()")
    public void after() {
        System.out.println("最终通知:释放资源");
    }

    /**
     * 环绕通知:包围目标方法,可在执行前后扩展逻辑,还能控制目标方法是否执行
     * @param pjd:连接点对象(封装了目标方法的信息)
     * @return 目标方法的返回值(可修改)
     * @throws Throwable 目标方法可能抛出的异常
     */
    @Around("pointcut()")
    public Object around(ProceedingJoinPoint pjd) throws Throwable {
        Object ret = null;
        // 环绕通知的前置逻辑(如权限校验)
        System.out.println("环绕通知:" + pjd);
        // 调用目标方法(若不调用则目标方法不会执行)
        ret = pjd.proceed();
        // 环绕通知的后置逻辑
        return ret; // 可修改目标方法的返回值(不建议随意修改)
    }
}

AspectJ 指示器详解

切点表达式中常用的 AspectJ 指示器如下表所示,可灵活组合实现精准的连接点筛选:

AspectJ 指示器 描述
arg() 限制连接点匹配参数为指定类型的方法(如 arg(String) 匹配参数为 String 类型的方法)
@args() 限制连接点匹配参数带有指定注解的方法(如 @args(MyAnnotation)
execution() 匹配连接点的执行方法(最常用,支持正则表达式匹配类、方法、参数)
this() 限制连接点匹配 AOP 代理对象为指定类型的类
target() 限制连接点匹配被代理对象(目标对象)为指定类型的类
@target() 限制连接点匹配被代理对象带有指定注解的类
within() 限制连接点匹配指定包下的方法(如 within(edu.uestc.avatar.service.*)
@within() 限制连接点匹配指定注解标注的类中的方法
@annotation() 限制连接点匹配带有指定注解的方法(如 @annotation(MyAnnotation)

步骤 4:测试 Spring AOP

编写 JUnit 测试用例,验证 AOP 通知是否生效:

package edu.uestc.avatar.service;

import edu.uestc.avatar.domain.Book;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

class BookServiceTest {
    private static ApplicationContext context;
    private static BookService service;

    // 初始化 Spring 容器(所有测试方法执行前执行一次)
    @BeforeAll
    public static void before() {
        context = new ClassPathXmlApplicationContext("spring-beans.xml");
        // 从容器中获取 BookService 代理对象(而非原始对象)
        service = context.getBean("bookService", BookService.class);
    }

    @Test
    void save() {
        var book = new Book()
                .setTitle("射雕英雄传-jdbc")
                .setAuthor("金庸")
                .setMarketPrice(300f)
                .setSellPrice(180f)
                .setBuyPrice(100f)
                .setPublisher("三联出版社");
        System.out.println(service.save(book));
    }

    @Test
    void removeById() {
        // 测试正常情况(id=22)
        service.removeById(22);
        // 测试异常情况(id=0,触发异常通知)
        // service.removeById(0);
    }

    @Test
    void update() {
        var book = service.findById(21);
        book.setTitle("spring data jdbc");
        service.update(book);
    }

    @Test
    void findById() {
        System.out.println(service.findById(21));
    }

    @Test
    void list() {
        service.list().forEach(System.out::println);
    }
}

关键补充:Spring AOP 的织入逻辑

Spring AOP 的织入逻辑与动态代理技术紧密相关,具体规则如下:

  1. 若目标类实现了接口:Spring 使用 JDK 动态代理生成代理对象,代理对象实现目标类的所有接口;
  2. 若目标类未实现接口:Spring 使用 CGLib 动态代理生成代理对象,代理对象是目标类的子类;
  3. 代理对象由 Spring IoC 容器自动生成,开发者无需手动创建,只需通过容器获取目标类的 Bean(实际获取的是代理对象)。

进阶用法 1:给通知传递参数

在实际开发中,通知方法可能需要获取目标方法的参数、返回值或异常信息,可通过以下方式实现:

获取目标方法参数

通过切点表达式的 args() 指示器绑定目标方法参数,传递给通知方法:

/**
 * 前置通知:获取目标方法中类型为 Book 的参数
 */
@Before("pointcut() && args(book)")
public void before(Book book) {
    System.out.println("前置通知:开启事务,参数 Book=" + book);
}

获取目标方法返回值

通过 @AfterReturning 注解的 returning 属性绑定目标方法返回值:

/**
 * 后置通知:获取目标方法返回值(仅拦截返回值类型为 Integer 的方法)
 */
@AfterReturning(pointcut = "pointcut()", returning = "retValue")
public void afterReturning(Integer retValue) {
    System.out.println("后置通知:提交事务,返回结果:" + retValue);
}

获取目标方法注解

通过 @annotation() 指示器绑定目标方法上的注解,实现权限校验等功能:

/**
 * 环绕通知:拦截带有 @Privilege 注解的方法,获取注解中的权限信息
 * @param pjd 连接点对象
 * @param privilege 目标方法上的 @Privilege 注解对象
 */
@Around("pointcut() && @annotation(privilege)")
public Object around(ProceedingJoinPoint pjd, Privilege privilege) throws Throwable {
    Object ret = null;
    // 获取注解中的权限信息
    System.out.println(pjd.getSignature() + "需要出示权限:" + privilege.permission());
    // 权限校验(伪代码)
    // if (当前用户拥有该权限) {
        ret = pjd.proceed(); // 执行目标方法
        System.out.println("方法返回值:" + ret);
    // } else {
    //     throw new NoPermissionException("无权限执行该方法");
    // }
    return ret;
}

说明@Privilege 是自定义注解,需提前定义(含 permission)。

进阶用法 2:引入(为已有类添加新接口)

引入是 Spring AOP 的特殊增强功能,可为已有的目标类动态添加新的接口和实现逻辑,无需修改目标类代码。

示例:为 BookService 添加 BookVerifier 接口

定义新接口(BookVerifier
public interface BookVerifier {
    // 检测 Book 对象是否不为空(默认方法,无需实现类重写)
    default boolean verify(Book book) {
        return book != null;
    }
}
在切面类中通过引入增强目标类

通过 @DeclareParents 注解为目标类引入新接口:

@Component
@Aspect
public class ServiceAspect {
    /**
     * 引入增强:为 BookService 接口的所有实现类添加 BookVerifier 接口
     * 语法:@DeclareParents(value = "目标类表达式", defaultImpl = 接口实现类.class)
     */
    @DeclareParents(
            value = "edu.uestc.avatar.service.BookService+", // + 表示所有实现类
            defaultImpl = BookVerifier.class // 接口的默认实现(此处用接口自身的默认方法)
    )
    private BookVerifier bookVerifier; // 引入的接口类型

    // 其他切点和通知方法...
}
使用引入的接口功能

通过 Spring 容器获取 BookService 代理对象后,可强制转换为 BookVerifier 接口并调用其方法:

@Test
void testIntroduce() {
    BookService service = context.getBean("bookService", BookService.class);
    // 强制转换为引入的 BookVerifier 接口
    BookVerifier verifier = (BookVerifier) service;
    Book book = new Book();
    System.out.println("Book 是否为空:" + !verifier.verify(book));
}
posted @ 2025-12-22 10:12  Jing61  阅读(3)  评论(0)    收藏  举报