mirror of
https://gitee.com/dromara/hutool.git
synced 2025-06-28 13:34:09 +08:00
707 lines
20 KiB
Java
Executable File
707 lines
20 KiB
Java
Executable File
package cn.hutool.http;
|
||
|
||
import cn.hutool.core.collection.CollUtil;
|
||
import cn.hutool.core.convert.Convert;
|
||
import cn.hutool.core.io.FastByteArrayOutputStream;
|
||
import cn.hutool.core.io.FileUtil;
|
||
import cn.hutool.core.io.IORuntimeException;
|
||
import cn.hutool.core.io.IoUtil;
|
||
import cn.hutool.core.io.StreamProgress;
|
||
import cn.hutool.core.io.resource.BytesResource;
|
||
import cn.hutool.core.lang.Assert;
|
||
import cn.hutool.core.util.ObjUtil;
|
||
import cn.hutool.core.util.ReUtil;
|
||
import cn.hutool.core.util.StrUtil;
|
||
import cn.hutool.core.util.URLUtil;
|
||
import cn.hutool.http.cookie.GlobalCookieManager;
|
||
|
||
import java.io.Closeable;
|
||
import java.io.EOFException;
|
||
import java.io.File;
|
||
import java.io.FileNotFoundException;
|
||
import java.io.IOException;
|
||
import java.io.InputStream;
|
||
import java.io.OutputStream;
|
||
import java.net.HttpCookie;
|
||
import java.nio.charset.Charset;
|
||
import java.util.List;
|
||
import java.util.Map.Entry;
|
||
|
||
/**
|
||
* Http响应类<br>
|
||
* 非线程安全对象
|
||
*
|
||
* @author Looly
|
||
*/
|
||
public class HttpResponse extends HttpBase<HttpResponse> implements Closeable {
|
||
|
||
/**
|
||
* Http配置
|
||
*/
|
||
protected HttpConfig config;
|
||
/**
|
||
* 持有连接对象
|
||
*/
|
||
protected HttpConnection httpConnection;
|
||
/**
|
||
* Http请求原始流
|
||
*/
|
||
protected InputStream in;
|
||
/**
|
||
* 是否异步,异步下只持有流,否则将在初始化时直接读取body内容
|
||
*/
|
||
private volatile boolean isAsync;
|
||
/**
|
||
* 响应状态码
|
||
*/
|
||
protected int status;
|
||
/**
|
||
* 是否忽略读取Http响应体
|
||
*/
|
||
private final boolean ignoreBody;
|
||
/**
|
||
* 从响应中获取的编码
|
||
*/
|
||
private Charset charsetFromResponse;
|
||
|
||
/**
|
||
* 构造
|
||
*
|
||
* @param httpConnection {@link HttpConnection}
|
||
* @param config Http配置
|
||
* @param charset 编码,从请求编码中获取默认编码
|
||
* @param isAsync 是否异步
|
||
* @param isIgnoreBody 是否忽略读取响应体
|
||
* @since 3.1.2
|
||
*/
|
||
protected HttpResponse(HttpConnection httpConnection, HttpConfig config, Charset charset, boolean isAsync, boolean isIgnoreBody) {
|
||
this.httpConnection = httpConnection;
|
||
this.config = config;
|
||
this.charset = charset;
|
||
this.isAsync = isAsync;
|
||
this.ignoreBody = isIgnoreBody;
|
||
initWithDisconnect();
|
||
}
|
||
|
||
/**
|
||
* 获取状态码
|
||
*
|
||
* @return 状态码
|
||
*/
|
||
public int getStatus() {
|
||
return this.status;
|
||
}
|
||
|
||
/**
|
||
* 请求是否成功,判断依据为:状态码范围在200~299内。
|
||
*
|
||
* @return 是否成功请求
|
||
* @since 4.1.9
|
||
*/
|
||
public boolean isOk() {
|
||
return this.status >= 200 && this.status < 300;
|
||
}
|
||
|
||
/**
|
||
* 同步<br>
|
||
* 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。<br>
|
||
* 当调用此方法时,异步状态转为同步状态,此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态,则不进行任何操作。
|
||
*
|
||
* @return this
|
||
*/
|
||
public HttpResponse sync() {
|
||
return this.isAsync ? forceSync() : this;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Http Response Header start
|
||
|
||
/**
|
||
* 获取内容编码
|
||
*
|
||
* @return String
|
||
*/
|
||
public String contentEncoding() {
|
||
return header(Header.CONTENT_ENCODING);
|
||
}
|
||
|
||
/**
|
||
* 获取内容长度,以下情况长度无效:
|
||
* <ul>
|
||
* <li>Transfer-Encoding: Chunked</li>
|
||
* <li>Content-Encoding: XXX</li>
|
||
* </ul>
|
||
* 参考:https://blog.csdn.net/jiang7701037/article/details/86304302
|
||
*
|
||
* @return 长度,-1表示服务端未返回或长度无效
|
||
* @since 5.7.9
|
||
*/
|
||
public long contentLength() {
|
||
long contentLength = Convert.toLong(header(Header.CONTENT_LENGTH), -1L);
|
||
if (contentLength > 0 && (isChunked() || StrUtil.isNotBlank(contentEncoding()))) {
|
||
//按照HTTP协议规范,在 Transfer-Encoding和Content-Encoding设置后 Content-Length 无效。
|
||
contentLength = -1;
|
||
}
|
||
return contentLength;
|
||
}
|
||
|
||
/**
|
||
* 是否为gzip压缩过的内容
|
||
*
|
||
* @return 是否为gzip压缩过的内容
|
||
*/
|
||
public boolean isGzip() {
|
||
final String contentEncoding = contentEncoding();
|
||
return "gzip".equalsIgnoreCase(contentEncoding);
|
||
}
|
||
|
||
/**
|
||
* 是否为zlib(Deflate)压缩过的内容
|
||
*
|
||
* @return 是否为zlib(Deflate)压缩过的内容
|
||
* @since 4.5.7
|
||
*/
|
||
public boolean isDeflate() {
|
||
final String contentEncoding = contentEncoding();
|
||
return "deflate".equalsIgnoreCase(contentEncoding);
|
||
}
|
||
|
||
/**
|
||
* 是否为Transfer-Encoding:Chunked的内容
|
||
*
|
||
* @return 是否为Transfer-Encoding:Chunked的内容
|
||
* @since 4.6.2
|
||
*/
|
||
public boolean isChunked() {
|
||
final String transferEncoding = header(Header.TRANSFER_ENCODING);
|
||
return "Chunked".equalsIgnoreCase(transferEncoding);
|
||
}
|
||
|
||
/**
|
||
* 获取本次请求服务器返回的Cookie信息
|
||
*
|
||
* @return Cookie字符串
|
||
* @since 3.1.1
|
||
*/
|
||
public String getCookieStr() {
|
||
return header(Header.SET_COOKIE);
|
||
}
|
||
|
||
/**
|
||
* 获取Cookie
|
||
*
|
||
* @return Cookie列表
|
||
* @since 3.1.1
|
||
*/
|
||
public List<HttpCookie> getCookies() {
|
||
return GlobalCookieManager.getCookies(this.httpConnection);
|
||
}
|
||
|
||
/**
|
||
* 获取Cookie
|
||
*
|
||
* @param name Cookie名
|
||
* @return {@link HttpCookie}
|
||
* @since 4.1.4
|
||
*/
|
||
public HttpCookie getCookie(String name) {
|
||
List<HttpCookie> cookie = getCookies();
|
||
if (null != cookie) {
|
||
for (HttpCookie httpCookie : cookie) {
|
||
if (httpCookie.getName().equals(name)) {
|
||
return httpCookie;
|
||
}
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 获取Cookie值
|
||
*
|
||
* @param name Cookie名
|
||
* @return Cookie值
|
||
* @since 4.1.4
|
||
*/
|
||
public String getCookieValue(String name) {
|
||
final HttpCookie cookie = getCookie(name);
|
||
return (null == cookie) ? null : cookie.getValue();
|
||
}
|
||
// ---------------------------------------------------------------- Http Response Header end
|
||
|
||
// ---------------------------------------------------------------- Body start
|
||
|
||
/**
|
||
* 获得服务区响应流<br>
|
||
* 异步模式下获取Http原生流,同步模式下获取获取到的在内存中的副本<br>
|
||
* 如果想在同步模式下获取流,请先调用{@link #sync()}方法强制同步<br>
|
||
* 流获取后处理完毕需关闭此类
|
||
*
|
||
* @return 响应流
|
||
*/
|
||
public InputStream bodyStream() {
|
||
if (isAsync) {
|
||
return this.in;
|
||
}
|
||
return null == this.body ? null : this.body.getStream();
|
||
}
|
||
|
||
/**
|
||
* 获取响应流字节码<br>
|
||
* 此方法会转为同步模式
|
||
*
|
||
* @return byte[]
|
||
*/
|
||
@Override
|
||
public byte[] bodyBytes() {
|
||
sync();
|
||
return super.bodyBytes();
|
||
}
|
||
|
||
/**
|
||
* 设置主体字节码<br>
|
||
* 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8
|
||
*
|
||
* @param bodyBytes 主体
|
||
* @return this
|
||
*/
|
||
public HttpResponse body(byte[] bodyBytes) {
|
||
sync();
|
||
if (null != bodyBytes) {
|
||
this.body = new BytesResource(bodyBytes);
|
||
}
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* 获取响应主体
|
||
*
|
||
* @return String
|
||
* @throws HttpException 包装IO异常
|
||
*/
|
||
public String body() throws HttpException {
|
||
return HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse);
|
||
}
|
||
|
||
/**
|
||
* 将响应内容写出到{@link OutputStream}<br>
|
||
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
|
||
* 写出后会关闭Http流(异步模式)
|
||
*
|
||
* @param out 写出的流
|
||
* @param isCloseOut 是否关闭输出流
|
||
* @param streamProgress 进度显示接口,通过实现此接口显示下载进度
|
||
* @return 写出bytes数
|
||
* @since 3.3.2
|
||
*/
|
||
public long writeBody(OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {
|
||
Assert.notNull(out, "[out] must be not null!");
|
||
final long contentLength = contentLength();
|
||
try {
|
||
return copyBody(bodyStream(), out, contentLength, streamProgress, this.config.ignoreEOFError);
|
||
} finally {
|
||
IoUtil.close(this);
|
||
if (isCloseOut) {
|
||
IoUtil.close(out);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将响应内容写出到文件<br>
|
||
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
|
||
* 写出后会关闭Http流(异步模式)
|
||
*
|
||
* @param targetFileOrDir 写出到的文件或目录
|
||
* @param streamProgress 进度显示接口,通过实现此接口显示下载进度
|
||
* @return 写出bytes数
|
||
* @since 3.3.2
|
||
*/
|
||
public long writeBody(File targetFileOrDir, StreamProgress streamProgress) {
|
||
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
|
||
|
||
final File outFile = completeFileNameFromHeader(targetFileOrDir);
|
||
return writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
|
||
}
|
||
|
||
/**
|
||
* 将响应内容写出到文件-避免未完成的文件<br>
|
||
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
|
||
* 写出后会关闭Http流(异步模式)<br>
|
||
* 来自:https://gitee.com/chinabugotech/hutool/pulls/407<br>
|
||
* 此方法原理是先在目标文件同级目录下创建临时文件,下载之,等下载完毕后重命名,避免因下载错误导致的文件不完整。
|
||
*
|
||
* @param targetFileOrDir 写出到的文件或目录
|
||
* @param tempFileSuffix 临时文件后缀,默认".temp"
|
||
* @param streamProgress 进度显示接口,通过实现此接口显示下载进度
|
||
* @return 写出bytes数
|
||
* @since 5.7.12
|
||
*/
|
||
public long writeBody(File targetFileOrDir, String tempFileSuffix, StreamProgress streamProgress) {
|
||
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
|
||
|
||
File outFile = completeFileNameFromHeader(targetFileOrDir);
|
||
|
||
if (StrUtil.isBlank(tempFileSuffix)) {
|
||
tempFileSuffix = ".temp";
|
||
} else {
|
||
tempFileSuffix = StrUtil.addPrefixIfNot(tempFileSuffix, StrUtil.DOT);
|
||
}
|
||
|
||
// 目标文件真实名称
|
||
final String fileName = outFile.getName();
|
||
// 临时文件名称
|
||
final String tempFileName = fileName + tempFileSuffix;
|
||
|
||
// 临时文件
|
||
outFile = new File(outFile.getParentFile(), tempFileName);
|
||
|
||
long length;
|
||
try {
|
||
length = writeBody(outFile, streamProgress);
|
||
// 重命名下载好的临时文件
|
||
FileUtil.rename(outFile, fileName, true);
|
||
} catch (Throwable e) {
|
||
// 异常则删除临时文件
|
||
FileUtil.del(outFile);
|
||
throw new HttpException(e);
|
||
}
|
||
return length;
|
||
}
|
||
|
||
/**
|
||
* 将响应内容写出到文件<br>
|
||
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
|
||
* 写出后会关闭Http流(异步模式)
|
||
*
|
||
* @param targetFileOrDir 写出到的文件
|
||
* @param streamProgress 进度显示接口,通过实现此接口显示下载进度
|
||
* @return 写出的文件
|
||
* @since 5.6.4
|
||
*/
|
||
public File writeBodyForFile(File targetFileOrDir, StreamProgress streamProgress) {
|
||
Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
|
||
|
||
final File outFile = completeFileNameFromHeader(targetFileOrDir);
|
||
writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
|
||
|
||
return outFile;
|
||
}
|
||
|
||
/**
|
||
* 将响应内容写出到文件<br>
|
||
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
|
||
* 写出后会关闭Http流(异步模式)
|
||
*
|
||
* @param targetFileOrDir 写出到的文件或目录
|
||
* @return 写出bytes数
|
||
* @since 3.3.2
|
||
*/
|
||
public long writeBody(File targetFileOrDir) {
|
||
return writeBody(targetFileOrDir, null);
|
||
}
|
||
|
||
/**
|
||
* 将响应内容写出到文件<br>
|
||
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
|
||
* 写出后会关闭Http流(异步模式)
|
||
*
|
||
* @param targetFileOrDir 写出到的文件或目录的路径
|
||
* @return 写出bytes数
|
||
* @since 3.3.2
|
||
*/
|
||
public long writeBody(String targetFileOrDir) {
|
||
return writeBody(FileUtil.file(targetFileOrDir));
|
||
}
|
||
// ---------------------------------------------------------------- Body end
|
||
|
||
@Override
|
||
public void close() {
|
||
IoUtil.close(this.in);
|
||
this.in = null;
|
||
// 关闭连接
|
||
this.httpConnection.disconnectQuietly();
|
||
}
|
||
|
||
@Override
|
||
public String toString() {
|
||
StringBuilder sb = StrUtil.builder();
|
||
sb.append("Response Headers: ").append(StrUtil.CRLF);
|
||
for (Entry<String, List<String>> entry : this.headers.entrySet()) {
|
||
sb.append(" ").append(entry).append(StrUtil.CRLF);
|
||
}
|
||
|
||
sb.append("Response Body: ").append(StrUtil.CRLF);
|
||
sb.append(" ").append(this.body()).append(StrUtil.CRLF);
|
||
|
||
return sb.toString();
|
||
}
|
||
|
||
/**
|
||
* 从响应头补全下载文件名
|
||
*
|
||
* @param targetFileOrDir 目标文件夹或者目标文件
|
||
* @return File 保存的文件
|
||
* @since 5.4.1
|
||
*/
|
||
public File completeFileNameFromHeader(File targetFileOrDir) {
|
||
if (false == targetFileOrDir.isDirectory()) {
|
||
// 非目录直接返回
|
||
return targetFileOrDir;
|
||
}
|
||
|
||
// 从头信息中获取文件名
|
||
String fileName = getFileNameFromDisposition(null);
|
||
if (StrUtil.isBlank(fileName)) {
|
||
final String path = httpConnection.getUrl().getPath();
|
||
// 从路径中获取文件名
|
||
fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
|
||
if (StrUtil.isBlank(fileName)) {
|
||
// 编码后的路径做为文件名
|
||
fileName = URLUtil.encodeQuery(path, charset);
|
||
} else {
|
||
// issue#I4K0FS@Gitee
|
||
fileName = URLUtil.decode(fileName, charset);
|
||
}
|
||
}
|
||
return FileUtil.file(targetFileOrDir, fileName);
|
||
}
|
||
|
||
/**
|
||
* 从Content-Disposition头中获取文件名
|
||
*
|
||
* @return 文件名,empty表示无
|
||
*/
|
||
public String getFileNameFromDisposition() {
|
||
return getFileNameFromDisposition(null);
|
||
}
|
||
|
||
/**
|
||
* 从Content-Disposition头中获取文件名,以参数名为`filename`为例,规则为:
|
||
* <ul>
|
||
* <li>首先按照RFC5987规范检查`filename*`参数对应的值,即:`filename*="example.txt"`,则获取`example.txt`</li>
|
||
* <li>如果找不到`filename*`参数,则检查`filename`参数对应的值,即:`filename="example.txt"`,则获取`example.txt`</li>
|
||
* </ul>
|
||
* 按照规范,`Content-Disposition`可能返回多个,此处遍历所有返回头,并且`filename*`始终优先获取,即使`filename`存在并更靠前。<br>
|
||
* 参考:https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Disposition
|
||
*
|
||
* @param paramName 文件参数名,如果为{@code null}则使用默认的`filename`
|
||
* @return 文件名,empty表示无
|
||
*/
|
||
public String getFileNameFromDisposition(String paramName) {
|
||
paramName = ObjUtil.defaultIfNull(paramName, "filename");
|
||
final List<String> dispositions = headerList(Header.CONTENT_DISPOSITION.getValue());
|
||
String fileName = null;
|
||
if (CollUtil.isNotEmpty(dispositions)) {
|
||
|
||
// filename* 采用了 RFC 5987 中规定的编码方式,优先读取
|
||
fileName = getFileNameFromDispositions(dispositions, StrUtil.addSuffixIfNot(paramName, "*"));
|
||
if ((!StrUtil.endWith(fileName, "*")) && StrUtil.isBlank(fileName)) {
|
||
fileName = getFileNameFromDispositions(dispositions, paramName);
|
||
}
|
||
}
|
||
|
||
return fileName;
|
||
}
|
||
|
||
// ---------------------------------------------------------------- Private method start
|
||
/**
|
||
* 从Content-Disposition头中获取文件名
|
||
*
|
||
* @param dispositions Content-Disposition头列表
|
||
* @param paramName 文件参数名
|
||
* @return 文件名,empty表示无
|
||
*/
|
||
private static String getFileNameFromDispositions(final List<String> dispositions, String paramName) {
|
||
// 正则转义
|
||
paramName = StrUtil.replace(paramName, "*", "\\*");
|
||
String fileName = null;
|
||
for (final String disposition : dispositions) {
|
||
fileName = ReUtil.getGroup1(paramName + "=([^;]+)", disposition);
|
||
if (StrUtil.isNotBlank(fileName)) {
|
||
break;
|
||
}
|
||
}
|
||
return getRfc5987Value(fileName);
|
||
}
|
||
|
||
/**
|
||
* 获取rfc5987标准的值,标准见:https://www.rfc-editor.org/rfc/rfc5987#section-3.2.1<br>
|
||
* 包括:
|
||
*
|
||
*<ul>
|
||
* <li>Non-extended:无双引号包裹的值</li>
|
||
* <li>Non-extended:双引号包裹的值</li>
|
||
* <li>Extended notation:编码'语言'值</li>
|
||
*</ul>
|
||
*
|
||
* @param value 值
|
||
* @return 结果值
|
||
*/
|
||
private static String getRfc5987Value(final String value){
|
||
final List<String> split = StrUtil.split(value, '\'');
|
||
if(3 == split.size()){
|
||
return split.get(2);
|
||
}
|
||
|
||
// 普通值
|
||
return StrUtil.unWrap(value, '"');
|
||
}
|
||
|
||
/**
|
||
* 初始化Http响应,并在报错时关闭连接。<br>
|
||
* 初始化包括:
|
||
*
|
||
* <pre>
|
||
* 1、读取Http状态
|
||
* 2、读取头信息
|
||
* 3、持有Http流,并不关闭流
|
||
* </pre>
|
||
*
|
||
* @return this
|
||
* @throws HttpException IO异常
|
||
*/
|
||
private HttpResponse initWithDisconnect() throws HttpException {
|
||
try {
|
||
init();
|
||
} catch (HttpException e) {
|
||
this.httpConnection.disconnectQuietly();
|
||
throw e;
|
||
}
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* 初始化Http响应<br>
|
||
* 初始化包括:
|
||
*
|
||
* <pre>
|
||
* 1、读取Http状态
|
||
* 2、读取头信息
|
||
* 3、持有Http流,并不关闭流
|
||
* </pre>
|
||
*
|
||
* @return this
|
||
* @throws HttpException IO异常
|
||
*/
|
||
private HttpResponse init() throws HttpException {
|
||
// 获取响应状态码
|
||
try {
|
||
this.status = httpConnection.responseCode();
|
||
} catch (IOException e) {
|
||
if (false == (e instanceof FileNotFoundException)) {
|
||
throw new HttpException(e);
|
||
}
|
||
// 服务器无返回内容,忽略之
|
||
}
|
||
|
||
|
||
// 读取响应头信息
|
||
try {
|
||
this.headers = httpConnection.headers();
|
||
} catch (IllegalArgumentException e) {
|
||
// ignore
|
||
// StaticLog.warn(e, e.getMessage());
|
||
}
|
||
|
||
// 存储服务端设置的Cookie信息
|
||
GlobalCookieManager.store(httpConnection);
|
||
|
||
// 获取响应编码
|
||
final Charset charset = httpConnection.getCharset();
|
||
this.charsetFromResponse = charset;
|
||
if (null != charset) {
|
||
this.charset = charset;
|
||
}
|
||
|
||
// 获取响应内容流
|
||
this.in = new HttpInputStream(this);
|
||
|
||
// 同步情况下强制同步
|
||
return this.isAsync ? this : forceSync();
|
||
}
|
||
|
||
/**
|
||
* 强制同步,用于初始化<br>
|
||
* 强制同步后变化如下:
|
||
*
|
||
* <pre>
|
||
* 1、读取body内容到内存
|
||
* 2、异步状态设为false(变为同步状态)
|
||
* 3、关闭Http流
|
||
* 4、断开与服务器连接
|
||
* </pre>
|
||
*
|
||
* @return this
|
||
*/
|
||
private HttpResponse forceSync() {
|
||
// 非同步状态转为同步状态
|
||
try {
|
||
this.readBody(this.in);
|
||
} catch (IORuntimeException e) {
|
||
//noinspection StatementWithEmptyBody
|
||
if (e.getCause() instanceof FileNotFoundException) {
|
||
// 服务器无返回内容,忽略之
|
||
} else {
|
||
throw new HttpException(e);
|
||
}
|
||
} finally {
|
||
if (this.isAsync) {
|
||
this.isAsync = false;
|
||
}
|
||
this.close();
|
||
}
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* 读取主体,忽略EOFException异常
|
||
*
|
||
* @param in 输入流
|
||
* @throws IORuntimeException IO异常
|
||
*/
|
||
private void readBody(InputStream in) throws IORuntimeException {
|
||
if (ignoreBody) {
|
||
return;
|
||
}
|
||
|
||
// issue#ICB1B8,如果用户定义忽略contentLength头,则不读取
|
||
final long contentLength = config.ignoreContentLength ? -1 : contentLength();
|
||
final FastByteArrayOutputStream out = new FastByteArrayOutputStream((int) contentLength);
|
||
copyBody(in, out, contentLength, null, this.config.ignoreEOFError);
|
||
this.body = new BytesResource(out.toByteArray());
|
||
}
|
||
|
||
/**
|
||
* 将响应内容写出到{@link OutputStream}<br>
|
||
* 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出<br>
|
||
* 写出后会关闭Http流(异步模式)
|
||
*
|
||
* @param in 输入流
|
||
* @param out 写出的流
|
||
* @param contentLength 总长度,-1表示未知
|
||
* @param streamProgress 进度显示接口,通过实现此接口显示下载进度
|
||
* @param isIgnoreEOFError 是否忽略响应读取时可能的EOF异常
|
||
* @return 拷贝长度
|
||
*/
|
||
private static long copyBody(InputStream in, OutputStream out, long contentLength, StreamProgress streamProgress, boolean isIgnoreEOFError) {
|
||
if (null == out) {
|
||
throw new NullPointerException("[out] is null!");
|
||
}
|
||
|
||
long copyLength = -1;
|
||
try {
|
||
copyLength = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE, contentLength, streamProgress);
|
||
} catch (IORuntimeException e) {
|
||
//noinspection StatementWithEmptyBody
|
||
if (isIgnoreEOFError
|
||
&& (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF"))) {
|
||
// 忽略读取HTTP流中的EOF错误
|
||
} else {
|
||
throw e;
|
||
}
|
||
}
|
||
return copyLength;
|
||
}
|
||
// ---------------------------------------------------------------- Private method end
|
||
}
|