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


    前情提要

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

    在 Controller 中定义如下接口:

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

    定义的过滤器如下:

    public class ServNoFilter extends OncePerRequestFilter {
        @Override
        protected 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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    此时启动项目,访问接口,则会在控制台打印如下异常信息: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 对象
         */
        @Override
        public ServletInputStream getInputStream() {
            return new ServletInputStreamWrapper(new ByteArrayInputStream(body));
        }
    
        /**
         * 重写 getReader 方法,返回经过包装后的 BufferedReader 对象
         *
         * @return 经过包装后的 BufferedReader 对象
         */
        @Override
        public 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
             */
            @Override
            public int read() {
                return inputStream.read();
            }
    
            /**
             * 覆盖 isFinished 方法,指示流是否已完成读取数据
             *
             * @return 始终返回 false,表示流未完成读取数据
             */
            @Override
            public boolean isFinished() {
                return false;
            }
    
            /**
             * 重写 isReady 方法,指示流是否准备好进行读取操作
             *
             * @return 始终返回 false,表示流未准备好进行读取操作
             */
            @Override
            public boolean isReady() {
                return false;
            }
    
            /**
             * 重写 setReadListener 方法,设置读取监听器
             *
             * @param readListener 读取监听器
             */
            @Override
            public void setReadListener(ReadListener readListener) {
    
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95

    自定义 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 对象
         */
        @Override
        public ServletOutputStream getOutputStream() {
            if (servletOutputStream == null) {
                servletOutputStream = new ServletOutputStreamWrapper(outputStream);
            }
            return servletOutputStream;
        }
    
        /**
         * 重写 getWriter 方法,返回经过包装后的 PrintWriter 对象
         *
         * @return 经过包装后的 PrintWriter 对象
         */
        @Override
        public 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 字节
             */
            @Override
            public void write(int b) {
                outputStream.write(b);
            }
    
            /**
             * 重写 isReady 方法,指示输出流是否准备好接收写入操作
             *
             * @return 始终返回 false,表示输出流未准备好接收写入操作
             */
            @Override
            public boolean isReady() {
                return false;
            }
    
            /**
             * 重写 setWriteListener 方法,设置写入监听器
             *
             * @param writeListener 写入监听器
             */
            @Override
            public void setWriteListener(WriteListener writeListener) {
    
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120

    自定义过滤器 MiddlewareFilter

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

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

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

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

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

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

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

    public class MiddlewareFilter extends OncePerRequestFilter {
    
        @Override
        protected 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. 调用下一个过滤器或 Servlet
            filterChain.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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60

    配置过滤器

    注解

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

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

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

    @WebFilter(value = "1000", urlPatterns = "/hello")
    public class MiddlewareFilter extends OncePerRequestFilter {
        ......
    }
    
    • 1
    • 2
    • 3
    • 4
    • 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);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    配置类

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

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

    @Configuration
    public class FilterConfig {
    
        @Bean
        public FilterRegistrationBean<MiddlewareFilter> middlewareFilter() {
            FilterRegistrationBean<MiddlewareFilter> registration = new FilterRegistrationBean<>();
            registration.setFilter(new MiddlewareFilter()); // 设置过滤器实例
            registration.addUrlPatterns("/hello"); // 拦截的 URL 路径
            registration.setOrder(1000); // 设置过滤器执行顺序(数字越小,越先执行)
            return registration;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在类中我们定义了名为 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));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    • 请求参数:

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

      {
          "paramKey": "paramValue",
          "responseKey": "responseValue",
          "name": "hello",
          "age": 20
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
    • /hello1 接口返回结果:

      {
          "name": "hello",
          "age": 20
      }
      
      • 1
      • 2
      • 3
      • 4

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

    {
        "paramKey": "paramValue",	//过滤器1
        "responseKey2": "responseValue2",	//过滤器2
        "responseKey": "responseValue",	//过滤器2
        "paramKey2": "paramValue2",	//过滤器1
        "name": "hello",
        "age": 20
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
  • 相关阅读:
    万向区块链肖风:产业元宇宙的“液化现象”
    elasticsearch(ES)分布式搜索引擎02——(DSL查询文档,搜索结果处理)
    MySQL学习笔记22
    软件测试工程师 | 不拼学历,还能进大厂吗?
    Spark SQL 结构化数据文件处理
    Java面试知识点概览(持续更新)
    Spring IOC
    Django之文件上传(一)
    栈Stack
    [yolo系列:如何固定随机种子(以yolov7为例)]
  • 原文地址:https://blog.csdn.net/qq_20185737/article/details/135327958