paramMap, final int timeout) {
- return HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().body();
+ return HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().bodyStr();
}
/**
@@ -235,7 +235,7 @@ public class HttpUtil {
*/
@SuppressWarnings("resource")
public static String post(final String urlString, final String body, final int timeout) {
- return HttpRequest.post(urlString).timeout(timeout).body(body).execute().body();
+ return HttpRequest.post(urlString).timeout(timeout).body(body).execute().bodyStr();
}
// ---------------------------------------------------------------------------------------- download
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/ClientConfig.java b/hutool-http/src/main/java/cn/hutool/http/client/ClientConfig.java
new file mode 100755
index 000000000..dbd94c587
--- /dev/null
+++ b/hutool-http/src/main/java/cn/hutool/http/client/ClientConfig.java
@@ -0,0 +1,241 @@
+package cn.hutool.http.client;
+
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.net.ssl.SSLUtil;
+import cn.hutool.http.HttpGlobalConfig;
+import cn.hutool.http.ssl.DefaultSSLInfo;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/**
+ * Http客户端配置
+ *
+ * @author looly
+ */
+public class ClientConfig {
+
+ /**
+ * 创建新的 ClientConfig
+ *
+ * @return ClientConfig
+ */
+ public static ClientConfig of() {
+ return new ClientConfig();
+ }
+
+ /**
+ * 默认连接超时
+ */
+ private int connectionTimeout;
+ /**
+ * 默认读取超时
+ */
+ private int readTimeout;
+
+ /**
+ * HostnameVerifier,用于HTTPS安全连接
+ */
+ private HostnameVerifier hostnameVerifier;
+ /**
+ * SSLSocketFactory,用于HTTPS安全连接
+ */
+ private SSLSocketFactory socketFactory;
+ /**
+ * 是否禁用缓存
+ */
+ public boolean disableCache;
+ /**
+ * 代理
+ */
+ public Proxy proxy;
+
+ /**
+ * 构造
+ */
+ public ClientConfig() {
+ connectionTimeout = HttpGlobalConfig.getTimeout();
+ readTimeout = HttpGlobalConfig.getTimeout();
+ hostnameVerifier = DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER;
+ socketFactory = DefaultSSLInfo.DEFAULT_SSF;
+ }
+
+ /**
+ * 设置超时,单位:毫秒
+ * 超时包括:
+ *
+ *
+ * 1. 连接超时
+ * 2. 读取响应超时
+ *
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @see #setConnectionTimeout(int)
+ * @see #setReadTimeout(int)
+ */
+ public ClientConfig setTimeout(final int milliseconds) {
+ setConnectionTimeout(milliseconds);
+ setReadTimeout(milliseconds);
+ return this;
+ }
+
+ /**
+ * 获取连接超时,单位:毫秒
+ *
+ * @return 连接超时,单位:毫秒
+ */
+ public int getConnectionTimeout() {
+ return connectionTimeout;
+ }
+
+ /**
+ * 设置连接超时,单位:毫秒
+ *
+ * @param connectionTimeout 超时毫秒数
+ * @return this
+ */
+ public ClientConfig setConnectionTimeout(final int connectionTimeout) {
+ this.connectionTimeout = connectionTimeout;
+ return this;
+ }
+
+ /**
+ * 获取读取超时,单位:毫秒
+ *
+ * @return 读取超时,单位:毫秒
+ */
+ public int getReadTimeout() {
+ return readTimeout;
+ }
+
+ /**
+ * 设置读取超时,单位:毫秒
+ *
+ * @param readTimeout 读取超时,单位:毫秒
+ * @return this
+ */
+ public ClientConfig setReadTimeout(final int readTimeout) {
+ this.readTimeout = readTimeout;
+ return this;
+ }
+
+ /**
+ * 获取域名验证器
+ *
+ * @return 域名验证器
+ */
+ public HostnameVerifier getHostnameVerifier() {
+ return hostnameVerifier;
+ }
+
+ /**
+ * 设置域名验证器
+ * 只针对HTTPS请求,如果不设置,不做验证,所有域名被信任
+ *
+ * @param hostnameVerifier HostnameVerifier
+ * @return this
+ */
+ public ClientConfig setHostnameVerifier(final HostnameVerifier hostnameVerifier) {
+ // 验证域
+ this.hostnameVerifier = hostnameVerifier;
+ return this;
+ }
+
+ /**
+ * 获取SSLSocketFactory
+ *
+ * @return SSLSocketFactory
+ */
+ public SSLSocketFactory getSocketFactory() {
+ return socketFactory;
+ }
+
+ /**
+ * 设置SSLSocketFactory
+ * 只针对HTTPS请求,如果不设置,使用默认的SSLSocketFactory
+ * 默认SSLSocketFactory为:SSLSocketFactoryBuilder.create().build();
+ *
+ * @param ssf SSLScketFactory
+ * @return this
+ */
+ public ClientConfig setSocketFactory(final SSLSocketFactory ssf) {
+ this.socketFactory = ssf;
+ return this;
+ }
+
+ /**
+ * 设置HTTPS安全连接协议,只针对HTTPS请求,可以使用的协议包括:
+ * 此方法调用后{@link #setSocketFactory(SSLSocketFactory)} 将被覆盖。
+ *
+ *
+ * 1. TLSv1.2
+ * 2. TLSv1.1
+ * 3. SSLv3
+ * ...
+ *
+ *
+ * @param protocol 协议
+ * @return this
+ * @see SSLUtil#createSSLContext(String)
+ * @see #setSocketFactory(SSLSocketFactory)
+ */
+ public ClientConfig setSSLProtocol(final String protocol) {
+ Assert.notBlank(protocol, "protocol must be not blank!");
+ setSocketFactory(SSLUtil.createSSLContext(protocol).getSocketFactory());
+ return this;
+ }
+
+ /**
+ * 是否禁用缓存
+ *
+ * @return 是否禁用缓存
+ */
+ public boolean isDisableCache() {
+ return disableCache;
+ }
+
+ /**
+ * 设置是否禁用缓存
+ *
+ * @param disableCache 是否禁用缓存
+ */
+ public void setDisableCache(final boolean disableCache) {
+ this.disableCache = disableCache;
+ }
+
+ /**
+ * 获取代理
+ *
+ * @return 代理
+ */
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ /**
+ * 设置Http代理
+ *
+ * @param host 代理 主机
+ * @param port 代理 端口
+ * @return this
+ */
+ public ClientConfig setHttpProxy(final String host, final int port) {
+ final Proxy proxy = new Proxy(Proxy.Type.HTTP,
+ new InetSocketAddress(host, port));
+ return setProxy(proxy);
+ }
+
+ /**
+ * 设置代理
+ *
+ * @param proxy 代理 {@link Proxy}
+ * @return this
+ */
+ public ClientConfig setProxy(final Proxy proxy) {
+ this.proxy = proxy;
+ return this;
+ }
+}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/Headers.java b/hutool-http/src/main/java/cn/hutool/http/client/HeaderOperation.java
similarity index 89%
rename from hutool-http/src/main/java/cn/hutool/http/client/Headers.java
rename to hutool-http/src/main/java/cn/hutool/http/client/HeaderOperation.java
index cdc84b9d5..5bd7682ec 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/Headers.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/HeaderOperation.java
@@ -1,6 +1,7 @@
package cn.hutool.http.client;
import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.http.meta.Header;
@@ -11,12 +12,12 @@ import java.util.List;
import java.util.Map;
/**
- * HTTP请求头的存储和相关方法
+ * HTTP请求头的存储和读取相关方法
*
* @param 返回对象类型,方便链式编程
*/
@SuppressWarnings("unchecked")
-public interface Headers> {
+public interface HeaderOperation> {
// region ----------------------------------------------------------- headers
@@ -100,6 +101,27 @@ public interface Headers> {
return header(name, value, true);
}
+ /**
+ * 设置请求头
+ * 不覆盖原有请求头
+ *
+ * @param headerMap 请求头
+ * @param isOverride 是否覆盖
+ * @return this
+ */
+ default T header(final Map> headerMap, final boolean isOverride) {
+ if (MapUtil.isNotEmpty(headerMap)) {
+ String name;
+ for (final Map.Entry> entry : headerMap.entrySet()) {
+ name = entry.getKey();
+ for (final String value : entry.getValue()) {
+ this.header(name, StrUtil.emptyIfNull(value), isOverride);
+ }
+ }
+ }
+ return (T) this;
+ }
+
/**
* 设置contentType
*
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/Request.java b/hutool-http/src/main/java/cn/hutool/http/client/Request.java
index b1532586d..4be1df12a 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/Request.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/Request.java
@@ -4,10 +4,11 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.UrlBuilder;
+import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.HttpGlobalConfig;
-import cn.hutool.http.client.body.RequestBody;
+import cn.hutool.http.client.body.HttpBody;
import cn.hutool.http.meta.Header;
import cn.hutool.http.meta.Method;
@@ -23,7 +24,7 @@ import java.util.Map;
* @author looly
* @since 6.0.0
*/
-public class Request implements Headers {
+public class Request implements HeaderOperation {
/**
* 构建一个HTTP请求
@@ -70,7 +71,7 @@ public class Request implements Headers {
/**
* 请求方法
*/
- private Method method = Method.GET;
+ private Method method;
/**
* 请求的URL
*/
@@ -78,8 +79,24 @@ public class Request implements Headers {
/**
* 存储头信息
*/
- private final Map> headers = new HashMap<>();
- private RequestBody body;
+ private final Map> headers;
+ /**
+ * 请求体
+ */
+ private HttpBody body;
+ /**
+ * 最大重定向次数
+ */
+ private int maxRedirectCount;
+
+ /**
+ * 默认构造
+ */
+ public Request() {
+ method = Method.GET;
+ headers = new HashMap<>();
+ maxRedirectCount = HttpGlobalConfig.getMaxRedirectCount();
+ }
/**
* 获取Http请求方法
@@ -195,7 +212,7 @@ public class Request implements Headers {
*
* @return this
*/
- public RequestBody body() {
+ public HttpBody body() {
return this.body;
}
@@ -205,8 +222,35 @@ public class Request implements Headers {
* @param body 请求体,可以是文本、表单、流、byte[] 或 Multipart
* @return this
*/
- public Request body(final RequestBody body) {
+ public Request body(final HttpBody body) {
this.body = body;
+
+ // 根据内容赋值默认Content-Type
+ if (StrUtil.isBlank(header(Header.CONTENT_TYPE))) {
+ header(Header.CONTENT_TYPE, body.getContentType(), true);
+ }
+
+ return this;
+ }
+
+ /**
+ * 获取最大重定向请求次数
+ *
+ * @return 最大重定向请求次数
+ */
+ public int maxRedirectCount() {
+ return maxRedirectCount;
+ }
+
+ /**
+ * 设置最大重定向次数
+ * 如果次数小于1则表示不重定向,大于等于1表示打开重定向
+ *
+ * @param maxRedirectCount 最大重定向次数
+ * @return this
+ */
+ public Request setMaxRedirectCount(final int maxRedirectCount) {
+ this.maxRedirectCount = Math.max(maxRedirectCount, 0);
return this;
}
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/Response.java b/hutool-http/src/main/java/cn/hutool/http/client/Response.java
index 98070a750..b6e74878d 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/Response.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/Response.java
@@ -1,10 +1,12 @@
package cn.hutool.http.client;
import cn.hutool.core.convert.Convert;
+import cn.hutool.core.io.IoUtil;
import cn.hutool.core.text.StrUtil;
-import cn.hutool.http.meta.Header;
import cn.hutool.http.HttpException;
import cn.hutool.http.HttpUtil;
+import cn.hutool.http.client.body.ResponseBody;
+import cn.hutool.http.meta.Header;
import java.io.Closeable;
import java.io.InputStream;
@@ -49,14 +51,32 @@ public interface Response extends Closeable {
*/
InputStream bodyStream();
+ /**
+ * 获取响应体,包含服务端返回的内容和Content-Type信息
+ * @return {@link ResponseBody}
+ */
+ default ResponseBody body(){
+ return new ResponseBody(this, true);
+ }
+
/**
* 获取响应主体
*
* @return String
* @throws HttpException 包装IO异常
*/
- default String body() throws HttpException {
- return HttpUtil.getString(bodyStream(), charset(), true);
+ default String bodyStr() throws HttpException {
+ return HttpUtil.getString(bodyBytes(), charset(), true);
+ }
+
+ /**
+ * 获取响应流字节码
+ * 此方法会转为同步模式
+ *
+ * @return byte[]
+ */
+ default byte[] bodyBytes() {
+ return IoUtil.readBytes(bodyStream());
}
/**
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/FormBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/FormBody.java
index 3fd6a6089..777608ea1 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/body/FormBody.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/body/FormBody.java
@@ -22,7 +22,7 @@ import java.util.Map;
* @author looly
*/
@SuppressWarnings("unchecked")
-public abstract class FormBody> implements RequestBody {
+public abstract class FormBody> implements HttpBody {
/**
* 存储表单数据
*/
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/RequestBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/HttpBody.java
similarity index 86%
rename from hutool-http/src/main/java/cn/hutool/http/client/body/RequestBody.java
rename to hutool-http/src/main/java/cn/hutool/http/client/body/HttpBody.java
index 8aef7b60a..09ff0ca1c 100644
--- a/hutool-http/src/main/java/cn/hutool/http/client/body/RequestBody.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/body/HttpBody.java
@@ -9,7 +9,7 @@ import java.io.OutputStream;
/**
* 定义请求体接口
*/
-public interface RequestBody {
+public interface HttpBody {
/**
* 写出数据,不关闭流
@@ -18,6 +18,13 @@ public interface RequestBody {
*/
void write(OutputStream out);
+ /**
+ * 获取Content-Type
+ *
+ * @return Content-Type值
+ */
+ String getContentType();
+
/**
* 写出并关闭{@link OutputStream}
*
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/MultipartBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/MultipartBody.java
index f3fa4e8fb..657a4bf2a 100644
--- a/hutool-http/src/main/java/cn/hutool/http/client/body/MultipartBody.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/body/MultipartBody.java
@@ -59,6 +59,7 @@ public class MultipartBody extends FormBody {
public MultipartBody(final Map form, final Charset charset, final String boundary) {
super(form, charset);
this.boundary = boundary;
+
}
/**
@@ -66,6 +67,7 @@ public class MultipartBody extends FormBody {
*
* @return Multipart的Content-Type类型
*/
+ @Override
public String getContentType() {
return CONTENT_TYPE_MULTIPART_PREFIX + boundary;
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/ResourceBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/ResourceBody.java
index fe98d28d9..c8528c3c2 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/body/ResourceBody.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/body/ResourceBody.java
@@ -1,5 +1,6 @@
package cn.hutool.http.client.body;
+import cn.hutool.core.io.resource.HttpResource;
import cn.hutool.core.io.resource.Resource;
import java.io.InputStream;
@@ -11,9 +12,9 @@ import java.io.OutputStream;
* @author looly
* @since 6.0.0
*/
-public class ResourceBody implements RequestBody {
+public class ResourceBody implements HttpBody {
- private final Resource resource;
+ private final HttpResource resource;
/**
* 创建 Http request body
@@ -21,7 +22,7 @@ public class ResourceBody implements RequestBody {
* @param resource body内容
* @return BytesBody
*/
- public static ResourceBody of(final Resource resource) {
+ public static ResourceBody of(final HttpResource resource) {
return new ResourceBody(resource);
}
@@ -30,7 +31,7 @@ public class ResourceBody implements RequestBody {
*
* @param resource Body内容
*/
- public ResourceBody(final Resource resource) {
+ public ResourceBody(final HttpResource resource) {
this.resource = resource;
}
@@ -52,4 +53,9 @@ public class ResourceBody implements RequestBody {
public InputStream getStream() {
return resource.getStream();
}
+
+ @Override
+ public String getContentType() {
+ return this.resource.getContentType();
+ }
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/ResponseBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/ResponseBody.java
new file mode 100755
index 000000000..3a2510771
--- /dev/null
+++ b/hutool-http/src/main/java/cn/hutool/http/client/body/ResponseBody.java
@@ -0,0 +1,243 @@
+package cn.hutool.http.client.body;
+
+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.file.FileNameUtil;
+import cn.hutool.core.lang.Assert;
+import cn.hutool.core.regex.ReUtil;
+import cn.hutool.core.text.StrUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.http.HttpException;
+import cn.hutool.http.client.Response;
+import cn.hutool.http.meta.Header;
+
+import java.io.EOFException;
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * 响应体部分封装
+ *
+ * @author looly
+ */
+public class ResponseBody implements HttpBody {
+
+ private final Response response;
+ /**
+ * 是否忽略响应读取时可能的EOF异常。
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
+ */
+ private final boolean isIgnoreEOFError;
+
+ /**
+ * 构造
+ *
+ * @param response 响应体
+ * @param isIgnoreEOFError 是否忽略EOF错误
+ */
+ public ResponseBody(final Response response, final boolean isIgnoreEOFError) {
+ this.response = response;
+ this.isIgnoreEOFError = isIgnoreEOFError;
+ }
+
+ @Override
+ public String getContentType() {
+ return response.header(Header.CONTENT_TYPE);
+ }
+
+ @Override
+ public InputStream getStream() {
+ return response.bodyStream();
+ }
+
+ @Override
+ public void write(final OutputStream out) {
+ write(out, false, null);
+ }
+
+ /**
+ * 将响应内容写出到{@link OutputStream}
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param out 写出的流
+ * @param isCloseOut 是否关闭输出流
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出bytes数
+ * @since 3.3.2
+ */
+ public long write(final OutputStream out, final boolean isCloseOut, final StreamProgress streamProgress) {
+ Assert.notNull(out, "[out] must be not null!");
+ final long contentLength = response.contentLength();
+ try {
+ return copyBody(getStream(), out, contentLength, streamProgress, isIgnoreEOFError);
+ } finally {
+ if (isCloseOut) {
+ IoUtil.close(out);
+ }
+ }
+ }
+
+ /**
+ * 将响应内容写出到文件
+ *
+ * @param targetFileOrDir 写出到的文件或目录的路径
+ * @return 写出的文件
+ */
+ public File write(final String targetFileOrDir) {
+ return write(FileUtil.file(targetFileOrDir));
+ }
+
+ /**
+ * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param targetFileOrDir 写出到的文件或目录
+ * @return 写出的文件
+ */
+ public File write(final File targetFileOrDir) {
+ return write(targetFileOrDir, null);
+ }
+
+ /**
+ * 将响应内容写出到文件-避免未完成的文件
+ * 来自:https://gitee.com/dromara/hutool/pulls/407
+ * 此方法原理是先在目标文件同级目录下创建临时文件,下载之,等下载完毕后重命名,避免因下载错误导致的文件不完整。
+ *
+ * @param targetFileOrDir 写出到的文件或目录
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出的文件对象
+ */
+ public File write(final File targetFileOrDir, final StreamProgress streamProgress) {
+ return write(targetFileOrDir, null, streamProgress);
+ }
+
+ /**
+ * 将响应内容写出到文件-避免未完成的文件
+ * 来自:https://gitee.com/dromara/hutool/pulls/407
+ * 此方法原理是先在目标文件同级目录下创建临时文件,下载之,等下载完毕后重命名,避免因下载错误导致的文件不完整。
+ *
+ * @param targetFileOrDir 写出到的文件或目录
+ * @param tempFileSuffix 临时文件后缀,默认".temp"
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出的文件对象
+ * @since 5.7.12
+ */
+ public File write(final File targetFileOrDir, final String tempFileSuffix, final StreamProgress streamProgress) {
+ Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
+ File outFile = getTargetFile(targetFileOrDir, null);
+ // 目标文件真实名称
+ final String fileName = outFile.getName();
+
+ // 临时文件
+ outFile = new File(outFile.getParentFile(), FileNameUtil.addTempSuffix(fileName, tempFileSuffix));
+
+ try {
+ outFile = writeDirect(outFile, null, streamProgress);
+ // 重命名下载好的临时文件
+ return FileUtil.rename(outFile, fileName, true);
+ } catch (final Throwable e) {
+ // 异常则删除临时文件
+ FileUtil.del(outFile);
+ throw new HttpException(e);
+ }
+ }
+
+ /**
+ * 将响应内容直接写出到文件,目标为目录则从Content-Disposition中获取文件名
+ *
+ * @param targetFileOrDir 写出到的文件
+ * @param customParamName 自定义的Content-Disposition中文件名的参数名
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出的文件
+ */
+ public File writeDirect(final File targetFileOrDir, final String customParamName, final StreamProgress streamProgress) {
+ Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
+
+ final File outFile = getTargetFile(targetFileOrDir, customParamName);
+ write(FileUtil.getOutputStream(outFile), true, streamProgress);
+
+ return outFile;
+ }
+
+ // region ---------------------------------------------------------------------------- Private Methods
+
+ /**
+ * 从响应头补全下载文件名,返回补全名称后的文件
+ *
+ * @param targetFileOrDir 目标文件夹或者目标文件
+ * @param customParamName 自定义的参数名称,如果传入{@code null},默认使用"filename"
+ * @return File 保存的文件
+ * @since 5.4.1
+ */
+ private File getTargetFile(final File targetFileOrDir, final String customParamName) {
+ if (false == targetFileOrDir.isDirectory()) {
+ // 非目录直接返回
+ return targetFileOrDir;
+ }
+
+ // 从头信息中获取文件名
+ final String fileName = getFileNameFromDisposition(ObjUtil.defaultIfNull(customParamName, "filename"));
+ if (StrUtil.isBlank(fileName)) {
+ throw new HttpException("Can`t get file name from [Content-Disposition]!");
+ }
+ return FileUtil.file(targetFileOrDir, fileName);
+ }
+
+ /**
+ * 从Content-Disposition头中获取文件名
+ *
+ * @param paramName 文件名的参数名
+ * @return 文件名,empty表示无
+ * @since 5.8.10
+ */
+ private String getFileNameFromDisposition(final String paramName) {
+ String fileName = null;
+ final String disposition = response.header(Header.CONTENT_DISPOSITION);
+ if (StrUtil.isNotBlank(disposition)) {
+ fileName = ReUtil.get(paramName + "=\"(.*?)\"", disposition, 1);
+ if (StrUtil.isBlank(fileName)) {
+ fileName = StrUtil.subAfter(disposition, paramName + "=", true);
+ }
+ }
+ return fileName;
+ }
+
+ /**
+ * 将响应内容写出到{@link OutputStream}
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param in 输入流
+ * @param out 写出的流
+ * @param contentLength 总长度,-1表示未知
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @param isIgnoreEOFError 是否忽略响应读取时可能的EOF异常
+ * @return 拷贝长度
+ */
+ private static long copyBody(final InputStream in, final OutputStream out, final long contentLength, final StreamProgress streamProgress, final 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 (final IORuntimeException e) {
+ //noinspection StatementWithEmptyBody
+ if (isIgnoreEOFError
+ && (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF"))) {
+ // 忽略读取HTTP流中的EOF错误
+ } else {
+ throw e;
+ }
+ }
+ return copyLength;
+ }
+ // endregion ---------------------------------------------------------------------------- Private Methods
+}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/StringBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/StringBody.java
index 06dae7ff8..c83ab718e 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/body/StringBody.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/body/StringBody.java
@@ -1,5 +1,6 @@
package cn.hutool.http.client.body;
+import cn.hutool.core.io.resource.HttpResource;
import cn.hutool.core.io.resource.StringResource;
import cn.hutool.http.HttpUtil;
@@ -30,6 +31,6 @@ public class StringBody extends ResourceBody {
* @param charset 自定义编码
*/
public StringBody(final String body, final String contentType, final Charset charset) {
- super(new StringResource(body, contentType, charset));
+ super(new HttpResource(new StringResource(body, contentType, charset), contentType));
}
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/UrlEncodedFormBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/UrlEncodedFormBody.java
index 109769373..d18b7fd44 100644
--- a/hutool-http/src/main/java/cn/hutool/http/client/body/UrlEncodedFormBody.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/body/UrlEncodedFormBody.java
@@ -3,6 +3,7 @@ package cn.hutool.http.client.body;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.text.StrUtil;
+import cn.hutool.http.meta.ContentType;
import java.io.OutputStream;
import java.nio.charset.Charset;
@@ -42,4 +43,9 @@ public class UrlEncodedFormBody extends FormBody {
final byte[] bytes = StrUtil.bytes(UrlQuery.of(form, true).build(charset), charset);
IoUtil.write(out, false, bytes);
}
+
+ @Override
+ public String getContentType() {
+ return ContentType.FORM_URLENCODED.toString(charset);
+ }
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4BodyEntity.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4BodyEntity.java
index a2906ffc1..0d72fecf0 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4BodyEntity.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4BodyEntity.java
@@ -1,7 +1,7 @@
package cn.hutool.http.client.engine.httpclient4;
import cn.hutool.http.client.body.BytesBody;
-import cn.hutool.http.client.body.RequestBody;
+import cn.hutool.http.client.body.HttpBody;
import org.apache.http.entity.AbstractHttpEntity;
import java.io.InputStream;
@@ -9,14 +9,14 @@ import java.io.OutputStream;
import java.nio.charset.Charset;
/**
- * {@link RequestBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象
+ * {@link HttpBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象
*
* @author looly
* @since 6.0.0
*/
public class HttpClient4BodyEntity extends AbstractHttpEntity {
- private final RequestBody body;
+ private final HttpBody body;
/**
* 构造
@@ -24,9 +24,9 @@ public class HttpClient4BodyEntity extends AbstractHttpEntity {
* @param contentType Content-Type类型
* @param charset 自定义请求编码
* @param chunked 是否块模式传输
- * @param body {@link RequestBody}
+ * @param body {@link HttpBody}
*/
- public HttpClient4BodyEntity(final String contentType, final Charset charset, final boolean chunked, final RequestBody body) {
+ public HttpClient4BodyEntity(final String contentType, final Charset charset, final boolean chunked, final HttpBody body) {
super();
setContentType(contentType);
setContentEncoding(null == charset ? null : charset.name());
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Engine.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Engine.java
index 751337244..2d5d69d6c 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Engine.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Engine.java
@@ -7,7 +7,7 @@ import cn.hutool.http.HttpException;
import cn.hutool.http.client.ClientEngine;
import cn.hutool.http.client.Request;
import cn.hutool.http.client.Response;
-import cn.hutool.http.client.body.RequestBody;
+import cn.hutool.http.client.body.HttpBody;
import org.apache.http.Header;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
@@ -87,7 +87,7 @@ public class HttpClient4Engine implements ClientEngine {
request.setHeaders(toHeaderList(message.headers()).toArray(new Header[0]));
// 填充自定义消息体
- final RequestBody body = message.body();
+ final HttpBody body = message.body();
request.setEntity(new HttpClient4BodyEntity(
// 用户自定义的内容类型
message.header(cn.hutool.http.meta.Header.CONTENT_TYPE),
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Response.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Response.java
index d0f5b84b7..562ed9be6 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Response.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient4/HttpClient4Response.java
@@ -81,7 +81,7 @@ public class HttpClient4Response implements Response {
}
@Override
- public String body() throws HttpException {
+ public String bodyStr() throws HttpException {
try {
return EntityUtils.toString(rawRes.getEntity(), charset());
} catch (final IOException e) {
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5BodyEntity.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5BodyEntity.java
index bd8af189d..b2f3ce07b 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5BodyEntity.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5BodyEntity.java
@@ -1,7 +1,7 @@
package cn.hutool.http.client.engine.httpclient5;
import cn.hutool.http.client.body.BytesBody;
-import cn.hutool.http.client.body.RequestBody;
+import cn.hutool.http.client.body.HttpBody;
import org.apache.hc.core5.http.io.entity.AbstractHttpEntity;
import java.io.IOException;
@@ -10,14 +10,14 @@ import java.io.OutputStream;
import java.nio.charset.Charset;
/**
- * {@link RequestBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象
+ * {@link HttpBody}转换为{@link org.apache.hc.core5.http.HttpEntity}对象
*
* @author looly
* @since 6.0.0
*/
public class HttpClient5BodyEntity extends AbstractHttpEntity {
- private final RequestBody body;
+ private final HttpBody body;
/**
* 构造
@@ -25,9 +25,9 @@ public class HttpClient5BodyEntity extends AbstractHttpEntity {
* @param contentType Content-Type类型
* @param charset 自定义请求编码
* @param chunked 是否块模式传输
- * @param body {@link RequestBody}
+ * @param body {@link HttpBody}
*/
- public HttpClient5BodyEntity(final String contentType, final Charset charset, final boolean chunked, final RequestBody body) {
+ public HttpClient5BodyEntity(final String contentType, final Charset charset, final boolean chunked, final HttpBody body) {
super(contentType, null == charset ? null : charset.name(), chunked);
this.body = body;
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Engine.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Engine.java
index 4a33b164d..7214ff404 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Engine.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Engine.java
@@ -7,7 +7,7 @@ import cn.hutool.http.HttpException;
import cn.hutool.http.client.ClientEngine;
import cn.hutool.http.client.Request;
import cn.hutool.http.client.Response;
-import cn.hutool.http.client.body.RequestBody;
+import cn.hutool.http.client.body.HttpBody;
import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
@@ -83,7 +83,7 @@ public class HttpClient5Engine implements ClientEngine {
request.setHeaders(toHeaderList(message.headers()).toArray(new Header[0]));
// 填充自定义消息体
- final RequestBody body = message.body();
+ final HttpBody body = message.body();
request.setEntity(new HttpClient5BodyEntity(
// 用户自定义的内容类型
message.header(cn.hutool.http.meta.Header.CONTENT_TYPE),
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Response.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Response.java
index 4d1ee55f3..d9cffc62f 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Response.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/httpclient5/HttpClient5Response.java
@@ -81,7 +81,7 @@ public class HttpClient5Response implements Response {
}
@Override
- public String body() throws HttpException {
+ public String bodyStr() throws HttpException {
try {
return EntityUtils.toString(rawRes.getEntity(), charset());
} catch (final IOException e) {
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpBase.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpBase.java
index 91005f87f..f9ea28555 100644
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpBase.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpBase.java
@@ -6,7 +6,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.http.meta.Header;
-import cn.hutool.http.client.Headers;
+import cn.hutool.http.client.HeaderOperation;
import java.nio.charset.Charset;
import java.util.ArrayList;
@@ -23,7 +23,7 @@ import java.util.Map.Entry;
* @author Looly
*/
@SuppressWarnings("unchecked")
-public abstract class HttpBase> implements Headers {
+public abstract class HttpBase> implements HeaderOperation {
/**
* 默认的请求编码、URL的encode、decode编码
@@ -253,10 +253,19 @@ public abstract class HttpBase> implements Headers {
*
* @return 字符集
*/
- public String charset() {
+ public String charsetName() {
return charset.name();
}
+ /**
+ * 返回字符集
+ *
+ * @return 字符集
+ */
+ public Charset charset() {
+ return this.charset;
+ }
+
/**
* 设置字符集
*
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpConnection.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpConnection.java
index d69313ed5..956837980 100644
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpConnection.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpConnection.java
@@ -1,13 +1,12 @@
package cn.hutool.http.client.engine.jdk;
-import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.URLUtil;
import cn.hutool.core.reflect.FieldUtil;
import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.http.HttpException;
import cn.hutool.http.HttpUtil;
-import cn.hutool.http.meta.Header;
+import cn.hutool.http.client.HeaderOperation;
import cn.hutool.http.meta.Method;
import cn.hutool.http.ssl.DefaultSSLInfo;
@@ -21,23 +20,21 @@ import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URL;
-import java.net.URLConnection;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.List;
import java.util.Map;
-import java.util.Map.Entry;
/**
* http连接对象,对HttpURLConnection的包装
*
* @author Looly
*/
-public class HttpConnection {
+public class HttpConnection implements HeaderOperation {
private final URL url;
private final Proxy proxy;
- private HttpURLConnection conn;
+ private final HttpURLConnection conn;
/**
* 创建HttpConnection
@@ -61,7 +58,7 @@ public class HttpConnection {
return new HttpConnection(url, proxy);
}
- // --------------------------------------------------------------- Constructor start
+ // region --------------------------------------------------------------- Constructor
/**
* 构造HttpConnection
@@ -74,31 +71,14 @@ public class HttpConnection {
this.proxy = proxy;
// 初始化Http连接
- initConn();
- }
-
- // --------------------------------------------------------------- Constructor end
-
- /**
- * 初始化连接相关信息
- *
- * @return HttpConnection
- * @since 4.4.1
- */
- public HttpConnection initConn() {
- try {
- this.conn = openHttp();
- } catch (final IOException e) {
- throw new HttpException(e);
- }
-
+ this.conn = HttpUrlConnectionUtil.openHttp(url, proxy);
// 默认读取响应内容
this.conn.setDoInput(true);
-
- return this;
}
- // --------------------------------------------------------------- Getters And Setters start
+ // endregion --------------------------------------------------------------- Constructor
+
+ // region --------------------------------------------------------------- Getters And Setters
/**
* 获取请求方法,GET/POST
@@ -165,127 +145,15 @@ public class HttpConnection {
return conn;
}
- // --------------------------------------------------------------- Getters And Setters end
-
- // ---------------------------------------------------------------- Headers start
-
/**
- * 设置请求头
- * 当请求头存在时,覆盖之
- *
- * @param header 头名
- * @param value 头值
- * @param isOverride 是否覆盖旧值
- * @return HttpConnection
- */
- public HttpConnection header(final String header, final String value, final boolean isOverride) {
- if (null != this.conn) {
- if (isOverride) {
- this.conn.setRequestProperty(header, value);
- } else {
- this.conn.addRequestProperty(header, value);
- }
- }
-
- return this;
- }
-
- /**
- * 设置请求头
- * 当请求头存在时,覆盖之
- *
- * @param header 头名
- * @param value 头值
- * @param isOverride 是否覆盖旧值
- * @return HttpConnection
- */
- public HttpConnection header(final Header header, final String value, final boolean isOverride) {
- return header(header.toString(), value, isOverride);
- }
-
- /**
- * 设置请求头
- * 不覆盖原有请求头
- *
- * @param headerMap 请求头
- * @param isOverride 是否覆盖
- * @return this
- */
- public HttpConnection header(final Map> headerMap, final boolean isOverride) {
- if (MapUtil.isNotEmpty(headerMap)) {
- String name;
- for (final Entry> entry : headerMap.entrySet()) {
- name = entry.getKey();
- for (final String value : entry.getValue()) {
- this.header(name, StrUtil.emptyIfNull(value), isOverride);
- }
- }
- }
- return this;
- }
-
- /**
- * 获取Http请求头
- *
- * @param name Header名
- * @return Http请求头值
- */
- public String header(final String name) {
- return this.conn.getHeaderField(name);
- }
-
- /**
- * 获取Http请求头
- *
- * @param name Header名
- * @return Http请求头值
- */
- public String header(final Header name) {
- return header(name.toString());
- }
-
- /**
- * 获取所有Http请求头
- *
- * @return Http请求头Map
- */
- public Map> headers() {
- return this.conn.getHeaderFields();
- }
-
- // ---------------------------------------------------------------- Headers end
-
- /**
- * 设置https请求参数
- * 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
- *
- * @param hostnameVerifier 域名验证器,非https传入null
- * @param ssf SSLSocketFactory,非https传入null
- * @return this
- * @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装
- */
- public HttpConnection setHttpsInfo(final HostnameVerifier hostnameVerifier, final SSLSocketFactory ssf) throws HttpException {
- final HttpURLConnection conn = this.conn;
-
- if (conn instanceof HttpsURLConnection) {
- // Https请求
- final HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
- // 验证域
- httpsConn.setHostnameVerifier(ObjUtil.defaultIfNull(hostnameVerifier, DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER));
- httpsConn.setSSLSocketFactory(ObjUtil.defaultIfNull(ssf, DefaultSSLInfo.DEFAULT_SSF));
- }
-
- return this;
- }
-
- /**
- * 关闭缓存
+ * 是否禁用缓存
*
+ * @param isDisableCache 是否禁用缓存
* @return this
* @see HttpURLConnection#setUseCaches(boolean)
*/
- public HttpConnection disableCache() {
- this.conn.setUseCaches(false);
+ public HttpConnection setDisableCache(final boolean isDisableCache) {
+ this.conn.setUseCaches(!isDisableCache);
return this;
}
@@ -331,15 +199,25 @@ public class HttpConnection {
}
/**
- * 设置Cookie
+ * 设置https请求参数
+ * 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
*
- * @param cookie Cookie
+ * @param hostnameVerifier 域名验证器,非https传入null
+ * @param ssf SSLSocketFactory,非https传入null
* @return this
+ * @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装
*/
- public HttpConnection setCookie(final String cookie) {
- if (cookie != null) {
- header(Header.COOKIE, cookie, true);
+ public HttpConnection setHttpsInfo(final HostnameVerifier hostnameVerifier, final SSLSocketFactory ssf) throws HttpException {
+ final HttpURLConnection conn = this.conn;
+
+ if (conn instanceof HttpsURLConnection) {
+ // Https请求
+ final HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
+ // 验证域
+ httpsConn.setHostnameVerifier(ObjUtil.defaultIfNull(hostnameVerifier, DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER));
+ httpsConn.setSSLSocketFactory(ObjUtil.defaultIfNull(ssf, DefaultSSLInfo.DEFAULT_SSF));
}
+
return this;
}
@@ -368,6 +246,55 @@ public class HttpConnection {
return this;
}
+ // endregion --------------------------------------------------------------- Getters And Setters
+
+ // region ---------------------------------------------------------------- Headers
+
+ /**
+ * 设置请求头
+ * 当请求头存在时,覆盖之
+ *
+ * @param header 头名
+ * @param value 头值
+ * @param isOverride 是否覆盖旧值
+ * @return HttpConnection
+ */
+ @Override
+ public HttpConnection header(final String header, final String value, final boolean isOverride) {
+ if (null != this.conn) {
+ if (isOverride) {
+ this.conn.setRequestProperty(header, value);
+ } else {
+ this.conn.addRequestProperty(header, value);
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * 获取Http请求头
+ *
+ * @param name Header名
+ * @return Http请求头值
+ */
+ @Override
+ public String header(final String name) {
+ return this.conn.getHeaderField(name);
+ }
+
+ /**
+ * 获取所有Http请求头
+ *
+ * @return Http请求头Map
+ */
+ @Override
+ public Map> headers() {
+ return this.conn.getHeaderFields();
+ }
+
+ // endregion---------------------------------------------------------------- Headers
+
/**
* 连接
*
@@ -456,7 +383,7 @@ public class HttpConnection {
// 在sun.net.www.protocol.http.HttpURLConnection.getOutputStream0方法中,会把GET方法
// 修改为POST,而且无法调用setRequestMethod方法修改,因此此处使用反射强制修改字段属性值
// https://stackoverflow.com/questions/978061/http-get-with-request-body/983458
- if(method == Method.GET && method != getMethod()){
+ if (method == Method.GET && method != getMethod()) {
FieldUtil.setFieldValue(this.conn, "method", Method.GET.name());
}
@@ -469,7 +396,7 @@ public class HttpConnection {
* @return 响应码
* @throws IOException IO异常
*/
- public int responseCode() throws IOException {
+ public int getCode() throws IOException {
if (null != this.conn) {
return this.conn.getResponseCode();
}
@@ -521,32 +448,4 @@ public class HttpConnection {
return sb.toString();
}
- // --------------------------------------------------------------- Private Method start
-
- /**
- * 初始化http或https请求参数
- * 有些时候https请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
- *
- * @return {@link HttpURLConnection},https返回{@link HttpsURLConnection}
- */
- private HttpURLConnection openHttp() throws IOException {
- final URLConnection conn = openConnection();
- if (false == conn instanceof HttpURLConnection) {
- // 防止其它协议造成的转换异常
- throw new HttpException("'{}' of URL [{}] is not a http connection, make sure URL is format for http.", conn.getClass().getName(), this.url);
- }
-
- return (HttpURLConnection) conn;
- }
-
- /**
- * 建立连接
- *
- * @return {@link URLConnection}
- * @throws IOException IO异常
- */
- private URLConnection openConnection() throws IOException {
- return (null == this.proxy) ? url.openConnection() : url.openConnection(this.proxy);
- }
- // --------------------------------------------------------------- Private Method end
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpInterceptor.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpInterceptor.java
deleted file mode 100644
index fd3df8629..000000000
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpInterceptor.java
+++ /dev/null
@@ -1,56 +0,0 @@
-package cn.hutool.http.client.engine.jdk;
-
-import java.util.Iterator;
-import java.util.LinkedList;
-import java.util.List;
-
-/**
- * Http拦截器接口,通过实现此接口,完成请求发起前或结束后对请求的编辑工作
- *
- * @param 过滤参数类型,HttpRequest或者HttpResponse
- * @author looly
- * @since 5.7.16
- */
-@FunctionalInterface
-public interface HttpInterceptor> {
-
- /**
- * 处理请求
- *
- * @param httpObj 请求或响应对象
- */
- void process(T httpObj);
-
- /**
- * 拦截器链
- *
- * @param 过滤参数类型,HttpRequest或者HttpResponse
- * @author looly
- * @since 5.7.16
- */
- class Chain> implements cn.hutool.core.lang.Chain, Chain> {
- private final List> interceptors = new LinkedList<>();
-
- @Override
- public Chain addChain(final HttpInterceptor element) {
- interceptors.add(element);
- return this;
- }
-
- @Override
- public Iterator> iterator() {
- return interceptors.iterator();
- }
-
- /**
- * 清空
- *
- * @return this
- * @since 5.8.0
- */
- public Chain clear() {
- interceptors.clear();
- return this;
- }
- }
-}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpRequest.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpRequest.java
index 092ccfbf6..feb0ac0c2 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpRequest.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpRequest.java
@@ -16,32 +16,30 @@ import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.text.StrUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
-import cn.hutool.http.meta.ContentType;
import cn.hutool.http.GlobalHeaders;
-import cn.hutool.http.meta.Header;
import cn.hutool.http.HttpConfig;
import cn.hutool.http.HttpException;
import cn.hutool.http.HttpGlobalConfig;
-import cn.hutool.http.meta.HttpStatus;
import cn.hutool.http.HttpUtil;
-import cn.hutool.http.meta.Method;
import cn.hutool.http.client.body.BytesBody;
-import cn.hutool.http.client.body.UrlEncodedFormBody;
import cn.hutool.http.client.body.MultipartBody;
-import cn.hutool.http.client.body.RequestBody;
+import cn.hutool.http.client.body.HttpBody;
+import cn.hutool.http.client.body.UrlEncodedFormBody;
import cn.hutool.http.client.cookie.GlobalCookieManager;
+import cn.hutool.http.meta.ContentType;
+import cn.hutool.http.meta.Header;
+import cn.hutool.http.meta.HttpStatus;
+import cn.hutool.http.meta.Method;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.net.CookieManager;
-import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URLStreamHandler;
import java.nio.charset.Charset;
-import java.util.Collection;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -352,28 +350,6 @@ public class HttpRequest extends HttpBase {
// ---------------------------------------------------------------- Http Request Header start
- /**
- * 设置contentType
- *
- * @param contentType contentType
- * @return HttpRequest
- */
- public HttpRequest contentType(final String contentType) {
- header(Header.CONTENT_TYPE, contentType);
- return this;
- }
-
- /**
- * 设置是否为长连接
- *
- * @param isKeepAlive 是否长连接
- * @return HttpRequest
- */
- public HttpRequest keepAlive(final boolean isKeepAlive) {
- header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
- return this;
- }
-
/**
* @return 获取是否为长连接
*/
@@ -406,35 +382,6 @@ public class HttpRequest extends HttpBase {
return this;
}
- /**
- * 设置Cookie
- * 自定义Cookie后会覆盖Hutool的默认Cookie行为
- *
- * @param cookies Cookie值数组,如果为{@code null}则设置无效,使用默认Cookie行为
- * @return this
- * @since 5.4.1
- */
- public HttpRequest cookie(final Collection cookies) {
- return cookie(CollUtil.isEmpty(cookies) ? null : cookies.toArray(new HttpCookie[0]));
- }
-
- /**
- * 设置Cookie
- * 自定义Cookie后会覆盖Hutool的默认Cookie行为
- *
- * @param cookies Cookie值数组,如果为{@code null}则设置无效,使用默认Cookie行为
- * @return this
- * @since 3.1.1
- */
- public HttpRequest cookie(final HttpCookie... cookies) {
- if (ArrayUtil.isEmpty(cookies)) {
- return disableCookie();
- }
- // 名称/值对之间用分号和空格 ('; ')
- // https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cookie
- return cookie(ArrayUtil.join(cookies, "; "));
- }
-
/**
* 设置Cookie
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
@@ -447,27 +394,6 @@ public class HttpRequest extends HttpBase {
this.cookie = cookie;
return this;
}
-
- /**
- * 禁用默认Cookie行为,此方法调用后会将Cookie置为空。
- * 如果想重新启用Cookie,请调用:{@link #cookie(String)}方法自定义Cookie。
- * 如果想启动默认的Cookie行为(自动回填服务器传回的Cookie),则调用{@link #enableDefaultCookie()}
- *
- * @return this
- * @since 3.0.7
- */
- public HttpRequest disableCookie() {
- return cookie(StrUtil.EMPTY);
- }
-
- /**
- * 打开默认的Cookie行为(自动回填服务器传回的Cookie)
- *
- * @return this
- */
- public HttpRequest enableDefaultCookie() {
- return cookie((String) null);
- }
// ---------------------------------------------------------------- Http Request Header end
// ---------------------------------------------------------------- Form start
@@ -947,42 +873,6 @@ public class HttpRequest extends HttpBase {
return this;
}
- /**
- * 设置拦截器,用于在请求前重新编辑请求
- *
- * @param interceptor 拦截器实现
- * @return this
- * @see #addRequestInterceptor(HttpInterceptor)
- * @since 5.7.16
- */
- public HttpRequest addInterceptor(final HttpInterceptor interceptor) {
- return addRequestInterceptor(interceptor);
- }
-
- /**
- * 设置拦截器,用于在请求前重新编辑请求
- *
- * @param interceptor 拦截器实现
- * @return this
- * @since 5.8.0
- */
- public HttpRequest addRequestInterceptor(final HttpInterceptor interceptor) {
- config.addRequestInterceptor(interceptor);
- return this;
- }
-
- /**
- * 设置拦截器,用于在请求前重新编辑请求
- *
- * @param interceptor 拦截器实现
- * @return this
- * @since 5.8.0
- */
- public HttpRequest addResponseInterceptor(final HttpInterceptor interceptor) {
- config.addResponseInterceptor(interceptor);
- return this;
- }
-
/**
* 执行Reuqest请求
*
@@ -1013,7 +903,7 @@ public class HttpRequest extends HttpBase {
* @return this
*/
public HttpResponse execute(final boolean isAsync) {
- return doExecute(isAsync, config.requestInterceptors, config.responseInterceptors);
+ return doExecute(isAsync);
}
/**
@@ -1074,41 +964,6 @@ public class HttpRequest extends HttpBase {
return proxyAuth(HttpUtil.buildBasicAuth(username, password, charset));
}
- /**
- * 令牌验证,生成的头类似于:"Authorization: Bearer XXXXX",一般用于JWT
- *
- * @param token 令牌内容
- * @return HttpRequest
- * @since 5.5.3
- */
- public HttpRequest bearerAuth(final String token) {
- return auth("Bearer " + token);
- }
-
- /**
- * 验证,简单插入Authorization头
- *
- * @param content 验证内容
- * @return HttpRequest
- * @since 5.2.4
- */
- public HttpRequest auth(final String content) {
- header(Header.AUTHORIZATION, content, true);
- return this;
- }
-
- /**
- * 验证,简单插入Authorization头
- *
- * @param content 验证内容
- * @return HttpRequest
- * @since 5.4.6
- */
- public HttpRequest proxyAuth(final String content) {
- header(Header.PROXY_AUTHORIZATION, content, true);
- return this;
- }
-
@Override
public String toString() {
final StringBuilder sb = StrUtil.builder();
@@ -1123,18 +978,9 @@ public class HttpRequest extends HttpBase {
* 执行Reuqest请求
*
* @param isAsync 是否异步
- * @param requestInterceptors 请求拦截器列表
- * @param responseInterceptors 响应拦截器列表
* @return this
*/
- private HttpResponse doExecute(final boolean isAsync, final HttpInterceptor.Chain requestInterceptors,
- final HttpInterceptor.Chain responseInterceptors) {
- if (null != requestInterceptors) {
- for (final HttpInterceptor interceptor : requestInterceptors) {
- interceptor.process(this);
- }
- }
-
+ private HttpResponse doExecute(final boolean isAsync) {
// 初始化URL
urlWithParamIfGet();
// 初始化 connection
@@ -1147,14 +993,7 @@ public class HttpRequest extends HttpBase {
// 获取响应
if (null == httpResponse) {
- httpResponse = new HttpResponse(this.httpConnection, this.config, this.charset, isAsync, isIgnoreResponseBody());
- }
-
- // 拦截响应
- if (null != responseInterceptors) {
- for (final HttpInterceptor interceptor : responseInterceptors) {
- interceptor.process(httpResponse);
- }
+ httpResponse = new HttpResponse(this.httpConnection, this.config.isIgnoreEOFError(), this.charset, isAsync, isIgnoreResponseBody());
}
return httpResponse;
@@ -1186,16 +1025,14 @@ public class HttpRequest extends HttpBase {
if (null != this.cookie) {
// 当用户自定义Cookie时,全局Cookie自动失效
- this.httpConnection.setCookie(this.cookie);
+ this.httpConnection.cookie(this.cookie);
} else {
// 读取全局Cookie信息并附带到请求中
GlobalCookieManager.add(this.httpConnection);
}
// 是否禁用缓存
- if (config.isDisableCache) {
- this.httpConnection.disableCache();
- }
+ this.httpConnection.setDisableCache(config.isDisableCache);
}
/**
@@ -1229,17 +1066,17 @@ public class HttpRequest extends HttpBase {
private HttpResponse sendRedirectIfPossible(final boolean isAsync) {
// 手动实现重定向
if (config.maxRedirectCount > 0) {
- final int responseCode;
+ final int code;
try {
- responseCode = httpConnection.responseCode();
+ code = httpConnection.getCode();
} catch (final IOException e) {
// 错误时静默关闭连接
this.httpConnection.disconnectQuietly();
throw new HttpException(e);
}
- if (responseCode != HttpURLConnection.HTTP_OK) {
- if (HttpStatus.isRedirected(responseCode)) {
+ if (code != HttpURLConnection.HTTP_OK) {
+ if (HttpStatus.isRedirected(code)) {
final UrlBuilder redirectUrl;
String location = httpConnection.header(Header.LOCATION);
@@ -1258,9 +1095,7 @@ public class HttpRequest extends HttpBase {
setUrl(redirectUrl);
if (redirectCount < config.maxRedirectCount) {
redirectCount++;
- // 重定向不再走过滤器
- return doExecute(isAsync, config.interceptorOnRedirect ? config.requestInterceptors : null,
- config.interceptorOnRedirect ? config.responseInterceptors : null);
+ return doExecute(isAsync);
}
}
}
@@ -1307,7 +1142,7 @@ public class HttpRequest extends HttpBase {
}
// Write的时候会优先使用body中的内容,write时自动关闭OutputStream
- final RequestBody body;
+ final HttpBody body;
if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
body = BytesBody.of(this.bodyBytes);
} else {
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpResponse.java
index 9e362c843..c27da6469 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpResponse.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpResponse.java
@@ -1,30 +1,19 @@
package cn.hutool.http.client.engine.jdk;
-import cn.hutool.core.convert.Convert;
-import cn.hutool.core.io.stream.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.lang.Assert;
-import cn.hutool.core.net.url.URLEncoder;
-import cn.hutool.core.regex.ReUtil;
+import cn.hutool.core.io.stream.FastByteArrayOutputStream;
import cn.hutool.core.text.StrUtil;
-import cn.hutool.core.util.ObjUtil;
-import cn.hutool.http.meta.Header;
-import cn.hutool.http.HttpConfig;
import cn.hutool.http.HttpException;
-import cn.hutool.http.HttpUtil;
+import cn.hutool.http.client.Response;
+import cn.hutool.http.client.body.ResponseBody;
import cn.hutool.http.client.cookie.GlobalCookieManager;
import java.io.ByteArrayInputStream;
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;
@@ -36,12 +25,14 @@ import java.util.Map.Entry;
*
* @author Looly
*/
-public class HttpResponse extends HttpBase implements Closeable {
+public class HttpResponse extends HttpBase implements Response, Closeable {
/**
- * Http配置
+ * 是否忽略响应读取时可能的EOF异常。
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
*/
- protected HttpConfig config;
+ protected boolean ignoreEOFError;
/**
* 持有连接对象
*/
@@ -71,15 +62,14 @@ public class HttpResponse extends HttpBase implements Closeable {
* 构造
*
* @param httpConnection {@link HttpConnection}
- * @param config Http配置
+ * @param ignoreEOFError 是否忽略响应读取时可能的EOF异常
* @param charset 编码,从请求编码中获取默认编码
* @param isAsync 是否异步
* @param isIgnoreBody 是否忽略读取响应体
- * @since 3.1.2
*/
- protected HttpResponse(final HttpConnection httpConnection, final HttpConfig config, final Charset charset, final boolean isAsync, final boolean isIgnoreBody) {
+ protected HttpResponse(final HttpConnection httpConnection, final boolean ignoreEOFError, final Charset charset, final boolean isAsync, final boolean isIgnoreBody) {
this.httpConnection = httpConnection;
- this.config = config;
+ this.ignoreEOFError = ignoreEOFError;
this.charset = charset;
this.isAsync = isAsync;
this.ignoreBody = isIgnoreBody;
@@ -91,20 +81,11 @@ public class HttpResponse extends HttpBase implements Closeable {
*
* @return 状态码
*/
+ @Override
public int getStatus() {
return this.status;
}
- /**
- * 请求是否成功,判断依据为:状态码范围在200~299内。
- *
- * @return 是否成功请求
- * @since 4.1.9
- */
- public boolean isOk() {
- return this.status >= 200 && this.status < 300;
- }
-
/**
* 同步
* 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。
@@ -118,56 +99,6 @@ public class HttpResponse extends HttpBase implements Closeable {
// ---------------------------------------------------------------- Http Response Header start
- /**
- * 获取内容编码
- *
- * @return String
- */
- public String contentEncoding() {
- return header(Header.CONTENT_ENCODING);
- }
-
- /**
- * 获取内容长度,以下情况长度无效:
- *
- * - Transfer-Encoding: Chunked
- * - Content-Encoding: XXX
- *
- * 参考: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;
- }
-
- /**
- * 是否为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
*
@@ -220,6 +151,7 @@ public class HttpResponse extends HttpBase implements Closeable {
*
* @return 响应流
*/
+ @Override
public InputStream bodyStream() {
if (isAsync) {
return this.in;
@@ -227,6 +159,11 @@ public class HttpResponse extends HttpBase implements Closeable {
return new ByteArrayInputStream(this.bodyBytes);
}
+ @Override
+ public ResponseBody body() {
+ return new ResponseBody(this, this.ignoreEOFError);
+ }
+
/**
* 获取响应流字节码
* 此方法会转为同步模式
@@ -241,148 +178,7 @@ public class HttpResponse extends HttpBase implements Closeable {
}
/**
- * 获取响应主体
- *
- * @return String
- * @throws HttpException 包装IO异常
- */
- public String body() throws HttpException {
- return HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse);
- }
-
- /**
- * 将响应内容写出到{@link OutputStream}
- * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
- * 写出后会关闭Http流(异步模式)
- *
- * @param out 写出的流
- * @param isCloseOut 是否关闭输出流
- * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
- * @return 写出bytes数
- * @since 3.3.2
- */
- public long writeBody(final OutputStream out, final boolean isCloseOut, final StreamProgress streamProgress) {
- Assert.notNull(out, "[out] must be not null!");
- final long contentLength = contentLength();
- try {
- return copyBody(bodyStream(), out, contentLength, streamProgress, this.config.isIgnoreEOFError());
- } finally {
- IoUtil.close(this);
- if (isCloseOut) {
- IoUtil.close(out);
- }
- }
- }
-
- /**
- * 将响应内容写出到文件
- * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
- * 写出后会关闭Http流(异步模式)
- *
- * @param targetFileOrDir 写出到的文件或目录
- * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
- * @return 写出bytes数
- * @since 3.3.2
- */
- public long writeBody(final File targetFileOrDir, final StreamProgress streamProgress) {
- Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
-
- final File outFile = completeFileNameFromHeader(targetFileOrDir, null);
- return writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
- }
-
- /**
- * 将响应内容写出到文件-避免未完成的文件
- * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
- * 写出后会关闭Http流(异步模式)
- * 来自:https://gitee.com/dromara/hutool/pulls/407
- * 此方法原理是先在目标文件同级目录下创建临时文件,下载之,等下载完毕后重命名,避免因下载错误导致的文件不完整。
- *
- * @param targetFileOrDir 写出到的文件或目录
- * @param tempFileSuffix 临时文件后缀,默认".temp"
- * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
- * @return 写出bytes数
- * @since 5.7.12
- */
- public long writeBody(final File targetFileOrDir, String tempFileSuffix, final StreamProgress streamProgress) {
- Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
-
- File outFile = completeFileNameFromHeader(targetFileOrDir, null);
-
- 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);
-
- final long length;
- try {
- length = writeBody(outFile, streamProgress);
- // 重命名下载好的临时文件
- FileUtil.rename(outFile, fileName, true);
- } catch (final Throwable e) {
- // 异常则删除临时文件
- FileUtil.del(outFile);
- throw new HttpException(e);
- }
- return length;
- }
-
- /**
- * 将响应内容写出到文件
- * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
- * 写出后会关闭Http流(异步模式)
- *
- * @param targetFileOrDir 写出到的文件
- * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
- * @return 写出的文件
- * @since 5.6.4
- */
- public File writeBodyForFile(final File targetFileOrDir, final StreamProgress streamProgress) {
- Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
-
- final File outFile = completeFileNameFromHeader(targetFileOrDir, null);
- writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
-
- return outFile;
- }
-
- /**
- * 将响应内容写出到文件
- * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
- * 写出后会关闭Http流(异步模式)
- *
- * @param targetFileOrDir 写出到的文件或目录
- * @return 写出bytes数
- * @since 3.3.2
- */
- public long writeBody(final File targetFileOrDir) {
- return writeBody(targetFileOrDir, null);
- }
-
- /**
- * 将响应内容写出到文件
- * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
- * 写出后会关闭Http流(异步模式)
- *
- * @param targetFileOrDir 写出到的文件或目录的路径
- * @return 写出bytes数
- * @since 3.3.2
- */
- public long writeBody(final String targetFileOrDir) {
- return writeBody(FileUtil.file(targetFileOrDir));
- }
-
- /**
- * 设置主体字节码,一版用于拦截器修改响应内容
+ * 设置主体字节码,一般用于拦截器修改响应内容
* 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8
*
* @param bodyBytes 主体
@@ -420,54 +216,9 @@ public class HttpResponse extends HttpBase implements Closeable {
return sb.toString();
}
- /**
- * 从响应头补全下载文件名
- *
- * @param targetFileOrDir 目标文件夹或者目标文件
- * @param customParamName 自定义的参数名称,如果传入{@code null},默认使用"filename"
- * @return File 保存的文件
- * @since 5.4.1
- */
- public File completeFileNameFromHeader(final File targetFileOrDir, final String customParamName) {
- if (false == targetFileOrDir.isDirectory()) {
- // 非目录直接返回
- return targetFileOrDir;
- }
-
- // 从头信息中获取文件名
- String fileName = getFileNameFromDisposition(ObjUtil.defaultIfNull(customParamName, "filename"));
- if (StrUtil.isBlank(fileName)) {
- final String path = httpConnection.getUrl().getPath();
- // 从路径中获取文件名
- fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
- if (StrUtil.isBlank(fileName)) {
- // 编码后的路径做为文件名
- fileName = URLEncoder.encodeQuery(path, charset);
- } else {
- // issue#I4K0FS@Gitee
- fileName = URLEncoder.encodeQuery(fileName, charset);
- }
- }
- return FileUtil.file(targetFileOrDir, fileName);
- }
-
- /**
- * 从Content-Disposition头中获取文件名
- *
- * @param paramName 文件名的参数名
- * @return 文件名,empty表示无
- * @since 5.8.10
- */
- public String getFileNameFromDisposition(final String paramName) {
- String fileName = null;
- final String disposition = header(Header.CONTENT_DISPOSITION);
- if (StrUtil.isNotBlank(disposition)) {
- fileName = ReUtil.get(paramName + "=\"(.*?)\"", disposition, 1);
- if (StrUtil.isBlank(fileName)) {
- fileName = StrUtil.subAfter(disposition, paramName + "=", true);
- }
- }
- return fileName;
+ @Override
+ public String header(final String name) {
+ return super.header(name);
}
// ---------------------------------------------------------------- Private method start
@@ -509,7 +260,7 @@ public class HttpResponse extends HttpBase implements Closeable {
private void init() throws HttpException {
// 获取响应状态码
try {
- this.status = httpConnection.responseCode();
+ this.status = httpConnection.getCode();
} catch (final IOException e) {
if (false == (e instanceof FileNotFoundException)) {
throw new HttpException(e);
@@ -529,11 +280,10 @@ public class HttpResponse extends HttpBase implements Closeable {
// 存储服务端设置的Cookie信息
GlobalCookieManager.store(httpConnection, this.headers);
- // 获取响应编码
- final Charset charset = httpConnection.getCharset();
- this.charsetFromResponse = charset;
- if (null != charset) {
- this.charset = charset;
+ // 获取响应编码,如果非空,替换用户定义的编码
+ final Charset charsetFromResponse = httpConnection.getCharset();
+ if (null != charsetFromResponse) {
+ this.charset = charsetFromResponse;
}
// 获取响应内容流
@@ -591,40 +341,8 @@ public class HttpResponse extends HttpBase implements Closeable {
final long contentLength = contentLength();
final FastByteArrayOutputStream out = new FastByteArrayOutputStream((int) contentLength);
- copyBody(in, out, contentLength, null, this.config.isIgnoreEOFError());
+ body().writeClose(out);
this.bodyBytes = out.toByteArray();
}
-
- /**
- * 将响应内容写出到{@link OutputStream}
- * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
- * 写出后会关闭Http流(异步模式)
- *
- * @param in 输入流
- * @param out 写出的流
- * @param contentLength 总长度,-1表示未知
- * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
- * @param isIgnoreEOFError 是否忽略响应读取时可能的EOF异常
- * @return 拷贝长度
- */
- private static long copyBody(final InputStream in, final OutputStream out, final long contentLength, final StreamProgress streamProgress, final 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 (final 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
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpUrlConnectionUtil.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpUrlConnectionUtil.java
index 7eb60d4c4..6e8db220b 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpUrlConnectionUtil.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpUrlConnectionUtil.java
@@ -1,13 +1,19 @@
package cn.hutool.http.client.engine.jdk;
+import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.reflect.FieldUtil;
import cn.hutool.core.reflect.ModifierUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.SystemUtil;
import cn.hutool.http.HttpException;
+import javax.net.ssl.HttpsURLConnection;
+import java.io.IOException;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.URLConnection;
import java.security.AccessController;
import java.security.PrivilegedAction;
@@ -55,4 +61,38 @@ public class HttpUrlConnectionUtil {
FieldUtil.setStaticFieldValue(methodsField, METHODS);
}
}
+
+ /**
+ * 初始化http或https请求参数
+ * 有些时候https请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
+ *
+ * @param url 请求的URL,必须为http
+ * @param proxy 代理,无代理传{@code null}
+ * @return {@link HttpURLConnection},https返回{@link HttpsURLConnection}
+ * @throws IORuntimeException IO异常
+ */
+ public static HttpURLConnection openHttp(final URL url, final Proxy proxy) throws IORuntimeException {
+ final URLConnection conn = openConnection(url, proxy);
+ if (false == conn instanceof HttpURLConnection) {
+ // 防止其它协议造成的转换异常
+ throw new HttpException("'{}' of URL [{}] is not a http connection, make sure URL is format for http.",
+ conn.getClass().getName(), url);
+ }
+
+ return (HttpURLConnection) conn;
+ }
+
+ /**
+ * 建立连接
+ *
+ * @return {@link URLConnection}
+ * @throws IORuntimeException IO异常
+ */
+ private static URLConnection openConnection(final URL url, final Proxy proxy) throws IORuntimeException {
+ try {
+ return (null == proxy) ? url.openConnection() : url.openConnection(proxy);
+ } catch (final IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/JdkClientEngine.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/JdkClientEngine.java
new file mode 100755
index 000000000..b6e570438
--- /dev/null
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/JdkClientEngine.java
@@ -0,0 +1,208 @@
+package cn.hutool.http.client.engine.jdk;
+
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.net.url.UrlBuilder;
+import cn.hutool.core.text.StrUtil;
+import cn.hutool.http.HttpException;
+import cn.hutool.http.HttpUtil;
+import cn.hutool.http.client.ClientConfig;
+import cn.hutool.http.client.ClientEngine;
+import cn.hutool.http.client.Request;
+import cn.hutool.http.client.Response;
+import cn.hutool.http.client.body.HttpBody;
+import cn.hutool.http.client.cookie.GlobalCookieManager;
+import cn.hutool.http.meta.Header;
+import cn.hutool.http.meta.HttpStatus;
+import cn.hutool.http.meta.Method;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+
+/**
+ * 基于JDK的UrlConnection的Http客户端引擎实现
+ *
+ * @author looly
+ */
+public class JdkClientEngine implements ClientEngine {
+
+ private final ClientConfig config;
+ private HttpConnection conn;
+ /**
+ * 重定向次数计数器,内部使用
+ */
+ private int redirectCount;
+
+ /**
+ * 构造
+ */
+ public JdkClientEngine() {
+ this.config = ClientConfig.of();
+ }
+
+ @Override
+ public Response send(final Request message) {
+ return send(message, false);
+ }
+
+ /**
+ * 发送请求
+ *
+ * @param message 请求消息
+ * @param isAsync 是否异步,异步不会立即读取响应内容
+ * @return {@link Response}
+ */
+ public HttpResponse send(final Request message, final boolean isAsync) {
+ initConn(message);
+ try {
+ doSend(message);
+ } catch (final IOException e) {
+ // 出错后关闭连接
+ IoUtil.close(this);
+ throw new RuntimeException(e);
+ }
+
+ return sendRedirectIfPossible(message, isAsync);
+ }
+
+ @Override
+ public Object getRawEngine() {
+ return this;
+ }
+
+ @Override
+ public void close() throws IOException {
+ if (null != conn) {
+ conn.disconnectQuietly();
+ }
+ }
+
+ /**
+ * 执行发送
+ *
+ * @param message 请求消息
+ * @throws IOException IO异常
+ */
+ private void doSend(final Request message) throws IOException {
+ final HttpBody body = message.body();
+ if (null != body) {
+ // 带有消息体,一律按照Rest方式发送
+ body.writeClose(this.conn.getOutputStream());
+ return;
+ }
+
+ // 非Rest简单GET请求
+ this.conn.connect();
+ }
+
+ /**
+ * 初始化连接对象
+ *
+ * @param message 请求消息
+ */
+ private void initConn(final Request message) {
+ // 执行下次请求时自动关闭上次请求(常用于转发)
+ IoUtil.close(this);
+
+ this.conn = buildConn(message);
+ }
+
+ /**
+ * 构建{@link HttpConnection}
+ *
+ * @param message {@link Request}消息
+ * @return {@link HttpConnection}
+ */
+ private HttpConnection buildConn(final Request message) {
+ final HttpConnection conn = HttpConnection
+ .of(message.url().toURL(), config.proxy)
+ .setConnectTimeout(config.getConnectionTimeout())
+ .setReadTimeout(config.getReadTimeout())
+ .setMethod(message.method())//
+ .setHttpsInfo(config.getHostnameVerifier(), config.getSocketFactory())
+ // 关闭JDK自动转发,采用手动转发方式
+ .setInstanceFollowRedirects(false)
+ .setChunkedStreamingMode(message.isChunked() ? 4096 : -1)
+ .setDisableCache(config.isDisableCache())
+ // 覆盖默认Header
+ .header(message.headers(), true);
+
+ if (null == message.header(Header.COOKIE)) {
+ // 用户没有自定义Cookie,则读取全局Cookie信息并附带到请求中
+ GlobalCookieManager.add(conn);
+ }
+
+ return conn;
+ }
+
+ /**
+ * 调用转发,如果需要转发返回转发结果,否则返回{@code null}
+ *
+ * @param isAsync 最终请求是否异步
+ * @return {@link HttpResponse},无转发返回 {@code null}
+ */
+ private HttpResponse sendRedirectIfPossible(final Request message, final boolean isAsync) {
+ final HttpConnection conn = this.conn;
+ // 手动实现重定向
+ if (message.maxRedirectCount() > 0) {
+ final int code;
+ try {
+ code = conn.getCode();
+ } catch (final IOException e) {
+ // 错误时静默关闭连接
+ conn.disconnectQuietly();
+ throw new HttpException(e);
+ }
+
+ if (code != HttpURLConnection.HTTP_OK) {
+ if (HttpStatus.isRedirected(code)) {
+ message.url(getLocationUrl(message.url(), conn.header(Header.LOCATION)));
+ if (redirectCount < message.maxRedirectCount()) {
+ redirectCount++;
+ return send(message, isAsync);
+ }
+ }
+ }
+ }
+
+ // 最终页面
+ return new HttpResponse(this.conn, true, message.charset(), isAsync,
+ isIgnoreResponseBody(message.method()));
+ }
+
+ /**
+ * 获取转发的新的URL
+ *
+ * @param parentUrl 上级请求的URL
+ * @param location 获取的Location
+ * @return 新的URL
+ */
+ private static UrlBuilder getLocationUrl(final UrlBuilder parentUrl, String location) {
+ final UrlBuilder redirectUrl;
+ if (false == HttpUtil.isHttp(location) && false == HttpUtil.isHttps(location)) {
+ // issue#I5TPSY
+ // location可能为相对路径
+ if (false == location.startsWith("/")) {
+ location = StrUtil.addSuffixIfNot(parentUrl.getPathStr(), "/") + location;
+ }
+ redirectUrl = UrlBuilder.of(parentUrl.getScheme(), parentUrl.getHost(), parentUrl.getPort(),
+ location, null, null, parentUrl.getCharset());
+ } else {
+ redirectUrl = UrlBuilder.ofHttpWithoutEncode(location);
+ }
+
+ return redirectUrl;
+ }
+
+ /**
+ * 是否忽略读取响应body部分
+ * HEAD、CONNECT、OPTIONS、TRACE方法将不读取响应体
+ *
+ * @return 是否需要忽略响应body部分
+ */
+ private static boolean isIgnoreResponseBody(final Method method) {
+ return Method.HEAD == method //
+ || Method.CONNECT == method //
+ || Method.OPTIONS == method //
+ || Method.TRACE == method;
+ }
+}
diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpRequestBody.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpRequestBody.java
index 9060dde8f..25c3b94a7 100755
--- a/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpRequestBody.java
+++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpRequestBody.java
@@ -1,24 +1,24 @@
package cn.hutool.http.client.engine.okhttp;
-import cn.hutool.http.client.body.RequestBody;
+import cn.hutool.http.client.body.HttpBody;
import okhttp3.MediaType;
import okio.BufferedSink;
/**
- * OkHttp的请求体实现,通过{@link RequestBody}转换实现
+ * OkHttp的请求体实现,通过{@link HttpBody}转换实现
*
* @author looly
*/
public class OkHttpRequestBody extends okhttp3.RequestBody {
- private final RequestBody body;
+ private final HttpBody body;
/**
* 构造
*
- * @param body 请求体{@link RequestBody}
+ * @param body 请求体{@link HttpBody}
*/
- public OkHttpRequestBody(final RequestBody body) {
+ public OkHttpRequestBody(final HttpBody body) {
this.body = body;
}
diff --git a/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java b/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java
index 2f21cfe1f..8efd4aa9c 100644
--- a/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java
+++ b/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java
@@ -184,22 +184,6 @@ public class HttpRequestTest {
Console.log(execute.getStatus(), execute.header(Header.LOCATION));
}
- @Test
- @Ignore
- public void addInterceptorTest() {
- HttpUtil.createGet("https://hutool.cn")
- .addInterceptor(Console::log)
- .addResponseInterceptor((res)-> Console.log(res.getStatus()))
- .execute();
- }
-
- @Test
- @Ignore
- public void addGlobalInterceptorTest() {
- GlobalInterceptor.INSTANCE.addRequestInterceptor(Console::log);
- HttpUtil.createGet("https://hutool.cn").execute();
- }
-
@Test
@Ignore
public void getWithFormTest(){