OkHttp中ResponseBody无法第二次调用string方法

出错的代码
@Test
public void okHttpTest() throws IOException {
String result = getAchievement();
}
private String getAchievement() throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.url("https://api.sunofbeaches.com/ast/achievement/1204736502274318336")
.get()
.build();
Response response = client.newCall(request)
.execute();
ResponseBody responseBody = response.body();
if (responseBody != null) {
System.out.println("result:" + responseBody.string());
return responseBody.string();
}
return null;
}
问题描述
报错日志
closed
java.lang.IllegalStateException: closed
at okio.RealBufferedSource.select(RealBufferedSource.java:93)
at okhttp3.internal.Util.bomAwareCharset(Util.java:467)
at okhttp3.ResponseBody.string(ResponseBody.java:181)
阅读源码
源码注释:
ResponseBody.java
A one-shot stream from the origin server to the client application with the raw bytes of the response body. Each response body is supported by an active connection to the webserver. This imposes both obligations and limits on the client application. The response body must be closed. Each response body is backed by a limited resource like a socket (live network responses) or an open file (for cached responses). Failing to close the response body will leak resources and may ultimately cause the application to slow down or crash. Both this class and Response implement Closeable. Closing a response simply closes its response body. If you invoke Call.execute() or implement Callback.onResponse you must close this body by calling any of the following methods: Response.close() Response.body().close() Response.body().source().close() Response.body().charStream().close() Response.body().byteStream().close() Response.body().bytes() Response.body().string() There is no benefit to invoking multiple close() methods for the same response body. For synchronous calls, the easiest way to make sure a response body is closed is with a try block. With this structure the compiler inserts an implicit finally clause that calls close() for you.
Call call = client.newCall(request); try (Response response = call.execute()) { ... // Use the response. }
You can use a similar block for asynchronous calls:
Call call = client.newCall(request); call.enqueue(new Callback() { public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { ... // Use the response. } }
public void onFailure(Call call, IOException e) { ... // Handle the failure. }
});
These examples will not work if you're consuming the response body on another thread. In such cases the consuming thread must call close when it has finished reading the response body. The response body can be consumed only once. This class may be used to stream very large responses. For example, it is possible to use this class to read a response that is larger than the entire memory allocated to the current process. It can even stream a response larger than the total storage on the current device, which is a common requirement for video streaming applications. Because this class does not buffer the full response in memory, the application may not re-read the bytes of the response. Use this one shot to read the entire response into memory with bytes() or string(). Or stream the response with either source(), byteStream(), or charStream().
译:
从源服务器到客户端应用程序的一次性流,带有响应正文的原始字节。 每个响应正文都由与网络服务器的活动连接支持。 这对客户端应用程序施加了义务和限制。 响应主体必须关闭。 每个响应主体都由有限的资源支持,如套接字(实时网络响应)或打开的文件(用于缓存响应)。 未能关闭响应正文将泄漏资源并最终可能导致应用程序变慢或崩溃。 这个类和Response实现了Closeable 。 关闭响应只是关闭其响应主体。 如果调用Call.execute()或实现Callback.onResponse ,则必须通过调用以下任何方法来关闭此主体: Response.close() Response.body().close() Response.body().source().close() Response.body().charStream().close() Response.body().byteStream().close() Response.body().bytes() Response.body().string() 为同一个响应体调用多个close()方法没有任何好处。 对于同步调用,确保响应主体关闭的最简单方法是使用try块。 使用此结构,编译器会插入一个隐式finally子句,为您调用close() 。
Call call = client.newCall(request); try (Response response = call.execute()) { ... // Use the response. }
您可以使用类似的块进行异步调用:
Call call = client.newCall(request); call.enqueue(new Callback() { public void onResponse(Call call, Response response) throws IOException { try (ResponseBody responseBody = response.body()) { ... // Use the response. } }
public void onFailure(Call call, IOException e) { ... // Handle the failure. }
});
如果您在另一个线程上使用响应正文,这些示例将不起作用。 在这种情况下,消费线程必须在完成读取响应正文后调用close 。 响应体只能被消耗一次。 此类可用于流式传输非常大的响应。 例如,可以使用此类读取大于分配给当前进程的整个内存的响应。 它甚至可以流式传输大于当前设备上总存储量的响应,这是视频流应用程序的常见要求。 由于此类不在内存中缓冲完整的响应,因此应用程序可能不会重新读取响应的字节。 使用此单次使用bytes()或string()将整个响应读入内存。 或者使用source() 、 byteStream()或charStream()流式传输响应。
string 方法源码:
/**
* Returns the response as a string.
*
* <p>If the response starts with a <a href="https://en.wikipedia.org/wiki/Byte_order_mark">Byte
* Order Mark (BOM)</a>, it is consumed and used to determine the charset of the response bytes.
*
* <p>Otherwise if the response has a Content-Type header that specifies a charset, that is used
* to determine the charset of the response bytes.
*
* <p>Otherwise the response bytes are decoded as UTF-8.
*
* <p>This method loads entire response body into memory. If the response body is very large this
* may trigger an {@link OutOfMemoryError}. Prefer to stream the response body if this is a
* possibility for your response.
*
* 译:
* 以字符串形式返回响应。
* 如果响应以字节顺序标记 (BOM) 开头,则使用它并用于确定响应字节的字符集。
* 否则,如果响应具有指定字符集的 Content-Type 标头,则用于确定响应字节的字符集。
* 否则,响应字节将被解码为 UTF-8。
* 此方法将整个响应体加载到内存中。 如果响应正文非常大,这可能会触发OutOfMemoryError 。 如果您的响应有可能,则首选流式传输响应正文。
*/
public final String string() throws IOException {
try (BufferedSource source = source()) {
Charset charset = Util.bomAwareCharset(source, charset());
return source.readString(charset);
}
}
源码解读
因为响应正文可能会非常大,所以响应体只能被消耗一次。如果想要多次使用,用一个临时变量接收即可。
修复后的代码
@Test
public void okHttpTest() throws IOException {
String result = getAchievement();
}
private String getAchievement() throws IOException {
OkHttpClient client = new OkHttpClient.Builder()
.build();
Request request = new Request.Builder()
.url("https://api.sunofbeaches.com/ast/achievement/1204736502274318336")
.get()
.build();
Response response = client.newCall(request)
.execute();
ResponseBody responseBody = response.body();
if (responseBody != null) {
String result = responseBody.string();
System.out.println("result:" + result);
return result;
}
return null;
}
参考链接
java.lang.IllegalStateException: closed when trying to access response in onResponse(Response response) :https://github.com/square/okhttp/issues/1240
结语
有经验的同学们都踩过这个坑,包括我。记录一下,留给刚学习 OkHttp 时踩坑的同学们。