joken-前端工程师

  博客园 :: 首页 :: 新随笔 :: :: :: 管理 ::

切面(Aspect)的核心机制就是拦截(Interception),但切面的概念比单纯的拦截更加丰富和系统化。

让我用一个更准确的比喻来解释:

🎯 拦截 vs 切面

特性 单纯的拦截(Interception) Spring AOP 切面(Aspect)
目的 阻止或修改方法调用 模块化横切关注点
范围 通常针对单个方法或类 跨越多个类、模块的通用功能
设计理念 技术实现手段 编程范式和解耦设计
功能 主要关注方法调用前后 支持前置、后置、环绕、异常等多种通知类型

🔧 切面不仅仅是拦截

切面确实通过拦截机制来实现,但它提供了更完整的AOP解决方案:

@Aspect
@Component
public class ExampleAspect {
    
    // 1. 前置通知 - 在方法执行前拦截
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("方法执行前: " + joinPoint.getSignature().getName());
    }
    
    // 2. 后置通知 - 在方法执行后拦截(无论成功失败)
    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice(JoinPoint joinPoint) {
        System.out.println("方法执行完成: " + joinPoint.getSignature().getName());
    }
    
    // 3. 返回通知 - 只在方法成功返回后拦截
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        System.out.println("方法返回结果: " + result);
    }
    
    // 4. 异常通知 - 只在方法抛出异常时拦截
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
        System.out.println("方法抛出异常: " + ex.getMessage());
    }
    
    // 5. 环绕通知 - 最强大的拦截方式(可以控制是否执行原方法)
    @Around("execution(* com.example.service.*.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("方法执行前");
        Object result = joinPoint.proceed(); // 可选择是否执行原方法
        System.out.println("方法执行后");
        return result;
    }
}

🌟 切面的核心价值

切面之所以比单纯拦截更强大,是因为它:

  1. 模块化横切关注点:将日志、事务、安全、缓存等分散在各个模块中的通用功能集中管理
  2. 声明式编程:通过注解配置,而不是硬编码在业务逻辑中
  3. 解耦:业务代码不需要知道增强逻辑的存在
  4. 可维护性:修改通用功能只需修改切面,而不需要改动所有相关类

📊 实际应用场景

场景 拦截内容 切面实现的功能
事务管理 拦截Service方法 自动开启/提交/回滚事务
日志记录 拦截所有方法调用 记录方法入参、出参、执行时间
权限校验 拦截Controller方法 验证用户权限和角色
性能监控 拦截关键业务方法 统计方法执行时间和调用次数
缓存处理 拦截数据查询方法 自动缓存查询结果

💡 简单总结

切面 = 拦截 + 增强逻辑 + 模块化管理

所以你说"切面就是拦截"是基本正确的,但更准确的说法是:拦截是切面实现其功能的底层机制,而切面是在拦截基础上提供的一套完整的面向切面编程的解决方案

这种设计让开发者可以专注于业务逻辑,而将横切关注点(cross-cutting concerns)交给切面来处理,大大提高了代码的质量和可维护性。

demo

Spring Boot AOP 切面 Demo

下面是一个完整的 Spring Boot 切面示例,演示日志记录、性能监控和缓存功能。

1. 项目结构

src/main/java/com/example/demo/
├── DemoApplication.java
├── annotation/
│   ├── Loggable.java
│   ├── PerformanceMonitor.java
│   └── Cacheable.java
├── aspect/
│   ├── LoggingAspect.java
│   ├── PerformanceAspect.java
│   └── CacheAspect.java
└── service/
    └── UserService.java

2. 自定义注解

Loggable.java

package com.example.demo.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
    boolean logParams() default true;
    boolean logResult() default true;
}

PerformanceMonitor.java

package com.example.demo.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PerformanceMonitor {
    long threshold() default 1000; // 阈值,单位毫秒
}

Cacheable.java

package com.example.demo.annotation;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
    String keyPrefix() default "";
    long expireTime() default 300; // 过期时间,单位秒
}

3. 切面实现

LoggingAspect.java

package com.example.demo.aspect;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.example.demo.annotation.Loggable;

