添加Spring AOP学习笔记
- 创建Spring AOP详细学习笔记,包含基础概念、使用方式、切入点表达式等 - 创建Spring AOP自定义注解详解,包含实战案例和最佳实践 - 涵盖日志、权限、缓存、限流、重试等常见应用场景 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
af08782474
commit
2a876d1d38
669
Spring-AOP-学习笔记.md
Normal file
669
Spring-AOP-学习笔记.md
Normal file
@ -0,0 +1,669 @@
|
|||||||
|
# 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。
|
||||||
971
Spring-AOP-自定义注解详解.md
Normal file
971
Spring-AOP-自定义注解详解.md
Normal file
@ -0,0 +1,971 @@
|
|||||||
|
# Spring AOP 自定义注解详解
|
||||||
|
|
||||||
|
## 一、为什么使用自定义注解配合AOP
|
||||||
|
|
||||||
|
### 1.1 优势
|
||||||
|
1. **代码简洁**:只需在方法上添加注解即可
|
||||||
|
2. **语义明确**:注解名称直接表达意图
|
||||||
|
3. **易于维护**:修改切面逻辑不影响业务代码
|
||||||
|
4. **灵活配置**:通过注解参数传递配置信息
|
||||||
|
5. **解耦合**:业务逻辑与横切关注点分离
|
||||||
|
|
||||||
|
### 1.2 对比传统方式
|
||||||
|
```java
|
||||||
|
// 传统方式:基于方法名匹配
|
||||||
|
@Pointcut("execution(* com.example.service.*.*(..))")
|
||||||
|
|
||||||
|
// 注解方式:更精确、更灵活
|
||||||
|
@Pointcut("@annotation(com.example.annotation.Log)")
|
||||||
|
```
|
||||||
|
|
||||||
|
## 二、自定义注解的创建
|
||||||
|
|
||||||
|
### 2.1 基本结构
|
||||||
|
```java
|
||||||
|
@Target(ElementType.METHOD) // 注解作用目标
|
||||||
|
@Retention(RetentionPolicy.RUNTIME) // 注解保留策略
|
||||||
|
@Documented // 生成文档
|
||||||
|
public @interface Log {
|
||||||
|
String value() default ""; // 注解参数
|
||||||
|
String module() default ""; // 模块名
|
||||||
|
LogLevel level() default LogLevel.INFO; // 日志级别
|
||||||
|
}
|
||||||
|
|
||||||
|
// 枚举类型
|
||||||
|
public enum LogLevel {
|
||||||
|
DEBUG, INFO, WARN, ERROR
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 元注解说明
|
||||||
|
- **@Target**:指定注解可以用在哪些地方
|
||||||
|
- ElementType.METHOD:方法
|
||||||
|
- ElementType.TYPE:类
|
||||||
|
- ElementType.FIELD:字段
|
||||||
|
- ElementType.PARAMETER:参数
|
||||||
|
|
||||||
|
- **@Retention**:指定注解的生命周期
|
||||||
|
- RetentionPolicy.SOURCE:源码期
|
||||||
|
- RetentionPolicy.CLASS:编译期
|
||||||
|
- RetentionPolicy.RUNTIME:运行期(AOP必须使用)
|
||||||
|
|
||||||
|
- **@Documented**:注解信息包含在JavaDoc中
|
||||||
|
- **@Inherited**:子类可以继承父类的注解
|
||||||
|
|
||||||
|
## 三、完整示例:日志注解
|
||||||
|
|
||||||
|
### 3.1 创建日志注解
|
||||||
|
```java
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface SysLog {
|
||||||
|
// 操作描述
|
||||||
|
String value() default "";
|
||||||
|
|
||||||
|
// 操作类型
|
||||||
|
OperationType type() default OperationType.OTHER;
|
||||||
|
|
||||||
|
// 是否保存请求参数
|
||||||
|
boolean saveParams() default true;
|
||||||
|
|
||||||
|
// 是否保存响应结果
|
||||||
|
boolean saveResult() default true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 操作类型枚举
|
||||||
|
public enum OperationType {
|
||||||
|
INSERT("新增"),
|
||||||
|
UPDATE("修改"),
|
||||||
|
DELETE("删除"),
|
||||||
|
SELECT("查询"),
|
||||||
|
EXPORT("导出"),
|
||||||
|
IMPORT("导入"),
|
||||||
|
OTHER("其他");
|
||||||
|
|
||||||
|
private final String desc;
|
||||||
|
|
||||||
|
OperationType(String desc) {
|
||||||
|
this.desc = desc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDesc() {
|
||||||
|
return desc;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 创建日志切面
|
||||||
|
```java
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class SysLogAspect {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private SysLogService sysLogService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
// 使用@annotation匹配带有@SysLog注解的方法
|
||||||
|
@Pointcut("@annotation(com.example.annotation.SysLog)")
|
||||||
|
public void logPointCut() {}
|
||||||
|
|
||||||
|
@Around("logPointCut()")
|
||||||
|
public Object around(ProceedingJoinPoint point) throws Throwable {
|
||||||
|
long beginTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
// 获取注解信息
|
||||||
|
SysLog sysLog = getAnnotation(point);
|
||||||
|
|
||||||
|
// 创建日志对象
|
||||||
|
SysLogEntity logEntity = new SysLogEntity();
|
||||||
|
logEntity.setOperation(sysLog.value());
|
||||||
|
logEntity.setType(sysLog.type().getDesc());
|
||||||
|
logEntity.setCreateTime(new Date());
|
||||||
|
|
||||||
|
// 获取请求信息
|
||||||
|
logEntity.setIp(getIpAddress(request));
|
||||||
|
logEntity.setUrl(request.getRequestURL().toString());
|
||||||
|
logEntity.setMethod(request.getMethod());
|
||||||
|
|
||||||
|
// 获取用户信息
|
||||||
|
String username = getCurrentUsername();
|
||||||
|
logEntity.setUsername(username);
|
||||||
|
|
||||||
|
// 获取方法信息
|
||||||
|
String className = point.getTarget().getClass().getName();
|
||||||
|
String methodName = point.getSignature().getName();
|
||||||
|
logEntity.setClassName(className);
|
||||||
|
logEntity.setMethodName(methodName);
|
||||||
|
|
||||||
|
// 保存请求参数
|
||||||
|
if (sysLog.saveParams()) {
|
||||||
|
Object[] args = point.getArgs();
|
||||||
|
String params = JSON.toJSONString(args);
|
||||||
|
logEntity.setParams(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 执行方法
|
||||||
|
Object result = point.proceed();
|
||||||
|
|
||||||
|
// 保存响应结果
|
||||||
|
if (sysLog.saveResult() && result != null) {
|
||||||
|
String resultStr = JSON.toJSONString(result);
|
||||||
|
logEntity.setResult(resultStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置执行时间
|
||||||
|
long time = System.currentTimeMillis() - beginTime;
|
||||||
|
logEntity.setTime(time);
|
||||||
|
logEntity.setStatus(1); // 成功
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// 记录异常信息
|
||||||
|
logEntity.setStatus(0); // 失败
|
||||||
|
logEntity.setError(e.getMessage());
|
||||||
|
logEntity.setTime(System.currentTimeMillis() - beginTime);
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
// 异步保存日志
|
||||||
|
CompletableFuture.runAsync(() -> sysLogService.save(logEntity));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取注解对象
|
||||||
|
private SysLog getAnnotation(ProceedingJoinPoint point) {
|
||||||
|
MethodSignature signature = (MethodSignature) point.getSignature();
|
||||||
|
Method method = signature.getMethod();
|
||||||
|
return method.getAnnotation(SysLog.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取IP地址
|
||||||
|
private String getIpAddress(HttpServletRequest request) {
|
||||||
|
String ip = request.getHeader("X-Forwarded-For");
|
||||||
|
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("Proxy-Client-IP");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前用户名(示例)
|
||||||
|
private String getCurrentUsername() {
|
||||||
|
// 从Security Context或Session中获取
|
||||||
|
return "admin";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 使用示例
|
||||||
|
```java
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@SysLog(value = "新增用户", type = OperationType.INSERT)
|
||||||
|
public Result<User> createUser(@RequestBody User user) {
|
||||||
|
User savedUser = userService.save(user);
|
||||||
|
return Result.success(savedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@SysLog(value = "修改用户信息", type = OperationType.UPDATE)
|
||||||
|
public Result<User> updateUser(@PathVariable Long id, @RequestBody User user) {
|
||||||
|
user.setId(id);
|
||||||
|
User updatedUser = userService.update(user);
|
||||||
|
return Result.success(updatedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
@SysLog(value = "删除用户", type = OperationType.DELETE, saveResult = false)
|
||||||
|
public Result<Void> deleteUser(@PathVariable Long id) {
|
||||||
|
userService.delete(id);
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
@SysLog(value = "查询用户详情", type = OperationType.SELECT, saveParams = false)
|
||||||
|
public Result<User> getUser(@PathVariable Long id) {
|
||||||
|
User user = userService.findById(id);
|
||||||
|
return Result.success(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 四、实战案例集合
|
||||||
|
|
||||||
|
### 4.1 权限控制注解
|
||||||
|
```java
|
||||||
|
// 权限注解定义
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface RequiresPermission {
|
||||||
|
// 权限标识
|
||||||
|
String[] value();
|
||||||
|
|
||||||
|
// 逻辑关系:AND 或 OR
|
||||||
|
Logical logical() default Logical.AND;
|
||||||
|
|
||||||
|
enum Logical {
|
||||||
|
AND, OR
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 角色注解
|
||||||
|
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface RequiresRole {
|
||||||
|
String[] value();
|
||||||
|
Logical logical() default Logical.AND;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 权限切面
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class PermissionAspect {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private AuthService authService;
|
||||||
|
|
||||||
|
@Before("@annotation(permission)")
|
||||||
|
public void checkPermission(RequiresPermission permission) {
|
||||||
|
String[] requiredPerms = permission.value();
|
||||||
|
Logical logical = permission.logical();
|
||||||
|
|
||||||
|
Set<String> userPerms = authService.getCurrentUserPermissions();
|
||||||
|
|
||||||
|
boolean hasPermission = false;
|
||||||
|
if (logical == Logical.AND) {
|
||||||
|
// 需要所有权限
|
||||||
|
hasPermission = userPerms.containsAll(Arrays.asList(requiredPerms));
|
||||||
|
} else {
|
||||||
|
// 只需要其中一个权限
|
||||||
|
hasPermission = Arrays.stream(requiredPerms)
|
||||||
|
.anyMatch(userPerms::contains);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
throw new UnauthorizedException("权限不足:需要权限 " + Arrays.toString(requiredPerms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before("@annotation(role)")
|
||||||
|
public void checkRole(RequiresRole role) {
|
||||||
|
String[] requiredRoles = role.value();
|
||||||
|
Set<String> userRoles = authService.getCurrentUserRoles();
|
||||||
|
|
||||||
|
boolean hasRole = Arrays.stream(requiredRoles)
|
||||||
|
.anyMatch(userRoles::contains);
|
||||||
|
|
||||||
|
if (!hasRole) {
|
||||||
|
throw new UnauthorizedException("角色不足:需要角色 " + Arrays.toString(requiredRoles));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/admin")
|
||||||
|
public class AdminController {
|
||||||
|
|
||||||
|
@GetMapping("/users")
|
||||||
|
@RequiresPermission("user:view")
|
||||||
|
public List<User> listUsers() {
|
||||||
|
// 查看用户列表
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/users")
|
||||||
|
@RequiresPermission({"user:create", "user:edit"})
|
||||||
|
public User createUser(@RequestBody User user) {
|
||||||
|
// 创建用户(需要同时有创建和编辑权限)
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/users/{id}")
|
||||||
|
@RequiresRole("ADMIN")
|
||||||
|
@RequiresPermission("user:delete")
|
||||||
|
public void deleteUser(@PathVariable Long id) {
|
||||||
|
// 删除用户(需要管理员角色和删除权限)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 缓存注解
|
||||||
|
```java
|
||||||
|
// 缓存注解
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface Cache {
|
||||||
|
// 缓存key前缀
|
||||||
|
String prefix() default "";
|
||||||
|
|
||||||
|
// 缓存key(支持SpEL表达式)
|
||||||
|
String key() default "";
|
||||||
|
|
||||||
|
// 过期时间(秒)
|
||||||
|
int expire() default 300;
|
||||||
|
|
||||||
|
// 条件(SpEL表达式,返回true时才缓存)
|
||||||
|
String condition() default "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除缓存注解
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface CacheEvict {
|
||||||
|
String prefix() default "";
|
||||||
|
String key() default "";
|
||||||
|
boolean allEntries() default false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存切面
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class CacheAspect {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
private final SpelExpressionParser parser = new SpelExpressionParser();
|
||||||
|
|
||||||
|
@Around("@annotation(cache)")
|
||||||
|
public Object cache(ProceedingJoinPoint point, Cache cache) throws Throwable {
|
||||||
|
// 构建缓存key
|
||||||
|
String key = buildKey(point, cache.prefix(), cache.key());
|
||||||
|
|
||||||
|
// 检查条件
|
||||||
|
if (!checkCondition(point, cache.condition())) {
|
||||||
|
return point.proceed();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试从缓存获取
|
||||||
|
Object cached = redisTemplate.opsForValue().get(key);
|
||||||
|
if (cached != null) {
|
||||||
|
log.debug("缓存命中: {}", key);
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行方法
|
||||||
|
Object result = point.proceed();
|
||||||
|
|
||||||
|
// 存入缓存
|
||||||
|
if (result != null) {
|
||||||
|
redisTemplate.opsForValue().set(key, result, cache.expire(), TimeUnit.SECONDS);
|
||||||
|
log.debug("缓存存储: {}", key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@After("@annotation(evict)")
|
||||||
|
public void evict(JoinPoint point, CacheEvict evict) {
|
||||||
|
if (evict.allEntries()) {
|
||||||
|
// 清除所有缓存
|
||||||
|
Set<String> keys = redisTemplate.keys(evict.prefix() + "*");
|
||||||
|
if (keys != null && !keys.isEmpty()) {
|
||||||
|
redisTemplate.delete(keys);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 清除指定缓存
|
||||||
|
String key = buildKey(point, evict.prefix(), evict.key());
|
||||||
|
redisTemplate.delete(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建缓存key
|
||||||
|
private String buildKey(JoinPoint point, String prefix, String key) {
|
||||||
|
if (StringUtils.isEmpty(key)) {
|
||||||
|
// 默认使用方法签名作为key
|
||||||
|
return prefix + ":" + point.getSignature().toLongString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析SpEL表达式
|
||||||
|
EvaluationContext context = new StandardEvaluationContext();
|
||||||
|
Method method = ((MethodSignature) point.getSignature()).getMethod();
|
||||||
|
String[] paramNames = new LocalVariableTableParameterNameDiscoverer()
|
||||||
|
.getParameterNames(method);
|
||||||
|
Object[] args = point.getArgs();
|
||||||
|
|
||||||
|
for (int i = 0; i < paramNames.length; i++) {
|
||||||
|
context.setVariable(paramNames[i], args[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Expression expression = parser.parseExpression(key);
|
||||||
|
return prefix + ":" + expression.getValue(context, String.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查条件
|
||||||
|
private boolean checkCondition(JoinPoint point, String condition) {
|
||||||
|
if (StringUtils.isEmpty(condition)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析条件表达式
|
||||||
|
EvaluationContext context = new StandardEvaluationContext();
|
||||||
|
// ... 设置变量
|
||||||
|
|
||||||
|
Expression expression = parser.parseExpression(condition);
|
||||||
|
return Boolean.TRUE.equals(expression.getValue(context, Boolean.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
@Service
|
||||||
|
public class ProductService {
|
||||||
|
|
||||||
|
@Cache(prefix = "product", key = "#id", expire = 600)
|
||||||
|
public Product findById(Long id) {
|
||||||
|
// 从数据库查询
|
||||||
|
return productRepository.findById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Cache(prefix = "product", key = "#category + ':' + #page", expire = 300)
|
||||||
|
public List<Product> findByCategory(String category, int page) {
|
||||||
|
// 分页查询
|
||||||
|
return productRepository.findByCategory(category, page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict(prefix = "product", key = "#product.id")
|
||||||
|
public void updateProduct(Product product) {
|
||||||
|
// 更新产品,清除缓存
|
||||||
|
productRepository.save(product);
|
||||||
|
}
|
||||||
|
|
||||||
|
@CacheEvict(prefix = "product", allEntries = true)
|
||||||
|
public void clearAllCache() {
|
||||||
|
// 清除所有产品缓存
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 限流注解
|
||||||
|
```java
|
||||||
|
// 限流注解
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface RateLimit {
|
||||||
|
// 限流key
|
||||||
|
String key() default "";
|
||||||
|
|
||||||
|
// 时间窗口(秒)
|
||||||
|
int period() default 60;
|
||||||
|
|
||||||
|
// 限流次数
|
||||||
|
int count() default 100;
|
||||||
|
|
||||||
|
// 限流类型
|
||||||
|
LimitType limitType() default LimitType.IP;
|
||||||
|
|
||||||
|
enum LimitType {
|
||||||
|
IP, // 根据IP限流
|
||||||
|
USER, // 根据用户限流
|
||||||
|
GLOBAL, // 全局限流
|
||||||
|
CUSTOM // 自定义key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 限流切面
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class RateLimitAspect {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private HttpServletRequest request;
|
||||||
|
|
||||||
|
@Before("@annotation(rateLimit)")
|
||||||
|
public void limit(JoinPoint point, RateLimit rateLimit) {
|
||||||
|
String key = buildLimitKey(point, rateLimit);
|
||||||
|
|
||||||
|
// 使用Redis实现滑动窗口限流
|
||||||
|
long currentTime = System.currentTimeMillis();
|
||||||
|
long windowStart = currentTime - rateLimit.period() * 1000;
|
||||||
|
|
||||||
|
// 移除窗口外的记录
|
||||||
|
redisTemplate.opsForZSet().removeRangeByScore(key, 0, windowStart);
|
||||||
|
|
||||||
|
// 获取窗口内的请求数
|
||||||
|
Long count = redisTemplate.opsForZSet().count(key, windowStart, currentTime);
|
||||||
|
|
||||||
|
if (count != null && count >= rateLimit.count()) {
|
||||||
|
throw new RateLimitException("访问太频繁,请稍后再试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加当前请求
|
||||||
|
redisTemplate.opsForZSet().add(key, UUID.randomUUID().toString(), currentTime);
|
||||||
|
|
||||||
|
// 设置过期时间
|
||||||
|
redisTemplate.expire(key, rateLimit.period(), TimeUnit.SECONDS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildLimitKey(JoinPoint point, RateLimit rateLimit) {
|
||||||
|
StringBuilder key = new StringBuilder("rate_limit:");
|
||||||
|
|
||||||
|
switch (rateLimit.limitType()) {
|
||||||
|
case IP:
|
||||||
|
key.append(getIpAddress()).append(":");
|
||||||
|
break;
|
||||||
|
case USER:
|
||||||
|
key.append(getCurrentUserId()).append(":");
|
||||||
|
break;
|
||||||
|
case GLOBAL:
|
||||||
|
key.append("global:");
|
||||||
|
break;
|
||||||
|
case CUSTOM:
|
||||||
|
key.append(rateLimit.key()).append(":");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加方法签名
|
||||||
|
key.append(point.getSignature().toLongString());
|
||||||
|
|
||||||
|
return key.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getIpAddress() {
|
||||||
|
// 获取真实IP
|
||||||
|
String ip = request.getHeader("X-Real-IP");
|
||||||
|
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("X-Forwarded-For");
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCurrentUserId() {
|
||||||
|
// 从认证信息中获取用户ID
|
||||||
|
return "user123";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api")
|
||||||
|
public class ApiController {
|
||||||
|
|
||||||
|
@GetMapping("/search")
|
||||||
|
@RateLimit(period = 60, count = 10, limitType = LimitType.IP)
|
||||||
|
public Result search(@RequestParam String keyword) {
|
||||||
|
// IP限流:每个IP每分钟最多10次
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/order")
|
||||||
|
@RateLimit(period = 3600, count = 100, limitType = LimitType.USER)
|
||||||
|
public Result createOrder(@RequestBody Order order) {
|
||||||
|
// 用户限流:每个用户每小时最多100单
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/hot-deal")
|
||||||
|
@RateLimit(period = 1, count = 1000, limitType = LimitType.GLOBAL)
|
||||||
|
public Result getHotDeal() {
|
||||||
|
// 全局限流:每秒最多1000次请求
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 重试注解
|
||||||
|
```java
|
||||||
|
// 重试注解
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface Retry {
|
||||||
|
// 重试次数
|
||||||
|
int maxAttempts() default 3;
|
||||||
|
|
||||||
|
// 重试间隔(毫秒)
|
||||||
|
long delay() default 1000;
|
||||||
|
|
||||||
|
// 指数退避
|
||||||
|
boolean exponentialBackoff() default false;
|
||||||
|
|
||||||
|
// 需要重试的异常类型
|
||||||
|
Class<? extends Throwable>[] retryFor() default {Exception.class};
|
||||||
|
|
||||||
|
// 不需要重试的异常类型
|
||||||
|
Class<? extends Throwable>[] noRetryFor() default {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重试切面
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class RetryAspect {
|
||||||
|
|
||||||
|
@Around("@annotation(retry)")
|
||||||
|
public Object retry(ProceedingJoinPoint point, Retry retry) throws Throwable {
|
||||||
|
int attempts = 0;
|
||||||
|
long delay = retry.delay();
|
||||||
|
|
||||||
|
while (attempts < retry.maxAttempts()) {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
return point.proceed();
|
||||||
|
} catch (Throwable e) {
|
||||||
|
// 检查是否需要重试
|
||||||
|
if (!shouldRetry(e, retry)) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempts >= retry.maxAttempts()) {
|
||||||
|
log.error("重试{}次后仍然失败", retry.maxAttempts());
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.warn("方法执行失败,{}ms后进行第{}次重试", delay, attempts);
|
||||||
|
|
||||||
|
Thread.sleep(delay);
|
||||||
|
|
||||||
|
// 指数退避
|
||||||
|
if (retry.exponentialBackoff()) {
|
||||||
|
delay = delay * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalStateException("不应该到达这里");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldRetry(Throwable e, Retry retry) {
|
||||||
|
// 检查不需要重试的异常
|
||||||
|
for (Class<? extends Throwable> noRetryClass : retry.noRetryFor()) {
|
||||||
|
if (noRetryClass.isAssignableFrom(e.getClass())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查需要重试的异常
|
||||||
|
for (Class<? extends Throwable> retryClass : retry.retryFor()) {
|
||||||
|
if (retryClass.isAssignableFrom(e.getClass())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
@Service
|
||||||
|
public class PaymentService {
|
||||||
|
|
||||||
|
@Retry(maxAttempts = 3, delay = 2000, retryFor = {TimeoutException.class, ConnectException.class})
|
||||||
|
public PaymentResult processPayment(PaymentRequest request) {
|
||||||
|
// 支付处理,可能超时
|
||||||
|
return paymentGateway.process(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Retry(maxAttempts = 5, delay = 1000, exponentialBackoff = true)
|
||||||
|
public void sendNotification(String message) {
|
||||||
|
// 发送通知,使用指数退避策略
|
||||||
|
notificationService.send(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 参数校验注解
|
||||||
|
```java
|
||||||
|
// 参数校验注解
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface ValidateParams {
|
||||||
|
// 是否校验请求参数
|
||||||
|
boolean validateRequest() default true;
|
||||||
|
|
||||||
|
// 是否校验响应结果
|
||||||
|
boolean validateResponse() default false;
|
||||||
|
|
||||||
|
// 校验组
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 校验切面
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class ValidationAspect {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private Validator validator;
|
||||||
|
|
||||||
|
@Around("@annotation(validateParams)")
|
||||||
|
public Object validate(ProceedingJoinPoint point, ValidateParams validateParams) throws Throwable {
|
||||||
|
// 校验请求参数
|
||||||
|
if (validateParams.validateRequest()) {
|
||||||
|
Object[] args = point.getArgs();
|
||||||
|
for (Object arg : args) {
|
||||||
|
if (arg != null && !isBasicType(arg)) {
|
||||||
|
Set<ConstraintViolation<Object>> violations = validator.validate(arg, validateParams.groups());
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
throw new ValidationException(buildErrorMessage(violations));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行方法
|
||||||
|
Object result = point.proceed();
|
||||||
|
|
||||||
|
// 校验响应结果
|
||||||
|
if (validateParams.validateResponse() && result != null) {
|
||||||
|
Set<ConstraintViolation<Object>> violations = validator.validate(result, validateParams.groups());
|
||||||
|
if (!violations.isEmpty()) {
|
||||||
|
log.error("响应数据校验失败: {}", buildErrorMessage(violations));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isBasicType(Object obj) {
|
||||||
|
return obj instanceof String || obj instanceof Number ||
|
||||||
|
obj instanceof Boolean || obj instanceof Character;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildErrorMessage(Set<ConstraintViolation<Object>> violations) {
|
||||||
|
return violations.stream()
|
||||||
|
.map(v -> v.getPropertyPath() + " " + v.getMessage())
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用示例
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/user")
|
||||||
|
public class UserController {
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@ValidateParams(groups = {Create.class})
|
||||||
|
public Result<User> createUser(@RequestBody User user) {
|
||||||
|
// 自动校验User对象的Create组约束
|
||||||
|
return Result.success(userService.save(user));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
@ValidateParams(groups = {Update.class}, validateResponse = true)
|
||||||
|
public Result<User> updateUser(@PathVariable Long id, @RequestBody User user) {
|
||||||
|
// 校验请求和响应
|
||||||
|
return Result.success(userService.update(user));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 实体类
|
||||||
|
@Data
|
||||||
|
public class User {
|
||||||
|
@Null(groups = Create.class)
|
||||||
|
@NotNull(groups = Update.class)
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@NotBlank(message = "用户名不能为空")
|
||||||
|
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Email(message = "邮箱格式不正确")
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||||
|
private String phone;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 五、高级技巧
|
||||||
|
|
||||||
|
### 5.1 组合注解
|
||||||
|
```java
|
||||||
|
// 组合多个注解功能
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@SysLog
|
||||||
|
@RequiresPermission
|
||||||
|
@RateLimit
|
||||||
|
public @interface SecureApi {
|
||||||
|
// 日志描述
|
||||||
|
String value() default "";
|
||||||
|
|
||||||
|
// 需要的权限
|
||||||
|
String[] permissions() default {};
|
||||||
|
|
||||||
|
// 限流配置
|
||||||
|
int rateLimit() default 100;
|
||||||
|
int ratePeriod() default 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用组合注解
|
||||||
|
@PostMapping("/sensitive-operation")
|
||||||
|
@SecureApi(
|
||||||
|
value = "执行敏感操作",
|
||||||
|
permissions = {"admin:sensitive:operate"},
|
||||||
|
rateLimit = 10,
|
||||||
|
ratePeriod = 60
|
||||||
|
)
|
||||||
|
public Result sensitiveOperation(@RequestBody Request request) {
|
||||||
|
// 一个注解实现:日志记录 + 权限校验 + 限流
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 注解继承
|
||||||
|
```java
|
||||||
|
// 基础审计注解
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface Auditable {
|
||||||
|
boolean enabled() default true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继承审计功能的业务注解
|
||||||
|
@Target(ElementType.METHOD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
@Auditable
|
||||||
|
public @interface BankingOperation {
|
||||||
|
String operationType();
|
||||||
|
RiskLevel riskLevel() default RiskLevel.LOW;
|
||||||
|
|
||||||
|
enum RiskLevel {
|
||||||
|
LOW, MEDIUM, HIGH, CRITICAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统一处理所有审计注解
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class AuditAspect {
|
||||||
|
|
||||||
|
@Around("@annotation(auditable) || @annotation(banking)")
|
||||||
|
public Object audit(ProceedingJoinPoint point, Auditable auditable, BankingOperation banking) throws Throwable {
|
||||||
|
// 处理审计逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 动态注解处理
|
||||||
|
```java
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
public class DynamicAnnotationAspect {
|
||||||
|
|
||||||
|
@Around("@annotation(anno)")
|
||||||
|
public Object handleDynamically(ProceedingJoinPoint point, Annotation anno) throws Throwable {
|
||||||
|
// 动态获取注解类型
|
||||||
|
Class<? extends Annotation> annotationType = anno.annotationType();
|
||||||
|
|
||||||
|
// 根据不同注解类型执行不同逻辑
|
||||||
|
if (annotationType == Cache.class) {
|
||||||
|
return handleCache(point, (Cache) anno);
|
||||||
|
} else if (annotationType == Log.class) {
|
||||||
|
return handleLog(point, (Log) anno);
|
||||||
|
}
|
||||||
|
|
||||||
|
return point.proceed();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 六、最佳实践总结
|
||||||
|
|
||||||
|
### 6.1 设计原则
|
||||||
|
1. **单一职责**:每个注解只负责一个功能
|
||||||
|
2. **语义清晰**:注解名称要表达明确意图
|
||||||
|
3. **参数简洁**:只提供必要的配置参数
|
||||||
|
4. **默认值合理**:为参数提供合理的默认值
|
||||||
|
|
||||||
|
### 6.2 性能优化
|
||||||
|
1. **异步处理**:日志等操作使用异步方式
|
||||||
|
2. **缓存结果**:避免重复计算
|
||||||
|
3. **条件判断**:提前判断是否需要执行切面逻辑
|
||||||
|
|
||||||
|
### 6.3 错误处理
|
||||||
|
1. **异常传播**:不要吞掉业务异常
|
||||||
|
2. **降级处理**:切面异常不应影响业务执行
|
||||||
|
3. **日志记录**:记录关键错误信息
|
||||||
|
|
||||||
|
### 6.4 测试建议
|
||||||
|
```java
|
||||||
|
@SpringBootTest
|
||||||
|
public class AopTest {
|
||||||
|
|
||||||
|
@MockBean
|
||||||
|
private SysLogService logService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testLogAspect() {
|
||||||
|
// 测试日志切面
|
||||||
|
User user = new User();
|
||||||
|
userService.createUser(user);
|
||||||
|
|
||||||
|
// 验证日志服务被调用
|
||||||
|
verify(logService, times(1)).save(any(SysLogEntity.class));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
自定义注解配合AOP是Spring中非常强大和优雅的编程方式,能够让代码更加模块化、可维护和可扩展。
|
||||||
Loading…
x
Reference in New Issue
Block a user