diff --git a/hutool-core/src/main/java/cn/hutool/core/map/multi/MultiValueMap.java b/hutool-core/src/main/java/cn/hutool/core/map/multi/MultiValueMap.java index e7af04b7e..8d0468072 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/multi/MultiValueMap.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/multi/MultiValueMap.java @@ -21,12 +21,14 @@ import java.util.stream.Collectors; *

当通过实例方法获得值集合时,若该集合允许修改,则对值集合的修改将会影响到其所属的{@link MultiValueMap}实例,反之亦然。 * 因此当同时遍历当前实例或者值集合时,若存在写操作,则需要注意可能引发的{@link ConcurrentModificationException}。 * + * @param 键类型 + * @param 值类型 * @author huangchengxing - * @since 6.0.0 * @see AbsCollValueMap * @see CollectionValueMap * @see ListValueMap * @see SetValueMap + * @since 6.0.0 */ public interface MultiValueMap extends Map> { @@ -64,7 +66,7 @@ public interface MultiValueMap extends Map> { * Collection coll = entry.getValues(); * for (V val : coll) { * map.putValue(key, val) - * } + * } * } * } * @@ -81,7 +83,7 @@ public interface MultiValueMap extends Map> { *

{@code
 	 * 	for (V val : coll) {
 	 * 		map.putValue(key, val)
-	 * 	}
+	 *    }
 	 * }
* * @param key 键 @@ -95,7 +97,7 @@ public interface MultiValueMap extends Map> { *
{@code
 	 * 	for (V val : values) {
 	 * 		map.putValue(key, val)
-	 * 	}
+	 *    }
 	 * }
