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代理人。能观察不代理,需代理必用环绕

相关推荐
Halo_tjn2 小时前
Java IO流实现文件操作知识点
java·开发语言·windows·算法
神奇小汤圆2 小时前
告别繁琐!MapStruct-Plus 让对象映射效率飙升,这波操作太香了!
后端
CryptoRzz2 小时前
StockTV API 对接全攻略(股票、期货、IPO)
java·javascript·git·web3·区块链·github
小菜鸡ps2 小时前
【flowable专栏】网关类型
后端·工作流引擎
王中阳Go2 小时前
字节开源 Eino 框架上手体验:Go 语言终于有能打的 Agent 编排工具了(含 RAG 实战代码)
人工智能·后端·go
零_守墓人2 小时前
Patroni 中备份恢复和数据迁移
后端
用户1565845925052 小时前
Go技术专家进阶营 从代码开发到架构设计,开启Go技术专家之路
后端
iReachers2 小时前
为什么HTML打包安卓APP安装时会覆盖或者报错?
android·java·html·html打包apk·网页打包
苏近之2 小时前
Rust 中实现定时任务管理
后端·架构·rust