【原創】InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times


一、背景

基於SpringBoot 構建了一個http文件下載服務,檢查tomcat access 發現偶爾出現500 狀態碼的請求,檢查拋出的異常堆棧

2019-03-20 10:03:14,273 ERROR [http-bio-8080-exec-3] o.s.b.w.s.s.ErrorPageFilter - Forwarding to error page from request [/demo.xls] due to exception [org.springframewo
rk.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: InputStream has already been read - do not use InputStreamResource if a stream needs to be
read multiple times]
javax.servlet.ServletException: org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: InputStream has already been read - do not
 use InputStreamResource if a stream needs to be read multiple times
        at com.vdian.vtrace.VtraceFilter.doFilter(VtraceFilter.java:65)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at org.springframework.web.filter.HttpPutFormContentFilter.doFilterInternal(HttpPutFormContentFilter.java:109)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:81)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:115)
        at org.springframework.boot.web.servlet.support.ErrorPageFilter.access$000(ErrorPageFilter.java:59)
        at org.springframework.boot.web.servlet.support.ErrorPageFilter$1.doFilterInternal(ErrorPageFilter.java:90)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.springframework.boot.web.servlet.support.ErrorPageFilter.doFilter(ErrorPageFilter.java:108)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
        at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
        at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
        at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:505)
        at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:169)
        at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
        at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:956)
        at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
        at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:436)
        at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1078)
        at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:625)
        at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
        at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
        at java.lang.Thread.run(Thread.java:745)
Caused by: org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalStateException: InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:986)
        at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:870)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:624)
        at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:855)
        at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
        at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
        at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
        at com.vdian.vtrace.VtraceFilter.doFilter(VtraceFilter.java:50)
        ... 40 common frames omitted
Caused by: java.lang.IllegalStateException: InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times
        at org.springframework.core.io.InputStreamResource.getInputStream(InputStreamResource.java:97)
        at org.springframework.http.converter.ResourceHttpMessageConverter.writeContent(ResourceHttpMessageConverter.java:130)
        at org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:124)
        at org.springframework.http.converter.ResourceHttpMessageConverter.writeInternal(ResourceHttpMessageConverter.java:45)
        at org.springframework.http.converter.AbstractHttpMessageConverter.write(AbstractHttpMessageConverter.java:230)
        at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor.writeWithMessageConverters(AbstractMessageConverterMethodProcessor.java:274)
        at org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor.handleReturnValue(HttpEntityMethodProcessor.java:218)
        at org.springframework.web.method.support.HandlerMethodReturnValueHandlerComposite.handleReturnValue(HandlerMethodReturnValueHandlerComposite.java:82)
        at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:119)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:870)
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:776)
        at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
        at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:991)
        at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:925)
        at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:978)
        ... 50 common frames omitted

二、問題排查

從exception message 看,是 

java.lang.IllegalStateException: InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times

StackOverflow 查一下發現有解決方案,就是把Resonse body 由InputStreamResource 類型改為ByteArrayResource即可,

但想八一八什么原因

(一)初步嘗試

取異常請求的 URL,在chrome 瀏覽器上重試,並沒有復現問題

(二)抓包查看

發現 500的請求中有Range Header

(三)問題復現

curl -v -o test.amr -H "Range:bytes=0-" "http://aud.idcheihei.com/se1-sellerexpt-31e9000001698f06b9370a216239.amr"

 使用curl 命令帶上Range header 請求后端,請求返回500,復現問題

(四)根據堆棧查看代碼

org.springframework.core.io.InputStreamResource#getInputStream

inputstream 只能讀取一次內容,所以這里有一個read 成員變量,控制resource 的讀取

當resource 被讀取一次后,第二次讀取就會拋異常

那為什么會讀取兩次呢?

(五)本地debug

根據堆棧,打斷點,org.springframework.core.io.InputStreamResource#getInputStream

逐步調試發現發起第一次getInputStream 調用的地方是:

org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)

由 outputValue = HttpRange.toResourceRegions(httpRanges, resource);拋異常,進入catch 邏輯

org.springframework.http.HttpRange#toResourceRegion 實現體

    public ResourceRegion toResourceRegion(Resource resource) {
        Assert.isTrue(resource.getClass() != InputStreamResource.class, "Cannot convert an InputStreamResource to a ResourceRegion");

        try {
            long contentLength = resource.contentLength();
            Assert.isTrue(contentLength > 0L, "Resource content length should be > 0");
            long start = this.getRangeStart(contentLength);
            long end = this.getRangeEnd(contentLength);
            return new ResourceRegion(resource, start, end - start + 1L);
        } catch (IOException var8) {
            throw new IllegalArgumentException("Failed to convert Resource to ResourceRegion", var8);
        }
    }

org.springframework.core.io.AbstractResource#contentLength的實現體:

 public long contentLength() throws IOException {
        InputStream is = this.getInputStream();

        try {
            long size = 0L;

            int read;
            for(byte[] buf = new byte[255]; (read = is.read(buf)) != -1; size += (long)read) {
            }

            long var6 = size;
            return var6;
        } finally {
            try {
                is.close();
            } catch (IOException var14) {
            }
        }
    } 

 第二次getInputStream 調用的地方是輸出內容的地方,/org/springframework/web/servlet/mvc/method/annotation/AbstractMessageConverterMethodProcessor.class:214

debug 到這里,已定位問題

(六)結論

1、文件下載 http 請求使用range header 時,response 需要設置一個Content-Range header,設置這個header 需要獲取response body 的contentlength,對於InputStreamResource這種resource,需要讀取整個inputstream的內容后才能得到body 的長度
2、resonse 填充文件內容的時候,由於 inputstream 讀取完成后不能再次讀取,所以拋出了InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times 異常

 

本文作者:Blyde
原文鏈接:https://www.cnblogs.com/gliu/p/10570687.html 
版權歸作者所有,轉載請注明出處

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM