2

前言

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设计为不可重复读取?


Null
137 声望31 粉丝

免费的东西是最贵的,好走的只是下坡路