diff --git a/javascript/common/Utils.js b/javascript/common/Utils.js index 6c960d1..5b7d700 100644 --- a/javascript/common/Utils.js +++ b/javascript/common/Utils.js @@ -1,9 +1,9 @@ -window.Utils = { +class CusUtils { /** * @author youhong.ai * @desc 发起请求 */ - request: function (url, type = "GET", data, isAsync = true, success = () => { + request(url, type = "GET", data, isAsync = true, success = () => { }, error = () => { }, complete = () => { }, contentType = 'application/json', beforeSend = () => { @@ -24,13 +24,13 @@ window.Utils = { options.data = JSON.stringify(data) } return $.ajax(options) - }, + } /** * @author youhong.ai * @desc 发起请求 */ - api: function (requestOptions = { + api(requestOptions = { url: "", type: "GET", data: "", @@ -61,13 +61,13 @@ window.Utils = { } }, requestOptions) return $.ajax(options) - }, + } /** * @author youhong.ai * @desc 获取react组件实例 */ - findReact: function (dom, traverseUp = 0) { + findReact(dom, traverseUp = 0) { const key = Object.keys(dom).find(key => { return key.startsWith("__reactFiber$") // react 17+ || key.startsWith("__reactInternalInstance$") @@ -96,7 +96,7 @@ window.Utils = { compFiber = GetCompFiber(compFiber); } return compFiber.stateNode; - }, + } /** @@ -104,7 +104,7 @@ window.Utils = { * @param fieldName 字段名称 * @returns {*|string} */ - convertNameToIdUtil: function (fieldName) { + convertNameToIdUtil(fieldName) { let fieldIds = []; if (fieldName instanceof Array) { fieldName.forEach(item => { @@ -113,19 +113,19 @@ window.Utils = { return fieldIds.join(',') } return Utils.convertNameObjToId(fieldName) - }, + } /** * 将字段名称转为字段id * @param fieldObj 字段名称对象 {string|object} * @returns {*} */ - convertNameObjToId: function (fieldObj = {fieldName: '', table: 'main'}) { + convertNameObjToId(fieldObj = {fieldName: '', table: 'main'}) { if (typeof fieldObj === 'object') { return WfForm.convertFieldNameToId(fieldObj.fieldName, fieldObj.table) } return WfForm.convertFieldNameToId(fieldObj) - }, + } /** * 根据字段名称查询字段值 @@ -133,17 +133,18 @@ window.Utils = { * @param rowIndex 明细行下表(明细获取才传递) * @returns {*} 字段值 */ - getFiledValueByName: function (fieldName, rowIndex) { + getFiledValueByName(fieldName, rowIndex) { return WfForm.getFieldValue(Utils.convertNameObjToId(fieldName) + (rowIndex ? '_' + rowIndex : '')) - }, + } /** * 通过字段名称修改字段值 * @param fieldName 字段名称 * @param value 值 */ - changeFieldValueByName: function (fieldName, value) { + changeFieldValueByName(fieldName, value) { WfForm.changeFieldValue(Utils.convertNameObjToId(fieldName), {value}) } - } + +window.Utils = new CusUtils() diff --git a/javascript/common/dev.js b/javascript/common/dev.js index cb46f64..331fa30 100644 --- a/javascript/common/dev.js +++ b/javascript/common/dev.js @@ -1,3 +1,10 @@ +const ecodeSDK = {} +ecodeSDK.setCom = (id, name, Com) => { +} +ecodeSDK.imp = (obj) => { +} +ecodeSDK.exp = (obj) => { +} const WfForm = { isMobile: () => { // true表示是eMobile、微信、钉钉等移动终端,false代表PC端 diff --git a/javascript/youhong.ai/pcn/workflow_code_block.js b/javascript/youhong.ai/pcn/workflow_code_block.js index ff52a8a..f99dbcc 100644 --- a/javascript/youhong.ai/pcn/workflow_code_block.js +++ b/javascript/youhong.ai/pcn/workflow_code_block.js @@ -680,4 +680,62 @@ $(() => { }) -/* ******************* 明细数据数量统计添加 end ******************* */ \ No newline at end of file +/* ******************* 明细数据数量统计添加 end ******************* */ + + +/* ******************* 计算年月日 start ******************* */ + +$(() => { + const config = [{ + // 源字段 + sourceField: '', + // 目标字段 + targetField: '', + // 日期加月份字段 + numberField: '' + }, { + sourceField: '', + targetField: '', + numberField: '' + }] + + runJs(); + + function runJs() { + config.forEach(item => { + bindAction(item) + }) + } + + function bindAction(configItem) { + let fieldId = WfForm.convertFieldNameToId(configItem.numberField) + WfForm.bindFieldChangeEvent(fieldId, (obj, id, value) => { + if ("" == value) { + WfForm.changeFieldValue(WfForm.convertFieldNameToId(configItem.targetField, {value: ""})) + return + } + let sourceValue = WfForm.getFieldValue(WfForm.convertFieldNameToId(configItem.sourceField)); + let date = new Date(sourceValue) + date.setMonth(date.getMonth() + +value) + let objectDate = new Date(); + + + let day = objectDate.getDate(); + let month = objectDate.getMonth() + 1; + let year = objectDate.getFullYear(); + WfForm.changeFieldValue(WfForm.convertFieldNameToId(configItem.targetField, { + value: `${year}-${fullNum(month)}-${fullNum(day)}` + })) + }) + } + + function fullNum(i) { + if (i <= 9) { + return '0' + i + } else { + return i + } + } + +}) +/* ******************* 计算年月日 end ******************* */ \ No newline at end of file diff --git a/src/main/java/aiyh/utils/ScriptUtil.java b/src/main/java/aiyh/utils/ScriptUtil.java new file mode 100644 index 0000000..7f6cbd1 --- /dev/null +++ b/src/main/java/aiyh/utils/ScriptUtil.java @@ -0,0 +1,25 @@ +package aiyh.utils; + +import aiyh.utils.tool.org.apache.commons.jexl3.*; + +import java.util.Map; + +/** + *
create: 2023/3/3 23:03
+ * + * @author youHong.ai + */ +public class ScriptUtil { + private static final JexlEngine jexl = new JexlBuilder().create(); + + public static Object invokeScript(String script, Map+ * 1. application/json + * 1. application/xml + *+ * + * @param body 请求参数体 + * @return Content-Type类型,如果无法判断返回null + */ + public static ContentType get(String body) { + ContentType contentType = null; + if (StrUtil.isNotBlank(body)) { + char firstChar = body.charAt(0); + switch (firstChar) { + case '{': + case '[': + // JSON请求体 + contentType = JSON; + break; + case '<': + // XML请求体 + contentType = XML; + break; + + default: + break; + } + } + return contentType; + } + + /** + * 输出Content-Type字符串,附带编码信息 + * + * @param contentType Content-Type类型 + * @param charset 编码 + * @return Content-Type字符串 + * @since 4.5.4 + */ + public static String build(String contentType, Charset charset) { + return StrUtil.format("{};charset={}", contentType, charset.name()); + } + + /** + * 输出Content-Type字符串,附带编码信息 + * + * @param contentType Content-Type 枚举类型 + * @param charset 编码 + * @return Content-Type字符串 + * @since 5.7.15 + */ + public static String build(ContentType contentType, Charset charset) { + return build(contentType.getValue(), charset); + } +} diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/GlobalHeaders.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/GlobalHeaders.java new file mode 100644 index 0000000..49de06c --- /dev/null +++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/GlobalHeaders.java @@ -0,0 +1,229 @@ +package aiyh.utils.tool.cn.hutool.http; + +import aiyh.utils.tool.cn.hutool.core.collection.CollectionUtil; +import aiyh.utils.tool.cn.hutool.core.map.MapUtil; +import aiyh.utils.tool.cn.hutool.core.util.StrUtil; + +import java.util.*; +import java.util.Map.Entry; + +/** + * 全局头部信息
+ * 此类中的方法非线程安全 + *
+ * + *+ * String clean = new HTMLFilter().filter(input); + *+ *
+ * 此类来自:http://xss-html-filter.sf.net
+ *
+ * @author Joseph O'Connell
+ * @author Cal Hendersen
+ * @author Michael Semb Wever
+ */
+public final class HTMLFilter {
+
+ /**
+ * regex flag union representing /si modifiers in php
+ **/
+ private static final int REGEX_FLAGS_SI = Pattern.CASE_INSENSITIVE | Pattern.DOTALL;
+ private static final Pattern P_COMMENTS = Pattern.compile("", Pattern.DOTALL);
+ private static final Pattern P_COMMENT = Pattern.compile("^!--(.*)--$", REGEX_FLAGS_SI);
+ private static final Pattern P_TAGS = Pattern.compile("<(.*?)>", Pattern.DOTALL);
+ private static final Pattern P_END_TAG = Pattern.compile("^/([a-z0-9]+)", REGEX_FLAGS_SI);
+ private static final Pattern P_START_TAG = Pattern.compile("^([a-z0-9]+)(.*?)(/?)$", REGEX_FLAGS_SI);
+ private static final Pattern P_QUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)=([\"'])(.*?)\\2", REGEX_FLAGS_SI);
+ private static final Pattern P_UNQUOTED_ATTRIBUTES = Pattern.compile("([a-z0-9]+)(=)([^\"\\s']+)", REGEX_FLAGS_SI);
+ private static final Pattern P_PROTOCOL = Pattern.compile("^([^:]+):", REGEX_FLAGS_SI);
+ private static final Pattern P_ENTITY = Pattern.compile("(\\d+);?");
+ private static final Pattern P_ENTITY_UNICODE = Pattern.compile("([0-9a-f]+);?");
+ private static final Pattern P_ENCODE = Pattern.compile("%([0-9a-f]{2});?");
+ private static final Pattern P_VALID_ENTITIES = Pattern.compile("&([^&;]*)(?=(;|&|$))");
+ private static final Pattern P_VALID_QUOTES = Pattern.compile("(>|^)([^<]+?)(<|$)", Pattern.DOTALL);
+ private static final Pattern P_END_ARROW = Pattern.compile("^>");
+ private static final Pattern P_BODY_TO_END = Pattern.compile("<([^>]*?)(?=<|$)");
+ private static final Pattern P_XML_CONTENT = Pattern.compile("(^|>)([^<]*?)(?=>)");
+ private static final Pattern P_STRAY_LEFT_ARROW = Pattern.compile("<([^>]*?)(?=<|$)");
+ private static final Pattern P_STRAY_RIGHT_ARROW = Pattern.compile("(^|>)([^<]*?)(?=>)");
+ private static final Pattern P_AMP = Pattern.compile("&");
+ private static final Pattern P_QUOTE = Pattern.compile("\"");
+ private static final Pattern P_LEFT_ARROW = Pattern.compile("<");
+ private static final Pattern P_RIGHT_ARROW = Pattern.compile(">");
+ private static final Pattern P_BOTH_ARROWS = Pattern.compile("<>");
+
+ // @xxx could grow large... maybe use sesat's ReferenceMap
+ private static final ConcurrentMap
+ * 比如我们在使用爬虫爬取HTML页面后,需要对返回页面的HTML内容做一定处理,
+ * 部分环境下需要单独设置此项,例如当 WebLogic Server 实例充当 SSL 客户端角色(它会尝试通过 SSL 连接到其他服务器或应用程序)时,
+ * 相关issue见:https://gitee.com/dromara/hutool/issues/IMD1X
+ *
+ * @param urlHandler {@link URLStreamHandler}
+ * @return this
+ * @since 4.1.9
+ */
+ public HttpRequest setUrlHandler(URLStreamHandler urlHandler) {
+ this.urlHandler = urlHandler;
+ return this;
+ }
+
+ /**
+ * 获取Http请求方法
+ *
+ * @return {@link aiyh.utils.tool.cn.hutool.http.Method}
+ * @since 4.1.8
+ */
+ public aiyh.utils.tool.cn.hutool.http.Method getMethod() {
+ return this.method;
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param method HTTP方法
+ * @return HttpRequest
+ * @see #method(aiyh.utils.tool.cn.hutool.http.Method)
+ * @since 4.1.8
+ */
+ public HttpRequest setMethod(aiyh.utils.tool.cn.hutool.http.Method method) {
+ return method(method);
+ }
+
+ /**
+ * 获取{@link HttpConnection}
+ * 需要注意的是,当设置为{@code true}时,如果全局重定向次数非0,直接复用,否则设置默认2次。
+ * 一般执行完execute之后会把响应内容全部读出来放在一个 byte数组里,如果你响应的内容太多内存就爆了,此法是发送完请求不直接读响应内容,等有需要的时候读。
+ *
+ * @return 异步对象,使用get方法获取HttpResponse对象
+ */
+ public aiyh.utils.tool.cn.hutool.http.HttpResponse executeAsync() {
+ return this.execute(true);
+ }
+
+ /**
+ * 执行Reuqest请求
+ *
+ * @param isAsync 是否异步
+ * @return this
+ */
+ public aiyh.utils.tool.cn.hutool.http.HttpResponse execute(boolean isAsync) {
+ return doExecute(isAsync, config.requestInterceptors, config.responseInterceptors);
+ }
+
+ /**
+ * 执行Request请求后,对响应内容后续处理 注意,此方法只能标准化整个URL,并不适合于单独编码参数值 注意,此方法只能标准化整个URL,并不适合于单独编码参数值
+ * 或者:
+ *
+ *
+ * 见:https://stackoverflow.com/questions/16305486/cookiemanager-for-multiple-threads
+ *
+ * @author looly
+ * @since 4.1.18
+ */
+public class ThreadLocalCookieStore implements CookieStore {
+
+ private final static ThreadLocal
+ * 默认检测的Header:
+ *
+ *
+ * otherHeaderNames参数用于自定义检测的Header
+ * headerNames参数用于自定义检测的Header
+ * 最后发现原来是某些SSL协议没有开启
+ *
+ * @author MikaGuraNTK
+ */
+public class AndroidSupportSSLFactory extends CustomProtocolsSSLFactory {
+
+ // Android低版本不重置的话某些SSL访问就会失败
+ private static final String[] protocols = {
+ SSLProtocols.SSLv3, SSLProtocols.TLSv1, SSLProtocols.TLSv11, SSLProtocols.TLSv12};
+
+ public AndroidSupportSSLFactory() throws IORuntimeException {
+ super(protocols);
+ }
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java
new file mode 100644
index 0000000..22de69b
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/CustomProtocolsSSLFactory.java
@@ -0,0 +1,97 @@
+package aiyh.utils.tool.cn.hutool.http.ssl;
+
+import aiyh.utils.tool.cn.hutool.core.io.IORuntimeException;
+import aiyh.utils.tool.cn.hutool.core.net.SSLUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ArrayUtil;
+
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+
+/**
+ * 自定义支持协议类型的SSLSocketFactory
+ *
+ * @author looly
+ */
+public class CustomProtocolsSSLFactory extends SSLSocketFactory {
+
+ private final String[] protocols;
+ private final SSLSocketFactory base;
+
+ /**
+ * 构造
+ *
+ * @param protocols 支持协议列表
+ * @throws IORuntimeException IO异常
+ */
+ public CustomProtocolsSSLFactory(String... protocols) throws IORuntimeException {
+ this.protocols = protocols;
+ this.base = SSLUtil.createSSLContext(null).getSocketFactory();
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return base.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return base.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket() throws IOException {
+ final SSLSocket sslSocket = (SSLSocket) base.createSocket();
+ resetProtocols(sslSocket);
+ return sslSocket;
+ }
+
+ @Override
+ public SSLSocket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
+ final SSLSocket socket = (SSLSocket) base.createSocket(s, host, port, autoClose);
+ resetProtocols(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ final SSLSocket socket = (SSLSocket) base.createSocket(host, port);
+ resetProtocols(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
+ final SSLSocket socket = (SSLSocket) base.createSocket(host, port, localHost, localPort);
+ resetProtocols(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ final SSLSocket socket = (SSLSocket) base.createSocket(host, port);
+ resetProtocols(socket);
+ return socket;
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+ final SSLSocket socket = (SSLSocket) base.createSocket(address, port, localAddress, localPort);
+ resetProtocols(socket);
+ return socket;
+ }
+
+ /**
+ * 重置可用策略
+ *
+ * @param socket SSLSocket
+ */
+ private void resetProtocols(SSLSocket socket) {
+ if (ArrayUtil.isNotEmpty(this.protocols)) {
+ socket.setEnabledProtocols(this.protocols);
+ }
+ }
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/DefaultSSLFactory.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/DefaultSSLFactory.java
new file mode 100644
index 0000000..45c5d95
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/DefaultSSLFactory.java
@@ -0,0 +1,14 @@
+package aiyh.utils.tool.cn.hutool.http.ssl;
+
+/**
+ * 默认的SSLSocketFactory
+ *
+ * @author Looly
+ * @since 5.1.2
+ */
+public class DefaultSSLFactory extends CustomProtocolsSSLFactory {
+
+ public DefaultSSLFactory() {
+ }
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/DefaultSSLInfo.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/DefaultSSLInfo.java
new file mode 100644
index 0000000..fa4116a
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/DefaultSSLInfo.java
@@ -0,0 +1,32 @@
+package aiyh.utils.tool.cn.hutool.http.ssl;
+
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * 默认的全局SSL配置,当用户未设置相关信息时,使用默认设置,默认设置为单例模式。
+ *
+ * @author looly
+ * @since 5.1.2
+ */
+public class DefaultSSLInfo {
+ /**
+ * 默认信任全部的域名校验器
+ */
+ public static final aiyh.utils.tool.cn.hutool.http.ssl.TrustAnyHostnameVerifier TRUST_ANY_HOSTNAME_VERIFIER;
+ /**
+ * 默认的SSLSocketFactory,区分安卓
+ */
+ public static final SSLSocketFactory DEFAULT_SSF;
+
+ static {
+ TRUST_ANY_HOSTNAME_VERIFIER = new TrustAnyHostnameVerifier();
+ if (StrUtil.equalsIgnoreCase("dalvik", System.getProperty("java.vm.name"))) {
+ // 兼容android低版本SSL连接
+ DEFAULT_SSF = new AndroidSupportSSLFactory();
+ } else {
+ DEFAULT_SSF = new DefaultSSLFactory();
+ }
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java
new file mode 100755
index 0000000..f4d6050
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/SSLSocketFactoryBuilder.java
@@ -0,0 +1,95 @@
+package aiyh.utils.tool.cn.hutool.http.ssl;
+
+import aiyh.utils.tool.cn.hutool.core.net.SSLContextBuilder;
+import aiyh.utils.tool.cn.hutool.core.net.SSLProtocols;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+/**
+ * SSLSocketFactory构建器
+ *
+ * @author Looly
+ * @see SSLContextBuilder
+ * @deprecated 请使用 {@link SSLContextBuilder}
+ */
+@Deprecated
+public class SSLSocketFactoryBuilder implements SSLProtocols {
+
+ SSLContextBuilder sslContextBuilder;
+
+ /**
+ * 构造
+ */
+ public SSLSocketFactoryBuilder() {
+ this.sslContextBuilder = SSLContextBuilder.create();
+ }
+
+ /**
+ * 创建 SSLSocketFactoryBuilder
+ *
+ * @return SSLSocketFactoryBuilder
+ */
+ public static SSLSocketFactoryBuilder create() {
+ return new SSLSocketFactoryBuilder();
+ }
+
+ /**
+ * 设置协议
+ *
+ * @param protocol 协议
+ * @return 自身
+ */
+ public SSLSocketFactoryBuilder setProtocol(String protocol) {
+ this.sslContextBuilder.setProtocol(protocol);
+ return this;
+ }
+
+ /**
+ * 设置信任信息
+ *
+ * @param trustManagers TrustManager列表
+ * @return 自身
+ */
+ public SSLSocketFactoryBuilder setTrustManagers(TrustManager... trustManagers) {
+ this.sslContextBuilder.setTrustManagers(trustManagers);
+ return this;
+ }
+
+ /**
+ * 设置 JSSE key managers
+ *
+ * @param keyManagers JSSE key managers
+ * @return 自身
+ */
+ public SSLSocketFactoryBuilder setKeyManagers(KeyManager... keyManagers) {
+ this.sslContextBuilder.setKeyManagers(keyManagers);
+ return this;
+ }
+
+ /**
+ * 设置 SecureRandom
+ *
+ * @param secureRandom SecureRandom
+ * @return 自己
+ */
+ public SSLSocketFactoryBuilder setSecureRandom(SecureRandom secureRandom) {
+ this.sslContextBuilder.setSecureRandom(secureRandom);
+ return this;
+ }
+
+ /**
+ * 构建SSLSocketFactory
+ *
+ * @return SSLSocketFactory
+ * @throws NoSuchAlgorithmException 无此算法
+ * @throws KeyManagementException Key管理异常
+ */
+ public SSLSocketFactory build() throws NoSuchAlgorithmException, KeyManagementException {
+ return this.sslContextBuilder.buildChecked().getSocketFactory();
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java
new file mode 100644
index 0000000..e60a685
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/TrustAnyHostnameVerifier.java
@@ -0,0 +1,17 @@
+package aiyh.utils.tool.cn.hutool.http.ssl;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSession;
+
+/**
+ * https 域名校验
+ *
+ * @author Looly
+ */
+public class TrustAnyHostnameVerifier implements HostnameVerifier {
+
+ @Override
+ public boolean verify(String hostname, SSLSession session) {
+ return true;// 直接返回true
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/package-info.java
new file mode 100644
index 0000000..3de697d
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * SSL封装
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http.ssl;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Browser.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Browser.java
new file mode 100755
index 0000000..f75fc07
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Browser.java
@@ -0,0 +1,140 @@
+package aiyh.utils.tool.cn.hutool.http.useragent;
+
+import aiyh.utils.tool.cn.hutool.core.collection.CollUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ReUtil;
+
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * 浏览器对象
+ *
+ * @author looly
+ * @since 4.2.1
+ */
+public class Browser extends UserAgentInfo {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 未知
+ */
+ public static final Browser Unknown = new Browser(NameUnknown, null, null);
+ /**
+ * 其它版本
+ */
+ public static final String Other_Version = "[\\/ ]([\\d\\w\\.\\-]+)";
+
+ /**
+ * 支持的浏览器类型
+ */
+ public static final List
+ * 此对象用于构建一个SOAP消息,并通过HTTP接口发出消息内容。
+ * SOAP消息本质上是一个XML文本,可以通过调用{@link #getMsgStr(boolean)} 方法获取消息体
+ *
+ * 使用方法:
+ *
+ *
+ * 重置后需调用serMethod方法重新指定请求方法,并调用setParam方法重新定义参数
+ *
+ * @return this
+ * @since 4.6.7
+ */
+ public SoapClient reset() {
+ try {
+ this.message = factory.createMessage();
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+ this.methodEle = null;
+
+ return this;
+ }
+
+ /**
+ * 设置编码
+ *
+ * @param charset 编码
+ * @return this
+ * @see #charset(Charset)
+ */
+ public SoapClient setCharset(Charset charset) {
+ return this.charset(charset);
+ }
+
+ @Override
+ public SoapClient charset(Charset charset) {
+ super.charset(charset);
+ try {
+ this.message.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, this.charset());
+ this.message.setProperty(SOAPMessage.WRITE_XML_DECLARATION, "true");
+ } catch (SOAPException e) {
+ // ignore
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置Webservice请求地址
+ *
+ * @param url Webservice请求地址
+ * @return this
+ */
+ public SoapClient setUrl(String url) {
+ this.url = url;
+ return this;
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param name 头信息标签名
+ * @param actorURI 中间的消息接收者
+ * @param roleUri Role的URI
+ * @param mustUnderstand 标题项对于要对其进行处理的接收者来说是强制的还是可选的
+ * @param relay relay属性
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.4
+ */
+ public SOAPHeaderElement addSOAPHeader(QName name, String actorURI, String roleUri, Boolean mustUnderstand, Boolean relay) {
+ final SOAPHeaderElement ele = addSOAPHeader(name);
+ try {
+ if (StrUtil.isNotBlank(roleUri)) {
+ ele.setRole(roleUri);
+ }
+ if (null != relay) {
+ ele.setRelay(relay);
+ }
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ if (StrUtil.isNotBlank(actorURI)) {
+ ele.setActor(actorURI);
+ }
+ if (null != mustUnderstand) {
+ ele.setMustUnderstand(mustUnderstand);
+ }
+
+ return ele;
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param localName 头节点名称
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.7
+ */
+ public SOAPHeaderElement addSOAPHeader(String localName) {
+ return addSOAPHeader(new QName(localName));
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param localName 头节点名称
+ * @param value 头节点的值
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.7
+ */
+ public SOAPHeaderElement addSOAPHeader(String localName, String value) {
+ final SOAPHeaderElement soapHeaderElement = addSOAPHeader(localName);
+ soapHeaderElement.setTextContent(value);
+ return soapHeaderElement;
+ }
+
+ /**
+ * 增加SOAP头信息,方法返回{@link SOAPHeaderElement}可以设置具体属性和子节点
+ *
+ * @param name 头节点名称
+ * @return {@link SOAPHeaderElement}
+ * @since 5.4.4
+ */
+ public SOAPHeaderElement addSOAPHeader(QName name) {
+ SOAPHeaderElement ele;
+ try {
+ ele = this.message.getSOAPHeader().addHeaderElement(name);
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+ return ele;
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param name 方法名及其命名空间
+ * @param params 参数
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ */
+ public SoapClient setMethod(Name name, Map create: 2023/5/4 17:13 create: 2023/5/4 17:35 create: 2023/5/4 17:23
+ * Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
+ *
+ */
+ AUTHORIZATION("Authorization"),
+ /**
+ * 提供给代理服务器的用于身份验证的凭证,例如:
+ *
+ * Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
+ *
+ */
+ PROXY_AUTHORIZATION("Proxy-Authorization"),
+ /**
+ * 提供日期和时间标志,说明报文是什么时间创建的
+ */
+ DATE("Date"),
+ /**
+ * 允许客户端和服务器指定与请求/响应连接有关的选项
+ */
+ CONNECTION("Connection"),
+ /**
+ * 给出发送端使用的MIME版本
+ */
+ MIME_VERSION("MIME-Version"),
+ /**
+ * 如果报文采用了分块传输编码(chunked transfer encoding) 方式,就可以用这个首部列出位于报文拖挂(trailer)部分的首部集合
+ */
+ TRAILER("Trailer"),
+ /**
+ * 告知接收端为了保证报文的可靠传输,对报文采用了什么编码方式
+ */
+ TRANSFER_ENCODING("Transfer-Encoding"),
+ /**
+ * 给出了发送端可能想要"升级"使用的新版本和协议
+ */
+ UPGRADE("Upgrade"),
+ /**
+ * 显示了报文经过的中间节点
+ */
+ VIA("Via"),
+ /**
+ * 指定请求和响应遵循的缓存机制
+ */
+ CACHE_CONTROL("Cache-Control"),
+ /**
+ * 用来包含实现特定的指令,最常用的是Pragma:no-cache。在HTTP/1.1协议中,它的含义和Cache- Control:no-cache相同
+ */
+ PRAGMA("Pragma"),
+ /**
+ * 请求表示提交内容类型或返回返回内容的MIME类型
+ */
+ CONTENT_TYPE("Content-Type"),
+
+ //------------------------------------------------------------- 请求头域
+ /**
+ * 指定请求资源的Intenet主机和端口号,必须表示请求url的原始服务器或网关的位置。HTTP/1.1请求必须包含主机头域,否则系统会以400状态码返回
+ */
+ HOST("Host"),
+ /**
+ * 允许客户端指定请求uri的源资源地址,这可以允许服务器生成回退链表,可用来登陆、优化cache等。他也允许废除的或错误的连接由于维护的目的被 追踪。如果请求的uri没有自己的uri地址,Referer不能被发送。如果指定的是部分uri地址,则此地址应该是一个相对地址
+ */
+ REFERER("Referer"),
+ /**
+ * 指定请求的域
+ */
+ ORIGIN("Origin"),
+ /**
+ * HTTP客户端运行的浏览器类型的详细信息。通过该头部信息,web服务器可以判断到当前HTTP请求的客户端浏览器类别
+ */
+ USER_AGENT("User-Agent"),
+ /**
+ * 指定客户端能够接收的内容类型,内容类型中的先后次序表示客户端接收的先后次序
+ */
+ ACCEPT("Accept"),
+ /**
+ * 指定HTTP客户端浏览器用来展示返回信息所优先选择的语言
+ */
+ ACCEPT_LANGUAGE("Accept-Language"),
+ /**
+ * 指定客户端浏览器可以支持的web服务器返回内容压缩编码类型
+ */
+ ACCEPT_ENCODING("Accept-Encoding"),
+ /**
+ * 浏览器可以接受的字符编码集
+ */
+ ACCEPT_CHARSET("Accept-Charset"),
+ /**
+ * HTTP请求发送时,会把保存在该请求域名下的所有cookie值一起发送给web服务器
+ */
+ COOKIE("Cookie"),
+ /**
+ * 请求的内容长度
+ */
+ CONTENT_LENGTH("Content-Length"),
+
+ //------------------------------------------------------------- 响应头域
+ /**
+ * 提供WWW验证响应头
+ */
+ WWW_AUTHENTICATE("WWW-Authenticate"),
+ /**
+ * Cookie
+ */
+ SET_COOKIE("Set-Cookie"),
+ /**
+ * Content-Encoding
+ */
+ CONTENT_ENCODING("Content-Encoding"),
+ /**
+ * Content-Disposition
+ */
+ CONTENT_DISPOSITION("Content-Disposition"),
+ /**
+ * ETag
+ */
+ ETAG("ETag"),
+ /**
+ * 重定向指示到的URL
+ */
+ LOCATION("Location");
+
+ private final String value;
+
+ Header(String value) {
+ this.value = value;
+ }
+
+ /**
+ * 获取值
+ *
+ * @return 值
+ */
+ public String getValue() {
+ return this.value;
+ }
+
+ @Override
+ public String toString() {
+ return getValue();
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HtmlUtil.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HtmlUtil.java
new file mode 100755
index 0000000..7b9f040
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HtmlUtil.java
@@ -0,0 +1,212 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.util.EscapeUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ReUtil;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+/**
+ * HTML工具类
+ *
+ *
+ * 比如去掉指定标签(例如广告栏等)、去除JS、去掉样式等等,这些操作都可以使用此工具类完成。
+ *
+ * @author xiaoleilu
+ */
+public class HtmlUtil {
+
+ public static final String NBSP = StrUtil.HTML_NBSP;
+ public static final String AMP = StrUtil.HTML_AMP;
+ public static final String QUOTE = StrUtil.HTML_QUOTE;
+ public static final String APOS = StrUtil.HTML_APOS;
+ public static final String LT = StrUtil.HTML_LT;
+ public static final String GT = StrUtil.HTML_GT;
+
+ public static final String RE_HTML_MARK = "(<[^<]*?>)|(<[\\s]*?/[^<]*?>)|(<[^<]*?/[\\s]*?>)";
+ public static final String RE_SCRIPT = "<[\\s]*?script[^>]*?>.*?<[\\s]*?\\/[\\s]*?script[\\s]*?>";
+
+ private static final char[][] TEXT = new char[256][];
+
+ static {
+ // ascii码值最大的是【0x7f=127】,扩展ascii码值最大的是【0xFF=255】,因为ASCII码使用指定的7位或8位二进制数组合来表示128或256种可能的字符,标准ASCII码也叫基础ASCII码。
+ for (int i = 0; i < 256; i++) {
+ TEXT[i] = new char[]{(char) i};
+ }
+
+ // special HTML characters
+ TEXT['\''] = "'".toCharArray(); // 单引号 (''' doesn't work - it is not by the w3 specs)
+ TEXT['"'] = QUOTE.toCharArray(); // 双引号
+ TEXT['&'] = AMP.toCharArray(); // &符
+ TEXT['<'] = LT.toCharArray(); // 小于号
+ TEXT['>'] = GT.toCharArray(); // 大于号
+ TEXT[' '] = NBSP.toCharArray(); // 不断开空格(non-breaking space,缩写nbsp。ASCII值是32:是用键盘输入的空格;ASCII值是160:不间断空格,即  ,所产生的空格,作用是在页面换行时不被打断)
+ }
+
+ /**
+ * 转义文本中的HTML字符为安全的字符,以下字符被转义:
+ *
+ *
+ *
+ * @param text 被转义的文本
+ * @return 转义后的文本
+ */
+ public static String escape(String text) {
+ return encode(text);
+ }
+
+ /**
+ * 还原被转义的HTML特殊字符
+ *
+ * @param htmlStr 包含转义符的HTML内容
+ * @return 转换后的字符串
+ */
+ public static String unescape(String htmlStr) {
+ if (StrUtil.isBlank(htmlStr)) {
+ return htmlStr;
+ }
+
+ return EscapeUtil.unescapeHtml4(htmlStr);
+ }
+
+ // ---------------------------------------------------------------- encode text
+
+ /**
+ * 清除所有HTML标签,但是不删除标签内的内容
+ *
+ * @param content 文本
+ * @return 清除标签后的文本
+ */
+ public static String cleanHtmlTag(String content) {
+ return content.replaceAll(RE_HTML_MARK, "");
+ }
+
+ /**
+ * 清除指定HTML标签和被标签包围的内容
+ * 不区分大小写
+ *
+ * @param content 文本
+ * @param tagNames 要清除的标签
+ * @return 去除标签后的文本
+ */
+ public static String removeHtmlTag(String content, String... tagNames) {
+ return removeHtmlTag(content, true, tagNames);
+ }
+
+ /**
+ * 清除指定HTML标签,不包括内容
+ * 不区分大小写
+ *
+ * @param content 文本
+ * @param tagNames 要清除的标签
+ * @return 去除标签后的文本
+ */
+ public static String unwrapHtmlTag(String content, String... tagNames) {
+ return removeHtmlTag(content, false, tagNames);
+ }
+
+ /**
+ * 清除指定HTML标签
+ * 不区分大小写
+ *
+ * @param content 文本
+ * @param withTagContent 是否去掉被包含在标签中的内容
+ * @param tagNames 要清除的标签
+ * @return 去除标签后的文本
+ */
+ public static String removeHtmlTag(String content, boolean withTagContent, String... tagNames) {
+ String regex;
+ for (String tagName : tagNames) {
+ if (StrUtil.isBlank(tagName)) {
+ continue;
+ }
+ tagName = tagName.trim();
+ // (?i)表示其后面的表达式忽略大小写
+ if (withTagContent) {
+ // 标签及其包含内容
+ regex = StrUtil.format("(?i)<{}(\\s+[^>]*?)?/?>(.*?{}>)?", tagName, tagName);
+ } else {
+ // 标签不包含内容
+ regex = StrUtil.format("(?i)<{}(\\s+[^>]*?)?/?>|?{}>", tagName, tagName);
+ }
+
+ content = ReUtil.delAll(regex, content); // 非自闭标签小写
+ }
+ return content;
+ }
+
+ /**
+ * 去除HTML标签中的属性,如果多个标签有相同属性,都去除
+ *
+ * @param content 文本
+ * @param attrs 属性名(不区分大小写)
+ * @return 处理后的文本
+ */
+ public static String removeHtmlAttr(String content, String... attrs) {
+ String regex;
+ for (String attr : attrs) {
+ // (?i) 表示忽略大小写
+ // \s* 属性名前后的空白符去除
+ // [^>]+? 属性值,至少有一个非>的字符,>表示标签结束
+ // \s+(?=>) 表示属性值后跟空格加>,即末尾的属性,此时去掉空格
+ // (?=\s|>) 表示属性值后跟空格(属性后还有别的属性)或者跟>(最后一个属性)
+ regex = StrUtil.format("(?i)(\\s*{}\\s*=[^>]+?\\s+(?=>))|(\\s*{}\\s*=[^>]+?(?=\\s|>))", attr, attr);
+ content = content.replaceAll(regex, StrUtil.EMPTY);
+ }
+ return content;
+ }
+
+ /**
+ * 去除指定标签的所有属性
+ *
+ * @param content 内容
+ * @param tagNames 指定标签
+ * @return 处理后的文本
+ */
+ public static String removeAllHtmlAttr(String content, String... tagNames) {
+ String regex;
+ for (String tagName : tagNames) {
+ regex = StrUtil.format("(?i)<{}[^>]*?>", tagName);
+ content = content.replaceAll(regex, StrUtil.format("<{}>", tagName));
+ }
+ return content;
+ }
+
+ /**
+ * Encoder
+ *
+ * @param text 被编码的文本
+ * @return 编码后的字符
+ */
+ private static String encode(String text) {
+ int len;
+ if ((text == null) || ((len = text.length()) == 0)) {
+ return StrUtil.EMPTY;
+ }
+ StringBuilder buffer = new StringBuilder(len + (len >> 2));
+ char c;
+ for (int i = 0; i < len; i++) {
+ c = text.charAt(i);
+ if (c < 256) {
+ buffer.append(TEXT[c]);
+ } else {
+ buffer.append(c);
+ }
+ }
+ return buffer.toString();
+ }
+
+ /**
+ * 过滤HTML文本,防止XSS攻击
+ *
+ * @param htmlContent HTML内容
+ * @return 过滤后的内容
+ */
+ public static String filter(String htmlContent) {
+ return new HTMLFilter().filter(htmlContent);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpBase.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpBase.java
new file mode 100644
index 0000000..38042ca
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpBase.java
@@ -0,0 +1,357 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.collection.CollUtil;
+import aiyh.utils.tool.cn.hutool.core.collection.CollectionUtil;
+import aiyh.utils.tool.cn.hutool.core.map.CaseInsensitiveMap;
+import aiyh.utils.tool.cn.hutool.core.map.MapUtil;
+import aiyh.utils.tool.cn.hutool.core.util.CharsetUtil;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.Map.Entry;
+
+/**
+ * http基类
+ *
+ * @param
+ * 根据RFC2616规范,header的name不区分大小写
+ *
+ * @param name Header名
+ * @return Header值
+ */
+ public String header(String name) {
+ final List
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中
+ *
+ * @param name Header名
+ * @param value Header值
+ * @param isOverride 是否覆盖已有值
+ * @return T 本身
+ */
+ public T header(String name, String value, boolean isOverride) {
+ if (null != name && null != value) {
+ final List
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中
+ *
+ * @param name Header名
+ * @param value Header值
+ * @param isOverride 是否覆盖已有值
+ * @return T 本身
+ */
+ public T header(Header name, String value, boolean isOverride) {
+ return header(name.toString(), value, isOverride);
+ }
+
+ /**
+ * 设置一个header
+ * 覆盖模式,则替换之前的值
+ *
+ * @param name Header名
+ * @param value Header值
+ * @return T 本身
+ */
+ public T header(Header name, String value) {
+ return header(name.toString(), value, true);
+ }
+
+ /**
+ * 设置一个header
+ * 覆盖模式,则替换之前的值
+ *
+ * @param name Header名
+ * @param value Header值
+ * @return T 本身
+ */
+ public T header(String name, String value) {
+ return header(name, value, true);
+ }
+
+ /**
+ * 设置请求头
+ *
+ * @param headers 请求头
+ * @param isOverride 是否覆盖已有头信息
+ * @return this
+ * @since 4.6.3
+ */
+ public T headerMap(Map
+ * 不覆盖原有请求头
+ *
+ * @param headers 请求头
+ * @return this
+ */
+ public T header(Map
+ * 不覆盖原有请求头
+ *
+ * @param headers 请求头
+ * @return this
+ * @since 4.0.3
+ */
+ public T addHeaders(Map
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
+ */
+ boolean ignoreEOFError = HttpGlobalConfig.isIgnoreEOFError();
+ /**
+ * 获取是否忽略解码URL,包括URL中的Path部分和Param部分。
+ * 在构建Http请求时,用户传入的URL可能有编码后和未编码的内容混合在一起,如果此参数为{@code true},则会统一解码编码后的参数,
+ * 按照RFC3986规范,在发送请求时,全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。
+ */
+ boolean decodeUrl = HttpGlobalConfig.isDecodeUrl();
+
+ /**
+ * 请求前的拦截器,用于在请求前重新编辑请求
+ */
+ final HttpInterceptor.Chain
+ * 超时包括:
+ *
+ *
+ * 1. 连接超时
+ * 2. 读取响应超时
+ *
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @see #setConnectionTimeout(int)
+ * @see #setReadTimeout(int)
+ */
+ public HttpConfig timeout(int milliseconds) {
+ setConnectionTimeout(milliseconds);
+ setReadTimeout(milliseconds);
+ return this;
+ }
+
+ /**
+ * 设置连接超时,单位:毫秒
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ */
+ public HttpConfig setConnectionTimeout(int milliseconds) {
+ this.connectionTimeout = milliseconds;
+ return this;
+ }
+
+ /**
+ * 设置连接超时,单位:毫秒
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ */
+ public HttpConfig setReadTimeout(int milliseconds) {
+ this.readTimeout = milliseconds;
+ return this;
+ }
+
+ /**
+ * 禁用缓存
+ *
+ * @return this
+ */
+ public HttpConfig disableCache() {
+ this.isDisableCache = true;
+ return this;
+ }
+
+ /**
+ * 设置最大重定向次数
+ * 如果次数小于1则表示不重定向,大于等于1表示打开重定向
+ *
+ * @param maxRedirectCount 最大重定向次数
+ * @return this
+ */
+ public HttpConfig setMaxRedirectCount(int maxRedirectCount) {
+ this.maxRedirectCount = Math.max(maxRedirectCount, 0);
+ return this;
+ }
+
+ /**
+ * 设置域名验证器
+ * 只针对HTTPS请求,如果不设置,不做验证,所有域名被信任
+ *
+ * @param hostnameVerifier HostnameVerifier
+ * @return this
+ */
+ public HttpConfig setHostnameVerifier(HostnameVerifier hostnameVerifier) {
+ // 验证域
+ this.hostnameVerifier = hostnameVerifier;
+ return this;
+ }
+
+ /**
+ * 设置Http代理
+ *
+ * @param host 代理 主机
+ * @param port 代理 端口
+ * @return this
+ */
+ public HttpConfig setHttpProxy(String host, int port) {
+ final Proxy proxy = new Proxy(Proxy.Type.HTTP,
+ new InetSocketAddress(host, port));
+ return setProxy(proxy);
+ }
+
+ /**
+ * 设置代理
+ *
+ * @param proxy 代理 {@link Proxy}
+ * @return this
+ */
+ public HttpConfig setProxy(Proxy proxy) {
+ this.proxy = proxy;
+ return this;
+ }
+
+ /**
+ * 设置SSLSocketFactory
+ * 只针对HTTPS请求,如果不设置,使用默认的SSLSocketFactory
+ * 默认SSLSocketFactory为:SSLSocketFactoryBuilder.create().build();
+ *
+ * @param ssf SSLScketFactory
+ * @return this
+ */
+ public HttpConfig setSSLSocketFactory(SSLSocketFactory ssf) {
+ this.ssf = 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 HttpConfig setSSLProtocol(String protocol) {
+ Assert.notBlank(protocol, "protocol must be not blank!");
+ setSSLSocketFactory(SSLUtil.createSSLContext(protocol).getSocketFactory());
+ return this;
+ }
+
+ /**
+ * 采用流方式上传数据,无需本地缓存数据。
+ * HttpUrlConnection默认是将所有数据读到本地缓存,然后再发送给服务器,这样上传大文件时就会导致内存溢出。
+ *
+ * @param blockSize 块大小(bytes数),0或小于0表示不设置Chuncked模式
+ * @return this
+ */
+ public HttpConfig setBlockSize(int blockSize) {
+ this.blockSize = blockSize;
+ return this;
+ }
+
+ /**
+ * 设置是否忽略响应读取时可能的EOF异常。
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
+ *
+ * @param ignoreEOFError 是否忽略响应读取时可能的EOF异常。
+ * @return this
+ * @since 5.7.20
+ */
+ public HttpConfig setIgnoreEOFError(boolean ignoreEOFError) {
+ this.ignoreEOFError = ignoreEOFError;
+ return this;
+ }
+
+ /**
+ * 设置是否忽略解码URL,包括URL中的Path部分和Param部分。
+ * 在构建Http请求时,用户传入的URL可能有编码后和未编码的内容混合在一起,如果此参数为{@code true},则会统一解码编码后的参数,
+ * 按照RFC3986规范,在发送请求时,全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。
+ *
+ * @param decodeUrl 是否忽略解码URL
+ * @return this
+ */
+ public HttpConfig setDecodeUrl(boolean decodeUrl) {
+ this.decodeUrl = decodeUrl;
+ return this;
+ }
+
+ /**
+ * 设置拦截器,用于在请求前重新编辑请求
+ *
+ * @param interceptor 拦截器实现
+ * @return this
+ */
+ public HttpConfig addRequestInterceptor(HttpInterceptor
+ * 当请求头存在时,覆盖之
+ *
+ * @param header 头名
+ * @param value 头值
+ * @param isOverride 是否覆盖旧值
+ * @return HttpConnection
+ */
+ public HttpConnection header(String header, String value, boolean isOverride) {
+ if (null != this.conn) {
+ if (isOverride) {
+ this.conn.setRequestProperty(header, value);
+ } else {
+ this.conn.addRequestProperty(header, value);
+ }
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置请求头
+ * 当请求头存在时,覆盖之
+ *
+ * @param header 头名
+ * @param value 头值
+ * @param isOverride 是否覆盖旧值
+ * @return HttpConnection
+ */
+ public HttpConnection header(aiyh.utils.tool.cn.hutool.http.Header header, String value, boolean isOverride) {
+ return header(header.toString(), value, isOverride);
+ }
+
+ /**
+ * 设置请求头
+ * 不覆盖原有请求头
+ *
+ * @param headerMap 请求头
+ * @param isOverride 是否覆盖
+ * @return this
+ */
+ public HttpConnection header(Map
+ * 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
+ *
+ * @param hostnameVerifier 域名验证器,非https传入null
+ * @param ssf SSLSocketFactory,非https传入null
+ * @return this
+ * @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装
+ */
+ public HttpConnection setHttpsInfo(HostnameVerifier hostnameVerifier, SSLSocketFactory ssf) throws HttpException {
+ final HttpURLConnection conn = this.conn;
+
+ if (conn instanceof HttpsURLConnection) {
+ // Https请求
+ final HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
+ // 验证域
+ httpsConn.setHostnameVerifier(ObjectUtil.defaultIfNull(hostnameVerifier, DefaultSSLInfo.TRUST_ANY_HOSTNAME_VERIFIER));
+ httpsConn.setSSLSocketFactory(ObjectUtil.defaultIfNull(ssf, DefaultSSLInfo.DEFAULT_SSF));
+ }
+
+ return this;
+ }
+
+ /**
+ * 关闭缓存
+ *
+ * @return this
+ * @see HttpURLConnection#setUseCaches(boolean)
+ */
+ public HttpConnection disableCache() {
+ this.conn.setUseCaches(false);
+ return this;
+ }
+
+ /**
+ * 设置连接超时
+ *
+ * @param timeout 超时
+ * @return this
+ */
+ public HttpConnection setConnectTimeout(int timeout) {
+ if (timeout > 0 && null != this.conn) {
+ this.conn.setConnectTimeout(timeout);
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置读取超时
+ *
+ * @param timeout 超时
+ * @return this
+ */
+ public HttpConnection setReadTimeout(int timeout) {
+ if (timeout > 0 && null != this.conn) {
+ this.conn.setReadTimeout(timeout);
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置连接和读取的超时时间
+ *
+ * @param timeout 超时时间
+ * @return this
+ */
+ public HttpConnection setConnectionAndReadTimeout(int timeout) {
+ setConnectTimeout(timeout);
+ setReadTimeout(timeout);
+
+ return this;
+ }
+
+ /**
+ * 设置Cookie
+ *
+ * @param cookie Cookie
+ * @return this
+ */
+ public HttpConnection setCookie(String cookie) {
+ if (cookie != null) {
+ header(Header.COOKIE, cookie, true);
+ }
+ return this;
+ }
+
+ /**
+ * 采用流方式上传数据,无需本地缓存数据。
+ * HttpUrlConnection默认是将所有数据读到本地缓存,然后再发送给服务器,这样上传大文件时就会导致内存溢出。
+ *
+ * @param blockSize 块大小(bytes数),0或小于0表示不设置Chuncked模式
+ * @return this
+ */
+ public HttpConnection setChunkedStreamingMode(int blockSize) {
+ if (blockSize > 0) {
+ conn.setChunkedStreamingMode(blockSize);
+ }
+ return this;
+ }
+
+ /**
+ * 设置自动HTTP 30X跳转
+ *
+ * @param isInstanceFollowRedirects 是否自定跳转
+ * @return this
+ */
+ public HttpConnection setInstanceFollowRedirects(boolean isInstanceFollowRedirects) {
+ conn.setInstanceFollowRedirects(isInstanceFollowRedirects);
+ return this;
+ }
+
+ /**
+ * 连接
+ *
+ * @return this
+ * @throws IOException IO异常
+ */
+ public HttpConnection connect() throws IOException {
+ if (null != this.conn) {
+ this.conn.connect();
+ }
+ return this;
+ }
+
+ /**
+ * 静默断开连接。不抛出异常
+ *
+ * @return this
+ * @since 4.6.0
+ */
+ public HttpConnection disconnectQuietly() {
+ try {
+ disconnect();
+ } catch (Throwable e) {
+ // ignore
+ }
+
+ return this;
+ }
+
+ /**
+ * 断开连接
+ *
+ * @return this
+ */
+ public HttpConnection disconnect() {
+ if (null != this.conn) {
+ this.conn.disconnect();
+ }
+ return this;
+ }
+
+ /**
+ * 获得输入流对象
+ * 输入流对象用于读取数据
+ *
+ * @return 输入流对象
+ * @throws IOException IO异常
+ */
+ public InputStream getInputStream() throws IOException {
+ if (null != this.conn) {
+ return this.conn.getInputStream();
+ }
+ return null;
+ }
+
+ /**
+ * 当返回错误代码时,获得错误内容流
+ *
+ * @return 错误内容
+ */
+ public InputStream getErrorStream() {
+ if (null != this.conn) {
+ return this.conn.getErrorStream();
+ }
+ return null;
+ }
+
+ /**
+ * 获取输出流对象 输出流对象用于发送数据
+ *
+ * @return OutputStream
+ * @throws IOException IO异常
+ */
+ public OutputStream getOutputStream() throws IOException {
+ if (null == this.conn) {
+ throw new IOException("HttpURLConnection has not been initialized.");
+ }
+
+ final aiyh.utils.tool.cn.hutool.http.Method method = getMethod();
+
+ // 当有写出需求时,自动打开之
+ this.conn.setDoOutput(true);
+ final OutputStream out = this.conn.getOutputStream();
+
+ // 解决在Rest请求中,GET请求附带body导致GET请求被强制转换为POST
+ // 在sun.net.www.protocol.http.HttpURLConnection.getOutputStream0方法中,会把GET方法
+ // 修改为POST,而且无法调用setRequestMethod方法修改,因此此处使用反射强制修改字段属性值
+ // https://stackoverflow.com/questions/978061/http-get-with-request-body/983458
+ if (method == aiyh.utils.tool.cn.hutool.http.Method.GET && method != getMethod()) {
+ reflectSetMethod(method);
+ }
+
+ return out;
+ }
+
+ /**
+ * 获取响应码
+ *
+ * @return 响应码
+ * @throws IOException IO异常
+ */
+ public int responseCode() throws IOException {
+ if (null != this.conn) {
+ return this.conn.getResponseCode();
+ }
+ return 0;
+ }
+
+ /**
+ * 获得字符集编码
+ * 从Http连接的头信息中获得字符集
+ * 从ContentType中获取
+ *
+ * @return 字符集编码
+ */
+ public String getCharsetName() {
+ return HttpUtil.getCharset(conn);
+ }
+
+ /**
+ * 获取字符集编码
+ * 从Http连接的头信息中获得字符集
+ * 从ContentType中获取
+ *
+ * @return {@link Charset}编码
+ * @since 3.0.9
+ */
+ public Charset getCharset() {
+ Charset charset = null;
+ final String charsetName = getCharsetName();
+ if (StrUtil.isNotBlank(charsetName)) {
+ try {
+ charset = Charset.forName(charsetName);
+ } catch (UnsupportedCharsetException e) {
+ // ignore
+ }
+ }
+ return charset;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = StrUtil.builder();
+ sb.append("Request URL: ").append(this.url).append(StrUtil.CRLF);
+ sb.append("Request Method: ").append(this.getMethod()).append(StrUtil.CRLF);
+ // sb.append("Request Headers: ").append(StrUtil.CRLF);
+ // for (Entry
+ * 有些时候https请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
+ *
+ * @return {@link HttpURLConnection},https返回{@link HttpsURLConnection}
+ */
+ private HttpURLConnection openHttp() throws IOException {
+ final URLConnection conn = openConnection();
+ if (!(conn instanceof HttpURLConnection)) {
+ // 防止其它协议造成的转换异常
+ throw new HttpException("'{}' of URL [{}] is not a http connection, make sure URL is format for http.", conn.getClass().getName(), this.url);
+ }
+
+ return (HttpURLConnection) conn;
+ }
+
+ /**
+ * 建立连接
+ *
+ * @return {@link URLConnection}
+ * @throws IOException IO异常
+ */
+ private URLConnection openConnection() throws IOException {
+ return (null == this.proxy) ? url.openConnection() : url.openConnection(this.proxy);
+ }
+
+ /**
+ * 通过反射设置方法名,首先设置HttpURLConnection本身的方法名,再检查是否为代理类,如果是,设置带路对象的方法名。
+ *
+ * @param method 方法名
+ */
+ private void reflectSetMethod(Method method) {
+ ReflectUtil.setFieldValue(this.conn, "method", method.name());
+
+ // HttpsURLConnectionImpl实现中,使用了代理类,需要修改被代理类的method方法
+ final Object delegate = ReflectUtil.getFieldValue(this.conn, "delegate");
+ if (null != delegate) {
+ ReflectUtil.setFieldValue(delegate, "method", method.name());
+ }
+ }
+ // --------------------------------------------------------------- Private Method end
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpDownloader.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpDownloader.java
new file mode 100644
index 0000000..d330a70
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpDownloader.java
@@ -0,0 +1,122 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.io.FastByteArrayOutputStream;
+import aiyh.utils.tool.cn.hutool.core.io.StreamProgress;
+import aiyh.utils.tool.cn.hutool.core.lang.Assert;
+
+import java.io.File;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * 下载封装,下载统一使用{@code GET}请求,默认支持30x跳转
+ *
+ * @author looly
+ * @since 5.6.4
+ */
+public class HttpDownloader {
+
+ /**
+ * 下载远程文本
+ *
+ * @param url 请求的url
+ * @param customCharset 自定义的字符集,可以使用{@code CharsetUtil#charset} 方法转换
+ * @param streamPress 进度条 {@link StreamProgress}
+ * @return 文本
+ */
+ public static String downloadString(String url, Charset customCharset, StreamProgress streamPress) {
+ final FastByteArrayOutputStream out = new FastByteArrayOutputStream();
+ download(url, out, true, streamPress);
+ return null == customCharset ? out.toString() : out.toString(customCharset);
+ }
+
+ /**
+ * 下载远程文件数据,支持30x跳转
+ *
+ * @param url 请求的url
+ * @return 文件数据
+ */
+ public static byte[] downloadBytes(String url) {
+ return requestDownload(url, -1).bodyBytes();
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param targetFileOrDir 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @param timeout 超时,单位毫秒,-1表示默认超时
+ * @param streamProgress 进度条
+ * @return 文件大小
+ */
+ public static long downloadFile(String url, File targetFileOrDir, int timeout, StreamProgress streamProgress) {
+ return requestDownload(url, timeout).writeBody(targetFileOrDir, streamProgress);
+ }
+
+ /**
+ * 下载文件-避免未完成的文件
+ * 来自:https://gitee.com/dromara/hutool/pulls/407
+ * 此方法原理是先在目标文件同级目录下创建临时文件,下载之,等下载完毕后重命名,避免因下载错误导致的文件不完整。
+ *
+ * @param url 请求的url
+ * @param targetFileOrDir 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @param tempFileSuffix 临时文件后缀,默认".temp"
+ * @param timeout 超时,单位毫秒,-1表示默认超时
+ * @param streamProgress 进度条
+ * @return 下载大小
+ * @since 5.7.12
+ */
+ public long downloadFile(String url, File targetFileOrDir, String tempFileSuffix, int timeout, StreamProgress streamProgress) {
+ return requestDownload(url, timeout).writeBody(targetFileOrDir, tempFileSuffix, streamProgress);
+ }
+
+ /**
+ * 下载远程文件,返回文件
+ *
+ * @param url 请求的url
+ * @param targetFileOrDir 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @param timeout 超时,单位毫秒,-1表示默认超时
+ * @param streamProgress 进度条
+ * @return 文件
+ */
+ public static File downloadForFile(String url, File targetFileOrDir, int timeout, StreamProgress streamProgress) {
+ return requestDownload(url, timeout).writeBodyForFile(targetFileOrDir, streamProgress);
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param out 将下载内容写到输出流中 {@link OutputStream}
+ * @param isCloseOut 是否关闭输出流
+ * @param streamProgress 进度条
+ * @return 文件大小
+ */
+ public static long download(String url, OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {
+ Assert.notNull(out, "[out] is null !");
+
+ return requestDownload(url, -1).writeBody(out, isCloseOut, streamProgress);
+ }
+
+ /**
+ * 请求下载文件
+ *
+ * @param url 请求下载文件地址
+ * @param timeout 超时时间
+ * @return HttpResponse
+ * @since 5.4.1
+ */
+ private static aiyh.utils.tool.cn.hutool.http.HttpResponse requestDownload(String url, int timeout) {
+ Assert.notBlank(url, "[url] is blank !");
+
+ final HttpResponse response = HttpUtil.createGet(url, true)
+ .timeout(timeout)
+ .executeAsync();
+
+ if (response.isOk()) {
+ return response;
+ }
+
+ throw new HttpException("Server response error with status code: [{}]", response.getStatus());
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpException.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpException.java
new file mode 100644
index 0000000..11153cb
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpException.java
@@ -0,0 +1,36 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+/**
+ * HTTP异常
+ *
+ * @author xiaoleilu
+ */
+public class HttpException extends RuntimeException {
+ private static final long serialVersionUID = 8247610319171014183L;
+
+ public HttpException(Throwable e) {
+ super(e.getMessage(), e);
+ }
+
+ public HttpException(String message) {
+ super(message);
+ }
+
+ public HttpException(String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params));
+ }
+
+ public HttpException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+
+ public HttpException(String message, Throwable throwable, boolean enableSuppression, boolean writableStackTrace) {
+ super(message, throwable, enableSuppression, writableStackTrace);
+ }
+
+ public HttpException(Throwable throwable, String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params), throwable);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpGlobalConfig.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpGlobalConfig.java
new file mode 100755
index 0000000..5e8e524
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpGlobalConfig.java
@@ -0,0 +1,214 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.util.ArrayUtil;
+import aiyh.utils.tool.cn.hutool.core.util.RandomUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ReflectUtil;
+import aiyh.utils.tool.cn.hutool.http.cookie.GlobalCookieManager;
+
+import java.io.Serializable;
+import java.lang.reflect.Field;
+import java.net.CookieManager;
+import java.net.HttpURLConnection;
+
+/**
+ * HTTP 全局参数配置
+ *
+ * @author Looly
+ * @since 4.6.2
+ */
+public class HttpGlobalConfig implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * -1: 含义,永不超时。
+ * 如果:设置timeout = 3s(3000 ms), 那一次请求最大超时:就是:6s
+ * 官方含义:timeout of zero is interpreted as an infinite timeout. (0的超时被解释为无限超时。)
+ * 这里实际项目一定要进行修改,防止把系统拖死.
+ * 底层调用:{@link HttpURLConnection#setReadTimeout(int)} 同时设置: 读取超时
+ * 底层调用:{@link HttpURLConnection#setConnectTimeout(int)} 同时设置: 连接超时
+ */
+ private static int timeout = -1;
+ private static boolean isAllowPatch = false;
+ private static String boundary = "--------------------Hutool_" + RandomUtil.randomString(16);
+ private static int maxRedirectCount = 0;
+ private static boolean ignoreEOFError = true;
+ private static boolean decodeUrl = false;
+
+ /**
+ * 获取全局默认的超时时长
+ *
+ * @return 全局默认的超时时长
+ */
+ public static int getTimeout() {
+ return timeout;
+ }
+
+ /**
+ * 设置默认的连接和读取超时时长
+ * -1: 含义,永不超时。
+ * 如果:设置timeout = 3s(3000 ms), 那一次请求最大超时:就是:6s
+ * 官方含义:timeout of zero is interpreted as an infinite timeout. (0的超时被解释为无限超时。)
+ * 这里实际项目一定要进行修改,防止把系统拖死.
+ * 底层调用:{@link HttpURLConnection#setReadTimeout(int)} 同时设置: 读取超时
+ * 底层调用:{@link HttpURLConnection#setConnectTimeout(int)} 同时设置: 连接超时
+ *
+ * @param customTimeout 超时时长
+ */
+ synchronized public static void setTimeout(int customTimeout) {
+ timeout = customTimeout;
+ }
+
+ /**
+ * 获取全局默认的Multipart边界
+ *
+ * @return 全局默认的Multipart边界
+ * @since 5.7.17
+ */
+ public static String getBoundary() {
+ return boundary;
+ }
+
+ /**
+ * 设置默认的Multipart边界
+ *
+ * @param customBoundary 自定义Multipart边界
+ * @since 5.7.17
+ */
+ synchronized public static void setBoundary(String customBoundary) {
+ boundary = customBoundary;
+ }
+
+ /**
+ * 获取全局默认的最大重定向次数,如设置0表示不重定向
+ * 如果设置为1,表示重定向一次,即请求两次
+ *
+ * @return 全局默认的最大重定向次数
+ * @since 5.7.19
+ */
+ public static int getMaxRedirectCount() {
+ return maxRedirectCount;
+ }
+
+ /**
+ * 设置默认全局默认的最大重定向次数,如设置0表示不重定向
+ * 如果设置为1,表示重定向一次,即请求两次
+ *
+ * @param customMaxRedirectCount 全局默认的最大重定向次数
+ * @since 5.7.19
+ */
+ synchronized public static void setMaxRedirectCount(int customMaxRedirectCount) {
+ maxRedirectCount = customMaxRedirectCount;
+ }
+
+ /**
+ * 获取是否忽略响应读取时可能的EOF异常。
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
+ *
+ * @return 是否忽略响应读取时可能的EOF异常
+ * @since 5.7.20
+ */
+ public static boolean isIgnoreEOFError() {
+ return ignoreEOFError;
+ }
+
+ /**
+ * 设置是否忽略响应读取时可能的EOF异常。
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
+ *
+ * @param customIgnoreEOFError 是否忽略响应读取时可能的EOF异常。
+ * @since 5.7.20
+ */
+ synchronized public static void setIgnoreEOFError(boolean customIgnoreEOFError) {
+ ignoreEOFError = customIgnoreEOFError;
+ }
+
+ /**
+ * 获取是否忽略解码URL,包括URL中的Path部分和Param部分。
+ * 在构建Http请求时,用户传入的URL可能有编码后和未编码的内容混合在一起,如果此参数为{@code true},则会统一解码编码后的参数,
+ * 按照RFC3986规范,在发送请求时,全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。
+ *
+ * @return 是否忽略解码URL
+ * @since 5.7.22
+ */
+ public static boolean isDecodeUrl() {
+ return decodeUrl;
+ }
+
+ /**
+ * 设置是否忽略解码URL,包括URL中的Path部分和Param部分。
+ * 在构建Http请求时,用户传入的URL可能有编码后和未编码的内容混合在一起,如果此参数为{@code true},则会统一解码编码后的参数,
+ * 按照RFC3986规范,在发送请求时,全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。
+ *
+ * @param customDecodeUrl 是否忽略解码URL
+ * @since 5.7.22
+ */
+ synchronized public static void setDecodeUrl(boolean customDecodeUrl) {
+ decodeUrl = customDecodeUrl;
+ }
+
+ /**
+ * 获取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
+ */
+ synchronized public static void setCookieManager(CookieManager customCookieManager) {
+ GlobalCookieManager.setCookieManager(customCookieManager);
+ }
+
+ /**
+ * 关闭Cookie
+ *
+ * @see GlobalCookieManager#setCookieManager(CookieManager)
+ * @since 4.1.9
+ */
+ synchronized public static void closeCookie() {
+ GlobalCookieManager.setCookieManager(null);
+ }
+
+ /**
+ * 增加支持的METHOD方法
+ * 此方法通过注入方式修改{@link HttpURLConnection}中的methods静态属性,增加PATCH方法
+ * see: https://stackoverflow.com/questions/25163131/httpurlconnection-invalid-http-method-patch
+ *
+ * @since 5.7.4
+ */
+ synchronized public static void allowPatch() {
+ if (isAllowPatch) {
+ return;
+ }
+ final Field methodsField = ReflectUtil.getField(HttpURLConnection.class, "methods");
+ if (null == methodsField) {
+ throw new HttpException("None static field [methods] with Java version: [{}]", System.getProperty("java.version"));
+ }
+
+ // 去除final修饰
+ ReflectUtil.removeFinalModify(methodsField);
+ final String[] methods = {
+ "GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE", "TRACE", "PATCH"
+ };
+ ReflectUtil.setFieldValue(null, methodsField, methods);
+
+ // 检查注入是否成功
+ final Object staticFieldValue = ReflectUtil.getStaticFieldValue(methodsField);
+ if (!ArrayUtil.equals(methods, staticFieldValue)) {
+ throw new HttpException("Inject value to field [methods] failed!");
+ }
+
+ isAllowPatch = true;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpInputStream.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpInputStream.java
new file mode 100644
index 0000000..d1c2dce
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpInputStream.java
@@ -0,0 +1,108 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+import java.io.ByteArrayInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+import java.util.zip.Inflater;
+import java.util.zip.InflaterInputStream;
+
+/**
+ * HTTP输入流,此流用于包装Http请求响应内容的流,用于解析各种压缩、分段的响应流内容
+ *
+ * @author Looly
+ *
+ */
+public class HttpInputStream extends InputStream {
+
+ /** 原始流 */
+ private InputStream in;
+
+ /**
+ * 构造
+ *
+ * @param response 响应对象
+ */
+ public HttpInputStream(aiyh.utils.tool.cn.hutool.http.HttpResponse response) {
+ init(response);
+ }
+
+ @Override
+ public int read() throws IOException {
+ return this.in.read();
+ }
+
+ @SuppressWarnings("NullableProblems")
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ return this.in.read(b, off, len);
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ return this.in.skip(n);
+ }
+
+ @Override
+ public int available() throws IOException {
+ return this.in.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ this.in.close();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ this.in.mark(readlimit);
+ }
+
+ @Override
+ public synchronized void reset() throws IOException {
+ this.in.reset();
+ }
+
+ @Override
+ public boolean markSupported() {
+ return this.in.markSupported();
+ }
+
+ /**
+ * 初始化流
+ *
+ * @param response 响应对象
+ */
+ private void init(HttpResponse response) {
+ try {
+ this.in = (response.status < HttpStatus.HTTP_BAD_REQUEST) ? response.httpConnection.getInputStream() : response.httpConnection.getErrorStream();
+ } catch (IOException e) {
+ if (false == (e instanceof FileNotFoundException)) {
+ throw new HttpException(e);
+ }
+ // 服务器无返回内容,忽略之
+ }
+
+ // 在一些情况下,返回的流为null,此时提供状态码说明
+ if (null == this.in) {
+ this.in = new ByteArrayInputStream(StrUtil.format("Error request, response status: {}", response.status).getBytes());
+ return;
+ }
+
+ if (response.isGzip() && false == (response.in instanceof GZIPInputStream)) {
+ // Accept-Encoding: gzip
+ try {
+ this.in = new GZIPInputStream(this.in);
+ } catch (IOException e) {
+ // 在类似于Head等方法中无body返回,此时GZIPInputStream构造会出现错误,在此忽略此错误读取普通数据
+ // ignore
+ }
+ } else if (response.isDeflate() && false == (this.in instanceof InflaterInputStream)) {
+ // Accept-Encoding: defalte
+ this.in = new InflaterInputStream(this.in, new Inflater(true));
+ }
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpInterceptor.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpInterceptor.java
new file mode 100644
index 0000000..4b4507c
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpInterceptor.java
@@ -0,0 +1,56 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * Http拦截器接口,通过实现此接口,完成请求发起前或结束后对请求的编辑工作
+ *
+ * @param
+ * Http请求类用于构建Http请求并同步获取结果,此类通过CookieManager持有域名对应的Cookie值,再次请求时会自动附带Cookie信息
+ *
+ * @author Looly
+ */
+public class HttpRequest extends HttpBase
+ * 对于传入的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(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(String url, Charset charset) {
+ return of(UrlBuilder.ofHttp(url, charset));
+ }
+
+ /**
+ * 构建一个HTTP请求
+ *
+ * @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 aiyh.utils.tool.cn.hutool.http.Method method = aiyh.utils.tool.cn.hutool.http.Method.GET;
+ /**
+ * 连接对象
+ */
+ private HttpConnection httpConnection;
+
+ /**
+ * 存储表单数据
+ */
+ private Map
+ * 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配,则删除此连接。
+ * 因此weblogic不支持https的sni协议的主机名验证,此时需要将此值设置为sun.net.www.protocol.https.Handler对象。
+ *
+ * 在{@link #execute()} 执行前此对象为null
+ *
+ * @return {@link HttpConnection}
+ * @since 4.2.2
+ */
+ public HttpConnection getConnection() {
+ return this.httpConnection;
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param method HTTP方法
+ * @return HttpRequest
+ */
+ public HttpRequest method(aiyh.utils.tool.cn.hutool.http.Method method) {
+ this.method = method;
+ return this;
+ }
+
+ // ---------------------------------------------------------------- Http Request Header start
+
+ /**
+ * 设置contentType
+ *
+ * @param contentType contentType
+ * @return HttpRequest
+ */
+ public HttpRequest contentType(String contentType) {
+ header(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_TYPE, contentType);
+ return this;
+ }
+
+ /**
+ * 设置是否为长连接
+ *
+ * @param isKeepAlive 是否长连接
+ * @return HttpRequest
+ */
+ public HttpRequest keepAlive(boolean isKeepAlive) {
+ header(aiyh.utils.tool.cn.hutool.http.Header.CONNECTION, isKeepAlive ? "Keep-Alive" : "Close");
+ return this;
+ }
+
+ /**
+ * @return 获取是否为长连接
+ */
+ public boolean isKeepAlive() {
+ String connection = header(aiyh.utils.tool.cn.hutool.http.Header.CONNECTION);
+ if (connection == null) {
+ return !HTTP_1_0.equalsIgnoreCase(httpVersion);
+ }
+
+ return !"close".equalsIgnoreCase(connection);
+ }
+
+ /**
+ * 获取内容长度
+ *
+ * @return String
+ */
+ public String contentLength() {
+ return header(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_LENGTH);
+ }
+
+ /**
+ * 设置内容长度
+ *
+ * @param value 长度
+ * @return HttpRequest
+ */
+ public HttpRequest contentLength(int value) {
+ header(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_LENGTH, String.valueOf(value));
+ return this;
+ }
+
+ /**
+ * 设置Cookie
+ * 自定义Cookie后会覆盖Hutool的默认Cookie行为
+ *
+ * @param cookies Cookie值数组,如果为{@code null}则设置无效,使用默认Cookie行为
+ * @return this
+ * @since 5.4.1
+ */
+ public HttpRequest cookie(Collection
+ * 自定义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
+ * 自定义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置为空。
+ * 如果想重新启用Cookie,请调用:{@link #cookie(String)}方法自定义Cookie。
+ * 如果想启动默认的Cookie行为(自动回填服务器传回的Cookie),则调用{@link #enableDefaultCookie()}
+ *
+ * @return this
+ * @since 3.0.7
+ */
+ public HttpRequest disableCookie() {
+ return cookie(StrUtil.EMPTY);
+ }
+
+ /**
+ * 打开默认的Cookie行为(自动回填服务器传回的Cookie)
+ *
+ * @return this
+ */
+ public HttpRequest enableDefaultCookie() {
+ return cookie((String) null);
+ }
+ // ---------------------------------------------------------------- Http Request Header end
+
+ // ---------------------------------------------------------------- Form start
+
+ /**
+ * 设置表单数据
+ *
+ * @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.bodyBytes = 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
+ * 一旦有文件加入,表单变为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));
+ }
+
+ /**
+ * 文件表单项
+ * 一旦有文件加入,表单变为multipart/form-data
+ *
+ * @param name 名
+ * @param file 需要上传的文件
+ * @return this
+ */
+ public HttpRequest form(String name, File file) {
+ return form(name, file, file.getName());
+ }
+
+ /**
+ * 文件表单项
+ * 一旦有文件加入,表单变为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[]表单项
+ * 一旦有文件加入,表单变为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;
+ }
+
+ /**
+ * 文件表单项
+ * 一旦有文件加入,表单变为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 (!isKeepAlive()) {
+ keepAlive(true);
+ }
+
+ this.isMultiPart = true;
+ return putToForm(name, resource);
+ }
+ return this;
+ }
+
+ /**
+ * 获取表单数据
+ *
+ * @return 表单Map
+ */
+ public Map
+ * 请求体body参数支持两种类型:
+ *
+ *
+ * 1. 标准参数,例如 a=1&b=2 这种格式
+ * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
+ *
+ *
+ * @param body 请求体
+ * @return this
+ */
+ public HttpRequest body(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(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(aiyh.utils.tool.cn.hutool.http.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(byte[] bodyBytes) {
+ if (null != bodyBytes) {
+ this.bodyBytes = bodyBytes;
+ }
+ return this;
+ }
+ // ---------------------------------------------------------------- Body end
+
+ /**
+ * 将新的配置加入
+ * 注意加入的配置可能被修改
+ *
+ * @param config 配置
+ * @return this
+ */
+ public HttpRequest setConfig(HttpConfig config) {
+ this.config = config;
+ return this;
+ }
+
+ /**
+ * 设置超时,单位:毫秒
+ * 超时包括:
+ *
+ *
+ * 1. 连接超时
+ * 2. 读取响应超时
+ *
+ *
+ * @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
+ * 此方法效果与{@link #setMaxRedirectCount(int)} 一致
+ *
+ *
+ * 当设置为{@code false}时,无论全局是否设置次数,都设置为0。
+ * 不调用此方法的情况下,使用全局默认的次数。
+ *
+ * 如果次数小于1则表示不重定向,大于等于1表示打开重定向
+ *
+ * @param maxRedirectCount 最大重定向次数
+ * @return this
+ * @since 3.3.0
+ */
+ public HttpRequest setMaxRedirectCount(int maxRedirectCount) {
+ config.setMaxRedirectCount(maxRedirectCount);
+ return this;
+ }
+
+ /**
+ * 设置域名验证器
+ * 只针对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
+ * 只针对HTTPS请求,如果不设置,使用默认的SSLSocketFactory
+ * 默认SSLSocketFactory为:SSLSocketFactoryBuilder.create().build();
+ *
+ * @param ssf SSLScketFactory
+ * @return this
+ */
+ public HttpRequest setSSLSocketFactory(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(String protocol) {
+ config.setSSLProtocol(protocol);
+ return this;
+ }
+
+ /**
+ * 设置是否rest模式
+ * rest模式下get请求不会把参数附加到URL之后
+ *
+ * @param isRest 是否rest模式
+ * @return this
+ * @since 4.5.0
+ */
+ public HttpRequest setRest(boolean isRest) {
+ this.isRest = isRest;
+ return this;
+ }
+
+ /**
+ * 采用流方式上传数据,无需本地缓存数据。
+ * 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
+ * 异步请求后获取的{@link aiyh.utils.tool.cn.hutool.http.HttpResponse} 为异步模式,执行完此方法后发送请求到服务器,但是并不立即读取响应内容。
+ * 此时保持Http连接不关闭,直调用获取内容方法为止。
+ *
+ *
+ * 处理结束后关闭连接
+ *
+ * @param consumer 响应内容处理函数
+ * @since 5.7.8
+ */
+ public void then(Consumer
+ * 处理结束后关闭连接
+ *
+ * @param
+ * Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
+ *
+ *
+ * @param username 用户名
+ * @param password 密码
+ * @return this
+ */
+ public HttpRequest basicAuth(String username, 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(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(aiyh.utils.tool.cn.hutool.http.Header.AUTHORIZATION, content, true);
+ return this;
+ }
+
+ /**
+ * 验证,简单插入Authorization头
+ *
+ * @param content 验证内容
+ * @return HttpRequest
+ * @since 5.4.6
+ */
+ public HttpRequest proxyAuth(String content) {
+ header(aiyh.utils.tool.cn.hutool.http.Header.PROXY_AUTHORIZATION, content, true);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ 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 是否异步
+ * @param requestInterceptors 请求拦截器列表
+ * @param responseInterceptors 响应拦截器列表
+ * @return this
+ */
+ private aiyh.utils.tool.cn.hutool.http.HttpResponse doExecute(boolean isAsync, HttpInterceptor.Chain
+ * 此处不对URL中的特殊字符做单独编码
+ * 对于非rest的GET请求,且处于重定向时,参数丢弃
+ */
+ private void urlWithParamIfGet() {
+ if (aiyh.utils.tool.cn.hutool.http.Method.GET.equals(method) && !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 aiyh.utils.tool.cn.hutool.http.HttpResponse},无转发返回 {@code null}
+ */
+ private HttpResponse sendRedirectIfPossible(boolean isAsync) {
+ // 手动实现重定向
+ if (config.maxRedirectCount > 0) {
+ int responseCode;
+ try {
+ responseCode = httpConnection.responseCode();
+ } catch (IOException e) {
+ // 错误时静默关闭连接
+ this.httpConnection.disconnectQuietly();
+ throw new HttpException(e);
+ }
+
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ if (HttpStatus.isRedirected(responseCode)) {
+ final UrlBuilder redirectUrl;
+ String location = httpConnection.header(aiyh.utils.tool.cn.hutool.http.Header.LOCATION);
+ if (!HttpUtil.isHttp(location) && !HttpUtil.isHttps(location)) {
+ // issue#I5TPSY
+ // location可能为相对路径
+ if (!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, config.interceptorOnRedirect ? config.requestInterceptors : null,
+ config.interceptorOnRedirect ? config.responseInterceptors : null);
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 发送数据流
+ *
+ * @throws IORuntimeException IO异常
+ */
+ private void send() throws IORuntimeException {
+ try {
+ if (aiyh.utils.tool.cn.hutool.http.Method.POST.equals(this.method) //
+ || aiyh.utils.tool.cn.hutool.http.Method.PUT.equals(this.method) //
+ || aiyh.utils.tool.cn.hutool.http.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);
+ }
+ }
+
+ /**
+ * 发送普通表单
+ * 发送数据后自动关闭输出流
+ *
+ * @throws IOException IO异常
+ */
+ private void sendFormUrlEncoded() throws IOException {
+ if (StrUtil.isBlank(this.header(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_TYPE))) {
+ // 如果未自定义Content-Type,使用默认的application/x-www-form-urlencoded
+ this.httpConnection.header(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_TYPE, ContentType.FORM_URLENCODED.toString(this.charset), true);
+ }
+
+ // Write的时候会优先使用body中的内容,write时自动关闭OutputStream
+ RequestBody body;
+ if (ArrayUtil.isNotEmpty(this.bodyBytes)) {
+ body = BytesBody.create(this.bodyBytes);
+ } else {
+ body = FormUrlEncodedBody.create(this.form, this.charset);
+ }
+ body.writeClose(this.httpConnection.getOutputStream());
+ }
+
+ /**
+ * 发送多组件请求(例如包含文件的表单)
+ * 发送数据后自动关闭输出流
+ *
+ * @throws IOException IO异常
+ */
+ private void sendMultipart() throws IOException {
+ final MultipartBody multipartBody = MultipartBody.create(this.form, this.charset);
+ // 设置表单类型为Multipart(文件上传)
+ this.httpConnection.header(aiyh.utils.tool.cn.hutool.http.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 aiyh.utils.tool.cn.hutool.http.Method.HEAD == this.method //
+ || aiyh.utils.tool.cn.hutool.http.Method.CONNECT == this.method //
+ || aiyh.utils.tool.cn.hutool.http.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(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
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpResource.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpResource.java
new file mode 100644
index 0000000..1b345f9
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpResource.java
@@ -0,0 +1,56 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.io.resource.Resource;
+import aiyh.utils.tool.cn.hutool.core.lang.Assert;
+
+import java.io.InputStream;
+import java.io.Serializable;
+import java.net.URL;
+
+/**
+ * HTTP资源,可自定义Content-Type
+ *
+ * @author looly
+ * @since 5.7.17
+ */
+public class HttpResource implements Resource, Serializable {
+ private static final long serialVersionUID = 1L;
+
+ private final Resource resource;
+ private final String contentType;
+
+ /**
+ * 构造
+ *
+ * @param resource 资源,非空
+ * @param contentType Content-Type类型,{@code null}表示不设置
+ */
+ public HttpResource(Resource resource, String contentType) {
+ this.resource = Assert.notNull(resource, "Resource must be not null !");
+ this.contentType = contentType;
+ }
+
+ @Override
+ public String getName() {
+ return resource.getName();
+ }
+
+ @Override
+ public URL getUrl() {
+ return resource.getUrl();
+ }
+
+ @Override
+ public InputStream getStream() {
+ return resource.getStream();
+ }
+
+ /**
+ * 获取自定义Content-Type类型
+ *
+ * @return Content-Type类型
+ */
+ public String getContentType() {
+ return this.contentType;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpResponse.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpResponse.java
new file mode 100755
index 0000000..379f947
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpResponse.java
@@ -0,0 +1,644 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.convert.Convert;
+import aiyh.utils.tool.cn.hutool.core.io.FastByteArrayOutputStream;
+import aiyh.utils.tool.cn.hutool.core.io.FileUtil;
+import aiyh.utils.tool.cn.hutool.core.io.IORuntimeException;
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+import aiyh.utils.tool.cn.hutool.core.io.StreamProgress;
+import aiyh.utils.tool.cn.hutool.core.lang.Assert;
+import aiyh.utils.tool.cn.hutool.core.util.ObjUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ReUtil;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+import aiyh.utils.tool.cn.hutool.core.util.URLUtil;
+import aiyh.utils.tool.cn.hutool.http.cookie.GlobalCookieManager;
+
+import java.io.ByteArrayInputStream;
+import java.io.Closeable;
+import java.io.EOFException;
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpCookie;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map.Entry;
+
+/**
+ * Http响应类
+ * 非线程安全对象
+ *
+ * @author Looly
+ */
+public class HttpResponse extends HttpBase
+ * 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。
+ * 当调用此方法时,异步状态转为同步状态,此时从Http链接流中读取body内容并暂存在内容中。如果已经是同步状态,则不进行任何操作。
+ *
+ * @return this
+ */
+ public HttpResponse sync() {
+ return this.isAsync ? forceSync() : this;
+ }
+
+ // ---------------------------------------------------------------- Http Response Header start
+
+ /**
+ * 获取内容编码
+ *
+ * @return String
+ */
+ public String contentEncoding() {
+ return header(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_ENCODING);
+ }
+
+ /**
+ * 获取内容长度,以下情况长度无效:
+ *
+ *
+ * 参考:https://blog.csdn.net/jiang7701037/article/details/86304302
+ *
+ * @return 长度,-1表示服务端未返回或长度无效
+ * @since 5.7.9
+ */
+ public long contentLength() {
+ long contentLength = Convert.toLong(header(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_LENGTH), -1L);
+ if (contentLength > 0 && (isChunked() || StrUtil.isNotBlank(contentEncoding()))) {
+ //按照HTTP协议规范,在 Transfer-Encoding和Content-Encoding设置后 Content-Length 无效。
+ contentLength = -1;
+ }
+ return contentLength;
+ }
+
+ /**
+ * 是否为gzip压缩过的内容
+ *
+ * @return 是否为gzip压缩过的内容
+ */
+ public boolean isGzip() {
+ final String contentEncoding = contentEncoding();
+ return "gzip".equalsIgnoreCase(contentEncoding);
+ }
+
+ /**
+ * 是否为zlib(Deflate)压缩过的内容
+ *
+ * @return 是否为zlib(Deflate)压缩过的内容
+ * @since 4.5.7
+ */
+ public boolean isDeflate() {
+ final String contentEncoding = contentEncoding();
+ return "deflate".equalsIgnoreCase(contentEncoding);
+ }
+
+ /**
+ * 是否为Transfer-Encoding:Chunked的内容
+ *
+ * @return 是否为Transfer-Encoding:Chunked的内容
+ * @since 4.6.2
+ */
+ public boolean isChunked() {
+ final String transferEncoding = header(aiyh.utils.tool.cn.hutool.http.Header.TRANSFER_ENCODING);
+ return "Chunked".equalsIgnoreCase(transferEncoding);
+ }
+
+ /**
+ * 获取本次请求服务器返回的Cookie信息
+ *
+ * @return Cookie字符串
+ * @since 3.1.1
+ */
+ public String getCookieStr() {
+ return header(aiyh.utils.tool.cn.hutool.http.Header.SET_COOKIE);
+ }
+
+ /**
+ * 获取Cookie
+ *
+ * @return Cookie列表
+ * @since 3.1.1
+ */
+ public List
+ * 异步模式下获取Http原生流,同步模式下获取获取到的在内存中的副本
+ * 如果想在同步模式下获取流,请先调用{@link #sync()}方法强制同步
+ * 流获取后处理完毕需关闭此类
+ *
+ * @return 响应流
+ */
+ public InputStream bodyStream() {
+ if (isAsync) {
+ return this.in;
+ }
+ return new ByteArrayInputStream(this.bodyBytes);
+ }
+
+ /**
+ * 获取响应流字节码
+ * 此方法会转为同步模式
+ *
+ * @return byte[]
+ */
+ @Override
+ public byte[] bodyBytes() {
+ sync();
+ return this.bodyBytes;
+ }
+
+ /**
+ * 设置主体字节码
+ * 需在此方法调用前使用charset方法设置编码,否则使用默认编码UTF-8
+ *
+ * @param bodyBytes 主体
+ * @return this
+ */
+ public HttpResponse body(byte[] bodyBytes) {
+ sync();
+ if (null != bodyBytes) {
+ this.bodyBytes = bodyBytes;
+ }
+ return this;
+ }
+
+ /**
+ * 获取响应主体
+ *
+ * @return String
+ * @throws HttpException 包装IO异常
+ */
+ public String body() throws HttpException {
+ return HttpUtil.getString(bodyBytes(), this.charset, null == this.charsetFromResponse);
+ }
+
+ /**
+ * 将响应内容写出到{@link OutputStream}
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param out 写出的流
+ * @param isCloseOut 是否关闭输出流
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出bytes数
+ * @since 3.3.2
+ */
+ public long writeBody(OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {
+ Assert.notNull(out, "[out] must be not null!");
+ final long contentLength = contentLength();
+ try {
+ return copyBody(bodyStream(), out, contentLength, streamProgress, this.config.ignoreEOFError);
+ } finally {
+ IoUtil.close(this);
+ if (isCloseOut) {
+ IoUtil.close(out);
+ }
+ }
+ }
+
+ /**
+ * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param targetFileOrDir 写出到的文件或目录
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出bytes数
+ * @since 3.3.2
+ */
+ public long writeBody(File targetFileOrDir, StreamProgress streamProgress) {
+ Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
+
+ final File outFile = completeFileNameFromHeader(targetFileOrDir);
+ return writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
+ }
+
+ /**
+ * 将响应内容写出到文件-避免未完成的文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ * 来自:https://gitee.com/dromara/hutool/pulls/407
+ * 此方法原理是先在目标文件同级目录下创建临时文件,下载之,等下载完毕后重命名,避免因下载错误导致的文件不完整。
+ *
+ * @param targetFileOrDir 写出到的文件或目录
+ * @param tempFileSuffix 临时文件后缀,默认".temp"
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出bytes数
+ * @since 5.7.12
+ */
+ public long writeBody(File targetFileOrDir, String tempFileSuffix, StreamProgress streamProgress) {
+ Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
+
+ File outFile = completeFileNameFromHeader(targetFileOrDir);
+
+ if (StrUtil.isBlank(tempFileSuffix)) {
+ tempFileSuffix = ".temp";
+ } else {
+ tempFileSuffix = StrUtil.addPrefixIfNot(tempFileSuffix, StrUtil.DOT);
+ }
+
+ // 目标文件真实名称
+ final String fileName = outFile.getName();
+ // 临时文件名称
+ final String tempFileName = fileName + tempFileSuffix;
+
+ // 临时文件
+ outFile = new File(outFile.getParentFile(), tempFileName);
+
+ long length;
+ try {
+ length = writeBody(outFile, streamProgress);
+ // 重命名下载好的临时文件
+ FileUtil.rename(outFile, fileName, true);
+ } catch (Throwable e) {
+ // 异常则删除临时文件
+ FileUtil.del(outFile);
+ throw new HttpException(e);
+ }
+ return length;
+ }
+
+ /**
+ * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param targetFileOrDir 写出到的文件
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @return 写出的文件
+ * @since 5.6.4
+ */
+ public File writeBodyForFile(File targetFileOrDir, StreamProgress streamProgress) {
+ Assert.notNull(targetFileOrDir, "[targetFileOrDir] must be not null!");
+
+ final File outFile = completeFileNameFromHeader(targetFileOrDir);
+ writeBody(FileUtil.getOutputStream(outFile), true, streamProgress);
+
+ return outFile;
+ }
+
+ /**
+ * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param targetFileOrDir 写出到的文件或目录
+ * @return 写出bytes数
+ * @since 3.3.2
+ */
+ public long writeBody(File targetFileOrDir) {
+ return writeBody(targetFileOrDir, null);
+ }
+
+ /**
+ * 将响应内容写出到文件
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param targetFileOrDir 写出到的文件或目录的路径
+ * @return 写出bytes数
+ * @since 3.3.2
+ */
+ public long writeBody(String targetFileOrDir) {
+ return writeBody(FileUtil.file(targetFileOrDir));
+ }
+ // ---------------------------------------------------------------- Body end
+
+ @Override
+ public void close() {
+ IoUtil.close(this.in);
+ this.in = null;
+ // 关闭连接
+ this.httpConnection.disconnectQuietly();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = StrUtil.builder();
+ sb.append("Response Headers: ").append(StrUtil.CRLF);
+ for (Entry
+ * 初始化包括:
+ *
+ *
+ * 1、读取Http状态
+ * 2、读取头信息
+ * 3、持有Http流,并不关闭流
+ *
+ *
+ * @return this
+ * @throws HttpException IO异常
+ */
+ private HttpResponse initWithDisconnect() throws HttpException {
+ try {
+ init();
+ } catch (HttpException e) {
+ this.httpConnection.disconnectQuietly();
+ throw e;
+ }
+ return this;
+ }
+
+ /**
+ * 初始化Http响应
+ * 初始化包括:
+ *
+ *
+ * 1、读取Http状态
+ * 2、读取头信息
+ * 3、持有Http流,并不关闭流
+ *
+ *
+ * @return this
+ * @throws HttpException IO异常
+ */
+ private HttpResponse init() throws HttpException {
+ // 获取响应状态码
+ try {
+ this.status = httpConnection.responseCode();
+ } catch (IOException e) {
+ if (false == (e instanceof FileNotFoundException)) {
+ throw new HttpException(e);
+ }
+ // 服务器无返回内容,忽略之
+ }
+
+
+ // 读取响应头信息
+ try {
+ this.headers = httpConnection.headers();
+ } catch (IllegalArgumentException e) {
+ // ignore
+ // StaticLog.warn(e, e.getMessage());
+ }
+
+ // 存储服务端设置的Cookie信息
+ GlobalCookieManager.store(httpConnection);
+
+ // 获取响应编码
+ final Charset charset = httpConnection.getCharset();
+ this.charsetFromResponse = charset;
+ if (null != charset) {
+ this.charset = charset;
+ }
+
+ // 获取响应内容流
+ this.in = new HttpInputStream(this);
+
+ // 同步情况下强制同步
+ return this.isAsync ? this : forceSync();
+ }
+
+ /**
+ * 强制同步,用于初始化
+ * 强制同步后变化如下:
+ *
+ *
+ * 1、读取body内容到内存
+ * 2、异步状态设为false(变为同步状态)
+ * 3、关闭Http流
+ * 4、断开与服务器连接
+ *
+ *
+ * @return this
+ */
+ private HttpResponse forceSync() {
+ // 非同步状态转为同步状态
+ try {
+ this.readBody(this.in);
+ } catch (IORuntimeException e) {
+ //noinspection StatementWithEmptyBody
+ if (e.getCause() instanceof FileNotFoundException) {
+ // 服务器无返回内容,忽略之
+ } else {
+ throw new HttpException(e);
+ }
+ } finally {
+ if (this.isAsync) {
+ this.isAsync = false;
+ }
+ this.close();
+ }
+ return this;
+ }
+
+ /**
+ * 读取主体,忽略EOFException异常
+ *
+ * @param in 输入流
+ * @throws IORuntimeException IO异常
+ */
+ private void readBody(InputStream in) throws IORuntimeException {
+ if (ignoreBody) {
+ return;
+ }
+
+ final long contentLength = contentLength();
+ final FastByteArrayOutputStream out = new FastByteArrayOutputStream((int) contentLength);
+ copyBody(in, out, contentLength, null, this.config.ignoreEOFError);
+ this.bodyBytes = out.toByteArray();
+ }
+
+ /**
+ * 将响应内容写出到{@link OutputStream}
+ * 异步模式下直接读取Http流写出,同步模式下将存储在内存中的响应内容写出
+ * 写出后会关闭Http流(异步模式)
+ *
+ * @param in 输入流
+ * @param out 写出的流
+ * @param contentLength 总长度,-1表示未知
+ * @param streamProgress 进度显示接口,通过实现此接口显示下载进度
+ * @param isIgnoreEOFError 是否忽略响应读取时可能的EOF异常
+ * @return 拷贝长度
+ */
+ private static long copyBody(InputStream in, OutputStream out, long contentLength, StreamProgress streamProgress, boolean isIgnoreEOFError) {
+ if (null == out) {
+ throw new NullPointerException("[out] is null!");
+ }
+
+ long copyLength = -1;
+ try {
+ copyLength = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE, contentLength, streamProgress);
+ } catch (IORuntimeException e) {
+ //noinspection StatementWithEmptyBody
+ if (isIgnoreEOFError
+ && (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF"))) {
+ // 忽略读取HTTP流中的EOF错误
+ } else {
+ throw e;
+ }
+ }
+ return copyLength;
+ }
+ // ---------------------------------------------------------------- Private method end
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpStatus.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpStatus.java
new file mode 100644
index 0000000..90c856c
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpStatus.java
@@ -0,0 +1,222 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+/**
+ * HTTP状态码
+ *
+ * @author Looly
+ * @see java.net.HttpURLConnection
+ *
+ */
+public class HttpStatus {
+
+ /* 2XX: generally "OK" */
+
+ /**
+ * HTTP Status-Code 200: OK.
+ */
+ public static final int HTTP_OK = 200;
+
+ /**
+ * HTTP Status-Code 201: Created.
+ */
+ public static final int HTTP_CREATED = 201;
+
+ /**
+ * HTTP Status-Code 202: Accepted.
+ */
+ public static final int HTTP_ACCEPTED = 202;
+
+ /**
+ * HTTP Status-Code 203: Non-Authoritative Information.
+ */
+ public static final int HTTP_NOT_AUTHORITATIVE = 203;
+
+ /**
+ * HTTP Status-Code 204: No Content.
+ */
+ public static final int HTTP_NO_CONTENT = 204;
+
+ /**
+ * HTTP Status-Code 205: Reset Content.
+ */
+ public static final int HTTP_RESET = 205;
+
+ /**
+ * HTTP Status-Code 206: Partial Content.
+ */
+ public static final int HTTP_PARTIAL = 206;
+
+ /* 3XX: relocation/redirect */
+
+ /**
+ * HTTP Status-Code 300: Multiple Choices.
+ */
+ public static final int HTTP_MULT_CHOICE = 300;
+
+ /**
+ * HTTP Status-Code 301: Moved Permanently.
+ */
+ public static final int HTTP_MOVED_PERM = 301;
+
+ /**
+ * HTTP Status-Code 302: Temporary Redirect.
+ */
+ public static final int HTTP_MOVED_TEMP = 302;
+
+ /**
+ * HTTP Status-Code 303: See Other.
+ */
+ public static final int HTTP_SEE_OTHER = 303;
+
+ /**
+ * HTTP Status-Code 304: Not Modified.
+ */
+ public static final int HTTP_NOT_MODIFIED = 304;
+
+ /**
+ * HTTP Status-Code 305: Use Proxy.
+ */
+ public static final int HTTP_USE_PROXY = 305;
+
+ /**
+ * HTTP 1.1 Status-Code 307: Temporary Redirect.
+ * 见:RFC-7231
+ */
+ public static final int HTTP_TEMP_REDIRECT = 307;
+
+ /**
+ * HTTP 1.1 Status-Code 308: Permanent Redirect 永久重定向
+ * 见:RFC-7231
+ */
+ public static final int HTTP_PERMANENT_REDIRECT = 308;
+
+ /* 4XX: client error */
+
+ /**
+ * HTTP Status-Code 400: Bad Request.
+ */
+ public static final int HTTP_BAD_REQUEST = 400;
+
+ /**
+ * HTTP Status-Code 401: Unauthorized.
+ */
+ public static final int HTTP_UNAUTHORIZED = 401;
+
+ /**
+ * HTTP Status-Code 402: Payment Required.
+ */
+ public static final int HTTP_PAYMENT_REQUIRED = 402;
+
+ /**
+ * HTTP Status-Code 403: Forbidden.
+ */
+ public static final int HTTP_FORBIDDEN = 403;
+
+ /**
+ * HTTP Status-Code 404: Not Found.
+ */
+ public static final int HTTP_NOT_FOUND = 404;
+
+ /**
+ * HTTP Status-Code 405: Method Not Allowed.
+ */
+ public static final int HTTP_BAD_METHOD = 405;
+
+ /**
+ * HTTP Status-Code 406: Not Acceptable.
+ */
+ public static final int HTTP_NOT_ACCEPTABLE = 406;
+
+ /**
+ * HTTP Status-Code 407: Proxy Authentication Required.
+ */
+ public static final int HTTP_PROXY_AUTH = 407;
+
+ /**
+ * HTTP Status-Code 408: Request Time-Out.
+ */
+ public static final int HTTP_CLIENT_TIMEOUT = 408;
+
+ /**
+ * HTTP Status-Code 409: Conflict.
+ */
+ public static final int HTTP_CONFLICT = 409;
+
+ /**
+ * HTTP Status-Code 410: Gone.
+ */
+ public static final int HTTP_GONE = 410;
+
+ /**
+ * HTTP Status-Code 411: Length Required.
+ */
+ public static final int HTTP_LENGTH_REQUIRED = 411;
+
+ /**
+ * HTTP Status-Code 412: Precondition Failed.
+ */
+ public static final int HTTP_PRECON_FAILED = 412;
+
+ /**
+ * HTTP Status-Code 413: Request Entity Too Large.
+ */
+ public static final int HTTP_ENTITY_TOO_LARGE = 413;
+
+ /**
+ * HTTP Status-Code 414: Request-URI Too Large.
+ */
+ public static final int HTTP_REQ_TOO_LONG = 414;
+
+ /**
+ * HTTP Status-Code 415: Unsupported Media Type.
+ */
+ public static final int HTTP_UNSUPPORTED_TYPE = 415;
+
+ /* 5XX: server error */
+
+ /**
+ * HTTP Status-Code 500: Internal Server Error.
+ */
+ public static final int HTTP_INTERNAL_ERROR = 500;
+
+ /**
+ * HTTP Status-Code 501: Not Implemented.
+ */
+ public static final int HTTP_NOT_IMPLEMENTED = 501;
+
+ /**
+ * HTTP Status-Code 502: Bad Gateway.
+ */
+ public static final int HTTP_BAD_GATEWAY = 502;
+
+ /**
+ * HTTP Status-Code 503: Service Unavailable.
+ */
+ public static final int HTTP_UNAVAILABLE = 503;
+
+ /**
+ * HTTP Status-Code 504: Gateway Timeout.
+ */
+ public static final int HTTP_GATEWAY_TIMEOUT = 504;
+
+ /**
+ * HTTP Status-Code 505: HTTP Version Not Supported.
+ */
+ public static final int HTTP_VERSION = 505;
+
+ /**
+ * 是否为重定向状态码
+ * @param responseCode 被检查的状态码
+ * @return 是否为重定向状态码
+ * @since 5.6.3
+ */
+ public static boolean isRedirected(int responseCode){
+ return responseCode == HTTP_MOVED_PERM
+ || responseCode == HTTP_MOVED_TEMP
+ || responseCode == HTTP_SEE_OTHER
+ // issue#1504@Github,307和308是RFC 7538中http 1.1定义的规范
+ || responseCode == HTTP_TEMP_REDIRECT
+ || responseCode == HTTP_PERMANENT_REDIRECT;
+
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpUtil.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpUtil.java
new file mode 100755
index 0000000..51393c9
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpUtil.java
@@ -0,0 +1,893 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.codec.Base64;
+import aiyh.utils.tool.cn.hutool.core.convert.Convert;
+import aiyh.utils.tool.cn.hutool.core.io.FileUtil;
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+import aiyh.utils.tool.cn.hutool.core.io.StreamProgress;
+import aiyh.utils.tool.cn.hutool.core.map.MapUtil;
+import aiyh.utils.tool.cn.hutool.core.net.RFC3986;
+import aiyh.utils.tool.cn.hutool.core.net.url.UrlQuery;
+import aiyh.utils.tool.cn.hutool.core.text.StrBuilder;
+import aiyh.utils.tool.cn.hutool.core.util.CharsetUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ObjectUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ReUtil;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+import aiyh.utils.tool.cn.hutool.core.util.URLUtil;
+import aiyh.utils.tool.cn.hutool.http.cookie.GlobalCookieManager;
+import aiyh.utils.tool.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;
+
+/**
+ * Http请求工具类
+ *
+ * @author xiaoleilu
+ */
+public class HttpUtil {
+
+ /**
+ * 正则:Content-Type中的编码信息
+ */
+ public static final Pattern CHARSET_PATTERN = Pattern.compile("charset\\s*=\\s*([a-z0-9-]*)", Pattern.CASE_INSENSITIVE);
+ /**
+ * 正则:匹配meta标签的编码信息
+ */
+ public static final Pattern META_CHARSET_PATTERN = Pattern.compile("]*?charset\\s*=\\s*['\"]?([a-z0-9-]*)", Pattern.CASE_INSENSITIVE);
+
+ /**
+ * 检测是否https
+ *
+ * @param url URL
+ * @return 是否https
+ */
+ public static boolean isHttps(String url) {
+ return StrUtil.startWithIgnoreCase(url, "https:");
+ }
+
+ /**
+ * 检测是否http
+ *
+ * @param url URL
+ * @return 是否http
+ * @since 5.3.8
+ */
+ public static boolean isHttp(String url) {
+ return StrUtil.startWithIgnoreCase(url, "http:");
+ }
+
+ /**
+ * 创建Http请求对象
+ *
+ * @param method 方法枚举{@link aiyh.utils.tool.cn.hutool.http.Method}
+ * @param url 请求的URL,可以使HTTP或者HTTPS
+ * @return {@link aiyh.utils.tool.cn.hutool.http.HttpRequest}
+ * @since 3.0.9
+ */
+ public static aiyh.utils.tool.cn.hutool.http.HttpRequest createRequest(Method method, String url) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.of(url).method(method);
+ }
+
+ /**
+ * 创建Http GET请求对象
+ *
+ * @param url 请求的URL,可以使HTTP或者HTTPS
+ * @return {@link aiyh.utils.tool.cn.hutool.http.HttpRequest}
+ * @since 3.2.0
+ */
+ public static aiyh.utils.tool.cn.hutool.http.HttpRequest createGet(String url) {
+ return createGet(url, false);
+ }
+
+ /**
+ * 创建Http GET请求对象
+ *
+ * @param url 请求的URL,可以使HTTP或者HTTPS
+ * @param isFollowRedirects 是否打开重定向
+ * @return {@link aiyh.utils.tool.cn.hutool.http.HttpRequest}
+ * @since 5.6.4
+ */
+ public static aiyh.utils.tool.cn.hutool.http.HttpRequest createGet(String url, boolean isFollowRedirects) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.get(url).setFollowRedirects(isFollowRedirects);
+ }
+
+ /**
+ * 创建Http POST请求对象
+ *
+ * @param url 请求的URL,可以使HTTP或者HTTPS
+ * @return {@link aiyh.utils.tool.cn.hutool.http.HttpRequest}
+ * @since 3.2.0
+ */
+ public static aiyh.utils.tool.cn.hutool.http.HttpRequest createPost(String url) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.post(url);
+ }
+
+ /**
+ * 发送get请求
+ *
+ * @param urlString 网址
+ * @param customCharset 自定义请求字符集,如果字符集获取不到,使用此字符集
+ * @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null
+ */
+ public static String get(String urlString, Charset customCharset) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.get(urlString).charset(customCharset).execute().body();
+ }
+
+ /**
+ * 发送get请求
+ *
+ * @param urlString 网址
+ * @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null
+ */
+ public static String get(String urlString) {
+ return get(urlString, HttpGlobalConfig.getTimeout());
+ }
+
+ /**
+ * 发送get请求
+ *
+ * @param urlString 网址
+ * @param timeout 超时时长,-1表示默认超时,单位毫秒
+ * @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null
+ * @since 3.2.0
+ */
+ public static String get(String urlString, int timeout) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.get(urlString).timeout(timeout).execute().body();
+ }
+
+ /**
+ * 发送get请求
+ *
+ * @param urlString 网址
+ * @param paramMap post表单数据
+ * @return 返回数据
+ */
+ public static String get(String urlString, Map
+ * 请求体body参数支持两种类型:
+ *
+ *
+ * 1. 标准参数,例如 a=1&b=2 这种格式
+ * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
+ *
+ *
+ * @param urlString 网址
+ * @param body post表单数据
+ * @return 返回数据
+ */
+ public static String post(String urlString, String body) {
+ return post(urlString, body, HttpGlobalConfig.getTimeout());
+ }
+
+ /**
+ * 发送post请求
+ * 请求体body参数支持两种类型:
+ *
+ *
+ * 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
+ */
+ public static String post(String urlString, String body, int timeout) {
+ return HttpRequest.post(urlString).timeout(timeout).body(body).execute().body();
+ }
+
+ // ---------------------------------------------------------------------------------------- download
+
+ /**
+ * 下载远程文本
+ *
+ * @param url 请求的url
+ * @param customCharsetName 自定义的字符集
+ * @return 文本
+ */
+ public static String downloadString(String url, String customCharsetName) {
+ return downloadString(url, CharsetUtil.charset(customCharsetName), null);
+ }
+
+ /**
+ * 下载远程文本
+ *
+ * @param url 请求的url
+ * @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换
+ * @return 文本
+ */
+ public static String downloadString(String url, Charset customCharset) {
+ return downloadString(url, customCharset, null);
+ }
+
+ /**
+ * 下载远程文本
+ *
+ * @param url 请求的url
+ * @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换
+ * @param streamPress 进度条 {@link StreamProgress}
+ * @return 文本
+ */
+ public static String downloadString(String url, Charset customCharset, StreamProgress streamPress) {
+ return HttpDownloader.downloadString(url, customCharset, streamPress);
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param dest 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @return 文件大小
+ */
+ public static long downloadFile(String url, String dest) {
+ return downloadFile(url, FileUtil.file(dest));
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @return 文件大小
+ */
+ public static long downloadFile(String url, File destFile) {
+ return downloadFile(url, destFile, null);
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @param timeout 超时,单位毫秒,-1表示默认超时
+ * @return 文件大小
+ * @since 4.0.4
+ */
+ public static long downloadFile(String url, File destFile, int timeout) {
+ return downloadFile(url, destFile, timeout, null);
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @param streamProgress 进度条
+ * @return 文件大小
+ */
+ public static long downloadFile(String url, File destFile, 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 long downloadFile(String url, File destFile, int timeout, 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(String url, String dest) {
+ return downloadFileFromUrl(url, FileUtil.file(dest));
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param destFile 目标文件或目录,当为目录时,取URL中的文件名,取不到使用编码后的URL做为文件名
+ * @return 下载的文件对象
+ * @since 5.4.1
+ */
+ public static File downloadFileFromUrl(String url, 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(String url, File destFile, 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(String url, File destFile, 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(String url, File destFile, int timeout, StreamProgress streamProgress) {
+ return HttpDownloader.downloadForFile(url, destFile, timeout, streamProgress);
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param out 将下载内容写到输出流中 {@link OutputStream}
+ * @param isCloseOut 是否关闭输出流
+ * @return 文件大小
+ */
+ public static long download(String url, OutputStream out, boolean isCloseOut) {
+ return download(url, out, isCloseOut, null);
+ }
+
+ /**
+ * 下载远程文件
+ *
+ * @param url 请求的url
+ * @param out 将下载内容写到输出流中 {@link OutputStream}
+ * @param isCloseOut 是否关闭输出流
+ * @param streamProgress 进度条
+ * @return 文件大小
+ */
+ public static long download(String url, OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {
+ return HttpDownloader.download(url, out, isCloseOut, streamProgress);
+ }
+
+ /**
+ * 下载远程文件数据,支持30x跳转
+ *
+ * @param url 请求的url
+ * @return 文件数据
+ * @since 5.3.6
+ */
+ public static byte[] downloadBytes(String url) {
+ return HttpDownloader.downloadBytes(url);
+ }
+
+ /**
+ * 将Map形式的Form表单数据转换为Url参数形式,会自动url编码键和值
+ *
+ * @param paramMap 表单数据
+ * @return url参数
+ */
+ public static String toParams(Map
+ * 编码键和值对
+ *
+ * @param paramMap 表单数据
+ * @param charsetName 编码
+ * @return url参数
+ * @deprecated 请使用 {@link #toParams(Map, Charset)}
+ */
+ @Deprecated
+ public static String toParams(Map
+ * 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(Map
+ * 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(Map
+ * 提供的值可以是url附带参数,但是不能只是url
+ *
+ *
+ * 表单的键值对会被url编码,但是url中原参数不会被编码
+ *
+ * @param url URL
+ * @param form 表单数据
+ * @param charset 编码
+ * @param isEncodeParams 是否对键和值做转义处理
+ * @return 合成后的URL
+ */
+ public static String urlWithForm(String url, Map
+ * 从ContentType中获取
+ *
+ * @param conn HTTP连接对象
+ * @return 字符集
+ */
+ public static String getCharset(HttpURLConnection conn) {
+ if (conn == null) {
+ return null;
+ }
+ return getCharset(conn.getContentType());
+ }
+
+ /**
+ * 从Http连接的头信息中获得字符集
+ * 从ContentType中获取
+ *
+ * @param contentType Content-Type
+ * @return 字符集
+ * @since 5.2.6
+ */
+ public static String getCharset(String contentType) {
+ if (StrUtil.isBlank(contentType)) {
+ return null;
+ }
+ return ReUtil.get(CHARSET_PATTERN, contentType, 1);
+ }
+
+ /**
+ * 从流中读取内容
+ * 首先尝试使用charset编码读取内容(如果为空默认UTF-8),如果isGetCharsetFromContent为true,则通过正则在正文中获取编码信息,转换为指定编码;
+ *
+ * @param in 输入流
+ * @param charset 字符集
+ * @param isGetCharsetFromContent 是否从返回内容中获得编码信息
+ * @return 内容
+ */
+ public static String getString(InputStream in, Charset charset, boolean isGetCharsetFromContent) {
+ final byte[] contentBytes = IoUtil.readBytes(in);
+ return getString(contentBytes, charset, isGetCharsetFromContent);
+ }
+
+ /**
+ * 从流中读取内容
+ * 首先尝试使用charset编码读取内容(如果为空默认UTF-8),如果isGetCharsetFromContent为true,则通过正则在正文中获取编码信息,转换为指定编码;
+ *
+ * @param contentBytes 内容byte数组
+ * @param charset 字符集
+ * @param isGetCharsetFromContent 是否从返回内容中获得编码信息
+ * @return 内容
+ */
+ public static String getString(byte[] contentBytes, Charset charset, boolean isGetCharsetFromContent) {
+ if (null == contentBytes) {
+ return null;
+ }
+
+ if (null == charset) {
+ charset = CharsetUtil.CHARSET_UTF_8;
+ }
+ String content = new String(contentBytes, charset);
+ if (isGetCharsetFromContent) {
+ final String charsetInContentStr = ReUtil.get(META_CHARSET_PATTERN, content, 1);
+ if (StrUtil.isNotBlank(charsetInContentStr)) {
+ Charset charsetInContent = null;
+ try {
+ charsetInContent = Charset.forName(charsetInContentStr);
+ } catch (Exception e) {
+ if (StrUtil.containsIgnoreCase(charsetInContentStr, "utf-8") || StrUtil.containsIgnoreCase(charsetInContentStr, "utf8")) {
+ charsetInContent = CharsetUtil.CHARSET_UTF_8;
+ } else if (StrUtil.containsIgnoreCase(charsetInContentStr, "gbk")) {
+ charsetInContent = CharsetUtil.CHARSET_GBK;
+ }
+ // ignore
+ }
+ if (null != charsetInContent && false == charset.equals(charsetInContent)) {
+ content = new String(contentBytes, charsetInContent);
+ }
+ }
+ }
+ return content;
+ }
+
+ /**
+ * 根据文件扩展名获得MimeType
+ *
+ * @param filePath 文件路径或文件名
+ * @param defaultValue 当获取MimeType为null时的默认值
+ * @return MimeType
+ * @see FileUtil#getMimeType(String)
+ * @since 4.6.5
+ */
+ public static String getMimeType(String filePath, String defaultValue) {
+ return ObjectUtil.defaultIfNull(getMimeType(filePath), defaultValue);
+ }
+
+ /**
+ * 根据文件扩展名获得MimeType
+ *
+ * @param filePath 文件路径或文件名
+ * @return MimeType
+ * @see FileUtil#getMimeType(String)
+ */
+ public static String getMimeType(String filePath) {
+ return FileUtil.getMimeType(filePath);
+ }
+
+ /**
+ * 从请求参数的body中判断请求的Content-Type类型,支持的类型有:
+ *
+ *
+ * 1. application/json
+ * 1. application/xml
+ *
+ *
+ * @param body 请求参数体
+ * @return Content-Type类型,如果无法判断返回null
+ * @see aiyh.utils.tool.cn.hutool.http.ContentType#get(String)
+ * @since 3.2.0
+ */
+ public static String getContentTypeByRequestBody(String body) {
+ final aiyh.utils.tool.cn.hutool.http.ContentType contentType = ContentType.get(body);
+ return (null == contentType) ? null : contentType.toString();
+ }
+
+ /**
+ * 创建简易的Http服务器
+ *
+ * @param port 端口
+ * @return {@link SimpleServer}
+ * @since 5.2.6
+ */
+ public static SimpleServer createServer(int port) {
+ return new SimpleServer(port);
+ }
+
+ /**
+ * 构建简单的账号秘密验证信息,构建后类似于:
+ *
+ * Basic YWxhZGRpbjpvcGVuc2VzYW1l
+ *
+ *
+ * @param username 账号
+ * @param password 密码
+ * @param charset 编码(如果账号或密码中有非ASCII字符适用)
+ * @return 密码验证信息
+ * @since 5.4.6
+ */
+ public static String buildBasicAuth(String username, String password, Charset charset) {
+ final String data = username.concat(":").concat(password);
+ return "Basic " + Base64.encode(data, charset);
+ }
+
+ /**
+ * 关闭Cookie
+ *
+ * @see GlobalCookieManager#setCookieManager(CookieManager)
+ * @since 5.6.5
+ */
+ public static void closeCookie() {
+ GlobalCookieManager.setCookieManager(null);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/Method.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/Method.java
new file mode 100644
index 0000000..2ef3498
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/Method.java
@@ -0,0 +1,10 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+/**
+ * Http方法枚举
+ *
+ * @author Looly
+ */
+public enum Method {
+ GET, POST, HEAD, OPTIONS, PUT, DELETE, TRACE, CONNECT, PATCH
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/MultipartOutputStream.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/MultipartOutputStream.java
new file mode 100644
index 0000000..a043513
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/MultipartOutputStream.java
@@ -0,0 +1,185 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.convert.Convert;
+import aiyh.utils.tool.cn.hutool.core.io.IORuntimeException;
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+import aiyh.utils.tool.cn.hutool.core.io.resource.MultiResource;
+import aiyh.utils.tool.cn.hutool.core.io.resource.Resource;
+import aiyh.utils.tool.cn.hutool.core.io.resource.StringResource;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+
+/**
+ * Multipart/form-data输出流封装
+ * 遵循RFC2388规范
+ *
+ * @author looly
+ * @since 5.7.17
+ */
+public class MultipartOutputStream extends OutputStream {
+
+ private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n";
+ private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
+
+ private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n";
+
+ private final OutputStream out;
+ private final Charset charset;
+ private final String boundary;
+
+ private boolean isFinish;
+
+ /**
+ * 构造,使用全局默认的边界字符串
+ *
+ * @param out HTTP写出流
+ * @param charset 编码
+ */
+ public MultipartOutputStream(OutputStream out, Charset charset) {
+ this(out, charset, HttpGlobalConfig.getBoundary());
+ }
+
+ /**
+ * 构造
+ *
+ * @param out HTTP写出流
+ * @param charset 编码
+ * @param boundary 边界符
+ * @since 5.7.17
+ */
+ public MultipartOutputStream(OutputStream out, Charset charset, String boundary) {
+ this.out = out;
+ this.charset = charset;
+ this.boundary = boundary;
+ }
+
+ /**
+ * 添加Multipart表单的数据项
+ *
+ * --分隔符(boundary)[换行]
+ * Content-Disposition: form-data; name="参数名"[换行]
+ * [换行]
+ * 参数值[换行]
+ *
+ *
+ * --分隔符(boundary)[换行]
+ * Content-Disposition: form-data; name="表单名"; filename="文件名"[换行]
+ * Content-Type: MIME类型[换行]
+ * [换行]
+ * 文件的二进制内容[换行]
+ *
+ *
+ * @param formFieldName 表单名
+ * @param value 值,可以是普通值、资源(如文件等)
+ * @return this
+ * @throws IORuntimeException IO异常
+ */
+ public MultipartOutputStream write(String formFieldName, Object value) throws IORuntimeException {
+ // 多资源
+ if (value instanceof MultiResource) {
+ for (Resource subResource : (MultiResource) value) {
+ write(formFieldName, subResource);
+ }
+ return this;
+ }
+
+ // --分隔符(boundary)[换行]
+ beginPart();
+
+ if (value instanceof Resource) {
+ appendResource(formFieldName, (Resource) value);
+ } else {
+ appendResource(formFieldName,
+ new StringResource(Convert.toStr(value), null, this.charset));
+ }
+
+ write(StrUtil.CRLF);
+ return this;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ this.out.write(b);
+ }
+
+ /**
+ * 上传表单结束
+ *
+ * @throws IORuntimeException IO异常
+ */
+ public void finish() throws IORuntimeException {
+ if (!isFinish) {
+ write(StrUtil.format("--{}--\r\n", boundary));
+ this.isFinish = true;
+ }
+ }
+
+ @Override
+ public void close() {
+ finish();
+ IoUtil.close(this.out);
+ }
+
+ /**
+ * 添加Multipart表单的Resource数据项,支持包括{@link HttpResource}资源格式
+ *
+ * @param formFieldName 表单名
+ * @param resource 资源
+ * @throws IORuntimeException IO异常
+ */
+ private void appendResource(String formFieldName, Resource resource) throws IORuntimeException {
+ final String fileName = resource.getName();
+
+ // Content-Disposition
+ if (null == fileName) {
+ // Content-Disposition: form-data; name="参数名"[换行]
+ write(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName));
+ } else {
+ // Content-Disposition: form-data; name="参数名"; filename="文件名"[换行]
+ write(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, fileName));
+ }
+
+ // Content-Type
+ if (resource instanceof HttpResource) {
+ final String contentType = ((HttpResource) resource).getContentType();
+ if (StrUtil.isNotBlank(contentType)) {
+ // Content-Type: 类型[换行]
+ write(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, contentType));
+ }
+ } else if (StrUtil.isNotEmpty(fileName)) {
+ // 根据name的扩展名指定互联网媒体类型,默认二进制流数据
+ write(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE,
+ HttpUtil.getMimeType(fileName, ContentType.OCTET_STREAM.getValue())));
+ }
+
+ // 内容
+ write("\r\n");
+ resource.writeTo(this);
+ }
+
+ /**
+ * part开始,写出:
+ *
+ * --分隔符(boundary)[换行]
+ *
+ */
+ private void beginPart() {
+ // --分隔符(boundary)[换行]
+ write("--", boundary, StrUtil.CRLF);
+ }
+
+ /**
+ * 写出对象
+ *
+ * @param objs 写出的对象(转换为字符串)
+ */
+ private void write(Object... objs) {
+ IoUtil.write(this, this.charset, false, objs);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/Status.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/Status.java
new file mode 100644
index 0000000..6ea34fd
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/Status.java
@@ -0,0 +1,189 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+/**
+ * 返回状态码
+ *
+ * @author Looly
+ */
+interface Status {
+ /**
+ * HTTP Status-Code 200: OK.
+ */
+ int HTTP_OK = 200;
+
+ /**
+ * HTTP Status-Code 201: Created.
+ */
+ int HTTP_CREATED = 201;
+
+ /**
+ * HTTP Status-Code 202: Accepted.
+ */
+ int HTTP_ACCEPTED = 202;
+
+ /**
+ * HTTP Status-Code 203: Non-Authoritative Information.
+ */
+ int HTTP_NOT_AUTHORITATIVE = 203;
+
+ /**
+ * HTTP Status-Code 204: No Content.
+ */
+ int HTTP_NO_CONTENT = 204;
+
+ /**
+ * HTTP Status-Code 205: Reset Content.
+ */
+ int HTTP_RESET = 205;
+
+ /**
+ * HTTP Status-Code 206: Partial Content.
+ */
+ int HTTP_PARTIAL = 206;
+
+ /* 3XX: relocation/redirect */
+
+ /**
+ * HTTP Status-Code 300: Multiple Choices.
+ */
+ int HTTP_MULT_CHOICE = 300;
+
+ /**
+ * HTTP Status-Code 301: Moved Permanently.
+ */
+ int HTTP_MOVED_PERM = 301;
+
+ /**
+ * HTTP Status-Code 302: Temporary Redirect.
+ */
+ int HTTP_MOVED_TEMP = 302;
+
+ /**
+ * HTTP Status-Code 303: See Other.
+ */
+ int HTTP_SEE_OTHER = 303;
+
+ /**
+ * HTTP Status-Code 304: Not Modified.
+ */
+ int HTTP_NOT_MODIFIED = 304;
+
+ /**
+ * HTTP Status-Code 305: Use Proxy.
+ */
+ int HTTP_USE_PROXY = 305;
+
+ /* 4XX: client error */
+
+ /**
+ * HTTP Status-Code 400: Bad Request.
+ */
+ int HTTP_BAD_REQUEST = 400;
+
+ /**
+ * HTTP Status-Code 401: Unauthorized.
+ */
+ int HTTP_UNAUTHORIZED = 401;
+
+ /**
+ * HTTP Status-Code 402: Payment Required.
+ */
+ int HTTP_PAYMENT_REQUIRED = 402;
+
+ /**
+ * HTTP Status-Code 403: Forbidden.
+ */
+ int HTTP_FORBIDDEN = 403;
+
+ /**
+ * HTTP Status-Code 404: Not Found.
+ */
+ int HTTP_NOT_FOUND = 404;
+
+ /**
+ * HTTP Status-Code 405: Method Not Allowed.
+ */
+ int HTTP_BAD_METHOD = 405;
+
+ /**
+ * HTTP Status-Code 406: Not Acceptable.
+ */
+ int HTTP_NOT_ACCEPTABLE = 406;
+
+ /**
+ * HTTP Status-Code 407: Proxy Authentication Required.
+ */
+ int HTTP_PROXY_AUTH = 407;
+
+ /**
+ * HTTP Status-Code 408: Request Time-Out.
+ */
+ int HTTP_CLIENT_TIMEOUT = 408;
+
+ /**
+ * HTTP Status-Code 409: Conflict.
+ */
+ int HTTP_CONFLICT = 409;
+
+ /**
+ * HTTP Status-Code 410: Gone.
+ */
+ int HTTP_GONE = 410;
+
+ /**
+ * HTTP Status-Code 411: Length Required.
+ */
+ int HTTP_LENGTH_REQUIRED = 411;
+
+ /**
+ * HTTP Status-Code 412: Precondition Failed.
+ */
+ int HTTP_PRECON_FAILED = 412;
+
+ /**
+ * HTTP Status-Code 413: Request Entity Too Large.
+ */
+ int HTTP_ENTITY_TOO_LARGE = 413;
+
+ /**
+ * HTTP Status-Code 414: Request-URI Too Large.
+ */
+ int HTTP_REQ_TOO_LONG = 414;
+
+ /**
+ * HTTP Status-Code 415: Unsupported Media Type.
+ */
+ int HTTP_UNSUPPORTED_TYPE = 415;
+
+ /* 5XX: server error */
+
+ /**
+ * HTTP Status-Code 500: Internal Server Error.
+ */
+ int HTTP_INTERNAL_ERROR = 500;
+
+ /**
+ * HTTP Status-Code 501: Not Implemented.
+ */
+ int HTTP_NOT_IMPLEMENTED = 501;
+
+ /**
+ * HTTP Status-Code 502: Bad Gateway.
+ */
+ int HTTP_BAD_GATEWAY = 502;
+
+ /**
+ * HTTP Status-Code 503: Service Unavailable.
+ */
+ int HTTP_UNAVAILABLE = 503;
+
+ /**
+ * HTTP Status-Code 504: Gateway Timeout.
+ */
+ int HTTP_GATEWAY_TIMEOUT = 504;
+
+ /**
+ * HTTP Status-Code 505: HTTP Version Not Supported.
+ */
+ int HTTP_VERSION = 505;
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/body/BytesBody.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/BytesBody.java
new file mode 100644
index 0000000..11643af
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/BytesBody.java
@@ -0,0 +1,40 @@
+package aiyh.utils.tool.cn.hutool.http.body;
+
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+
+import java.io.OutputStream;
+
+/**
+ * bytes类型的Http request body,主要发送编码后的表单数据或rest body(如JSON或XML)
+ *
+ * @author looly
+ * @since 5.7.17
+ */
+public class BytesBody implements RequestBody {
+
+ private final byte[] content;
+
+ /**
+ * 创建 Http request body
+ *
+ * @param content body内容,编码后
+ * @return BytesBody
+ */
+ public static BytesBody create(byte[] content) {
+ return new BytesBody(content);
+ }
+
+ /**
+ * 构造
+ *
+ * @param content Body内容,编码后
+ */
+ public BytesBody(byte[] content) {
+ this.content = content;
+ }
+
+ @Override
+ public void write(OutputStream out) {
+ IoUtil.write(out, false, content);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/body/FormUrlEncodedBody.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/FormUrlEncodedBody.java
new file mode 100644
index 0000000..087b93f
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/FormUrlEncodedBody.java
@@ -0,0 +1,38 @@
+package aiyh.utils.tool.cn.hutool.http.body;
+
+import aiyh.utils.tool.cn.hutool.core.net.url.UrlQuery;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+import java.nio.charset.Charset;
+import java.util.Map;
+
+/**
+ * application/x-www-form-urlencoded 类型请求body封装
+ *
+ * @author looly
+ * @since 5.7.17
+ */
+public class FormUrlEncodedBody extends BytesBody {
+
+ /**
+ * 创建 Http request body
+ *
+ * @param form 表单
+ * @param charset 编码
+ * @return FormUrlEncodedBody
+ */
+ public static FormUrlEncodedBody create(Map
+ * 遵循RFC2388规范
+ *
+ * @author looly
+ * @since 5.3.5
+ */
+public class MultipartBody implements RequestBody {
+
+ private static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary=";
+
+ /**
+ * 存储表单数据
+ */
+ private final Map
+ *
+ * 1、从Content-Type头中获取编码,类似于:text/html;charset=utf-8
+ *
+ *
+ * @return 编码,默认UTF-8
+ */
+ public Charset getCharset() {
+ if (null == this.charsetCache) {
+ final String contentType = getContentType();
+ final String charsetStr = aiyh.utils.tool.cn.hutool.http.HttpUtil.getCharset(contentType);
+ this.charsetCache = CharsetUtil.parse(charsetStr, DEFAULT_CHARSET);
+ }
+
+ return this.charsetCache;
+ }
+
+ /**
+ * 获得User-Agent
+ *
+ * @return User-Agent字符串
+ */
+ public String getUserAgentStr() {
+ return getHeader(aiyh.utils.tool.cn.hutool.http.Header.USER_AGENT);
+ }
+
+ /**
+ * 获得User-Agent,未识别返回null
+ *
+ * @return User-Agent字符串,未识别返回null
+ */
+ public UserAgent getUserAgent() {
+ return UserAgentUtil.parse(getUserAgentStr());
+ }
+
+ /**
+ * 获得Cookie信息字符串
+ *
+ * @return cookie字符串
+ */
+ public String getCookiesStr() {
+ return getHeader(Header.COOKIE);
+ }
+
+ /**
+ * 获得Cookie信息列表
+ *
+ * @return Cookie信息列表
+ */
+ public Collection
+ * 使用{@link #getCharset()}判断编码,判断失败使用UTF-8编码
+ *
+ * @return 请求
+ */
+ public String getBody() {
+ return getBody(getCharset());
+ }
+
+ /**
+ * 获取请求体文本,可以是form表单、json、xml等任意内容
+ *
+ * @param charset 编码
+ * @return 请求
+ */
+ public String getBody(Charset charset) {
+ return StrUtil.str(getBodyBytes(), charset);
+ }
+
+ /**
+ * 获取body的bytes数组
+ *
+ * @return body的bytes数组
+ */
+ public byte[] getBodyBytes() {
+ if (null == this.bodyCache) {
+ this.bodyCache = IoUtil.readBytes(getBodyStream(), true);
+ }
+ return this.bodyCache;
+ }
+
+ /**
+ * 获取请求体的流,流中可以读取请求内容,包括请求表单数据或文件上传数据
+ *
+ * @return 流
+ */
+ public InputStream getBodyStream() {
+ return this.httpExchange.getRequestBody();
+ }
+
+ /**
+ * 获取指定名称的参数值,取第一个值
+ *
+ * @param name 参数名
+ * @return 参数值
+ * @since 5.5.8
+ */
+ public String getParam(String name) {
+ return getParams().get(name, 0);
+ }
+
+ /**
+ * 获取指定名称的参数值
+ *
+ * @param name 参数名
+ * @return 参数值
+ * @since 5.5.8
+ */
+ public List
+ * 1、X-Forwarded-For
+ * 2、X-Real-IP
+ * 3、Proxy-Client-IP
+ * 4、WL-Proxy-Client-IP
+ *
+ *
+ *
+ * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。
+ *
+ * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。
+ *
+ * 包括文件和普通表单数据
+ * 在同一次请求中,此方法只能被执行一次!
+ *
+ * @param uploadSetting 上传文件的设定,包括最大文件大小、保存在内存的边界大小、临时目录、扩展名限定等
+ * @return MultiPart表单
+ * @throws IORuntimeException IO异常
+ * @since 5.3.0
+ */
+ public MultipartFormData parseMultipart(UploadSetting uploadSetting) throws IORuntimeException {
+ final MultipartFormData formData = new MultipartFormData(uploadSetting);
+ try {
+ formData.parseRequestStream(getBodyStream(), getCharset());
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+
+ return formData;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerResponse.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerResponse.java
new file mode 100644
index 0000000..eb30108
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerResponse.java
@@ -0,0 +1,429 @@
+package aiyh.utils.tool.cn.hutool.http.server;
+
+import aiyh.utils.tool.cn.hutool.core.io.FileUtil;
+import aiyh.utils.tool.cn.hutool.core.io.IORuntimeException;
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ObjectUtil;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+import aiyh.utils.tool.cn.hutool.core.util.URLUtil;
+import aiyh.utils.tool.cn.hutool.http.ContentType;
+import aiyh.utils.tool.cn.hutool.http.Header;
+import aiyh.utils.tool.cn.hutool.http.HttpStatus;
+import aiyh.utils.tool.cn.hutool.http.HttpUtil;
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpExchange;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Http响应对象,用于写出数据到客户端
+ */
+public class HttpServerResponse extends HttpServerBase {
+
+ private Charset charset;
+ /**
+ * 是否已经发送了Http状态码,如果没有,提前写出状态码
+ */
+ private boolean isSendCode;
+
+ /**
+ * 构造
+ *
+ * @param httpExchange {@link HttpExchange}
+ */
+ public HttpServerResponse(HttpExchange httpExchange) {
+ super(httpExchange);
+ }
+
+ /**
+ * 发送HTTP状态码,Content-Length为0不定长度,会输出Transfer-encoding: chunked
+ *
+ * @param httpStatusCode HTTP状态码,见HttpStatus
+ * @return this
+ */
+ public HttpServerResponse send(int httpStatusCode) {
+ return send(httpStatusCode, 0);
+ }
+
+ /**
+ * 发送成功状态码
+ *
+ * @return this
+ */
+ public HttpServerResponse sendOk() {
+ return send(aiyh.utils.tool.cn.hutool.http.HttpStatus.HTTP_OK);
+ }
+
+ /**
+ * 发送成功状态码
+ *
+ * @param bodyLength 响应体长度,默认0表示不定长度,会输出Transfer-encoding: chunked
+ * @return this
+ * @since 5.5.7
+ */
+ public HttpServerResponse sendOk(int bodyLength) {
+ return send(aiyh.utils.tool.cn.hutool.http.HttpStatus.HTTP_OK, bodyLength);
+ }
+
+ /**
+ * 发送404错误页
+ *
+ * @param content 错误页页面内容,默认text/html类型
+ * @return this
+ */
+ public HttpServerResponse send404(String content) {
+ return sendError(HttpStatus.HTTP_NOT_FOUND, content);
+ }
+
+ /**
+ * 发送错误页
+ *
+ * @param errorCode HTTP错误状态码,见HttpStatus
+ * @param content 错误页页面内容,默认text/html类型
+ * @return this
+ */
+ public HttpServerResponse sendError(int errorCode, String content) {
+ send(errorCode);
+ setContentType(aiyh.utils.tool.cn.hutool.http.ContentType.TEXT_HTML.toString());
+ return write(content);
+ }
+
+ /**
+ * 发送HTTP状态码
+ *
+ * @param httpStatusCode HTTP状态码,见HttpStatus
+ * @param bodyLength 响应体长度,默认0表示不定长度,会输出Transfer-encoding: chunked
+ * @return this
+ */
+ public HttpServerResponse send(int httpStatusCode, long bodyLength) {
+ if (this.isSendCode) {
+ throw new IORuntimeException("Http status code has been send!");
+ }
+
+ try {
+ this.httpExchange.sendResponseHeaders(httpStatusCode, bodyLength);
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+
+ this.isSendCode = true;
+ return this;
+ }
+
+ /**
+ * 获得所有响应头,获取后可以添加新的响应头
+ *
+ * @return 响应头
+ */
+ public Headers getHeaders() {
+ return this.httpExchange.getResponseHeaders();
+ }
+
+ /**
+ * 添加响应头,如果已经存在,则追加
+ *
+ * @param header 头key
+ * @param value 值
+ * @return this
+ */
+ public HttpServerResponse addHeader(String header, String value) {
+ getHeaders().add(header, value);
+ return this;
+ }
+
+ /**
+ * 设置响应头,如果已经存在,则覆盖
+ *
+ * @param header 头key
+ * @param value 值
+ * @return this
+ */
+ public HttpServerResponse setHeader(aiyh.utils.tool.cn.hutool.http.Header header, String value) {
+ return setHeader(header.getValue(), value);
+ }
+
+ /**
+ * 设置响应头,如果已经存在,则覆盖
+ *
+ * @param header 头key
+ * @param value 值
+ * @return this
+ */
+ public HttpServerResponse setHeader(String header, String value) {
+ getHeaders().set(header, value);
+ return this;
+ }
+
+ /**
+ * 设置响应头,如果已经存在,则覆盖
+ *
+ * @param header 头key
+ * @param value 值列表
+ * @return this
+ */
+ public HttpServerResponse setHeader(String header, List
+ * 如果用户传入的信息无charset信息,自动根据charset补充,charset设置见{@link #setCharset(Charset)}
+ *
+ * @param contentType Content-Type头内容
+ * @return this
+ */
+ public HttpServerResponse setContentType(String contentType) {
+ if (null != contentType && null != this.charset) {
+ if (!contentType.contains(";charset=")) {
+ contentType = ContentType.build(contentType, this.charset);
+ }
+ }
+
+ return setHeader(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_TYPE, contentType);
+ }
+
+ /**
+ * 设置Content-Length头
+ *
+ * @param contentLength Content-Length头内容
+ * @return this
+ */
+ public HttpServerResponse setContentLength(long contentLength) {
+ return setHeader(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_LENGTH, String.valueOf(contentLength));
+ }
+
+ /**
+ * 设置响应的编码
+ *
+ * @param charset 编码
+ * @return this
+ */
+ public HttpServerResponse setCharset(Charset charset) {
+ this.charset = charset;
+ return this;
+ }
+
+ /**
+ * 设置属性
+ *
+ * @param name 属性名
+ * @param value 属性值
+ * @return this
+ */
+ public HttpServerResponse setAttr(String name, Object value) {
+ this.httpExchange.setAttribute(name, value);
+ return this;
+ }
+
+ /**
+ * 获取响应数据流
+ *
+ * @return 响应数据流
+ */
+ public OutputStream getOut() {
+ if (!this.isSendCode) {
+ sendOk();
+ }
+ return this.httpExchange.getResponseBody();
+ }
+
+ /**
+ * 获取响应数据流
+ *
+ * @return 响应数据流
+ */
+ public PrintWriter getWriter() {
+ final Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
+ return new PrintWriter(new OutputStreamWriter(getOut(), charset));
+ }
+
+ /**
+ * 写出数据到客户端
+ *
+ * @param data 数据
+ * @param contentType Content-Type类型
+ * @return this
+ */
+ public HttpServerResponse write(String data, String contentType) {
+ setContentType(contentType);
+ return write(data);
+ }
+
+ /**
+ * 写出数据到客户端
+ *
+ * @param data 数据
+ * @return this
+ */
+ public HttpServerResponse write(String data) {
+ final Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
+ return write(StrUtil.bytes(data, charset));
+ }
+
+ /**
+ * 写出数据到客户端
+ *
+ * @param data 数据
+ * @param contentType 返回的类型
+ * @return this
+ */
+ public HttpServerResponse write(byte[] data, String contentType) {
+ setContentType(contentType);
+ return write(data);
+ }
+
+ /**
+ * 写出数据到客户端
+ *
+ * @param data 数据
+ * @return this
+ */
+ public HttpServerResponse write(byte[] data) {
+ final ByteArrayInputStream in = new ByteArrayInputStream(data);
+ return write(in, in.available());
+ }
+
+ /**
+ * 返回数据给客户端
+ *
+ * @param in 需要返回客户端的内容
+ * @param contentType 返回的类型
+ * @return this
+ * @since 5.2.6
+ */
+ public HttpServerResponse write(InputStream in, String contentType) {
+ return write(in, 0, contentType);
+ }
+
+ /**
+ * 返回数据给客户端
+ *
+ * @param in 需要返回客户端的内容
+ * @param length 内容长度,默认0表示不定长度,会输出Transfer-encoding: chunked
+ * @param contentType 返回的类型
+ * @return this
+ * @since 5.2.7
+ */
+ public HttpServerResponse write(InputStream in, int length, String contentType) {
+ setContentType(contentType);
+ return write(in, length);
+ }
+
+ /**
+ * 写出数据到客户端
+ *
+ * @param in 数据流
+ * @return this
+ */
+ public HttpServerResponse write(InputStream in) {
+ return write(in, 0);
+ }
+
+ /**
+ * 写出数据到客户端
+ *
+ * @param in 数据流
+ * @param length 指定响应内容长度,默认0表示不定长度,会输出Transfer-encoding: chunked
+ * @return this
+ */
+ public HttpServerResponse write(InputStream in, int length) {
+ if (!isSendCode) {
+ sendOk(Math.max(0, length));
+ }
+ OutputStream out = null;
+ try {
+ out = this.httpExchange.getResponseBody();
+ IoUtil.copy(in, out);
+ } finally {
+ IoUtil.close(out);
+ IoUtil.close(in);
+ }
+ return this;
+ }
+
+ /**
+ * 返回文件给客户端(文件下载)
+ *
+ * @param file 写出的文件对象
+ * @return this
+ * @since 5.2.6
+ */
+ public HttpServerResponse write(File file) {
+ return write(file, null);
+ }
+
+ /**
+ * 返回文件给客户端(文件下载)
+ *
+ * @param file 写出的文件对象
+ * @param fileName 文件名
+ * @return this
+ * @since 5.5.8
+ */
+ public HttpServerResponse write(File file, String fileName) {
+ final long fileSize = file.length();
+ if (fileSize > Integer.MAX_VALUE) {
+ throw new IllegalArgumentException("File size is too bigger than " + Integer.MAX_VALUE);
+ }
+
+ if (StrUtil.isBlank(fileName)) {
+ fileName = file.getName();
+ }
+ final String contentType = ObjectUtil.defaultIfNull(HttpUtil.getMimeType(fileName), "application/octet-stream");
+ BufferedInputStream in = null;
+ try {
+ in = FileUtil.getInputStream(file);
+ write(in, (int) fileSize, contentType, fileName);
+ } finally {
+ IoUtil.close(in);
+ }
+ return this;
+ }
+
+ /**
+ * 返回文件数据给客户端(文件下载)
+ *
+ * @param in 需要返回客户端的内容
+ * @param contentType 返回的类型
+ * @param fileName 文件名
+ * @since 5.2.6
+ */
+ public void write(InputStream in, String contentType, String fileName) {
+ write(in, 0, contentType, fileName);
+ }
+
+ /**
+ * 返回文件数据给客户端(文件下载)
+ *
+ * @param in 需要返回客户端的内容
+ * @param length 长度
+ * @param contentType 返回的类型
+ * @param fileName 文件名
+ * @return this
+ * @since 5.2.7
+ */
+ public HttpServerResponse write(InputStream in, int length, String contentType, String fileName) {
+ final Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
+
+ if (!contentType.startsWith("text/")) {
+ // 非文本类型数据直接走下载
+ setHeader(Header.CONTENT_DISPOSITION, StrUtil.format("attachment;filename={}", URLUtil.encode(fileName, charset)));
+ }
+ return write(in, length, contentType);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/SimpleServer.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/SimpleServer.java
new file mode 100644
index 0000000..7c00924
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/SimpleServer.java
@@ -0,0 +1,226 @@
+package aiyh.utils.tool.cn.hutool.http.server;
+
+import aiyh.utils.tool.cn.hutool.core.io.IORuntimeException;
+import aiyh.utils.tool.cn.hutool.core.lang.Console;
+import aiyh.utils.tool.cn.hutool.core.thread.GlobalThreadPool;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+import aiyh.utils.tool.cn.hutool.http.server.action.Action;
+import aiyh.utils.tool.cn.hutool.http.server.action.RootAction;
+import aiyh.utils.tool.cn.hutool.http.server.filter.HttpFilter;
+import aiyh.utils.tool.cn.hutool.http.server.filter.SimpleFilter;
+import aiyh.utils.tool.cn.hutool.http.server.handler.ActionHandler;
+import com.sun.net.httpserver.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * 简易Http服务器,基于{@link HttpServer}
+ *
+ * @author looly
+ * @since 5.2.5
+ */
+public class SimpleServer {
+
+ private final HttpServer server;
+ private final List
+ * 此方法需在以下方法前之前调用:
+ *
+ *
+ *
+ *
+ * @param filter {@link Filter} 请求过滤器
+ * @return this
+ * @since 5.5.7
+ */
+ public SimpleServer addFilter(Filter filter) {
+ this.filters.add(filter);
+ return this;
+ }
+
+ /**
+ * 增加请求过滤器,此过滤器对所有请求有效
+ * 此方法需在以下方法前之前调用:
+ *
+ *
+ *
+ *
+ * @param filter {@link Filter} 请求过滤器
+ * @return this
+ * @since 5.5.7
+ */
+ public SimpleServer addFilter(HttpFilter filter) {
+ return addFilter(new SimpleFilter() {
+ @Override
+ public void doFilter(HttpExchange httpExchange, Chain chain) throws IOException {
+ filter.doFilter(new HttpServerRequest(httpExchange), new HttpServerResponse(httpExchange), chain);
+ }
+ });
+ }
+
+ /**
+ * 增加请求处理规则
+ *
+ * @param path 路径,例如:/a/b 或者 a/b
+ * @param handler 处理器,包括请求和响应处理
+ * @return this
+ * @see #createContext(String, HttpHandler)
+ */
+ public SimpleServer addHandler(String path, HttpHandler handler) {
+ createContext(path, handler);
+ return this;
+ }
+
+ /**
+ * 创建请求映射上下文,创建后,用户访问指定路径可使用{@link HttpHandler} 中的规则进行处理
+ *
+ * @param path 路径,例如:/a/b 或者 a/b
+ * @param handler 处理器,包括请求和响应处理
+ * @return {@link HttpContext}
+ * @since 5.5.7
+ */
+ public HttpContext createContext(String path, HttpHandler handler) {
+ // 非/开头的路径会报错
+ path = StrUtil.addPrefixIfNot(path, StrUtil.SLASH);
+ final HttpContext context = this.server.createContext(path, handler);
+ // 增加整体过滤器
+ context.getFilters().addAll(this.filters);
+ return context;
+ }
+
+ /**
+ * 设置根目录,默认的页面从root目录中读取解析返回
+ *
+ * @param root 路径
+ * @return this
+ */
+ public SimpleServer setRoot(String root) {
+ return setRoot(new File(root));
+ }
+
+ /**
+ * 设置根目录,默认的页面从root目录中读取解析返回
+ *
+ * @param root 路径
+ * @return this
+ */
+ public SimpleServer setRoot(File root) {
+ return addAction("/", new RootAction(root));
+ }
+
+ /**
+ * 增加请求处理规则
+ *
+ * @param path 路径
+ * @param action 处理器
+ * @return this
+ */
+ public SimpleServer addAction(String path, Action action) {
+ return addHandler(path, new ActionHandler(action));
+ }
+
+ /**
+ * 设置自定义线程池
+ *
+ * @param executor {@link Executor}
+ * @return this
+ */
+ public SimpleServer setExecutor(Executor executor) {
+ this.server.setExecutor(executor);
+ return this;
+ }
+
+ /**
+ * 获得原始HttpServer对象
+ *
+ * @return {@link HttpServer}
+ */
+ public HttpServer getRawServer() {
+ return this.server;
+ }
+
+ /**
+ * 获取服务器地址信息
+ *
+ * @return {@link InetSocketAddress}
+ */
+ public InetSocketAddress getAddress() {
+ return this.server.getAddress();
+ }
+
+ /**
+ * 启动Http服务器,启动后会阻塞当前线程
+ */
+ public void start() {
+ final InetSocketAddress address = getAddress();
+ Console.log("Hutool Simple Http Server listen on 【{}:{}】", address.getHostName(), address.getPort());
+ this.server.start();
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/Action.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/Action.java
new file mode 100644
index 0000000..749a4fb
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/Action.java
@@ -0,0 +1,26 @@
+package aiyh.utils.tool.cn.hutool.http.server.action;
+
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerRequest;
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerResponse;
+
+import java.io.IOException;
+
+/**
+ * 请求处理接口
+ * 当用户请求某个Path,则调用相应Action的doAction方法
+ *
+ * @author Looly
+ * @since 5.2.6
+ */
+@FunctionalInterface
+public interface Action {
+
+ /**
+ * 处理请求
+ *
+ * @param request 请求对象
+ * @param response 响应对象
+ * @throws IOException IO异常
+ */
+ void doAction(HttpServerRequest request, HttpServerResponse response) throws IOException;
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/RootAction.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/RootAction.java
new file mode 100644
index 0000000..3731d3f
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/RootAction.java
@@ -0,0 +1,86 @@
+package aiyh.utils.tool.cn.hutool.http.server.action;
+
+import aiyh.utils.tool.cn.hutool.core.collection.CollUtil;
+import aiyh.utils.tool.cn.hutool.core.io.FileUtil;
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerRequest;
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerResponse;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * 默认的处理器,通过解析用户传入的path,找到网页根目录下对应文件后返回
+ *
+ * @author looly
+ * @since 5.2.6
+ */
+public class RootAction implements Action {
+
+ public static final String DEFAULT_INDEX_FILE_NAME = "index.html";
+
+ private final File rootDir;
+ private final List
+ * 在测试HttpUrlConnection的时候,发现一部分手机无法连接[GithubPage]
+ *
+ *
+ * SoapClient client = SoapClient.create(url)
+ * .setMethod(methodName, namespaceURI)
+ * .setCharset(CharsetUtil.CHARSET_GBK)
+ * .setParam(param1, "XXX");
+ *
+ * String response = client.send(true);
+ *
+ *
+ *
+ * @author looly
+ * @since 4.5.4
+ */
+public class SoapClient extends HttpBase
+ * 方法名自动识别前缀,前缀和方法名使用“:”分隔
+ * 当识别到前缀后,自动添加xmlns属性,关联到默认的namespaceURI
+ *
+ * @param methodName 方法名
+ * @return this
+ */
+ public SoapClient setMethod(String methodName) {
+ return setMethod(methodName, ObjectUtil.defaultIfNull(this.namespaceURI, XMLConstants.NULL_NS_URI));
+ }
+
+ /**
+ * 设置请求方法
+ * 方法名自动识别前缀,前缀和方法名使用“:”分隔
+ * 当识别到前缀后,自动添加xmlns属性,关联到传入的namespaceURI
+ *
+ * @param methodName 方法名(可有前缀也可无)
+ * @param namespaceURI 命名空间URI
+ * @return this
+ */
+ public SoapClient setMethod(String methodName, String namespaceURI) {
+ final List
+ * 用于创建子节点等操作
+ *
+ * @return {@link SOAPBodyElement}
+ * @since 4.5.6
+ */
+ public SOAPBodyElement getMethodEle() {
+ return this.methodEle;
+ }
+
+ /**
+ * 获取SOAP消息对象 {@link SOAPMessage}
+ *
+ * @return {@link SOAPMessage}
+ * @since 4.5.6
+ */
+ public SOAPMessage getMessage() {
+ return this.message;
+ }
+
+ /**
+ * 获取SOAP请求消息
+ *
+ * @param pretty 是否格式化
+ * @return 消息字符串
+ */
+ public String getMsgStr(boolean pretty) {
+ return SoapUtil.toString(this.message, pretty, this.charset);
+ }
+
+ /**
+ * 将SOAP消息的XML内容输出到流
+ *
+ * @param out 输出流
+ * @return this
+ * @since 4.5.6
+ */
+ public SoapClient write(OutputStream out) {
+ try {
+ this.message.writeTo(out);
+ } catch (SOAPException | IOException e) {
+ throw new SoapRuntimeException(e);
+ }
+ return this;
+ }
+
+ /**
+ * 设置超时,单位:毫秒
+ * 超时包括:
+ *
+ *
+ * 1. 连接超时
+ * 2. 读取响应超时
+ *
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @see #setConnectionTimeout(int)
+ * @see #setReadTimeout(int)
+ */
+ public SoapClient timeout(int milliseconds) {
+ setConnectionTimeout(milliseconds);
+ setReadTimeout(milliseconds);
+ return this;
+ }
+
+ /**
+ * 设置连接超时,单位:毫秒
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @since 4.5.6
+ */
+ public SoapClient setConnectionTimeout(int milliseconds) {
+ this.connectionTimeout = milliseconds;
+ return this;
+ }
+
+ /**
+ * 设置连接超时,单位:毫秒
+ *
+ * @param milliseconds 超时毫秒数
+ * @return this
+ * @since 4.5.6
+ */
+ public SoapClient setReadTimeout(int milliseconds) {
+ this.readTimeout = milliseconds;
+ return this;
+ }
+
+ /**
+ * 执行Webservice请求,即发送SOAP内容
+ *
+ * @return 返回结果
+ */
+ public SOAPMessage sendForMessage() {
+ final aiyh.utils.tool.cn.hutool.http.HttpResponse res = sendForResponse();
+ final MimeHeaders headers = new MimeHeaders();
+ for (Entry任务列表元素
+ *
+ * 配置表
+ *
+ * 查询配置信息
+ *
+ * @return 配置信息
+ */
+ @Select("select * from uf_ihg_el_config ")
+ @Associations({
+ @Association(
+ property = "icon",
+ column = "icon",
+ id = @Id(methodId = 1, value = String.class)),
+ @Association(
+ property = "iconActive",
+ column = "icon_active",
+ id = @Id(methodId = 1, value = String.class))
+ })
+ List查询配置信息
+ *
+ * @param configId 配置id
+ * @return 配置信息
+ */
+ @Select("select * from uf_ihg_el_config where id = #{configId}")
+ @Associations({
+ @Association(
+ property = "icon",
+ column = "icon",
+ id = @Id(methodId = 1, value = String.class)),
+ @Association(
+ property = "iconActive",
+ column = "icon_active",
+ id = @Id(methodId = 1, value = String.class))
+ })
+ IhgTaskElementConfigItem selectConfig(@ParamMapper("configId") String configId);
+
+
+ /**
+ * 查询图片地址
+ *
+ * @param id docId
+ * @return 图片地址
+ */
+ @Select("select concat('/weaver/weaver.file.FileDownload?fileid=',IMAGEFILEID) from docimagefile where DOCID = #{id}")
+ @AssociationMethod(1)
+ String selectImageFileId(String id);
+
+ /**
+ * 查询权限信息
+ *
+ * @param viewAuthoritySql 自定义权限sql
+ * @param user 用户
+ * @param map 参数
+ * @param where 条件参数
+ * @return 权限信息
+ */
+ @Select(custom = true)
+ List