爲何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設計爲不可重複讀取?

相關文章
相關標籤/搜索