This commit is contained in:
Looly
2022-11-30 23:51:36 +08:00
parent 185cd09ab0
commit 75d536839c
34 changed files with 877 additions and 2145 deletions

View File

@@ -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;
/**
* 全局头部信息<br>
* 所有Http请求将共用此全局头部信息,除非在{@link HttpRequest}中自定义头部信息覆盖之
* 所有Http请求将共用此全局头部信息
*
* @author looly
*/

View File

@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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请求<br>
* 请求体body参数支持两种类型
* 使用默认HTTP引擎发送HTTP请求
*
* <pre>
* 1. 标准参数,例如 a=1&amp;b=2 这种格式
* 2. Rest模式此时body需要传入一个JSON或者XML字符串Hutool会自动绑定其对应的Content-Type
* </pre>
*
* @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<String, ?> paramMap) {
return toParams(paramMap, CharsetUtil.UTF_8);
}
/**
* 将Map形式的Form表单数据转换为Url参数形式<br>
* paramMap中如果key为空null和""会被忽略如果value为null会被做为空白符""<br>
* 会自动url编码键和值<br>
* 此方法用于拼接URL中的Query部分并不适用于POST请求中的表单
*
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
* </pre>
*
* @param paramMap 表单数据
* @param charset 编码,{@code null} 表示不encode键值对
* @return url参数
* @see #toParams(Map, Charset, boolean)
*/
public static String toParams(final Map<String, ?> paramMap, final Charset charset) {
return toParams(paramMap, charset, false);
}
/**
* 将Map形式的Form表单数据转换为Url参数形式<br>
* paramMap中如果key为空null和""会被忽略如果value为null会被做为空白符""<br>
* 会自动url编码键和值
*
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
* </pre>
*
* @param paramMap 表单数据
* @param charset 编码null表示不encode键值对
* @param isFormUrlEncoded 是否为x-www-form-urlencoded模式此模式下空格会编码为'+'
* @return url参数
* @since 5.7.16
*/
public static String toParams(final Map<String, ?> paramMap, final Charset charset, final boolean isFormUrlEncoded) {
return UrlQuery.of(paramMap, isFormUrlEncoded).build(charset);
}
/**
* 对URL参数做编码只编码键和值<br>
* 提供的值可以是url附带参数但是不能只是url
*
* <p>注意此方法只能标准化整个URL并不适合于单独编码参数值</p>
*
* @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中后的部分
*
* <p>注意此方法只能标准化整个URL并不适合于单独编码参数值</p>
*
* @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<String, String> decodeParamMap(final String paramsStr, final Charset charset) {
final Map<CharSequence, CharSequence> 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<String, List<String>> 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<String, List<String>> decodeParams(final String paramsStr, final Charset charset) {
final Map<CharSequence, CharSequence> queryMap = UrlQuery.of(paramsStr, charset).getQueryMap();
if (MapUtil.isEmpty(queryMap)) {
return MapUtil.empty();
}
final Map<String, List<String>> params = new LinkedHashMap<>();
queryMap.forEach((key, value) -> {
final List<String> 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<String, Object> 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();
}

View File

@@ -46,11 +46,11 @@ public class ClientConfig {
/**
* 是否禁用缓存
*/
public boolean disableCache;
private boolean disableCache;
/**
* 代理
*/
public Proxy proxy;
private Proxy proxy;
/**
* 构造

View File

@@ -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请求消息

View File

@@ -47,7 +47,7 @@ public interface HeaderOperation<T extends HeaderOperation<T>> {
* @return header值
*/
default String header(final Header header) {
return header(header.name());
return header(header.getValue());
}
/**

View File

@@ -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;

View File

@@ -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<Request> {
method = Method.GET;
headers = new HashMap<>();
maxRedirectCount = HttpGlobalConfig.getMaxRedirectCount();
// 全局默认请求头
header(GlobalHeaders.INSTANCE.headers(), false);
}
/**
@@ -198,9 +205,7 @@ public class Request implements HeaderOperation<Request> {
final List<String> values = headers.get(name.trim());
if (isOverride || CollUtil.isEmpty(values)) {
final ArrayList<String> 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<Request> {
return this.body;
}
/**
* 添加请求表单内容
*
* @param formMap 表单内容
* @return this
*/
public Request form(final Map<String, Object> 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<Request> {
// 根据内容赋值默认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<Request> {
this.maxRedirectCount = Math.max(maxRedirectCount, 0);
return this;
}
/**
* 发送请求
*
* @return 响应内容
*/
public Response send() {
return HttpUtil.send(this);
}
}

View File

@@ -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<String, List<String>> 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<Response> consumer) {
consumer.accept(this);
}
}

View File

@@ -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}
*

View File

@@ -25,7 +25,7 @@ public class ClientEngineFactory {
}
/**
* 根据用户引入的拼音引擎jar自动创建对应的拼音引擎对象<br>
* 根据用户引入的HTTP客户端引擎jar自动创建对应的拼音引擎对象<br>
* 推荐创建的引擎单例使用,此方法每次调用会返回新的引擎
*
* @return {@code ClientEngine}

View File

@@ -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();
}
/**
* 构建请求体
*

View File

@@ -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响应包装<br>
@@ -59,6 +64,17 @@ public class HttpClient4Response implements Response {
return null;
}
@Override
public Map<String, List<String>> headers() {
final Header[] headers = rawRes.getAllHeaders();
final HashMap<String, List<String>> result = new LinkedHashMap<>(headers.length, 1);
for (final Header header : headers) {
final List<String> valueList = result.computeIfAbsent(header.getName(), k -> new ArrayList<>());
valueList.add(header.getValue());
}
return result;
}
@Override
public long contentLength() {
return rawRes.getEntity().getContentLength();

View File

@@ -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();
}
/**
* 构建请求体
*

View File

@@ -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响应包装<br>
@@ -59,6 +64,17 @@ public class HttpClient5Response implements Response {
return null;
}
@Override
public Map<String, List<String>> headers() {
final Header[] headers = rawRes.getHeaders();
final HashMap<String, List<String>> result = new LinkedHashMap<>(headers.length, 1);
for (final Header header : headers) {
final List<String> valueList = result.computeIfAbsent(header.getName(), k -> new ArrayList<>());
valueList.add(header.getValue());
}
return result;
}
@Override
public long contentLength() {
return rawRes.getEntity().getContentLength();

View File

@@ -95,6 +95,7 @@ public class HttpResponse implements Response, Closeable {
*
* @return Headers Map
*/
@Override
public Map<String, List<String>> 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 {

View File

@@ -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())//

View File

@@ -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);
}

View File

@@ -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<String, List<String>> headers() {
final Headers headers = rawRes.headers();
final HashMap<String, List<String>> result = new LinkedHashMap<>(headers.size(), 1);
for (final Pair<? extends String, ? extends String> header : headers) {
final List<String> 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);

View File

@@ -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));
}
}
}

View File

@@ -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<SoapClient> {
* @return 返回结果
*/
public SOAPMessage sendForMessage() {
final HttpResponse res = sendForResponse();
final Response res = sendForResponse();
final MimeHeaders headers = new MimeHeaders();
for (final Entry<String, List<String>> entry : res.headers().entrySet()) {
if (StrUtil.isNotEmpty(entry.getKey())) {
@@ -585,15 +586,13 @@ public class SoapClient extends HttpBase<SoapClient> {
*
* @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);
}
/**