Spring AOP 切面打印日志完整版
我的项目使用的是 SpringBoot 3。
要在 Spring Boot 3 项目中使用 AOP(面向切面编程)来打印接收和响应的参数,如 URL、参数、头部信息、请求体等,可以按照以下步骤操作:
步骤 1: 添加依赖
确保你的 pom.xml 文件中包含 spring-boot-starter-aop 依赖。如果你创建的是一个标准的 Spring Boot 项目,这个依赖通常已经包含在内了。如果没有,请手动添加:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤 2: 创建切面类
创建一个新的类用于定义你的切面逻辑。例如,可以命名为 LoggingAspect.java
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import jakarta.servlet.http.HttpServletRequest;@Aspect
@Component
public class LoggingAspect {@Before("execution(* your.package.name.controller..*(..))")public void logRequest(JoinPoint joinPoint) {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 打印URLSystem.out.println("URL : " + request.getRequestURL().toString());// 打印HTTP方法System.out.println("HTTP Method : " + request.getMethod());// 打印Header信息Enumeration<String> headerNames = request.getHeaderNames();while (headerNames.hasMoreElements()) {String headerName = headerNames.nextElement();System.out.println(headerName + " : " + request.getHeader(headerName));}// 打印请求参数System.out.println("Params : " + request.getQueryString());// 注意:直接从request对象获取body内容比较复杂,因为一旦读取就无法再次读取。// 对于需要读取body的情况,你可能需要使用HttpServletRequestWrapper或者Filter来实现。}@AfterReturning(pointcut = "execution(* your.package.name.controller..*(..))", returning = "result")public void logResponse(Object result) {System.out.println("Response Body : " + result);}
}
请注意,直接从 HttpServletRequest 中读取 body 内容可能会遇到挑战,因为一旦读取后,流将不可再次读取。对于需要访问 body 的场景,你可能需要使用 HttpServletRequestWrapper 或者 Filter 来缓存请求体,以便多次读取。
步骤 3: 配置类(如果需要)
对于某些高级需求,比如需要读取请求体,你可能还需要创建一个配置类或使用过滤器(Filter)来处理请求体的重复读取问题。
以上就是如何在 Spring Boot 3 项目中使用 AOP 打印接收和响应参数的基本步骤。根据实际需要调整包名和类名,并考虑是否需要处理请求体的特殊情形。
没了吗?
别急, 继续挑战 body 输出。接来下要做改造了。
为了在 Spring Boot 应用中使用 AOP 记录请求体(body),同时避免由于 InputStream 只能被读取一次而导致的问题,你可以通过创建一个自定义的 HttpServletRequestWrapper 来缓存请求体。这样,在你的切面逻辑中就可以多次访问请求体了。下面是一个具体的实现示例:
步骤 1: 创建 CachedBodyHttpServletRequest 类
首先,创建一个类继承 HttpServletRequestWrapper,用于缓存请求体。
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;/*** 自定义 HttpServletRequestWrapper 来缓存请求体。*/
public class CachedBodyHttpServletRequest extends HttpServletRequestWrapper {private byte[] cachedBody;/*** 构造函数,读取并缓存原始请求体。*/public CachedBodyHttpServletRequest(HttpServletRequest request) throws IOException {super(request);InputStream requestInputStream = request.getInputStream();this.cachedBody = requestInputStream.readAllBytes();}/*** 获取缓存的请求体。*/public byte[] getCachedBody() {return cachedBody;}@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(this.getInputStream()));}@Overridepublic ServletInputStream getInputStream() {return new CachedBodyServletInputStream(this.cachedBody);}
}
步骤 2: 创建 CachedBodyServletInputStream 类
然后,创建一个类来包装原始的 ServletInputStream,以便提供缓存的请求体数据。
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;/*** 自定义 ServletInputStream 来包装缓存的请求体。*/
public class CachedBodyServletInputStream extends ServletInputStream {private final ByteArrayInputStream cachedBodyInputStream;/*** 使用缓存的请求体构造实例。*/public CachedBodyServletInputStream(byte[] cachedBody) {this.cachedBodyInputStream = new ByteArrayInputStream(cachedBody);}@Overridepublic boolean isFinished() {return this.cachedBodyInputStream.available() == 0;}@Overridepublic boolean isReady() {return true;}@Overridepublic void setReadListener(ReadListener listener) {throw new UnsupportedOperationException();}@Overridepublic int read() throws IOException {return this.cachedBodyInputStream.read();}
}
步骤 3: 创建并注册 Filter
接下来,创建一个过滤器来替换原始请求为你的 CachedBodyHttpServletRequest 实例。
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;import java.io.IOException;/*** 过滤器用于将原始请求替换为自定义的 CachedBodyHttpServletRequest。*/
@Component
public class CachingRequestBodyFilter implements Filter {@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;CachedBodyHttpServletRequest cachedBodyRequest = new CachedBodyHttpServletRequest(httpRequest);chain.doFilter(cachedBodyRequest, response);}@Overridepublic void destroy() {}
}
注意:确保你在 Spring Boot 配置中注册这个过滤器,例如通过添加 @Component 注解或者在配置类中进行注册。
步骤 4: 修改 LoggingAspect 类
最后,修改你的 LoggingAspect 类以利用缓存请求体的功能:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;/*** 使用 AOP 记录请求的 URL、参数、头部信息以及 body 内容。*/
@Aspect
@Component
@Slf4j
public class LoggingAspect {@Before("execution(* com.tylerzhong.web.controller..*(..))")public void logRequest(JoinPoint joinPoint) throws Exception {log.info("=======================请求数据start=======================");// 获取当前请求属性ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {// 从请求属性中获取 HttpServletRequestHttpServletRequest request = attributes.getRequest();if (request instanceof CachedBodyHttpServletRequest) {CachedBodyHttpServletRequest cachedBodyRequest = (CachedBodyHttpServletRequest) request;// 打印URLlog.info("请求地址:{}", request.getRequestURL().toString());// 打印HTTP方法log.info("请求方式:{}", request.getMethod());// 打印Header信息var headerNames = request.getHeaderNames();log.info("请求头:");while (headerNames.hasMoreElements()) {String headerName = headerNames.nextElement();System.out.println(headerName + ":" + request.getHeader(headerName));}// 打印请求参数String queryString = request.getQueryString();// 此处进行解码处理String decodedQueryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8);log.info("请求参数:{}", decodedQueryString);// 获取请求体字节数组byte[] requestBodyBytes = cachedBodyRequest.getCachedBody();String rawRequestBody = new String(requestBodyBytes, StandardCharsets.UTF_8);log.info("请求体:{}", rawRequestBody);log.info("=======================请求数据end=======================");}}}@AfterReturning(pointcut = "execution(* com.tylerzhong.web.controller..*(..))", returning = "result")public void logResponse(Object result) {log.info("-----------------------响应数据start-----------------------");log.info("响应体:{}", result);log.info("-----------------------响应数据end-----------------------");}
}
代码中的下面这段代码进行解码打印是因为请求参数在传输过程中被进行了 URL 编码(也称为百分号编码)。URL 编码是一种用于将字符转换为可以在 URL 中安全传输的格式的编码方式。例如,中文字符“姓名”和“年龄”会被编码为 %E5%A7%93%E5%90%8D 和 %E5%B9%B4%E9%BE%84。
如果你希望打印出原始的、未经过 URL 编码的请求参数,你需要对这些参数进行解码。Java 提供了 java.net.URLDecoder 类来帮助你完成这个任务。
String queryString = request.getQueryString();
// 此处进行解码处理
String decodedQueryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8);
到上面为止,基本可以说完成了AOP切面打印日志的所有内容。
但是我的项目中会上传大量的文件,并且是通过二进制流传递的,所以打印出来的都是二进制数据,输出到控制台会很长,所以我这里需要对它进行 base64 编码输出,但是如果传的 json 数据,我需要输出原始数据,即不进行编码。所以我就想了一个折中的办法,一般上传文件的话,字节数据大小都是几百KB,所以我就判断,如果字节数据的大于100KB就进行 base64 编码输出,小于 100KB 就输出原始数据。这样既可以节省空间,也简化了的输出内容。
所以再进行改造一下:
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import jakarta.servlet.http.HttpServletRequest;import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Base64;/*** 使用 AOP 记录请求的 URL、参数、头部信息以及 body 内容。*/
@Aspect
@Component
@Slf4j
public class LoggingAspect {private static final int MAX_RAW_BODY_SIZE = 102400; // 100KB@Before("execution(* com.tylerzhong.web.controller..*(..))")public void logRequest(JoinPoint joinPoint) throws Exception {log.info("=======================请求数据start=======================");// 获取当前请求属性ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes != null) {// 从请求属性中获取 HttpServletRequestHttpServletRequest request = attributes.getRequest();if (request instanceof CachedBodyHttpServletRequest) {CachedBodyHttpServletRequest cachedBodyRequest = (CachedBodyHttpServletRequest) request;// 打印URLlog.info("请求地址:{}", request.getRequestURL().toString());// 打印HTTP方法log.info("请求方式:{}", request.getMethod());// 打印Header信息var headerNames = request.getHeaderNames();log.info("请求头:");while (headerNames.hasMoreElements()) {String headerName = headerNames.nextElement();System.out.println(headerName + ":" + request.getHeader(headerName));}// 打印请求参数String queryString = request.getQueryString();String decodedQueryString = URLDecoder.decode(queryString, StandardCharsets.UTF_8);log.info("请求参数:{}", decodedQueryString);// 获取请求体字节数组byte[] requestBodyBytes = cachedBodyRequest.getCachedBody();if (requestBodyBytes.length > MAX_RAW_BODY_SIZE) {// 如果字节数组长度超过阈值,进行Base64编码并打印String base64EncodedRequestBody = Base64.getEncoder().encodeToString(requestBodyBytes);log.info("请求体:{}", base64EncodedRequestBody);} else {// 否则直接打印原始内容String rawRequestBody = new String(requestBodyBytes, StandardCharsets.UTF_8);log.info("请求体:{}", rawRequestBody);}log.info("=======================请求数据end=======================");}}}@AfterReturning(pointcut = "execution(* com.tylerzhong.web.controller..*(..))", returning = "result")public void logResponse(Object result) {log.info("-----------------------响应数据start-----------------------");log.info("响应体:{}", result);log.info("-----------------------响应数据end-----------------------");}
}
请确保替换 com.tylerzhong.web 和 com.tylerzhong.web.controller 为你实际的应用包名和控制器所在的包路径。
以上代码段提供了一个完整的解决方案,用于在 Spring Boot 3 应用中使用 AOP 来记录请求的详细信息,包括 URL、参数、头部信息以及 body 内容。注意,这里假设你已经在项目中正确配置了 Spring AOP 相关依赖,并且你的应用是基于 Spring Boot 构建的。
相关文章:
Spring AOP 切面打印日志完整版
我的项目使用的是 SpringBoot 3。 要在 Spring Boot 3 项目中使用 AOP(面向切面编程)来打印接收和响应的参数,如 URL、参数、头部信息、请求体等,可以按照以下步骤操作: 步骤 1: 添加依赖 确保你的 pom.xml 文件中包…...
【数据结构】(12) 反射、枚举、lambda 表达式
一、反射 1、反射机制定义及作用 反射是允许程序在运行时检查和操作类、方法、属性等的机制,能够动态地获取信息、调用方法等。换句话说,在编写程序时,不需要知道要操作的类的具体信息,而是在程序运行时获取和使用。 2、反射机制…...
SEO炼金术(4)| Next.js SEO 全攻略
在上一篇文章 SEO炼金术(3)| 深入解析 SEO 关键要素 中,我们深入解析了 SEO 关键要素,包括 meta 标签、robots.txt、canonical、sitemap.xml 和 hreflang,并探讨了它们在搜索引擎优化(SEO)中的作…...
ONES 功能上新|ONES Copilot、ONES Project 新功能一览
ONES Copilot 智能 AI 助手模型可配置多种类型模型,服务提供方 Dashscope 的模型列表中新增 DeepSeek V3 与 DeepSeek R1;选择自定义模型配置时,填写私有部署的 DeepSeek 模型相关参数即可。 应用场景: 企业内部自部署或在模型服务…...
STM32寄存器控制引脚高低电平
一. 引子 最近在学习32代码的过程当中,虽然在学习IMX6ULL开发板的过程中接触过很多寄存器,最近在返回去看32的时候,在研究代码的时候发现自己对于寄存器的有些特性理解的不够深刻,所以下来的时候去查了资料,以及问了一…...
SOC-ATF 安全启动BL1流程分析(1)
一、ATF 源码下载链接 1. ARM Trusted Firmware (ATF) 官方 GitHub 仓库 GitHub 地址: https://github.com/ARM-software/arm-trusted-firmware 这是 ATF 的官方源码仓库,包含最新的代码、文档和示例。 下载方式: 使用 Git 克隆仓库: git…...
QVariantList使用详解
QVariantList 1. 基本概念2. 使用场景3. 基本操作3.1 创建和初始化3.2 访问元素3.3 修改元素3.4 删除元素 4. 实际应用示例5. 总结其他QT文章推荐 QVariantList 是 Qt 框架中的一个类,用于存储和操作 QVariant 对象的列表。 QVariant 是 Qt 中用于封装各种类型的通…...
TDesign:Cascader 级联选择器(省市区三级联动)
Cascader 级联选择器API 参考官方示例代码 在自己的模板中使用:view import package:ducafe_ui_core/ducafe_ui_core.dart; import package:flutter/material.dart; import package:get/get.dart; import package:tdesign_flutter/tdesign_flutter.dart;import i…...
Ubuntu20.04下各类常用软件及库安装汇总
1.Miniconda的安装 Ubuntu 20.04版本快速安装 Miniconda(宝宝级攻略) 2.Nvidia显卡驱动/CUDA/cuDNN安装 Ubuntu 20.04安装nvidia显卡驱动/CUDA/cuDNN Ubuntu 20.04:CUDAcuDNN安装&卸载 Y9000P24款 win11ubuntu20.04 双系统教程 3.SGL…...
linux中安装部署Jenkins,成功构建springboot项目详细教程
参考别人配置Jenkins的git地址为https,无法连上github拉取项目,所以本章节介绍通过配置SSH地址来连github拉取项目 目录: 1、springboot项目 1.1 创建名为springcloudproject的springboot项目工程 1.2 已将工程上传到github中,g…...
2025系统架构师(一考就过):案例之四:架构复用、架构评估、特定架构(DSSA)、架构开发方法(ABSD)
二、软件架构复用 ◆软件产品线是指一组软件密集型系统,它们共享一个公共的、可管理的特性集,满足某个特定市场或任务的具体需要,是以规定的方式用公共的核心资产集成开发出来的。即围绕核心资产库进行管理复用、集成新的系统。 ◆软件架构…...
基于定制开发开源AI大模型S2B2C商城小程序的商品选品策略研究
摘要:随着电子商务的蓬勃发展和技术的不断进步,商品选品在电商领域中的重要性日益凸显。特别是在定制开发开源AI大模型S2B2C商城小程序的环境下,如何精准、高效地选择推广商品,成为商家面临的一大挑战。本文首先分析了商品选品的基…...
后端之JPA(EntityGraph+JsonView)
不同表之间的级联操作或者说关联查询是很多业务场景都会用到的。 对于这种需求最朴素的方法自然是手动写关联表,然后对被关联的表也是手动插入数据。但是手写容易最后写成一堆shit代码,而且修改起来也是非常麻烦的。 学会使用现成的工具还是非常有利的…...
linux--卡顿
1,swappiness swappiness是一个影响操作系统如何使用交换空间(swap space)的内核参数。它的值范围是从0到100,这个数值决定了系统将数据从物理内存(RAM)移动到交换分区或交换文件的倾向程度,交…...
【Python pro】函数
1、函数的定义及调用 1.1 为什么需要函数 提高代码复用性——封装将复杂问题分而治之——模块化利于代码的维护和管理 1.1.1 顺序式 n 5 res 1 for i in range(1, n1):res * i print(res) # 输出:1201.1.2 抽象成函数 def factorial(n):res 1for i in range(1…...
Docker 2025/2/24
用来快速构建、运行和管理应用的工具。帮助部署。 快速入门 代码略 解释 docker run :创建并运行一个容器,-d是让容器在后台运行 --name mysql :给容器起个名字,必须唯一 -p 3306:3306 :设置端口映射 -e KEYVALUE :是设置环境变量 mysql :指定运行的…...
4. 示例:创建带约束的随机地址生成器(范围0x1000-0xFFFF)
文章目录 前言代码示例:运行方法:查看结果:关键功能说明:扩展功能建议: 前言 以下是一个完整的SystemVerilog测试平台示例,包含约束随机地址生成、日志输出和波形生成功能: 代码示例࿱…...
爱普生SG-8101CE可编程晶振赋能智能手机的精准心脏
在智能手机高速迭代的今天,高性能、低功耗与小型化已成为核心诉求。智能手机作为人们生活中不可或缺的工具,需要在各种复杂场景下稳定运行。爱普生SG-8101CE可编程晶振凭借其卓越性能,成为智能手机中不可或缺的精密时钟源,为通信、…...
指针解剖学:穿透C/C++内存操作的核心密码与避坑指南
一、指针的本质与内存模型 指针是C/C的核心特性,本质是内存地址的变量化表示。每个变量在内存中占据连续的字节空间,地址是内存单元的唯一编号(如0x0028FF40)。指针变量存储的是目标数据的首地址,通过地址间接操作数据…...
Qt关于平滑滚动的使用QScroller及QScrollerProperties类说明
一、触控时代的滚动工具:QScroller类设计介绍 1.1 从机械滚轮到数字惯性 在触控设备普及前,滚动操作如同老式打字机的滚轴,只能通过鼠标滚轮或滚动条进行离散式控制。QScroller的出现如同给数字界面装上了"惯性飞轮",…...
【音视频】编解码相关概念总结
NALU RTP PS流 三者总体关系 NALU在RTP中的应用:视频流的RTP传输通常将NALU作为基本的单元进行传输。每个RTP包携带一个或多个NALU,这些NALU包含了视频编码数据。RTP协议通过其头部信息(如时间戳、序列号等)帮助接收端重新排列和…...
Vue3 + Vite + TS,使用 配置项目别名属性:resolve
使用 resolve 配置全局项目路径别名 1.优化了开发中单页面引用其他模块的路径复杂性 2.妥妥解决了,组件复用当中提高开发效率 // 不使用配置 import { useStore } from ../../../stores // 使用配置 可根据开发者需求任意定义,较多 import { useStore…...
docker创建nginx
docker run -d -p 8080:80 --name my-nginx-container nginx docker:命令 run:命令 -d:在后台运行容器 -p:8080:80:将容器内部的80端口映射到宿主机的8080端口。 --name my-nginx-container:为容器指定一个…...
StableDiffusion打包 项目迁移 项目分发 1
文章目录 SD项目迁移前置知识webui-user.batwebui.batlaunch_utils.py 下一篇开始实践 SD项目迁移 显卡驱动更新:https://www.nvidia.cn/geforce/drivers/ 下载安装三个程序: python3.10.6: https://www.python.org/downloads/release/python-3106/gi…...
【数据结构进阶】哈希表
🌟🌟作者主页:ephemerals__ 🌟🌟所属专栏:数据结构 目录 前言 一、哈希表的概念 二、哈希函数的实现方法 1. 直接定址法 2. 除留余数法 三、哈希冲突 1. 开放定址法(闭散列࿰…...
【蓝桥杯嵌入式】各模块学习总结
系列文章目录 留空 文章目录 系列文章目录前言一、LED模块1.1 赛题要求1.2 模块原理图1.3 编写代码1.4 赛题实战 二、LCD模块2.1 赛题要求2.2 模块原理图2.3 编写代码2.4 赛题实战 三、按键模块3.1 赛题要求3.2 模块原理图3.3 编写代码3.4 赛题实战 四、串口模块4.1 赛题要求4…...
Rust学习总结之-枚举
枚举是一个很多语言都有的功能,不过不同语言中其功能各不相同但是要表达的意思是一致的,枚举就是对于一个事物可以穷举出所有可能得值。比如说人的性别就可以用枚举,男人和女人两种。下面我们来学习Rust中的枚举。 一:枚举定义 …...
Linux系统管理(十七)——配置英伟达驱动、Cuda、cudnn、Conda、Pytorch、Pycharm等Python深度学习环境
文章目录 前言安装驱动下载安装Cuda编辑环境变量安装Cudnn安装conda验证安装成功配置conda镜像退出conda环境创建python环境查看当前conda环境激活环境安装python包安装pytorch 安装pycharm安装jupyter notebook 前言 深度学习和大语言模型的部署不免会用到Linux系统ÿ…...
SLAM算法工程师的技术图谱和学习路径
SLAM(Simultaneous Localization and Mapping)算法工程师是负责开发和实现用于机器人、自动驾驶车辆等领域的SLAM算法的专业人士。下面是SLAM算法工程师需要掌握的基础理论知识: 机器人运动学和动力学:理解机器人在空间中的运动方式和控制方法,包括轮式、蜘蛛腿、飞行器等…...
【第三天】零基础学习量化基础代码分析-持续更新
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 记录量化基础代码总览引言基本概念量化投资 伪代码示例:量化投资模型框架总结 每日-往期回看 第一天零基础学量化基础知识点总览-持续更新 第二天零基础…...
