转载

为什么ServletInputStream不支持多次读取

前言

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

虽然网上有很多解决方案的例子,但是我发现没有一篇文章解释为什么会这样的文章,所以决定自己去研究源码。

ServletInputStream和InputStream

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

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

既然这样,那限制流重复读取的原因是否是在 InputStream 中呢?

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

  • 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 的内部类,关键代码如下

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 不能重复读取的情况呢?

于是我又再次进行调试,总算发现了问题在于应用服务器上,由于我调试的代码是用 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 设计为不可重复读取?

原文  https://segmentfault.com/a/1190000021413998
正文到此结束
Loading...