notes/Spring-AOP-学习笔记.md
root 2a876d1d38 添加Spring AOP学习笔记
- 创建Spring AOP详细学习笔记,包含基础概念、使用方式、切入点表达式等
- 创建Spring AOP自定义注解详解,包含实战案例和最佳实践
- 涵盖日志、权限、缓存、限流、重试等常见应用场景

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-26 01:15:22 +08:00

19 KiB
Raw Permalink Blame History

Spring AOP 详细学习笔记

一、AOP基础概念

1.1 什么是AOP

AOPAspect-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不生效的原因

  1. 目标类没有被Spring管理未添加@Component等注解
  2. 调用的是类内部方法self-invocation
  3. 方法是private的Spring AOP只能代理public方法
  4. 没有启用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提供了一种优雅的方式来处理横切关注点通过本笔记的学习你应该掌握了

  1. AOP的核心概念和原理
  2. 基于注解和XML的AOP配置方式
  3. 各种切入点表达式的使用
  4. 不同类型通知的应用场景
  5. 实际项目中的AOP应用案例
  6. AOP的最佳实践和常见问题解决

建议通过实际编码练习来加深理解特别是在日志记录、权限控制、事务管理等场景中应用AOP。