From 35a8f5e281f400c23baf2659cbdea4ab72696fcd Mon Sep 17 00:00:00 2001 From: Looly Date: Thu, 27 Oct 2022 03:21:40 +0800 Subject: [PATCH] fix code --- .../cn/hutool/core/io/file/FileNameUtil.java | 17 + .../cn/hutool/http/GlobalInterceptor.java | 98 ----- .../main/java/cn/hutool/http/HttpConfig.java | 36 +- .../main/java/cn/hutool/http/HttpUtil.java | 12 +- .../cn/hutool/http/client/ClientConfig.java | 241 +++++++++++++ .../{Headers.java => HeaderOperation.java} | 26 +- .../java/cn/hutool/http/client/Request.java | 58 ++- .../java/cn/hutool/http/client/Response.java | 26 +- .../cn/hutool/http/client/body/FormBody.java | 2 +- .../body/{RequestBody.java => HttpBody.java} | 9 +- .../http/client/body/MultipartBody.java | 2 + .../hutool/http/client/body/ResourceBody.java | 14 +- .../hutool/http/client/body/ResponseBody.java | 243 +++++++++++++ .../hutool/http/client/body/StringBody.java | 3 +- .../http/client/body/UrlEncodedFormBody.java | 6 + .../httpclient4/HttpClient4BodyEntity.java | 10 +- .../engine/httpclient4/HttpClient4Engine.java | 4 +- .../httpclient4/HttpClient4Response.java | 2 +- .../httpclient5/HttpClient5BodyEntity.java | 10 +- .../engine/httpclient5/HttpClient5Engine.java | 4 +- .../httpclient5/HttpClient5Response.java | 2 +- .../http/client/engine/jdk/HttpBase.java | 15 +- .../client/engine/jdk/HttpConnection.java | 257 ++++--------- .../client/engine/jdk/HttpInterceptor.java | 56 --- .../http/client/engine/jdk/HttpRequest.java | 199 +---------- .../http/client/engine/jdk/HttpResponse.java | 338 ++---------------- .../engine/jdk/HttpUrlConnectionUtil.java | 40 +++ .../client/engine/jdk/JdkClientEngine.java | 208 +++++++++++ .../engine/okhttp/OkHttpRequestBody.java | 10 +- .../java/cn/hutool/http/HttpRequestTest.java | 16 - 30 files changed, 1039 insertions(+), 925 deletions(-) delete mode 100755 hutool-http/src/main/java/cn/hutool/http/GlobalInterceptor.java create mode 100755 hutool-http/src/main/java/cn/hutool/http/client/ClientConfig.java rename hutool-http/src/main/java/cn/hutool/http/client/{Headers.java => HeaderOperation.java} (89%) rename hutool-http/src/main/java/cn/hutool/http/client/body/{RequestBody.java => HttpBody.java} (86%) create mode 100755 hutool-http/src/main/java/cn/hutool/http/client/body/ResponseBody.java delete mode 100644 hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpInterceptor.java create mode 100755 hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/JdkClientEngine.java diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java index e8ac70863..227c195b1 100755 --- a/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/FileNameUtil.java @@ -133,6 +133,23 @@ public class FileNameUtil { return extName(fileName); } + /** + * 增加临时扩展名 + * + * @param fileName 文件名 + * @param suffix 临时扩展名,如果为空,使用`.temp` + * @return 临时文件名 + */ + public static String addTempSuffix(final String fileName, String suffix){ + if (StrUtil.isBlank(suffix)) { + suffix = ".temp"; + } else { + suffix = StrUtil.addPrefixIfNot(suffix, StrUtil.DOT); + } + + return fileName + suffix; + } + /** * 返回主文件名 * diff --git a/hutool-http/src/main/java/cn/hutool/http/GlobalInterceptor.java b/hutool-http/src/main/java/cn/hutool/http/GlobalInterceptor.java deleted file mode 100755 index f988379a9..000000000 --- a/hutool-http/src/main/java/cn/hutool/http/GlobalInterceptor.java +++ /dev/null @@ -1,98 +0,0 @@ -package cn.hutool.http; - -import cn.hutool.http.client.engine.jdk.HttpInterceptor; -import cn.hutool.http.client.engine.jdk.HttpRequest; -import cn.hutool.http.client.engine.jdk.HttpResponse; - -/** - * 全局的拦截器
- * 包括请求拦截器和响应拦截器 - * - * @author looly - * @since 5.8.0 - */ -public enum GlobalInterceptor { - INSTANCE; - - private final HttpInterceptor.Chain requestInterceptors = new HttpInterceptor.Chain<>(); - private final HttpInterceptor.Chain responseInterceptors = new HttpInterceptor.Chain<>(); - - /** - * 设置拦截器,用于在请求前重新编辑请求 - * - * @param interceptor 拦截器实现 - * @return this - */ - synchronized public GlobalInterceptor addRequestInterceptor(final HttpInterceptor interceptor) { - this.requestInterceptors.addChain(interceptor); - return this; - } - - /** - * 设置拦截器,用于在响应读取后完成编辑或读取 - * - * @param interceptor 拦截器实现 - * @return this - */ - synchronized public GlobalInterceptor addResponseInterceptor(final HttpInterceptor interceptor) { - this.responseInterceptors.addChain(interceptor); - return this; - } - - /** - * 清空请求和响应拦截器 - * - * @return this - */ - public GlobalInterceptor clear() { - clearRequest(); - clearResponse(); - return this; - } - - /** - * 清空请求拦截器 - * - * @return this - */ - synchronized public GlobalInterceptor clearRequest() { - requestInterceptors.clear(); - return this; - } - - /** - * 清空响应拦截器 - * - * @return this - */ - synchronized public GlobalInterceptor clearResponse() { - responseInterceptors.clear(); - return this; - } - - /** - * 复制请求过滤器列表 - * - * @return {@link HttpInterceptor.Chain} - */ - HttpInterceptor.Chain getCopiedRequestInterceptor() { - final HttpInterceptor.Chain copied = new HttpInterceptor.Chain<>(); - for (final HttpInterceptor interceptor : this.requestInterceptors) { - copied.addChain(interceptor); - } - return copied; - } - - /** - * 复制响应过滤器列表 - * - * @return {@link HttpInterceptor.Chain} - */ - HttpInterceptor.Chain getCopiedResponseInterceptor() { - final HttpInterceptor.Chain copied = new HttpInterceptor.Chain<>(); - for (final HttpInterceptor interceptor : this.responseInterceptors) { - copied.addChain(interceptor); - } - return copied; - } -} diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpConfig.java b/hutool-http/src/main/java/cn/hutool/http/HttpConfig.java index a4d5aa09b..36cc0af66 100755 --- a/hutool-http/src/main/java/cn/hutool/http/HttpConfig.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpConfig.java @@ -2,9 +2,6 @@ package cn.hutool.http; import cn.hutool.core.lang.Assert; import cn.hutool.core.net.ssl.SSLUtil; -import cn.hutool.http.client.engine.jdk.HttpInterceptor; -import cn.hutool.http.client.engine.jdk.HttpRequest; -import cn.hutool.http.client.engine.jdk.HttpResponse; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLSocketFactory; @@ -79,15 +76,6 @@ public class HttpConfig { */ boolean decodeUrl = HttpGlobalConfig.isDecodeUrl(); - /** - * 请求前的拦截器,用于在请求前重新编辑请求 - */ - public final HttpInterceptor.Chain requestInterceptors = GlobalInterceptor.INSTANCE.getCopiedRequestInterceptor(); - /** - * 响应后的拦截器,用于在响应后处理逻辑 - */ - public final HttpInterceptor.Chain responseInterceptors = GlobalInterceptor.INSTANCE.getCopiedResponseInterceptor(); - /** * 重定向时是否使用拦截器 */ @@ -125,7 +113,7 @@ public class HttpConfig { } /** - * 设置连接超时,单位:毫秒 + * 设置读取超时,单位:毫秒 * * @param milliseconds 超时毫秒数 * @return this @@ -268,28 +256,6 @@ public class HttpConfig { return this; } - /** - * 设置拦截器,用于在请求前重新编辑请求 - * - * @param interceptor 拦截器实现 - * @return this - */ - public HttpConfig addRequestInterceptor(final HttpInterceptor interceptor) { - this.requestInterceptors.addChain(interceptor); - return this; - } - - /** - * 设置拦截器,用于在请求前重新编辑请求 - * - * @param interceptor 拦截器实现 - * @return this - */ - public HttpConfig addResponseInterceptor(final HttpInterceptor interceptor) { - this.responseInterceptors.addChain(interceptor); - return this; - } - /** * 重定向时是否使用拦截器 * diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java index 276fd74ef..a8ec94d2f 100755 --- a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java @@ -124,7 +124,7 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String get(final String urlString, final Charset customCharset) { - return HttpRequest.get(urlString).charset(customCharset).execute().body(); + return HttpRequest.get(urlString).charset(customCharset).execute().bodyStr(); } /** @@ -147,7 +147,7 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String get(final String urlString, final int timeout) { - return HttpRequest.get(urlString).timeout(timeout).execute().body(); + return HttpRequest.get(urlString).timeout(timeout).execute().bodyStr(); } /** @@ -159,7 +159,7 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String get(final String urlString, final Map paramMap) { - return HttpRequest.get(urlString).form(paramMap).execute().body(); + return HttpRequest.get(urlString).form(paramMap).execute().bodyStr(); } /** @@ -173,7 +173,7 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String get(final String urlString, final Map paramMap, final int timeout) { - return HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().body(); + return HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().bodyStr(); } /** @@ -198,7 +198,7 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String post(final String urlString, final Map 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(){