# Spring AOP 详细学习笔记
## 一、AOP基础概念
### 1.1 什么是AOP
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,它通过将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,实现了关注点的模块化。
### 1.2 AOP的核心概念
#### 1.2.1 切面(Aspect)
切面是横切关注点的模块化,包含了通知和切入点的定义。
#### 1.2.2 连接点(Join Point)
程序执行过程中的某个特定点,如方法调用、方法执行、字段访问等。在Spring AOP中,连接点总是方法的执行。
#### 1.2.3 通知(Advice)
切面在特定连接点执行的动作。主要有以下类型:
- **前置通知(Before)**:在目标方法执行前执行
- **后置通知(After)**:在目标方法执行后执行(无论是否异常)
- **返回通知(After Returning)**:在目标方法正常返回后执行
- **异常通知(After Throwing)**:在目标方法抛出异常后执行
- **环绕通知(Around)**:包围目标方法,可以在方法执行前后都执行自定义逻辑
#### 1.2.4 切入点(Pointcut)
用于匹配连接点的谓词表达式。通过切入点表达式,我们可以指定哪些方法需要被增强。
#### 1.2.5 引入(Introduction)
允许向现有类添加新方法或属性。
#### 1.2.6 目标对象(Target Object)
被一个或多个切面通知的对象。
#### 1.2.7 AOP代理(AOP Proxy)
AOP框架创建的对象,用于实现切面契约。在Spring中,AOP代理可以是JDK动态代理或CGLIB代理。
#### 1.2.8 织入(Weaving)
将切面应用到目标对象并创建代理对象的过程。
### 1.3 Spring AOP的特点
- 基于代理的AOP实现
- 只支持方法级别的连接点
- 与Spring IoC容器完美集成
- 支持基于注解和XML的配置方式
## 二、Spring AOP的使用方式
### 2.1 引入依赖
```xml
org.springframework
spring-aop
5.3.23
org.aspectj
aspectjweaver
1.9.7
```
### 2.2 启用AOP
#### 注解方式启用
```java
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// 配置类内容
}
```
#### XML方式启用
```xml
```
## 三、基于注解的AOP
### 3.1 定义切面类
```java
@Aspect
@Component
public class LoggingAspect {
// 定义切入点
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {}
// 前置通知
@Before("serviceLayer()")
public void logBefore(JoinPoint joinPoint) {
System.out.println("执行方法: " + joinPoint.getSignature().getName());
System.out.println("参数: " + Arrays.toString(joinPoint.getArgs()));
}
// 后置通知
@After("serviceLayer()")
public void logAfter(JoinPoint joinPoint) {
System.out.println("方法执行完毕: " + joinPoint.getSignature().getName());
}
// 返回通知
@AfterReturning(pointcut = "serviceLayer()", returning = "result")
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("方法返回值: " + result);
}
// 异常通知
@AfterThrowing(pointcut = "serviceLayer()", throwing = "error")
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
System.out.println("方法异常: " + error.getMessage());
}
// 环绕通知
@Around("serviceLayer()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("环绕通知 - 方法执行前");
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
System.out.println("方法执行时间: " + (endTime - startTime) + "ms");
return result;
} catch (Throwable e) {
System.out.println("环绕通知 - 捕获异常: " + e.getMessage());
throw e;
}
}
}
```
### 3.2 示例服务类
```java
@Service
public class UserService {
public User findUserById(Long id) {
System.out.println("查找用户: " + id);
return new User(id, "张三");
}
public void saveUser(User user) {
System.out.println("保存用户: " + user.getName());
}
public void deleteUser(Long id) {
System.out.println("删除用户: " + id);
if (id == null) {
throw new IllegalArgumentException("用户ID不能为空");
}
}
}
```
## 四、切入点表达式详解
### 4.1 execution表达式
最常用的切入点表达式,用于匹配方法执行。
语法:`execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throws-pattern?)`
示例:
```java
// 匹配所有public方法
@Pointcut("execution(public * *(..))")
// 匹配指定包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
// 匹配指定类的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
// 匹配所有返回值为void的方法
@Pointcut("execution(void *(..))")
// 匹配第一个参数为Long类型的方法
@Pointcut("execution(* *(Long,..))")
// 匹配所有以save开头的方法
@Pointcut("execution(* save*(..))")
```
### 4.2 within表达式
限制匹配特定类型内的连接点。
```java
// 匹配指定包下的所有方法
@Pointcut("within(com.example.service.*)")
// 匹配指定类的所有方法
@Pointcut("within(com.example.service.UserService)")
```
### 4.3 @annotation表达式
匹配带有特定注解的方法。
```java
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
String value() default "";
}
// 切入点表达式
@Pointcut("@annotation(com.example.annotation.Loggable)")
public void loggableMethods() {}
// 使用注解
@Service
public class OrderService {
@Loggable("创建订单")
public void createOrder(Order order) {
// 业务逻辑
}
}
```
### 4.4 @within表达式
匹配特定注解标注的类内的所有方法。
```java
@Pointcut("@within(org.springframework.stereotype.Service)")
public void allServiceMethods() {}
```
### 4.5 args表达式
匹配特定参数类型的方法。
```java
// 匹配只有一个String参数的方法
@Pointcut("args(String)")
// 匹配第一个参数为String的方法
@Pointcut("args(String,..)")
```
### 4.6 组合切入点表达式
使用&&、||、!组合多个切入点表达式。
```java
@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(Loggable)")
public void serviceLoggableMethods() {}
@Pointcut("within(com.example.service.*) || within(com.example.dao.*)")
public void serviceOrDaoMethods() {}
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.*.get*(..))")
public void nonGetterServiceMethods() {}
```
## 五、通知参数传递
### 5.1 获取方法参数
```java
@Aspect
@Component
public class ParameterAspect {
@Before("execution(* com.example.service.*.*(..)) && args(id,name,..)")
public void logWithParams(Long id, String name) {
System.out.println("方法参数 - ID: " + id + ", Name: " + name);
}
@Around("@annotation(loggable)")
public Object logWithAnnotation(ProceedingJoinPoint joinPoint, Loggable loggable) throws Throwable {
System.out.println("注解值: " + loggable.value());
return joinPoint.proceed();
}
}
```
### 5.2 获取目标对象和代理对象
```java
@Before("execution(* com.example.service.*.*(..))")
public void logTarget(JoinPoint joinPoint) {
Object target = joinPoint.getTarget(); // 目标对象
Object proxy = joinPoint.getThis(); // 代理对象
System.out.println("目标类: " + target.getClass().getName());
}
```
## 六、基于XML的AOP配置
### 6.1 XML配置示例
```xml
```
### 6.2 XML配置的切面类
```java
public class LoggingAspect {
public void logBefore(JoinPoint joinPoint) {
System.out.println("XML配置 - 前置通知: " + joinPoint.getSignature().getName());
}
public void logAfter(JoinPoint joinPoint) {
System.out.println("XML配置 - 后置通知: " + joinPoint.getSignature().getName());
}
public void logAfterReturning(JoinPoint joinPoint, Object result) {
System.out.println("XML配置 - 返回通知: " + result);
}
public void logAfterThrowing(JoinPoint joinPoint, Throwable error) {
System.out.println("XML配置 - 异常通知: " + error.getMessage());
}
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("XML配置 - 环绕通知开始");
Object result = joinPoint.proceed();
System.out.println("XML配置 - 环绕通知结束");
return result;
}
}
```
## 七、实际应用场景
### 7.1 日志记录
```java
@Aspect
@Component
@Slf4j
public class LogAspect {
@Around("@annotation(Log)")
public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("开始执行方法: {}, 参数: {}", methodName, Arrays.toString(args));
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
log.info("方法执行成功: {}, 耗时: {}ms, 返回值: {}", methodName, costTime, result);
return result;
} catch (Throwable e) {
long costTime = System.currentTimeMillis() - startTime;
log.error("方法执行失败: {}, 耗时: {}ms, 异常: {}", methodName, costTime, e.getMessage());
throw e;
}
}
}
```
### 7.2 权限控制
```java
@Aspect
@Component
public class SecurityAspect {
@Autowired
private SecurityService securityService;
@Before("@annotation(RequiresPermission)")
public void checkPermission(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
RequiresPermission permission = signature.getMethod().getAnnotation(RequiresPermission.class);
if (!securityService.hasPermission(permission.value())) {
throw new UnauthorizedException("没有权限执行此操作: " + permission.value());
}
}
}
// 使用示例
@Service
public class AdminService {
@RequiresPermission("admin:user:delete")
public void deleteUser(Long userId) {
// 删除用户逻辑
}
}
```
### 7.3 缓存处理
```java
@Aspect
@Component
public class CacheAspect {
private Map cache = new ConcurrentHashMap<>();
@Around("@annotation(Cacheable)")
public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
String key = generateKey(joinPoint);
if (cache.containsKey(key)) {
System.out.println("从缓存获取数据: " + key);
return cache.get(key);
}
Object result = joinPoint.proceed();
cache.put(key, result);
System.out.println("将数据放入缓存: " + key);
return result;
}
private String generateKey(ProceedingJoinPoint joinPoint) {
return joinPoint.getSignature().toString() + Arrays.toString(joinPoint.getArgs());
}
}
```
### 7.4 事务管理
```java
@Aspect
@Component
public class TransactionAspect {
@Autowired
private TransactionManager transactionManager;
@Around("@annotation(Transactional)")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
transactionManager.beginTransaction();
try {
Object result = joinPoint.proceed();
transactionManager.commit();
return result;
} catch (Exception e) {
transactionManager.rollback();
throw e;
}
}
}
```
### 7.5 性能监控
```java
@Aspect
@Component
public class PerformanceAspect {
private static final int SLOW_EXECUTION_THRESHOLD = 1000; // 1秒
@Around("@annotation(MonitorPerformance)")
public Object monitor(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().toShortString();
long startTime = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long executionTime = System.currentTimeMillis() - startTime;
if (executionTime > SLOW_EXECUTION_THRESHOLD) {
log.warn("方法执行缓慢: {} 耗时 {}ms", methodName, executionTime);
// 可以发送告警通知
}
// 记录性能指标
recordMetrics(methodName, executionTime);
}
}
private void recordMetrics(String methodName, long executionTime) {
// 将性能数据发送到监控系统
}
}
```
### 7.6 重试机制
```java
@Aspect
@Component
public class RetryAspect {
@Around("@annotation(Retry)")
public Object retry(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Retry retry = signature.getMethod().getAnnotation(Retry.class);
int attempts = retry.attempts();
long delay = retry.delay();
Exception lastException = null;
for (int i = 0; i < attempts; i++) {
try {
return joinPoint.proceed();
} catch (Exception e) {
lastException = e;
if (i < attempts - 1) {
log.warn("方法执行失败,{}ms后重试,当前第{}次尝试", delay, i + 1);
Thread.sleep(delay);
}
}
}
throw new RuntimeException("重试" + attempts + "次后仍然失败", lastException);
}
}
// 使用示例
@Service
public class EmailService {
@Retry(attempts = 3, delay = 1000)
public void sendEmail(String to, String subject, String content) {
// 发送邮件逻辑,可能会失败
}
}
```
## 八、最佳实践
### 8.1 切面优先级
当多个切面作用于同一个连接点时,可以通过@Order注解指定优先级:
```java
@Aspect
@Component
@Order(1) // 数字越小,优先级越高
public class SecurityAspect {
// 安全检查逻辑
}
@Aspect
@Component
@Order(2)
public class LoggingAspect {
// 日志记录逻辑
}
```
### 8.2 避免循环依赖
切面不应该通知自己,避免无限循环:
```java
@Aspect
@Component
public class LoggingAspect {
// 排除切面类本身
@Pointcut("execution(* com.example..*.*(..)) && !within(com.example.aspect..*)")
public void applicationMethods() {}
}
```
### 8.3 合理使用通知类型
- 使用@Before进行参数验证和权限检查
- 使用@AfterReturning处理返回值
- 使用@AfterThrowing进行异常处理和告警
- 使用@Around进行性能监控和事务管理
- 简单场景避免使用@Around,因为它最强大但也最复杂
### 8.4 切入点表达式优化
- 尽量缩小切入点范围,避免过度匹配
- 使用within()限定包或类范围
- 组合使用多个表达式提高精确度
### 8.5 异常处理
在环绕通知中正确处理异常:
```java
@Around("applicationMethods()")
public Object handleException(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();
} catch (BusinessException e) {
// 业务异常,记录日志后继续抛出
log.error("业务异常: {}", e.getMessage());
throw e;
} catch (Exception e) {
// 系统异常,转换为统一的异常类型
log.error("系统异常", e);
throw new SystemException("系统错误", e);
}
}
```
## 九、常见问题
### 9.1 AOP不生效的原因
1. 目标类没有被Spring管理(未添加@Component等注解)
2. 调用的是类内部方法(self-invocation)
3. 方法是private的(Spring AOP只能代理public方法)
4. 没有启用AOP(@EnableAspectJAutoProxy)
### 9.2 JDK代理 vs CGLIB代理
- JDK代理:基于接口,目标类必须实现接口
- CGLIB代理:基于继承,可以代理没有实现接口的类
配置使用CGLIB代理:
```java
@EnableAspectJAutoProxy(proxyTargetClass = true)
```
### 9.3 内部方法调用问题
解决方案:
```java
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己的代理对象
public void method1() {
// 通过代理对象调用,AOP生效
self.method2();
}
@Transactional
public void method2() {
// 业务逻辑
}
}
```
## 十、总结
Spring AOP提供了一种优雅的方式来处理横切关注点,通过本笔记的学习,你应该掌握了:
1. AOP的核心概念和原理
2. 基于注解和XML的AOP配置方式
3. 各种切入点表达式的使用
4. 不同类型通知的应用场景
5. 实际项目中的AOP应用案例
6. AOP的最佳实践和常见问题解决
建议通过实际编码练习来加深理解,特别是在日志记录、权限控制、事务管理等场景中应用AOP。