记一次 java.io.IOException: Attempted read from closed stream

在一次项目问题排查过程中,发现总是 报错 java.io.IOException: Attempted read from closed stream,根据异常的堆栈分析,定位到了问题的根源。

问题示例如下:

public class Main {

    public static void main(String[] args) throws ClientProtocolException, IOException {
        HttpGet get = new HttpGet("http://www.baidu.com");
        CloseableHttpClient client = HttpClientBuilder.create().build();
        CloseableHttpResponse response = client.execute(get);
        String result = EntityUtils.toString(response.getEntity(), Charset.forName("utf-8"));
        String result1 = EntityUtils.toString(response.getEntity(), Charset.forName("utf-8"));
        System.out.println("Result:" + result);
        System.out.println("Result1:" + result1);
    }
    
}
---------------------------------------------------------------------------
Exception in thread "main" java.io.IOException: Stream closed
	at java.util.zip.GZIPInputStream.ensureOpen(GZIPInputStream.java:62)
	at java.util.zip.GZIPInputStream.read(GZIPInputStream.java:113)
	at org.apache.http.client.entity.LazyDecompressingInputStream.read(LazyDecompressingInputStream.java:73)
	at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:284)
	at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:326)
	at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:178)
	at java.io.InputStreamReader.read(InputStreamReader.java:184)
	at java.io.Reader.read(Reader.java:140)
	at com.beng.http.util.Main.toString(Main.java:71)
	at com.beng.http.util.Main.main(Main.java:30)

原因是因为我们调用了两次的 EntityUtils.toString( ) 这个方法,我们打开这个源码看一下:

public static String toString(final HttpEntity entity, final Charset defaultCharset)
    throws IOException, ParseException {
    Args.notNull(entity, "Entity");
    final InputStream instream = entity.getContent();
    if (instream == null) {
        return null;
    }
    try {
        Args.check(entity.getContentLength() <= Integer.MAX_VALUE,
                "HTTP entity too large to be buffered in memory");
        int i = (int) entity.getContentLength();
        if (i < 0) {
            i = 4096;
        }
        Charset charset = null;
        try {
            final ContentType contentType = ContentType.get(entity);
            if (contentType != null) {
                charset = contentType.getCharset();
            }
        } catch (final UnsupportedCharsetException ex) {
            if (defaultCharset == null) {
                throw new UnsupportedEncodingException(ex.getMessage());
            }
        }
        if (charset == null) {
            charset = defaultCharset;
        }
        if (charset == null) {
            charset = HTTP.DEF_CONTENT_CHARSET;
        }
        final Reader reader = new InputStreamReader(instream, charset);
        final CharArrayBuffer buffer = new CharArrayBuffer(i);
        final char[] tmp = new char[1024];
        int l;
        while ((l = reader.read(tmp)) != -1) {
            buffer.append(tmp, 0, l);
        }
        return buffer.toString();
    } finally {
        instream.close();
    }
}

这里 final InputStream instream = entity.getContent(),我们读取请求返回内容的时候其实就是一个 io 流,并且将其读取完并关闭了这个io流(instream.close())。所以在你第二次调用的时候,读取的是已经关闭的流,所以会报这个异常。
instream.close() 这行代码注释掉,再运行一次:

结果1:<!DOCTYPE html> ...
结果2:

可以看到,结果2 打印出的是空值,因为在第一次读取的时候,就已经将流中数据全部读取出来了。
同时,从源码中可以看到其 final InputStream instream = entity.getContent() 的 Entity 中获取的 Content 是一个IO流。

那么我们顺便看一下,HttpClient 的这个请求流程。

HttpClient 构建

CloseableHttpClient client = HttpClientBuilder.create().build() 这个方法:

public CloseableHttpClient build() {
	....
	return new InternalHttpClient(
                execChain,
                connManagerCopy,
                routePlannerCopy,
                cookieSpecRegistryCopy,
                authSchemeRegistryCopy,
                defaultCookieStore,
                defaultCredentialsProvider,
                defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT,
                closeablesCopy);
}

