AOP 日志记录实战
1. 概述
本文主要介绍基于 Spring AOP 的5 种通知类型实现统一日志记录的代码实战。
关于 AOP 的核心理论概念,请参考笔记 [[AOP面向切面编程]]。
核心思路:
通过自定义注解标记需要记录日志的业务方法,利用 AOP 切面拦截带有该注解的方法,在方法执行的不同阶段(前置、后置、异常、环绕)输出日志信息。
2. 环境准备
2.1 定义目标方法
为了演示效果,我们先定义一个简单的 Controller 或 Service 方法作为连接点(Join Point)。
2.2 自定义注解
创建自定义注解@AopLogText,用于标记切入点并传递业务描述参数。
相关基础知识可参考 [[自定义注解]]。
packagecom.example.annotation;importjava.lang.annotation.*;@Target(ElementType.METHOD)// 作用范围:方法@Retention(RetentionPolicy.RUNTIME)// 生命周期:运行时保留(关键,否则AOP无法通过反射获取)@Documentedpublic@interfaceAopLogText{/** * 业务描述信息 * 例如:@AopLogText("用户登录") */Stringvalue()default"";}3. 切面类实现
创建一个切面类,用于封装日志处理逻辑。
核心注解说明:
@Aspect: 标记该类为一个切面类。@Component: 将该类注册到 Spring 容器中,使其生效。@Slf4j: (可选) Lombok 注解,用于快速使用日志对象log。
3.1 定义切入点
这里直接使用@Pointcut定义通用的匹配规则,或者在通知注解中直接写表达式。
本例主要演示基于注解的切入方式:@annotation(com.example.annotation.AopLogText)。
4. 通知类型详解与代码
4.1 前置通知 (Before)
在目标方法执行之前触发。常用于记录“开始执行某动作”及入参信息。
/** * 前置通知 * @param joinPoint 连接点对象,可获取方法签名、参数等信息 * @param aopLog 注解对象,通过参数绑定直接获取(注意参数名需匹配) */@Before("@annotation(aopLog)")publicvoidbeforeMethod(JoinPointjoinPoint,AopLogTextaopLog){// 1. 获取目标方法名StringmethodName=joinPoint.getSignature().getName();// 2. 获取注解上的参数值 (如: "开始查询")StringannotationValue=aopLog.value();log.info("[前置通知] 准备执行方法:{},业务描述:{}",methodName,annotationValue);}4.2 返回通知 (AfterReturning)
在目标方法正常返回后触发。可以获取方法的返回值。
- 注意:如果方法抛出异常,此通知不会执行。
/** * 返回通知 * @param joinPoint 连接点 * @param result 目标方法的返回值 (参数名必须与 returning 属性一致) */@AfterReturning(value="@annotation(com.example.annotation.AopLogText)",returning="result")publicvoidafterReturning(JoinPointjoinPoint,Objectresult){StringmethodName=joinPoint.getSignature().getName();log.info("[返回通知] 方法 {} 执行完成,返回值:{}",methodName,result);}4.3 异常通知 (AfterThrowing)
在目标方法抛出异常时触发。可以获取具体的异常对象。
/** * 异常通知 * @param joinPoint 连接点 * @param ex 抛出的异常对象 (参数名必须与 throwing 属性一致) */@AfterThrowing(value="@annotation(com.example.annotation.AopLogText)",throwing="ex")publicvoidafterThrowing(JoinPointjoinPoint,Exceptionex){StringmethodName=joinPoint.getSignature().getName();log.error("[异常通知] 方法 {} 执行异常,原因:{}",methodName,ex.getMessage());}4.4 最终通知 (After)
在目标方法执行之后触发。
- 特点:无论方法是正常结束还是抛出异常,都会执行(类似于
try-catch-finally中的finally块)。
@After("@annotation(com.example.annotation.AopLogText)")publicvoidafterMethod(JoinPointjoinPoint){StringmethodName=joinPoint.getSignature().getName();log.info("[最终通知] 方法 {} 执行结束(无论是否异常)",methodName);}4.5 环绕通知 (Around)
功能最强大的通知类型。它包裹了目标方法,可以在方法执行前后自定义逻辑,甚至控制方法是否执行、修改返回值。
- 适用场景:统计方法耗时、统一异常处理、事务控制。
/** * 环绕通知 * @param joinPoint ProceedingJoinPoint 是 JoinPoint 的子接口,增加了 proceed() 方法 * @param aopLogText 注解对象 * @return 目标方法的返回值 (必须返回,否则调用方收不到数据) */@Around("@annotation(aopLogText)")publicObjectaroundMethod(ProceedingJoinPointjoinPoint,AopLogTextaopLogText)throwsThrowable{StringmethodName=joinPoint.getSignature().getName();StringannotationValue=aopLogText.value();log.info("【环绕通知】开始执行方法:{},注解参数:{}",methodName,annotationValue);longstartTime=System.currentTimeMillis();Objectresult=null;try{// 核心:执行目标方法// result 为目标方法的返回值result=joinPoint.proceed();}catch(Throwablee){log.error("【环绕通知】方法 {} 执行异常:{}",methodName,e.getMessage());// 注意:通常需要将异常抛出,否则上层(如全局异常处理器)无法感知throwe;}finally{longcostTime=System.currentTimeMillis()-startTime;log.info("【环绕通知】方法 {} 执行耗时:{}ms",methodName,costTime);}// 必须返回结果returnresult;}5. 关键细节:参数绑定
在 AOP 中获取注解参数时,有一种简便的参数绑定写法,无需通过反射手动解析。
操作步骤:
- 在切入点表达式中指定参数名,例如
@annotation(aopLog)。 - 在通知方法的参数列表中声明同名的参数
AopLogText aopLog。
示意图:
注意事项:
切面方法中的参数名(如aopLog)必须与注解表达式@annotation(aopLog)中的名称严格一致,否则 Spring 无法完成映射。
6. 最佳实践
在实际项目中,应根据需求选择合适的通知类型:
基础日志记录:
- 推荐组合使用
@Before(记录入参)、@AfterReturning(记录返回值)和@AfterThrowing(记录异常)。 - 优点:职责单一,代码逻辑清晰,解耦性好。
- 推荐组合使用
复杂场景 / 性能监控:
- 推荐使用
@Around。 - 场景:需要统计方法耗时(Start Time - End Time)、需要改变返回值、或者需要在一个上下文中同时处理入参和结果。
- 推荐使用
避免滥用
@Around:- 虽然环绕通知功能最强,但如果只是简单的打印日志,使用组合通知的可读性更高。
- 环绕通知需要手动调用
proceed()并处理异常,编写不当容易导致目标方法不执行或异常被吞没。
进阶扩展:
在生产环境中,通常会将日志信息(操作人、时间、IP、耗时、结果)异步存储到数据库或发送到ELK(Elasticsearch, Logstash, Kibana) 系统中,以便进行审计和分析。
7. 参考文章与学习来源
- 参考文章:Spring AOP 实现日志记录案例详解
- 知识问答:亲爱的豆包老师
- 笔记/术语优化:gemini-3-pro-preivew