- 创建Spring AOP详细学习笔记,包含基础概念、使用方式、切入点表达式等 - 创建Spring AOP自定义注解详解,包含实战案例和最佳实践 - 涵盖日志、权限、缓存、限流、重试等常见应用场景 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
669 lines
19 KiB
Markdown
669 lines
19 KiB
Markdown
# 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
|
||
<dependency>
|
||
<groupId>org.springframework</groupId>
|
||
<artifactId>spring-aop</artifactId>
|
||
<version>5.3.23</version>
|
||
</dependency>
|
||
<dependency>
|
||
<groupId>org.aspectj</groupId>
|
||
<artifactId>aspectjweaver</artifactId>
|
||
<version>1.9.7</version>
|
||
</dependency>
|
||
```
|
||
|
||
### 2.2 启用AOP
|
||
#### 注解方式启用
|
||
```java
|
||
@Configuration
|
||
@EnableAspectJAutoProxy
|
||
public class AppConfig {
|
||
// 配置类内容
|
||
}
|
||
```
|
||
|
||
#### XML方式启用
|
||
```xml
|
||
<aop:aspectj-autoproxy/>
|
||
```
|
||
|
||
## 三、基于注解的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
|
||
<?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:aop="http://www.springframework.org/schema/aop"
|
||
xsi:schemaLocation="
|
||
http://www.springframework.org/schema/beans
|
||
http://www.springframework.org/schema/beans/spring-beans.xsd
|
||
http://www.springframework.org/schema/aop
|
||
http://www.springframework.org/schema/aop/spring-aop.xsd">
|
||
|
||
<!-- 定义切面Bean -->
|
||
<bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>
|
||
|
||
<!-- AOP配置 -->
|
||
<aop:config>
|
||
<!-- 定义切入点 -->
|
||
<aop:pointcut id="serviceOperation"
|
||
expression="execution(* com.example.service.*.*(..))"/>
|
||
|
||
<!-- 定义切面 -->
|
||
<aop:aspect ref="loggingAspect">
|
||
<!-- 前置通知 -->
|
||
<aop:before method="logBefore" pointcut-ref="serviceOperation"/>
|
||
|
||
<!-- 后置通知 -->
|
||
<aop:after method="logAfter" pointcut-ref="serviceOperation"/>
|
||
|
||
<!-- 返回通知 -->
|
||
<aop:after-returning method="logAfterReturning"
|
||
returning="result"
|
||
pointcut-ref="serviceOperation"/>
|
||
|
||
<!-- 异常通知 -->
|
||
<aop:after-throwing method="logAfterThrowing"
|
||
throwing="error"
|
||
pointcut-ref="serviceOperation"/>
|
||
|
||
<!-- 环绕通知 -->
|
||
<aop:around method="logAround" pointcut-ref="serviceOperation"/>
|
||
</aop:aspect>
|
||
</aop:config>
|
||
</beans>
|
||
```
|
||
|
||
### 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<String, Object> 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。 |