2023-10-25 11:38:49 +08:00

1459 lines
37 KiB
Java
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cn.hutool.http;
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.SSLUtil;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.body.FormUrlEncodedBody;
import cn.hutool.http.body.MultipartBody;
import cn.hutool.http.body.RequestBody;
import cn.hutool.http.body.ResourceBody;
import cn.hutool.http.cookie.GlobalCookieManager;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.net.*;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* http请求类<br>
* Http请求类用于构建Http请求并同步获取结果此类通过CookieManager持有域名对应的Cookie值再次请求时会自动附带Cookie信息
*
* @author Looly
*/
public class HttpRequest extends HttpBase<HttpRequest> {
// ---------------------------------------------------------------- static Http Method start
/**
* POST请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest post(String url) {
return of(url).method(Method.POST);
}
/**
* GET请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest get(String url) {
return of(url).method(Method.GET);
}
/**
* HEAD请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest head(String url) {
return of(url).method(Method.HEAD);
}
/**
* OPTIONS请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest options(String url) {
return of(url).method(Method.OPTIONS);
}
/**
* PUT请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest put(String url) {
return of(url).method(Method.PUT);
}
/**
* PATCH请求
*
* @param url URL
* @return HttpRequest
* @since 3.0.9
*/
public static HttpRequest patch(String url) {
return of(url).method(Method.PATCH);
}
/**
* DELETE请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest delete(String url) {
return of(url).method(Method.DELETE);
}
/**
* TRACE请求
*
* @param url URL
* @return HttpRequest
*/
public static HttpRequest trace(String url) {
return of(url).method(Method.TRACE);
}
/**
* 构建一个HTTP请求<br>
* 对于传入的URL可以自定义是否解码已经编码的内容设置见{@link HttpGlobalConfig#setDecodeUrl(boolean)}<br>
* 在构建Http请求时用户传入的URL可能有编码后和未编码的内容混合在一起如果{@link HttpGlobalConfig#isDecodeUrl()}为{@code true},则会统一解码编码后的参数,<br>
* 按照RFC3986规范在发送请求时全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。
*
* @param url URL链接默认自动编码URL中的参数等信息
* @return HttpRequest
* @since 5.7.18
*/
public static HttpRequest of(String url) {
return of(url, HttpGlobalConfig.isDecodeUrl() ? DEFAULT_CHARSET : null);
}
/**
* 构建一个HTTP请求<br>
* 对于传入的URL可以自定义是否解码已经编码的内容。<br>
* 在构建Http请求时用户传入的URL可能有编码后和未编码的内容混合在一起如果charset参数不为{@code null},则会统一解码编码后的参数,<br>
* 按照RFC3986规范在发送请求时全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。
*
* @param url URL链接
* @param charset 编码,如果为{@code null}不自动解码编码URL
* @return HttpRequest
* @since 5.7.18
*/
public static HttpRequest of(String url, Charset charset) {
return of(UrlBuilder.ofHttp(url, charset));
}
/**
* 构建一个HTTP请求<br>
*
* @param url {@link UrlBuilder}
* @return HttpRequest
* @since 5.8.0
*/
public static HttpRequest of(UrlBuilder url) {
return new HttpRequest(url);
}
/**
* 设置全局默认的连接和读取超时时长
*
* @param customTimeout 超时时长
* @see HttpGlobalConfig#setTimeout(int)
* @since 4.6.2
*/
public static void setGlobalTimeout(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(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.create();
private UrlBuilder url;
private URLStreamHandler urlHandler;
private Method method = Method.GET;
/**
* 连接对象
*/
private HttpConnection httpConnection;
/**
* 存储表单数据
*/
private Map<String, Object> form;
/**
* Cookie
*/
private String cookie;
/**
* 是否为Multipart表单
*/
private boolean isMultiPart;
/**
* 是否是REST请求模式
*/
private boolean isRest;
/**
* 重定向次数计数器,内部使用
*/
private int redirectCount;
/**
* 构造URL编码默认使用UTF-8
*
* @param url URL
* @deprecated 请使用 {@link #of(String)}
*/
@Deprecated
public HttpRequest(String url) {
this(UrlBuilder.ofHttp(url));
}
/**
* 构造
*
* @param url {@link UrlBuilder}
*/
public HttpRequest(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(String url) {
return setUrl(UrlBuilder.ofHttp(url, this.charset));
}
/**
* 设置URL
*
* @param urlBuilder url字符串
* @return this
* @since 5.3.1
*/
public HttpRequest setUrl(UrlBuilder urlBuilder) {
this.url = urlBuilder;
return this;
}
/**
* 设置{@link URLStreamHandler}
* <p>
* 部分环境下需要单独设置此项,例如当 WebLogic Server 实例充当 SSL 客户端角色(它会尝试通过 SSL 连接到其他服务器或应用程序)时,<br>
* 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配,则删除此连接。<br>
* 因此weblogic不支持https的sni协议的主机名验证此时需要将此值设置为sun.net.www.protocol.https.Handler对象。
* <p>
* 相关issue见<a href="https://gitee.com/dromara/hutool/issues/IMD1X">https://gitee.com/dromara/hutool/issues/IMD1X</a>
*
* @param urlHandler {@link URLStreamHandler}
* @return this
* @since 4.1.9
*/
public HttpRequest setUrlHandler(URLStreamHandler urlHandler) {
this.urlHandler = urlHandler;
return this;
}
/**
* 获取Http请求方法
*
* @return {@link Method}
* @since 4.1.8
*/
public Method getMethod() {
return this.method;
}
/**
* 设置请求方法
*
* @param method HTTP方法
* @return HttpRequest
* @see #method(Method)
* @since 4.1.8
*/
public HttpRequest setMethod(Method method) {
return method(method);
}
/**
* 获取{@link HttpConnection}<br>
* 在{@link #execute()} 执行前此对象为null
*
* @return {@link HttpConnection}
* @since 4.2.2
*/
public HttpConnection getConnection() {
return this.httpConnection;
}
/**
* 设置请求方法
*
* @param method HTTP方法
* @return HttpRequest
*/
public HttpRequest method(Method method) {
this.method = method;
return this;
}
// ---------------------------------------------------------------- Http Request Header start
/**
* 设置contentType
*
* @param contentType contentType
* @return HttpRequest
*/
public HttpRequest contentType(String contentType) {
header(Header.CONTENT_TYPE, contentType);
return this;
}
/**
* 设置是否为长连接
*
* @param isKeepAlive 是否长连接
* @return HttpRequest
*/
public HttpRequest keepAlive(boolean isKeepAlive) {
header(Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
return this;
}
/**
* @return 获取是否为长连接
*/
public boolean isKeepAlive() {
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(int value) {
header(Header.CONTENT_LENGTH, String.valueOf(value));
return this;
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookies Cookie值数组如果为{@code null}则设置无效使用默认Cookie行为
* @return this
* @since 5.4.1
*/
public HttpRequest cookie(Collection<HttpCookie> cookies) {
return cookie(CollUtil.isEmpty(cookies) ? null : cookies.toArray(new HttpCookie[0]));
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookies Cookie值数组如果为{@code null}则设置无效使用默认Cookie行为
* @return this
* @since 3.1.1
*/
public HttpRequest cookie(HttpCookie... cookies) {
if (ArrayUtil.isEmpty(cookies)) {
return disableCookie();
}
// 名称/值对之间用分号和空格 ('; ')
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Cookie
return cookie(ArrayUtil.join(cookies, "; "));
}
/**
* 设置Cookie<br>
* 自定义Cookie后会覆盖Hutool的默认Cookie行为
*
* @param cookie Cookie值如果为{@code null}则设置无效使用默认Cookie行为
* @return this
* @since 3.0.7
*/
public HttpRequest cookie(String cookie) {
this.cookie = cookie;
return this;
}
/**
* 禁用默认Cookie行为此方法调用后会将Cookie置为空。<br>
* 如果想重新启用Cookie请调用{@link #cookie(String)}方法自定义Cookie。<br>
* 如果想启动默认的Cookie行为自动回填服务器传回的Cookie则调用{@link #enableDefaultCookie()}
*
* @return this
* @since 3.0.7
*/
public HttpRequest disableCookie() {
return cookie(StrUtil.EMPTY);
}
/**
* 打开默认的Cookie行为自动回填服务器传回的Cookie
*
* @return this
*/
public HttpRequest enableDefaultCookie() {
return cookie((String) null);
}
// ---------------------------------------------------------------- Http Request Header end
// ---------------------------------------------------------------- Form start
/**
* 设置表单数据<br>
*
* @param name 名
* @param value 值
* @return this
*/
public HttpRequest form(String name, Object value) {
if (StrUtil.isBlank(name) || ObjectUtil.isNull(value)) {
return this; // 忽略非法的form表单项内容;
}
// 停用body
this.body = null;
if (value instanceof File) {
// 文件上传
return this.form(name, (File) value);
}
if (value instanceof Resource) {
return form(name, (Resource) value);
}
// 普通值
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(String name, Object value, 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(Map<String, Object> formMap) {
if (MapUtil.isNotEmpty(formMap)) {
formMap.forEach(this::form);
}
return this;
}
/**
* 设置map&lt;String, String&gt;类型表单数据
*
* @param formMapStr 表单内容
* @return this
* @since 5.6.7
*/
public HttpRequest formStr(Map<String, String> formMapStr) {
if (MapUtil.isNotEmpty(formMapStr)) {
formMapStr.forEach(this::form);
}
return this;
}
/**
* 文件表单项<br>
* 一旦有文件加入表单变为multipart/form-data
*
* @param name 名
* @param files 需要上传的文件,为空跳过
* @return this
*/
public HttpRequest form(String name, 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));
}
/**
* 文件表单项<br>
* 一旦有文件加入表单变为multipart/form-data
*
* @param name 名
* @param file 需要上传的文件
* @return this
*/
public HttpRequest form(String name, File file) {
return form(name, file, file.getName());
}
/**
* 文件表单项<br>
* 一旦有文件加入表单变为multipart/form-data
*
* @param name 名
* @param file 需要上传的文件
* @param fileName 文件名,为空使用文件默认的文件名
* @return this
*/
public HttpRequest form(String name, File file, String fileName) {
if (null != file) {
form(name, new FileResource(file, fileName));
}
return this;
}
/**
* 文件byte[]表单项<br>
* 一旦有文件加入表单变为multipart/form-data
*
* @param name 名
* @param fileBytes 需要上传的文件
* @param fileName 文件名
* @return this
* @since 4.1.0
*/
public HttpRequest form(String name, byte[] fileBytes, String fileName) {
if (null != fileBytes) {
form(name, new BytesResource(fileBytes, fileName));
}
return this;
}
/**
* 文件表单项<br>
* 一旦有文件加入表单变为multipart/form-data
*
* @param name 名
* @param resource 数据源,文件可以使用{@link FileResource}包装使用
* @return this
* @since 4.0.9
*/
public HttpRequest form(String name, Resource resource) {
if (null != resource) {
if (false == isKeepAlive()) {
keepAlive(true);
}
this.isMultiPart = true;
return putToForm(name, resource);
}
return this;
}
/**
* 获取表单数据
*
* @return 表单Map
*/
public Map<String, Object> form() {
return this.form;
}
/**
* 获取文件表单数据
*
* @return 文件表单Map
* @since 3.3.0
*/
public Map<String, Resource> fileForm() {
final Map<String, Resource> result = MapUtil.newHashMap();
this.form.forEach((key, value) -> {
if (value instanceof Resource) {
result.put(key, (Resource) value);
}
});
return result;
}
// ---------------------------------------------------------------- Form end
// ---------------------------------------------------------------- Body start
/**
* 设置内容主体<br>
* 请求体body参数支持两种类型
*
* <pre>
* 1. 标准参数,例如 a=1&amp;b=2 这种格式
* 2. Rest模式此时body需要传入一个JSON或者XML字符串Hutool会自动绑定其对应的Content-Type
* </pre>
*
* @param body 请求体
* @return this
*/
public HttpRequest body(String body) {
return this.body(body, null);
}
/**
* 设置内容主体<br>
* 请求体body参数支持两种类型
*
* <pre>
* 1. 标准参数,例如 a=1&amp;b=2 这种格式
* 2. Rest模式此时body需要传入一个JSON或者XML字符串Hutool会自动绑定其对应的Content-Type
* </pre>
*
* @param body 请求体
* @param contentType 请求体类型,{@code null}表示自动判断类型
* @return this
*/
public HttpRequest body(String body, String contentType) {
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;
}
/**
* 设置主体字节码<br>
* 需在此方法调用前使用charset方法设置编码否则使用默认编码UTF-8
*
* @param bodyBytes 主体
* @return this
*/
public HttpRequest body(byte[] bodyBytes) {
if (ArrayUtil.isNotEmpty(bodyBytes)) {
return body(new BytesResource(bodyBytes));
}
return this;
}
/**
* 设置主体字节码<br>
* 需在此方法调用前使用charset方法设置编码否则使用默认编码UTF-8
*
* @param resource 主体
* @return this
*/
public HttpRequest body(Resource resource) {
if (null != resource) {
this.body = resource;
}
return this;
}
// ---------------------------------------------------------------- Body end
/**
* 将新的配置加入<br>
* 注意加入的配置可能被修改
*
* @param config 配置
* @return this
*/
public HttpRequest setConfig(HttpConfig config) {
this.config = config;
return this;
}
/**
* 设置超时,单位:毫秒<br>
* 超时包括:
*
* <pre>
* 1. 连接超时
* 2. 读取响应超时
* </pre>
*
* @param milliseconds 超时毫秒数
* @return this
* @see #setConnectionTimeout(int)
* @see #setReadTimeout(int)
*/
public HttpRequest timeout(int milliseconds) {
config.timeout(milliseconds);
return this;
}
/**
* 设置连接超时,单位:毫秒
*
* @param milliseconds 超时毫秒数
* @return this
* @since 4.5.6
*/
public HttpRequest setConnectionTimeout(int milliseconds) {
config.setConnectionTimeout(milliseconds);
return this;
}
/**
* 设置连接超时,单位:毫秒
*
* @param milliseconds 超时毫秒数
* @return this
* @since 4.5.6
*/
public HttpRequest setReadTimeout(int milliseconds) {
config.setReadTimeout(milliseconds);
return this;
}
/**
* 禁用缓存
*
* @return this
*/
public HttpRequest disableCache() {
config.disableCache();
return this;
}
/**
* 设置是否打开重定向如果打开默认重定向次数为2<br>
* 此方法效果与{@link #setMaxRedirectCount(int)} 一致
*
* <p>
* 需要注意的是,当设置为{@code true}时如果全局重定向次数非0直接复用否则设置默认2次。<br>
* 当设置为{@code false}时无论全局是否设置次数都设置为0。<br>
* 不调用此方法的情况下,使用全局默认的次数。
* </p>
*
* @param isFollowRedirects 是否打开重定向
* @return this
*/
public HttpRequest setFollowRedirects(boolean isFollowRedirects) {
if (isFollowRedirects) {
if (config.maxRedirectCount <= 0) {
// 默认两次跳转
return setMaxRedirectCount(2);
}
} else {
// 手动强制关闭重定向,此时不受全局重定向设置影响
if (config.maxRedirectCount < 0) {
return setMaxRedirectCount(0);
}
}
return this;
}
/**
* 自动重定向时是否处理cookie
* @param followRedirectsCookie 自动重定向时是否处理cookie
* @return this
*/
public HttpRequest setFollowRedirectsCookie(boolean followRedirectsCookie) {
config.setFollowRedirectsCookie(followRedirectsCookie);
return this;
}
/**
* 设置最大重定向次数<br>
* 如果次数小于1则表示不重定向大于等于1表示打开重定向
*
* @param maxRedirectCount 最大重定向次数
* @return this
* @since 3.3.0
*/
public HttpRequest setMaxRedirectCount(int maxRedirectCount) {
config.setMaxRedirectCount(maxRedirectCount);
return this;
}
/**
* 设置域名验证器<br>
* 只针对HTTPS请求如果不设置不做验证所有域名被信任
*
* @param hostnameVerifier HostnameVerifier
* @return this
*/
public HttpRequest setHostnameVerifier(HostnameVerifier hostnameVerifier) {
config.setHostnameVerifier(hostnameVerifier);
return this;
}
/**
* 设置Http代理
*
* @param host 代理 主机
* @param port 代理 端口
* @return this
* @since 5.4.5
*/
public HttpRequest setHttpProxy(String host, int port) {
config.setHttpProxy(host, port);
return this;
}
/**
* 设置代理
*
* @param proxy 代理 {@link Proxy}
* @return this
*/
public HttpRequest setProxy(Proxy proxy) {
config.setProxy(proxy);
return this;
}
/**
* 设置SSLSocketFactory<br>
* 只针对HTTPS请求如果不设置使用默认的SSLSocketFactory<br>
* 默认SSLSocketFactory为SSLSocketFactoryBuilder.create().build();
*
* @param ssf SSLScketFactory
* @return this
*/
public HttpRequest setSSLSocketFactory(SSLSocketFactory ssf) {
config.setSSLSocketFactory(ssf);
return this;
}
/**
* 设置HTTPS安全连接协议只针对HTTPS请求可以使用的协议包括<br>
* 此方法调用后{@link #setSSLSocketFactory(SSLSocketFactory)} 将被覆盖。
*
* <pre>
* 1. TLSv1.2
* 2. TLSv1.1
* 3. SSLv3
* ...
* </pre>
*
* @param protocol 协议
* @return this
* @see SSLUtil#createSSLContext(String)
* @see #setSSLSocketFactory(SSLSocketFactory)
*/
public HttpRequest setSSLProtocol(String protocol) {
config.setSSLProtocol(protocol);
return this;
}
/**
* 设置是否rest模式<br>
* rest模式下get请求不会把参数附加到URL之后
*
* @param isRest 是否rest模式
* @return this
* @since 4.5.0
*/
public HttpRequest setRest(boolean isRest) {
this.isRest = isRest;
return this;
}
/**
* 采用流方式上传数据,无需本地缓存数据。<br>
* HttpUrlConnection默认是将所有数据读到本地缓存然后再发送给服务器这样上传大文件时就会导致内存溢出。
*
* @param blockSize 块大小bytes数0或小于0表示不设置Chuncked模式
* @return this
* @since 4.6.5
*/
public HttpRequest setChunkedStreamingMode(int blockSize) {
config.setBlockSize(blockSize);
return this;
}
/**
* 设置拦截器,用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
* @see #addRequestInterceptor(HttpInterceptor)
* @since 5.7.16
*/
public HttpRequest addInterceptor(HttpInterceptor<HttpRequest> interceptor) {
return addRequestInterceptor(interceptor);
}
/**
* 设置拦截器,用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
* @since 5.8.0
*/
public HttpRequest addRequestInterceptor(HttpInterceptor<HttpRequest> interceptor) {
config.addRequestInterceptor(interceptor);
return this;
}
/**
* 设置拦截器,用于在请求前重新编辑请求
*
* @param interceptor 拦截器实现
* @return this
* @since 5.8.0
*/
public HttpRequest addResponseInterceptor(HttpInterceptor<HttpResponse> interceptor) {
config.addResponseInterceptor(interceptor);
return this;
}
/**
* 执行Reuqest请求
*
* @return this
*/
public HttpResponse execute() {
return this.execute(false);
}
/**
* 异步请求<br>
* 异步请求后获取的{@link HttpResponse} 为异步模式,执行完此方法后发送请求到服务器,但是并不立即读取响应内容。<br>
* 此时保持Http连接不关闭直调用获取内容方法为止。
*
* <p>
* 一般执行完execute之后会把响应内容全部读出来放在一个 byte数组里如果你响应的内容太多内存就爆了此法是发送完请求不直接读响应内容等有需要的时候读。
*
* @return 异步对象使用get方法获取HttpResponse对象
*/
public HttpResponse executeAsync() {
return this.execute(true);
}
/**
* 执行Reuqest请求
*
* @param isAsync 是否异步
* @return this
*/
public HttpResponse execute(boolean isAsync) {
return doExecute(isAsync, config.requestInterceptors, config.responseInterceptors);
}
/**
* 执行Request请求后对响应内容后续处理<br>
* 处理结束后关闭连接
*
* @param consumer 响应内容处理函数
* @since 5.7.8
*/
public void then(Consumer<HttpResponse> consumer) {
try (final HttpResponse response = execute(true)) {
consumer.accept(response);
}
}
/**
* 执行Request请求后对响应内容后续处理<br>
* 处理结束后关闭连接
*
* @param <T> 处理结果类型
* @param function 响应内容处理函数
* @return 处理结果
* @since 5.8.5
*/
public <T> T thenFunction(Function<HttpResponse, T> function) {
try (final HttpResponse response = execute(true)) {
return function.apply(response);
}
}
/**
* 简单验证,生成的头信息类似于:
* <pre>
* Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
* </pre>
*
* @param username 用户名
* @param password 密码
* @return this
*/
public HttpRequest basicAuth(String username, String password) {
return auth(HttpUtil.buildBasicAuth(username, password, charset));
}
/**
* 简单代理验证,生成的头信息类似于:
* <pre>
* Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
* </pre>
*
* @param username 用户名
* @param password 密码
* @return this
* @since 5.4.6
*/
public HttpRequest basicProxyAuth(String username, String password) {
return proxyAuth(HttpUtil.buildBasicAuth(username, password, charset));
}
/**
* 令牌验证,生成的头类似于:"Authorization: Bearer XXXXX"一般用于JWT
*
* @param token 令牌内容
* @return HttpRequest
* @since 5.5.3
*/
public HttpRequest bearerAuth(String token) {
return auth("Bearer " + token);
}
/**
* 验证简单插入Authorization头
*
* @param content 验证内容
* @return HttpRequest
* @since 5.2.4
*/
public HttpRequest auth(String content) {
header(Header.AUTHORIZATION, content, true);
return this;
}
/**
* 验证简单插入Authorization头
*
* @param content 验证内容
* @return HttpRequest
* @since 5.4.6
*/
public HttpRequest proxyAuth(String content) {
header(Header.PROXY_AUTHORIZATION, content, true);
return this;
}
@Override
public String toString() {
final StringBuilder sb = StrUtil.builder();
sb.append("Request Url: ").append(this.url.setCharset(this.charset)).append(StrUtil.CRLF);
// header
sb.append("Request Headers: ").append(StrUtil.CRLF);
for (Map.Entry<String, List<String>> entry : this.headers.entrySet()) {
sb.append(" ").append(
entry.getKey()).append(": ").append(CollUtil.join(entry.getValue(), ","))
.append(StrUtil.CRLF);
}
// body
sb.append("Request Body: ").append(StrUtil.CRLF);
sb.append(" ").append(createBody()).append(StrUtil.CRLF);
return sb.toString();
}
// ---------------------------------------------------------------- Private method start
/**
* 执行Reuqest请求
*
* @param isAsync 是否异步
* @param requestInterceptors 请求拦截器列表
* @param responseInterceptors 响应拦截器列表
* @return this
*/
private HttpResponse doExecute(boolean isAsync, HttpInterceptor.Chain<HttpRequest> requestInterceptors,
HttpInterceptor.Chain<HttpResponse> responseInterceptors) {
if (null != requestInterceptors) {
for (HttpInterceptor<HttpRequest> interceptor : requestInterceptors) {
interceptor.process(this);
}
}
// 初始化URL
urlWithParamIfGet();
// 初始化 connection
initConnection();
// 发送请求
send();
// 手动实现重定向
HttpResponse httpResponse = sendRedirectIfPossible(isAsync);
// 获取响应
if (null == httpResponse) {
httpResponse = new HttpResponse(this.httpConnection, this.config, this.charset, isAsync, isIgnoreResponseBody());
}
// 拦截响应
if (null != responseInterceptors) {
for (HttpInterceptor<HttpResponse> interceptor : responseInterceptors) {
interceptor.process(httpResponse);
}
}
return httpResponse;
}
/**
* 初始化网络连接
*/
private void initConnection() {
if (null != this.httpConnection) {
// 执行下次请求时自动关闭上次请求(常用于转发)
this.httpConnection.disconnectQuietly();
}
this.httpConnection = HttpConnection
// issue#I50NHQ
// 在生成正式URL前设置自定义编码
.create(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.setCookie(this.cookie);
} else {
// 读取全局Cookie信息并附带到请求中
GlobalCookieManager.add(this.httpConnection);
}
// 是否禁用缓存
if (config.isDisableCache) {
this.httpConnection.disableCache();
}
}
/**
* 对于GET请求将参数加到URL中<br>
* 此处不对URL中的特殊字符做单独编码<br>
* 对于非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 (null != this.body) {
query.parse(StrUtil.str(this.body.readBytes(), this.charset), this.charset);
} else {
query.addAll(this.form);
}
}
}
/**
* 调用转发,如果需要转发返回转发结果,否则返回{@code null}
*
* @param isAsync 是否异步
* @return {@link HttpResponse},无转发返回 {@code null}
*/
private HttpResponse sendRedirectIfPossible(boolean isAsync) {
// 手动实现重定向
if (config.maxRedirectCount > 0) {
final int responseCode;
try {
responseCode = httpConnection.responseCode();
} catch (IOException e) {
// 错误时静默关闭连接
this.httpConnection.disconnectQuietly();
throw new HttpException(e);
}
// 支持自动重定向时处理cookie
// https://github.com/dromara/hutool/issues/2960
if (config.followRedirectsCookie) {
GlobalCookieManager.store(httpConnection);
}
if (responseCode != HttpURLConnection.HTTP_OK) {
if (HttpStatus.isRedirected(responseCode)) {
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;
}
// issue#3265, 相对路径中可能存在参数,单独处理参数
final String query;
final List<String> split = StrUtil.split(location, '?', 2);
if (split.size() == 2) {
// 存在参数
location = split.get(0);
query = split.get(1);
} else {
query = null;
}
redirectUrl = UrlBuilder.of(this.url.getScheme(), this.url.getHost(), this.url.getPort()
, location, query, null, this.charset);
} else {
redirectUrl = UrlBuilder.ofHttpWithoutEncode(location);
}
setUrl(redirectUrl);
if (redirectCount < config.maxRedirectCount) {
redirectCount++;
// 重定向不再走过滤器
return doExecute(isAsync, config.interceptorOnRedirect ? config.requestInterceptors : null,
config.interceptorOnRedirect ? config.responseInterceptors : null);
}
}
}
}
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 (IOException e) {
// 异常时关闭连接
this.httpConnection.disconnectQuietly();
throw new IORuntimeException(e);
}
}
/**
* 发送普通表单<br>
* 发送数据后自动关闭输出流
*
* @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
createBody().writeClose(this.httpConnection.getOutputStream());
}
/**
* 创建body
*
* @return body
*/
private RequestBody createBody(){
// Write的时候会优先使用body中的内容write时自动关闭OutputStream
if (null != this.body) {
return ResourceBody.create(this.body);
} else {
return FormUrlEncodedBody.create(this.form, this.charset);
}
}
/**
* 发送多组件请求(例如包含文件的表单)<br>
* 发送数据后自动关闭输出流
*
* @throws IOException IO异常
*/
private void sendMultipart() throws IOException {
final RequestBody body;
// issue#3158当用户自定义为multipart同时传入body则不做单独处理
if(null == form && null != this.body) {
body = ResourceBody.create(this.body);
}else{
final MultipartBody multipartBody = MultipartBody.create(this.form, this.charset);
//设置表单类型为Multipart文件上传
this.httpConnection.header(Header.CONTENT_TYPE, multipartBody.getContentType(), true);
body = multipartBody;
}
body.writeClose(this.httpConnection.getOutputStream());
}
/**
* 是否忽略读取响应body部分<br>
* HEAD、CONNECT、TRACE方法将不读取响应体
*
* @return 是否需要忽略响应body部分
* @since 3.1.2
*/
private boolean isIgnoreResponseBody() {
return Method.HEAD == this.method //
|| Method.CONNECT == this.method //
|| Method.TRACE == this.method;
}
/**
* 判断是否为multipart/form-data表单条件如下
*
* <pre>
* 1. 存在资源对象fileForm非空
* 2. 用户自定义头为multipart/form-data开头
* </pre>
*
* @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(String name, 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
}