Spring AOP
Spring AOP
代理模式
代理模式是 GoF 提出的 23 种设计模式中最为经典的模式之一,属于对象的结构模式。它的核心思想是给某一个对象提供一个代理对象,并由代理对象控制对原对象的引用。简单来说,代理对象可以承担比原对象更多的职责,当需要为原对象添加横切关注功能时,代理对象就能发挥作用。
生活中也有很多代理模式的应用场景,比如我们打开 Office 系列的 Word 文档时,若文档包含插图,刚加载文档时,插图仅以虚框占位符的形式呈现,直到用户翻到对应页面需要查看该图片时,才会真正加载图片。这里代替真正图片的虚框就是一个典型的虚拟代理。
代理模式的核心流程
代理模式的实现需要经过两个关键步骤,结合以下结构示意图可清晰理解:
客户端 -> 代理对象 -> 目标对象
- 代理对象和真实对象(目标对象)建立代理关系;
- 实现代理对象的代理逻辑方法。
代理模式的分类
代理可分为静态代理和动态代理,以下通过“找枪手代考”的示例详细演示两种代理模式的使用。
静态代理
静态代理的核心是代理类和目标类实现相同的接口,通过代理类封装目标类的逻辑并扩展额外功能。
步骤 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();
}
}
静态代理的优缺点
- 优点:实现客户端与目标类的解耦,客户端无需知道目标类的具体实现,只需与代理类交互。
- 缺点:
- 代码冗余:代理类和目标类实现相同接口,若接口新增方法,所有实现类和代理类都需同步实现,增加维护成本;
- 灵活性差:代理类仅服务于一种类型的对象,若需代理多种类型对象,需为每种对象单独创建代理类,无法适应大规模程序。
动态代理
从 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 实现方式:
- 使用
ProxyFactoryBean和对应接口实现 AOP; - 使用 XML 配置 AOP(仅在遗留项目中出现,极少使用);
- 使用
@AspectJ注解驱动切面(主流方式); - 使用 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 的织入逻辑与动态代理技术紧密相关,具体规则如下:
- 若目标类实现了接口:Spring 使用 JDK 动态代理生成代理对象,代理对象实现目标类的所有接口;
- 若目标类未实现接口:Spring 使用 CGLib 动态代理生成代理对象,代理对象是目标类的子类;
- 代理对象由 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));
}

浙公网安备 33010602011771号