spring boot拦截器获取requestBody的最佳实践

之前有一篇文章介绍了,spring boot拦截器获取requestBody的巨坑

https://blog.csdn.net/shenyunsese/article/details/152163374

今日,本着具体该怎么解决这个问题开展。

背景

我们往往会采用spring的拦截器HandlerInterceptor,来处理进入controller之前做一些通用逻辑。如打印最原始的请求日志,权限拦截,数据请求的验签等操作。

一般我们会从request这个javaee的标准对象中去获取数据,如获取query参数,header参数,路径以及requestBody等。但是requestBody是一个输入流,获取了一次之后就没法再获取了。所以,我们要想办法。

思路

我们先使用一个Filter,对request对象做包装,让包装后的request可以被多次获取requestBody输入流。

实现

WrappingFilter

java 复制代码
package com.shenyun.lyguide.config;

import com.shenyun.lyguide.config.httphelper.GlobalHttpServletRequestWrapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;

import java.io.IOException;

@Component
public class WrappingFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        HttpServletRequest requestWrapper=request;
        if(request.getContentType()!=null&&request.getContentType().toLowerCase().contains("application/json")){
            // 返回包装后的request 对象
            requestWrapper = new GlobalHttpServletRequestWrapper(request);
        }
        ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
        filterChain.doFilter(requestWrapper, responseWrapper);
        if(!responseWrapper.isCommitted()){
            responseWrapper.copyBodyToResponse();
        }
    }
}

GlobalHttpServletRequestWrapper

java 复制代码
package com.shenyun.lyguide.config.httphelper;
import java.io.*;

import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;

public class GlobalHttpServletRequestWrapper extends HttpServletRequestWrapper{

    private byte[] body;

    public GlobalHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    public byte[] getContentAsByteArray(){
        if(body==null){
            try {
                body=getRequestBodyAsByteArray(super.getInputStream());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        return body;
    }
    private byte[] getRequestBodyAsByteArray(ServletInputStream inputStream) throws IOException {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];  // 缓冲区大小为1024字节
        int bytesRead;

        while ((bytesRead = inputStream.read(buffer)) != -1) {
            byteArrayOutputStream.write(buffer, 0, bytesRead);  // 写入已读取的数据块
        }

        return byteArrayOutputStream.toByteArray();  // 获取完整的字节数组
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if(body==null|| body.length == 0){
            body=getRequestBodyAsByteArray(super.getInputStream());
        }

        final ByteArrayInputStream bais = new ByteArrayInputStream(body);

        return new ServletInputStream() {

            @Override
            public int read() throws IOException {
                return bais.read();
            }

            @Override
            public boolean isFinished() {
                return false;
            }

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

            @Override
            public void setReadListener(ReadListener readListener) {

            }
        };
    }
}

核心方式是`public ServletInputStream getInputStream()`

GlobalHttpServletRequestWrapper类中的getInputStream方法重载了父类的该方法。读取了一次输入流之后,将输入流做了缓存,这样以后再读取的时候就可以从缓存中读取了。

拦截器获取requestBody

java 复制代码
@Slf4j
public class AuditInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 拦截逻辑
        String method=request.getMethod();
        String uri=request.getRequestURI();
        String queryString=request.getQueryString();

        // 获取requestBody
        String requestBody = "";
        if(request instanceof GlobalHttpServletRequestWrapper) {
            byte[]  bytes=((GlobalHttpServletRequestWrapper)request).getContentAsByteArray();
            requestBody=new String(bytes, StandardCharsets.UTF_8);
        }

        request.setAttribute("startTime",System.currentTimeMillis());
        log.info("请求地址:{},method:{},queryString:{},body:{}",uri,method,queryString,requestBody);

        // 继续处理请求
        return true;
    }
}

实践的提醒

1、不是任何时候都可以包装请求的request对象的,如果是上传文件,文件比较大,包装request的请求流会造成占用过多的内存。

2、如果是打印入参的数据的请求,还可以对controller层做切面来做。

3、建议仅仅对数据原始验签等操作,可以使用包装来获取requestBody验签。打印请求日志这些,可以用切面来做,笔者的实践哈,每个项目组也有自己的规范。

4、(扩展的)同样的道理,出参响应也可以用这种思路来包装对象。但,看第4条。

5、(扩展的)出参响应尽量不要采用包装对象。当对接ai模型使用sse,streamable这些协议的时候,包装出参响应会有问题。当然下载文件的话,肯定也不要包装出参响应。因为response流底层有缓存空间,内部会有机制在适当的时候把响应数据流刷给请求方,如果采用了包装就一定要等到数据写完了到缓存才能响应,如果包装了对文件下载,流式响应场景有影响。

相关推荐
李白的粉20 小时前
基于springboot的知识管理系统
java·spring boot·毕业设计·课程设计·知识管理系统·源代码
三水不滴21 小时前
Elasticsearch 实战系列(二):SpringBoot 集成 Elasticsearch,从 0 到 1 实现商品搜索系统
经验分享·spring boot·笔记·后端·elasticsearch·搜索引擎
QQ243919721 小时前
spring boot医院挂号就诊系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·vue.js·spring boot
Coder-coco21 小时前
家政服务管理系统|基于springboot + vue家政服务管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·家政服务管理系统
张道宁21 小时前
基于Spring Boot与Docker的YOLOv8检测服务实战
spring boot·yolo·docker
学亮编程手记21 小时前
Mars-Admin 基于Spring Boot 3 + Vue 3 + UniApp的企业级管理系统
vue.js·spring boot·uni-app
宸津-代码粉碎机1 天前
SpringBoot 任务执行链路追踪实战:TraceID 透传全解析,实现从调度到执行的全链路可观测
开发语言·人工智能·spring boot·后端·python
Mr.45671 天前
Spring Boot 3 + EasyExcel 3.x 实战:构建高效、可靠的Excel导入导出服务
spring boot·后端·excel
悟空码字1 天前
别再让你的SpringBoot包"虚胖"了!这份瘦身攻略请收好
java·spring boot·后端
闻哥1 天前
深入理解 MySQL InnoDB Buffer Pool 的 LRU 冷热数据机制
android·java·jvm·spring boot·mysql·adb·面试