Spring AOP 常用注解完全指南

本文系统总结 Spring AOP 六大核心注解,涵盖定义、语法、示例和最佳实践,并附完整可运行代码。


1. 注解速查表

注解 作用 执行时机 能否改参 典型场景
@Aspect 声明切面类 - - 标识切面
@Pointcut 定义切入点 - - 复用切点表达式
@Before 前置通知 目标方法前 ❌ 否 权限校验、日志记录
@After 最终通知 目标方法后(finally) ❌ 否 资源释放、清理动作
@AfterReturning 返回后通知 成功返回后 ❌ 否 结果缓存、成功日志
@AfterThrowing 异常后通知 抛出异常后 ❌ 否 异常告警、事务回滚
@Around 环绕通知 包裹整个调用链 ✅ 是 性能监控、事务管理、参数修改
@Order 控制顺序 - - 多切面执行优先级

2. 详细注解说明

2.1 @Aspect - 切面声明

作用:标识一个类为切面类,Spring 会自动扫描并织入通知。

使用方式

复制代码
@Aspect
@Component  // 必须交予 Spring 管理
public class LoggingAspect {
    // 通知方法...
}

要点

  • 必须与 @Component@Configuration 配合使用

  • 一个类可包含多个 @Pointcut 和多种通知


2.2 @Pointcut - 切入点定义

作用:定义可复用的切点表达式,避免在多个通知中重复编写。

语法

复制代码
@Pointcut("execution(修饰符? 返回类型 包名.类名.方法名(参数))")
public void methodName() {}  // 方法体为空,仅作为标识

常用表达式

  • execution(* com.example.service.*.*(..)):service 包下所有类的所有方法

  • execution(public * com.example.controller.*.*(..)):controller 包下所有 public 方法

  • @annotation(com.example.annotation.Log):标注了 @Log 注解的方法

  • args(java.io.Serializable):参数为 Serializable 的方法

  • within(com.example.service..*):service 包及其子包

示例

复制代码
@Pointcut("execution(* com.dycjr.xiakuan.report.support.indicator.controller.StatisticsController.*(..))")
public void statisticsControllerMethods() {}

@Pointcut("@annotation(com.dycjr.xiakuan.report.support.basePermission.controller.BasePermissionLimit)")
public void permissionLimitedMethods() {}

2.3 @Before - 前置通知

作用:在目标方法执行前运行,常用于权限校验、参数校验、日志记录。

特点

  • 无法修改参数 :只能读取,不能通过 args[i] = ... 替换参数

  • 无法阻止执行:只能通过抛异常中断流程

  • 无返回值 :方法返回类型必须是 void

示例

复制代码
@Slf4j
@Aspect
@Component
public class SecurityAspect {

    @Pointcut("@annotation(com.example.annotation.RequiresPermission)")
    public void permissionCheck() {}
    
    @Before("permissionCheck()")
    public void checkPermission(JoinPoint joinPoint) {
        // 1. 获取方法签名
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getName();
        
        // 2. 获取参数
        Object[] args = joinPoint.getArgs();
        Long userId = (Long) args[0];
        
        // 3. 执行校验逻辑
        if (!hasPermission(userId)) {
            log.error("用户 {} 无权限访问方法 {}", userId, methodName);
            throw new PermissionDeniedException("无权限");
        }
        
        // ✅ 推荐:只读操作,不修改参数
        log.info("用户 {} 访问方法 {}", userId, methodName);
    }
    
    private boolean hasPermission(Long userId) {
        // 权限校验逻辑
        return true;
    }
}

适用场景

  • 权限校验(无权限则抛异常)

  • 参数合法性检查

  • 方法调用日志记录

  • Read-Only 操作

⚠️ 陷阱警告

复制代码
@Before("permissionCheck()")
public void badExample(JoinPoint jp) {
    Object[] args = jp.getArgs();
    ((UserDTO) args[0]).setName("modified"); // ⚠️ 虽生效但不推荐!
    args[1] = newDefaultValue; // ❌ 无效!Controller 接收不到
}

2.4 @After - 最终通知

作用 :在目标方法执行后(无论成功或异常)运行,类似 finally 块。

特点

  • 总会执行:目标方法正常返回或抛异常都会执行

  • 无法获取返回值:不知道方法执行结果

  • 无法阻止异常抛出:只是 finally 逻辑

示例

复制代码
@Slf4j
@Aspect
@Component
public class ResourceCleanAspect {
    
    @Pointcut("execution(* com.example.service.FileService.process*(..))")
    public void fileOperations() {}
    
    @After("fileOperations()")
    public void cleanUpResources() {
        // 释放文件句柄、关闭流等操作
        FileTempHolder.clear();
        log.info("资源清理完成");
    }
}