其返回了 InternalHttpClient 这个 client,这个 client 又继承了 CloseableHttpClient

class InternalHttpClient extends CloseableHttpClient implements Configurable 
执行 GET 请求

CloseableHttpResponse response = client.execute(get)
InternalHttpClient 源码:

@Override
public CloseableHttpResponse execute(
         final HttpUriRequest request) throws IOException, ClientProtocolException {
     return execute(request, (HttpContext) null);
 }

@Override
public CloseableHttpResponse execute(
        final HttpUriRequest request,
        final HttpContext context) throws IOException, ClientProtocolException {
    Args.notNull(request, "HTTP request");
    return doExecute(determineTarget(request), request, context);
}

doExecute:
@Override
protected CloseableHttpResponse doExecute(
        final HttpHost target,
        final HttpRequest request,
        final HttpContext context) throws IOException, ClientProtocolException {
    Args.notNull(request, "HTTP request");
    HttpExecutionAware execAware = null;
    if (request instanceof HttpExecutionAware) {
        execAware = (HttpExecutionAware) request;
    }
    try {
        final HttpRequestWrapper wrapper = HttpRequestWrapper.wrap(request, target);
        final HttpClientContext localcontext = HttpClientContext.adapt(
                context != null ? context : new BasicHttpContext());
        RequestConfig config = null;
        if (request instanceof Configurable) {
            config = ((Configurable) request).getConfig();
        }
        if (config == null) {
            final HttpParams params = request.getParams();
            if (params instanceof HttpParamsNames) {
                if (!((HttpParamsNames) params).getNames().isEmpty()) {
                    config = HttpClientParamConfig.getRequestConfig(params);
                }
            } else {
                config = HttpClientParamConfig.getRequestConfig(params);
            }
        }
        if (config != null) {
            localcontext.setRequestConfig(config);
        }
        setupContext(localcontext);
        final HttpRoute route = determineRoute(target, wrapper, localcontext);
        return this.execChain.execute(route, wrapper, localcontext, execAware);
    } catch (final HttpException httpException) {
        throw new ClientProtocolException(httpException);
    }
}

this.execChain.execute(route, wrapper, localcontext, execAware)
通多断点,调用的 RedirectExec 的 execute 方法:

@Override
    public CloseableHttpResponse execute(
            final HttpRoute route,
            final HttpRequestWrapper request,
            final HttpClientContext context,
            final HttpExecutionAware execAware) throws IOException, HttpException {
        ....
        for (int redirectCount = 0;;) {
            final CloseableHttpResponse response = requestExecutor.execute(
                    currentRoute, currentRequest, context, execAware);
            try {
                if (config.isRedirectsEnabled() &&
                        this.redirectStrategy.isRedirected(currentRequest.getOriginal(), response, context)) {
                     ....
                    currentRoute = this.routePlanner.determineRoute(newTarget, currentRequest, context);
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Redirecting to '" + uri + "' via " + currentRoute);
                    }
                    EntityUtils.consume(response.getEntity());
                    response.close();
                } else {
                    return response;
                }
            } catch (final RuntimeException ex) {
                response.close();
                throw ex;
            } catch (final IOException ex) {
                response.close();
                throw ex;
            } catch (final HttpException ex) {
                // Protocol exception related to a direct.
                // The underlying connection may still be salvaged.
                try {
                    EntityUtils.consume(response.getEntity());
                } catch (final IOException ioex) {
                    this.log.debug("I/O error while releasing connection", ioex);
                } finally {
                    response.close();
                }
                throw ex;
            }
        }
    }

可以看到,在方法中会先判断是否允许自动处理重定向,如果允许,会调用
EntityUtils.consume(response.getEntity()) 这个方法,消费掉 Entity
,如果不允许定向,就返回 response,由我们自己处理。

所以在使用 httpClient 时,要注意:

  1. 注意 EntityUtils 方法使用,只能读取一次
  2. 注意 response 是否关闭
  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值