【SpringBoot】SpringBoot中使用AOP实现日志记录功能

在这里插入图片描述

  • 前言
  • 一、AOP基本概念
  • 二、项目准备
  • 三、实现日志记录切面
    • 1、创建自定义日志注解
    • 2、实现日志切面
    • 3、配置AOP
  • 四、使用示例
    • 1. 在Controller中使用
    • 2. 在Service中使用
  • 六、高级配置
    • 1. 日志内容格式化
    • 2. 异步日志记录
    • 3. 日志脱敏处理
  • 七、代理类生成的核心逻辑
    • 问题1: 既然是代理类,那么像@Transaction 注解标注在方法上怎么搞?给方法生成代理类?(显然不是)
    • 问题2:自定义的注解@LogExecutionTime,也会生成代理类吗?
  • 八、具体执行步骤

前言

在开发企业级应用时,完善的日志记录系统对于问题排查、系统监控和用户行为分析都至关重要。传统的日志记录方式往往需要在每个方法中手动添加日志代码,这不仅增加了代码量,也使得业务逻辑与日志记录代码耦合在一起。Spring
AOP(面向切面编程)为我们提供了一种优雅的解决方案,可以无侵入式地实现日志记录功能。

本文将详细介绍如何在SpringBoot项目中利用AOP实现统一的日志记录功能。

一、AOP基本概念

在开始实现之前,我们先了解几个AOP的核心概念:

  • 切面(Aspect):横切关注点的模块化,如日志记录就是一个切面

  • 连接点(Joinpoint):程序执行过程中的某个特定点,如方法调用或异常抛出

  • 通知(Advice):在切面的某个连接点上执行的动作

  • 切入点(Pointcut):匹配连接点的谓词,用于确定哪些连接点需要执行通知

  • 目标对象(Target Object):被一个或多个切面通知的对象

在这里插入图片描述

二、项目准备

需要创建一个SpringBoot项目,添加以下依赖:

<dependencies><!-- Spring Boot Starter Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Boot Starter AOP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- Lombok 简化代码 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>

三、实现日志记录切面

1、创建自定义日志注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {/*** 操作名称* @return*/String operation() default "";/*** 操作的类型* @return*/BusinessType businessType() default BusinessType.OTHER;
}

2、实现日志切面

创建一个切面类,实现日志切面功能。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;@Aspect
@Component
public class LoggingAspect {private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);/*** 定义切入点:所有带有@Loggable注解的方法*/@Pointcut("@annotation(com.yourpackage.Loggable)")public void loggableMethods() {}/*** 环绕通知:记录方法执行前后的日志*/@Around("loggableMethods()")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();Loggable loggable = method.getAnnotation(Loggable.class);String methodName = joinPoint.getTarget().getClass().getName() + "." + method.getName();// 记录方法开始日志if (loggable.recordParams()) {logger.info("===> 开始执行 {},参数: {}", methodName, Arrays.toString(joinPoint.getArgs()));} else {logger.info("===> 开始执行 {}", methodName);}long startTime = System.currentTimeMillis();try {// 执行目标方法Object result = joinPoint.proceed();// 记录方法结束日志long elapsedTime = System.currentTimeMillis() - startTime;if (loggable.recordResult()) {logger.info("<=== 执行完成 {},耗时: {}ms,结果: {}", methodName, elapsedTime, result);} else {logger.info("<=== 执行完成 {},耗时: {}ms", methodName, elapsedTime);}return result;} catch (Exception e) {// 记录异常日志long elapsedTime = System.currentTimeMillis() - startTime;logger.error("<=== 执行异常 {},耗时: {}ms,异常: {}", methodName, elapsedTime, e.getMessage(), e);throw e;}}/*** 对Controller层的方法进行日志记录*/@Pointcut("execution(* com.yourpackage.controller..*.*(..))")public void controllerLog() {}@Before("controllerLog()")public void doBefore(JoinPoint joinPoint) {// 获取请求信息ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return;}HttpServletRequest request = attributes.getRequest();// 记录请求信息logger.info("==============================请求开始==============================");logger.info("请求URL: {}", request.getRequestURL().toString());logger.info("HTTP方法: {}", request.getMethod());logger.info("IP地址: {}", request.getRemoteAddr());logger.info("类方法: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());logger.info("请求参数: {}", Arrays.toString(joinPoint.getArgs()));}@AfterReturning(returning = "result", pointcut = "controllerLog()")public void doAfterReturning(Object result) {logger.info("返回结果: {}", result);logger.info("==============================请求结束==============================");}
}

