在一次项目问题排查过程中,发现总是 报错 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 时,要注意:
- 注意 EntityUtils 方法使用,只能读取一次
- 注意 response 是否关闭