- 创建Spring AOP详细学习笔记,包含基础概念、使用方式、切入点表达式等 - 创建Spring AOP自定义注解详解,包含实战案例和最佳实践 - 涵盖日志、权限、缓存、限流、重试等常见应用场景 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
19 KiB
19 KiB
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 引入依赖
<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
注解方式启用
@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
// 配置类内容
}
XML方式启用
<aop:aspectj-autoproxy/>
三、基于注解的AOP
3.1 定义切面类
@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 示例服务类
@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?)
示例:
// 匹配所有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表达式
限制匹配特定类型内的连接点。
// 匹配指定包下的所有方法
@Pointcut("within(com.example.service.*)")
// 匹配指定类的所有方法
@Pointcut("within(com.example.service.UserService)")
4.3 @annotation表达式
匹配带有特定注解的方法。
// 自定义注解
@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表达式
匹配特定注解标注的类内的所有方法。
@Pointcut("@within(org.springframework.stereotype.Service)")
public void allServiceMethods() {}
4.5 args表达式
匹配特定参数类型的方法。
// 匹配只有一个String参数的方法
@Pointcut("args(String)")
// 匹配第一个参数为String的方法
@Pointcut("args(String,..)")
4.6 组合切入点表达式
使用&&、||、!组合多个切入点表达式。
@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 获取方法参数
@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 获取目标对象和代理对象
@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 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配置的切面类
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 日志记录
@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 权限控制
@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 缓存处理
@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 事务管理
@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 性能监控
@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 重试机制
@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注解指定优先级:
@Aspect
@Component
@Order(1) // 数字越小,优先级越高
public class SecurityAspect {
// 安全检查逻辑
}
@Aspect
@Component
@Order(2)
public class LoggingAspect {
// 日志记录逻辑
}
8.2 避免循环依赖
切面不应该通知自己,避免无限循环:
@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 异常处理
在环绕通知中正确处理异常:
@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不生效的原因
- 目标类没有被Spring管理(未添加@Component等注解)
- 调用的是类内部方法(self-invocation)
- 方法是private的(Spring AOP只能代理public方法)
- 没有启用AOP(@EnableAspectJAutoProxy)
9.2 JDK代理 vs CGLIB代理
- JDK代理:基于接口,目标类必须实现接口
- CGLIB代理:基于继承,可以代理没有实现接口的类
配置使用CGLIB代理:
@EnableAspectJAutoProxy(proxyTargetClass = true)
9.3 内部方法调用问题
解决方案:
@Service
public class UserService {
@Autowired
private UserService self; // 注入自己的代理对象
public void method1() {
// 通过代理对象调用,AOP生效
self.method2();
}
@Transactional
public void method2() {
// 业务逻辑
}
}
十、总结
Spring AOP提供了一种优雅的方式来处理横切关注点,通过本笔记的学习,你应该掌握了:
- AOP的核心概念和原理
- 基于注解和XML的AOP配置方式
- 各种切入点表达式的使用
- 不同类型通知的应用场景
- 实际项目中的AOP应用案例
- AOP的最佳实践和常见问题解决
建议通过实际编码练习来加深理解,特别是在日志记录、权限控制、事务管理等场景中应用AOP。