Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体

本文主要是介绍Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前情提要
    • 解决方案
      • 自定义 HttpServletRequest 包装类 RequestWrapper
      • 自定义 HttpServletResponse 包装类 ResponseWrapper
      • 自定义过滤器 MiddlewareFilter
      • 配置过滤器
        • 注解
        • 配置类
      • 编写 Controller 测试

前情提要

在项目中需要使用过滤器 在请求调用 Controller 方法前修改请求参数和在结果返回之前修改返回结果

在 Controller 中定义如下接口:

@PostMapping("/hello")
public JSONObject hello(@RequestBody Map<String, Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));
}

定义的过滤器如下:

public class ServNoFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {// 获取请求体内容String requestBody = getRequestBody(httpServletRequest);// 业务处理......// 放行filterChain.doFilter(httpServletRequest, httpServletResponse);}private String getRequestBody(HttpServletRequest request) throws IOException {BufferedReader reader = new BufferedReader(request.getReader());StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line);}return sb.toString();}
}

此时启动项目,访问接口,则会在控制台打印如下异常信息:Request processing failed; nested exception is java.lang.IllegalStateException: getReader() has already been called for this request

表示在过滤器中已经通过 request.getReader() 方法将请求流读取。

如果在过滤器中将 getReader() 换成 getInputStream() 就会报请求体为空异常:org.springframework.http.converter.HttpMessageNotReadableException: Required request body is missing

这是因为在 Servlet 中,请求对象的输入流只能被读取一次。而在第一次读取请求体时,Servlet 容器会将请求体保存在内存中,并将其解析成相应的请求参数和请求头信息。如果在后续的处理中再次读取请求体,就可能会导致数据错误或异常。

解决方案

自定义 HttpServletRequest 包装类 RequestWrapper

在 Servlet 中,原始的 HttpServletRequest 对象中的请求流(即请求体)只能读取一次。这是因为 HTTP 协议是基于流的协议,服务器在读取请求流时会将其消耗掉,一旦读取完毕,就无法再次读取

当 Servlet 容器读取完请求流后,会将请求的内容解析并储存在相应的属性中,如请求参数、请求头信息等。在后续的处理过程中,Servlet 可以从这些属性中获取请求内容,而不必再次读取请求流。

因此,我们需要自定义 RequestWrapper 将请求流保存下来,并提供方法来多次读取请求体的内容。

自定义 HttpServletRequest 包装类 RequestWrapper 如下:

/*** HttpServletRequest 包装类,允许在 Servlet 中多次读取请求体内容* 重写了 getInputStream()方法和 getReader() 方法,返回可以多次读取的流。*/
public class RequestWrapper extends HttpServletRequestWrapper {private final byte[] body;/*** 构造 RequestWrapper 对象** @param request 原始 HttpServletRequest 对象* @param context 请求体内容*/public RequestWrapper(HttpServletRequest request, String context) {super(request);this.body = context.getBytes(StandardCharsets.UTF_8);}/*** 重写 getInputStream 方法,返回经过包装后的 ServletInputStream 对象** @return 经过包装后的 ServletInputStream 对象*/@Overridepublic ServletInputStream getInputStream() {return new ServletInputStreamWrapper(new ByteArrayInputStream(body));}/*** 重写 getReader 方法,返回经过包装后的 BufferedReader 对象** @return 经过包装后的 BufferedReader 对象*/@Overridepublic BufferedReader getReader() {return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));}/*** 私有内部类,用于包装 ServletInputStream 对象*/private static class ServletInputStreamWrapper extends ServletInputStream {private final ByteArrayInputStream inputStream;/*** 构造函数,传入待包装的 ByteArrayInputStream 对象** @param inputStream 待包装的 ByteArrayInputStream 对象*/public ServletInputStreamWrapper(ByteArrayInputStream inputStream) {this.inputStream = inputStream;}/*** 重写 read 方法,读取流中的下一个字节** @return 读取到的下一个字节,如果已达到流的末尾,则返回-1*/@Overridepublic int read() {return inputStream.read();}/*** 覆盖 isFinished 方法,指示流是否已完成读取数据** @return 始终返回 false,表示流未完成读取数据*/@Overridepublic boolean isFinished() {return false;}/*** 重写 isReady 方法,指示流是否准备好进行读取操作** @return 始终返回 false,表示流未准备好进行读取操作*/@Overridepublic boolean isReady() {return false;}/*** 重写 setReadListener 方法,设置读取监听器** @param readListener 读取监听器*/@Overridepublic void setReadListener(ReadListener readListener) {}}
}