适用场景

  • 资源释放(文件句柄、数据库连接)

  • ThreadLocal 清理

  • 性能监控结束标记


2.5 @AfterReturning - 返回后通知

作用 :在目标方法成功返回后执行,可获取返回值。

特点

  • 仅正常返回时执行:方法抛异常时不执行

  • 可获取返回值 :通过 returning 属性绑定

  • 无法修改返回值:只能读取

示例

复制代码
@Slf4j
@Aspect
@Component
public class ResultCacheAspect {
    
    @Pointcut("execution(* com.example.service.UserService.getUser(..))")
    public void userQuery() {}
    
    @AfterReturning(pointcut = "userQuery()", returning = "result")
    public void cacheResult(Object result) {
        // 1. result 参数就是目标方法的返回值
        if (result != null) {
            // 2. 写入缓存
            cacheManager.put("userCache", result);
            log.info("查询结果已写入缓存: {}", result);
        }
    }
}

适用场景

  • 结果缓存

  • 成功日志记录

  • 返回值审计

  • 数据同步


2.6 @AfterThrowing - 异常后通知

作用 :在目标方法抛出异常后执行,可获取异常对象。

特点

  • 仅异常时执行:方法正常返回时不执行

  • 可获取异常 :通过 throwing 属性绑定

  • 无法吞掉异常:异常会继续向上抛出

示例

复制代码
@Slf4j
@Aspect
@Component
public class ExceptionAlertAspect {
    
    @Pointcut("execution(* com.example.service.OrderService.createOrder(..))")
    public void orderCreation() {}
    
    @AfterThrowing(pointcut = "orderCreation()", throwing = "ex")
    public void sendAlert(Exception ex) {
        // 1. ex 就是抛出的异常
        String errorMessage = ex.getMessage();
        
        // 2. 发送告警
        alertService.send("订单创建失败: " + errorMessage);
        
        // 3. 记录错误日志
        log.error("订单创建异常", ex);
    }
}

适用场景

  • 异常告警(钉钉、邮件)

  • 事务回滚标记

  • 错误日志记录

  • 失败指标统计


2.7 @Around - 环绕通知(全能型)

作用:包裹整个调用链,可完全控制目标方法的执行。

特点

  • 全能:可改参、可阻止、可改返回值、可捕获异常

  • **必须调用 proceed() **:否则目标方法不执行

  • **必须返回结果 **:返回值会替换原方法返回值

** 示例 1:性能监控 **:

复制代码
@Slf4j
@Aspect
@Component
public class PerformanceAspect {
    
    @Pointcut("@annotation(com.example.annotation.Monitor)")
    public void monitoredMethods() {}
    
    @Around("monitoredMethods()")
    public Object monitorTime(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 方法执行前
        long start = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        
        try {
            // 2. 执行目标方法
            Object result = joinPoint.proceed();
            
            // 3. 返回后记录性能
            long cost = System.currentTimeMillis() - start;
            log.info("方法 {} 执行耗时: {}ms", methodName, cost);
            
            return result;
        } catch (Exception e) {
            // 4. 异常时记录
            long cost = System.currentTimeMillis() - start;
            log.error("方法 {} 执行异常,耗时: {}ms", methodName, cost, e);
            throw e;
        }
    }
}

** 示例 2:参数修改 **(核心场景):

复制代码
@Slf4j
@Aspect
@Component
public class ParameterEnhanceAspect {
    
    @Pointcut("execution(* com.example.controller.*.query*(..))")
    public void queryMethods() {}
    
    @Around("queryMethods()")
    public Object enhanceParameter(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 获取并拷贝参数(防御性编程)
        Object[] originalArgs = joinPoint.getArgs();
        Object[] processedArgs = Arrays.copyOf(originalArgs, originalArgs.length);
        
        // 2. 遍历并增强参数
        for (int i = 0; i < processedArgs.length; i++) {
            Object arg = processedArgs[i];
            
            if (arg instanceof BasePermissionQuery) {
                // ✅ 正确:创建拷贝后修改
                BasePermissionQuery query = (BasePermissionQuery) arg;
                query = query.clone(); // 或 new BasePermissionQuery(arg)
                query.setUserId(getCurrentUserId());
                query.setDataScope(getUserDataScope());
                processedArgs[i] = query; // ✅ 显式替换
            }
            
            // ✅ 支持替换整个参数
            if (arg == null) {
                processedArgs[i] = createDefaultQuery();
            }
        }
        
        log.info("参数增强前: {}, 增强后: {}", 
            Arrays.toString(originalArgs), 
            Arrays.toString(processedArgs));
        
        // 3. 传递增强后的参数执行
        return joinPoint.proceed(processedArgs);
    }
}

** 示例3:阻止方法执行 **:

