SpringBoot Tomcat(5) 过滤器与请求体的解析

780 阅读6分钟

经过一系列管道之后,来到StandardWrapperValve。Wrapper属于Tomcat中4个级别容器中最小级别的容器,与之对应的是Servlet。

路由映射器确定了Wrapper,此处将会实例化。单例则使用已经创建的,否则每次都创建。在SpringBoot中,默认对应的是DispatcherServlet。

过滤器链

在StandardWrapperValue中将创建一个过滤器链ApplicationFilterChain对象,创建时过滤器链对象做了如下处理(代码见ApplicationFilterFactory->createFilterChain()):

  • 从Context容器中获取所有过滤器的相关信息
  • 通过URL匹配过滤链,匹配的加入到过滤器链中
  • 通过Servlet名称匹配过滤链,匹配的加入到过滤器中

创建完成后,将调用过滤器链的doFilter方法,doFilter方法默认调用internalDoFilter方法,拿到过滤器之后,调用过滤器的doFilter方法,过滤器完成,继续调用过滤器链的doFilter方法进行下一步的操作。

过滤器链全部调用完成后,会调用servlet的service方法,至此Tomcat部分结束。

OrderedCharacterEncodingFilter

将请求与响应的字符编码置为utf-8

OrderedHiddenHttpMethodFilter

如果是POST请求,能够将请求参数_method中的值转为实际的请求,如PUT、DELTE、PATCH等

OrderedFormContentFilter

如果是PUT、PATCH、DELETE请求并且类型是application/x-www-form-urlencoded的话,分析body内容获取参数params,如果params不为空,则封装请求为一个FormContentRequestWrapper然后继续过滤器链的调用(之后使用getParameter()可以获得body内请求参数),否则使用原来的请求继续过滤器链的调用(Tomcat中,application/x-www-form-urlencoded只解析POST类型的请求体

OrderedRequestContextFilter

将请求和响应封装成ServletRequestAttributes,并放入RequestContextHolder中。它的应用:获得httpServletRequest的方式

WsFilter

与WebSocket有关

自定义过滤器

实现Filter接口并注入IoC容器即可

请求体的解析

前言:不同body类型在byteBuffer中的体现

Http11Processor->service()方法解析请求行与请求体,初次调用Request->getParameter()方法时,会对请求体进行解析。

Request->getParameter():
    if (!parametersParsed) {
        parseParameters();
    }
    return coyoteRequest.getParameters().getParameter(name);
// getParameters()返回的是一个Parameters对象,key为参数名称,值为参数值的List集合	
Request->parseParameters();
    parametersParsed = true;
    Parameters parameters = coyoteRequest.getParameters();
    ......
    // 记录url中的请求参数
    parameters.handleQueryParameters();
    // 如果调用过getInputStream或者是getReader方法,就不会解析了
    if (usingInputStream || usingReader) {
        success = true;
        return;
    }
    ......
    // form-data形式
    if ("multipart/form-data".equals(contentType)) {
        // 解析完之后返回
        parseParts(false);
        success = true;
        return;
    }
    // 下面可以认为是application/x-www-form-urlencode形式的解析
    // 	如果不是POST请求的话,直接跳过
    if( !getConnector().isParseBodyMethod(getMethod()) ) {
        success = true;
        return;
    }
    // 如果body类型不是application/x-www-form-urlencode的话,不解析直接就返回了
    if (!("application/x-www-form-urlencoded".equals(contentType))) {
        success = true;
        return;
    }
    // getContentLength是Content-Length的值,表示的是请求体的长度,如果len为0说明没有内容,跳过解析
    int len = getContentLength();
    if (len > 0) {
        // 有值的话就解析并且加入到请求参数集合的Map中
        ......
    }

如果是以form-data形式请求,读出请求体的内容,先创建临时目录,对参数取随机文件名,创建文件并放入临时目录中。加入到parts,如果请求体中有fileName的话,认为是文件,没有fileName的话认为是请求参数,执行parameters->addParameter()方法。

MVC部分中的DispatcherServlet->doDispatch():processedRequest = checkMultipart(request);,如果contentTypemultipart/开头,会将请求类型从RequestFacade转为StandardMultipartHttpServletRequest,表示这是一个多部分的请求类型。执行完方法后,删除暂存目录中的文件。 如图,multipartParameterNames是form-data中text类型的请求参数集合,multipartFiles是file类型的请求参数集合,它的key是参数名,value是文件信息,包含上传的文件名,文件流,文件暂存位置等等。业务逻辑中可通过StandardMultipartHttpServletRequest获取文件相关信息做各种操作。

DispatcherServlet->doDispatch():
    finally {
        if (multipartRequestParsed) {
            // 如果是form-data类型,删除暂存目录中的文件
            cleanupMultipart(processedRequest);
        }
    }

getParameter()multipart/form-dataapplication/x-www-form-urlencode的请求体进行了解析,在请求参数的绑定中,如果遇到了@RequestBody,这个时候才会解析请求体

附录:多次获得请求体

在业务逻辑中会有记录请求信息的需求,一种是给Controller做AoP,这种方式只能记录mapping中url存在的情况,另外一种是利用Filter,在到达MVC之前记录参数,但这种方式要注意,body流只能读取一次,如果参数中有@RequestBody就会报错,因此需要有一种方式多次获得请求体。

方式一:ContentCachingRequestWrapper

public class DemoFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if ("application/json".equals(request.getContentType())) {
            ContentCachingRequestWrapper requestWrapper = new ContentCachingRequestWrapper((HttpServletRequest) request);
            byte[] bytes = new byte[request.getContentLength()];
            requestWrapper.getInputStream().read(bytes);
            request = requestWrapper;
        }
        filterChain.doFilter(request, response);
    }
}

