切面编程(AOP)详解
切面编程(Aspect-Oriented Programming,AOP) 是一种编程范式,旨在将横切关注点(如日志、事务、安全等)从核心业务逻辑中分离,通过模块化的方式实现代码复用和解耦。
AOP 核心概念
- 切面(Aspect):封装横切关注点的模块(如日志模块)
- 连接点(Join Point):程序执行过程中的点(如方法调用)
- 切点(Pointcut):匹配连接点的表达式(定义哪些方法需要增强)
- 通知(Advice):切面在连接点执行的动作
@Before:方法执行前@After:方法执行后(无论是否异常)@AfterReturning:方法正常返回后@AfterThrowing:方法抛出异常后@Around:包裹方法执行(最强大)
- 织入(Weaving):将切面应用到目标对象的过程
下面是一个完整的 Spring AOP 示例,包含正确的 Maven 依赖和完整的包导入:
Maven 配置 (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>aop-demo</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
</parent>
<dependencies>
<!-- Spring AOP 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 其他必要依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<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>
</plugin>
</plugins>
</build>
</project>
完整的切面实现 (包含所有导入)
1. 业务服务类
package com.example.service;
import org.springframework.stereotype.Service;
@Service
public class PaymentService {
public String processPayment(String userId, double amount) {
System.out.println("处理支付: " + userId + " 金额: " + amount);
// 模拟业务逻辑
if (amount > 1000) {
throw new RuntimeException("支付金额超过限额");
}
return "支付成功: " + amount;
}
public String refundPayment(String transactionId) {
System.out.println("处理退款: " + transactionId);
return "退款成功: " + transactionId;
}
}
2. 切面类 (包含完整导入)
package com.example.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Aspect
@Component
public class PaymentAspect {
// 定义切点:匹配 PaymentService 的所有方法
@Pointcut("execution(* com.example.service.PaymentService.*(..))")
public void paymentOperations() {}
// 方法执行前通知
@Before("paymentOperations()")
public void logMethodCall(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("[日志] 调用方法: " + methodName + " | 参数: " + args);
}
// 方法返回后通知
@AfterReturning(pointcut = "paymentOperations()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
System.out.println("[日志] 方法返回: " + result);
}
// 异常通知
@AfterThrowing(pointcut = "paymentOperations()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
System.out.println("[错误] 方法异常: " + ex.getMessage());
}
// 环绕通知:性能监控
@Around("paymentOperations()")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
// 执行目标方法
Object result = joinPoint.proceed();
return result;
} finally {
long duration = System.currentTimeMillis() - startTime;
System.out.println("[监控] " + methodName + " 执行耗时: " + duration + "ms");
}
}
// 安全校验切面
@Before("execution(* com.example.service.PaymentService.processPayment(..)) && args(userId, amount)")
public void checkSecurity(JoinPoint joinPoint, String userId, double amount) {
if (!userId.startsWith("user_")) {
throw new SecurityException("非法用户ID格式");
}
System.out.println("[安全] 用户验证通过: " + userId);
}
}
3. 主应用类
package com.example;
import com.example.service.PaymentService;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class AopDemoApplication {
public static void main(String[] args) {
SpringApplication.run(AopDemoApplication.class, args);
}
@Bean
public CommandLineRunner demo(PaymentService paymentService) {
return args -> {
System.out.println("===== 正常支付测试 =====");
paymentService.processPayment("user_123", 500.0);
System.out.println("\n===== 退款测试 =====");
paymentService.refundPayment("txn_789");
System.out.println("\n===== 异常支付测试 =====");
try {
paymentService.processPayment("user_456", 1500.0);
} catch (Exception e) {
System.out.println("捕获异常: " + e.getMessage());
}
System.out.println("\n===== 安全测试 =====");
try {
paymentService.processPayment("invalid_id", 200.0);
} catch (Exception e) {
System.out.println("安全拦截: " + e.getMessage());
}
};
}
}
输出结果示例
===== 正常支付测试 =====
[日志] 调用方法: processPayment | 参数: [user_123, 500.0]
[安全] 用户验证通过: user_123
处理支付: user_123 金额: 500.0
[日志] 方法返回: 支付成功: 500.0
[监控] processPayment 执行耗时: 15ms
===== 退款测试 =====
[日志] 调用方法: refundPayment | 参数: [txn_789]
处理退款: txn_789
[日志] 方法返回: 退款成功: txn_789
[监控] refundPayment 执行耗时: 5ms
===== 异常支付测试 =====
[日志] 调用方法: processPayment | 参数: [user_456, 1500.0]
[安全] 用户验证通过: user_456
处理支付: user_456 金额: 1500.0
[错误] 方法异常: 支付金额超过限额
[监控] processPayment 执行耗时: 8ms
捕获异常: 支付金额超过限额
===== 安全测试 =====
[日志] 调用方法: processPayment | 参数: [invalid_id, 200.0]
[监控] processPayment 执行耗时: 0ms
安全拦截: 非法用户ID格式
AOP实现
-
切点表达式详解:
execution(* com.example.service.PaymentService.*(..))*:任意返回类型com.example.service.PaymentService:目标类.*:所有方法(..):任意参数
-
参数绑定:
@Before("execution(* processPayment(..)) && args(userId, amount)") public void checkSecurity(String userId, double amount) { // 直接使用方法的参数 } -
通知类型组合:
@Before:方法执行前(参数校验)@AfterReturning:成功返回后(日志记录)@AfterThrowing:异常时(错误处理)@Around:包裹整个方法(性能监控)
-
执行顺序:
1. Around 开始 2. Before 3. 目标方法执行 4. AfterReturning/AfterThrowing 5. Around 结束
关键点解析
一、整体架构
二、核心组件详解
1. 切面类 (PaymentAspect)
@Aspect // 声明这是一个切面
@Component // 让Spring管理这个Bean
public class PaymentAspect {
// 切点定义
@Pointcut("execution(* com.example.service.PaymentService.*(..))")
public void paymentOperations() {}
// 各种通知类型...
}
2. 切点表达式解析
@Pointcut("execution(* com.example.service.PaymentService.*(..))")
execution: 匹配方法执行连接点*: 任意返回类型com.example.service.PaymentService: 目标类.*: 所有方法(..): 任意参数
三、五种通知类型详解
1. 前置通知 (@Before)
@Before("paymentOperations()")
public void logMethodCall(JoinPoint joinPoint) {
// 获取方法信息
String methodName = joinPoint.getSignature().getName();
// 获取参数
String args = Arrays.toString(joinPoint.getArgs());
System.out.println("[日志] 调用方法: " + methodName + " | 参数: " + args);
}
- 作用:方法执行前记录日志
- 获取信息:
joinPoint.getSignature().getName(): 方法名joinPoint.getArgs(): 方法参数
2. 返回通知 (@AfterReturning)
@AfterReturning(pointcut = "paymentOperations()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
System.out.println("[日志] 方法返回: " + result);
}
- 作用:方法正常返回后记录结果
- 关键参数:
returning = "result": 绑定返回值Object result: 接收返回值
3. 异常通知 (@AfterThrowing)
@AfterThrowing(pointcut = "paymentOperations()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
System.out.println("[错误] 方法异常: " + ex.getMessage());
}
- 作用:方法抛出异常时记录错误
- 关键参数:
throwing = "ex": 绑定异常对象Exception ex: 接收异常实例
4. 环绕通知 (@Around)
@Around("paymentOperations()")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().getName();
try {
// 执行目标方法
Object result = joinPoint.proceed();
return result;
} finally {
// 无论成功失败都执行
long duration = System.currentTimeMillis() - startTime;
System.out.println("[监控] " + methodName + " 执行耗时: " + duration + "ms");
}
}
- 作用:方法执行性能监控
- 关键特性:
ProceedingJoinPoint: 可控制目标方法执行joinPoint.proceed(): 执行目标方法finally块确保始终记录耗时
5. 参数化通知 (带条件的前置通知)
@Before("execution(* processPayment(..)) && args(userId, amount)")
public void checkSecurity(JoinPoint joinPoint, String userId, double amount) {
if (!userId.startsWith("user_")) {
throw new SecurityException("非法用户ID格式");
}
System.out.println("[安全] 用户验证通过: " + userId);
}
- 作用:特定方法的参数校验
- 关键特性:
args(userId, amount): 直接绑定方法参数- 可访问具体参数值进行业务逻辑
- 可抛出异常中断方法执行
四、执行流程分析
五、核心设计思想
1. 关注点分离 (Separation of Concerns)
- 业务逻辑:PaymentService 只处理支付核心逻辑
- 横切关注点:
- 日志记录 →
logMethodCall() - 性能监控 →
monitorPerformance() - 安全控制 →
checkSecurity() - 异常处理 →
logException()
- 日志记录 →
2. 非侵入式设计
- 业务类无需修改:PaymentService 完全不知道切面的存在
- 解耦:新增日志/监控功能不需改动业务代码
3. 声明式编程
- 通过注解声明:而非硬编码
- 集中管理:所有横切逻辑在切面中统一维护
4. 动态织入
- 运行时代理:Spring 动态创建代理对象
- 灵活组合:通过切点表达式控制织入位置
六、关键实现细节
1. 参数绑定技巧
// 精确绑定特定方法的参数
@Before("execution(* processPayment(..)) && args(userId, amount)")
public void checkSecurity(String userId, double amount) { ... }
2. 异常处理策略
@Around("paymentOperations()")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed(); // 执行目标方法
} finally { // 确保总是执行耗时计算
// 无论成功失败都记录
}
}
3. 切点复用
// 定义公共切点
@Pointcut("execution(* com.example.service.PaymentService.*(..))")
public void paymentOperations() {}
// 多个通知复用同一切点
@Before("paymentOperations()")
@AfterReturning(pointcut = "paymentOperations()", ...)
七、实际输出解析
正常支付场景:
[日志] 调用方法: processPayment | 参数: [user_123, 500.0]
[安全] 用户验证通过: user_123
处理支付: user_123 金额: 500.0
[日志] 方法返回: 支付成功: 500.0
[监控] processPayment 执行耗时: 15ms
- 前置通知记录方法和参数
- 安全校验通过
- 执行业务逻辑
- 返回通知记录结果
- 环绕通知记录耗时
异常场景:
[日志] 调用方法: processPayment | 参数: [user_456, 1500.0]
[安全] 用户验证通过: user_456
处理支付: user_456 金额: 1500.0
[错误] 方法异常: 支付金额超过限额
[监控] processPayment 执行耗时: 8ms
- 前置通知和安全校验正常执行
- 业务方法抛出异常
- 异常通知捕获并记录
- 环绕通知仍执行耗时计算
安全拦截场景:
[日志] 调用方法: processPayment | 参数: [invalid_id, 200.0]
[监控] processPayment 执行耗时: 0ms
安全拦截: 非法用户ID格式
- 前置通知执行
- 安全校验抛出异常
- 业务方法未执行
- 直接进入异常处理
- 环绕通知仍执行(但耗时≈0)
八、最佳实践建议
- 切点命名规范:使用有意义的名称如
serviceLayer(),daoOperations() - 表达式优化:避免过于宽泛的切点(如
execution(* *.*(..))) - 通知顺序控制:使用
@Order注解管理多个切面的执行顺序 - 性能考量:
- 环绕通知中避免重操作
- 生产环境移除调试日志
- 组合使用:
@Around("transactionalOps()") @Transactional(propagation = Propagation.REQUIRED) public Object manageTransaction(ProceedingJoinPoint pjp) {...}
实际应用场景:
- 安全控制:在方法执行前验证权限
- 性能监控:测量方法执行时间
- 事务管理:统一的事务处理
- 日志记录:自动化的操作日志
- 异常处理:统一异常处理机制
- 缓存管理:自动缓存结果
最佳实践:将横切关注点(如日志、安全、事务)与业务逻辑分离,保持业务代码的纯净性,提高代码的可维护性和复用性。
1015

被折叠的 条评论
为什么被折叠?