复制代码
@Around("execution(* com.example.service.LegacyService.oldMethod(..))")
public Object blockLegacyMethod(ProceedingJoinPoint joinPoint) {
    log.warn("阻止调用废弃方法: {}", joinPoint.getSignature());
    // ❌ 不调用 proceed(),直接返回降级结果
    return "该功能已下线,请联系管理员";
}

** 适用场景 **:

  • ** 参数修改 **:唯一可靠方式

  • ** 性能监控 **:计时、统计

  • ** 事务管理 **:@Transactional 的底层实现

  • ** 缓存 **:有则返回,无则查询并缓存

  • ** 权限控制 **:阻止无权限调用


2.8 @Order - 切面执行顺序

** 作用 **:当多个切面作用于同一方法时,控制执行顺序。

** 规则 **:

  • ** 值越小越先执行 **(越靠近目标方法)

  • 默认值 Ordered.LOWEST_PRECEDENCEInteger.MAX_VALUE

  • 负值可让切面优先执行

** 示例 **:

复制代码
// 切面1:安全校验(最高优先级)
@Aspect
@Component
@Order(1)  // 最先执行
public class SecurityAspect {
    @Before("execution(* com.example..*Service.*(..))")
    public void check() { /* ... */ }
}

// 切面2:日志记录(次之)
@Aspect
@Component
@Order(2)
public class LoggingAspect {
    @Around("execution(* com.example..*Service.*(..))")
    public Object log(ProceedingJoinPoint pjp) throws Throwable { /* ... */ }
}

// 切面3:性能监控(最后)
@Aspect
@Component
@Order(3)
public class PerformanceAspect {
    @Around("execution(* com.example..*Service.*(..))")
    public Object monitor(ProceedingJoinPoint pjp) throws Throwable { /* ... */ }
}

执行顺序可视化

复制代码
请求进入
  ↓
@Order(3) PerformanceAspect(外层)→ start
  ↓ [proceed()]
@Order(2) LoggingAspect(中层)→ start
  ↓ [proceed()]
@Order(1) SecurityAspect(内层)→ check
  ↓
目标方法执行
  ↓
@Order(1) SecurityAspect
  ↓
@Order(2) LoggingAspect → end
  ↓
@Order(3) PerformanceAspect → end

要点

  • 相同 @Order:按切面类名排序(不确定,避免依赖)

  • 不同类型通知@Around > @Before > @After > @AfterReturning/@AfterThrowing

  • 推荐 :每个切面明确指定 @Order,避免歧义


3. 完整实战示例

3.1 项目结构

复制代码
src/main/java/com/example/demo/
├── DemoApplication.java
├── annotation/
│   └── LogExecution.java          // 自定义注解
├── aspect/
│   ├── LoggingAspect.java         // 日志切面
│   ├── SecurityAspect.java        // 安全切面
│   └── PerformanceAspect.java     // 性能切面
├── controller/
│   └── UserController.java        // 测试控制器
└── service/
    └── UserService.java           // 测试服务

3.2 自定义注解

复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecution {
    String value() default "";
}

3.3 多切面完整代码

UserService.java

复制代码
@Service
public class UserService {
    
    @LogExecution("查询用户")
    public User getUser(Long id, String name) {
        System.out.println("【目标方法】查询用户: id=" + id + ", name=" + name);
        return new User(id, name);
    }
}

LoggingAspect.java(@Before + @AfterReturning):

复制代码
@Slf4j
@Aspect
@Component
@Order(2)
public class LoggingAspect {
    
    @Pointcut("@annotation(com.example.demo.annotation.LogExecution)")
    public void loggingPointCut() {}
    
    @Before("loggingPointCut()")
    public void logBefore(JoinPoint joinPoint) {
        String method = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        log.info("【日志-前置】方法: {}, 参数: {}", method, Arrays.toString(args));
    }
    
    @AfterReturning(pointcut = "loggingPointCut()", returning = "result")
    public void logAfter(Object result) {
        log.info("【日志-返回】结果: {}", result);
    }
}

SecurityAspect.java(@Around):

复制代码
@Slf4j
@Aspect
@Component
@Order(1)
public class SecurityAspect {
    
    @Around("@annotation(logExecution)")
    public Object checkSecurity(ProceedingJoinPoint joinPoint, LogExecution logExecution) throws Throwable {
        String method = joinPoint.getSignature().getName();
        log.info("【安全-环绕】开始检查方法: {}, 注解: {}", method, logExecution.value());
        
        // 参数校验
        Object[] args = joinPoint.getArgs();
        Long userId = (Long) args[0];
        if (userId <= 0) {
            throw new IllegalArgumentException("非法用户ID: " + userId);
        }
        
        // 参数增强
        args[1] = "安全增强_" + args[1];
        
        try {
            Object result = joinPoint.proceed(args);
            log.info("【安全-环绕】方法 {} 执行成功", method);
            return result;
        } catch (Exception e) {
            log.error("【安全-环绕】方法 {} 执行异常", method, e);
            throw e;
        }
    }
}

