Spring AOP 切面打印日志完整版

news2025/2/28 22:29:25

我的项目使用的是 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();

        // 打印URL
        System.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;
    }

    @Override
    public BufferedReader getReader() {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    @Override
    public 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);
    }

    @Override
    public boolean isFinished() {
        return this.cachedBodyInputStream.available() == 0;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener listener) {
        throw new UnsupportedOperationException();
    }

    @Override
    public 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 {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        CachedBodyHttpServletRequest cachedBodyRequest = new CachedBodyHttpServletRequest(httpRequest);

        chain.doFilter(cachedBodyRequest, response);
    }

    @Override
    public 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) {
            // 从请求属性中获取 HttpServletRequest
            HttpServletRequest request = attributes.getRequest();
            if (request instanceof CachedBodyHttpServletRequest) {
                CachedBodyHttpServletRequest cachedBodyRequest = (CachedBodyHttpServletRequest) request;
                // 打印URL
                log.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) {
            // 从请求属性中获取 HttpServletRequest
            HttpServletRequest request = attributes.getRequest();
            if (request instanceof CachedBodyHttpServletRequest) {
                CachedBodyHttpServletRequest cachedBodyRequest = (CachedBodyHttpServletRequest) request;
                // 打印URL
                log.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 构建的。

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

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

相关文章

【数据结构】(12) 反射、枚举、lambda 表达式

一、反射 1、反射机制定义及作用 反射是允许程序在运行时检查和操作类、方法、属性等的机制&#xff0c;能够动态地获取信息、调用方法等。换句话说&#xff0c;在编写程序时&#xff0c;不需要知道要操作的类的具体信息&#xff0c;而是在程序运行时获取和使用。 2、反射机制…

ONES 功能上新|ONES Copilot、ONES Project 新功能一览

ONES Copilot 智能 AI 助手模型可配置多种类型模型&#xff0c;服务提供方 Dashscope 的模型列表中新增 DeepSeek V3 与 DeepSeek R1&#xff1b;选择自定义模型配置时&#xff0c;填写私有部署的 DeepSeek 模型相关参数即可。 应用场景&#xff1a; 企业内部自部署或在模型服务…

STM32寄存器控制引脚高低电平

一. 引子 最近在学习32代码的过程当中&#xff0c;虽然在学习IMX6ULL开发板的过程中接触过很多寄存器&#xff0c;最近在返回去看32的时候&#xff0c;在研究代码的时候发现自己对于寄存器的有些特性理解的不够深刻&#xff0c;所以下来的时候去查了资料&#xff0c;以及问了一…

SOC-ATF 安全启动BL1流程分析(1)

一、ATF 源码下载链接 1. ARM Trusted Firmware (ATF) 官方 GitHub 仓库 GitHub 地址: https://github.com/ARM-software/arm-trusted-firmware 这是 ATF 的官方源码仓库&#xff0c;包含最新的代码、文档和示例。 下载方式&#xff1a; 使用 Git 克隆仓库&#xff1a; git…

TDesign:Cascader 级联选择器(省市区三级联动)

Cascader 级联选择器API 参考官方示例代码 在自己的模板中使用&#xff1a;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…

linux中安装部署Jenkins,成功构建springboot项目详细教程

参考别人配置Jenkins的git地址为https&#xff0c;无法连上github拉取项目&#xff0c;所以本章节介绍通过配置SSH地址来连github拉取项目 目录&#xff1a; 1、springboot项目 1.1 创建名为springcloudproject的springboot项目工程 1.2 已将工程上传到github中&#xff0c;g…

2025系统架构师(一考就过):案例之四:架构复用、架构评估、特定架构(DSSA)、架构开发方法(ABSD)

二、软件架构复用 ◆软件产品线是指一组软件密集型系统&#xff0c;它们共享一个公共的、可管理的特性集&#xff0c;满足某个特定市场或任务的具体需要&#xff0c;是以规定的方式用公共的核心资产集成开发出来的。即围绕核心资产库进行管理复用、集成新的系统。 ◆软件架构…

基于定制开发开源AI大模型S2B2C商城小程序的商品选品策略研究

摘要&#xff1a;随着电子商务的蓬勃发展和技术的不断进步&#xff0c;商品选品在电商领域中的重要性日益凸显。特别是在定制开发开源AI大模型S2B2C商城小程序的环境下&#xff0c;如何精准、高效地选择推广商品&#xff0c;成为商家面临的一大挑战。本文首先分析了商品选品的基…

Docker 2025/2/24

用来快速构建、运行和管理应用的工具。帮助部署。 快速入门 代码略 解释 docker run :创建并运行一个容器&#xff0c;-d是让容器在后台运行 --name mysql :给容器起个名字&#xff0c;必须唯一 -p 3306:3306 :设置端口映射 -e KEYVALUE :是设置环境变量 mysql :指定运行的…

爱普生SG-8101CE可编程晶振赋能智能手机的精准心脏

在智能手机高速迭代的今天&#xff0c;高性能、低功耗与小型化已成为核心诉求。智能手机作为人们生活中不可或缺的工具&#xff0c;需要在各种复杂场景下稳定运行。爱普生SG-8101CE可编程晶振凭借其卓越性能&#xff0c;成为智能手机中不可或缺的精密时钟源&#xff0c;为通信、…

StableDiffusion打包 项目迁移 项目分发 1

文章目录 SD项目迁移前置知识webui-user.batwebui.batlaunch_utils.py 下一篇开始实践 SD项目迁移 显卡驱动更新&#xff1a;https://www.nvidia.cn/geforce/drivers/ 下载安装三个程序&#xff1a; python3.10.6: https://www.python.org/downloads/release/python-3106/gi…

【数据结构进阶】哈希表

&#x1f31f;&#x1f31f;作者主页&#xff1a;ephemerals__ &#x1f31f;&#x1f31f;所属专栏&#xff1a;数据结构 目录 前言 一、哈希表的概念 二、哈希函数的实现方法 1. 直接定址法 2. 除留余数法 三、哈希冲突 1. 开放定址法&#xff08;闭散列&#xff0…

【蓝桥杯嵌入式】各模块学习总结

系列文章目录 留空 文章目录 系列文章目录前言一、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学习总结之-枚举

枚举是一个很多语言都有的功能&#xff0c;不过不同语言中其功能各不相同但是要表达的意思是一致的&#xff0c;枚举就是对于一个事物可以穷举出所有可能得值。比如说人的性别就可以用枚举&#xff0c;男人和女人两种。下面我们来学习Rust中的枚举。 一&#xff1a;枚举定义 …

Linux系统管理(十七)——配置英伟达驱动、Cuda、cudnn、Conda、Pytorch、Pycharm等Python深度学习环境

文章目录 前言安装驱动下载安装Cuda编辑环境变量安装Cudnn安装conda验证安装成功配置conda镜像退出conda环境创建python环境查看当前conda环境激活环境安装python包安装pytorch 安装pycharm安装jupyter notebook 前言 深度学习和大语言模型的部署不免会用到Linux系统&#xff…

SLAM算法工程师的技术图谱和学习路径

SLAM(Simultaneous Localization and Mapping)算法工程师是负责开发和实现用于机器人、自动驾驶车辆等领域的SLAM算法的专业人士。下面是SLAM算法工程师需要掌握的基础理论知识: 机器人运动学和动力学:理解机器人在空间中的运动方式和控制方法,包括轮式、蜘蛛腿、飞行器等…

深入了解 Python 中的 MRO(方法解析顺序)

文章目录 深入了解 Python 中的 MRO&#xff08;方法解析顺序&#xff09;什么是 MRO&#xff1f;如何计算 MRO&#xff1f;C3 算法的合并规则C3 算法的合并步骤示例&#xff1a;合并过程解析 MRO 解析失败的场景使用 mro() 方法查看 MRO示例 1&#xff1a;基本用法 菱形继承与…

如何防止 Instagram 账号被盗用:安全设置与注意事项

如何防止 Instagram 账号被盗用&#xff1a;安全设置与注意事项 在这个数字化时代&#xff0c;社交媒体平台如 Instagram 已成为我们日常生活的一部分。然而&#xff0c;随着网络犯罪的增加&#xff0c;保护我们的在线账户安全变得尤为重要。以下是一些关键的安全设置和注意事…

采样算法二:去噪扩散隐式模型(DDIM)采样算法详解教程

参考 https://arxiv.org/pdf/2010.02502 一、背景与动机 去噪扩散隐式模型&#xff08;DDIM&#xff09; 是对DDPM的改进&#xff0c;旨在加速采样过程同时保持生成质量。DDPM虽然生成效果优异&#xff0c;但其采样需迭代数百至数千次&#xff0c;效率较低。DDIM通过以下关键…

各种类型网络安全竞赛有哪些 网络安全大赛的简称

本文是对入门学习的一些概念了解和一些常规场景记录 1.CTF&#xff08;capture the flag&#xff09;是夺旗赛的意思。 是网络安全技术人员之间进行攻防的比赛。 起源1996年DEFCON全球黑客大会&#xff0c;替代之前真实攻击的技术比拼。 (DEFCON极客大会诞生1993&#xff0c;…