3、配置AOP

确保SpringBoot应用启用了AOP支持(默认就是启用的),如果需要自定义配置,可以在application.properties中添加。

# AOP配置
spring.aop.auto=true
spring.aop.proxy-target-class=true

四、使用示例

1. 在Controller中使用

@RestController
@RequestMapping("/api/user")
public class UserController {@GetMapping("/{id}")@Loggable("根据ID获取用户信息")public User getUser(@PathVariable Long id) {// 业务逻辑return userService.getUserById(id);}@PostMapping@Loggable(value = "创建新用户", recordParams = false)public User createUser(@RequestBody User user) {// 业务逻辑return userService.createUser(user);}
}

2. 在Service中使用

@Service
public class UserService {@Loggable("根据ID查询用户")public User getUserById(Long id) {// 业务逻辑}@Loggable(value = "创建用户", recordResult = false)public User createUser(User user) {// 业务逻辑}
}

六、高级配置

1. 日志内容格式化

我们可以创建一个工具类来美化日志输出:

public class LogFormatUtils {public static String formatMethodCall(String className, String methodName, Object[] args) {StringBuilder sb = new StringBuilder();sb.append(className).append(".").append(methodName).append("(");if (args != null && args.length > 0) {for (int i = 0; i < args.length; i++) {if (i > 0) {sb.append(", ");}sb.append(formatArg(args[i]));}}sb.append(")");return sb.toString();}private static String formatArg(Object arg) {if (arg == null) {return "null";}// 对于基本类型和字符串直接返回if (arg instanceof Number || arg instanceof Boolean || arg instanceof Character || arg instanceof String) {return arg.toString();}// 对于集合和数组,只显示大小if (arg.getClass().isArray()) {return "array[" + Array.getLength(arg) + "]";}if (arg instanceof Collection) {return "collection[" + ((Collection<?>) arg).size() + "]";}if (arg instanceof Map) {return "map[" + ((Map<?, ?>) arg).size() + "]";}// 其他复杂对象只显示类名return arg.getClass().getSimpleName();}
}

然后在切面中使用:

@Around("loggableMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();String methodCall = LogFormatUtils.formatMethodCall(joinPoint.getTarget().getClass().getSimpleName(),signature.getName(),joinPoint.getArgs());logger.info("===> 调用: {}", methodCall);// ... 其他逻辑
}

2. 异步日志记录

对于一些并发场景,可以考虑异步记录日志以减少性能影响:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(100);executor.setThreadNamePrefix("AsyncLogger-");executor.initialize();return executor;}
}// 然后在切面方法上添加@Async注解
@Async
@Around("loggableMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {// 日志记录逻辑
}

3. 日志脱敏处理

对于敏感信息如手机号、身份证号等,需要进行脱敏处理:

public class SensitiveInfoUtils {private static final String PHONE_REGEX = "(\\d{3})\\d{4}(\\d{4})";private static final String ID_CARD_REGEX = "(\\d{4})\\d{10}(\\w{4})";public static String desensitize(Object arg) {if (arg == null) {return null;}String str = arg.toString();// 手机号脱敏if (str.matches("\\d{11}")) {return str.replaceAll(PHONE_REGEX, "$1****$2");}// 身份证号脱敏if (str.matches("\\d{18}|\\d{17}[xX]")) {return str.replaceAll(ID_CARD_REGEX, "$1**********$2");}// 其他敏感信息处理...return str;}
}// 在LogFormatUtils中使用
private static String formatArg(Object arg) {// ... 其他逻辑return SensitiveInfoUtils.desensitize(arg);
}

七、代理类生成的核心逻辑

问题1: 既然是代理类,那么像@Transaction 注解标注在方法上怎么搞?给方法生成代理类?(显然不是)

@Transactional 虽然通常标注在方法上,但 Spring 的代理生成策略会综合考虑 类级别和方法级别 的注解。
以下是其完整工作原理:
Spring 处理 @Transactional 时,代理生成分为两步:

步骤 1:扫描 Bean 的代理需求

