Project Experiment: Http RequestBody Repeatable Read


需求分析

项目中如果有这样的需求:针对 POST 请求,需要对请求体中的 JSON 数据进行预处理,比如接口鉴权或者参数校验,此时需要在 Spring MVC 读取 body 之前获取到 body 数据,但是实际情况是请求体的数据是在流中,而流只会被读取一次,为了处理这种场景,我们就需要对 Servlet 容器处理 HTTP 请求体的逻辑进行重构。

技术需求:

  • Servlet 容器技术(比如 Tomcat,了解容器针对 HTTP 请求的处理部分,Servlet、Filter 知识,包装器模式、门面模式、职责链模式);
  • Spring MVC(Handler Mapping、HandlerAdapter,了解从请求映射到具体处理的流程,拦截器知识,模板方法模式,建造者模式);
  • Spring Boot(Embed Container,了解 Spring Boot 嵌入 Servlet 容器的相关知识);
  • HTTP 请求具体的处理过程(web 原生过滤器链、Spring 代理过滤器链、拦截器、AOP 的执行顺序);
  • Java I/O (各种类型的流,装饰器模式);

Spring MVC 中的一些接口或类:

  • org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice
    • 在请求体准备好,还未反序列化,被 Handler 处理之前;
  • org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
    • 被 Handler 处理之后;
  • org.springframework.web.util.ContentCachingRequestWrapper
    • 在请求体被处理后,对请求体中的内容做缓存处理;
  • org.springframework.mock.web.DelegatingServletInputStream
    • Spring test 框架用于测试 controller 而提供的一个 ServletInputStream 的实现,我们可以参考该类对 HttpServletRequestWrapper 做一些扩展;

Servlet 中的某些接口或类:

  • javax.servlet.ServletInputStream
    • 从客户端请求中获取数据的输入流;
  • javax.servlet.http.HttpServletRequestWrapper
    • 对 HttpServletRequest 的包装,提供了一些便捷实现,是包装器(装饰器)模式的应用;
    • 开发者可以根据需要扩展该接口,用来调整 Servlet 请求;

实现

想要实现业务需求,只需要扩展 HttpServletRequestWrapper 即可:

首先提供一个 Filter:

import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.servlet.FilterChain;
import javax.servlet.ReadListener;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class ReadRequestBodyFilter extends HttpFilter {
    
    private static final Log log = LogFactory.getLog(ReadRequestBodyFilter.class);

    private ObjectMapper objectMapper;
    
    public ReadRequestBodyFilter(ObjectMapper objectMapper) {
        super();
        this.objectMapper = objectMapper;
    }

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        CustomRequestWrapper wrapper = new CustomRequestWrapper(request);
        String body = wrapper.getBody();
        
        log.warn("request body json: " + body);
        
        // TODO 这里根据业务需求对数据进行处理
        
        chain.doFilter(wrapper, response);
    }
    
    // 流只会被读取一次, 无法重复读, 但是可以先从流中读取数据, 备份数据后将其再次写入到另一个流中
    static class CustomRequestWrapper extends HttpServletRequestWrapper {
        
        private final String body;
        
        InputStream delegate = null;

        public CustomRequestWrapper(HttpServletRequest request) throws IOException {
            super(request);
            ServletInputStream is = request.getInputStream();
            BufferedInputStream bis = new BufferedInputStream(is, 1024);
            ByteArrayOutputStream baos = new ByteArrayOutputStream(1024);
            byte[] buf = new byte[1024];
            int len;
            while ((len = bis.read(buf)) != -1) {
                baos.write(buf, 0, len);
            }
            body = new String(baos.toByteArray(), StandardCharsets.UTF_8);
            
            baos.close();
            bis.close();
            is.close();

            delegate = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
        }

        public String getBody() {
            return body;
        }

        @Override
        public ServletInputStream getInputStream() throws IOException {
            return new ServletInputStream() {
                @Override
                public boolean isFinished() {
                    return false;
                }
                
                @Override
                public boolean isReady() {
                    return true;
                }

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

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

                @Override
                public int read(byte[] b, int off, int len) throws IOException {
                    return delegate.read(b, off, len);
                }

                @Override
                public int read(byte[] b) throws IOException {
                    return delegate.read(b);
                }

                @Override
                public long skip(long n) throws IOException {
                    return delegate.skip(n);
                }

                @Override
                public int available() throws IOException {
                    return delegate.available();
                }

                @Override
                public void close() throws IOException {
                    delegate.close();
                }

                @Override
                public synchronized void mark(int readlimit) {
                    delegate.mark(readlimit);
                }

                @Override
                public synchronized void reset() throws IOException { 
                    delegate.reset();
                }

                @Override
                public boolean markSupported() {
                    return delegate.markSupported();
                }
            };
        }
    }
}

简单提一下其中的关键点:

  • 日志使用 Springframework 默认使用的 JCL;
  • 序列化使用的是 Jackson;
  • 在 Spring MVC 从流中提取请求体数据之前,我们先将数据提取出来,然后备份该数据,最后将该数据写入另一个流并提供给 Spring MVC 以供读取数据;
  • 对 ServletInputStream 的实现参考:DelegatingServletInputStream

将该过滤器注入 web 原生过滤器链的首部:

@Configuration(proxyBeanMethods = false)
public class TomcatConfiguration {
    
    @Resource
    private ObjectMapper objectMapper;
    
    @Bean
    public FilterRegistrationBean<ReadRequestBodyFilter> readRequestBodyFilter() {
        FilterRegistrationBean<ReadRequestBodyFilter> registrationBean = new FilterRegistrationBean<>(new ReadRequestBodyFilter(this.objectMapper));
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
        registrationBean.setName("readRequestBodyFilter");
        registrationBean.addUrlPatterns("/api/*");
        
        return registrationBean;
    }
}

这里根据需要定制该过滤器行为;

测试 Controller:

@RestController
public class RequestBodyReadController {
    
    @PostMapping("/api/test")
    public String test(@RequestBody Object map) {
        return map.toString();
    }
}

Author: NaiveKyo
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source NaiveKyo !
  TOC