一、AOP核心概念
1. 为什么需要AOP?
传统OOP编程中,重复的逻辑(如日志、事务、权限)会散落在各业务方法中,造成代码冗余和维护困难。AOP通过横向切割将这些公共功能抽取成独立模块(切面),实现解耦。
2. AOP核心术语
术语 | 描述 | 生活类比 |
---|---|---|
Aspect(切面) | 封装横切逻辑的类(如日志、事务) | 安保系统:处理监控、门禁等统一功能 |
JoinPoint(连接点) | 程序执行期间的某个点(如方法执行、异常抛出) | 大楼出入口:可能被安保系统监控的位置 |
Pointcut(切入点) | 表达式,定义哪些连接点会应用切面逻辑 | 安保规则:规定哪些出入口需要检查身份证 |
Advice(通知) | 切面在特定连接点执行的动作(如方法执行前、后) | 安保动作:在出入口检查身份证、记录日志 |
Weaving(织入) | 将切面代码与目标对象结合的过程(编译期/运行时) | 在大楼建造时预埋监控设备 |
二、Spring AOP 实现详解
1. 依赖配置(Maven)
<XML>
<!-- Spring AOP 核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- AspectJ 支持 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
2. 切面定义示例:日志记录
<JAVA>
@Aspect
@Component
public class LoggingAspect {
// 定义切入点:service包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
// 前置通知:在方法执行前打印日志
@Before("serviceLayer()")
public void logMethodStart(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
System.out.println("【前置日志】方法 " + methodName + " 开始执行, 参数: " + Arrays.toString(joinPoint.getArgs()));
}
// 后置通知:在方法执行后打印日志(无论是否抛异常)
@After("serviceLayer()")
public void logMethodEnd(JoinPoint joinPoint) {
System.out.println("【后置日志】方法 " + joinPoint.getSignature().getName() + " 执行结束");
}
// 返回通知:获取方法返回值
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void logReturnValue(Object result) {
System.out.println("【返回日志】方法返回值: " + result);
}
// 异常通知:捕获异常信息
@AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
public void logException(JoinPoint joinPoint, Exception ex) {
System.out.println("【异常日志】方法 " + joinPoint.getSignature().getName() + " 抛出异常: " + ex.getMessage());
}
// 环绕通知:控制整个方法执行流程
@Around("serviceLayer()")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("【环绕前】准备执行方法");
Object result = pjp.proceed(); // 执行目标方法
System.out.println("【环绕后】方法执行完毕");
return result;
}
}
3. 目标业务类
<JAVA>
@Service
public class UserService {
public String getUserById(Long id) {
System.out.println("执行业务方法:根据ID查询用户");
if (id == 1) {
return "用户: Alice";
} else {
throw new RuntimeException("用户不存在");
}
}
}
4. 测试代码
<JAVA>
@SpringBootTest
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testLoggingAspect() {
userService.getUserById(1L);
System.out.println("------");
try {
userService.getUserById(2L);
} catch (Exception ignored) {}
}
}
5. 输出结果
【环绕前】准备执行方法
【前置日志】方法 getUserById 开始执行, 参数: [1]
执行业务方法:根据ID查询用户
【返回日志】方法返回值: 用户: Alice
【后置日志】方法 getUserById 执行结束
【环绕后】方法执行完毕
------
【环绕前】准备执行方法
【前置日志】方法 getUserById 开始执行, 参数: [2]
执行业务方法:根据ID查询用户
【异常日志】方法 getUserById 抛出异常: 用户不存在
【后置日志】方法 getUserById 执行结束
三、AOP高阶应用
1. 注解驱动自定义切面
定义自定义注解
<JAVA>
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {
}
切面实现方法耗时统计
<JAVA>
@Aspect
@Component
public class ExecutionTimeAspect {
@Around("@annotation(LogExecutionTime)")
public Object logExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
long startTime = System.currentTimeMillis();
Object result = pjp.proceed();
long duration = System.currentTimeMillis() - startTime;
System.out.println("方法 " + pjp.getSignature() + " 执行时间: " + duration + "ms");
return result;
}
}
在业务方法上使用注解
<JAVA>
@Service
public class OrderService {
@LogExecutionTime
public void processOrder() {
// 模拟耗时操作
try { Thread.sleep(1500); } catch (InterruptedException ignored) {}
}
}
2. 切入点表达式详解
表达式类型 | 示例 | 说明 |
---|---|---|
execution | execution(* com.service.*.*(..)) | 匹配service包下所有类的所有方法 |
@annotation | @annotation(com.example.LogExecutionTime) | 匹配被指定注解标记的方法 |
within | within(com.controller.*) | 匹配controller包中所有方法 |
args | args(java.lang.String, ..) | 匹配第一个参数为String类型的方法 |
bean | bean(userService) | 匹配Spring容器中名为userService的Bean的方法 |
四、AOP原理与常见问题
1. 动态代理实现方式
- JDK动态代理:基于接口代理,使用
Proxy
类和InvocationHandler
接口。 - CGLIB代理:通过继承目标类生成子类,覆盖方法实现代理(无需接口)。
2. 常见问题
Q1:为什么切入点表达式不生效?
- 检查包路径是否正确
- 确保目标方法是否为public
- 确认是否在同一个Spring容器中
Q2:AOP适用的场景有哪些?
- 日志记录、性能统计
- 事务管理(@Transactional底层基于AOP)
- 权限校验、接口限流
- 数据脱敏、缓存控制
Q3:如何选择Spring AOP和AspectJ?
对比项 | Spring AOP | AspectJ |
---|---|---|
织入方式 | 运行时通过动态代理实现 | 编译期/类加载期织入 |
功能范围 | 仅支持方法级别的切入点 | 支持字段、构造方法等更细致的控制 |
性能 | 有轻微运行时开销 | 编译期织入无运行时开销 |
易用性 | 简单,与Spring整合无缝 | 需要额外编译器/工具支持 |