将请求对象转为ContentCachingRequestWrapper,它继承了HttpServletRequestWrapper,HttpServletRequestWrapper实现了HttpServletRequest接口,从名字看出它是对请求的包装。

ContentCachingRequestWrapper->getInputStream():

    private final ByteArrayOutputStream cachedContent;
    
    private ServletInputStream inputStream;

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (this.inputStream == null) {
            this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
        }
        return this.inputStream;
    }

ContentCachingRequestWrapper->ContentCachingInputStream->read():
    @Override
    public int read() throws IOException {
        int ch = this.is.read();
        if (ch != -1 && !this.overflow) {
            if (contentCacheLimit != null && cachedContent.size() == contentCacheLimit) {
                this.overflow = true;
                handleContentOverflow(contentCacheLimit);
            } else {
                cachedContent.write(ch);
            }
        }
        return ch;
    }

ContentCachingRequestWrapper内部的inputStream是自定义的流,它继承了抽象类ServletInputStream,需要重写read方法,从上可以看出,read方法是请求流读取,所以依然只能读取一次,但是它将读取的结果存入了cachedContent对象中,所以后面可以从cachedContent获取请求体结果,对此,controller需要相应的改动

DemoController->test():
    @RequestMapping("/test/**")
    public String test2(HttpServletRequest httpServletRequest) throws Exception {
        JSONObject jsonObject = JSON.parseObject(new String(((ContentCachingRequestWrapper) httpServletRequest).getContentAsByteArray()));
        return "test**";
    }

原先写在方法里的@RequestBody JSONObject jsonObject就不能再使用了,将请求对象转为ContentCachingRequestWrapper,并调用getContentAsByteArray()获取请求体转为String类型,实现了请求体的二次获取。

方式二:重写读取流

第一种方式存在弊端,所有的方法都要改动,成本很大,但是它的实现思路值得参考,那就是缓冲流以及read方法的重写。

首先写一个自定义类BodyReaderHttpServletRequestWrapper继承HttpServletRequestWrapper

    private final byte[] body;
    private ServletInputStream inputStream;
    
    public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        byte[] bytes = new byte[request.getContentLength()];
        request.getInputStream().read(bytes);
        body = bytes;
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        this.inputStream = new ContentCachingInputStream(getRequest().getInputStream());
        return this.inputStream;
    }
    
    private class ContentCachingInputStream extends ServletInputStream {
        private final ServletInputStream is;
        private final ByteArrayInputStream byteArrayInputStream;

        public ContentCachingInputStream(ServletInputStream is) {
            this.is = is;
            byteArrayInputStream = new ByteArrayInputStream(body);
        }
        
        @Override
        public int read() throws IOException {
            return byteArrayInputStream.read();
        }
        
       // ......其他必须要实现的方法...... 
        
    }

在自定义类的构造方法中读取请求体,赋给body对象,getInputStream()方法每次新建自定义流。自定义流有两个成员变量,一个是请求流,一个是缓冲流,构造方法中初始化缓冲流的值为请求体内容,之后的read()方法从缓冲流读取,这样就保证了每次流的读取都是从起始位置开始。

public class DemoFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if ("application/json".equals(request.getContentType())) {
            BodyReaderHttpServletRequestWrapper requestWrapper = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request);
            request = requestWrapper;
    }
}
    @RequestMapping("/test/**")
    public String test2(@RequestBody String string, @RequestBody JSONObject jsonObject) throws Exception {
        return "test**";
    }

不管多少次读取流,都没有关系了。