为何ServletInputStream不支持屡次读取

前言

Springboot的项目中使用ServletFilter来实现方法签名时,发现ServletInputStream不支持屡次读取流。apache

虽然网上有不少解决方案的例子,可是我发现没有一篇文章解释为何会这样的文章,因此决定本身去研究源码。tomcat

ServletInputStream和InputStream

首先确定是研究ServletInputStream这个类了,却发现这个类只是一个抽象类,它继承了InputStream这个类。服务器

那么首先研究ServletInputStream,却发现惟一和流读取的方法readLine()并未限制流进行重复读取。eclipse

既然这样,那限制流重复读取的缘由是不是在InputStream中呢?ide

却在InputStream中发现了其实流是支持重复读取的相关方法定义:this

  • mark()标记当前流读取的位置
  • reset()重置流到mark()所标记的位置
  • markSupported()是否支持标记

既然不是因为ServletInputStream引发的,那只好辛苦点,调试整个请求的链路了。设计

AbstractMessageConverterMethodArgumentResolver

全链路跟踪调试后,总算是发现了端倪,在AbstractMessageConverterMethodArgumentResolver中发现了关键方法readWithMessageConverters(),关键代码以下调试

@Nullable
    protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
            Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
        ...省略非关键代码...
        EmptyBodyCheckingHttpInputMessage message;
        try {
            // 此处为关键代码
            message = new EmptyBodyCheckingHttpInputMessage(inputMessage);
            ...省略非关键代码...
        }
        catch (IOException ex) {
            throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
        }
        ...省略非关键代码...
        return body;
    }

在上面代码中EmptyBodyCheckingHttpInputMessage这个类就是关键类,而这个关键实际上是AbstractMessageConverterMethodArgumentResolver的内部类,关键代码以下code

public EmptyBodyCheckingHttpInputMessage(HttpInputMessage inputMessage) throws IOException {
            this.headers = inputMessage.getHeaders();
            InputStream inputStream = inputMessage.getBody();
            // 判断InputStream支持mark()
            if (inputStream.markSupported()) {
                // 在InputStream起始位置进行标记
                inputStream.mark(1);
                // 若是InputStream不为空则赋值
                this.body = (inputStream.read() != -1 ? inputStream : null);
                // 重置流,表示流能够进行重复读取
                inputStream.reset();
            }
            else {
                // PushbackInputStream是一个支持重复读取的流
                PushbackInputStream pushbackInputStream = new PushbackInputStream(inputStream);
                int b = pushbackInputStream.read();
                if (b == -1) { // 为-1表示流中没有数据
                    this.body = null;
                }
                else {
                    this.body = pushbackInputStream;
                    // 回退操做,使InputStream能够进行重复读取
                    pushbackInputStream.unread(b);
                }
            }
        }

从上面的代码能够看出,其实Spring MVC对于ServletInputStream是支持重复读的(关于PushbackInputStream的源码这里不进行展开)。可是为何会出现ServletInputStream不能重复读取的状况呢?server

因而我又再次进行调试,总算发现了问题在于应用服务器上,因为我调试的代码是用SpringBoot的,使用的应用服务器是tomcat

应用服务器

tomcat

tomcatorg.apache.catalina.connector.Request实现了HttpServletRequest,咱们首先要关注其实现的getInputStream()方法,关键代码以下

/**
     * ServletInputStream
     */
    protected CoyoteInputStream inputStream =
            new CoyoteInputStream(inputBuffer);
    
    // ...省略非关键代码...
    
    @Override
    public ServletInputStream getInputStream() throws IOException {
        ...省略非关键代码...
        if (inputStream == null) {
            // 关键代码
            inputStream = new CoyoteInputStream(inputBuffer);
        }
        return inputStream;

    }

从上面的关键代码能够得知,实际返回ServletInputStream实际上是CoyoteInputStream,继续研究CoyoteInputStream后发现其内部实际上是使用一个InputBuffer对象来存储实际的流数据,关键代码以下:

/**
     * 实际存储的数据
     */
    protected InputBuffer ib;
    
    @Override
    public int read() throws IOException {
        checkNonBlockingRead();
        
        if (SecurityUtil.isPackageProtectionEnabled()) {
            ...省略非关键代码...
        } else {
            // 关键代码
            return ib.readByte();
        }
    }

从上面的关键代码能够得知,实际上对于流的读取仍是使用了org.apache.catalina.connector.InputBufferreadByte()方法,InputBuffer的关键代码以下:

/**
     * The byte buffer.
     */
    private ByteBuffer bb;
    
    ...省略非关键代码...
    
    public int readByte() throws IOException {
        if (closed) {
            throw new IOException(sm.getString("inputBuffer.streamClosed"));
        }
        // 关键代码
        if (checkByteBufferEof()) {
            return -1;
        }
        return bb.get() & 0xFF;
    }
    
    private boolean checkByteBufferEof() throws IOException {
        if (bb.remaining() == 0) {
            int n = realReadBytes();
            if (n < 0) {
                return true;
            }
        }
        return false;
    }

后续不进行展开,由于tomcat的调用关系特别复杂。可是能够肯定了ServletInputStream不支持屡次读取是因为tomcat引发的。

后续我调试跟踪了jettyundertow,下面会提供关键类及关键方法,有兴趣的朋友能够自行断点调试。

jetty

jetty也是不支持ServletInputStream屡次读取,关键类及关键方法为org.eclipse.jetty.server.HttpInputread()方法

undertow

jetty也是不支持ServletInputStream屡次读取,关键类及关键方法为io.undertow.servlet.specread()方法

疑问

为何应用服务器都将ServletInputStream设计为不可重复读取?

相关文章
相关标签/搜索