Spring AOP 切面编程详解
在软件开发中,我们经常需要对业务逻辑进行统一处理,例如日志记录、权限验证、性能监控等。传统的方式是在每个业务方法中重复编写这些代码,导致代码冗余且难以维护。AOP(面向切面编程)就是为了解决这类问题而生的。
本文将详细介绍 Spring AOP 的核心概念、实现步骤以及底层原理。
一、AOP 核心概念
1.1 切面(Aspect)
切面是 AOP 的核心元素,它定义了"做什么"的问题。切面由切点和通知组成,既包含了横切逻辑的定义,也包括了连接点的定义。在 Spring 中,切面通常是一个使用 @Aspect 注解标注的类。
1.2 切点(Pointcut)
切点解决了"在哪里执行"的问题。切点提供了一套规则(使用 AspectJ 切点表达式语言描述),用于匹配满足条件的连接点。只有被切点匹配到的连接点,才会执行相应的通知。
1.3 通知(Advice)
通知定义了切面"何时执行"以及"执行什么"。Spring 框架提供了五种通知类型:
- @Before 前置通知:在目标方法执行之前执行
- @After 后置通知:在目标方法执行之后执行,无论是否抛出异常
- @AfterReturning 返回通知:在目标方法成功返回后执行
- @AfterThrowing 异常通知:在目标方法抛出异常后执行
- @Around 环绕通知:包裹目标方法,可以在方法执行前后自定义行为
1.4 连接点(Join Point)
连接点是应用执行过程中可以插入切面的点,通常是方法的调用处、异常抛出处或字段修改处。切点匹配的所有连接点都会被通知。
1.5 织入(Weaving)
织入是将切面应用到目标对象的过程,可以在编译期、类加载期或运行时完成。Spring AOP 主要在运行时完成织入。
二、Spring AOP 快速入门
2.1 添加依赖
在 Spring Boot 项目中,只需添加 starter 依赖即可启用 AOP 支持:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.2 定义切点和切面
首先定义一个简单的控制器类:
package com.test.sample.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/product")
public class ProductController {
@GetMapping("/list")
public String listProducts() {
System.out.println("查询产品列表");
return "product list";
}
@PostMapping("/add")
public String addProduct() {
System.out.println("添加产品");
return "add success";
}
@PostMapping("/delete")
public String deleteProduct() {
System.out.println("删除产品");
return "delete success";
}
}
接下来定义切面类:
package com.test.sample.aspect;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Aspect
@Component
public class LogAspect {
// 定义切点:拦截 ProductController 中所有方法
@Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
public void pointcut() {
// 空方法,仅作为标识作用
}
}
切点表达式说明:
execution:表示执行方法时触发*:匹配任意返回类型com.test.sample.controller.ProductController:完全限定类名.*:匹配该类中所有方法(..):匹配任意参数列表
2.3 通知类型实践
前置通知
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void beforeAdvice() {
System.out.println("=== 前置通知:开始执行 ===");
}
}
测试对比:访问 http://localhost:8080/product/list(被拦截)和 http://localhost:8080/user/info(未被拦截)
后置通知
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
public void pointcut() {
}
@Before("pointcut()")
public void beforeAdvice() {
System.out.println("=== 前置通知 ===");
}
@After("pointcut()")
public void afterAdvice() {
System.out.println("=== 后置通知 ===");
}
}
环绕通知
环绕通知功能最强大,可以控制目标方法的执行流程:
@Aspect
@Component
public class LogAspect {
@Pointcut("execution(* com.test.sample.controller.ProductController.*(..))")
public void pointcut() {
}
@Around("pointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
System.out.println("环绕通知:方法执行前");
// 执行目标方法
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("环绕通知:方法执行后");
System.out.println("方法执行耗时:" + (endTime - startTime) + "ms");
return result;
}
}
三、动态代理原理
Spring AOP 基于动态代理实现,理解其原理有助于更好地使用 AOP。
3.1 静态代理 vs 动态代理
静态代理在编译时就确定了代理关系,需要为每个被代理类创建代理类,代码冗余度高。
动态代理在运行时动态生成代理类,无需预先定义代理类,代码复用性高。
举例说明:假设一个班级有三名学生同时提交作业,没有动态代理时,需要老师逐一收取;有动态代理时,课代表(代理)一次性收取所有作业。
3.2 JDK 动态代理
JDK 动态代理要求目标类必须实现接口,通过 java.lang.reflect.Proxy 和 InvocationHandler 实现:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class WechatPayInvocationHandler implements InvocationHandler {
private Object target;
public WechatPayInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置处理
System.out.println("验证签名");
System.out.println("记录日志");
// 调用目标方法
Object result = method.invoke(target, args);
// 后置处理
System.out.println("统计耗时");
return result;
}
public static void main(String[] args) {
PayService target = new WechatPayService();
InvocationHandler handler = new WechatPayInvocationHandler(target);
PayService proxy = (PayService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
new Class[]{PayService.class},
handler
);
proxy.processPayment();
}
}
特点:
- 优点:实现简单,无需额外依赖
- 缺点:只能代理实现接口的类
3.3 CGLIB 动态代理
CGLIB 通过继承方式实现代理,无需目标类实现接口:
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
public class PaymentMethodInterceptor implements MethodInterceptor {
private Object target;
public PaymentMethodInterceptor(Object target) {
this.target = target;
}
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
System.out.println("验证签名");
System.out.println("记录日志");
Object result = proxy.invoke(target, args);
System.out.println("统计耗时");
return result;
}
public static void main(String[] args) {
PayService target = new WechatPayService();
PayService proxy = (PayService) Enhancer.create(
target.getClass(),
new PaymentMethodInterceptor(target)
);
proxy.processPayment();
}
}
特点:
- 优点:可代理未实现接口的类
- 缺点:无法代理 final 类和 final 方法
3.4 两种代理方式对比
| 特性 | JDK 动态代理 | CGLIB 动态代理 |
|---|---|---|
| 来源 | Java 标准库 | 第三方库 |
| 实现方式 | 接口实现 | 继承子类 |
| 代理范围 | 实现接口的类 | 任意类 |
| final 类 | 支持 | 不支持 |
| 性能(JDK 7+) | 略优 | 略差 |
Spring AOP 自动选择代理方式:实现了接口的类使用 JDK 代理,否则使用 CGLIB 代理。
四、总结
Spring AOP 提供了一种优雅的方式来实现横切关注点的分离,通过切面、切点、通知等概念,可以将日志、权限、事务等通用逻辑与业务逻辑分离。理解动态代理的实现原理有助于更好地使用和调试 AOP 相关代码。