自定义 HttpServletResponse 包装类 ResponseWrapper

与请求流(即请求体)一样,原始的 HttpServletResponse 对象中的响应流(即响应体)只能写入一次。当服务器在向客户端发送响应时,会将响应流写入到网络传输通道中,一旦写入完毕,就无法再次修改或写入。

因此我们需要通过自定义 ResponseWrapper 包装原始的 HttpServletResponse 对象并重写其输出流或者输出写方法,从而实现对响应流的修改和控制。

自定义 HttpServletResponse 包装类 ResponseWrapper 如下:

/*** HttpServletResponse 包装类对,提供对响应数据的处理和操作。*/
public class ResponseWrapper extends HttpServletResponseWrapper {private final ByteArrayOutputStream outputStream;private ServletOutputStream servletOutputStream;private PrintWriter writer;/*** 构造函数,传入原始的 HttpServletResponse 对象** @param response 原始的 HttpServletResponse 对象*/public ResponseWrapper(HttpServletResponse response) {super(response);this.outputStream = new ByteArrayOutputStream();}/*** 重写 getOutputStream 方法,返回经过包装后的 ServletOutputStream 对象** @return 经过包装后的 ServletOutputStream 对象*/@Overridepublic ServletOutputStream getOutputStream() {if (servletOutputStream == null) {servletOutputStream = new ServletOutputStreamWrapper(outputStream);}return servletOutputStream;}/*** 重写 getWriter 方法,返回经过包装后的 PrintWriter 对象** @return 经过包装后的 PrintWriter 对象*/@Overridepublic PrintWriter getWriter() {if (writer == null) {writer = new PrintWriter(getOutputStream());}return writer;}/*** 获取响应数据,并指定字符集** @param charsetName 字符集名称* @return 响应数据字符串*/public String getResponseData(String charsetName) {Charset charset = Charset.forName(charsetName);byte[] bytes = outputStream.toByteArray();return new String(bytes, charset);}/*** 设置响应数据,并指定字符集** @param responseData 响应数据字符串* @param charsetName  字符集名称*/public void setResponseData(String responseData, String charsetName) {Charset charset = Charset.forName(charsetName);byte[] bytes = responseData.getBytes(charset);outputStream.reset();try {outputStream.write(bytes);} catch (IOException e) {// 处理异常}setCharacterEncoding(charsetName);}/*** 私有内部类,用于包装 ServletOutputStream 对象*/private static class ServletOutputStreamWrapper extends ServletOutputStream {private final ByteArrayOutputStream outputStream;/*** 构造函数,传入待包装的 ByteArrayOutputStream 对象** @param outputStream 待包装的 ByteArrayOutputStream 对象*/public ServletOutputStreamWrapper(ByteArrayOutputStream outputStream) {this.outputStream = outputStream;}/*** 重写 write 方法,将指定字节写入输出流** @param b 字节*/@Overridepublic void write(int b) {outputStream.write(b);}/*** 重写 isReady 方法,指示输出流是否准备好接收写入操作** @return 始终返回 false,表示输出流未准备好接收写入操作*/@Overridepublic boolean isReady() {return false;}/*** 重写 setWriteListener 方法,设置写入监听器** @param writeListener 写入监听器*/@Overridepublic void setWriteListener(WriteListener writeListener) {}}
}

自定义过滤器 MiddlewareFilter

我们的需求是:在请求到达服务器之前,对请求参数进行修改;在响应返回之前,对响应结果进行处理。

对于这样的需求,我们可以通过自定义过滤器来实现。大致实现思路如下:

  • 修改请求参数(请求体),我们可以:

    1. 获取请求体内容。
    2. 修改请求体内容。
    3. 将修改后的请求对象替换原来的请求对象,以便后续获取修改后的参数。
  • 修改响应结果(响应体),我们可以:

    1. 获取响应数据。
    2. 对响应数据进行处理。
    3. 将修改后的数据作为最终结果返回。

同时为了确保每个请求在请求时只会被过滤一次,我们可以继承 OncePerRequestFilter 来定义自己的过滤器。

最终,自定义过滤器如下:

public class MiddlewareFilter extends OncePerRequestFilter {@Overrideprotected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {// 1. 从 HttpServletRequest 对象中获取请求体内容String requestBody = getRequestBody(httpServletRequest);// 2. 解析请求体内容为JSON对象JSONObject jsonBody = JSONObject.parseObject(requestBody);// 3. 修改请求体内容jsonBody.put("paramKey","paramValue");// 4. 包装 HttpServletRequest 对象为自定义的 RequestWrapper 对象,以便后续的处理RequestWrapper requestWrapper = new RequestWrapper(httpServletRequest, jsonBody.toJSONString());// 5. 包装 HttpServletResponse 对象为自定义的 ResponseWrapper 对象,以便后续的处理ResponseWrapper responseWrapper = new ResponseWrapper(httpServletResponse);// 6. 调用下一个过滤器或 ServletfilterChain.doFilter(requestWrapper, responseWrapper);// 7. 获取响应数据String responseData = responseWrapper.getResponseData(StandardCharsets.UTF_8.name());// 8. 解析响应数据为JSON对象JSONObject jsonData = JSONObject.parseObject(responseData);// 9. 在这里可以对响应数据进行处理jsonData.put("responseKey", "responseValue");// 10. 将修改后的 JSON 对象转换为字符串responseData = jsonData.toJSONString();// 11. 将修改后的 JSON 对象设置为最终的响应数据responseWrapper.setResponseData(responseData, StandardCharsets.UTF_8.name());// 12. 将响应数据写入原始的响应对象,解决响应数据无法被多个过滤器处理问题OutputStream outputStream = httpServletResponse.getOutputStream();outputStream.write(responseData.getBytes(StandardCharsets.UTF_8));outputStream.flush();}/*** 获取请求体内容。** @param request HttpServletRequest对象* @return 请求体内容* @throws IOException 如果读取请求体内容时发生I/O异常*/private String getRequestBody(HttpServletRequest request) throws IOException {BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));StringBuilder sb = new StringBuilder();String line;while ((line = reader.readLine()) != null) {sb.append(line);}return sb.toString();}
}

配置过滤器

注解

通过 Java Servlet 3.0 规范中引入的 @WebFilter 注解配置过滤器。

@WebFilter 注解可以应用在实现了 Filter 接口或继承自 OncePerRequestFilter 的类上,标识该类为过滤器,并指定过滤器的相关配置,包括拦截的 URL 路径、执行顺序以及初始化参数等。

我们可以在 MiddlewareFilter 过滤器上使用 @WebFilter 注解注册该过滤器并指定执行该过滤器执行的顺序和拦截的 URL:

@WebFilter(value = "1000", urlPatterns = "/hello")
public class MiddlewareFilter extends OncePerRequestFilter {......
}
  • value:设置过滤器的执行顺序,数字越小,优先级越高。
  • urlPatterns:指定要拦截的 URL 路径,允许指定多个 URL 路径urlPatterns = {"/hello","/hello1"}

还需要再启动类上使用@ServletComponentScan注解扫描和注册带有 @WebServlet@WebFilter@WebListener 注解的组件:

@ServletComponentScan
@SpringBootApplication
public class Demo1Application {public static void main(String[] args) {SpringApplication.run(Demo1Application.class, args);}}
配置类

除了注解的形式配置过滤器,我们还可以通过配置类的形式进行配置。

创建 FilterConfig 类用于配置需要注册的过滤器,同时在类上添加 @Configuration 注解,标识该类为配置类,在项目启动时 Spring 会自动扫描该类中的 Bean 定义,并将其加载到容器中:

@Configuration
public class FilterConfig {@Beanpublic FilterRegistrationBean<MiddlewareFilter> middlewareFilter() {FilterRegistrationBean<MiddlewareFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new MiddlewareFilter()); // 设置过滤器实例registration.addUrlPatterns("/hello"); // 拦截的 URL 路径registration.setOrder(1000); // 设置过滤器执行顺序(数字越小,越先执行)return registration;}}

在类中我们定义了名为 middlewareFilter 的方法,用于注册我们自定义的 MiddlewareFilter 过滤器。

在 方法中,创建了一个 FilterRegistrationBean 对象用于注册和配置过滤器,并设置 MiddlewareFilter 对象作为过滤器实例,指定了过滤器要拦截的 URL 路径,滤器执行顺序。

最后将 FilterRegistrationBean 对象返回,以便 Spring 自动进行注册和管理。

编写 Controller 测试

创建两个接口,同样的逻辑,接收一个请求体参数 params,再将接收的参数以 JSON 格式返回:

@RestController
public class BasicController {/*** 处理 /hello 请求的方法* @param params 请求体参数,以键值对的形式传递* @return 经过转换后的 JSONObject 对象*/@PostMapping("/hello")public JSONObject hello(@RequestBody Map<String, Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));}@PostMapping("/hello1")public JSONObject hello1(@RequestBody Map<String,Object> params) {return JSONObject.parseObject(JSON.toJSONString(params));}
}

启动项目,在 ApiFox 中分别以同样的请求参数发送 POST 请求调用 /hello/hello1 接口:

  • 请求参数:

    {"name": "hello","age": 20
    }
    
  • /hello 接口返回结果:

    {"paramKey": "paramValue","responseKey": "responseValue","name": "hello","age": 20
    }
    
  • /hello1 接口返回结果:

    {"name": "hello","age": 20
    }
    

复制多个 MiddlewareFilter 过滤器模拟多层过滤器修改请求体参数和返回结果,测试结果如下:

{"paramKey": "paramValue",	//过滤器1"responseKey2": "responseValue2",	//过滤器2"responseKey": "responseValue",	//过滤器2"paramKey2": "paramValue2",	//过滤器1"name": "hello","age": 20
}

这篇关于Springboot 使用【过滤器】实现在请求到达 Controller 之前修改请求体参数和在结果返回之前修改响应体的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/842156

相关文章

Java的栈与队列实现代码解析

《Java的栈与队列实现代码解析》栈是常见的线性数据结构,栈的特点是以先进后出的形式,后进先出,先进后出,分为栈底和栈顶,栈应用于内存的分配,表达式求值,存储临时的数据和方法的调用等,本文给大家介绍J... 目录栈的概念(Stack)栈的实现代码队列(Queue)模拟实现队列(双链表实现)循环队列(循环数组

使用Python从PPT文档中提取图片和图片信息(如坐标、宽度和高度等)

《使用Python从PPT文档中提取图片和图片信息(如坐标、宽度和高度等)》PPT是一种高效的信息展示工具,广泛应用于教育、商务和设计等多个领域,PPT文档中常常包含丰富的图片内容,这些图片不仅提升了... 目录一、引言二、环境与工具三、python 提取PPT背景图片3.1 提取幻灯片背景图片3.2 提取

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Python实现图片分割的多种方法总结

《Python实现图片分割的多种方法总结》图片分割是图像处理中的一个重要任务,它的目标是将图像划分为多个区域或者对象,本文为大家整理了一些常用的分割方法,大家可以根据需求自行选择... 目录1. 基于传统图像处理的分割方法(1) 使用固定阈值分割图片(2) 自适应阈值分割(3) 使用图像边缘检测分割(4)

Android实现在线预览office文档的示例详解

《Android实现在线预览office文档的示例详解》在移动端展示在线Office文档(如Word、Excel、PPT)是一项常见需求,这篇文章为大家重点介绍了两种方案的实现方法,希望对大家有一定的... 目录一、项目概述二、相关技术知识三、实现思路3.1 方案一:WebView + Office Onl

Java中Switch Case多个条件处理方法举例

《Java中SwitchCase多个条件处理方法举例》Java中switch语句用于根据变量值执行不同代码块,适用于多个条件的处理,:本文主要介绍Java中SwitchCase多个条件处理的相... 目录前言基本语法处理多个条件示例1:合并相同代码的多个case示例2:通过字符串合并多个case进阶用法使用

Java中的Lambda表达式及其应用小结

《Java中的Lambda表达式及其应用小结》Java中的Lambda表达式是一项极具创新性的特性,它使得Java代码更加简洁和高效,尤其是在集合操作和并行处理方面,:本文主要介绍Java中的La... 目录前言1. 什么是Lambda表达式?2. Lambda表达式的基本语法例子1:最简单的Lambda表

Java中Scanner的用法示例小结

《Java中Scanner的用法示例小结》有时候我们在编写代码的时候可能会使用输入和输出,那Java也有自己的输入和输出,今天我们来探究一下,对JavaScanner用法相关知识感兴趣的朋友一起看看吧... 目录前言一 输出二 输入Scanner的使用多组输入三 综合练习:猜数字游戏猜数字前言有时候我们在

C# foreach 循环中获取索引的实现方式

《C#foreach循环中获取索引的实现方式》:本文主要介绍C#foreach循环中获取索引的实现方式,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、手动维护索引变量二、LINQ Select + 元组解构三、扩展方法封装索引四、使用 for 循环替代

Spring Security+JWT如何实现前后端分离权限控制

《SpringSecurity+JWT如何实现前后端分离权限控制》本篇将手把手教你用SpringSecurity+JWT搭建一套完整的登录认证与权限控制体系,具有很好的参考价值,希望对大家... 目录Spring Security+JWT实现前后端分离权限控制实战一、为什么要用 JWT?二、JWT 基本结构