@Aspect
@Component
public class LoggingAspect {
    private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);

    @Pointcut("@annotation(loggable)")
    public void loggableMethod(Loggable loggable) {}

    @Before("loggableMethod(loggable)")
    public void logBefore(JoinPoint joinPoint, Loggable loggable) {
        if (loggable.logParams()) {
            logger.info("方法执行前: {} - 参数: {}", 
                joinPoint.getSignature().toShortString(), 
                java.util.Arrays.toString(joinPoint.getArgs()));
        } else {
            logger.info("方法执行前: {}", joinPoint.getSignature().toShortString());
        }
    }

    @AfterReturning(pointcut = "loggableMethod(loggable)", returning = "result")
    public void logAfterReturning(JoinPoint joinPoint, Loggable loggable, Object result) {
        if (loggable.logResult()) {
            logger.info("方法执行成功: {} - 返回值: {}", 
                joinPoint.getSignature().toShortString(), result);
        } else {
            logger.info("方法执行成功: {}", joinPoint.getSignature().toShortString());
        }
    }

    @AfterThrowing(pointcut = "loggableMethod(loggable)", throwing = "ex")
    public void logAfterThrowing(JoinPoint joinPoint, Loggable loggable, Exception ex) {
        logger.error("方法执行异常: {} - 异常信息: {}", 
            joinPoint.getSignature().toShortString(), ex.getMessage());
    }
}

PerformanceAspect.java

package com.example.demo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import com.example.demo.annotation.PerformanceMonitor;

@Aspect
@Component
public class PerformanceAspect {
    private static final Logger logger = LoggerFactory.getLogger(PerformanceAspect.class);

    @Around("@annotation(monitor)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint, PerformanceMonitor monitor) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        try {
            Object result = joinPoint.proceed();
            return result;
        } finally {
            long endTime = System.currentTimeMillis();
            long duration = endTime - startTime;
            
            if (duration > monitor.threshold()) {
                logger.warn("性能警告: {} 执行耗时: {}ms (阈值: {}ms)", 
                    joinPoint.getSignature().toShortString(), 
                    duration, 
                    monitor.threshold());
            } else {
                logger.info("方法执行时间: {} - 耗时: {}ms", 
                    joinPoint.getSignature().toShortString(), duration);
            }
        }
    }
}

CacheAspect.java (简单内存缓存示例)

package com.example.demo.aspect;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import com.example.demo.annotation.Cacheable;
import java.util.concurrent.ConcurrentHashMap;

@Aspect
@Component
public class CacheAspect {
    // 简单的内存缓存(实际项目中应该用Redis)
    private final ConcurrentHashMap<String, Object> cache = new ConcurrentHashMap<>();
    private final ConcurrentHashMap<String, Long> expireTimes = new ConcurrentHashMap<>();

    @Around("@annotation(cacheable)")
    public Object handleCache(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
        String cacheKey = generateCacheKey(cacheable.keyPrefix(), joinPoint);
        
        // 检查缓存是否存在且未过期
        if (isCacheValid(cacheKey, cacheable.expireTime())) {
            System.out.println("从缓存获取数据: " + cacheKey);
            return cache.get(cacheKey);
        }
        
        // 执行原方法
        Object result = joinPoint.proceed();
        
        // 缓存结果
        cache.put(cacheKey, result);
        expireTimes.put(cacheKey, System.currentTimeMillis());
        System.out.println("数据已缓存: " + cacheKey);
        
        return result;
    }

    private String generateCacheKey(String keyPrefix, ProceedingJoinPoint joinPoint) {
        StringBuilder key = new StringBuilder(keyPrefix);
        key.append(":").append(joinPoint.getSignature().getName());
        for (Object arg : joinPoint.getArgs()) {
            key.append(":").append(arg != null ? arg.toString() : "null");
        }
        return key.toString();
    }

    private boolean isCacheValid(String cacheKey, long expireTimeSeconds) {
        Long cacheTime = expireTimes.get(cacheKey);
        if (cacheTime == null) {
            return false;
        }
        
        long currentTime = System.currentTimeMillis();
        return (currentTime - cacheTime) < (expireTimeSeconds * 1000);
    }
}

4. 业务服务类

UserService.java

package com.example.demo.service;

import org.springframework.stereotype.Service;
import com.example.demo.annotation.Loggable;
import com.example.demo.annotation.PerformanceMonitor;
import com.example.demo.annotation.Cacheable;

@Service
public class UserService {

    @Loggable(logParams = true, logResult = true)
    @PerformanceMonitor(threshold = 500)
    @Cacheable(keyPrefix = "user", expireTime = 60)
    public String getUserInfo(Long userId) {
        // 模拟数据库查询
        simulateDatabaseQuery();
        return "用户信息 - ID: " + userId;
    }

    @Loggable
    @PerformanceMonitor
    public String updateUser(Long userId, String userName) {
        // 模拟业务逻辑
        simulateBusinessLogic();
        return "用户更新成功: " + userName;
    }

