Skip to content

面向切面编程 (AOP)

什么是AOP?

AOP (Aspect-Oriented Programming) 是一种编程范式,用于将横切关注点(cross-cutting concerns)从业务逻辑中分离出来,提高代码的模块化程度。

横切关注点示例

  • 日志记录
  • 事务管理
  • 安全检查
  • 性能监控
  • 异常处理
  • 缓存

核心概念

1. 切面 (Aspect)

横切关注点的模块化,包含切点和通知。

java
@Aspect
@Component
public class LoggingAspect {
    // 这是一个切面
}

2. 连接点 (Join Point)

程序执行过程中的某个点,如方法调用、异常抛出等。

java
// 方法执行就是一个连接点
public void saveUser(User user) {
    // ...
}

3. 切点 (Pointcut)

匹配连接点的表达式,定义在哪里应用通知。

java
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceLayer() {
    // 切点定义
}

4. 通知 (Advice)

在特定连接点执行的动作。

  • @Before:前置通知,方法执行前
  • @After:后置通知,方法执行后(无论成功失败)
  • @AfterReturning:返回通知,方法成功返回后
  • @AfterThrowing:异常通知,方法抛出异常后
  • @Around:环绕通知,方法执行前后

5. 织入 (Weaving)

将切面应用到目标对象的过程。

切点表达式

execution表达式

语法:

execution(modifiers? return-type declaring-type? method-name(params) throws?)

示例:

java
// 匹配所有public方法
@Pointcut("execution(public * *(..))")

// 匹配service包下所有类的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")

// 匹配service包及其子包下所有类的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")

// 匹配UserService类的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")

// 匹配所有save开头的方法
@Pointcut("execution(* save*(..))")

// 匹配第一个参数为String类型的方法
@Pointcut("execution(* *(String, ..))")

// 匹配返回User类型的方法
@Pointcut("execution(com.example.domain.User *(..))")

within表达式

匹配指定类型内的方法执行。

java
// 匹配service包下所有类
@Pointcut("within(com.example.service.*)")

// 匹配service包及其子包下所有类
@Pointcut("within(com.example.service..*)")

// 匹配UserService类
@Pointcut("within(com.example.service.UserService)")

@annotation表达式

匹配带有指定注解的方法。

java
// 匹配带有@Transactional注解的方法
@Pointcut("@annotation(org.springframework.transaction.annotation.Transactional)")

// 自定义注解
@Pointcut("@annotation(com.example.annotation.Log)")

组合表达式

java
// AND
@Pointcut("execution(* com.example.service.*.*(..)) && within(com.example.service.*)")

// OR
@Pointcut("execution(* save*(..)) || execution(* update*(..))")

// NOT
@Pointcut("execution(* com.example.service.*.*(..)) && !execution(* com.example.service.Internal*.*(..))")

通知类型详解

1. @Before - 前置通知

在方法执行前执行。

java
@Aspect
@Component
public class SecurityAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    public void checkSecurity(JoinPoint joinPoint) {
        System.out.println("安全检查: " + joinPoint.getSignature().getName());
        // 执行安全检查逻辑
    }
}

2. @After - 后置通知

在方法执行后执行(无论成功或失败)。

java
@Aspect
@Component
public class LoggingAspect {
    
    @After("execution(* com.example.service.*.*(..))")
    public void logAfter(JoinPoint joinPoint) {
        System.out.println("方法执行完成: " + joinPoint.getSignature().getName());
    }
}

3. @AfterReturning - 返回通知

在方法成功返回后执行。

java
@Aspect
@Component
public class LoggingAspect {
    
    @AfterReturning(
        pointcut = "execution(* com.example.service.*.*(..))",
        returning = "result"
    )
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("方法返回值: " + result);
    }
}

4. @AfterThrowing - 异常通知

在方法抛出异常后执行。

java
@Aspect
@Component
public class ExceptionHandlingAspect {
    
    @AfterThrowing(
        pointcut = "execution(* com.example.service.*.*(..))",
        throwing = "ex"
    )
    public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
        System.err.println("方法抛出异常: " + ex.getMessage());
        // 记录异常日志
    }
}

5. @Around - 环绕通知

最强大的通知类型,可以完全控制方法执行。

java
@Aspect
@Component
public class PerformanceAspect {
    
    @Around("execution(* com.example.service.*.*(..))")
    public Object measurePerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        
        try {
            // 执行目标方法
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long duration = System.currentTimeMillis() - start;
            System.out.println(joinPoint.getSignature().getName() + 
                             " 执行时间: " + duration + "ms");
        }
    }
}

实战示例

1. 日志切面

java
@Aspect
@Component
@Slf4j
public class LoggingAspect {
    
    @Pointcut("execution(* com.example.service..*.*(..))")
    public void serviceLayer() {}
    
    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) {
        log.info("执行方法: {}.{}",
                joinPoint.getTarget().getClass().getName(),
                joinPoint.getSignature().getName());
        
        Object[] args = joinPoint.getArgs();
        if (args.length > 0) {
            log.info("方法参数: {}", Arrays.toString(args));
        }
    }
    
    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Object result) {
        log.info("方法返回: {}", result);
    }
    
    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Exception ex) {
        log.error("方法异常: {}.{}, 异常信息: {}",
                joinPoint.getTarget().getClass().getName(),
                joinPoint.getSignature().getName(),
                ex.getMessage());
    }
}

2. 性能监控切面