  • 类级别检查:Spring 在创建 Bean 时,会检查 类或父类 是否有 @Transactional 注解(类级别注解会影响所有方法)。

  • 方法级别检查:如果类未被注解,则扫描所有 公有方法(public),发现任意方法有 @Transactional 时,整个类会被代理

步骤 2:生成代理对象

  • JDK 动态代理:如果类实现了接口,默认使用 JDK 代理(基于 InvocationHandler)。

  • CGLIB 代理:如果类未实现接口,则生成子类代理(通过字节码增强)。

问题2:自定义的注解@LogExecutionTime,也会生成代理类吗?

自定义的注解 @LogExecutionTime 是否会生成代理类,取决于如何实现这个注解的功能。如果通过 Spring AOP 来实现 @LogExecutionTime 的功能(例如记录方法执行时间),那么确实会生成代理类。比如下面的代码

//自定义一个注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME) // 注解在运行时保留
@Target(ElementType.METHOD) // 注解可以应用于方法
public @interface LogExecutionTime {
}//定义切面类,使用 @Aspect 注解标记,并在其中定义切点(Pointcut)和通知(Advice)。
//切点通过 @annotation(LogExecutionTime) 匹配所有带有 @LogExecutionTime 注解的方法。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;@Aspect // 声明这是一个切面类
@Component // 确保切面类被 Spring 容器管理
public class LogExecutionTimeAspect {// 定义环绕通知,匹配所有带有 @LogExecutionTime 注解的方法@Around("@annotation(LogExecutionTime)")public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {long startTime = System.currentTimeMillis(); // 记录方法开始执行时间// 执行目标方法Object result = joinPoint.proceed();long executionTime = System.currentTimeMillis() - startTime; // 计算方法执行时间// 输出日志System.out.println("Method " + joinPoint.getSignature().getName() + " executed in " + executionTime + "ms");return result; // 返回目标方法的执行结果}
}//在业务方法上面使用这个自定义注解
@Service
public class MyService {@LogExecutionTime // 标记需要记录执行时间的方法public void myBusinessMethod() {// 模拟业务逻辑try {Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Business logic executed.");}
}

那么生成代理类的流程是这样的,