    private void simulateDatabaseQuery() {
        try {
            Thread.sleep(200); // 模拟数据库查询耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    private void simulateBusinessLogic() {
        try {
            Thread.sleep(100); // 模拟业务逻辑耗时
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

5. 主应用类

DemoApplication.java

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import com.example.demo.service.UserService;

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
        
        // 测试切面功能
        UserService userService = context.getBean(UserService.class);
        
        System.out.println("=== 第一次调用(会执行方法并缓存)===");
        String result1 = userService.getUserInfo(1L);
        System.out.println("结果: " + result1);
        
        System.out.println("\n=== 第二次调用(从缓存获取)===");
        String result2 = userService.getUserInfo(1L);
        System.out.println("结果: " + result2);
        
        System.out.println("\n=== 测试更新方法 ===");
        String updateResult = userService.updateUser(1L, "张三");
        System.out.println("结果: " + updateResult);
    }
}

6. 依赖配置 (pom.xml)

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

7. 运行结果示例

运行应用后,你会在控制台看到类似输出:

=== 第一次调用(会执行方法并缓存)===
方法执行前: UserService.getUserInfo(..) - 参数: [1]
数据已缓存: user:getUserInfo:1
方法执行时间: UserService.getUserInfo(..) - 耗时: 203ms
方法执行成功: UserService.getUserInfo(..) - 返回值: 用户信息 - ID: 1
结果: 用户信息 - ID: 1

=== 第二次调用(从缓存获取)===
从缓存获取数据: user:getUserInfo:1
结果: 用户信息 - ID: 1

=== 测试更新方法 ===
方法执行前: UserService.updateUser(..)
方法执行时间: UserService.updateUser(..) - 耗时: 102ms
方法执行成功: UserService.updateUser(..) - 返回值: 用户更新成功: 张三
结果: 用户更新成功: 张三

这个 Demo 展示了:

  1. 日志切面:记录方法调用前后信息
  2. 性能监控切面:统计方法执行时间并警告超时
  3. 缓存切面:自动缓存方法结果
  4. 注解驱动:通过自定义注解控制切面行为

你可以根据需要扩展这些切面,比如添加数据库事务、权限校验等功能。

Spring AOP 切面的完整生命周期

切面的生命周期指的是从方法调用开始到结束的整个过程中,切面各个通知(Advice)的执行顺序和时机。让我详细解释整个生命周期。

📊 切面生命周期全景图

sequenceDiagram participant C as Client participant P as Proxy participant A as Aspect participant T as Target Method C->>P: 调用业务方法 P->>A: 触发环绕通知(@Around) - 前半部分 A->>A: 执行前置逻辑 alt 环绕通知决定继续执行 A->>T: 调用proceed() T->>T: 执行业务逻辑 alt 业务正常执行 T->>A: 返回正常结果 A->>A: 执行返回通知(@AfterReturning) else 业务抛出异常 T->>A: 抛出异常 A->>A: 执行异常通知(@AfterThrowing) end A->>A: 执行后置通知(@After) - 无论成功失败 A->>A: 执行环绕通知(@Around) - 后半部分 A->>P: 返回最终结果 else 环绕通知决定不执行原方法 A->>P: 直接返回自定义结果 end P->>C: 返回方法结果

🔄 完整的通知执行顺序

1. 正常执行流程(方法成功完成)

@Aspect
@Component
public class FullLifecycleAspect {
    
    // 执行顺序演示
    @Around("execution(* com.example.service.*.*(..))")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("1. @Around - 前置部分");
        
        try {
            Object result = joinPoint.proceed(); // 调用原方法
            System.out.println("5. @Around - 后置部分(正常)");
            return result;
        } catch (Exception e) {
            System.out.println("5. @Around - 后置部分(异常)");
            throw e;
        }
    }
    
    @Before("execution(* com.example.service.*.*(..))")
    public void beforeAdvice(JoinPoint joinPoint) {
        System.out.println("2. @Before - 方法执行前");
    }
    
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        System.out.println("4. @AfterReturning - 方法成功返回");
    }
    
    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice(JoinPoint joinPoint) {
        System.out.println("3. @After - 方法执行完成(无论成功失败)");
    }
}

正常执行输出:

1. @Around - 前置部分
2. @Before - 方法执行前
[执行业务方法]
4. @AfterReturning - 方法成功返回
3. @After - 方法执行完成(无论成功失败)
5. @Around - 后置部分(正常)

2. 异常执行流程(方法抛出异常)

@Aspect
@Component
public class ExceptionLifecycleAspect {
    