PerformanceAspect.java(@Around):

复制代码
@Slf4j
@Aspect
@Component
@Order(3)
public class PerformanceAspect {
    
    @Around("@annotation(com.example.demo.annotation.LogExecution)")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        String method = joinPoint.getSignature().getName();
        
        log.info("【性能-环绕】方法 {} 开始执行", method);
        
        Object result = joinPoint.proceed();
        
        long cost = System.currentTimeMillis() - start;
        log.info("【性能-环绕】方法 {} 执行耗时: {}ms", method, cost);
        
        return result;
    }
}

UserController.java

复制代码
@RestController
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id, @RequestParam String name) {
        return userService.getUser(id, name);
    }
}

3.4 测试与输出

访问:GET /user/123?name=zhangsan

控制台输出

复制代码
【性能-环绕】方法 getUser 开始执行
【安全-环绕】开始检查方法: getUser, 注解: 查询用户
【日志-前置】方法: getUser, 参数: [123, zhangsan]
【目标方法】查询用户: id=123, name=安全增强_zhangsan
【日志-返回】结果: User(id=123, name=安全增强_zhangsan)
【安全-环绕】方法 getUser 执行成功
【性能-环绕】方法 getUser 执行耗时: 15ms

3.5 异常测试

访问:GET /user/-1?name=invalid

控制台输出

复制代码
【性能-环绕】方法 getUser 开始执行
【安全-环绕】开始检查方法: getUser, 注解: 查询用户
【安全-环绕】方法 getUser 执行异常  // 异常被安全切面捕获
java.lang.IllegalArgumentException: 非法用户ID: -1
【性能-环绕】方法 getUser 执行耗时: 3ms  // 性能切面记录到异常

4. 最佳实践与常见陷阱

4.1 核心原则

  1. 职责单一:一个切面只负责一个横切关注点(日志、安全、性能分离)

  2. 明确顺序 :总是使用 @Order 指定执行顺序

  3. 参数只读@Before 中绝不修改参数(即使对象内部状态)

  4. 环绕全能 :需要修改参数、控制流程时,一律使用 @Around

  5. 防御拷贝@Around 中修改参数前先拷贝,避免副作用

  6. 异常处理@Around 必须捕获异常并再次抛出,否则吞掉异常

4.2 常见陷阱

陷阱代码 问题 正确做法
@Beforeargs[i] = newObj 无效,Controller 收不到 改用 @Around + proceed(newArgs)
@Before 中修改对象属性 虽生效但隐藏副作用 改用 @Around,明确克隆后修改
@AfterReturning 中抛异常 异常会覆盖原方法返回值 仅用于日志/缓存,不抛业务异常
@Around 忘记 proceed() 目标方法不执行 确保有返回 proceed() 结果
@Around 吞掉异常 调用方收不到异常 catch 后必须 throw e
多个切面不指定 @Order 执行顺序不确定 所有切面都指定 @Order

4.3 性能建议

  • @Around 有一定性能开销,简单场景优先用 @Before

  • 切点表达式要精确,避免扫描过多方法

  • 避免在切面中执行耗时操作(如远程调用)

  • 生产环境可动态开关切面(通过配置中心)


5. 总结决策图

复制代码
是否需要阻止方法执行?
  ├─ 是 → @Around
  └─ 否 → 是否需要修改参数?
           ├─ 是 → @Around
           └─ 否 → 是否只需方法前执行?
                    ├─ 是 → @Before
                    └─ 否 → 是否只需方法后执行?
                             ├─ 是 → @AfterReturning / @After
                             └─ 否 → 重新设计切面职责

一句话总结@Before@After观察者@Around代理人。能观察不代理,需代理必用环绕

相关推荐
無限進步D5 小时前
Java 运行原理
java·开发语言·入门
難釋懷5 小时前
安装Canal
java
是苏浙5 小时前
JDK17新增特性
java·开发语言
不光头强5 小时前
spring cloud知识总结
后端·spring·spring cloud
GetcharZp8 小时前
告别 Python 依赖!用 LangChainGo 打造高性能大模型应用,Go 程序员必看!
后端
阿里加多8 小时前
第 4 章:Go 线程模型——GMP 深度解析
java·开发语言·后端·golang
likerhood8 小时前
java中`==`和`.equals()`区别
java·开发语言·python
小小李程序员9 小时前
Langchain4j工具调用获取不到ThreadLocal
java·后端·ai