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

文章目录

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

前情提要

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

在 Controller 中定义如下接口:

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

定义的过滤器如下:

java 复制代码
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();
    }
}

此时启动项目,访问接口,则会在控制台打印如下异常信息: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 如下:

java 复制代码
/**
 * 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) {

        }
    }
}

自定义 HttpServletResponse 包装类 ResponseWrapper

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

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

自定义 HttpServletResponse 包装类 ResponseWrapper 如下:

java 复制代码
/**
 * 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) {

        }
    }
}

自定义过滤器 MiddlewareFilter

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

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

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

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

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

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

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

java 复制代码
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();
    }
}

配置过滤器

注解

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

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

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

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

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

java 复制代码
@ServletComponentScan
@SpringBootApplication
public class Demo1Application {

    public static void main(String[] args) {
        SpringApplication.run(Demo1Application.class, args);
    }

}
配置类

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

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

java 复制代码
@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;
    }

}

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

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

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

编写 Controller 测试

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

java 复制代码
@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 接口:

  • 请求参数:

    json 复制代码
    {
        "name": "hello",
        "age": 20
    }
  • /hello 接口返回结果:

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

    json 复制代码
    {
        "name": "hello",
        "age": 20
    }

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

json 复制代码
{
    "paramKey": "paramValue",	//过滤器1
    "responseKey2": "responseValue2",	//过滤器2
    "responseKey": "responseValue",	//过滤器2
    "paramKey2": "paramValue2",	//过滤器1
    "name": "hello",
    "age": 20
}
相关推荐
葫芦和十三2 小时前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp3 小时前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑3 小时前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯4 小时前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan6 小时前
多Agent之间的区别
后端
青石路8 小时前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充8 小时前
1.面向对象设计思想
后端
IT_陈寒9 小时前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro9 小时前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗9 小时前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端