    @AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
    public void afterThrowingAdvice(JoinPoint joinPoint, Exception ex) {
        System.out.println("4. @AfterThrowing - 方法抛出异常: " + ex.getMessage());
    }
}

异常执行输出:

1. @Around - 前置部分
2. @Before - 方法执行前
[执行业务方法 - 抛出异常]
4. @AfterThrowing - 方法抛出异常: xxx
3. @After - 方法执行完成(无论成功失败)
5. @Around - 后置部分(异常)

🎯 各个通知的特点

@Before

  • 时机:方法执行前
  • 特点:无法阻止方法执行,但可以修改参数
  • 异常:如果抛出异常,会阻止方法执行

@AfterReturning

  • 时机:方法成功执行后
  • 特点:可以访问返回值,但不能修改
  • 条件:只在方法正常返回时执行

@AfterThrowing

  • 时机:方法抛出异常后
  • 特点:可以访问抛出的异常对象
  • 条件:只在方法抛出异常时执行

@After

  • 时机:方法执行完成后(无论成功或失败)
  • 特点:类似于 finally 块
  • 用途:资源清理、日志记录等

@Around(最强大)

  • 时机:包围整个方法执行
  • 特点
    • 完全控制是否执行原方法
    • 可以修改参数、返回值和异常
    • 必须调用 joinPoint.proceed() 来执行原方法
  • 能力:可以模拟其他所有通知的功能

💡 实际应用示例

完整的业务切面示例

@Aspect
@Component
@Slf4j
public class BusinessAspect {
    
    private final ThreadLocal<Long> startTime = new ThreadLocal<>();
    
    @Around("@annotation(com.example.annotation.BusinessLog)")
    public Object handleBusinessMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 前置处理
        startTime.set(System.currentTimeMillis());
        log.info("业务方法开始: {}", joinPoint.getSignature().getName());
        log.debug("方法参数: {}", Arrays.toString(joinPoint.getArgs()));
        
        try {
            // 2. 执行原方法
            Object result = joinPoint.proceed();
            
            // 3. 成功处理
            long duration = System.currentTimeMillis() - startTime.get();
            log.info("业务方法成功: {} - 耗时: {}ms", 
                    joinPoint.getSignature().getName(), duration);
            log.debug("返回结果: {}", result);
            
            return result;
            
        } catch (BusinessException ex) {
            // 4. 业务异常处理
            log.warn("业务异常: {} - 错误码: {}", 
                    ex.getMessage(), ex.getErrorCode());
            throw ex;
            
        } catch (Exception ex) {
            // 5. 系统异常处理
            log.error("系统异常: {}", ex.getMessage(), ex);
            throw new BusinessException("系统繁忙,请稍后重试");
            
        } finally {
            // 6. 最终清理
            startTime.remove();
            log.debug("业务方法执行完成: {}", joinPoint.getSignature().getName());
        }
    }
    
    @AfterReturning(
        pointcut = "@annotation(com.example.annotation.AuditLog)", 
        returning = "result"
    )
    public void auditLog(JoinPoint joinPoint, Object result) {
        // 审计日志记录
        AuditService.logOperation(
            joinPoint.getSignature().getName(),
            joinPoint.getArgs(),
            result,
            "SUCCESS"
        );
    }
    
    @AfterThrowing(
        pointcut = "@annotation(com.example.annotation.AuditLog)", 
        throwing = "ex"
    )
    public void auditLogException(JoinPoint joinPoint, Exception ex) {
        // 异常审计记录
        AuditService.logOperation(
            joinPoint.getSignature().getName(),
            joinPoint.getArgs(),
            null,
            "FAILED: " + ex.getMessage()
        );
    }
}

🚀 生命周期管理最佳实践

  1. 使用 ThreadLocal:在 @Around 中存储状态,在其他通知中共享
  2. 异常处理:在 @Around 中统一处理异常,保证稳定性
  3. 资源清理:在 @After 或 @Around 的 finally 中清理资源
  4. 性能监控:在 @Around 中计算执行时间
  5. 事务管理:结合 @Transactional 使用

📝 总结

切面的生命周期是一个精心设计的执行链:

正常流程:@Around(前) → @Before → 业务方法 → @AfterReturning → @After → @Around(后)

异常流程:@Around(前) → @Before → 业务方法 → @AfterThrowing → @After → @Around(后)

理解这个生命周期对于编写正确的切面逻辑至关重要!

posted on 2025-09-30 17:42  joken1310  阅读(27)  评论(0)    收藏  举报