  1. 切面类注册:Spring 扫描到 @Aspect 标注的类(如 LogExecutionTimeAspect ngAspect),将其注册为 Bean,并识别其中的切点和通知。(切面类不生成代理类)
  2. 接着根据切点,再扫描符合条件的bean,**@annotation(LogExecutionTime)**所有带有 @LogExecutionTime 注解的方法所在的类对应的bean都会生成代理类。

八、具体执行步骤

给符合条件的bean,生成代理类之后,就要开始执行了,由于不确定是哪一个方法加了@Transactional这种所谓的aop注解,所以代理类,会对每一个方法进行检查。具体步骤如下:

步骤 1:调用代理对象的方法

// 这里实际调用的是代理对象的方法,代理对象会去调用目标对象的原方法
userService.getUserById(1L);

步骤 2:代理对象检查方法是否匹配切点

检查目标方法:代理对象会检查 UserService.getUserById() 是否匹配切面定义的切点(即是否有 @LogExecutionTime 注解)。

// 伪代码:Spring 的切点匹配逻辑
if (方法有 @LogExecutionTime 注解 || 类有 @LogExecutionTime 注解) {将该方法加入拦截链;
}

步骤 3:执行拦截链

如果方法匹配切点,代理对象会:

找到所有匹配的 Advice(如 @Around 通知)。

按顺序执行通知链(如先执行 @Before,再执行 @Around)。

在 @Around 中,通过 ProceedingJoinPoint.proceed() 调用原始方法。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/80254.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux中的常用命令(一)

目录 常用的快捷键 1- tab键:命令或者路径提示及补全&#xff1b; 2-ctrlc:放弃当前输入&#xff0c;终止当前任务或程序 3-ctrll;清屏 4-ctrlinsert:复制 5-鼠标右键:粘贴&#xff1b; 6-altc:断开连接/ctrlshift r 重新连接 7-alt1/2/3/等&#xff1a;切换回话窗口 8-上下键…

Pycharm(十九)深度学习

一、深度学习概述 1.1 什么是深度学习 深度学习是机器学习中的一种特殊方法,它使用称为神经网络的复杂结构,特别是“深层”的神经网络,来学习和做出预测。深度学习特别适合处理大规模和高维度的数据,如图像、声音和文本。深度学习、机器学习和人工智能之间的关系如下图所…

多视图密集对应学习:细粒度3D分割的自监督革命

原文标题&#xff1a;Multi-view Dense Correspondence Learning (MvDeCor) 引言 在计算机视觉与图形学领域&#xff0c;3D形状分割一直是一个基础且具有挑战性的任务。如何在标注稀缺的情况下&#xff0c;实现对3D模型的细粒度分割&#xff1f;近期&#xff0c;斯坦福大学视觉…

Vue——前端vue3项目使用汉字转拼音

在 Vue3 项目中&#xff0c;可以通过以下 第三方 JavaScript 包 实现汉字转拼音。这些包均兼容 Vue3&#xff0c;且无需依赖后端处理&#xff1a; 推荐方案 1. pinyin-pro 特点&#xff1a;功能强大、支持多音字、声调、拼音匹配、轻量级&#xff08;~20KB&#xff09;。安装…

批量统计PDF页数,统计图像属性

软件介绍&#xff1a; 1、支持批量统计PDF、doc\docx、xls\xlsx页数 2、支持统计指定格式文件数量&#xff08;不填格式就是全部&#xff09; 3、支持统计JPG、JPEG、PNG图像属性 4、支持统计多页TIF页数、属性 5、支持统计PDF、JPG画幅 统计图像属性 「托马斯的文件助手」…

LeetCode 每日一题 2025/5/5-2025/5/11

记录了初步解题思路 以及本地实现代码&#xff1b;并不一定为最优 也希望大家能一起探讨 一起进步 目录 5/5 790. 多米诺和托米诺平铺5/6 1920. 基于排列构建数组5/7 3341. 到达最后一个房间的最少时间 I5/8 3342. 到达最后一个房间的最少时间 II5/9 3343. 统计平衡排列的数目5…

pytest自动化测试执行环境切换的两种解决方案

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 一、痛点分析 在实际企业的项目中&#xff0c;自动化测试的代码往往需要在不同的环境中进行切换&#xff0c;比如多套测试环境、预上线环境、UAT环境、线上环…

visual studio 2015 安装闪退问题

参考链接&#xff1a; VS2012安装时启动界面一闪而过问题解决办法 visual studio 2015 安装闪退问题

RocketMQ Kafka区别

架构 ZooKeeper&#xff1a;管理 Broker 注册、分区 Leader 选举及消费者组状态。Broker&#xff1a;存储 Partition数据&#xff0c;每个 Partition 为独立日志文件。Producer/Consumer&#xff1a;通过 ZooKeeper获取路由信息&#xff0c;实现消息分发与消费。 NameServer&am…

MySQL进阶篇2_SQL优化、锁

文章目录 1 SQL优化1.1插入数据优化1.2主键优化页分裂页合并主键设计原则 1.3order by设计优化1.4group by设计优化小理解 1.5limit设计优化顺序IO和随机IO小疑惑 1.6count设计优化1.7update优化关于隐式事务事务的DML操作 锁全局锁表级锁表锁元数据锁意向锁 行级锁锁的释放条件…

如何测试 esp-webrtc-solution_solutions_doorbell_demo 例程?

软件准备 esp-webrtc-solution/solutions/doorbell_demo 例程 此例程集成了 WebSocket 传输视频流的应用 硬件准备 ESP32P4-Function-Ev-Board 环境搭建 推荐基于 esp-idf v5.4.1 版本的环境来编译此例程 若编译时出现依赖的组件报错&#xff0c;可进行如下修改&#xff…

TransmittableThreadLocal:穿透线程边界的上下文传递艺术

文章目录 前言一、如何线程上下文传递1.1 ThreadLocal单线程1.2 InheritableThreadLocal的继承困境1.3 TTL的时空折叠术 二、TTL核心设计解析2.1 时空快照机制2.2 装饰器模式2.3 采用自动清理机制 三、设计思想启示四、实践启示录结语 前言 在并发编程领域&#xff0c;线程上下…

【数据结构】——栈

一、栈的概念和结构 栈其实就是一种特殊的顺序表&#xff0c;其只允许在一端进出&#xff0c;就是栈的数据的插入和删除只能在一端进行&#xff0c;进行数据的插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的元素遵循先进后出LIFO&#xff08;Last InFirst O…

大数据技术全景解析:Spark、Hadoop、Hive与SQL的协作与实战

引言&#xff1a;当数据成为新时代的“石油” 在数字经济时代&#xff0c;数据量以每年50%的速度爆发式增长。如何高效存储、处理和分析PB级数据&#xff0c;成为企业竞争力的核心命题。本文将通过通俗类比场景化拆解&#xff0c;带你深入理解四大关键技术&#xff1a;Hadoop、…

Android13 权限管理机制整理

一、概述 权限机制作为Android 系统安全的保证,很重要,这里整理一下 权限机制中framework 部分,selinux等其他的Android权限机制不在本次讨论范围内 二、个版本差异分类 Android13 Android12 Android11 及以下 抛开版本差异权限机制分为两大类 一类是之前apk在Android6.0…

MySQL的Order by与Group by优化详解!

目录 前言核心思想&#xff1a;让索引帮你“排好序”或“分好组”Part 1: ORDER BY 优化详解1.1 什么是 Filesort&#xff1f;为什么它慢&#xff1f;1.2 如何避免 Filesort&#xff1f;—— 利用索引的有序性1.3 EXPLAIN 示例 (ORDER BY) Part 2: GROUP BY 优化详解2.1 什么是…

awesome-digital-human本地部署及配置:打造高情绪价值互动指南

在数字化交互的浪潮中&#xff0c;awesome-digital-human-live2d项目为我们打开了本地数字人互动的大门。结合 dify 聊天 api&#xff0c;并借鉴 coze 夸夸机器人的设计思路&#xff0c;能为用户带来充满情绪价值的交互体验。本文将详细介绍其本地部署步骤、dify 配置方法及情绪…

[ctfshow web入门] web68

信息收集 highlight_file被禁用了&#xff0c;使用cinclude("php://filter/convert.base64-encode/resourceindex.php");读取index.php&#xff0c;使用cinclude("php://filter/convert.iconv.utf8.utf16/resourceindex.php");可能有些乱码&#xff0c;不…

计算机网络:深度解析基于链路状态的内部网关协议IS-IS

IS-IS(Intermediate System to Intermediate System)路由协议详解 IS-IS(Intermediate System to Intermediate System)是一种基于链路状态的内部网关协议(IGP),最初由ISO为OSI(开放系统互连)模型设计,后经扩展支持IP路由。它广泛应用于大型运营商网络、数据中心及复…

SEGGER项目

SystemView 查看版本, 查看SEGGER官网&#xff0c;release时间是2019-12-18日, 而3.12.0的版本日期是2020-05-04 #define SEGGER_SYSVIEW_MAJOR 3 #define SEGGER_SYSVIEW_MINOR 10 #define SEGGER_SYSVIEW_REV 0SEGGER EMBEDDED Studio 根据S…