* * @param key 键 @@ -137,7 +139,7 @@ public interface MultiValueMap extends Map> { /** * 将一批值从指定键下的值集合中删除 * - * @param key 键 + * @param key 键 * @param values 值数组 * @return 是否成功删除 */ @@ -149,7 +151,7 @@ public interface MultiValueMap extends Map> { /** * 将一批值从指定键下的值集合中删除 * - * @param key 键 + * @param key 键 * @param values 值集合 * @return 是否成功删除 */ @@ -236,7 +238,7 @@ public interface MultiValueMap extends Map> { * Collection coll = entry.getValues(); * for (V val : coll) { * consumer.accept(key, val); - * } + * } * } * } * @@ -259,8 +261,8 @@ public interface MultiValueMap extends Map> { */ default Collection allValues() { return values().stream() - .flatMap(Collection::stream) - .collect(Collectors.toList()); + .flatMap(Collection::stream) + .collect(Collectors.toList()); } } diff --git a/hutool-core/src/main/java/cn/hutool/core/net/url/URLUtil.java b/hutool-core/src/main/java/cn/hutool/core/net/url/URLUtil.java index 69d1213a4..4527681df 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/url/URLUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/URLUtil.java @@ -558,7 +558,6 @@ public class URLUtil { if (StrUtil.isNotEmpty(body)) { // 去除开头的\或者/ - //noinspection ConstantConditions body = body.replaceAll("^[\\\\/]+", StrUtil.EMPTY); // 替换\为/ body = body.replace("\\", "/"); diff --git a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlQueryUtil.java b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlQueryUtil.java new file mode 100755 index 000000000..07e620ad9 --- /dev/null +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlQueryUtil.java @@ -0,0 +1,215 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.convert.Convert; +import cn.hutool.core.map.MapUtil; +import cn.hutool.core.text.StrUtil; +import cn.hutool.core.util.CharsetUtil; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class UrlQueryUtil { + /** + * 将Map形式的Form表单数据转换为Url参数形式,会自动url编码键和值 + * + * @param paramMap 表单数据 + * @return url参数 + */ + public static String toQuery(final Map paramMap) { + return toQuery(paramMap, CharsetUtil.UTF_8); + } + + /** + * 将Map形式的Form表单数据转换为Url参数形式
+ * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
+ * 会自动url编码键和值
+ * 此方法用于拼接URL中的Query部分,并不适用于POST请求中的表单 + * + *
+	 * key1=v1&key2=&key3=v3
+	 * 
+ * + * @param paramMap 表单数据 + * @param charset 编码,{@code null} 表示不encode键值对 + * @return url参数 + * @see #toQuery(Map, Charset, boolean) + */ + public static String toQuery(final Map paramMap, final Charset charset) { + return toQuery(paramMap, charset, false); + } + + /** + * 将Map形式的Form表单数据转换为Url参数形式
+ * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
+ * 会自动url编码键和值 + * + *
+	 * key1=v1&key2=&key3=v3
+	 * 
+ * + * @param paramMap 表单数据 + * @param charset 编码,null表示不encode键值对 + * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+' + * @return url参数 + * @since 5.7.16 + */ + public static String toQuery(final Map paramMap, final Charset charset, final boolean isFormUrlEncoded) { + return UrlQuery.of(paramMap, isFormUrlEncoded).build(charset); + } + + /** + * 对URL参数做编码,只编码键和值
+ * 提供的值可以是url附带参数,但是不能只是url + * + *

注意,此方法只能标准化整个URL,并不适合于单独编码参数值

+ * + * @param urlWithParams url和参数,可以包含url本身,也可以单独参数 + * @param charset 编码 + * @return 编码后的url和参数 + * @since 4.0.1 + */ + public static String encodeQuery(final String urlWithParams, final Charset charset) { + if (StrUtil.isBlank(urlWithParams)) { + return StrUtil.EMPTY; + } + + String urlPart = null; // url部分,不包括问号 + String paramPart; // 参数部分 + final int pathEndPos = urlWithParams.indexOf('?'); + if (pathEndPos > -1) { + // url + 参数 + urlPart = StrUtil.subPre(urlWithParams, pathEndPos); + paramPart = StrUtil.subSuf(urlWithParams, pathEndPos + 1); + if (StrUtil.isBlank(paramPart)) { + // 无参数,返回url + return urlPart; + } + } else if (false == StrUtil.contains(urlWithParams, '=')) { + // 无参数的URL + return urlWithParams; + } else { + // 无URL的参数 + paramPart = urlWithParams; + } + + paramPart = normalizeQuery(paramPart, charset); + + return StrUtil.isBlank(urlPart) ? paramPart : urlPart + "?" + paramPart; + } + + /** + * 标准化参数字符串,即URL中?后的部分 + * + *

注意,此方法只能标准化整个URL,并不适合于单独编码参数值

+ * + * @param queryPart 参数字符串 + * @param charset 编码 + * @return 标准化的参数字符串 + * @since 4.5.2 + */ + public static String normalizeQuery(final String queryPart, final Charset charset) { + if (StrUtil.isEmpty(queryPart)) { + return queryPart; + } + final StringBuilder builder = new StringBuilder(queryPart.length() + 16); + final int len = queryPart.length(); + String name = null; + int pos = 0; // 未处理字符开始位置 + char c; // 当前字符 + int i; // 当前字符位置 + for (i = 0; i < len; i++) { + c = queryPart.charAt(i); + if (c == '=') { // 键值对的分界点 + if (null == name) { + // 只有=前未定义name时被当作键值分界符,否则做为普通字符 + name = (pos == i) ? StrUtil.EMPTY : queryPart.substring(pos, i); + pos = i + 1; + } + } else if (c == '&') { // 参数对的分界点 + if (pos != i) { + if (null == name) { + // 对于像&a&这类无参数值的字符串,我们将name为a的值设为"" + name = queryPart.substring(pos, i); + builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('='); + } else { + builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=') + .append(RFC3986.QUERY_PARAM_VALUE.encode(queryPart.substring(pos, i), charset)).append('&'); + } + name = null; + } + pos = i + 1; + } + } + + // 结尾处理 + if (null != name) { + builder.append(URLEncoder.encodeQuery(name, charset)).append('='); + } + if (pos != i) { + if (null == name && pos > 0) { + builder.append('='); + } + builder.append(URLEncoder.encodeQuery(queryPart.substring(pos, i), charset)); + } + + // 以&结尾则去除之 + final int lastIndex = builder.length() - 1; + if ('&' == builder.charAt(lastIndex)) { + builder.delete(lastIndex, builder.length()); + } + return builder.toString(); + } + + /** + * 将URL参数解析为Map(也可以解析Post中的键值对参数) + * + * @param paramsStr 参数字符串(或者带参数的Path) + * @param charset 字符集 + * @return 参数Map + * @since 5.2.6 + */ + public static Map decodeQuery(final String paramsStr, final Charset charset) { + final Map queryMap = UrlQuery.of(paramsStr, charset).getQueryMap(); + if (MapUtil.isEmpty(queryMap)) { + return MapUtil.empty(); + } + return Convert.toMap(String.class, String.class, queryMap); + } + + /** + * 将URL参数解析为Map(也可以解析Post中的键值对参数) + * + * @param paramsStr 参数字符串(或者带参数的Path) + * @param charset 字符集 + * @return 参数Map + */ + public static Map> decodeQuery(final String paramsStr, final String charset) { + return decodeQueryList(paramsStr, CharsetUtil.charset(charset)); + } + + /** + * 将URL参数解析为Map(也可以解析Post中的键值对参数) + * + * @param paramsStr 参数字符串(或者带参数的Path) + * @param charset 字符集 + * @return 参数Map + * @since 5.2.6 + */ + public static Map> decodeQueryList(final String paramsStr, final Charset charset) { + final Map queryMap = UrlQuery.of(paramsStr, charset).getQueryMap(); + if (MapUtil.isEmpty(queryMap)) { + return MapUtil.empty(); + } + + final Map> params = new LinkedHashMap<>(); + queryMap.forEach((key, value) -> { + final List values = params.computeIfAbsent(StrUtil.str(key), k -> new ArrayList<>(1)); + // 一般是一个参数 + values.add(StrUtil.str(value)); + }); + return params; + } +} diff --git a/hutool-core/src/test/java/cn/hutool/core/net/url/UrlQueryUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/net/url/UrlQueryUtilTest.java new file mode 100755 index 000000000..17994e794 --- /dev/null +++ b/hutool-core/src/test/java/cn/hutool/core/net/url/UrlQueryUtilTest.java @@ -0,0 +1,153 @@ +package cn.hutool.core.net.url; + +import cn.hutool.core.util.CharsetUtil; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.Map; + +public class UrlQueryUtilTest { + @Test + public void decodeQueryTest() { + final String paramsStr = "uuuu=0&a=b&c=%3F%23%40!%24%25%5E%26%3Ddsssss555555"; + final Map> map = UrlQueryUtil.decodeQuery(paramsStr, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("0", map.get("uuuu").get(0)); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("?#@!$%^&=dsssss555555", map.get("c").get(0)); + } + + @Test + public void decodeQueryTest2() { + // 参数值存在分界标记等号时 + final Map paramMap = UrlQueryUtil.decodeQuery("https://www.xxx.com/api.action?aa=123&f_token=NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=", CharsetUtil.UTF_8); + Assert.assertEquals("123",paramMap.get("aa")); + Assert.assertEquals("NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=",paramMap.get("f_token")); + } + + @Test + public void toQueryTest() { + final String paramsStr = "uuuu=0&a=b&c=3Ddsssss555555"; + final Map> map = UrlQueryUtil.decodeQuery(paramsStr, CharsetUtil.NAME_UTF_8); + + final String encodedParams = UrlQueryUtil.toQuery(map); + Assert.assertEquals(paramsStr, encodedParams); + } + + @Test + public void encodeParamTest() { + // ?单独存在去除之,&单位位于末尾去除之 + String paramsStr = "?a=b&c=d&"; + String encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("a=b&c=d", encode); + + // url不参与转码 + paramsStr = "http://www.abc.dd?a=b&c=d&"; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("http://www.abc.dd?a=b&c=d", encode); + + // b=b中的=被当作值的一部分,不做encode + paramsStr = "a=b=b&c=d&"; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("a=b=b&c=d", encode); + + // =d的情况被处理为key为空 + paramsStr = "a=bbb&c=d&=d"; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("a=bbb&c=d&=d", encode); + + // d=的情况被处理为value为空 + paramsStr = "a=bbb&c=d&d="; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("a=bbb&c=d&d=", encode); + + // 多个&&被处理为单个,相当于空条件 + paramsStr = "a=bbb&c=d&&&d="; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("a=bbb&c=d&d=", encode); + + // &d&相当于只有键,无值得情况 + paramsStr = "a=bbb&c=d&d&"; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("a=bbb&c=d&d=", encode); + + // 中文的键和值被编码 + paramsStr = "a=bbb&c=你好&哈喽&"; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD=", encode); + + // URL原样输出 + paramsStr = "https://www.hutool.cn/"; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals(paramsStr, encode); + + // URL原样输出 + paramsStr = "https://www.hutool.cn/?"; + encode = UrlQueryUtil.encodeQuery(paramsStr, CharsetUtil.UTF_8); + Assert.assertEquals("https://www.hutool.cn/", encode); + } + + @Test + public void decodeParamTest() { + // 开头的?被去除 + String a = "?a=b&c=d&"; + Map> map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + + // =e被当作空为key,e为value + a = "?a=b&c=d&=e"; + map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("e", map.get("").get(0)); + + // 多余的&去除 + a = "?a=b&c=d&=e&&&&"; + map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("e", map.get("").get(0)); + + // 值为空 + a = "?a=b&c=d&e="; + map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("", map.get("e").get(0)); + + // &=被作为键和值都为空 + a = "a=b&c=d&="; + map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertEquals("", map.get("").get(0)); + + // &e&这类单独的字符串被当作key + a = "a=b&c=d&e&"; + map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("b", map.get("a").get(0)); + Assert.assertEquals("d", map.get("c").get(0)); + Assert.assertNull(map.get("e").get(0)); + Assert.assertNull(map.get("").get(0)); + + // 被编码的键和值被还原 + a = "a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD="; + map = UrlQueryUtil.decodeQuery(a, CharsetUtil.NAME_UTF_8); + Assert.assertEquals("bbb", map.get("a").get(0)); + Assert.assertEquals("你好", map.get("c").get(0)); + Assert.assertEquals("", map.get("哈喽").get(0)); + } + + @Test + public void normalizeQueryTest() { + final String encodeResult = UrlQueryUtil.normalizeQuery("参数", CharsetUtil.UTF_8); + Assert.assertEquals("%E5%8F%82%E6%95%B0", encodeResult); + } + + @Test + public void normalizeBlankQueryTest() { + final String encodeResult = UrlQueryUtil.normalizeQuery("", CharsetUtil.UTF_8); + Assert.assertEquals("", encodeResult); + } +} diff --git a/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java b/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java index 382c3f827..2d3fcd114 100644 --- a/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java +++ b/hutool-http/src/main/java/cn/hutool/http/GlobalHeaders.java @@ -3,7 +3,6 @@ package cn.hutool.http; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.map.MapUtil; import cn.hutool.core.text.StrUtil; -import cn.hutool.http.client.engine.jdk.HttpRequest; import cn.hutool.http.meta.Header; import java.util.ArrayList; @@ -15,7 +14,7 @@ import java.util.Map.Entry; /** * 全局头部信息
- * 所有Http请求将共用此全局头部信息,除非在{@link HttpRequest}中自定义头部信息覆盖之 + * 所有Http请求将共用此全局头部信息 * * @author looly */ 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 03aa7d0d0..9fd7e52fb 100755 --- a/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java @@ -1,34 +1,26 @@ package cn.hutool.http; import cn.hutool.core.codec.BaseN.Base64; -import cn.hutool.core.convert.Convert; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IoUtil; -import cn.hutool.core.io.StreamProgress; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.net.url.RFC3986; -import cn.hutool.core.net.url.URLEncoder; -import cn.hutool.core.net.url.UrlQuery; +import cn.hutool.core.net.url.UrlQueryUtil; import cn.hutool.core.regex.ReUtil; import cn.hutool.core.text.StrUtil; import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.ObjUtil; -import cn.hutool.http.client.HttpDownloader; +import cn.hutool.http.client.ClientConfig; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.Response; import cn.hutool.http.client.cookie.GlobalCookieManager; -import cn.hutool.http.client.engine.jdk.HttpRequest; +import cn.hutool.http.client.engine.ClientEngineFactory; import cn.hutool.http.meta.ContentType; import cn.hutool.http.meta.Method; import cn.hutool.http.server.SimpleServer; -import java.io.File; import java.io.InputStream; -import java.io.OutputStream; import java.net.CookieManager; import java.net.HttpURLConnection; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -69,52 +61,6 @@ public class HttpUtil { return StrUtil.startWithIgnoreCase(url, "http:"); } - /** - * 创建Http请求对象 - * - * @param method 方法枚举{@link Method} - * @param url 请求的URL,可以使HTTP或者HTTPS - * @return {@link HttpRequest} - * @since 3.0.9 - */ - public static HttpRequest createRequest(final Method method, final String url) { - return HttpRequest.of(url).method(method); - } - - /** - * 创建Http GET请求对象 - * - * @param url 请求的URL,可以使HTTP或者HTTPS - * @return {@link HttpRequest} - * @since 3.2.0 - */ - public static HttpRequest createGet(final String url) { - return createGet(url, false); - } - - /** - * 创建Http GET请求对象 - * - * @param url 请求的URL,可以使HTTP或者HTTPS - * @param isFollowRedirects 是否打开重定向 - * @return {@link HttpRequest} - * @since 5.6.4 - */ - public static HttpRequest createGet(final String url, final boolean isFollowRedirects) { - return HttpRequest.get(url).setFollowRedirects(isFollowRedirects); - } - - /** - * 创建Http POST请求对象 - * - * @param url 请求的URL,可以使HTTP或者HTTPS - * @return {@link HttpRequest} - * @since 3.2.0 - */ - public static HttpRequest createPost(final String url) { - return HttpRequest.post(url); - } - /** * 发送get请求 * @@ -124,7 +70,7 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String get(final String urlString, final Charset customCharset) { - return HttpRequest.get(urlString).charset(customCharset).execute().bodyStr(); + return send(Request.of(urlString).charset(customCharset)).bodyStr(); } /** @@ -147,7 +93,9 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String get(final String urlString, final int timeout) { - return HttpRequest.get(urlString).timeout(timeout).execute().bodyStr(); + return ClientEngineFactory.get() + .setConfig(ClientConfig.of().setConnectionTimeout(timeout).setReadTimeout(timeout)) + .send(Request.of(urlString)).bodyStr(); } /** @@ -159,21 +107,8 @@ public class HttpUtil { */ @SuppressWarnings("resource") public static String get(final String urlString, final Map paramMap) { - return HttpRequest.get(urlString).form(paramMap).execute().bodyStr(); - } - - /** - * 发送get请求 - * - * @param urlString 网址 - * @param paramMap post表单数据 - * @param timeout 超时时长,-1表示默认超时,单位毫秒 - * @return 返回数据 - * @since 3.3.0 - */ - @SuppressWarnings("resource") - public static String get(final String urlString, final Map paramMap, final int timeout) { - return HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().bodyStr(); + return send(Request.of(urlString).form(paramMap)) + .bodyStr(); } /** @@ -183,22 +118,10 @@ public class HttpUtil { * @param paramMap post表单数据 * @return 返回数据 */ + @SuppressWarnings("resource") public static String post(final String urlString, final Map paramMap) { - return post(urlString, paramMap, HttpGlobalConfig.getTimeout()); - } - - /** - * 发送post请求 - * - * @param urlString 网址 - * @param paramMap post表单数据 - * @param timeout 超时时长,-1表示默认超时,单位毫秒 - * @return 返回数据 - * @since 3.2.0 - */ - @SuppressWarnings("resource") - public static String post(final String urlString, final Map paramMap, final int timeout) { - return HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().bodyStr(); + return send(Request.of(urlString).method(Method.POST).form(paramMap)) + .bodyStr(); } /** @@ -214,426 +137,21 @@ public class HttpUtil { * @param body post表单数据 * @return 返回数据 */ + @SuppressWarnings("resource") public static String post(final String urlString, final String body) { - return post(urlString, body, HttpGlobalConfig.getTimeout()); + return send(Request.of(urlString).method(Method.POST).body(body)) + .bodyStr(); } /** - * 发送post请求
- * 请求体body参数支持两种类型: + * 使用默认HTTP引擎,发送HTTP请求 * - *
-	 * 1. 标准参数,例如 a=1&b=2 这种格式
-	 * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
-	 * 
- * - * @param urlString 网址 - * @param body post表单数据 - * @param timeout 超时时长,-1表示默认超时,单位毫秒 - * @return 返回数据 - * @since 3.2.0 + * @param request HTTP请求 + * @return HTTP响应 + * @see ClientEngineFactory#get()#send(Request) */ - @SuppressWarnings("resource") - public static String post(final String urlString, final String body, final int timeout) { - return HttpRequest.post(urlString).timeout(timeout).body(body).execute().bodyStr(); - } - - // ---------------------------------------------------------------------------------------- download - - /** - * 下载远程文本 - * - * @param url 请求的url - * @param customCharsetName 自定义的字符集 - * @return 文本 - */ - public static String downloadString(final String url, final String customCharsetName) { - return downloadString(url, CharsetUtil.charset(customCharsetName), null); - } - - /** - * 下载远程文本 - * - * @param url 请求的url - * @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换 - * @return 文本 - */ - public static String downloadString(final String url, final Charset customCharset) { - return downloadString(url, customCharset, null); - } - - /** - * 下载远程文本 - * - * @param url 请求的url - * @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换 - * @param streamPress 进度条 {@link StreamProgress} - * @return 文本 - */ - public static String downloadString(final String url, final Charset customCharset, final StreamProgress streamPress) { - return HttpDownloader.downloadString(url, customCharset, streamPress); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param dest 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @return 文件 - */ - public static File downloadFile(final String url, final String dest) { - return downloadFile(url, FileUtil.file(dest)); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @return 文件 - */ - public static File downloadFile(final String url, final File destFile) { - return downloadFile(url, destFile, null); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @param timeout 超时,单位毫秒,-1表示默认超时 - * @return 文件 - * @since 4.0.4 - */ - public static File downloadFile(final String url, final File destFile, final int timeout) { - return downloadFile(url, destFile, timeout, null); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @param streamProgress 进度条 - * @return 文件 - */ - public static File downloadFile(final String url, final File destFile, final StreamProgress streamProgress) { - return downloadFile(url, destFile, -1, streamProgress); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @param timeout 超时,单位毫秒,-1表示默认超时 - * @param streamProgress 进度条 - * @return 文件 - * @since 4.0.4 - */ - public static File downloadFile(final String url, final File destFile, final int timeout, final StreamProgress streamProgress) { - return HttpDownloader.downloadFile(url, destFile, timeout, streamProgress); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param dest 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @return 下载的文件对象 - * @since 5.4.1 - */ - public static File downloadFileFromUrl(final String url, final String dest) { - return downloadFileFromUrl(url, FileUtil.file(dest)); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @return 下载的文件对象 - * @since 5.4.1 - */ - public static File downloadFileFromUrl(final String url, final File destFile) { - return downloadFileFromUrl(url, destFile, null); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @param timeout 超时,单位毫秒,-1表示默认超时 - * @return 下载的文件对象 - * @since 5.4.1 - */ - public static File downloadFileFromUrl(final String url, final File destFile, final int timeout) { - return downloadFileFromUrl(url, destFile, timeout, null); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @param streamProgress 进度条 - * @return 下载的文件对象 - * @since 5.4.1 - */ - public static File downloadFileFromUrl(final String url, final File destFile, final StreamProgress streamProgress) { - return downloadFileFromUrl(url, destFile, -1, streamProgress); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @param timeout 超时,单位毫秒,-1表示默认超时 - * @param streamProgress 进度条 - * @return 下载的文件对象 - * @since 5.4.1 - */ - public static File downloadFileFromUrl(final String url, final File destFile, final int timeout, final StreamProgress streamProgress) { - return HttpDownloader.downloadForFile(url, destFile, timeout, streamProgress); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param out 将下载内容写到输出流中 {@link OutputStream} - * @param isCloseOut 是否关闭输出流 - * @return 文件大小 - */ - public static long download(final String url, final OutputStream out, final boolean isCloseOut) { - return download(url, out, isCloseOut, null); - } - - /** - * 下载远程文件 - * - * @param url 请求的url - * @param out 将下载内容写到输出流中 {@link OutputStream} - * @param isCloseOut 是否关闭输出流 - * @param streamProgress 进度条 - * @return 文件大小 - */ - public static long download(final String url, final OutputStream out, final boolean isCloseOut, final StreamProgress streamProgress) { - return HttpDownloader.download(url, out, isCloseOut, streamProgress); - } - - /** - * 下载远程文件数据,支持30x跳转 - * - * @param url 请求的url - * @return 文件数据 - * @since 5.3.6 - */ - public static byte[] downloadBytes(final String url) { - return HttpDownloader.downloadBytes(url); - } - - /** - * 将Map形式的Form表单数据转换为Url参数形式,会自动url编码键和值 - * - * @param paramMap 表单数据 - * @return url参数 - */ - public static String toParams(final Map paramMap) { - return toParams(paramMap, CharsetUtil.UTF_8); - } - - /** - * 将Map形式的Form表单数据转换为Url参数形式
- * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
- * 会自动url编码键和值
- * 此方法用于拼接URL中的Query部分,并不适用于POST请求中的表单 - * - *
-	 * key1=v1&key2=&key3=v3
-	 * 
- * - * @param paramMap 表单数据 - * @param charset 编码,{@code null} 表示不encode键值对 - * @return url参数 - * @see #toParams(Map, Charset, boolean) - */ - public static String toParams(final Map paramMap, final Charset charset) { - return toParams(paramMap, charset, false); - } - - /** - * 将Map形式的Form表单数据转换为Url参数形式
- * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
- * 会自动url编码键和值 - * - *
-	 * key1=v1&key2=&key3=v3
-	 * 
- * - * @param paramMap 表单数据 - * @param charset 编码,null表示不encode键值对 - * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+' - * @return url参数 - * @since 5.7.16 - */ - public static String toParams(final Map paramMap, final Charset charset, final boolean isFormUrlEncoded) { - return UrlQuery.of(paramMap, isFormUrlEncoded).build(charset); - } - - /** - * 对URL参数做编码,只编码键和值
- * 提供的值可以是url附带参数,但是不能只是url - * - *

注意,此方法只能标准化整个URL,并不适合于单独编码参数值

- * - * @param urlWithParams url和参数,可以包含url本身,也可以单独参数 - * @param charset 编码 - * @return 编码后的url和参数 - * @since 4.0.1 - */ - public static String encodeParams(final String urlWithParams, final Charset charset) { - if (StrUtil.isBlank(urlWithParams)) { - return StrUtil.EMPTY; - } - - String urlPart = null; // url部分,不包括问号 - String paramPart; // 参数部分 - final int pathEndPos = urlWithParams.indexOf('?'); - if (pathEndPos > -1) { - // url + 参数 - urlPart = StrUtil.subPre(urlWithParams, pathEndPos); - paramPart = StrUtil.subSuf(urlWithParams, pathEndPos + 1); - if (StrUtil.isBlank(paramPart)) { - // 无参数,返回url - return urlPart; - } - } else if (false == StrUtil.contains(urlWithParams, '=')) { - // 无参数的URL - return urlWithParams; - } else { - // 无URL的参数 - paramPart = urlWithParams; - } - - paramPart = normalizeParams(paramPart, charset); - - return StrUtil.isBlank(urlPart) ? paramPart : urlPart + "?" + paramPart; - } - - /** - * 标准化参数字符串,即URL中?后的部分 - * - *

注意,此方法只能标准化整个URL,并不适合于单独编码参数值

- * - * @param paramPart 参数字符串 - * @param charset 编码 - * @return 标准化的参数字符串 - * @since 4.5.2 - */ - public static String normalizeParams(final String paramPart, final Charset charset) { - if (StrUtil.isEmpty(paramPart)) { - return paramPart; - } - final StringBuilder builder = new StringBuilder(paramPart.length() + 16); - final int len = paramPart.length(); - String name = null; - int pos = 0; // 未处理字符开始位置 - char c; // 当前字符 - int i; // 当前字符位置 - for (i = 0; i < len; i++) { - c = paramPart.charAt(i); - if (c == '=') { // 键值对的分界点 - if (null == name) { - // 只有=前未定义name时被当作键值分界符,否则做为普通字符 - name = (pos == i) ? StrUtil.EMPTY : paramPart.substring(pos, i); - pos = i + 1; - } - } else if (c == '&') { // 参数对的分界点 - if (pos != i) { - if (null == name) { - // 对于像&a&这类无参数值的字符串,我们将name为a的值设为"" - name = paramPart.substring(pos, i); - builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('='); - } else { - builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=') - .append(RFC3986.QUERY_PARAM_VALUE.encode(paramPart.substring(pos, i), charset)).append('&'); - } - name = null; - } - pos = i + 1; - } - } - - // 结尾处理 - if (null != name) { - builder.append(URLEncoder.encodeQuery(name, charset)).append('='); - } - if (pos != i) { - if (null == name && pos > 0) { - builder.append('='); - } - builder.append(URLEncoder.encodeQuery(paramPart.substring(pos, i), charset)); - } - - // 以&结尾则去除之 - final int lastIndex = builder.length() - 1; - if ('&' == builder.charAt(lastIndex)) { - builder.delete(lastIndex, builder.length()); - } - return builder.toString(); - } - - /** - * 将URL参数解析为Map(也可以解析Post中的键值对参数) - * - * @param paramsStr 参数字符串(或者带参数的Path) - * @param charset 字符集 - * @return 参数Map - * @since 5.2.6 - */ - public static Map decodeParamMap(final String paramsStr, final Charset charset) { - final Map queryMap = UrlQuery.of(paramsStr, charset).getQueryMap(); - if (MapUtil.isEmpty(queryMap)) { - return MapUtil.empty(); - } - return Convert.toMap(String.class, String.class, queryMap); - } - - /** - * 将URL参数解析为Map(也可以解析Post中的键值对参数) - * - * @param paramsStr 参数字符串(或者带参数的Path) - * @param charset 字符集 - * @return 参数Map - */ - public static Map> decodeParams(final String paramsStr, final String charset) { - return decodeParams(paramsStr, CharsetUtil.charset(charset)); - } - - /** - * 将URL参数解析为Map(也可以解析Post中的键值对参数) - * - * @param paramsStr 参数字符串(或者带参数的Path) - * @param charset 字符集 - * @return 参数Map - * @since 5.2.6 - */ - public static Map> decodeParams(final String paramsStr, final Charset charset) { - final Map queryMap = UrlQuery.of(paramsStr, charset).getQueryMap(); - if (MapUtil.isEmpty(queryMap)) { - return MapUtil.empty(); - } - - final Map> params = new LinkedHashMap<>(); - queryMap.forEach((key, value) -> { - final List values = params.computeIfAbsent(StrUtil.str(key), k -> new ArrayList<>(1)); - // 一般是一个参数 - values.add(StrUtil.str(value)); - }); - return params; + public static Response send(final Request request){ + return ClientEngineFactory.get().send(request); } /** @@ -649,11 +167,11 @@ public class HttpUtil { public static String urlWithForm(String url, final Map form, final Charset charset, final boolean isEncodeParams) { if (isEncodeParams && StrUtil.contains(url, '?')) { // 在需要编码的情况下,如果url中已经有部分参数,则编码之 - url = encodeParams(url, charset); + url = UrlQueryUtil.encodeQuery(url, charset); } // url和参数是分别编码的 - return urlWithForm(url, toParams(form, charset), charset, false); + return urlWithForm(url, UrlQueryUtil.toQuery(form, charset), charset, false); } /** @@ -670,7 +188,7 @@ public class HttpUtil { // 无额外参数 if (StrUtil.contains(url, '?')) { // url中包含参数 - return isEncode ? encodeParams(url, charset) : url; + return isEncode ? UrlQueryUtil.encodeQuery(url, charset) : url; } return url; } @@ -680,7 +198,7 @@ public class HttpUtil { final int qmIndex = url.indexOf('?'); if (qmIndex > 0) { // 原URL带参数,则对这部分参数单独编码(如果选项为进行编码) - urlBuilder.append(isEncode ? encodeParams(url, charset) : url); + urlBuilder.append(isEncode ? UrlQueryUtil.encodeQuery(url, charset) : url); if (false == StrUtil.endWith(url, '&')) { // 已经带参数的情况下追加参数 urlBuilder.append('&'); @@ -693,7 +211,7 @@ public class HttpUtil { urlBuilder.append('?'); } } - urlBuilder.append(isEncode ? encodeParams(queryString, charset) : queryString); + urlBuilder.append(isEncode ? UrlQueryUtil.encodeQuery(queryString, charset) : queryString); return urlBuilder.toString(); } 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 index dbd94c587..d07d27a40 100755 --- a/hutool-http/src/main/java/cn/hutool/http/client/ClientConfig.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/ClientConfig.java @@ -46,11 +46,11 @@ public class ClientConfig { /** * 是否禁用缓存 */ - public boolean disableCache; + private boolean disableCache; /** * 代理 */ - public Proxy proxy; + private Proxy proxy; /** * 构造 diff --git a/hutool-http/src/main/java/cn/hutool/http/client/ClientEngine.java b/hutool-http/src/main/java/cn/hutool/http/client/ClientEngine.java index d485f8d61..1621d4090 100755 --- a/hutool-http/src/main/java/cn/hutool/http/client/ClientEngine.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/ClientEngine.java @@ -10,6 +10,13 @@ import java.io.Closeable; */ public interface ClientEngine extends Closeable { + /** + * 设置客户端引擎参数,如超时、代理等信息 + * @param config 客户端设置 + * @return this + */ + ClientEngine setConfig(ClientConfig config); + /** * 发送HTTP请求 * @param message HTTP请求消息 diff --git a/hutool-http/src/main/java/cn/hutool/http/client/HeaderOperation.java b/hutool-http/src/main/java/cn/hutool/http/client/HeaderOperation.java index 5bd7682ec..f7376880d 100755 --- a/hutool-http/src/main/java/cn/hutool/http/client/HeaderOperation.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/HeaderOperation.java @@ -47,7 +47,7 @@ public interface HeaderOperation> { * @return header值 */ default String header(final Header header) { - return header(header.name()); + return header(header.getValue()); } /** diff --git a/hutool-http/src/main/java/cn/hutool/http/client/HttpDownloader.java b/hutool-http/src/main/java/cn/hutool/http/client/HttpDownloader.java index ad58f5510..feb4d2ee4 100644 --- a/hutool-http/src/main/java/cn/hutool/http/client/HttpDownloader.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/HttpDownloader.java @@ -1,11 +1,10 @@ package cn.hutool.http.client; -import cn.hutool.core.io.stream.FastByteArrayOutputStream; import cn.hutool.core.io.StreamProgress; +import cn.hutool.core.io.stream.FastByteArrayOutputStream; import cn.hutool.core.lang.Assert; import cn.hutool.http.HttpException; -import cn.hutool.http.HttpUtil; -import cn.hutool.http.client.engine.jdk.HttpResponse; +import cn.hutool.http.client.engine.ClientEngineFactory; import java.io.File; import java.io.OutputStream; @@ -44,6 +43,29 @@ public class HttpDownloader { return requestDownload(url, -1).bodyBytes(); } + /** + * 下载远程文件 + * + * @param url 请求的url + * @param targetFileOrDir 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 + * @return 文件 + */ + public static File downloadFile(final String url, final File targetFileOrDir) { + return downloadFile(url, targetFileOrDir, -1); + } + + /** + * 下载远程文件 + * + * @param url 请求的url + * @param targetFileOrDir 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 + * @param timeout 超时,单位毫秒,-1表示默认超时 + * @return 文件 + */ + public static File downloadFile(final String url, final File targetFileOrDir, final int timeout) { + return downloadFile(url, targetFileOrDir, timeout, null); + } + /** * 下载远程文件 * @@ -74,19 +96,6 @@ public class HttpDownloader { return requestDownload(url, timeout).body().write(targetFileOrDir, tempFileSuffix, streamProgress); } - /** - * 下载远程文件,返回文件 - * - * @param url 请求的url - * @param targetFileOrDir 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名 - * @param timeout 超时,单位毫秒,-1表示默认超时 - * @param streamProgress 进度条 - * @return 文件 - */ - public static File downloadForFile(final String url, final File targetFileOrDir, final int timeout, final StreamProgress streamProgress) { - return requestDownload(url, timeout).body().write(targetFileOrDir, streamProgress); - } - /** * 下载远程文件 * @@ -110,12 +119,12 @@ public class HttpDownloader { * @return HttpResponse * @since 5.4.1 */ - private static HttpResponse requestDownload(final String url, final int timeout) { + private static Response requestDownload(final String url, final int timeout) { Assert.notBlank(url, "[url] is blank !"); - final HttpResponse response = HttpUtil.createGet(url, true) - .timeout(timeout) - .executeAsync(); + final Response response = ClientEngineFactory.get() + .setConfig(ClientConfig.of().setConnectionTimeout(timeout).setReadTimeout(timeout)) + .send(Request.of(url)); if (response.isOk()) { return response; 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 4be1df12a..b22c395a7 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 @@ -1,19 +1,23 @@ package cn.hutool.http.client; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.collection.ListUtil; 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.GlobalHeaders; import cn.hutool.http.HttpGlobalConfig; +import cn.hutool.http.HttpUtil; import cn.hutool.http.client.body.HttpBody; +import cn.hutool.http.client.body.StringBody; +import cn.hutool.http.client.body.UrlEncodedFormBody; import cn.hutool.http.meta.Header; import cn.hutool.http.meta.Method; import java.nio.charset.Charset; -import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -96,6 +100,9 @@ public class Request implements HeaderOperation { method = Method.GET; headers = new HashMap<>(); maxRedirectCount = HttpGlobalConfig.getMaxRedirectCount(); + + // 全局默认请求头 + header(GlobalHeaders.INSTANCE.headers(), false); } /** @@ -198,9 +205,7 @@ public class Request implements HeaderOperation { final List values = headers.get(name.trim()); if (isOverride || CollUtil.isEmpty(values)) { - final ArrayList valueList = new ArrayList<>(); - valueList.add(value); - headers.put(name.trim(), valueList); + headers.put(name.trim(), ListUtil.of(value)); } else { values.add(value.trim()); } @@ -216,6 +221,26 @@ public class Request implements HeaderOperation { return this.body; } + /** + * 添加请求表单内容 + * + * @param formMap 表单内容 + * @return this + */ + public Request form(final Map formMap) { + return body(new UrlEncodedFormBody(formMap, charset())); + } + + /** + * 添加字符串请求体 + * + * @param body 请求体 + * @return this + */ + public Request body(final String body) { + return body(new StringBody(body, charset())); + } + /** * 添加请求体 * @@ -227,7 +252,7 @@ public class Request implements HeaderOperation { // 根据内容赋值默认Content-Type if (StrUtil.isBlank(header(Header.CONTENT_TYPE))) { - header(Header.CONTENT_TYPE, body.getContentType(), true); + header(Header.CONTENT_TYPE, body.getContentType(charset()), true); } return this; @@ -253,4 +278,13 @@ public class Request implements HeaderOperation { this.maxRedirectCount = Math.max(maxRedirectCount, 0); return this; } + + /** + * 发送请求 + * + * @return 响应内容 + */ + public Response send() { + return HttpUtil.send(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 b8c59c4b6..39586c476 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 @@ -10,6 +10,9 @@ import cn.hutool.http.meta.Header; import java.io.Closeable; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; /** * 响应内容接口,包括响应状态码、HTTP消息头、响应体等信息 @@ -35,12 +38,19 @@ public interface Response extends Closeable { */ String header(final String name); + /** + * 获取headers + * + * @return Headers Map + */ + Map> headers(); + /** * 获取字符集编码,默认为响应头中的编码 * * @return 字符集 */ - default Charset charset(){ + default Charset charset() { return HttpUtil.getCharset(header(Header.CONTENT_TYPE)); } @@ -54,9 +64,10 @@ public interface Response extends Closeable { /** * 获取响应体,包含服务端返回的内容和Content-Type信息 + * * @return {@link ResponseBody} */ - default ResponseBody body(){ + default ResponseBody body() { return new ResponseBody(this, bodyStream(), false, true); } @@ -152,4 +163,13 @@ public interface Response extends Closeable { default String getCookieStr() { return header(Header.SET_COOKIE); } + + /** + * 链式处理结果 + * + * @param consumer {@link Consumer} + */ + default void then(final Consumer consumer) { + consumer.accept(this); + } } diff --git a/hutool-http/src/main/java/cn/hutool/http/client/body/HttpBody.java b/hutool-http/src/main/java/cn/hutool/http/client/body/HttpBody.java index 09ff0ca1c..aa755f8ef 100644 --- a/hutool-http/src/main/java/cn/hutool/http/client/body/HttpBody.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/body/HttpBody.java @@ -5,6 +5,7 @@ import cn.hutool.core.io.IoUtil; import java.io.InputStream; import java.io.OutputStream; +import java.nio.charset.Charset; /** * 定义请求体接口 @@ -25,6 +26,20 @@ public interface HttpBody { */ String getContentType(); + /** + * 获取指定编码的Content-Type,类似于:application/json;charset=UTF-8 + * @param charset 编码 + * @return Content-Type + */ + default String getContentType(final Charset charset){ + final String contentType = getContentType(); + if(null == contentType){ + return null; + } + + return contentType + ";charset=" + charset.name(); + } + /** * 写出并关闭{@link OutputStream} * diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/ClientEngineFactory.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/ClientEngineFactory.java index d29e8f259..511ec716b 100755 --- a/hutool-http/src/main/java/cn/hutool/http/client/engine/ClientEngineFactory.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/ClientEngineFactory.java @@ -25,7 +25,7 @@ public class ClientEngineFactory { } /** - * 根据用户引入的拼音引擎jar,自动创建对应的拼音引擎对象
+ * 根据用户引入的HTTP客户端引擎jar,自动创建对应的拼音引擎对象
* 推荐创建的引擎单例使用,此方法每次调用会返回新的引擎 * * @return {@code ClientEngine} 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 2d5d69d6c..830fd98f1 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 @@ -1,14 +1,18 @@ package cn.hutool.http.client.engine.httpclient4; +import cn.hutool.core.io.IoUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.http.GlobalHeaders; import cn.hutool.http.HttpException; +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 org.apache.http.Header; +import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import org.apache.http.impl.client.CloseableHttpClient; @@ -29,20 +33,27 @@ import java.util.Map; */ public class HttpClient4Engine implements ClientEngine { - private final CloseableHttpClient engine; + private ClientConfig config; + private CloseableHttpClient engine; /** * 构造 */ - public HttpClient4Engine() { - this.engine = HttpClients.custom() - // 设置默认头信息 - .setDefaultHeaders(toHeaderList(GlobalHeaders.INSTANCE.headers())) - .build(); + public HttpClient4Engine() {} + + @Override + public HttpClient4Engine setConfig(final ClientConfig config) { + this.config = config; + // 重置客户端 + IoUtil.close(this.engine); + this.engine = null; + return this; } @Override public Response send(final Request message) { + initEngine(); + final HttpEntityEnclosingRequestBase request = buildRequest(message); final CloseableHttpResponse response; try { @@ -64,6 +75,29 @@ public class HttpClient4Engine implements ClientEngine { this.engine.close(); } + /** + * 初始化引擎 + */ + private void initEngine(){ + if(null != this.engine){ + return; + } + + RequestConfig requestConfig = null; + if(null != this.config){ + requestConfig = RequestConfig.custom() + .setConnectTimeout(this.config.getConnectionTimeout()) + .setConnectionRequestTimeout(this.config.getConnectionTimeout()) + .build(); + } + + this.engine = HttpClients.custom() + // 设置默认头信息 + .setDefaultRequestConfig(requestConfig) + .setDefaultHeaders(toHeaderList(GlobalHeaders.INSTANCE.headers())) + .build(); + } + /** * 构建请求体 * 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 cea125b19..85790372c 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 @@ -13,6 +13,11 @@ import org.apache.http.util.EntityUtils; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** * HttpClient响应包装
@@ -59,6 +64,17 @@ public class HttpClient4Response implements Response { return null; } + @Override + public Map> headers() { + final Header[] headers = rawRes.getAllHeaders(); + final HashMap> result = new LinkedHashMap<>(headers.length, 1); + for (final Header header : headers) { + final List valueList = result.computeIfAbsent(header.getName(), k -> new ArrayList<>()); + valueList.add(header.getValue()); + } + return result; + } + @Override public long contentLength() { return rawRes.getEntity().getContentLength(); 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 7214ff404..cd4bf9546 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 @@ -1,16 +1,20 @@ package cn.hutool.http.client.engine.httpclient5; +import cn.hutool.core.io.IoUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.http.GlobalHeaders; import cn.hutool.http.HttpException; +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 org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.http.ClassicHttpRequest; import org.apache.hc.core5.http.Header; @@ -21,6 +25,7 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.TimeUnit; /** * Apache HttpClient5的HTTP请求引擎 @@ -30,20 +35,27 @@ import java.util.Map; */ public class HttpClient5Engine implements ClientEngine { - private final CloseableHttpClient engine; + private ClientConfig config; + private CloseableHttpClient engine; /** * 构造 */ - public HttpClient5Engine() { - this.engine = HttpClients.custom() - // 设置默认头信息 - .setDefaultHeaders(toHeaderList(GlobalHeaders.INSTANCE.headers())) - .build(); + public HttpClient5Engine() {} + + @Override + public HttpClient5Engine setConfig(final ClientConfig config) { + this.config = config; + // 重置客户端 + IoUtil.close(this.engine); + this.engine = null; + return this; } @Override public Response send(final Request message) { + initEngine(); + final ClassicHttpRequest request = buildRequest(message); final CloseableHttpResponse response; try { @@ -65,6 +77,33 @@ public class HttpClient5Engine implements ClientEngine { this.engine.close(); } + /** + * 初始化引擎 + */ + private void initEngine(){ + if(null != this.engine){ + return; + } + + RequestConfig requestConfig = null; + if(null != this.config){ + requestConfig = RequestConfig.custom() + .setConnectTimeout(this.config.getConnectionTimeout(), TimeUnit.MILLISECONDS) + .setConnectionRequestTimeout(this.config.getConnectionTimeout(), TimeUnit.MILLISECONDS) + .setResponseTimeout(this.config.getReadTimeout(), TimeUnit.MILLISECONDS) + .build(); + } + + final HttpClientBuilder builder = HttpClients.custom() + // 设置默认头信息 + .setDefaultRequestConfig(requestConfig) + .setDefaultHeaders(toHeaderList(GlobalHeaders.INSTANCE.headers())); + + // TODO 设置代理 + + this.engine = builder.build(); + } + /** * 构建请求体 * 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 3b48847f0..02baef3d2 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 @@ -13,6 +13,11 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** * HttpClient响应包装
@@ -59,6 +64,17 @@ public class HttpClient5Response implements Response { return null; } + @Override + public Map> headers() { + final Header[] headers = rawRes.getHeaders(); + final HashMap> result = new LinkedHashMap<>(headers.length, 1); + for (final Header header : headers) { + final List valueList = result.computeIfAbsent(header.getName(), k -> new ArrayList<>()); + valueList.add(header.getValue()); + } + return result; + } + @Override public long contentLength() { return rawRes.getEntity().getContentLength(); 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 deleted file mode 100755 index d3500251d..000000000 --- a/hutool-http/src/main/java/cn/hutool/http/client/engine/jdk/HttpRequest.java +++ /dev/null @@ -1,1222 +0,0 @@ -package cn.hutool.http.client.engine.jdk; - -import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.convert.Convert; -import cn.hutool.core.io.IORuntimeException; -import cn.hutool.core.io.resource.BytesResource; -import cn.hutool.core.io.resource.FileResource; -import cn.hutool.core.io.resource.MultiFileResource; -import cn.hutool.core.io.resource.Resource; -import cn.hutool.core.lang.Assert; -import cn.hutool.core.map.MapUtil; -import cn.hutool.core.map.TableMap; -import cn.hutool.core.net.ssl.SSLUtil; -import cn.hutool.core.net.url.UrlBuilder; -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.GlobalHeaders; -import cn.hutool.http.HttpConfig; -import cn.hutool.http.HttpException; -import cn.hutool.http.HttpGlobalConfig; -import cn.hutool.http.HttpUtil; -import cn.hutool.http.client.body.BytesBody; -import cn.hutool.http.client.body.MultipartBody; -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.HttpURLConnection; -import java.net.Proxy; -import java.net.URLStreamHandler; -import java.nio.charset.Charset; -import java.util.Map; -import java.util.function.Consumer; -import java.util.function.Function; - -/** - * http请求类
- * Http请求类用于构建Http请求并同步获取结果,此类通过CookieManager持有域名对应的Cookie值,再次请求时会自动附带Cookie信息 - * - * @author Looly - */ -@Deprecated -public class HttpRequest extends HttpBase { - - // ---------------------------------------------------------------- static Http Method start - - /** - * POST请求 - * - * @param url URL - * @return HttpRequest - */ - public static HttpRequest post(final String url) { - return of(url).method(Method.POST); - } - - /** - * GET请求 - * - * @param url URL - * @return HttpRequest - */ - public static HttpRequest get(final String url) { - return of(url).method(Method.GET); - } - - /** - * HEAD请求 - * - * @param url URL - * @return HttpRequest - */ - public static HttpRequest head(final String url) { - return of(url).method(Method.HEAD); - } - - /** - * OPTIONS请求 - * - * @param url URL - * @return HttpRequest - */ - public static HttpRequest options(final String url) { - return of(url).method(Method.OPTIONS); - } - - /** - * PUT请求 - * - * @param url URL - * @return HttpRequest - */ - public static HttpRequest put(final String url) { - return of(url).method(Method.PUT); - } - - /** - * PATCH请求 - * - * @param url URL - * @return HttpRequest - * @since 3.0.9 - */ - public static HttpRequest patch(final String url) { - return of(url).method(Method.PATCH); - } - - /** - * DELETE请求 - * - * @param url URL - * @return HttpRequest - */ - public static HttpRequest delete(final String url) { - return of(url).method(Method.DELETE); - } - - /** - * TRACE请求 - * - * @param url URL - * @return HttpRequest - */ - public static HttpRequest trace(final String url) { - return of(url).method(Method.TRACE); - } - - /** - * 构建一个HTTP请求
- * 对于传入的URL,可以自定义是否解码已经编码的内容,设置见{@link HttpGlobalConfig#setDecodeUrl(boolean)}
- * 在构建Http请求时,用户传入的URL可能有编码后和未编码的内容混合在一起,如果{@link HttpGlobalConfig#isDecodeUrl()}为{@code true},则会统一解码编码后的参数,
- * 按照RFC3986规范,在发送请求时,全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。 - * - * @param url URL链接,默认自动编码URL中的参数等信息 - * @return HttpRequest - * @since 5.7.18 - */ - public static HttpRequest of(final String url) { - return of(url, HttpGlobalConfig.isDecodeUrl() ? DEFAULT_CHARSET : null); - } - - /** - * 构建一个HTTP请求
- * 对于传入的URL,可以自定义是否解码已经编码的内容。
- * 在构建Http请求时,用户传入的URL可能有编码后和未编码的内容混合在一起,如果charset参数不为{@code null},则会统一解码编码后的参数,
- * 按照RFC3986规范,在发送请求时,全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。 - * - * @param url URL链接 - * @param charset 编码,如果为{@code null}不自动解码编码URL - * @return HttpRequest - * @since 5.7.18 - */ - public static HttpRequest of(final String url, final Charset charset) { - return of(UrlBuilder.ofHttp(url, charset)); - } - - /** - * 构建一个HTTP请求
- * - * @param url {@link UrlBuilder} - * @return HttpRequest - * @since 5.8.0 - */ - public static HttpRequest of(final UrlBuilder url) { - return new HttpRequest(url); - } - - /** - * 设置全局默认的连接和读取超时时长 - * - * @param customTimeout 超时时长 - * @see HttpGlobalConfig#setTimeout(int) - * @since 4.6.2 - */ - public static void setGlobalTimeout(final int customTimeout) { - HttpGlobalConfig.setTimeout(customTimeout); - } - - /** - * 获取Cookie管理器,用于自定义Cookie管理 - * - * @return {@link CookieManager} - * @see GlobalCookieManager#getCookieManager() - * @since 4.1.0 - */ - public static CookieManager getCookieManager() { - return GlobalCookieManager.getCookieManager(); - } - - /** - * 自定义{@link CookieManager} - * - * @param customCookieManager 自定义的{@link CookieManager} - * @see GlobalCookieManager#setCookieManager(CookieManager) - * @since 4.5.14 - */ - public static void setCookieManager(final CookieManager customCookieManager) { - GlobalCookieManager.setCookieManager(customCookieManager); - } - - /** - * 关闭Cookie - * - * @see GlobalCookieManager#setCookieManager(CookieManager) - * @since 4.1.9 - */ - public static void closeCookie() { - GlobalCookieManager.setCookieManager(null); - } - // ---------------------------------------------------------------- static Http Method end - - private HttpConfig config = HttpConfig.of(); - private UrlBuilder url; - private URLStreamHandler urlHandler; - private Method method = Method.GET; - /** - * 连接对象 - */ - private HttpConnection httpConnection; - - /** - * 存储表单数据 - */ - private Map form; - /** - * Cookie - */ - private String cookie; - /** - * 是否为Multipart表单 - */ - private boolean isMultiPart; - /** - * 是否是REST请求模式 - */ - private boolean isRest; - /** - * 重定向次数计数器,内部使用 - */ - private int redirectCount; - - /** - * 构造 - * - * @param url {@link UrlBuilder} - */ - public HttpRequest(final UrlBuilder url) { - this.url = Assert.notNull(url, "URL must be not null!"); - // 给定默认URL编码 - final Charset charset = url.getCharset(); - if (null != charset) { - this.charset(charset); - } - // 给定一个默认头信息 - this.header(GlobalHeaders.INSTANCE.headers()); - } - - /** - * 获取请求URL - * - * @return URL字符串 - * @since 4.1.8 - */ - public String getUrl() { - return url.toString(); - } - - /** - * 设置URL - * - * @param url url字符串 - * @return this - * @since 4.1.8 - */ - public HttpRequest setUrl(final String url) { - return setUrl(UrlBuilder.ofHttp(url, this.charset)); - } - - /** - * 设置URL - * - * @param urlBuilder url字符串 - * @return this - * @since 5.3.1 - */ - public HttpRequest setUrl(final UrlBuilder urlBuilder) { - this.url = urlBuilder; - return this; - } - - /** - * 设置{@link URLStreamHandler} - *

- * 部分环境下需要单独设置此项,例如当 WebLogic Server 实例充当 SSL 客户端角色(它会尝试通过 SSL 连接到其他服务器或应用程序)时,
- * 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配,则删除此连接。
- * 因此weblogic不支持https的sni协议的主机名验证,此时需要将此值设置为sun.net.www.protocol.https.Handler对象。 - *

- * 相关issue见:https://gitee.com/dromara/hutool/issues/IMD1X - * - * @param urlHandler {@link URLStreamHandler} - * @return this - * @since 4.1.9 - */ - public HttpRequest setUrlHandler(final URLStreamHandler urlHandler) { - this.urlHandler = urlHandler; - return this; - } - - /** - * 获取{@link HttpConnection}
- * 在{@link #execute()} 执行前此对象为null - * - * @return {@link HttpConnection} - * @since 4.2.2 - */ - public HttpConnection getConnection() { - return this.httpConnection; - } - - /** - * 获取Http请求方法 - * - * @return {@link Method} - * @since 4.1.8 - */ - public Method getMethod() { - return this.method; - } - - /** - * 设置请求方法 - * - * @param method HTTP方法 - * @return HttpRequest - */ - public HttpRequest method(final Method method) { - this.method = method; - return this; - } - - // ---------------------------------------------------------------- Http Request Header start - - /** - * @return 获取是否为长连接 - */ - public boolean isKeepAlive() { - final String connection = header(Header.CONNECTION); - if (connection == null) { - return false == HTTP_1_0.equalsIgnoreCase(httpVersion); - } - - return false == "close".equalsIgnoreCase(connection); - } - - /** - * 获取内容长度 - * - * @return String - */ - public String contentLength() { - return header(Header.CONTENT_LENGTH); - } - - /** - * 设置内容长度 - * - * @param value 长度 - * @return HttpRequest - */ - public HttpRequest contentLength(final int value) { - header(Header.CONTENT_LENGTH, String.valueOf(value)); - return this; - } - - /** - * 设置Cookie
- * 自定义Cookie后会覆盖Hutool的默认Cookie行为 - * - * @param cookie Cookie值,如果为{@code null}则设置无效,使用默认Cookie行为 - * @return this - * @since 3.0.7 - */ - public HttpRequest cookie(final String cookie) { - this.cookie = cookie; - return this; - } - // ---------------------------------------------------------------- Http Request Header end - - // ---------------------------------------------------------------- Form start - - /** - * 设置表单数据
- * - * @param name 名 - * @param value 值 - * @return this - */ - public HttpRequest form(final String name, final Object value) { - if (StrUtil.isBlank(name) || ObjUtil.isNull(value)) { - return this; // 忽略非法的form表单项内容; - } - - // 停用body - this.bodyBytes = null; - - if (value instanceof File) { - // 文件上传 - return this.form(name, (File) value); - } - - if (value instanceof Resource) { - return form(name, (Resource) value); - } - - // 普通值 - final String strValue; - if (value instanceof Iterable) { - // 列表对象 - strValue = CollUtil.join((Iterable) value, ","); - } else if (ArrayUtil.isArray(value)) { - if (File.class == ArrayUtil.getComponentType(value)) { - // 多文件 - return this.form(name, (File[]) value); - } - // 数组对象 - strValue = ArrayUtil.join((Object[]) value, ","); - } else { - // 其他对象一律转换为字符串 - strValue = Convert.toStr(value, null); - } - - return putToForm(name, strValue); - } - - /** - * 设置表单数据 - * - * @param name 名 - * @param value 值 - * @param parameters 参数对,奇数为名,偶数为值 - * @return this - */ - public HttpRequest form(final String name, final Object value, final Object... parameters) { - form(name, value); - - for (int i = 0; i < parameters.length; i += 2) { - form(parameters[i].toString(), parameters[i + 1]); - } - return this; - } - - /** - * 设置map类型表单数据 - * - * @param formMap 表单内容 - * @return this - */ - public HttpRequest form(final Map formMap) { - if (MapUtil.isNotEmpty(formMap)) { - formMap.forEach(this::form); - } - return this; - } - - /** - * 设置map<String, String>类型表单数据 - * - * @param formMapStr 表单内容 - * @return this - * @since 5.6.7 - */ - public HttpRequest formStr(final Map formMapStr) { - if (MapUtil.isNotEmpty(formMapStr)) { - formMapStr.forEach(this::form); - } - return this; - } - - /** - * 文件表单项
- * 一旦有文件加入,表单变为multipart/form-data - * - * @param name 名 - * @param files 需要上传的文件,为空跳过 - * @return this - */ - public HttpRequest form(final String name, final File... files) { - if (ArrayUtil.isEmpty(files)) { - return this; - } - if (1 == files.length) { - final File file = files[0]; - return form(name, file, file.getName()); - } - return form(name, new MultiFileResource(files)); - } - - /** - * 文件表单项
- * 一旦有文件加入,表单变为multipart/form-data - * - * @param name 名 - * @param file 需要上传的文件 - * @return this - */ - public HttpRequest form(final String name, final File file) { - return form(name, file, file.getName()); - } - - /** - * 文件表单项
- * 一旦有文件加入,表单变为multipart/form-data - * - * @param name 名 - * @param file 需要上传的文件 - * @param fileName 文件名,为空使用文件默认的文件名 - * @return this - */ - public HttpRequest form(final String name, final File file, final String fileName) { - if (null != file) { - form(name, new FileResource(file, fileName)); - } - return this; - } - - /** - * 文件byte[]表单项
- * 一旦有文件加入,表单变为multipart/form-data - * - * @param name 名 - * @param fileBytes 需要上传的文件 - * @param fileName 文件名 - * @return this - * @since 4.1.0 - */ - public HttpRequest form(final String name, final byte[] fileBytes, final String fileName) { - if (null != fileBytes) { - form(name, new BytesResource(fileBytes, fileName)); - } - return this; - } - - /** - * 文件表单项
- * 一旦有文件加入,表单变为multipart/form-data - * - * @param name 名 - * @param resource 数据源,文件可以使用{@link FileResource}包装使用 - * @return this - * @since 4.0.9 - */ - public HttpRequest form(final String name, final Resource resource) { - if (null != resource) { - if (false == isKeepAlive()) { - keepAlive(true); - } - - this.isMultiPart = true; - return putToForm(name, resource); - } - return this; - } - - /** - * 获取表单数据 - * - * @return 表单Map - */ - public Map form() { - return this.form; - } - - /** - * 获取文件表单数据 - * - * @return 文件表单Map - * @since 3.3.0 - */ - public Map fileForm() { - final Map result = MapUtil.newHashMap(); - this.form.forEach((key, value) -> { - if (value instanceof Resource) { - result.put(key, (Resource) value); - } - }); - return result; - } - // ---------------------------------------------------------------- Form end - - // ---------------------------------------------------------------- Body start - - /** - * 设置内容主体
- * 请求体body参数支持两种类型: - * - *

-	 * 1. 标准参数,例如 a=1&b=2 这种格式
-	 * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
-	 * 
- * - * @param body 请求体 - * @return this - */ - public HttpRequest body(final String body) { - return this.body(body, null); - } - - /** - * 设置内容主体
- * 请求体body参数支持两种类型: - * - *
-	 * 1. 标准参数,例如 a=1&b=2 这种格式
-	 * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
-	 * 
- * - * @param body 请求体 - * @param contentType 请求体类型,{@code null}表示自动判断类型 - * @return this - */ - public HttpRequest body(final String body, String contentType) { - final byte[] bytes = StrUtil.bytes(body, this.charset); - body(bytes); - this.form = null; // 当使用body时,停止form的使用 - - if (null != contentType) { - // Content-Type自定义设置 - this.contentType(contentType); - } else { - // 在用户未自定义的情况下自动根据内容判断 - contentType = HttpUtil.getContentTypeByRequestBody(body); - if (null != contentType && ContentType.isDefault(this.header(Header.CONTENT_TYPE))) { - if (null != this.charset) { - // 附加编码信息 - contentType = ContentType.build(contentType, this.charset); - } - this.contentType(contentType); - } - } - - // 判断是否为rest请求 - if (StrUtil.containsAnyIgnoreCase(contentType, "json", "xml")) { - this.isRest = true; - contentLength(bytes.length); - } - return this; - } - - /** - * 设置主体字节码
- * 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8 - * - * @param bodyBytes 主体 - * @return this - */ - public HttpRequest body(final byte[] bodyBytes) { - if (null != bodyBytes) { - this.bodyBytes = bodyBytes; - } - return this; - } - // ---------------------------------------------------------------- Body end - - /** - * 将新的配置加入
- * 注意加入的配置可能被修改 - * - * @param config 配置 - * @return this - */ - public HttpRequest setConfig(final HttpConfig config) { - this.config = config; - return this; - } - - /** - * 设置超时,单位:毫秒
- * 超时包括: - * - *
-	 * 1. 连接超时
-	 * 2. 读取响应超时
-	 * 
- * - * @param milliseconds 超时毫秒数 - * @return this - * @see #setConnectionTimeout(int) - * @see #setReadTimeout(int) - */ - public HttpRequest timeout(final int milliseconds) { - config.timeout(milliseconds); - return this; - } - - /** - * 设置连接超时,单位:毫秒 - * - * @param milliseconds 超时毫秒数 - * @return this - * @since 4.5.6 - */ - public HttpRequest setConnectionTimeout(final int milliseconds) { - config.setConnectionTimeout(milliseconds); - return this; - } - - /** - * 设置连接超时,单位:毫秒 - * - * @param milliseconds 超时毫秒数 - * @return this - * @since 4.5.6 - */ - public HttpRequest setReadTimeout(final int milliseconds) { - config.setReadTimeout(milliseconds); - return this; - } - - /** - * 禁用缓存 - * - * @return this - */ - public HttpRequest disableCache() { - config.disableCache(); - return this; - } - - /** - * 设置是否打开重定向,如果打开默认重定向次数为2
- * 此方法效果与{@link #setMaxRedirectCount(int)} 一致 - * - *

- * 需要注意的是,当设置为{@code true}时,如果全局重定向次数非0,直接复用,否则设置默认2次。
- * 当设置为{@code false}时,无论全局是否设置次数,都设置为0。
- * 不调用此方法的情况下,使用全局默认的次数。 - *

- * - * @param isFollowRedirects 是否打开重定向 - * @return this - */ - public HttpRequest setFollowRedirects(final boolean isFollowRedirects) { - if (isFollowRedirects) { - if (config.maxRedirectCount <= 0) { - // 默认两次跳转 - return setMaxRedirectCount(2); - } - } else { - // 手动强制关闭重定向,此时不受全局重定向设置影响 - if (config.maxRedirectCount < 0) { - return setMaxRedirectCount(0); - } - } - return this; - } - - /** - * 设置最大重定向次数
- * 如果次数小于1则表示不重定向,大于等于1表示打开重定向 - * - * @param maxRedirectCount 最大重定向次数 - * @return this - * @since 3.3.0 - */ - public HttpRequest setMaxRedirectCount(final int maxRedirectCount) { - config.setMaxRedirectCount(maxRedirectCount); - return this; - } - - /** - * 设置域名验证器
- * 只针对HTTPS请求,如果不设置,不做验证,所有域名被信任 - * - * @param hostnameVerifier HostnameVerifier - * @return this - */ - public HttpRequest setHostnameVerifier(final HostnameVerifier hostnameVerifier) { - config.setHostnameVerifier(hostnameVerifier); - return this; - } - - /** - * 设置Http代理 - * - * @param host 代理 主机 - * @param port 代理 端口 - * @return this - * @since 5.4.5 - */ - public HttpRequest setHttpProxy(final String host, final int port) { - config.setHttpProxy(host, port); - return this; - } - - /** - * 设置代理 - * - * @param proxy 代理 {@link Proxy} - * @return this - */ - public HttpRequest setProxy(final Proxy proxy) { - config.setProxy(proxy); - return this; - } - - /** - * 设置SSLSocketFactory
- * 只针对HTTPS请求,如果不设置,使用默认的SSLSocketFactory
- * 默认SSLSocketFactory为:SSLSocketFactoryBuilder.create().build(); - * - * @param ssf SSLScketFactory - * @return this - */ - public HttpRequest setSSLSocketFactory(final SSLSocketFactory ssf) { - config.setSSLSocketFactory(ssf); - return this; - } - - /** - * 设置HTTPS安全连接协议,只针对HTTPS请求,可以使用的协议包括:
- * 此方法调用后{@link #setSSLSocketFactory(SSLSocketFactory)} 将被覆盖。 - * - *
-	 * 1. TLSv1.2
-	 * 2. TLSv1.1
-	 * 3. SSLv3
-	 * ...
-	 * 
- * - * @param protocol 协议 - * @return this - * @see SSLUtil#createSSLContext(String) - * @see #setSSLSocketFactory(SSLSocketFactory) - */ - public HttpRequest setSSLProtocol(final String protocol) { - config.setSSLProtocol(protocol); - return this; - } - - /** - * 设置是否rest模式
- * rest模式下get请求不会把参数附加到URL之后 - * - * @param isRest 是否rest模式 - * @return this - * @since 4.5.0 - */ - public HttpRequest setRest(final boolean isRest) { - this.isRest = isRest; - return this; - } - - /** - * 采用流方式上传数据,无需本地缓存数据。
- * HttpUrlConnection默认是将所有数据读到本地缓存,然后再发送给服务器,这样上传大文件时就会导致内存溢出。 - * - * @param blockSize 块大小(bytes数),0或小于0表示不设置Chuncked模式 - * @return this - * @since 4.6.5 - */ - public HttpRequest setChunkedStreamingMode(final int blockSize) { - config.setBlockSize(blockSize); - return this; - } - - /** - * 执行Reuqest请求 - * - * @return this - */ - public HttpResponse execute() { - return this.execute(false); - } - - /** - * 异步请求
- * 异步请求后获取的{@link HttpResponse} 为异步模式,执行完此方法后发送请求到服务器,但是并不立即读取响应内容。
- * 此时保持Http连接不关闭,直调用获取内容方法为止。 - * - *

- * 一般执行完execute之后会把响应内容全部读出来放在一个 byte数组里,如果你响应的内容太多内存就爆了,此法是发送完请求不直接读响应内容,等有需要的时候读。 - * - * @return 异步对象,使用get方法获取HttpResponse对象 - */ - public HttpResponse executeAsync() { - return this.execute(true); - } - - /** - * 执行Reuqest请求 - * - * @param isAsync 是否异步 - * @return this - */ - public HttpResponse execute(final boolean isAsync) { - return doExecute(isAsync); - } - - /** - * 执行Request请求后,对响应内容后续处理
- * 处理结束后关闭连接 - * - * @param consumer 响应内容处理函数 - * @since 5.7.8 - */ - public void then(final Consumer consumer) { - thenFunction(httpResponse -> { - consumer.accept(httpResponse); - return null; - }); - } - - /** - * 执行Request请求后,对响应内容后续处理
- * 处理结束后关闭连接 - * - * @param 结果类型 - * @param function 响应内容处理函数 - * @return 结果值 - * @since 5.8.5 - */ - public T thenFunction(final Function function) { - try (final HttpResponse response = execute(true)) { - return function.apply(response); - } - } - - /** - * 简单验证,生成的头信息类似于: - *

-	 * Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
-	 * 
- * - * @param username 用户名 - * @param password 密码 - * @return this - */ - public HttpRequest basicAuth(final String username, final String password) { - return auth(HttpUtil.buildBasicAuth(username, password, charset)); - } - - /** - * 简单代理验证,生成的头信息类似于: - *
-	 * Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
-	 * 
- * - * @param username 用户名 - * @param password 密码 - * @return this - * @since 5.4.6 - */ - public HttpRequest basicProxyAuth(final String username, final String password) { - return proxyAuth(HttpUtil.buildBasicAuth(username, password, charset)); - } - - @Override - public String toString() { - final StringBuilder sb = StrUtil.builder(); - sb.append("Request Url: ").append(this.url.setCharset(this.charset)).append(StrUtil.CRLF); - sb.append(super.toString()); - return sb.toString(); - } - - // ---------------------------------------------------------------- Private method start - - /** - * 执行Reuqest请求 - * - * @param isAsync 是否异步 - * @return this - */ - private HttpResponse doExecute(final boolean isAsync) { - // 初始化URL - urlWithParamIfGet(); - // 初始化 connection - initConnection(); - // 发送请求 - send(); - - // 手动实现重定向 - HttpResponse httpResponse = sendRedirectIfPossible(isAsync); - - // 获取响应 - if (null == httpResponse) { - httpResponse = new HttpResponse(this.httpConnection, this.config.isIgnoreEOFError(), this.charset, isAsync, isIgnoreResponseBody()); - } - - return httpResponse; - } - - /** - * 初始化网络连接 - */ - private void initConnection() { - if (null != this.httpConnection) { - // 执行下次请求时自动关闭上次请求(常用于转发) - this.httpConnection.disconnectQuietly(); - } - - this.httpConnection = HttpConnection - // issue#I50NHQ - // 在生成正式URL前,设置自定义编码 - .of(this.url.setCharset(this.charset).toURL(this.urlHandler), config.proxy)// - .setConnectTimeout(config.connectionTimeout)// - .setReadTimeout(config.readTimeout)// - .setMethod(this.method)// - .setHttpsInfo(config.hostnameVerifier, config.ssf)// - // 关闭JDK自动转发,采用手动转发方式 - .setInstanceFollowRedirects(false) - // 流方式上传数据 - .setChunkedStreamingMode(config.blockSize) - // 覆盖默认Header - .header(this.headers, true); - - if (null != this.cookie) { - // 当用户自定义Cookie时,全局Cookie自动失效 - this.httpConnection.cookie(this.cookie); - } else { - // 读取全局Cookie信息并附带到请求中 - GlobalCookieManager.add(this.httpConnection); - } - - // 是否禁用缓存 - this.httpConnection.setDisableCache(config.isDisableCache); - } - - /** - * 对于GET请求将参数加到URL中
- * 此处不对URL中的特殊字符做单独编码
- * 对于非rest的GET请求,且处于重定向时,参数丢弃 - */ - private void urlWithParamIfGet() { - if (Method.GET.equals(method) && false == this.isRest && this.redirectCount <= 0) { - UrlQuery query = this.url.getQuery(); - if (null == query) { - query = new UrlQuery(); - this.url.setQuery(query); - } - - // 优先使用body形式的参数,不存在使用form - if (ArrayUtil.isNotEmpty(this.bodyBytes)) { - query.parse(StrUtil.str(this.bodyBytes, this.charset), this.charset); - } else { - query.addAll(this.form); - } - } - } - - /** - * 调用转发,如果需要转发返回转发结果,否则返回{@code null} - * - * @param isAsync 是否异步 - * @return {@link HttpResponse},无转发返回 {@code null} - */ - private HttpResponse sendRedirectIfPossible(final boolean isAsync) { - // 手动实现重定向 - if (config.maxRedirectCount > 0) { - final int code; - try { - code = httpConnection.getCode(); - } catch (final IOException e) { - // 错误时静默关闭连接 - this.httpConnection.disconnectQuietly(); - throw new HttpException(e); - } - - if (code != HttpURLConnection.HTTP_OK) { - if (HttpStatus.isRedirected(code)) { - - final UrlBuilder redirectUrl; - String location = httpConnection.header(Header.LOCATION); - if (false == HttpUtil.isHttp(location) && false == HttpUtil.isHttps(location)) { - // issue#I5TPSY - // location可能为相对路径 - if (false == location.startsWith("/")) { - location = StrUtil.addSuffixIfNot(this.url.getPathStr(), "/") + location; - } - redirectUrl = UrlBuilder.of(this.url.getScheme(), this.url.getHost(), this.url.getPort() - , location, null, null, this.charset); - } else { - redirectUrl = UrlBuilder.ofHttpWithoutEncode(location); - } - - setUrl(redirectUrl); - if (redirectCount < config.maxRedirectCount) { - redirectCount++; - return doExecute(isAsync); - } - } - } - } - return null; - } - - /** - * 发送数据流 - * - * @throws IORuntimeException IO异常 - */ - private void send() throws IORuntimeException { - try { - if (Method.POST.equals(this.method) // - || Method.PUT.equals(this.method) // - || Method.DELETE.equals(this.method) // - || this.isRest) { - if (isMultipart()) { - sendMultipart(); // 文件上传表单 - } else { - sendFormUrlEncoded();// 普通表单 - } - } else { - this.httpConnection.connect(); - } - } catch (final IOException e) { - // 异常时关闭连接 - this.httpConnection.disconnectQuietly(); - throw new IORuntimeException(e); - } - } - - /** - * 发送普通表单
- * 发送数据后自动关闭输出流 - * - * @throws IOException IO异常 - */ - private void sendFormUrlEncoded() throws IOException { - if (StrUtil.isBlank(this.header(Header.CONTENT_TYPE))) { - // 如果未自定义Content-Type,使用默认的application/x-www-form-urlencoded - this.httpConnection.header(Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(this.charset), true); - } - - // Write的时候会优先使用body中的内容,write时自动关闭OutputStream - final HttpBody body; - if (ArrayUtil.isNotEmpty(this.bodyBytes)) { - body = BytesBody.of(this.bodyBytes); - } else { - body = UrlEncodedFormBody.of(this.form, this.charset); - } - body.writeClose(this.httpConnection.getOutputStream()); - } - - /** - * 发送多组件请求(例如包含文件的表单)
- * 发送数据后自动关闭输出流 - * - * @throws IOException IO异常 - */ - private void sendMultipart() throws IOException { - final MultipartBody multipartBody = MultipartBody.of(this.form, this.charset); - //设置表单类型为Multipart(文件上传) - this.httpConnection.header(Header.CONTENT_TYPE, multipartBody.getContentType(), true); - multipartBody.writeClose(this.httpConnection.getOutputStream()); - } - - /** - * 是否忽略读取响应body部分
- * HEAD、CONNECT、OPTIONS、TRACE方法将不读取响应体 - * - * @return 是否需要忽略响应body部分 - * @since 3.1.2 - */ - private boolean isIgnoreResponseBody() { - return Method.HEAD == this.method // - || Method.CONNECT == this.method // - || Method.OPTIONS == this.method // - || Method.TRACE == this.method; - } - - /** - * 判断是否为multipart/form-data表单,条件如下: - * - *
-	 *     1. 存在资源对象(fileForm非空)
-	 *     2. 用户自定义头为multipart/form-data开头
-	 * 
- * - * @return 是否为multipart/form-data表单 - * @since 5.3.5 - */ - private boolean isMultipart() { - if (this.isMultiPart) { - return true; - } - - final String contentType = header(Header.CONTENT_TYPE); - return StrUtil.isNotEmpty(contentType) && - contentType.startsWith(ContentType.MULTIPART.getValue()); - } - - /** - * 将参数加入到form中,如果form为空,新建之。 - * - * @param name 表单属性名 - * @param value 属性值 - * @return this - */ - private HttpRequest putToForm(final String name, final Object value) { - if (null == name || null == value) { - return this; - } - if (null == this.form) { - this.form = new TableMap<>(16); - } - this.form.put(name, value); - return this; - } - // ---------------------------------------------------------------- Private method end - -} 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 5240a651a..d22002d39 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 @@ -95,6 +95,7 @@ public class HttpResponse implements Response, Closeable { * * @return Headers Map */ + @Override public Map> headers() { return Collections.unmodifiableMap(headers); } @@ -234,7 +235,6 @@ public class HttpResponse implements Response, Closeable { * * @throws HttpException IO异常 */ - @SuppressWarnings("resource") private void init(final boolean isAsync, final boolean isIgnoreBody) throws HttpException { // 获取响应状态码 try { 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 index b6e570438..0541fbf60 100755 --- 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 @@ -3,6 +3,7 @@ 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.core.util.ObjUtil; import cn.hutool.http.HttpException; import cn.hutool.http.HttpUtil; import cn.hutool.http.client.ClientConfig; @@ -25,7 +26,7 @@ import java.net.HttpURLConnection; */ public class JdkClientEngine implements ClientEngine { - private final ClientConfig config; + private ClientConfig config; private HttpConnection conn; /** * 重定向次数计数器,内部使用 @@ -35,13 +36,21 @@ public class JdkClientEngine implements ClientEngine { /** * 构造 */ - public JdkClientEngine() { - this.config = ClientConfig.of(); + public JdkClientEngine() {} + + @Override + public JdkClientEngine setConfig(final ClientConfig config) { + this.config = config; + if(null != this.conn){ + this.conn.disconnectQuietly(); + this.conn = null; + } + return this; } @Override public Response send(final Request message) { - return send(message, false); + return send(message, true); } /** @@ -113,8 +122,10 @@ public class JdkClientEngine implements ClientEngine { * @return {@link HttpConnection} */ private HttpConnection buildConn(final Request message) { + final ClientConfig config = ObjUtil.defaultIfNull(this.config, ClientConfig::of); + final HttpConnection conn = HttpConnection - .of(message.url().toURL(), config.proxy) + .of(message.url().toURL(), config.getProxy()) .setConnectTimeout(config.getConnectionTimeout()) .setReadTimeout(config.getReadTimeout()) .setMethod(message.method())// diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpEngine.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpEngine.java index 9b97237dc..e3c6fbc13 100755 --- a/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpEngine.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpEngine.java @@ -1,6 +1,8 @@ package cn.hutool.http.client.engine.okhttp; import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.util.ObjUtil; +import cn.hutool.http.client.ClientConfig; import cn.hutool.http.client.ClientEngine; import cn.hutool.http.client.Request; import cn.hutool.http.client.Response; @@ -8,6 +10,8 @@ import okhttp3.OkHttpClient; import okhttp3.internal.http.HttpMethod; import java.io.IOException; +import java.net.Proxy; +import java.util.concurrent.TimeUnit; /** * OkHttp3客户端引擎封装 @@ -17,7 +21,8 @@ import java.io.IOException; */ public class OkHttpEngine implements ClientEngine { - private final OkHttpClient client; + private ClientConfig config; + private OkHttpClient client; /** * 构造 @@ -26,8 +31,18 @@ public class OkHttpEngine implements ClientEngine { this.client = new OkHttpClient(); } + @Override + public OkHttpEngine setConfig(final ClientConfig config) { + this.config = config; + // 重置客户端 + this.client = null; + return this; + } + @Override public Response send(final Request message) { + initEngine(); + final okhttp3.Response response; try { response = client.newCall(buildRequest(message)).execute(); @@ -48,6 +63,28 @@ public class OkHttpEngine implements ClientEngine { // ignore } + /** + * 初始化引擎 + */ + private void initEngine() { + if (null != this.client) { + return; + } + + final ClientConfig config = ObjUtil.defaultIfNull(this.config, ClientConfig::of); + final OkHttpClient.Builder builder = new OkHttpClient.Builder() + .connectTimeout(config.getConnectionTimeout(), TimeUnit.MILLISECONDS) + .readTimeout(config.getReadTimeout(), TimeUnit.MILLISECONDS); + + // 设置代理 + final Proxy proxy = config.getProxy(); + if(null != proxy){ + builder.proxy(proxy); + } + + this.client = builder.build(); + } + /** * 构建请求体 * @@ -59,9 +96,9 @@ public class OkHttpEngine implements ClientEngine { .url(message.url().toURL()); final String method = message.method().name(); - if(HttpMethod.permitsRequestBody(method)){ + if (HttpMethod.permitsRequestBody(method)) { builder.method(method, new OkHttpRequestBody(message.body())); - }else{ + } else { builder.method(method, null); } diff --git a/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpResponse.java index 5e1808f85..c5d13e56d 100755 --- a/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpResponse.java +++ b/hutool-http/src/main/java/cn/hutool/http/client/engine/okhttp/OkHttpResponse.java @@ -3,11 +3,18 @@ package cn.hutool.http.client.engine.okhttp; import cn.hutool.core.io.stream.EmptyInputStream; import cn.hutool.core.util.ObjUtil; import cn.hutool.http.client.Response; +import kotlin.Pair; +import okhttp3.Headers; import okhttp3.ResponseBody; import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; /** * OkHttp3的{@link okhttp3.Response} 响应包装 @@ -41,6 +48,17 @@ public class OkHttpResponse implements Response { return rawRes.header(name); } + @Override + public Map> headers() { + final Headers headers = rawRes.headers(); + final HashMap> result = new LinkedHashMap<>(headers.size(), 1); + for (final Pair header : headers) { + final List valueList = result.computeIfAbsent(header.getFirst(), k -> new ArrayList<>()); + valueList.add(header.getSecond()); + } + return result; + } + @Override public Charset charset() { return ObjUtil.defaultIfNull(Response.super.charset(), requestCharset); diff --git a/hutool-http/src/main/java/cn/hutool/http/server/HttpServerRequest.java b/hutool-http/src/main/java/cn/hutool/http/server/HttpServerRequest.java index 9cec2c3d9..12deeb661 100644 --- a/hutool-http/src/main/java/cn/hutool/http/server/HttpServerRequest.java +++ b/hutool-http/src/main/java/cn/hutool/http/server/HttpServerRequest.java @@ -8,6 +8,7 @@ import cn.hutool.core.map.multi.ListValueMap; import cn.hutool.core.net.NetUtil; import cn.hutool.core.net.multipart.MultipartFormData; import cn.hutool.core.net.multipart.UploadSetting; +import cn.hutool.core.net.url.UrlQueryUtil; import cn.hutool.core.text.StrUtil; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.CharsetUtil; @@ -329,7 +330,7 @@ public class HttpServerRequest extends HttpServerBase { //解析URL中的参数 final String query = getQuery(); if(StrUtil.isNotBlank(query)){ - this.paramsCache.putAll(HttpUtil.decodeParams(query, charset)); + this.paramsCache.putAll(UrlQueryUtil.decodeQueryList(query, charset)); } // 解析multipart中的参数 @@ -339,7 +340,7 @@ public class HttpServerRequest extends HttpServerBase { // 解析body中的参数 final String body = getBody(); if(StrUtil.isNotBlank(body)){ - this.paramsCache.putAll(HttpUtil.decodeParams(body, charset)); + this.paramsCache.putAll(UrlQueryUtil.decodeQueryList(body, charset)); } } } diff --git a/hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java index 0fe866a66..5754f581b 100644 --- a/hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java +++ b/hutool-http/src/main/java/cn/hutool/http/webservice/SoapClient.java @@ -3,13 +3,14 @@ package cn.hutool.http.webservice; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.IoUtil; import cn.hutool.core.map.MapUtil; -import cn.hutool.core.util.ObjUtil; import cn.hutool.core.text.StrUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.XmlUtil; -import cn.hutool.http.client.engine.jdk.HttpBase; import cn.hutool.http.HttpGlobalConfig; -import cn.hutool.http.client.engine.jdk.HttpRequest; -import cn.hutool.http.client.engine.jdk.HttpResponse; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.Response; +import cn.hutool.http.client.engine.ClientEngineFactory; +import cn.hutool.http.client.engine.jdk.HttpBase; import javax.xml.XMLConstants; import javax.xml.namespace.QName; @@ -542,7 +543,7 @@ public class SoapClient extends HttpBase { * @return 返回结果 */ public SOAPMessage sendForMessage() { - final HttpResponse res = sendForResponse(); + final Response res = sendForResponse(); final MimeHeaders headers = new MimeHeaders(); for (final Entry> entry : res.headers().entrySet()) { if (StrUtil.isNotEmpty(entry.getKey())) { @@ -585,15 +586,13 @@ public class SoapClient extends HttpBase { * * @return 响应对象 */ - public HttpResponse sendForResponse() { - return HttpRequest.post(this.url)// - .setFollowRedirects(true)// - .setConnectionTimeout(this.connectionTimeout) - .setReadTimeout(this.readTimeout) - .contentType(getXmlContentType())// - .header(this.headers()) - .body(getMsgStr(false))// - .executeAsync(); + public Response sendForResponse() { + final Request request = Request.of(this.url) + .setMaxRedirectCount(2) + .contentType(getXmlContentType()) + .header(this.headers, false) + .body(getMsgStr(false)); + return ClientEngineFactory.get().send(request); } /** diff --git a/hutool-http/src/test/java/cn/hutool/http/DownloadTest.java b/hutool-http/src/test/java/cn/hutool/http/DownloadTest.java index 8460794ae..15e73e42d 100644 --- a/hutool-http/src/test/java/cn/hutool/http/DownloadTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/DownloadTest.java @@ -1,14 +1,19 @@ package cn.hutool.http; +import cn.hutool.core.codec.BaseN.Base64; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.StreamProgress; import cn.hutool.core.lang.Console; -import cn.hutool.http.client.engine.jdk.HttpRequest; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.http.client.HttpDownloader; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.engine.ClientEngineFactory; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.nio.file.Files; @@ -26,7 +31,7 @@ public class DownloadTest { @Ignore public void downloadPicTest() { final String url = "http://wx.qlogo.cn/mmopen/vKhlFcibVUtNBVDjcIowlg0X8aJfHXrTNCEFBukWVH9ta99pfEN88lU39MKspCUCOP3yrFBH3y2NbV7sYtIIlon8XxLwAEqv2/0"; - HttpUtil.downloadFile(url, "e:/pic/t3.jpg"); + HttpDownloader.downloadFile(url, new File("e:/pic/t3.jpg")); Console.log("ok"); } @@ -34,13 +39,14 @@ public class DownloadTest { @Ignore public void downloadSizeTest() { final String url = "https://res.t-io.org/im/upload/img/67/8948/1119501/88097554/74541310922/85/231910/366466 - 副本.jpg"; - HttpRequest.get(url).setSSLProtocol("TLSv1.2").executeAsync().body().write("e:/pic/366466.jpg"); + ClientEngineFactory.get().send(Request.of(url)).body().write("e:/pic/366466.jpg"); + //HttpRequest.get(url).setSSLProtocol("TLSv1.2").executeAsync().body().write("e:/pic/366466.jpg"); } @Test @Ignore public void downloadTest1() { - final File size = HttpUtil.downloadFile("http://explorer.bbfriend.com/crossdomain.xml", "e:/temp/"); + final File size = HttpDownloader.downloadFile("http://explorer.bbfriend.com/crossdomain.xml", new File("e:/temp/")); System.out.println("Download size: " + size); } @@ -48,7 +54,7 @@ public class DownloadTest { @Ignore public void downloadTest() { // 带进度显示的文件下载 - HttpUtil.downloadFile("http://mirrors.sohu.com/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso", FileUtil.file("d:/"), new StreamProgress() { + HttpDownloader.downloadFile("http://mirrors.sohu.com/centos/7/isos/x86_64/CentOS-7-x86_64-DVD-2009.iso", FileUtil.file("d:/"), -1, new StreamProgress() { final long time = System.currentTimeMillis(); @@ -73,7 +79,7 @@ public class DownloadTest { @Test @Ignore public void downloadFileFromUrlTest1() { - final File file = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", "d:/download/temp"); + final File file = HttpDownloader.downloadFile("http://groovy-lang.org/changelogs/changelog-3.0.5.html", new File("d:/download/temp")); Assert.assertNotNull(file); Assert.assertTrue(file.isFile()); Assert.assertTrue(file.length() > 0); @@ -84,7 +90,7 @@ public class DownloadTest { public void downloadFileFromUrlTest2() { File file = null; try { - file = HttpUtil.downloadFileFromUrl("https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar", FileUtil.file("d:/download/temp"), 1, new StreamProgress() { + file = HttpDownloader.downloadFile("https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar", FileUtil.file("d:/download/temp"), 1, new StreamProgress() { @Override public void start() { System.out.println("start"); @@ -118,7 +124,7 @@ public class DownloadTest { public void downloadFileFromUrlTest3() { File file = null; try { - file = HttpUtil.downloadFileFromUrl("https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar", FileUtil.file("d:/download/temp"), new StreamProgress() { + file = HttpDownloader.downloadFile("https://repo1.maven.org/maven2/cn/hutool/hutool-all/5.4.0/hutool-all-5.4.0-sources.jar", FileUtil.file("d:/download/temp"), -1, new StreamProgress() { @Override public void start() { System.out.println("start"); @@ -150,7 +156,7 @@ public class DownloadTest { public void downloadFileFromUrlTest4() { File file = null; try { - file = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp"), 1); + file = HttpDownloader.downloadFile("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp"), 1); Assert.assertNotNull(file); Assert.assertTrue(file.exists()); @@ -170,7 +176,7 @@ public class DownloadTest { public void downloadFileFromUrlTest5() { File file = null; try { - file = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp", UUID.randomUUID().toString())); + file = HttpDownloader.downloadFile("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp", UUID.randomUUID().toString())); Assert.assertNotNull(file); Assert.assertTrue(file.exists()); @@ -182,7 +188,7 @@ public class DownloadTest { File file1 = null; try { - file1 = HttpUtil.downloadFileFromUrl("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp")); + file1 = HttpDownloader.downloadFile("http://groovy-lang.org/changelogs/changelog-3.0.5.html", FileUtil.file("d:/download/temp")); Assert.assertNotNull(file1); Assert.assertTrue(file1.exists()); @@ -200,7 +206,34 @@ public class DownloadTest { final String url = "https://download.teamviewer.com/download/TeamViewer_Setup_x64.exe"; HttpGlobalConfig.setMaxRedirectCount(20); final Path temp = Files.createTempFile("tmp", ".exe"); - final File file = HttpUtil.downloadFileFromUrl(url, temp.toFile()); + final File file = HttpDownloader.downloadFile(url, temp.toFile()); Console.log(file.length()); } + + @Test + @Ignore + public void downloadToStreamTest() { + String url2 = "http://storage.chancecloud.com.cn/20200413_%E7%B2%A4B12313_386.pdf"; + final ByteArrayOutputStream os2 = new ByteArrayOutputStream(); + HttpDownloader.download(url2, os2, false, null); + + url2 = "http://storage.chancecloud.com.cn/20200413_粤B12313_386.pdf"; + HttpDownloader.download(url2, os2, false, null); + } + + @Test + @Ignore + public void downloadStringTest() { + final String url = "https://www.baidu.com"; + // 从远程直接读取字符串,需要自定义编码,直接调用JDK方法 + final String content2 = HttpDownloader.downloadString(url, CharsetUtil.UTF_8, null); + Console.log(content2); + } + + @Test + @Ignore + public void gimg2Test(){ + final byte[] bytes = HttpDownloader.downloadBytes("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http%3A%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea"); + Console.log(Base64.encode(bytes)); + } } 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 2c96ed6f4..c2beb362e 100644 --- a/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/HttpRequestTest.java @@ -4,11 +4,11 @@ import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.StopWatch; import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Console; -import cn.hutool.core.net.ssl.SSLProtocols; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.util.CharsetUtil; -import cn.hutool.http.client.engine.jdk.HttpRequest; -import cn.hutool.http.client.engine.jdk.HttpResponse; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.Response; import cn.hutool.http.meta.Header; import cn.hutool.http.meta.Method; import org.junit.Ignore; @@ -20,7 +20,7 @@ import java.util.List; import java.util.Map; /** - * {@link HttpRequest}单元测试 + * {@link Request}单元测试 * * @author Looly */ @@ -28,18 +28,11 @@ import java.util.Map; public class HttpRequestTest { final String url = "http://photo.qzone.qq.com/fcgi-bin/fcg_list_album?uin=88888&outstyle=2"; - @Test - @Ignore - public void getHttpsTest() { - final String body = HttpRequest.get("https://www.hutool.cn/").timeout(10).execute().bodyStr(); - Console.log(body); - } - @Test @Ignore public void getHttpsThenTest() { - HttpRequest - .get("https://hutool.cn") + Request.of("https://hutool.cn") + .send() .then(response -> Console.log(response.body())); } @@ -47,9 +40,9 @@ public class HttpRequestTest { @Ignore public void getCookiesTest() { // 检查在Connection关闭情况下Cookie是否可以正常获取 - final HttpResponse res = HttpRequest.get("https://www.oschina.net/").execute(); + final Response res = Request.of("https://www.oschina.net/").send(); final String body = res.bodyStr(); - Console.log(res.getCookies()); + Console.log(res.getCookieStr()); Console.log(body); } @@ -58,14 +51,14 @@ public class HttpRequestTest { public void toStringTest() { final String url = "http://gc.ditu.aliyun.com/geocoding?ccc=你好"; - final HttpRequest request = HttpRequest.get(url).body("a=乌海"); + final Request request = Request.of(url).body("a=乌海"); Console.log(request.toString()); } @Test @Ignore public void asyncHeadTest() { - final HttpResponse response = HttpRequest.head(url).execute(); + final Response response = Request.of(url).method(Method.HEAD).send(); final Map> headers = response.headers(); Console.log(headers); Console.log(response.body()); @@ -76,7 +69,7 @@ public class HttpRequestTest { public void asyncGetTest() { final StopWatch timer = DateUtil.createStopWatch(); timer.start(); - final HttpResponse body = HttpRequest.get(url).charset("GBK").executeAsync(); + final Response body = Request.of(url).charset(CharsetUtil.GBK).send(); timer.stop(); final long interval = timer.getLastTaskTimeMillis(); timer.start(); @@ -91,7 +84,7 @@ public class HttpRequestTest { public void syncGetTest() { final StopWatch timer = DateUtil.createStopWatch(); timer.start(); - final HttpResponse body = HttpRequest.get(url).charset("GBK").execute(); + final Response body = Request.of(url).charset(CharsetUtil.GBK).send(); timer.stop(); final long interval = timer.getLastTaskTimeMillis(); @@ -102,26 +95,12 @@ public class HttpRequestTest { Console.log("Async response spend {}ms, body spend {}ms", interval, interval2); } - @Test - @Ignore - public void customGetTest() { - // 自定义构建HTTP GET请求,发送Http GET请求,针对HTTPS安全加密,可以自定义SSL - final HttpRequest request = HttpRequest.get(url) - // 自定义返回编码 - .charset(CharsetUtil.GBK) - // 禁用缓存 - .disableCache() - // 自定义SSL版本 - .setSSLProtocol(SSLProtocols.TLSv12); - Console.log(request.execute().body()); - } - @Test @Ignore public void getDeflateTest() { - final HttpResponse res = HttpRequest.get("https://comment.bilibili.com/67573272.xml") + final Response res = Request.of("https://comment.bilibili.com/67573272.xml") .header(Header.ACCEPT_ENCODING, "deflate") - .execute(); + .send(); Console.log(res.header(Header.CONTENT_ENCODING)); Console.log(res.body()); } @@ -129,7 +108,7 @@ public class HttpRequestTest { @Test @Ignore public void bodyTest() { - final String ddddd1 = HttpRequest.get("https://baijiahao.baidu.com/s").body("id=1625528941695652600").execute().bodyStr(); + final String ddddd1 = Request.of("https://baijiahao.baidu.com/s").body("id=1625528941695652600").send().bodyStr(); Console.log(ddddd1); } @@ -149,9 +128,9 @@ public class HttpRequestTest { map.put("size", "2"); map.put("sizes", list); - HttpRequest - .get("http://localhost:8888/get") - .form(map) + Request + .of("http://localhost:8888/get") + .form(map).send() .then(resp -> Console.log(resp.body())); } @@ -159,9 +138,9 @@ public class HttpRequestTest { @Ignore public void getWithoutEncodeTest() { final String url = "https://img-cloud.voc.com.cn/140/2020/09/03/c3d41b93e0d32138574af8e8b50928b376ca5ba61599127028157.png?imageMogr2/auto-orient/thumbnail/500&pid=259848"; - final HttpRequest get = HttpUtil.createGet(url); - Console.log(get.getUrl()); - final HttpResponse execute = get.execute(); + final Request get = Request.of(url); + Console.log(get.url()); + final Response execute = get.send(); Console.log(execute.body()); } @@ -176,11 +155,11 @@ public class HttpRequestTest { // 方式1:全局设置 HttpGlobalConfig.setMaxRedirectCount(1); - HttpResponse execute = HttpRequest.get(url).execute(); + Response execute = Request.of(url).send(); Console.log(execute.getStatus(), execute.header(Header.LOCATION)); // 方式2,单独设置 - execute = HttpRequest.get(url).setMaxRedirectCount(1).execute(); + execute = Request.of(url).setMaxRedirectCount(1).send(); Console.log(execute.getStatus(), execute.header(Header.LOCATION)); } @@ -190,8 +169,8 @@ public class HttpRequestTest { final String url = "https://postman-echo.com/get"; final Map map = new HashMap<>(); map.put("aaa", "application+1@qqq.com"); - final HttpRequest request =HttpUtil.createGet(url).form(map); - Console.log(request.execute().body()); + final Request request =Request.of(url).form(map); + Console.log(request.send().body()); } @Test @@ -200,51 +179,51 @@ public class HttpRequestTest { final UrlBuilder urlBuilder = new UrlBuilder(); urlBuilder.setScheme("https").setHost("hutool.cn"); - final HttpRequest httpRequest = new HttpRequest(urlBuilder); - httpRequest.method(Method.GET).execute(); + final Request httpRequest = Request.of(urlBuilder); + httpRequest.method(Method.GET).send(); } @Test @Ignore public void getCookieTest(){ - final HttpResponse execute = HttpRequest.get("http://localhost:8888/getCookier").execute(); - Console.log(execute.getCookies()); + final Response execute = Request.of("http://localhost:8888/getCookier").send(); + Console.log(execute.getCookieStr()); } @Test public void optionsTest() { - final HttpRequest options = HttpRequest.options("https://hutool.cn"); + final Request options = Request.of("https://hutool.cn").method(Method.OPTIONS); Assert.notNull(options.toString()); } @Test public void deleteTest() { - final HttpRequest options = HttpRequest.delete("https://hutool.cn"); + final Request options = Request.of("https://hutool.cn").method(Method.DELETE); Assert.notNull(options.toString()); } @Test public void traceTest() { - final HttpRequest options = HttpRequest.trace("https://hutool.cn"); + final Request options = Request.of("https://hutool.cn").method(Method.TRACE); Assert.notNull(options.toString()); } @Test public void getToStringTest() { - final HttpRequest a = HttpRequest.get("https://hutool.cn/").form("a", 1); + final Request a = Request.of("https://hutool.cn/").form(MapUtil.of("a", 1)); Assert.notNull(a.toString()); } @Test public void postToStringTest() { - final HttpRequest a = HttpRequest.post("https://hutool.cn/").form("a", 1); + final Request a = Request.of("https://hutool.cn/").method(Method.POST).form(MapUtil.of("a", 1)); Console.log(a.toString()); } @Test @Ignore public void issueI5Y68WTest() { - final HttpResponse httpResponse = HttpRequest.get("http://82.157.17.173:8100/app/getAddress").execute(); + final Response httpResponse = Request.of("http://82.157.17.173:8100/app/getAddress").send(); Console.log(httpResponse.body()); } } diff --git a/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java b/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java index 6ab2686f0..b39864316 100755 --- a/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/HttpUtilTest.java @@ -1,18 +1,16 @@ package cn.hutool.http; -import cn.hutool.core.codec.BaseN.Base64; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Console; -import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.regex.ReUtil; -import cn.hutool.http.client.engine.jdk.HttpRequest; +import cn.hutool.core.util.CharsetUtil; +import cn.hutool.http.client.Request; import cn.hutool.http.meta.Header; import cn.hutool.http.meta.Method; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; -import java.io.ByteArrayOutputStream; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -35,23 +33,13 @@ public class HttpUtilTest { Assert.assertFalse(HttpUtil.isHttps("ftp://aaa.bbb")); } - @Test - @Ignore - public void postTest() { - final String result = HttpUtil.createPost("api.uhaozu.com/goods/description/1120448506") - .charset(CharsetUtil.NAME_UTF_8) - .execute().bodyStr(); - Console.log(result); - } - @Test @Ignore public void postTest2() { // 某些接口对Accept头有特殊要求,此处自定义头 - final String result = HttpUtil - .createPost("http://cmp.ishanghome.com/cmp/v1/community/queryClusterCommunity") - .header(Header.ACCEPT, "*/*") - .execute() + final String result = HttpUtil.send(Request + .of("http://cmp.ishanghome.com/cmp/v1/community/queryClusterCommunity") + .header(Header.ACCEPT, "*/*")) .bodyStr(); Console.log(result); } @@ -68,9 +56,9 @@ public class HttpUtilTest { public void getTest2() { // 此链接较为特殊,User-Agent去掉后进入一个JS跳转页面,如果设置了,需要开启302跳转 // 自定义的默认header无效 - final String result = HttpRequest - .get("https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101457313&redirect_uri=http%3A%2F%2Fwww.benmovip.com%2Fpay-cloud%2Fqqlogin%2FgetCode&state=ok") - .removeHeader(Header.USER_AGENT).execute().bodyStr(); + final String result = Request + .of("https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101457313&redirect_uri=http%3A%2F%2Fwww.benmovip.com%2Fpay-cloud%2Fqqlogin%2FgetCode&state=ok") + .header(Header.USER_AGENT, null).send().bodyStr(); Console.log(result); } @@ -86,38 +74,17 @@ public class HttpUtilTest { @Ignore public void getTest4() { // 测试url中带有空格的情况 - final byte[] str = HttpRequest.get("http://img01.fs.yiban.cn/mobile/2D0Y71").execute().bodyBytes(); + final byte[] str = Request.of("http://img01.fs.yiban.cn/mobile/2D0Y71").send().bodyBytes(); FileUtil.writeBytes(str, "f:/test/2D.jpg"); Console.log(str); } - @Test - @Ignore - public void getTest5() { - String url2 = "http://storage.chancecloud.com.cn/20200413_%E7%B2%A4B12313_386.pdf"; - final ByteArrayOutputStream os2 = new ByteArrayOutputStream(); - HttpUtil.download(url2, os2, false); - - url2 = "http://storage.chancecloud.com.cn/20200413_粤B12313_386.pdf"; - HttpUtil.download(url2, os2, false); - } - @Test @Ignore public void get12306Test() { - HttpRequest.get("https://kyfw.12306.cn/otn/") - .setFollowRedirects(true) - .then(response -> Console.log(response.body())); - } - - @Test - @Ignore - public void downloadStringTest() { - final String url = "https://www.baidu.com"; - // 从远程直接读取字符串,需要自定义编码,直接调用JDK方法 - final String content2 = HttpUtil.downloadString(url, CharsetUtil.NAME_UTF_8); - Console.log(content2); + HttpUtil.send(Request.of("https://kyfw.12306.cn/otn/").setMaxRedirectCount(2)) + .then(response -> Console.log(response.bodyStr())); } @Test @@ -138,141 +105,10 @@ public class HttpUtilTest { } @Test - public void decodeParamsTest() { - final String paramsStr = "uuuu=0&a=b&c=%3F%23%40!%24%25%5E%26%3Ddsssss555555"; - final Map> map = HttpUtil.decodeParams(paramsStr, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("0", map.get("uuuu").get(0)); - Assert.assertEquals("b", map.get("a").get(0)); - Assert.assertEquals("?#@!$%^&=dsssss555555", map.get("c").get(0)); - } - - @Test - public void decodeParamMapTest() { - // 参数值存在分界标记等号时 - final Map paramMap = HttpUtil.decodeParamMap("https://www.xxx.com/api.action?aa=123&f_token=NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=", CharsetUtil.UTF_8); - Assert.assertEquals("123",paramMap.get("aa")); - Assert.assertEquals("NzBkMjQxNDM1MDVlMDliZTk1OTU3ZDI1OTI0NTBiOWQ=",paramMap.get("f_token")); - } - - @Test - public void toParamsTest() { - final String paramsStr = "uuuu=0&a=b&c=3Ddsssss555555"; - final Map> map = HttpUtil.decodeParams(paramsStr, CharsetUtil.NAME_UTF_8); - - final String encodedParams = HttpUtil.toParams(map); - Assert.assertEquals(paramsStr, encodedParams); - } - - @Test - public void encodeParamTest() { - // ?单独存在去除之,&单位位于末尾去除之 - String paramsStr = "?a=b&c=d&"; - String encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("a=b&c=d", encode); - - // url不参与转码 - paramsStr = "http://www.abc.dd?a=b&c=d&"; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("http://www.abc.dd?a=b&c=d", encode); - - // b=b中的=被当作值的一部分,不做encode - paramsStr = "a=b=b&c=d&"; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("a=b=b&c=d", encode); - - // =d的情况被处理为key为空 - paramsStr = "a=bbb&c=d&=d"; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("a=bbb&c=d&=d", encode); - - // d=的情况被处理为value为空 - paramsStr = "a=bbb&c=d&d="; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("a=bbb&c=d&d=", encode); - - // 多个&&被处理为单个,相当于空条件 - paramsStr = "a=bbb&c=d&&&d="; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("a=bbb&c=d&d=", encode); - - // &d&相当于只有键,无值得情况 - paramsStr = "a=bbb&c=d&d&"; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("a=bbb&c=d&d=", encode); - - // 中文的键和值被编码 - paramsStr = "a=bbb&c=你好&哈喽&"; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD=", encode); - - // URL原样输出 - paramsStr = "https://www.hutool.cn/"; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals(paramsStr, encode); - - // URL原样输出 - paramsStr = "https://www.hutool.cn/?"; - encode = HttpUtil.encodeParams(paramsStr, CharsetUtil.UTF_8); - Assert.assertEquals("https://www.hutool.cn/", encode); - } - - @Test - public void decodeParamTest() { - // 开头的?被去除 - String a = "?a=b&c=d&"; - Map> map = HttpUtil.decodeParams(a, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("b", map.get("a").get(0)); - Assert.assertEquals("d", map.get("c").get(0)); - - // =e被当作空为key,e为value - a = "?a=b&c=d&=e"; - map = HttpUtil.decodeParams(a, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("b", map.get("a").get(0)); - Assert.assertEquals("d", map.get("c").get(0)); - Assert.assertEquals("e", map.get("").get(0)); - - // 多余的&去除 - a = "?a=b&c=d&=e&&&&"; - map = HttpUtil.decodeParams(a, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("b", map.get("a").get(0)); - Assert.assertEquals("d", map.get("c").get(0)); - Assert.assertEquals("e", map.get("").get(0)); - - // 值为空 - a = "?a=b&c=d&e="; - map = HttpUtil.decodeParams(a, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("b", map.get("a").get(0)); - Assert.assertEquals("d", map.get("c").get(0)); - Assert.assertEquals("", map.get("e").get(0)); - - // &=被作为键和值都为空 - a = "a=b&c=d&="; - map = HttpUtil.decodeParams(a, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("b", map.get("a").get(0)); - Assert.assertEquals("d", map.get("c").get(0)); - Assert.assertEquals("", map.get("").get(0)); - - // &e&这类单独的字符串被当作key - a = "a=b&c=d&e&"; - map = HttpUtil.decodeParams(a, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("b", map.get("a").get(0)); - Assert.assertEquals("d", map.get("c").get(0)); - Assert.assertNull(map.get("e").get(0)); - Assert.assertNull(map.get("").get(0)); - - // 被编码的键和值被还原 - a = "a=bbb&c=%E4%BD%A0%E5%A5%BD&%E5%93%88%E5%96%BD="; - map = HttpUtil.decodeParams(a, CharsetUtil.NAME_UTF_8); - Assert.assertEquals("bbb", map.get("a").get(0)); - Assert.assertEquals("你好", map.get("c").get(0)); - Assert.assertEquals("", map.get("哈喽").get(0)); - } - - @Test - @Ignore + //@Ignore public void patchTest() { // 验证patch请求是否可用 - final String body = HttpRequest.patch("https://www.baidu.com").execute().bodyStr(); + final String body = HttpUtil.send(Request.of("https://hutool.cn").method(Method.PATCH)).bodyStr(); Console.log(body); } @@ -315,18 +151,6 @@ public class HttpUtilTest { Assert.assertEquals("utf-8", charsetName); } - @Test - public void normalizeParamsTest() { - final String encodeResult = HttpUtil.normalizeParams("参数", CharsetUtil.UTF_8); - Assert.assertEquals("%E5%8F%82%E6%95%B0", encodeResult); - } - - @Test - public void normalizeBlankParamsTest() { - final String encodeResult = HttpUtil.normalizeParams("", CharsetUtil.UTF_8); - Assert.assertEquals("", encodeResult); - } - @Test public void getMimeTypeTest() { final String mimeType = HttpUtil.getMimeType("aaa.aaa"); @@ -357,18 +181,10 @@ public class HttpUtilTest { Console.log(s); } - @Test - @Ignore - public void gimg2Test(){ - final byte[] bytes = HttpUtil.downloadBytes("https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http%3A%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea"); - Console.log(Base64.encode(bytes)); - } - @Test @Ignore public void acplayTest(){ - final String body = HttpRequest.get("https://api.acplay.net/api/v2/bangumi/9541") - .execute().bodyStr(); + final String body = HttpUtil.send(Request.of("https://api.acplay.net/api/v2/bangumi/9541")).bodyStr(); Console.log(body); } @@ -378,7 +194,7 @@ public class HttpUtilTest { HttpGlobalConfig.setDecodeUrl(false); final String url = "https://p3-sign.douyinpic.com/tos-cn-i-0813/f41afb2e79a94dcf80970affb9a69415~noop.webp?x-expires=1647738000&x-signature=%2Br1ekUCGjXiu50Y%2Bk0MO4ovulK8%3D&from=4257465056&s=PackSourceEnum_DOUYIN_REFLOW&se=false&sh=&sc=&l=2022021809224601020810013524310DD3&biz_tag=aweme_images"; - final HttpRequest request = HttpRequest.of(url).method(Method.GET); - Console.log(request.execute().body()); + final String body = HttpUtil.send(Request.of(url)).bodyStr(); + Console.log(body); } } diff --git a/hutool-http/src/test/java/cn/hutool/http/Issue2531Test.java b/hutool-http/src/test/java/cn/hutool/http/Issue2531Test.java index 88d928c3f..90fe889a9 100755 --- a/hutool-http/src/test/java/cn/hutool/http/Issue2531Test.java +++ b/hutool-http/src/test/java/cn/hutool/http/Issue2531Test.java @@ -3,8 +3,8 @@ package cn.hutool.http; import cn.hutool.core.lang.Console; import cn.hutool.core.map.MapUtil; import cn.hutool.core.net.url.UrlBuilder; -import cn.hutool.http.client.engine.jdk.HttpRequest; -import cn.hutool.http.client.engine.jdk.HttpResponse; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.Response; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -23,10 +23,10 @@ public class Issue2531Test { final String queryParam = MapUtil.join(map, "&", "=");//返回str=+123 Console.log(queryParam); - final HttpRequest request = HttpUtil.createGet("http://localhost:8888/formTest?" + queryParam); + final Request request = Request.of("http://localhost:8888/formTest?" + queryParam); //request.setUrl("http://localhost:8888/formTest" + "?" + queryParam); //noinspection resource - final HttpResponse execute = request.execute(); + final Response execute = request.send(); Console.log(execute.body()); } diff --git a/hutool-http/src/test/java/cn/hutool/http/IssueI5TPSYTest.java b/hutool-http/src/test/java/cn/hutool/http/IssueI5TPSYTest.java index 6b8133729..8bfb5e705 100755 --- a/hutool-http/src/test/java/cn/hutool/http/IssueI5TPSYTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/IssueI5TPSYTest.java @@ -1,7 +1,8 @@ package cn.hutool.http; import cn.hutool.core.lang.Console; -import cn.hutool.http.client.engine.jdk.HttpResponse; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.Response; import cn.hutool.http.meta.Header; import org.junit.Ignore; import org.junit.Test; @@ -12,10 +13,10 @@ public class IssueI5TPSYTest { @Ignore public void redirectTest() { final String url = "https://bsxt.gdzwfw.gov.cn/UnifiedReporting/auth/newIndex"; - final HttpResponse res = HttpUtil.createGet(url).setFollowRedirects(true) + final Response res = HttpUtil.send(Request.of(url) + .setMaxRedirectCount(2) .header(Header.USER_AGENT, "PostmanRuntime/7.29.2") - .cookie("jsessionid=s%3ANq6YTcIHQWrHkEqOSxiQNijDMhoFNV4_.h2MVD1CkW7sOZ60OSnPs7m4K%2FhENfYy%2FdzjKvSiZF4E") - .execute(); + .cookie("jsessionid=s%3ANq6YTcIHQWrHkEqOSxiQNijDMhoFNV4_.h2MVD1CkW7sOZ60OSnPs7m4K%2FhENfYy%2FdzjKvSiZF4E")); Console.log(res.body()); } } diff --git a/hutool-http/src/test/java/cn/hutool/http/IssueI5WAV4Test.java b/hutool-http/src/test/java/cn/hutool/http/IssueI5WAV4Test.java index dd324f36e..dc8687d10 100755 --- a/hutool-http/src/test/java/cn/hutool/http/IssueI5WAV4Test.java +++ b/hutool-http/src/test/java/cn/hutool/http/IssueI5WAV4Test.java @@ -1,6 +1,6 @@ package cn.hutool.http; -import cn.hutool.http.client.engine.jdk.HttpRequest; +import cn.hutool.http.client.Request; import cn.hutool.json.JSONUtil; import org.junit.Ignore; import org.junit.Test; @@ -19,7 +19,8 @@ public class IssueI5WAV4Test { map.put("flightID", 2879); - final String body = HttpRequest.get("http://localhost:8884/api/test/testHttpUtilGetWithBody").body(JSONUtil.toJsonStr(map)).execute().bodyStr(); + @SuppressWarnings("resource") + final String body = Request.of("http://localhost:8884/api/test/testHttpUtilGetWithBody").body(JSONUtil.toJsonStr(map)).send().bodyStr(); System.out.println("使用hutool返回结果:" + body); } } diff --git a/hutool-http/src/test/java/cn/hutool/http/IssueI5XBCFTest.java b/hutool-http/src/test/java/cn/hutool/http/IssueI5XBCFTest.java index 2dabccc15..43b0d5e5d 100755 --- a/hutool-http/src/test/java/cn/hutool/http/IssueI5XBCFTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/IssueI5XBCFTest.java @@ -1,7 +1,8 @@ package cn.hutool.http; import cn.hutool.core.lang.Console; -import cn.hutool.http.client.engine.jdk.HttpResponse; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.Response; import cn.hutool.http.meta.Header; import org.brotli.dec.BrotliInputStream; import org.junit.Ignore; @@ -14,9 +15,10 @@ public class IssueI5XBCFTest { public void getTest() { GlobalCompressStreamRegister.INSTANCE.register("br", BrotliInputStream.class); - @SuppressWarnings("resource") final HttpResponse s = HttpUtil.createGet("https://static-exp1.licdn.com/sc/h/br/1cp0oqz322bdprj3qd4pojqix") + @SuppressWarnings("resource") + final Response s = Request.of("https://static-exp1.licdn.com/sc/h/br/1cp0oqz322bdprj3qd4pojqix") .header(Header.ACCEPT_ENCODING, "br") - .execute(); + .send(); Console.log(s.body()); } } diff --git a/hutool-http/src/test/java/cn/hutool/http/RestTest.java b/hutool-http/src/test/java/cn/hutool/http/RestTest.java index 5ccbba7af..555a36872 100644 --- a/hutool-http/src/test/java/cn/hutool/http/RestTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/RestTest.java @@ -1,8 +1,9 @@ package cn.hutool.http; import cn.hutool.core.lang.Console; -import cn.hutool.http.client.engine.jdk.HttpRequest; +import cn.hutool.http.client.Request; import cn.hutool.http.meta.Header; +import cn.hutool.http.meta.Method; import cn.hutool.json.JSONUtil; import org.junit.Assert; import org.junit.Ignore; @@ -18,21 +19,24 @@ public class RestTest { @Test public void contentTypeTest() { - final HttpRequest request = HttpRequest.post("http://localhost:8090/rest/restTest/")// + final Request request = Request.of("http://localhost:8090/rest/restTest/") + .method(Method.POST) .body(JSONUtil.ofObj() .set("aaa", "aaaValue") .set("键2", "值2").toString()); Assert.assertEquals("application/json;charset=UTF-8", request.header(Header.CONTENT_TYPE)); } + @SuppressWarnings("resource") @Test @Ignore public void postTest() { - final HttpRequest request = HttpRequest.post("http://localhost:8090/rest/restTest/")// + final Request request = Request.of("http://localhost:8090/rest/restTest/") + .method(Method.POST) .body(JSONUtil.ofObj() .set("aaa", "aaaValue") .set("键2", "值2").toString()); - Console.log(request.execute().body()); + Console.log(request.send().body()); } @Test @@ -47,25 +51,12 @@ public class RestTest { @Test @Ignore public void getWithBodyTest() { - final HttpRequest request = HttpRequest.get("http://localhost:8888/restTest")// + final Request request = Request.of("http://localhost:8888/restTest")// .header(Header.CONTENT_TYPE, "application/json") .body(JSONUtil.ofObj() .set("aaa", "aaaValue") .set("键2", "值2").toString()); - Console.log(request.execute().body()); - } - - @Test - @Ignore - public void getWithBodyTest2() { - final HttpRequest request = HttpRequest.get("https://ad.oceanengine.com/open_api/2/advertiser/info/")// - // Charles代理 - .setHttpProxy("localhost", 8888) - .header("Access-Token","") - .body(JSONUtil.ofObj() - .set("advertiser_ids", new Long[] {1690657248243790L}) - .set("fields", new String[] {"id", "name", "status"}).toString()); - Console.log(request); - Console.log(request.execute().body()); + //noinspection resource + Console.log(request.send().body()); } } diff --git a/hutool-http/src/test/java/cn/hutool/http/UploadTest.java b/hutool-http/src/test/java/cn/hutool/http/UploadTest.java index 144f01196..b6095fadf 100644 --- a/hutool-http/src/test/java/cn/hutool/http/UploadTest.java +++ b/hutool-http/src/test/java/cn/hutool/http/UploadTest.java @@ -1,16 +1,17 @@ package cn.hutool.http; import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.MultiFileResource; import cn.hutool.core.lang.Console; -import cn.hutool.http.client.engine.jdk.HttpRequest; -import cn.hutool.http.client.engine.jdk.HttpResponse; +import cn.hutool.core.map.MapUtil; +import cn.hutool.http.client.Request; +import cn.hutool.http.client.Response; import cn.hutool.http.meta.Header; +import cn.hutool.http.meta.Method; import org.junit.Ignore; import org.junit.Test; import java.io.File; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; @@ -30,12 +31,18 @@ public class UploadTest { final File file = FileUtil.file("d:\\图片1.JPG"); final File file2 = FileUtil.file("d:\\图片3.png"); + final Map form = MapUtil.builder(new HashMap()) + .put("file", new MultiFileResource(file2, file)) + .put("fileType", "图片") + .build(); + // 方法一:自定义构建表单 - final HttpRequest request = HttpRequest// - .post("http://localhost:8888/file")// - .form("file", file2, file)// - .form("fileType", "图片"); - final HttpResponse response = request.execute(); + final Request request = Request// + .of("http://localhost:8888/file")// + .method(Method.POST) + .form(form); + //noinspection resource + final Response response = request.send(); Console.log(response.body()); } @@ -52,26 +59,6 @@ public class UploadTest { System.out.println(result); } - @Test - @Ignore - public void uploadTest2() { - //客户端 - final String url = "http://192.168.1.200:8888/meta/upload/img"; - final Path file = Paths.get("D:\\test\\testBigData_upload.xlsx"); - final Map headers = new HashMap<>(16); - headers.put("md5", "aaaaaaaa"); - - final Map params = new HashMap<>(16); - params.put("fileName", file.toFile().getName()); - params.put("file", file.toFile()); - final HttpRequest httpRequest = HttpRequest.post(url) - .setChunkedStreamingMode(1024 * 1024) - .headerMap(headers, false) - .form(params); - final HttpResponse httpResponse = httpRequest.execute(); - Console.log(httpResponse); - } - @Test @Ignore public void smmsTest(){ @@ -79,11 +66,13 @@ public class UploadTest { // hutool的user agent 被封了 final String token = "test"; final String url = "https://sm.ms/api/v2/upload"; - final String result = HttpUtil.createPost(url) + //noinspection resource + final String result = Request.of(url) + .method(Method.POST) .header(Header.USER_AGENT, "PostmanRuntime/7.28.4") .auth(token) - .form("smfile", FileUtil.file("d:/test/qrcodeCustom.png")) - .execute().bodyStr(); + .form(MapUtil.of("smfile", FileUtil.file("d:/test/qrcodeCustom.png"))) + .send().bodyStr(); Console.log(result); }