java
@Aspect
@Component
public class PerformanceMonitorAspect {
    
    private final Map<String, List<Long>> performanceData = new ConcurrentHashMap<>();
    
    @Around("execution(* com.example.service..*.*(..))")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().toShortString();
        long startTime = System.nanoTime();
        
        try {
            return joinPoint.proceed();
        } finally {
            long duration = System.nanoTime() - startTime;
            recordPerformance(methodName, duration);
            
            if (duration > 1_000_000_000) { // 超过1秒
                System.err.println("警告: " + methodName + 
                                 " 执行时间过长: " + duration / 1_000_000 + "ms");
            }
        }
    }
    
    private void recordPerformance(String methodName, long duration) {
        performanceData.computeIfAbsent(methodName, k -> new ArrayList<>())
                      .add(duration);
    }
    
    @Scheduled(fixedRate = 60000) // 每分钟报告一次
    public void reportPerformance() {
        performanceData.forEach((method, times) -> {
            long avg = times.stream().mapToLong(Long::longValue).sum() / times.size();
            System.out.println(method + " 平均执行时间: " + avg / 1_000_000 + "ms");
        });
        performanceData.clear();
    }
}

3. 缓存切面

java
@Aspect
@Component
public class CacheAspect {
    
    private final Map<String, Object> cache = new ConcurrentHashMap<>();
    
    @Around("@annotation(cacheable)")
    public Object cache(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
        String key = generateKey(joinPoint);
        
        // 尝试从缓存获取
        Object cachedResult = cache.get(key);
        if (cachedResult != null) {
            System.out.println("缓存命中: " + key);
            return cachedResult;
        }
        
        // 执行方法
        Object result = joinPoint.proceed();
        
        // 存入缓存
        cache.put(key, result);
        System.out.println("缓存更新: " + key);
        
        return result;
    }
    
    private String generateKey(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().toShortString();
        String args = Arrays.toString(joinPoint.getArgs());
        return methodName + ":" + args;
    }
}

// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
    String value() default "";
}

4. 事务切面(简化版)

java
@Aspect
@Component
public class TransactionAspect {
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    @Around("@annotation(org.springframework.transaction.annotation.Transactional)")
    public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
        TransactionStatus status = transactionManager.getTransaction(
            new DefaultTransactionDefinition()
        );
        
        try {
            Object result = joinPoint.proceed();
            transactionManager.commit(status);
            System.out.println("事务提交成功");
            return result;
        } catch (Exception e) {
            transactionManager.rollback(status);
            System.err.println("事务回滚: " + e.getMessage());
            throw e;
        }
    }
}

5. 自定义注解切面

java
// 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
    String value();
}

// 切面实现
@Aspect
@Component
public class PermissionAspect {
    
    @Before("@annotation(requirePermission)")
    public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
        String requiredPermission = requirePermission.value();
        
        // 获取当前用户
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        
        // 检查权限
        boolean hasPermission = auth.getAuthorities().stream()
            .anyMatch(a -> a.getAuthority().equals(requiredPermission));
        
        if (!hasPermission) {
            throw new AccessDeniedException("没有权限: " + requiredPermission);
        }
    }
}

// 使用
@Service
public class UserService {
    
    @RequirePermission("USER:DELETE")
    public void deleteUser(Long userId) {
        // 删除用户
    }
}

配置AOP

启用AOP

java
@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
    // 配置类
}

依赖配置

Maven:

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

最佳实践

  1. 合理使用切点表达式

    • 尽量精确匹配,避免过度拦截
    • 使用组合表达式提高可读性
  2. 选择合适的通知类型

    • 简单场景用@Before或@After
    • 需要控制流程用@Around
    • 处理异常用@AfterThrowing
  3. 注意性能影响

    • AOP会增加方法调用开销
    • 避免在高频方法上使用复杂切面
  4. 避免循环依赖

    • 切面不要依赖被切的Bean
    • 使用@Lazy延迟加载
  5. 切面执行顺序

    • 使用@Order注解控制顺序
    • 数字越小优先级越高
java
@Aspect
@Component
@Order(1)
public class FirstAspect {
    // 优先执行
}

@Aspect
@Component
@Order(2)
public class SecondAspect {
    // 后执行
}
  1. 测试切面
java
@SpringBootTest
class LoggingAspectTest {
    
    @Autowired
    private UserService userService;
    
    @Test
    void testLoggingAspect() {
        // 测试切面是否正常工作
        userService.getUser(1L);
    }
}

常见问题

1. 切面不生效

可能原因:

  • 没有启用AOP:添加@EnableAspectJAutoProxy
  • 切点表达式不正确
  • 目标类没有被Spring管理
  • 类内部调用方法(self-invocation)

2. 类内部调用问题

java
@Service
public class UserService {
    
    public void methodA() {
        // 直接调用,AOP不会生效
        this.methodB();
    }
    
    @Transactional
    public void methodB() {
        // ...
    }
}

解决方案:

java
@Service
public class UserService {
    
    @Autowired
    private ApplicationContext context;
    
    public void methodA() {
        // 通过代理对象调用
        UserService proxy = context.getBean(UserService.class);
        proxy.methodB();
    }
    
    @Transactional
    public void methodB() {
        // ...
    }
}

总结

AOP是Spring框架的核心特性之一,它让我们能够:

  • 分离横切关注点
  • 提高代码复用性
  • 降低代码耦合度
  • 简化开发和维护

合理使用AOP可以大大提升代码质量和开发效率。