interceptor : this.responseInterceptors) {
+ copied.addChain(interceptor);
+ }
+ return copied;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HTMLFilter.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HTMLFilter.java
new file mode 100644
index 0000000..fcea8de
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HTMLFilter.java
@@ -0,0 +1,539 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.lang.Console;
+import aiyh.utils.tool.cn.hutool.core.map.SafeConcurrentHashMap;
+import aiyh.utils.tool.cn.hutool.core.util.CharUtil;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * HTML过滤器,用于去除XSS(Cross Site Scripting) 漏洞隐患。
+ *
+ *
+ * 此类中的方法非线程安全
+ *
+ *
+ *
+ * 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 P_REMOVE_PAIR_BLANKS = new SafeConcurrentHashMap<>();
+ private static final ConcurrentMap P_REMOVE_SELF_BLANKS = new SafeConcurrentHashMap<>();
+
+ /**
+ * set of allowed html elements, along with allowed attributes for each element
+ **/
+ private final Map> vAllowed;
+ /**
+ * counts of open tags for each (allowable) html element
+ **/
+ private final Map vTagCounts = new HashMap<>();
+
+ /**
+ * html elements which must always be self-closing (e.g. "<img />")
+ **/
+ private final String[] vSelfClosingTags;
+ /**
+ * html elements which must always have separate opening and closing tags (e.g. "<b></b>")
+ **/
+ private final String[] vNeedClosingTags;
+ /**
+ * set of disallowed html elements
+ **/
+ private final String[] vDisallowed;
+ /**
+ * attributes which should be checked for valid protocols
+ **/
+ private final String[] vProtocolAtts;
+ /**
+ * allowed protocols
+ **/
+ private final String[] vAllowedProtocols;
+ /**
+ * tags which should be removed if they contain no content (e.g. "<b></b>" or "<b />")
+ **/
+ private final String[] vRemoveBlanks;
+ /**
+ * entities allowed within html markup
+ **/
+ private final String[] vAllowedEntities;
+ /**
+ * flag determining whether comments are allowed in input String.
+ */
+ private final boolean stripComment;
+ private final boolean encodeQuotes;
+ private boolean vDebug = false;
+ /**
+ * flag determining whether to try to make tags when presented with "unbalanced" angle brackets (e.g. "<b text </b>" becomes "<b> text </g>").
+ * If set to false, unbalanced angle brackets will be
+ * html escaped.
+ */
+ private final boolean alwaysMakeTags;
+
+ /**
+ * Default constructor.
+ */
+ public HTMLFilter() {
+ vAllowed = new HashMap<>();
+
+ final ArrayList a_atts = new ArrayList<>();
+ a_atts.add("href");
+ a_atts.add("target");
+ vAllowed.put("a", a_atts);
+
+ final ArrayList img_atts = new ArrayList<>();
+ img_atts.add("src");
+ img_atts.add("width");
+ img_atts.add("height");
+ img_atts.add("alt");
+ vAllowed.put("img", img_atts);
+
+ final ArrayList no_atts = new ArrayList<>();
+ vAllowed.put("b", no_atts);
+ vAllowed.put("strong", no_atts);
+ vAllowed.put("i", no_atts);
+ vAllowed.put("em", no_atts);
+
+ vSelfClosingTags = new String[]{"img"};
+ vNeedClosingTags = new String[]{"a", "b", "strong", "i", "em"};
+ vDisallowed = new String[]{};
+ vAllowedProtocols = new String[]{"http", "mailto", "https"}; // no ftp.
+ vProtocolAtts = new String[]{"src", "href"};
+ vRemoveBlanks = new String[]{"a", "b", "strong", "i", "em"};
+ vAllowedEntities = new String[]{"amp", "gt", "lt", "quot"};
+ stripComment = true;
+ encodeQuotes = true;
+ alwaysMakeTags = true;
+ }
+
+ /**
+ * Set debug flag to true. Otherwise use default settings. See the default constructor.
+ *
+ * @param debug turn debug on with a true argument
+ */
+ public HTMLFilter(final boolean debug) {
+ this();
+ vDebug = debug;
+
+ }
+
+ /**
+ * Map-parameter configurable constructor.
+ *
+ * @param conf map containing configuration. keys match field names.
+ */
+ @SuppressWarnings("unchecked")
+ public HTMLFilter(final Map conf) {
+
+ assert conf.containsKey("vAllowed") : "configuration requires vAllowed";
+ assert conf.containsKey("vSelfClosingTags") : "configuration requires vSelfClosingTags";
+ assert conf.containsKey("vNeedClosingTags") : "configuration requires vNeedClosingTags";
+ assert conf.containsKey("vDisallowed") : "configuration requires vDisallowed";
+ assert conf.containsKey("vAllowedProtocols") : "configuration requires vAllowedProtocols";
+ assert conf.containsKey("vProtocolAtts") : "configuration requires vProtocolAtts";
+ assert conf.containsKey("vRemoveBlanks") : "configuration requires vRemoveBlanks";
+ assert conf.containsKey("vAllowedEntities") : "configuration requires vAllowedEntities";
+
+ vAllowed = Collections.unmodifiableMap((HashMap>) conf.get("vAllowed"));
+ vSelfClosingTags = (String[]) conf.get("vSelfClosingTags");
+ vNeedClosingTags = (String[]) conf.get("vNeedClosingTags");
+ vDisallowed = (String[]) conf.get("vDisallowed");
+ vAllowedProtocols = (String[]) conf.get("vAllowedProtocols");
+ vProtocolAtts = (String[]) conf.get("vProtocolAtts");
+ vRemoveBlanks = (String[]) conf.get("vRemoveBlanks");
+ vAllowedEntities = (String[]) conf.get("vAllowedEntities");
+ stripComment = conf.containsKey("stripComment") ? (Boolean) conf.get("stripComment") : true;
+ encodeQuotes = conf.containsKey("encodeQuotes") ? (Boolean) conf.get("encodeQuotes") : true;
+ alwaysMakeTags = conf.containsKey("alwaysMakeTags") ? (Boolean) conf.get("alwaysMakeTags") : true;
+ }
+
+ private void reset() {
+ vTagCounts.clear();
+ }
+
+ private void debug(final String msg) {
+ if (vDebug) {
+ Console.log(msg);
+ }
+ }
+
+ // ---------------------------------------------------------------
+ // my versions of some PHP library functions
+ public static String chr(final int decimal) {
+ return String.valueOf((char) decimal);
+ }
+
+ public static String htmlSpecialChars(final String s) {
+ String result = s;
+ result = regexReplace(P_AMP, "&", result);
+ result = regexReplace(P_QUOTE, """, result);
+ result = regexReplace(P_LEFT_ARROW, "<", result);
+ result = regexReplace(P_RIGHT_ARROW, ">", result);
+ return result;
+ }
+
+ // ---------------------------------------------------------------
+
+ /**
+ * given a user submitted input String, filter out any invalid or restricted html.
+ *
+ * @param input text (i.e. submitted by a user) than may contain html
+ * @return "clean" version of input, with only valid, whitelisted html elements allowed
+ */
+ public String filter(final String input) {
+ reset();
+ String s = input;
+
+ debug("************************************************");
+ debug(" INPUT: " + input);
+
+ s = escapeComments(s);
+ debug(" escapeComments: " + s);
+
+ s = balanceHTML(s);
+ debug(" balanceHTML: " + s);
+
+ s = checkTags(s);
+ debug(" checkTags: " + s);
+
+ s = processRemoveBlanks(s);
+ debug("processRemoveBlanks: " + s);
+
+ s = validateEntities(s);
+ debug(" validateEntites: " + s);
+
+ debug("************************************************\n\n");
+ return s;
+ }
+
+ public boolean isAlwaysMakeTags() {
+ return alwaysMakeTags;
+ }
+
+ public boolean isStripComments() {
+ return stripComment;
+ }
+
+ private String escapeComments(final String s) {
+ final Matcher m = P_COMMENTS.matcher(s);
+ final StringBuffer buf = new StringBuffer();
+ if (m.find()) {
+ final String match = m.group(1); // (.*?)
+ m.appendReplacement(buf, Matcher.quoteReplacement(""));
+ }
+ m.appendTail(buf);
+
+ return buf.toString();
+ }
+
+ private String balanceHTML(String s) {
+ if (alwaysMakeTags) {
+ //
+ // try and form html
+ //
+ s = regexReplace(P_END_ARROW, "", s);
+ s = regexReplace(P_BODY_TO_END, "<$1>", s);
+ s = regexReplace(P_XML_CONTENT, "$1<$2", s);
+
+ } else {
+ //
+ // escape stray brackets
+ //
+ s = regexReplace(P_STRAY_LEFT_ARROW, "<$1", s);
+ s = regexReplace(P_STRAY_RIGHT_ARROW, "$1$2><", s);
+
+ //
+ // the last regexp causes '<>' entities to appear
+ // (we need to do a lookahead assertion so that the last bracket can
+ // be used in the next pass of the regexp)
+ //
+ s = regexReplace(P_BOTH_ARROWS, "", s);
+ }
+
+ return s;
+ }
+
+ private String checkTags(String s) {
+ Matcher m = P_TAGS.matcher(s);
+
+ final StringBuffer buf = new StringBuffer();
+ while (m.find()) {
+ String replaceStr = m.group(1);
+ replaceStr = processTag(replaceStr);
+ m.appendReplacement(buf, Matcher.quoteReplacement(replaceStr));
+ }
+ m.appendTail(buf);
+
+ // these get tallied in processTag
+ // (remember to reset before subsequent calls to filter method)
+ final StringBuilder sBuilder = new StringBuilder(buf.toString());
+ for (String key : vTagCounts.keySet()) {
+ for (int ii = 0; ii < vTagCounts.get(key); ii++) {
+ sBuilder.append("").append(key).append(">");
+ }
+ }
+ s = sBuilder.toString();
+
+ return s;
+ }
+
+ private String processRemoveBlanks(final String s) {
+ String result = s;
+ for (String tag : vRemoveBlanks) {
+ if (!P_REMOVE_PAIR_BLANKS.containsKey(tag)) {
+ P_REMOVE_PAIR_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?>" + tag + ">"));
+ }
+ result = regexReplace(P_REMOVE_PAIR_BLANKS.get(tag), "", result);
+ if (!P_REMOVE_SELF_BLANKS.containsKey(tag)) {
+ P_REMOVE_SELF_BLANKS.putIfAbsent(tag, Pattern.compile("<" + tag + "(\\s[^>]*)?/>"));
+ }
+ result = regexReplace(P_REMOVE_SELF_BLANKS.get(tag), "", result);
+ }
+
+ return result;
+ }
+
+ private static String regexReplace(final Pattern regex_pattern, final String replacement, final String s) {
+ Matcher m = regex_pattern.matcher(s);
+ return m.replaceAll(replacement);
+ }
+
+ private String processTag(final String s) {
+ // ending tags
+ Matcher m = P_END_TAG.matcher(s);
+ if (m.find()) {
+ final String name = m.group(1).toLowerCase();
+ if (allowed(name)) {
+ if (!inArray(name, vSelfClosingTags)) {
+ if (vTagCounts.containsKey(name)) {
+ vTagCounts.put(name, vTagCounts.get(name) - 1);
+ return "" + name + ">";
+ }
+ }
+ }
+ }
+
+ // starting tags
+ m = P_START_TAG.matcher(s);
+ if (m.find()) {
+ final String name = m.group(1).toLowerCase();
+ final String body = m.group(2);
+ String ending = m.group(3);
+
+ // debug( "in a starting tag, name='" + name + "'; body='" + body + "'; ending='" + ending + "'" );
+ if (allowed(name)) {
+ final StringBuilder params = new StringBuilder();
+
+ final Matcher m2 = P_QUOTED_ATTRIBUTES.matcher(body);
+ final Matcher m3 = P_UNQUOTED_ATTRIBUTES.matcher(body);
+ final List paramNames = new ArrayList<>();
+ final List paramValues = new ArrayList<>();
+ while (m2.find()) {
+ paramNames.add(m2.group(1)); // ([a-z0-9]+)
+ paramValues.add(m2.group(3)); // (.*?)
+ }
+ while (m3.find()) {
+ paramNames.add(m3.group(1)); // ([a-z0-9]+)
+ paramValues.add(m3.group(3)); // ([^\"\\s']+)
+ }
+
+ String paramName, paramValue;
+ for (int ii = 0; ii < paramNames.size(); ii++) {
+ paramName = paramNames.get(ii).toLowerCase();
+ paramValue = paramValues.get(ii);
+
+ // debug( "paramName='" + paramName + "'" );
+ // debug( "paramValue='" + paramValue + "'" );
+ // debug( "allowed? " + vAllowed.get( name ).contains( paramName ) );
+
+ if (allowedAttribute(name, paramName)) {
+ if (inArray(paramName, vProtocolAtts)) {
+ paramValue = processParamProtocol(paramValue);
+ }
+ params.append(CharUtil.SPACE).append(paramName).append("=\"").append(paramValue).append("\"");
+ }
+ }
+
+ if (inArray(name, vSelfClosingTags)) {
+ ending = " /";
+ }
+
+ if (inArray(name, vNeedClosingTags)) {
+ ending = "";
+ }
+
+ if (ending == null || ending.length() < 1) {
+ if (vTagCounts.containsKey(name)) {
+ vTagCounts.put(name, vTagCounts.get(name) + 1);
+ } else {
+ vTagCounts.put(name, 1);
+ }
+ } else {
+ ending = " /";
+ }
+ return "<" + name + params + ending + ">";
+ } else {
+ return "";
+ }
+ }
+
+ // comments
+ m = P_COMMENT.matcher(s);
+ if (!stripComment && m.find()) {
+ return "<" + m.group() + ">";
+ }
+
+ return "";
+ }
+
+ private String processParamProtocol(String s) {
+ s = decodeEntities(s);
+ final Matcher m = P_PROTOCOL.matcher(s);
+ if (m.find()) {
+ final String protocol = m.group(1);
+ if (!inArray(protocol, vAllowedProtocols)) {
+ // bad protocol, turn into local anchor link instead
+ s = "#" + s.substring(protocol.length() + 1);
+ if (s.startsWith("#//")) {
+ s = "#" + s.substring(3);
+ }
+ }
+ }
+
+ return s;
+ }
+
+ private String decodeEntities(String s) {
+ StringBuffer buf = new StringBuffer();
+
+ Matcher m = P_ENTITY.matcher(s);
+ while (m.find()) {
+ final String match = m.group(1);
+ final int decimal = Integer.decode(match);
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ buf = new StringBuffer();
+ m = P_ENTITY_UNICODE.matcher(s);
+ while (m.find()) {
+ final String match = m.group(1);
+ final int decimal = Integer.parseInt(match, 16);
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ buf = new StringBuffer();
+ m = P_ENCODE.matcher(s);
+ while (m.find()) {
+ final String match = m.group(1);
+ final int decimal = Integer.parseInt(match, 16);
+ m.appendReplacement(buf, Matcher.quoteReplacement(chr(decimal)));
+ }
+ m.appendTail(buf);
+ s = buf.toString();
+
+ s = validateEntities(s);
+ return s;
+ }
+
+ private String validateEntities(final String s) {
+ StringBuffer buf = new StringBuffer();
+
+ // validate entities throughout the string
+ Matcher m = P_VALID_ENTITIES.matcher(s);
+ while (m.find()) {
+ final String one = m.group(1); // ([^&;]*)
+ final String two = m.group(2); // (?=(;|&|$))
+ m.appendReplacement(buf, Matcher.quoteReplacement(checkEntity(one, two)));
+ }
+ m.appendTail(buf);
+
+ return encodeQuotes(buf.toString());
+ }
+
+ private String encodeQuotes(final String s) {
+ if (encodeQuotes) {
+ StringBuffer buf = new StringBuffer();
+ Matcher m = P_VALID_QUOTES.matcher(s);
+ while (m.find()) {
+ final String one = m.group(1); // (>|^)
+ final String two = m.group(2); // ([^<]+?)
+ final String three = m.group(3); // (<|$)
+ m.appendReplacement(buf, Matcher.quoteReplacement(one + regexReplace(P_QUOTE, """, two) + three));
+ }
+ m.appendTail(buf);
+ return buf.toString();
+ } else {
+ return s;
+ }
+ }
+
+ private String checkEntity(final String preamble, final String term) {
+
+ return ";".equals(term) && isValidEntity(preamble) ? '&' + preamble : "&" + preamble;
+ }
+
+ private boolean isValidEntity(final String entity) {
+ return inArray(entity, vAllowedEntities);
+ }
+
+ private static boolean inArray(final String s, final String[] array) {
+ for (String item : array) {
+ if (item != null && item.equals(s)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean allowed(final String name) {
+ return (vAllowed.isEmpty() || vAllowed.containsKey(name)) && !inArray(name, vDisallowed);
+ }
+
+ private boolean allowedAttribute(final String name, final String paramName) {
+ return allowed(name) && (vAllowed.isEmpty() || vAllowed.get(name).contains(paramName));
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/Header.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/Header.java
new file mode 100644
index 0000000..3723c1f
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/Header.java
@@ -0,0 +1,153 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+/**
+ * Http 头域
+ *
+ * @author Looly
+ */
+public enum Header {
+
+ //------------------------------------------------------------- 通用头域
+ /**
+ * 提供验证头,例如:
+ *
+ * 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工具类
+ *
+ *
+ * 比如我们在使用爬虫爬取HTML页面后,需要对返回页面的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字符为安全的字符,以下字符被转义:
+ *
+ * - ' 替换为 ' (' doesn't work in HTML4)
+ * - " 替换为 "
+ * - & 替换为 &
+ * - < 替换为 <
+ * - > 替换为 >
+ *
+ *
+ * @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 子类类型,方便链式编程
+ * @author Looly
+ */
+@SuppressWarnings("unchecked")
+public abstract class HttpBase {
+
+ /**
+ * 默认的请求编码、URL的encode、decode编码
+ */
+ protected static final Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;
+
+ /**
+ * HTTP/1.0
+ */
+ public static final String HTTP_1_0 = "HTTP/1.0";
+ /**
+ * HTTP/1.1
+ */
+ public static final String HTTP_1_1 = "HTTP/1.1";
+
+ /**
+ * 存储头信息
+ */
+ protected Map> headers = new HashMap<>();
+ /**
+ * 编码
+ */
+ protected Charset charset = DEFAULT_CHARSET;
+ /**
+ * http版本
+ */
+ protected String httpVersion = HTTP_1_1;
+ /**
+ * 存储主体
+ */
+ protected byte[] bodyBytes;
+
+ // ---------------------------------------------------------------- Headers start
+
+ /**
+ * 根据name获取头信息
+ * 根据RFC2616规范,header的name不区分大小写
+ *
+ * @param name Header名
+ * @return Header值
+ */
+ public String header(String name) {
+ final List values = headerList(name);
+ if (CollectionUtil.isEmpty(values)) {
+ return null;
+ }
+ return values.get(0);
+ }
+
+ /**
+ * 根据name获取头信息列表
+ *
+ * @param name Header名
+ * @return Header值
+ * @since 3.1.1
+ */
+ public List headerList(String name) {
+ if (StrUtil.isBlank(name)) {
+ return null;
+ }
+
+ final CaseInsensitiveMap> headersIgnoreCase = new CaseInsensitiveMap<>(this.headers);
+ return headersIgnoreCase.get(name.trim());
+ }
+
+ /**
+ * 根据name获取头信息
+ *
+ * @param name Header名
+ * @return Header值
+ */
+ public String header(Header name) {
+ if (null == name) {
+ return null;
+ }
+ return header(name.toString());
+ }
+
+ /**
+ * 设置一个header
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中
+ *
+ * @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 values = headers.get(name.trim());
+ if (isOverride || CollectionUtil.isEmpty(values)) {
+ final ArrayList valueList = new ArrayList<>();
+ valueList.add(value);
+ headers.put(name.trim(), valueList);
+ } else {
+ values.add(value.trim());
+ }
+ }
+ return (T) this;
+ }
+
+ /**
+ * 设置一个header
+ * 如果覆盖模式,则替换之前的值,否则加入到值列表中
+ *
+ * @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 headers, boolean isOverride) {
+ if (MapUtil.isEmpty(headers)) {
+ return (T) this;
+ }
+
+ for (Entry entry : headers.entrySet()) {
+ this.header(entry.getKey(), StrUtil.nullToEmpty(entry.getValue()), isOverride);
+ }
+ return (T) this;
+ }
+
+ /**
+ * 设置请求头
+ * 不覆盖原有请求头
+ *
+ * @param headers 请求头
+ * @return this
+ */
+ public T header(Map> headers) {
+ return header(headers, false);
+ }
+
+ /**
+ * 设置请求头
+ *
+ * @param headers 请求头
+ * @param isOverride 是否覆盖已有头信息
+ * @return this
+ * @since 4.0.8
+ */
+ public T header(Map> headers, boolean isOverride) {
+ if (MapUtil.isEmpty(headers)) {
+ return (T) this;
+ }
+
+ String name;
+ for (Entry> entry : headers.entrySet()) {
+ name = entry.getKey();
+ for (String value : entry.getValue()) {
+ this.header(name, StrUtil.nullToEmpty(value), isOverride);
+ }
+ }
+ return (T) this;
+ }
+
+ /**
+ * 新增请求头
+ * 不覆盖原有请求头
+ *
+ * @param headers 请求头
+ * @return this
+ * @since 4.0.3
+ */
+ public T addHeaders(Map headers) {
+ if (MapUtil.isEmpty(headers)) {
+ return (T) this;
+ }
+
+ for (Entry entry : headers.entrySet()) {
+ this.header(entry.getKey(), StrUtil.nullToEmpty(entry.getValue()), false);
+ }
+ return (T) this;
+ }
+
+ /**
+ * 移除一个头信息
+ *
+ * @param name Header名
+ * @return this
+ */
+ public T removeHeader(String name) {
+ if (name != null) {
+ headers.remove(name.trim());
+ }
+ return (T) this;
+ }
+
+ /**
+ * 移除一个头信息
+ *
+ * @param name Header名
+ * @return this
+ */
+ public T removeHeader(Header name) {
+ return removeHeader(name.toString());
+ }
+
+ /**
+ * 获取headers
+ *
+ * @return Headers Map
+ */
+ public Map> headers() {
+ return Collections.unmodifiableMap(headers);
+ }
+
+ /**
+ * 清除所有头信息,包括全局头信息
+ *
+ * @return this
+ * @since 5.7.13
+ */
+ public T clearHeaders() {
+ this.headers.clear();
+ return (T) this;
+ }
+ // ---------------------------------------------------------------- Headers end
+
+ /**
+ * 返回http版本
+ *
+ * @return String
+ */
+ public String httpVersion() {
+ return httpVersion;
+ }
+
+ /**
+ * 设置http版本,此方法不会影响到实际请求的HTTP版本,只用于帮助判断是否connect:Keep-Alive
+ *
+ * @param httpVersion Http版本,{@link HttpBase#HTTP_1_0},{@link HttpBase#HTTP_1_1}
+ * @return this
+ */
+ public T httpVersion(String httpVersion) {
+ this.httpVersion = httpVersion;
+ return (T) this;
+ }
+
+ /**
+ * 获取bodyBytes存储字节码
+ *
+ * @return byte[]
+ */
+ public byte[] bodyBytes() {
+ return this.bodyBytes;
+ }
+
+ /**
+ * 返回字符集
+ *
+ * @return 字符集
+ */
+ public String charset() {
+ return charset.name();
+ }
+
+ /**
+ * 设置字符集
+ *
+ * @param charset 字符集
+ * @return T 自己
+ * @see CharsetUtil
+ */
+ public T charset(String charset) {
+ if (StrUtil.isNotBlank(charset)) {
+ charset(Charset.forName(charset));
+ }
+ return (T) this;
+ }
+
+ /**
+ * 设置字符集
+ *
+ * @param charset 字符集
+ * @return T 自己
+ * @see CharsetUtil
+ */
+ public T charset(Charset charset) {
+ if (null != charset) {
+ this.charset = charset;
+ }
+ return (T) this;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = StrUtil.builder();
+ sb.append("Request Headers: ").append(StrUtil.CRLF);
+ for (Entry> entry : this.headers.entrySet()) {
+ sb.append(" ").append(
+ entry.getKey()).append(": ").append(CollUtil.join(entry.getValue(), ","))
+ .append(StrUtil.CRLF);
+ }
+
+ sb.append("Request Body: ").append(StrUtil.CRLF);
+ sb.append(" ").append(StrUtil.str(this.bodyBytes, this.charset)).append(StrUtil.CRLF);
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpConfig.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpConfig.java
new file mode 100755
index 0000000..ac885fb
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpConfig.java
@@ -0,0 +1,300 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.lang.Assert;
+import aiyh.utils.tool.cn.hutool.core.net.SSLUtil;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+/**
+ * Http配置项
+ *
+ * @author looly
+ * @since 5.8.0
+ */
+public class HttpConfig {
+
+ /**
+ * 创建默认Http配置信息
+ *
+ * @return HttpConfig
+ */
+ public static HttpConfig create() {
+ return new HttpConfig();
+ }
+
+ /**
+ * 默认连接超时
+ */
+ int connectionTimeout = HttpGlobalConfig.getTimeout();
+ /**
+ * 默认读取超时
+ */
+ int readTimeout = HttpGlobalConfig.getTimeout();
+
+ /**
+ * 是否禁用缓存
+ */
+ boolean isDisableCache;
+
+ /**
+ * 最大重定向次数
+ */
+ int maxRedirectCount = HttpGlobalConfig.getMaxRedirectCount();
+
+ /**
+ * 代理
+ */
+ Proxy proxy;
+
+ /**
+ * HostnameVerifier,用于HTTPS安全连接
+ */
+ HostnameVerifier hostnameVerifier;
+ /**
+ * SSLSocketFactory,用于HTTPS安全连接
+ */
+ SSLSocketFactory ssf;
+
+ /**
+ * Chuncked块大小,0或小于0表示不设置Chuncked模式
+ */
+ int blockSize;
+
+ /**
+ * 获取是否忽略响应读取时可能的EOF异常。
+ * 在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 requestInterceptors = GlobalInterceptor.INSTANCE.getCopiedRequestInterceptor();
+ /**
+ * 响应后的拦截器,用于在响应后处理逻辑
+ */
+ final HttpInterceptor.Chain responseInterceptors = GlobalInterceptor.INSTANCE.getCopiedResponseInterceptor();
+
+ /**
+ * 重定向时是否使用拦截器
+ */
+ boolean interceptorOnRedirect;
+
+ /**
+ * 设置超时,单位:毫秒
+ * 超时包括:
+ *
+ *
+ * 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 interceptor) {
+ this.requestInterceptors.addChain(interceptor);
+ return this;
+ }
+
+ /**
+ * 设置拦截器,用于在请求前重新编辑请求
+ *
+ * @param interceptor 拦截器实现
+ * @return this
+ */
+ public HttpConfig addResponseInterceptor(HttpInterceptor interceptor) {
+ this.responseInterceptors.addChain(interceptor);
+ return this;
+ }
+
+ /**
+ * 重定向时是否使用拦截器
+ *
+ * @param interceptorOnRedirect 重定向时是否使用拦截器
+ * @return this
+ */
+ public HttpConfig setInterceptorOnRedirect(boolean interceptorOnRedirect) {
+ this.interceptorOnRedirect = interceptorOnRedirect;
+ return this;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpConnection.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpConnection.java
new file mode 100644
index 0000000..571c8d2
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpConnection.java
@@ -0,0 +1,568 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.map.MapUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ObjectUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ReflectUtil;
+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.ssl.DefaultSSLInfo;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.*;
+import java.nio.charset.Charset;
+import java.nio.charset.UnsupportedCharsetException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * http连接对象,对HttpURLConnection的包装
+ *
+ * @author Looly
+ */
+public class HttpConnection {
+
+ private final URL url;
+ private final Proxy proxy;
+ private HttpURLConnection conn;
+
+ /**
+ * 创建HttpConnection
+ *
+ * @param urlStr URL
+ * @param proxy 代理,无代理传{@code null}
+ * @return HttpConnection
+ */
+ public static HttpConnection create(String urlStr, Proxy proxy) {
+ return create(URLUtil.toUrlForHttp(urlStr), proxy);
+ }
+
+ /**
+ * 创建HttpConnection
+ *
+ * @param url URL
+ * @param proxy 代理,无代理传{@code null}
+ * @return HttpConnection
+ */
+ public static HttpConnection create(URL url, Proxy proxy) {
+ return new HttpConnection(url, proxy);
+ }
+
+ // --------------------------------------------------------------- Constructor start
+
+ /**
+ * 构造HttpConnection
+ *
+ * @param url URL
+ * @param proxy 代理
+ */
+ public HttpConnection(URL url, Proxy proxy) {
+ this.url = url;
+ this.proxy = proxy;
+
+ // 初始化Http连接
+ initConn();
+ }
+
+ // --------------------------------------------------------------- Constructor end
+
+ /**
+ * 初始化连接相关信息
+ *
+ * @return HttpConnection
+ * @since 4.4.1
+ */
+ public HttpConnection initConn() {
+ try {
+ this.conn = openHttp();
+ } catch (IOException e) {
+ throw new HttpException(e);
+ }
+
+ // 默认读取响应内容
+ this.conn.setDoInput(true);
+
+ return this;
+ }
+
+ // --------------------------------------------------------------- Getters And Setters start
+
+ /**
+ * 获取请求方法,GET/POST
+ *
+ * @return 请求方法, GET/POST
+ */
+ public aiyh.utils.tool.cn.hutool.http.Method getMethod() {
+ return aiyh.utils.tool.cn.hutool.http.Method.valueOf(this.conn.getRequestMethod());
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param method 请求方法
+ * @return 自己
+ */
+ public HttpConnection setMethod(aiyh.utils.tool.cn.hutool.http.Method method) {
+ if (aiyh.utils.tool.cn.hutool.http.Method.POST.equals(method) //
+ || aiyh.utils.tool.cn.hutool.http.Method.PUT.equals(method)//
+ || aiyh.utils.tool.cn.hutool.http.Method.PATCH.equals(method)//
+ || aiyh.utils.tool.cn.hutool.http.Method.DELETE.equals(method)) {
+ this.conn.setUseCaches(false);
+
+ // 增加PATCH方法支持
+ if (aiyh.utils.tool.cn.hutool.http.Method.PATCH.equals(method)) {
+ try {
+ HttpGlobalConfig.allowPatch();
+ } catch (Exception ignore) {
+ // ignore
+ // https://github.com/dromara/hutool/issues/2832
+ }
+ }
+ }
+
+ // method
+ try {
+ this.conn.setRequestMethod(method.toString());
+ } catch (ProtocolException e) {
+ if (aiyh.utils.tool.cn.hutool.http.Method.PATCH.equals(method)) {
+ // 如果全局设置失效,此处针对单独链接重新设置
+ reflectSetMethod(method);
+ } else {
+ throw new HttpException(e);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 获取请求URL
+ *
+ * @return 请求URL
+ */
+ public URL getUrl() {
+ return url;
+ }
+
+ /**
+ * 获得代理
+ *
+ * @return {@link Proxy}
+ */
+ public Proxy getProxy() {
+ return proxy;
+ }
+
+ /**
+ * 获取HttpURLConnection对象
+ *
+ * @return HttpURLConnection
+ */
+ public HttpURLConnection getHttpURLConnection() {
+ return conn;
+ }
+
+ // --------------------------------------------------------------- Getters And Setters end
+
+ // ---------------------------------------------------------------- Headers start
+
+ /**
+ * 设置请求头
+ * 当请求头存在时,覆盖之
+ *
+ * @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> headerMap, boolean isOverride) {
+ if (MapUtil.isNotEmpty(headerMap)) {
+ String name;
+ for (Entry> entry : headerMap.entrySet()) {
+ name = entry.getKey();
+ for (String value : entry.getValue()) {
+ this.header(name, StrUtil.nullToEmpty(value), isOverride);
+ }
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 获取Http请求头
+ *
+ * @param name Header名
+ * @return Http请求头值
+ */
+ public String header(String name) {
+ return this.conn.getHeaderField(name);
+ }
+
+ /**
+ * 获取Http请求头
+ *
+ * @param name Header名
+ * @return Http请求头值
+ */
+ public String header(aiyh.utils.tool.cn.hutool.http.Header name) {
+ return header(name.toString());
+ }
+
+ /**
+ * 获取所有Http请求头
+ *
+ * @return Http请求头Map
+ */
+ public Map> headers() {
+ return this.conn.getHeaderFields();
+ }
+
+ // ---------------------------------------------------------------- Headers end
+
+ /**
+ * 设置https请求参数
+ * 有些时候htts请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
+ *
+ * @param hostnameVerifier 域名验证器,非https传入null
+ * @param ssf SSLSocketFactory,非https传入null
+ * @return this
+ * @throws HttpException KeyManagementException和NoSuchAlgorithmException异常包装
+ */
+ public HttpConnection setHttpsInfo(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> entry : this.conn.getHeaderFields().entrySet()) {
+ // sb.append(" ").append(entry).append(StrUtil.CRLF);
+ // }
+
+ return sb.toString();
+ }
+
+ // --------------------------------------------------------------- Private Method start
+
+ /**
+ * 初始化http或https请求参数
+ * 有些时候https请求会出现com.sun.net.ssl.internal.www.protocol.https.HttpsURLConnectionOldImpl的实现,此为sun内部api,按照普通http请求处理
+ *
+ * @return {@link HttpURLConnection},https返回{@link HttpsURLConnection}
+ */
+ private HttpURLConnection openHttp() throws IOException {
+ final URLConnection conn = openConnection();
+ if (!(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 过滤参数类型,HttpRequest或者HttpResponse
+ * @author looly
+ * @since 5.7.16
+ */
+@FunctionalInterface
+public interface HttpInterceptor> {
+
+ /**
+ * 处理请求
+ *
+ * @param httpObj 请求或响应对象
+ */
+ void process(T httpObj);
+
+ /**
+ * 拦截器链
+ *
+ * @param 过滤参数类型,HttpRequest或者HttpResponse
+ * @author looly
+ * @since 5.7.16
+ */
+ class Chain> implements aiyh.utils.tool.cn.hutool.core.lang.Chain, Chain> {
+ private final List> interceptors = new LinkedList<>();
+
+ @Override
+ public Chain addChain(HttpInterceptor element) {
+ interceptors.add(element);
+ return this;
+ }
+
+ @Override
+ public Iterator> iterator() {
+ return interceptors.iterator();
+ }
+
+ /**
+ * 清空
+ *
+ * @return this
+ * @since 5.8.0
+ */
+ public Chain clear() {
+ interceptors.clear();
+ return this;
+ }
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpRequest.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpRequest.java
new file mode 100755
index 0000000..61f77c8
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/HttpRequest.java
@@ -0,0 +1,1393 @@
+package aiyh.utils.tool.cn.hutool.http;
+
+import aiyh.utils.tool.cn.hutool.core.collection.CollUtil;
+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.resource.BytesResource;
+import aiyh.utils.tool.cn.hutool.core.io.resource.FileResource;
+import aiyh.utils.tool.cn.hutool.core.io.resource.MultiFileResource;
+import aiyh.utils.tool.cn.hutool.core.io.resource.Resource;
+import aiyh.utils.tool.cn.hutool.core.lang.Assert;
+import aiyh.utils.tool.cn.hutool.core.map.MapUtil;
+import aiyh.utils.tool.cn.hutool.core.map.TableMap;
+import aiyh.utils.tool.cn.hutool.core.net.SSLUtil;
+import aiyh.utils.tool.cn.hutool.core.net.url.UrlBuilder;
+import aiyh.utils.tool.cn.hutool.core.net.url.UrlQuery;
+import aiyh.utils.tool.cn.hutool.core.util.ArrayUtil;
+import aiyh.utils.tool.cn.hutool.core.util.ObjectUtil;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+import aiyh.utils.tool.cn.hutool.http.body.BytesBody;
+import aiyh.utils.tool.cn.hutool.http.body.FormUrlEncodedBody;
+import aiyh.utils.tool.cn.hutool.http.body.MultipartBody;
+import aiyh.utils.tool.cn.hutool.http.body.RequestBody;
+import aiyh.utils.tool.cn.hutool.http.cookie.GlobalCookieManager;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.File;
+import java.io.IOException;
+import java.net.*;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * http请求类
+ * Http请求类用于构建Http请求并同步获取结果,此类通过CookieManager持有域名对应的Cookie值,再次请求时会自动附带Cookie信息
+ *
+ * @author Looly
+ */
+public class HttpRequest extends HttpBase {
+
+ // ---------------------------------------------------------------- static Http Method start
+
+ /**
+ * POST请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ */
+ public static HttpRequest post(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.POST);
+ }
+
+ /**
+ * GET请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ */
+ public static HttpRequest get(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.GET);
+ }
+
+ /**
+ * HEAD请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ */
+ public static HttpRequest head(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.HEAD);
+ }
+
+ /**
+ * OPTIONS请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ */
+ public static HttpRequest options(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.OPTIONS);
+ }
+
+ /**
+ * PUT请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ */
+ public static HttpRequest put(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.PUT);
+ }
+
+ /**
+ * PATCH请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ * @since 3.0.9
+ */
+ public static HttpRequest patch(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.PATCH);
+ }
+
+ /**
+ * DELETE请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ */
+ public static HttpRequest delete(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.DELETE);
+ }
+
+ /**
+ * TRACE请求
+ *
+ * @param url URL
+ * @return HttpRequest
+ */
+ public static HttpRequest trace(String url) {
+ return of(url).method(aiyh.utils.tool.cn.hutool.http.Method.TRACE);
+ }
+
+ /**
+ * 构建一个HTTP请求
+ * 对于传入的URL,可以自定义是否解码已经编码的内容,设置见{@link HttpGlobalConfig#setDecodeUrl(boolean)}
+ * 在构建Http请求时,用户传入的URL可能有编码后和未编码的内容混合在一起,如果{@link HttpGlobalConfig#isDecodeUrl()}为{@code true},则会统一解码编码后的参数,
+ * 按照RFC3986规范,在发送请求时,全部编码之。如果为{@code false},则不会解码已经编码的内容,在请求时只编码需要编码的部分。
+ *
+ * @param url URL链接,默认自动编码URL中的参数等信息
+ * @return HttpRequest
+ * @since 5.7.18
+ */
+ public static HttpRequest of(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 form;
+ /**
+ * Cookie
+ */
+ private String cookie;
+ /**
+ * 是否为Multipart表单
+ */
+ private boolean isMultiPart;
+ /**
+ * 是否是REST请求模式
+ */
+ private boolean isRest;
+ /**
+ * 重定向次数计数器,内部使用
+ */
+ private int redirectCount;
+
+ /**
+ * 构造,URL编码默认使用UTF-8
+ *
+ * @param url URL
+ * @deprecated 请使用 {@link #of(String)}
+ */
+ @Deprecated
+ public HttpRequest(String url) {
+ this(UrlBuilder.ofHttp(url));
+ }
+
+ /**
+ * 构造
+ *
+ * @param url {@link UrlBuilder}
+ */
+ public HttpRequest(UrlBuilder url) {
+ this.url = Assert.notNull(url, "URL must be not null!");
+ // 给定默认URL编码
+ final Charset charset = url.getCharset();
+ if (null != charset) {
+ this.charset(charset);
+ }
+ // 给定一个默认头信息
+ this.header(GlobalHeaders.INSTANCE.headers);
+ }
+
+ /**
+ * 获取请求URL
+ *
+ * @return URL字符串
+ * @since 4.1.8
+ */
+ public String getUrl() {
+ return url.toString();
+ }
+
+ /**
+ * 设置URL
+ *
+ * @param url url字符串
+ * @return this
+ * @since 4.1.8
+ */
+ public HttpRequest setUrl(String url) {
+ return setUrl(UrlBuilder.ofHttp(url, this.charset));
+ }
+
+ /**
+ * 设置URL
+ *
+ * @param urlBuilder url字符串
+ * @return this
+ * @since 5.3.1
+ */
+ public HttpRequest setUrl(UrlBuilder urlBuilder) {
+ this.url = urlBuilder;
+ return this;
+ }
+
+ /**
+ * 设置{@link URLStreamHandler}
+ *
+ * 部分环境下需要单独设置此项,例如当 WebLogic Server 实例充当 SSL 客户端角色(它会尝试通过 SSL 连接到其他服务器或应用程序)时,
+ * 它会验证 SSL 服务器在数字证书中返回的主机名是否与用于连接 SSL 服务器的 URL 主机名相匹配。如果主机名不匹配,则删除此连接。
+ * 因此weblogic不支持https的sni协议的主机名验证,此时需要将此值设置为sun.net.www.protocol.https.Handler对象。
+ *
+ * 相关issue见:https://gitee.com/dromara/hutool/issues/IMD1X
+ *
+ * @param urlHandler {@link URLStreamHandler}
+ * @return this
+ * @since 4.1.9
+ */
+ public HttpRequest setUrlHandler(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}
+ * 在{@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 cookies) {
+ return cookie(CollUtil.isEmpty(cookies) ? null : cookies.toArray(new HttpCookie[0]));
+ }
+
+ /**
+ * 设置Cookie
+ * 自定义Cookie后会覆盖Hutool的默认Cookie行为
+ *
+ * @param cookies Cookie值数组,如果为{@code null}则设置无效,使用默认Cookie行为
+ * @return this
+ * @since 3.1.1
+ */
+ public HttpRequest cookie(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 formMap) {
+ if (MapUtil.isNotEmpty(formMap)) {
+ formMap.forEach(this::form);
+ }
+ return this;
+ }
+
+ /**
+ * 设置map<String, String>类型表单数据
+ *
+ * @param formMapStr 表单内容
+ * @return this
+ * @since 5.6.7
+ */
+ public HttpRequest formStr(Map formMapStr) {
+ if (MapUtil.isNotEmpty(formMapStr)) {
+ formMapStr.forEach(this::form);
+ }
+ return this;
+ }
+
+ /**
+ * 文件表单项
+ * 一旦有文件加入,表单变为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 form() {
+ return this.form;
+ }
+
+ /**
+ * 获取文件表单数据
+ *
+ * @return 文件表单Map
+ * @since 3.3.0
+ */
+ public Map fileForm() {
+ final Map result = MapUtil.newHashMap();
+ this.form.forEach((key, value) -> {
+ if (value instanceof Resource) {
+ result.put(key, (Resource) value);
+ }
+ });
+ return result;
+ }
+ // ---------------------------------------------------------------- Form end
+
+ // ---------------------------------------------------------------- Body start
+
+ /**
+ * 设置内容主体
+ * 请求体body参数支持两种类型:
+ *
+ *
+ * 1. 标准参数,例如 a=1&b=2 这种格式
+ * 2. Rest模式,此时body需要传入一个JSON或者XML字符串,Hutool会自动绑定其对应的Content-Type
+ *
+ *
+ * @param body 请求体
+ * @return this
+ */
+ public HttpRequest body(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 true}时,如果全局重定向次数非0,直接复用,否则设置默认2次。
+ * 当设置为{@code false}时,无论全局是否设置次数,都设置为0。
+ * 不调用此方法的情况下,使用全局默认的次数。
+ *
+ *
+ * @param isFollowRedirects 是否打开重定向
+ * @return this
+ */
+ public HttpRequest setFollowRedirects(boolean isFollowRedirects) {
+ if (isFollowRedirects) {
+ if (config.maxRedirectCount <= 0) {
+ // 默认两次跳转
+ return setMaxRedirectCount(2);
+ }
+ } else {
+ // 手动强制关闭重定向,此时不受全局重定向设置影响
+ if (config.maxRedirectCount < 0) {
+ return setMaxRedirectCount(0);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * 设置最大重定向次数
+ * 如果次数小于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 interceptor) {
+ return addRequestInterceptor(interceptor);
+ }
+
+ /**
+ * 设置拦截器,用于在请求前重新编辑请求
+ *
+ * @param interceptor 拦截器实现
+ * @return this
+ * @since 5.8.0
+ */
+ public HttpRequest addRequestInterceptor(HttpInterceptor interceptor) {
+ config.addRequestInterceptor(interceptor);
+ return this;
+ }
+
+ /**
+ * 设置拦截器,用于在请求前重新编辑请求
+ *
+ * @param interceptor 拦截器实现
+ * @return this
+ * @since 5.8.0
+ */
+ public HttpRequest addResponseInterceptor(HttpInterceptor interceptor) {
+ config.addResponseInterceptor(interceptor);
+ return this;
+ }
+
+ /**
+ * 执行Reuqest请求
+ *
+ * @return this
+ */
+ public aiyh.utils.tool.cn.hutool.http.HttpResponse execute() {
+ return this.execute(false);
+ }
+
+ /**
+ * 异步请求
+ * 异步请求后获取的{@link aiyh.utils.tool.cn.hutool.http.HttpResponse} 为异步模式,执行完此方法后发送请求到服务器,但是并不立即读取响应内容。
+ * 此时保持Http连接不关闭,直调用获取内容方法为止。
+ *
+ *
+ * 一般执行完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请求后,对响应内容后续处理
+ * 处理结束后关闭连接
+ *
+ * @param consumer 响应内容处理函数
+ * @since 5.7.8
+ */
+ public void then(Consumer consumer) {
+ try (final aiyh.utils.tool.cn.hutool.http.HttpResponse response = execute(true)) {
+ consumer.accept(response);
+ }
+ }
+
+ /**
+ * 执行Request请求后,对响应内容后续处理
+ * 处理结束后关闭连接
+ *
+ * @param 处理结果类型
+ * @param function 响应内容处理函数
+ * @return 处理结果
+ * @since 5.8.5
+ */
+ public T thenFunction(Function function) {
+ try (final aiyh.utils.tool.cn.hutool.http.HttpResponse response = execute(true)) {
+ return function.apply(response);
+ }
+ }
+
+ /**
+ * 简单验证,生成的头信息类似于:
+ *
+ * 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 requestInterceptors,
+ HttpInterceptor.Chain responseInterceptors) {
+ if (null != requestInterceptors) {
+ for (HttpInterceptor interceptor : requestInterceptors) {
+ interceptor.process(this);
+ }
+ }
+
+ // 初始化URL
+ urlWithParamIfGet();
+ // 初始化 connection
+ initConnection();
+ // 发送请求
+ send();
+
+ // 手动实现重定向
+ aiyh.utils.tool.cn.hutool.http.HttpResponse httpResponse = sendRedirectIfPossible(isAsync);
+
+ // 获取响应
+ if (null == httpResponse) {
+ httpResponse = new aiyh.utils.tool.cn.hutool.http.HttpResponse(this.httpConnection, this.config, this.charset, isAsync, isIgnoreResponseBody());
+ }
+
+ // 拦截响应
+ if (null != responseInterceptors) {
+ for (HttpInterceptor interceptor : responseInterceptors) {
+ interceptor.process(httpResponse);
+ }
+ }
+
+ return httpResponse;
+ }
+
+ /**
+ * 初始化网络连接
+ */
+ private void initConnection() {
+ if (null != this.httpConnection) {
+ // 执行下次请求时自动关闭上次请求(常用于转发)
+ this.httpConnection.disconnectQuietly();
+ }
+
+ this.httpConnection = HttpConnection
+ // issue#I50NHQ
+ // 在生成正式URL前,设置自定义编码
+ .create(this.url.setCharset(this.charset).toURL(this.urlHandler), config.proxy)//
+ .setConnectTimeout(config.connectionTimeout)//
+ .setReadTimeout(config.readTimeout)//
+ .setMethod(this.method)//
+ .setHttpsInfo(config.hostnameVerifier, config.ssf)//
+ // 关闭JDK自动转发,采用手动转发方式
+ .setInstanceFollowRedirects(false)
+ // 流方式上传数据
+ .setChunkedStreamingMode(config.blockSize)
+ // 覆盖默认Header
+ .header(this.headers, true);
+
+ if (null != this.cookie) {
+ // 当用户自定义Cookie时,全局Cookie自动失效
+ this.httpConnection.setCookie(this.cookie);
+ } else {
+ // 读取全局Cookie信息并附带到请求中
+ GlobalCookieManager.add(this.httpConnection);
+ }
+
+ // 是否禁用缓存
+ if (config.isDisableCache) {
+ this.httpConnection.disableCache();
+ }
+ }
+
+ /**
+ * 对于GET请求将参数加到URL中
+ * 此处不对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 implements Closeable {
+
+ /**
+ * Http配置
+ */
+ protected HttpConfig config;
+ /**
+ * 持有连接对象
+ */
+ protected HttpConnection httpConnection;
+ /**
+ * Http请求原始流
+ */
+ protected InputStream in;
+ /**
+ * 是否异步,异步下只持有流,否则将在初始化时直接读取body内容
+ */
+ private volatile boolean isAsync;
+ /**
+ * 响应状态码
+ */
+ protected int status;
+ /**
+ * 是否忽略读取Http响应体
+ */
+ private final boolean ignoreBody;
+ /**
+ * 从响应中获取的编码
+ */
+ private Charset charsetFromResponse;
+
+ /**
+ * 构造
+ *
+ * @param httpConnection {@link HttpConnection}
+ * @param config Http配置
+ * @param charset 编码,从请求编码中获取默认编码
+ * @param isAsync 是否异步
+ * @param isIgnoreBody 是否忽略读取响应体
+ * @since 3.1.2
+ */
+ protected HttpResponse(HttpConnection httpConnection, HttpConfig config, Charset charset, boolean isAsync, boolean isIgnoreBody) {
+ this.httpConnection = httpConnection;
+ this.config = config;
+ this.charset = charset;
+ this.isAsync = isAsync;
+ this.ignoreBody = isIgnoreBody;
+ initWithDisconnect();
+ }
+
+ /**
+ * 获取状态码
+ *
+ * @return 状态码
+ */
+ public int getStatus() {
+ return this.status;
+ }
+
+ /**
+ * 请求是否成功,判断依据为:状态码范围在200~299内。
+ *
+ * @return 是否成功请求
+ * @since 4.1.9
+ */
+ public boolean isOk() {
+ return this.status >= 200 && this.status < 300;
+ }
+
+ /**
+ * 同步
+ * 如果为异步状态,则暂时不读取服务器中响应的内容,而是持有Http链接的{@link InputStream}。
+ * 当调用此方法时,异步状态转为同步状态,此时从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);
+ }
+
+ /**
+ * 获取内容长度,以下情况长度无效:
+ *
+ * - Transfer-Encoding: Chunked
+ * - Content-Encoding: XXX
+ *
+ * 参考:https://blog.csdn.net/jiang7701037/article/details/86304302
+ *
+ * @return 长度,-1表示服务端未返回或长度无效
+ * @since 5.7.9
+ */
+ public long contentLength() {
+ long contentLength = Convert.toLong(header(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 getCookies() {
+ return GlobalCookieManager.getCookies(this.httpConnection);
+ }
+
+ /**
+ * 获取Cookie
+ *
+ * @param name Cookie名
+ * @return {@link HttpCookie}
+ * @since 4.1.4
+ */
+ public HttpCookie getCookie(String name) {
+ List cookie = getCookies();
+ if (null != cookie) {
+ for (HttpCookie httpCookie : cookie) {
+ if (httpCookie.getName().equals(name)) {
+ return httpCookie;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * 获取Cookie值
+ *
+ * @param name Cookie名
+ * @return Cookie值
+ * @since 4.1.4
+ */
+ public String getCookieValue(String name) {
+ HttpCookie cookie = getCookie(name);
+ return (null == cookie) ? null : cookie.getValue();
+ }
+ // ---------------------------------------------------------------- Http Response Header end
+
+ // ---------------------------------------------------------------- Body start
+
+ /**
+ * 获得服务区响应流
+ * 异步模式下获取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> entry : this.headers.entrySet()) {
+ sb.append(" ").append(entry).append(StrUtil.CRLF);
+ }
+
+ sb.append("Response Body: ").append(StrUtil.CRLF);
+ sb.append(" ").append(this.body()).append(StrUtil.CRLF);
+
+ return sb.toString();
+ }
+
+ /**
+ * 从响应头补全下载文件名
+ *
+ * @param targetFileOrDir 目标文件夹或者目标文件
+ * @return File 保存的文件
+ * @since 5.4.1
+ */
+ public File completeFileNameFromHeader(File targetFileOrDir) {
+ if (false == targetFileOrDir.isDirectory()) {
+ // 非目录直接返回
+ return targetFileOrDir;
+ }
+
+ // 从头信息中获取文件名
+ String fileName = getFileNameFromDisposition(null);
+ if (StrUtil.isBlank(fileName)) {
+ final String path = httpConnection.getUrl().getPath();
+ // 从路径中获取文件名
+ fileName = StrUtil.subSuf(path, path.lastIndexOf('/') + 1);
+ if (StrUtil.isBlank(fileName)) {
+ // 编码后的路径做为文件名
+ fileName = URLUtil.encodeQuery(path, charset);
+ } else {
+ // issue#I4K0FS@Gitee
+ fileName = URLUtil.decode(fileName, charset);
+ }
+ }
+ return FileUtil.file(targetFileOrDir, fileName);
+ }
+
+ /**
+ * 从Content-Disposition头中获取文件名
+ * @param paramName 文件参数名
+ *
+ * @return 文件名,empty表示无
+ */
+ public String getFileNameFromDisposition(String paramName) {
+ paramName = ObjUtil.defaultIfNull(paramName, "filename");
+ String fileName = null;
+ final String disposition = header(Header.CONTENT_DISPOSITION);
+ if (StrUtil.isNotBlank(disposition)) {
+ fileName = ReUtil.get(paramName+"=\"(.*?)\"", disposition, 1);
+ if (StrUtil.isBlank(fileName)) {
+ fileName = StrUtil.subAfter(disposition, paramName + "=", true);
+ }
+ }
+ return fileName;
+ }
+
+ // ---------------------------------------------------------------- Private method start
+
+ /**
+ * 初始化Http响应,并在报错时关闭连接。
+ * 初始化包括:
+ *
+ *
+ * 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 paramMap) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.get(urlString).form(paramMap).execute().body();
+ }
+
+ /**
+ * 发送get请求
+ *
+ * @param urlString 网址
+ * @param paramMap post表单数据
+ * @param timeout 超时时长,-1表示默认超时,单位毫秒
+ * @return 返回数据
+ * @since 3.3.0
+ */
+ public static String get(String urlString, Map paramMap, int timeout) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().body();
+ }
+
+ /**
+ * 发送post请求
+ *
+ * @param urlString 网址
+ * @param paramMap post表单数据
+ * @return 返回数据
+ */
+ public static String post(String urlString, Map paramMap) {
+ return post(urlString, paramMap, HttpGlobalConfig.getTimeout());
+ }
+
+ /**
+ * 发送post请求
+ *
+ * @param urlString 网址
+ * @param paramMap post表单数据
+ * @param timeout 超时时长,-1表示默认超时,单位毫秒
+ * @return 返回数据
+ * @since 3.2.0
+ */
+ public static String post(String urlString, Map paramMap, int timeout) {
+ return aiyh.utils.tool.cn.hutool.http.HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().body();
+ }
+
+ /**
+ * 发送post请求
+ * 请求体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 paramMap) {
+ return toParams(paramMap, CharsetUtil.CHARSET_UTF_8);
+ }
+
+ /**
+ * 将Map形式的Form表单数据转换为Url参数形式
+ * 编码键和值对
+ *
+ * @param paramMap 表单数据
+ * @param charsetName 编码
+ * @return url参数
+ * @deprecated 请使用 {@link #toParams(Map, Charset)}
+ */
+ @Deprecated
+ public static String toParams(Map paramMap, String charsetName) {
+ return toParams(paramMap, CharsetUtil.charset(charsetName));
+ }
+
+ /**
+ * 将Map形式的Form表单数据转换为Url参数形式
+ * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
+ * 会自动url编码键和值
+ * 此方法用于拼接URL中的Query部分,并不适用于POST请求中的表单
+ *
+ *
+ * key1=v1&key2=&key3=v3
+ *
+ *
+ * @param paramMap 表单数据
+ * @param charset 编码,{@code null} 表示不encode键值对
+ * @return url参数
+ * @see #toParams(Map, Charset, boolean)
+ */
+ public static String toParams(Map paramMap, Charset charset) {
+ return toParams(paramMap, charset, false);
+ }
+
+ /**
+ * 将Map形式的Form表单数据转换为Url参数形式
+ * paramMap中如果key为空(null和"")会被忽略,如果value为null,会被做为空白符("")
+ * 会自动url编码键和值
+ *
+ *
+ * key1=v1&key2=&key3=v3
+ *
+ *
+ * @param paramMap 表单数据
+ * @param charset 编码,null表示不encode键值对
+ * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+'
+ * @return url参数
+ * @since 5.7.16
+ */
+ public static String toParams(Map paramMap, Charset charset, boolean isFormUrlEncoded) {
+ return UrlQuery.of(paramMap, isFormUrlEncoded).build(charset);
+ }
+
+ /**
+ * 对URL参数做编码,只编码键和值
+ * 提供的值可以是url附带参数,但是不能只是url
+ *
+ * 注意,此方法只能标准化整个URL,并不适合于单独编码参数值
+ *
+ * @param urlWithParams url和参数,可以包含url本身,也可以单独参数
+ * @param charset 编码
+ * @return 编码后的url和参数
+ * @since 4.0.1
+ */
+ public static String encodeParams(String urlWithParams, Charset charset) {
+ if (StrUtil.isBlank(urlWithParams)) {
+ return StrUtil.EMPTY;
+ }
+
+ String urlPart = null; // url部分,不包括问号
+ String paramPart; // 参数部分
+ final int pathEndPos = urlWithParams.indexOf('?');
+ if (pathEndPos > -1) {
+ // url + 参数
+ urlPart = StrUtil.subPre(urlWithParams, pathEndPos);
+ paramPart = StrUtil.subSuf(urlWithParams, pathEndPos + 1);
+ if (StrUtil.isBlank(paramPart)) {
+ // 无参数,返回url
+ return urlPart;
+ }
+ } else if (false == StrUtil.contains(urlWithParams, '=')) {
+ // 无参数的URL
+ return urlWithParams;
+ } else {
+ // 无URL的参数
+ paramPart = urlWithParams;
+ }
+
+ paramPart = normalizeParams(paramPart, charset);
+
+ return StrUtil.isBlank(urlPart) ? paramPart : urlPart + "?" + paramPart;
+ }
+
+ /**
+ * 标准化参数字符串,即URL中?后的部分
+ *
+ * 注意,此方法只能标准化整个URL,并不适合于单独编码参数值
+ *
+ * @param paramPart 参数字符串
+ * @param charset 编码
+ * @return 标准化的参数字符串
+ * @since 4.5.2
+ */
+ public static String normalizeParams(String paramPart, Charset charset) {
+ if(StrUtil.isEmpty(paramPart)){
+ return paramPart;
+ }
+ final StrBuilder builder = StrBuilder.create(paramPart.length() + 16);
+ final int len = paramPart.length();
+ String name = null;
+ int pos = 0; // 未处理字符开始位置
+ char c; // 当前字符
+ int i; // 当前字符位置
+ for (i = 0; i < len; i++) {
+ c = paramPart.charAt(i);
+ if (c == '=') { // 键值对的分界点
+ if (null == name) {
+ // 只有=前未定义name时被当作键值分界符,否则做为普通字符
+ name = (pos == i) ? StrUtil.EMPTY : paramPart.substring(pos, i);
+ pos = i + 1;
+ }
+ } else if (c == '&') { // 参数对的分界点
+ if (pos != i) {
+ if (null == name) {
+ // 对于像&a&这类无参数值的字符串,我们将name为a的值设为""
+ name = paramPart.substring(pos, i);
+ builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=');
+ } else {
+ builder.append(RFC3986.QUERY_PARAM_NAME.encode(name, charset)).append('=')
+ .append(RFC3986.QUERY_PARAM_VALUE.encode(paramPart.substring(pos, i), charset)).append('&');
+ }
+ name = null;
+ }
+ pos = i + 1;
+ }
+ }
+
+ // 结尾处理
+ if (null != name) {
+ builder.append(URLUtil.encodeQuery(name, charset)).append('=');
+ }
+ if (pos != i) {
+ if (null == name && pos > 0) {
+ builder.append('=');
+ }
+ builder.append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset));
+ }
+
+ // 以&结尾则去除之
+ int lastIndex = builder.length() - 1;
+ if ('&' == builder.charAt(lastIndex)) {
+ builder.delTo(lastIndex);
+ }
+ return builder.toString();
+ }
+
+ /**
+ * 将URL参数解析为Map(也可以解析Post中的键值对参数)
+ *
+ * @param paramsStr 参数字符串(或者带参数的Path)
+ * @param charset 字符集
+ * @return 参数Map
+ * @since 5.2.6
+ */
+ public static Map decodeParamMap(String paramsStr, Charset charset) {
+ final Map queryMap = UrlQuery.of(paramsStr, charset).getQueryMap();
+ if (MapUtil.isEmpty(queryMap)) {
+ return MapUtil.empty();
+ }
+ return Convert.toMap(String.class, String.class, queryMap);
+ }
+
+ /**
+ * 将URL参数解析为Map(也可以解析Post中的键值对参数)
+ *
+ * @param paramsStr 参数字符串(或者带参数的Path)
+ * @param charset 字符集
+ * @return 参数Map
+ */
+ public static Map> decodeParams(String paramsStr, String charset) {
+ return decodeParams(paramsStr, charset, false);
+ }
+
+ /**
+ * 将URL参数解析为Map(也可以解析Post中的键值对参数)
+ *
+ * @param paramsStr 参数字符串(或者带参数的Path)
+ * @param charset 字符集
+ * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+'
+ * @return 参数Map
+ * @since 5.8.12
+ */
+ public static Map> decodeParams(String paramsStr, String charset, boolean isFormUrlEncoded) {
+ return decodeParams(paramsStr, CharsetUtil.charset(charset), isFormUrlEncoded);
+ }
+
+ /**
+ * 将URL QueryString参数解析为Map
+ *
+ * @param paramsStr 参数字符串(或者带参数的Path)
+ * @param charset 字符集
+ * @return 参数Map
+ * @since 5.2.6
+ */
+ public static Map> decodeParams(String paramsStr, Charset charset) {
+ return decodeParams(paramsStr, charset, false);
+ }
+
+ /**
+ * 将URL参数解析为Map(也可以解析Post中的键值对参数)
+ *
+ * @param paramsStr 参数字符串(或者带参数的Path)
+ * @param charset 字符集
+ * @param isFormUrlEncoded 是否为x-www-form-urlencoded模式,此模式下空格会编码为'+'
+ * @return 参数Map
+ */
+ public static Map> decodeParams(String paramsStr, Charset charset, boolean isFormUrlEncoded) {
+ final Map queryMap =
+ UrlQuery.of(paramsStr, charset, true, isFormUrlEncoded).getQueryMap();
+ if (MapUtil.isEmpty(queryMap)) {
+ return MapUtil.empty();
+ }
+
+ final Map> params = new LinkedHashMap<>();
+ queryMap.forEach((key, value) -> {
+ final List values = params.computeIfAbsent(StrUtil.str(key), k -> new ArrayList<>(1));
+ // 一般是一个参数
+ values.add(StrUtil.str(value));
+ });
+ return params;
+ }
+
+ /**
+ * 将表单数据加到URL中(用于GET表单提交)
+ * 表单的键值对会被url编码,但是url中原参数不会被编码
+ *
+ * @param url URL
+ * @param form 表单数据
+ * @param charset 编码
+ * @param isEncodeParams 是否对键和值做转义处理
+ * @return 合成后的URL
+ */
+ public static String urlWithForm(String url, Map form, Charset charset, boolean isEncodeParams) {
+ if (isEncodeParams && StrUtil.contains(url, '?')) {
+ // 在需要编码的情况下,如果url中已经有部分参数,则编码之
+ url = encodeParams(url, charset);
+ }
+
+ // url和参数是分别编码的
+ return urlWithForm(url, toParams(form, charset), charset, false);
+ }
+
+ /**
+ * 将表单数据字符串加到URL中(用于GET表单提交)
+ *
+ * @param url URL
+ * @param queryString 表单数据字符串
+ * @param charset 编码
+ * @param isEncode 是否对键和值做转义处理
+ * @return 拼接后的字符串
+ */
+ public static String urlWithForm(String url, String queryString, Charset charset, boolean isEncode) {
+ if (StrUtil.isBlank(queryString)) {
+ // 无额外参数
+ if (StrUtil.contains(url, '?')) {
+ // url中包含参数
+ return isEncode ? encodeParams(url, charset) : url;
+ }
+ return url;
+ }
+
+ // 始终有参数
+ final StrBuilder urlBuilder = StrBuilder.create(url.length() + queryString.length() + 16);
+ int qmIndex = url.indexOf('?');
+ if (qmIndex > 0) {
+ // 原URL带参数,则对这部分参数单独编码(如果选项为进行编码)
+ urlBuilder.append(isEncode ? encodeParams(url, charset) : url);
+ if (false == StrUtil.endWith(url, '&')) {
+ // 已经带参数的情况下追加参数
+ urlBuilder.append('&');
+ }
+ } else {
+ // 原url无参数,则不做编码
+ urlBuilder.append(url);
+ if (qmIndex < 0) {
+ // 无 '?' 追加之
+ urlBuilder.append('?');
+ }
+ }
+ urlBuilder.append(isEncode ? encodeParams(queryString, charset) : queryString);
+ return urlBuilder.toString();
+ }
+
+ /**
+ * 从Http连接的头信息中获得字符集
+ * 从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 form, Charset charset) {
+ return new FormUrlEncodedBody(form, charset);
+ }
+
+ /**
+ * 构造
+ *
+ * @param form 表单
+ * @param charset 编码
+ */
+ public FormUrlEncodedBody(Map form, Charset charset) {
+ super(StrUtil.bytes(UrlQuery.of(form, true).build(charset), charset));
+ }
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/body/MultipartBody.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/MultipartBody.java
new file mode 100644
index 0000000..a03942d
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/MultipartBody.java
@@ -0,0 +1,89 @@
+package aiyh.utils.tool.cn.hutool.http.body;
+
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+import aiyh.utils.tool.cn.hutool.core.map.MapUtil;
+import aiyh.utils.tool.cn.hutool.http.ContentType;
+import aiyh.utils.tool.cn.hutool.http.HttpGlobalConfig;
+import aiyh.utils.tool.cn.hutool.http.MultipartOutputStream;
+
+import java.io.ByteArrayOutputStream;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.Map;
+
+/**
+ * Multipart/form-data数据的请求体封装
+ * 遵循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 form;
+ /**
+ * 编码
+ */
+ private final Charset charset;
+ /**
+ * 边界
+ */
+ private final String boundary = HttpGlobalConfig.getBoundary();
+
+ /**
+ * 根据已有表单内容,构建MultipartBody
+ *
+ * @param form 表单
+ * @param charset 编码
+ * @return MultipartBody
+ */
+ public static MultipartBody create(Map form, Charset charset) {
+ return new MultipartBody(form, charset);
+ }
+
+ /**
+ * 获取Multipart的Content-Type类型
+ *
+ * @return Multipart的Content-Type类型
+ */
+ public String getContentType() {
+ return CONTENT_TYPE_MULTIPART_PREFIX + boundary;
+ }
+
+ /**
+ * 构造
+ *
+ * @param form 表单
+ * @param charset 编码
+ */
+ public MultipartBody(Map form, Charset charset) {
+ this.form = form;
+ this.charset = charset;
+ }
+
+ /**
+ * 写出Multiparty数据,不关闭流
+ *
+ * @param out out流
+ */
+ @Override
+ public void write(OutputStream out) {
+ final MultipartOutputStream stream = new MultipartOutputStream(out, this.charset, this.boundary);
+ if (MapUtil.isNotEmpty(this.form)) {
+ this.form.forEach(stream::write);
+ }
+ stream.finish();
+ }
+
+ @Override
+ public String toString() {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ write(out);
+ return IoUtil.toStr(out, this.charset);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/body/RequestBody.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/RequestBody.java
new file mode 100644
index 0000000..a3a9709
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/RequestBody.java
@@ -0,0 +1,32 @@
+package aiyh.utils.tool.cn.hutool.http.body;
+
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+
+import java.io.OutputStream;
+
+/**
+ * 定义请求体接口
+ */
+public interface RequestBody {
+
+ /**
+ * 写出数据,不关闭流
+ *
+ * @param out out流
+ */
+ void write(OutputStream out);
+
+ /**
+ * 写出并关闭{@link OutputStream}
+ *
+ * @param out {@link OutputStream}
+ * @since 5.7.17
+ */
+ default void writeClose(OutputStream out) {
+ try {
+ write(out);
+ } finally {
+ IoUtil.close(out);
+ }
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/body/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/package-info.java
new file mode 100644
index 0000000..92f47e0
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/body/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 请求体封装实现
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http.body;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/GlobalCookieManager.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/GlobalCookieManager.java
new file mode 100644
index 0000000..f0e0c9a
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/GlobalCookieManager.java
@@ -0,0 +1,109 @@
+package aiyh.utils.tool.cn.hutool.http.cookie;
+
+import aiyh.utils.tool.cn.hutool.core.io.IORuntimeException;
+import aiyh.utils.tool.cn.hutool.core.util.URLUtil;
+import aiyh.utils.tool.cn.hutool.http.HttpConnection;
+
+import java.io.IOException;
+import java.net.CookieManager;
+import java.net.CookiePolicy;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 全局Cookie管理器,只针对Hutool请求有效
+ *
+ * @author Looly
+ * @since 4.5.15
+ */
+public class GlobalCookieManager {
+
+ /** Cookie管理 */
+ private static CookieManager cookieManager;
+
+ static {
+ cookieManager = new CookieManager(new ThreadLocalCookieStore(), CookiePolicy.ACCEPT_ALL);
+ }
+
+ /**
+ * 自定义{@link CookieManager}
+ *
+ * @param customCookieManager 自定义的{@link CookieManager}
+ */
+ public static void setCookieManager(CookieManager customCookieManager) {
+ cookieManager = customCookieManager;
+ }
+
+ /**
+ * 获取全局{@link CookieManager}
+ *
+ * @return {@link CookieManager}
+ */
+ public static CookieManager getCookieManager() {
+ return cookieManager;
+ }
+
+ /**
+ * 获取指定域名下所有Cookie信息
+ *
+ * @param conn HTTP连接
+ * @return Cookie信息列表
+ * @since 4.6.9
+ */
+ public static List getCookies(aiyh.utils.tool.cn.hutool.http.HttpConnection conn) {
+ return cookieManager.getCookieStore().get(getURI(conn));
+ }
+
+ /**
+ * 将本地存储的Cookie信息附带到Http请求中,不覆盖用户定义好的Cookie
+ *
+ * @param conn {@link aiyh.utils.tool.cn.hutool.http.HttpConnection}
+ */
+ public static void add(aiyh.utils.tool.cn.hutool.http.HttpConnection conn) {
+ if (null == cookieManager) {
+ // 全局Cookie管理器关闭
+ return;
+ }
+
+ Map> cookieHeader;
+ try {
+ cookieHeader = cookieManager.get(getURI(conn), new HashMap<>(0));
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+
+ // 不覆盖模式回填Cookie头,这样用户定义的Cookie将优先
+ conn.header(cookieHeader, false);
+ }
+
+ /**
+ * 存储响应的Cookie信息到本地
+ *
+ * @param conn {@link aiyh.utils.tool.cn.hutool.http.HttpConnection}
+ */
+ public static void store(aiyh.utils.tool.cn.hutool.http.HttpConnection conn) {
+ if (null == cookieManager) {
+ // 全局Cookie管理器关闭
+ return;
+ }
+
+ try {
+ cookieManager.put(getURI(conn), conn.headers());
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ }
+
+ /**
+ * 获取连接的URL中URI信息
+ *
+ * @param conn HttpConnection
+ * @return URI
+ */
+ private static URI getURI(HttpConnection conn) {
+ return URLUtil.toURI(conn.getUrl());
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/ThreadLocalCookieStore.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/ThreadLocalCookieStore.java
new file mode 100644
index 0000000..79a3af7
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/ThreadLocalCookieStore.java
@@ -0,0 +1,75 @@
+package aiyh.utils.tool.cn.hutool.http.cookie;
+
+import java.net.CookieManager;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.util.List;
+
+/**
+ * 线程隔离的Cookie存储。多线程环境下Cookie隔离使用,防止Cookie覆盖
+ *
+ * 见:https://stackoverflow.com/questions/16305486/cookiemanager-for-multiple-threads
+ *
+ * @author looly
+ * @since 4.1.18
+ */
+public class ThreadLocalCookieStore implements CookieStore {
+
+ private final static ThreadLocal STORES = new ThreadLocal() {
+ @Override
+ protected synchronized CookieStore initialValue() {
+ /* InMemoryCookieStore */
+ return (new CookieManager()).getCookieStore();
+ }
+ };
+
+ /**
+ * 获取本线程下的CookieStore
+ *
+ * @return CookieStore
+ */
+ public CookieStore getCookieStore() {
+ return STORES.get();
+ }
+
+ /**
+ * 移除当前线程的Cookie
+ *
+ * @return this
+ */
+ public ThreadLocalCookieStore removeCurrent() {
+ STORES.remove();
+ return this;
+ }
+
+ @Override
+ public void add(URI uri, HttpCookie cookie) {
+ getCookieStore().add(uri, cookie);
+ }
+
+ @Override
+ public List get(URI uri) {
+ return getCookieStore().get(uri);
+ }
+
+ @Override
+ public List getCookies() {
+ return getCookieStore().getCookies();
+ }
+
+ @Override
+ public List getURIs() {
+ return getCookieStore().getURIs();
+ }
+
+ @Override
+ public boolean remove(URI uri, HttpCookie cookie) {
+ return getCookieStore().remove(uri, cookie);
+ }
+
+ @Override
+ public boolean removeAll() {
+ return getCookieStore().removeAll();
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/package-info.java
new file mode 100644
index 0000000..38eaed5
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/cookie/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * 自定义Cookie
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http.cookie;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/package-info.java
new file mode 100644
index 0000000..7140c78
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Hutool-http针对JDK的HttpUrlConnection做一层封装,简化了HTTPS请求、文件上传、Cookie记忆等操作,使Http请求变得无比简单。
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerBase.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerBase.java
new file mode 100644
index 0000000..b5cd882
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerBase.java
@@ -0,0 +1,57 @@
+package aiyh.utils.tool.cn.hutool.http.server;
+
+import aiyh.utils.tool.cn.hutool.core.util.CharsetUtil;
+import com.sun.net.httpserver.HttpContext;
+import com.sun.net.httpserver.HttpExchange;
+
+import java.io.Closeable;
+import java.nio.charset.Charset;
+
+/**
+ * HttpServer公用对象,提供HttpExchange包装和公用方法
+ *
+ * @author looly
+ * @since 5.2.6
+ */
+public class HttpServerBase implements Closeable {
+
+ final static Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;
+
+ final HttpExchange httpExchange;
+
+ /**
+ * 构造
+ *
+ * @param httpExchange {@link HttpExchange}
+ */
+ public HttpServerBase(HttpExchange httpExchange) {
+ this.httpExchange = httpExchange;
+ }
+
+ /**
+ * 获取{@link HttpExchange}对象
+ *
+ * @return {@link HttpExchange}对象
+ */
+ public HttpExchange getHttpExchange() {
+ return this.httpExchange;
+ }
+
+ /**
+ * 获取{@link HttpContext}
+ *
+ * @return {@link HttpContext}
+ * @since 5.5.7
+ */
+ public HttpContext getHttpContext() {
+ return getHttpExchange().getHttpContext();
+ }
+
+ /**
+ * 调用{@link HttpExchange#close()},关闭请求流和响应流
+ */
+ @Override
+ public void close() {
+ this.httpExchange.close();
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerRequest.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerRequest.java
new file mode 100644
index 0000000..4f262cf
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/HttpServerRequest.java
@@ -0,0 +1,442 @@
+package aiyh.utils.tool.cn.hutool.http.server;
+
+import aiyh.utils.tool.cn.hutool.core.collection.CollUtil;
+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.map.CaseInsensitiveMap;
+import aiyh.utils.tool.cn.hutool.core.map.multi.ListValueMap;
+import aiyh.utils.tool.cn.hutool.core.net.NetUtil;
+import aiyh.utils.tool.cn.hutool.core.net.multipart.MultipartFormData;
+import aiyh.utils.tool.cn.hutool.core.net.multipart.UploadSetting;
+import aiyh.utils.tool.cn.hutool.core.util.ArrayUtil;
+import aiyh.utils.tool.cn.hutool.core.util.CharsetUtil;
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+import aiyh.utils.tool.cn.hutool.http.Header;
+import aiyh.utils.tool.cn.hutool.http.HttpUtil;
+import aiyh.utils.tool.cn.hutool.http.Method;
+import aiyh.utils.tool.cn.hutool.http.useragent.UserAgent;
+import aiyh.utils.tool.cn.hutool.http.useragent.UserAgentUtil;
+import com.sun.net.httpserver.Headers;
+import com.sun.net.httpserver.HttpExchange;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpCookie;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Http请求对象,对{@link HttpExchange}封装
+ *
+ * @author looly
+ * @since 5.2.6
+ */
+public class HttpServerRequest extends HttpServerBase {
+
+ private Map cookieCache;
+ private ListValueMap paramsCache;
+ private MultipartFormData multipartFormDataCache;
+ private Charset charsetCache;
+ private byte[] bodyCache;
+
+ /**
+ * 构造
+ *
+ * @param httpExchange {@link HttpExchange}
+ */
+ public HttpServerRequest(HttpExchange httpExchange) {
+ super(httpExchange);
+ }
+
+ /**
+ * 获得Http Method
+ *
+ * @return Http Method
+ */
+ public String getMethod() {
+ return this.httpExchange.getRequestMethod();
+ }
+
+ /**
+ * 是否为GET请求
+ *
+ * @return 是否为GET请求
+ */
+ public boolean isGetMethod() {
+ return aiyh.utils.tool.cn.hutool.http.Method.GET.name().equalsIgnoreCase(getMethod());
+ }
+
+ /**
+ * 是否为POST请求
+ *
+ * @return 是否为POST请求
+ */
+ public boolean isPostMethod() {
+ return Method.POST.name().equalsIgnoreCase(getMethod());
+ }
+
+ /**
+ * 获得请求URI
+ *
+ * @return 请求URI
+ */
+ public URI getURI() {
+ return this.httpExchange.getRequestURI();
+ }
+
+ /**
+ * 获得请求路径Path
+ *
+ * @return 请求路径
+ */
+ public String getPath() {
+ return getURI().getPath();
+ }
+
+ /**
+ * 获取请求参数
+ *
+ * @return 参数字符串
+ */
+ public String getQuery() {
+ return getURI().getQuery();
+ }
+
+ /**
+ * 获得请求header中的信息
+ *
+ * @return header值
+ */
+ public Headers getHeaders() {
+ return this.httpExchange.getRequestHeaders();
+ }
+
+ /**
+ * 获得请求header中的信息
+ *
+ * @param headerKey 头信息的KEY
+ * @return header值
+ */
+ public String getHeader(aiyh.utils.tool.cn.hutool.http.Header headerKey) {
+ return getHeader(headerKey.toString());
+ }
+
+ /**
+ * 获得请求header中的信息
+ *
+ * @param headerKey 头信息的KEY
+ * @return header值
+ */
+ public String getHeader(String headerKey) {
+ return getHeaders().getFirst(headerKey);
+ }
+
+ /**
+ * 获得请求header中的信息
+ *
+ * @param headerKey 头信息的KEY
+ * @param charset 字符集
+ * @return header值
+ */
+ public String getHeader(String headerKey, Charset charset) {
+ final String header = getHeader(headerKey);
+ if (null != header) {
+ return CharsetUtil.convert(header, CharsetUtil.CHARSET_ISO_8859_1, charset);
+ }
+ return null;
+ }
+
+ /**
+ * 获取Content-Type头信息
+ *
+ * @return Content-Type头信息
+ */
+ public String getContentType() {
+ return getHeader(aiyh.utils.tool.cn.hutool.http.Header.CONTENT_TYPE);
+ }
+
+ /**
+ * 获取编码,获取失败默认使用UTF-8,获取规则如下:
+ *
+ *
+ * 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 getCookies() {
+ return getCookieMap().values();
+ }
+
+ /**
+ * 获得Cookie信息Map,键为Cookie名,值为HttpCookie对象
+ *
+ * @return Cookie信息Map
+ */
+ public Map getCookieMap() {
+ if (null == this.cookieCache) {
+ cookieCache = Collections.unmodifiableMap(CollUtil.toMap(
+ NetUtil.parseCookies(getCookiesStr()),
+ new CaseInsensitiveMap<>(),
+ HttpCookie::getName));
+ }
+ return cookieCache;
+ }
+
+ /**
+ * 获得指定Cookie名对应的HttpCookie对象
+ *
+ * @param cookieName Cookie名
+ * @return HttpCookie对象
+ */
+ public HttpCookie getCookie(String cookieName) {
+ return getCookieMap().get(cookieName);
+ }
+
+ /**
+ * 是否为Multipart类型表单,此类型表单用于文件上传
+ *
+ * @return 是否为Multipart类型表单,此类型表单用于文件上传
+ */
+ public boolean isMultipart() {
+ if (!isPostMethod()) {
+ return false;
+ }
+
+ final String contentType = getContentType();
+ if (StrUtil.isBlank(contentType)) {
+ return false;
+ }
+ return contentType.toLowerCase().startsWith("multipart/");
+ }
+
+ /**
+ * 获取请求体文本,可以是form表单、json、xml等任意内容
+ * 使用{@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 getParams(String name) {
+ return getParams().get(name);
+ }
+
+ /**
+ * 获取参数Map
+ *
+ * @return 参数map
+ */
+ public ListValueMap getParams() {
+ if (null == this.paramsCache) {
+ this.paramsCache = new ListValueMap<>();
+ final Charset charset = getCharset();
+
+ // 解析URL中的参数
+ final String query = getQuery();
+ if (StrUtil.isNotBlank(query)) {
+ this.paramsCache.putAll(aiyh.utils.tool.cn.hutool.http.HttpUtil.decodeParams(query, charset, false));
+ }
+
+ // 解析multipart中的参数
+ if (isMultipart()) {
+ this.paramsCache.putAll(getMultipart().getParamListMap());
+ } else {
+ // 解析body中的参数
+ final String body = getBody();
+ if (StrUtil.isNotBlank(body)) {
+ this.paramsCache.putAll(HttpUtil.decodeParams(body, charset, true));
+ }
+ }
+ }
+
+ return this.paramsCache;
+ }
+
+ /**
+ * 获取客户端IP
+ *
+ *
+ * 默认检测的Header:
+ *
+ *
+ * 1、X-Forwarded-For
+ * 2、X-Real-IP
+ * 3、Proxy-Client-IP
+ * 4、WL-Proxy-Client-IP
+ *
+ *
+ *
+ * otherHeaderNames参数用于自定义检测的Header
+ * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。
+ *
+ *
+ * @param otherHeaderNames 其他自定义头文件,通常在Http服务器(例如Nginx)中配置
+ * @return IP地址
+ */
+ public String getClientIP(String... otherHeaderNames) {
+ String[] headers = {"X-Forwarded-For", "X-Real-IP", "Proxy-Client-IP", "WL-Proxy-Client-IP", "HTTP_CLIENT_IP", "HTTP_X_FORWARDED_FOR"};
+ if (ArrayUtil.isNotEmpty(otherHeaderNames)) {
+ headers = ArrayUtil.addAll(headers, otherHeaderNames);
+ }
+
+ return getClientIPByHeader(headers);
+ }
+
+ /**
+ * 获取客户端IP
+ *
+ *
+ * headerNames参数用于自定义检测的Header
+ * 需要注意的是,使用此方法获取的客户IP地址必须在Http服务器(例如Nginx)中配置头信息,否则容易造成IP伪造。
+ *
+ *
+ * @param headerNames 自定义头,通常在Http服务器(例如Nginx)中配置
+ * @return IP地址
+ * @since 4.4.1
+ */
+ public String getClientIPByHeader(String... headerNames) {
+ String ip;
+ for (String header : headerNames) {
+ ip = getHeader(header);
+ if (!NetUtil.isUnknown(ip)) {
+ return NetUtil.getMultistageReverseProxyIp(ip);
+ }
+ }
+
+ ip = this.httpExchange.getRemoteAddress().getHostName();
+ return NetUtil.getMultistageReverseProxyIp(ip);
+ }
+
+ /**
+ * 获得MultiPart表单内容,多用于获得上传的文件
+ *
+ * @return MultipartFormData
+ * @throws IORuntimeException IO异常
+ * @since 5.3.0
+ */
+ public MultipartFormData getMultipart() throws IORuntimeException {
+ if (null == this.multipartFormDataCache) {
+ this.multipartFormDataCache = parseMultipart(new UploadSetting());
+ }
+ return this.multipartFormDataCache;
+ }
+
+ /**
+ * 获得multipart/form-data 表单内容
+ * 包括文件和普通表单数据
+ * 在同一次请求中,此方法只能被执行一次!
+ *
+ * @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 value) {
+ getHeaders().put(header, value);
+ return this;
+ }
+
+ /**
+ * 设置所有响应头,如果已经存在,则覆盖
+ *
+ * @param headers 响应头map
+ * @return this
+ */
+ public HttpServerResponse setHeaders(Map> headers) {
+ getHeaders().putAll(headers);
+ return this;
+ }
+
+ /**
+ * 设置Content-Type头,类似于:text/html;charset=utf-8
+ * 如果用户传入的信息无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 filters;
+
+ /**
+ * 构造
+ *
+ * @param port 监听端口
+ */
+ public SimpleServer(int port) {
+ this(new InetSocketAddress(port));
+ }
+
+ /**
+ * 构造
+ *
+ * @param hostname 监听地址
+ * @param port 监听端口
+ */
+ public SimpleServer(String hostname, int port) {
+ this(new InetSocketAddress(hostname, port));
+ }
+
+ /**
+ * 构造
+ *
+ * @param address 监听地址
+ */
+ public SimpleServer(InetSocketAddress address) {
+ this(address, null);
+ }
+
+ /**
+ * 构造
+ *
+ * @param address 监听地址
+ * @param configurator https配置信息,用于使用自定义SSL(TLS)证书等
+ */
+ public SimpleServer(InetSocketAddress address, HttpsConfigurator configurator) {
+ try {
+ if (null != configurator) {
+ final HttpsServer server = HttpsServer.create(address, 0);
+ server.setHttpsConfigurator(configurator);
+ this.server = server;
+ } else {
+ this.server = HttpServer.create(address, 0);
+ }
+ } catch (IOException e) {
+ throw new IORuntimeException(e);
+ }
+ setExecutor(GlobalThreadPool.getExecutor());
+ filters = new ArrayList<>();
+ }
+
+ /**
+ * 增加请求过滤器,此过滤器对所有请求有效
+ * 此方法需在以下方法前之前调用:
+ *
+ *
+ * - {@link #setRoot(File)}
+ * - {@link #setRoot(String)}
+ * - {@link #createContext(String, HttpHandler)}
+ * - {@link #addHandler(String, HttpHandler)}
+ * - {@link #addAction(String, Action)}
+ *
+ *
+ * @param filter {@link Filter} 请求过滤器
+ * @return this
+ * @since 5.5.7
+ */
+ public SimpleServer addFilter(Filter filter) {
+ this.filters.add(filter);
+ return this;
+ }
+
+ /**
+ * 增加请求过滤器,此过滤器对所有请求有效
+ * 此方法需在以下方法前之前调用:
+ *
+ *
+ * - {@link #setRoot(File)}
+ * - {@link #setRoot(String)}
+ * - {@link #createContext(String, HttpHandler)}
+ * - {@link #addHandler(String, HttpHandler)}
+ * - {@link #addAction(String, Action)}
+ *
+ *
+ * @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 indexFileNames;
+
+ /**
+ * 构造
+ *
+ * @param rootDir 网页根目录
+ */
+ public RootAction(String rootDir) {
+ this(new File(rootDir));
+ }
+
+ /**
+ * 构造
+ *
+ * @param rootDir 网页根目录
+ */
+ public RootAction(File rootDir) {
+ this(rootDir, DEFAULT_INDEX_FILE_NAME);
+ }
+
+ /**
+ * 构造
+ *
+ * @param rootDir 网页根目录
+ * @param indexFileNames 主页文件名列表
+ */
+ public RootAction(String rootDir, String... indexFileNames) {
+ this(new File(rootDir), indexFileNames);
+ }
+
+ /**
+ * 构造
+ *
+ * @param rootDir 网页根目录
+ * @param indexFileNames 主页文件名列表
+ * @since 5.4.0
+ */
+ public RootAction(File rootDir, String... indexFileNames) {
+ this.rootDir = rootDir;
+ this.indexFileNames = CollUtil.toList(indexFileNames);
+ }
+
+ @Override
+ public void doAction(HttpServerRequest request, HttpServerResponse response) {
+ final String path = request.getPath();
+
+ File file = FileUtil.file(rootDir, path);
+ if (file.exists()) {
+ if (file.isDirectory()) {
+ for (String indexFileName : indexFileNames) {
+ // 默认读取主页
+ file = FileUtil.file(file, indexFileName);
+ if (file.exists() && file.isFile()) {
+ response.write(file);
+ }
+ }
+ } else {
+ final String name = request.getParam("name");
+ response.write(file, name);
+ }
+ }
+
+ response.send404("404 Not Found !");
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/package-info.java
new file mode 100644
index 0000000..ebb51f2
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/action/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * {@link com.sun.net.httpserver.HttpServer} 封装
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http.server.action;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/HttpFilter.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/HttpFilter.java
new file mode 100644
index 0000000..9c291fc
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/HttpFilter.java
@@ -0,0 +1,27 @@
+package aiyh.utils.tool.cn.hutool.http.server.filter;
+
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerRequest;
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerResponse;
+import com.sun.net.httpserver.Filter;
+
+import java.io.IOException;
+
+/**
+ * 过滤器接口,用于简化{@link Filter} 使用
+ *
+ * @author looly
+ * @since 5.5.7
+ */
+@FunctionalInterface
+public interface HttpFilter {
+
+ /**
+ * 执行过滤
+ *
+ * @param req {@link aiyh.utils.tool.cn.hutool.http.server.HttpServerRequest} 请求对象,用于获取请求内容
+ * @param res {@link aiyh.utils.tool.cn.hutool.http.server.HttpServerResponse} 响应对象,用于写出内容
+ * @param chain {@link Filter.Chain}
+ * @throws IOException IO异常
+ */
+ void doFilter(HttpServerRequest req, HttpServerResponse res, Filter.Chain chain) throws IOException;
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/SimpleFilter.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/SimpleFilter.java
new file mode 100644
index 0000000..0ab9e7f
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/SimpleFilter.java
@@ -0,0 +1,17 @@
+package aiyh.utils.tool.cn.hutool.http.server.filter;
+
+import com.sun.net.httpserver.Filter;
+
+/**
+ * 匿名简单过滤器,跳过了描述
+ *
+ * @author looly
+ * @since 5.5.7
+ */
+public abstract class SimpleFilter extends Filter {
+
+ @Override
+ public String description() {
+ return "Anonymous Filter";
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/package-info.java
new file mode 100644
index 0000000..91fdbb8
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/filter/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * {@link com.sun.net.httpserver.Filter} 实现包装
+ */
+package aiyh.utils.tool.cn.hutool.http.server.filter;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/handler/ActionHandler.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/handler/ActionHandler.java
new file mode 100644
index 0000000..17c0c6f
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/handler/ActionHandler.java
@@ -0,0 +1,38 @@
+package aiyh.utils.tool.cn.hutool.http.server.handler;
+
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerRequest;
+import aiyh.utils.tool.cn.hutool.http.server.HttpServerResponse;
+import aiyh.utils.tool.cn.hutool.http.server.action.Action;
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+import java.io.IOException;
+
+/**
+ * Action处理器,用于将HttpHandler转换为Action形式
+ *
+ * @author looly
+ * @since 5.2.6
+ */
+public class ActionHandler implements HttpHandler {
+
+ private final Action action;
+
+ /**
+ * 构造
+ *
+ * @param action Action
+ */
+ public ActionHandler(Action action) {
+ this.action = action;
+ }
+
+ @Override
+ public void handle(HttpExchange httpExchange) throws IOException {
+ action.doAction(
+ new HttpServerRequest(httpExchange),
+ new HttpServerResponse(httpExchange)
+ );
+ httpExchange.close();
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/handler/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/handler/package-info.java
new file mode 100644
index 0000000..c4113d6
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/handler/package-info.java
@@ -0,0 +1,4 @@
+/**
+ * {@link com.sun.net.httpserver.HttpHandler} 实现包装
+ */
+package aiyh.utils.tool.cn.hutool.http.server.handler;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/server/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/package-info.java
new file mode 100644
index 0000000..47d979f
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/server/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Http服务器封装
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http.server;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/AndroidSupportSSLFactory.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/AndroidSupportSSLFactory.java
new file mode 100644
index 0000000..ceb0805
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/ssl/AndroidSupportSSLFactory.java
@@ -0,0 +1,25 @@
+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.SSLProtocols;
+
+/**
+ * 兼容android低版本SSL连接
+ * 在测试HttpUrlConnection的时候,发现一部分手机无法连接[GithubPage]
+ *
+ *
+ * 最后发现原来是某些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 browers = CollUtil.newArrayList(
+ // 部分特殊浏览器是基于安卓、Iphone等的,需要优先判断
+ // 企业微信 企业微信使用微信浏览器内核,会包含 MicroMessenger 所以要放在前面
+ new Browser("wxwork", "wxwork", "wxwork\\/([\\d\\w\\.\\-]+)"),
+ // 微信
+ new Browser("MicroMessenger", "MicroMessenger", Other_Version),
+ // 微信小程序
+ new Browser("miniProgram", "miniProgram", Other_Version),
+ // QQ浏览器
+ new Browser("QQBrowser", "MQQBrowser", "MQQBrowser\\/([\\d\\w\\.\\-]+)"),
+ // 钉钉PC端浏览器
+ new Browser("DingTalk-win", "dingtalk-win", "DingTalk\\(([\\d\\w\\.\\-]+)\\)"),
+ // 钉钉内置浏览器
+ new Browser("DingTalk", "DingTalk", "AliApp\\(DingTalk\\/([\\d\\w\\.\\-]+)\\)"),
+ // 支付宝内置浏览器
+ new Browser("Alipay", "AlipayClient", "AliApp\\(AP\\/([\\d\\w\\.\\-]+)\\)"),
+ // 淘宝内置浏览器
+ new Browser("Taobao", "taobao", "AliApp\\(TB\\/([\\d\\w\\.\\-]+)\\)"),
+ // UC浏览器
+ new Browser("UCBrowser", "UC?Browser", "UC?Browser\\/([\\d\\w\\.\\-]+)"),
+ // XiaoMi 浏览器
+ new Browser("MiuiBrowser", "MiuiBrowser|mibrowser", "MiuiBrowser\\/([\\d\\w\\.\\-]+)"),
+ // 夸克浏览器
+ new Browser("Quark", "Quark", Other_Version),
+ // 联想浏览器
+ new Browser("Lenovo", "SLBrowser", "SLBrowser/([\\d\\w\\.\\-]+)"),
+ new Browser("MSEdge", "Edge|Edg", "(?:edge|Edg|EdgA)\\/([\\d\\w\\.\\-]+)"),
+ new Browser("Chrome", "chrome", Other_Version),
+ new Browser("Firefox", "firefox", Other_Version),
+ new Browser("IEMobile", "iemobile", Other_Version),
+ new Browser("Android Browser", "android", "version\\/([\\d\\w\\.\\-]+)"),
+ new Browser("Safari", "safari", "version\\/([\\d\\w\\.\\-]+)"),
+ new Browser("Opera", "opera", Other_Version),
+ new Browser("Konqueror", "konqueror", Other_Version),
+ new Browser("PS3", "playstation 3", "([\\d\\w\\.\\-]+)\\)\\s*$"),
+ new Browser("PSP", "playstation portable", "([\\d\\w\\.\\-]+)\\)?\\s*$"),
+ new Browser("Lotus", "lotus.notes", "Lotus-Notes\\/([\\w.]+)"),
+ new Browser("Thunderbird", "thunderbird", Other_Version),
+ new Browser("Netscape", "netscape", Other_Version),
+ new Browser("Seamonkey", "seamonkey", Other_Version),
+ new Browser("Outlook", "microsoft.outlook", Other_Version),
+ new Browser("Evolution", "evolution", Other_Version),
+ new Browser("MSIE", "msie", "msie ([\\d\\w\\.\\-]+)"),
+ new Browser("MSIE11", "rv:11", "rv:([\\d\\w\\.\\-]+)"),
+ new Browser("Gabble", "Gabble", Other_Version),
+ new Browser("Yammer Desktop", "AdobeAir", "([\\d\\w\\.\\-]+)\\/Yammer"),
+ new Browser("Yammer Mobile", "Yammer[\\s]+([\\d\\w\\.\\-]+)", "Yammer[\\s]+([\\d\\w\\.\\-]+)"),
+ new Browser("Apache HTTP Client", "Apache\\\\-HttpClient", "Apache\\-HttpClient\\/([\\d\\w\\.\\-]+)"),
+ new Browser("BlackBerry", "BlackBerry", "BlackBerry[\\d]+\\/([\\d\\w\\.\\-]+)")
+ );
+
+ /**
+ * 添加自定义的浏览器类型
+ *
+ * @param name 浏览器名称
+ * @param regex 关键字或表达式
+ * @param versionRegex 匹配版本的正则
+ * @since 5.7.4
+ */
+ synchronized public static void addCustomBrowser(String name, String regex, String versionRegex) {
+ browers.add(new Browser(name, regex, versionRegex));
+ }
+
+ private Pattern versionPattern;
+
+ /**
+ * 构造
+ *
+ * @param name 浏览器名称
+ * @param regex 关键字或表达式
+ * @param versionRegex 匹配版本的正则
+ */
+ public Browser(String name, String regex, String versionRegex) {
+ super(name, regex);
+ if (Other_Version.equals(versionRegex)) {
+ versionRegex = name + versionRegex;
+ }
+ if (null != versionRegex) {
+ this.versionPattern = Pattern.compile(versionRegex, Pattern.CASE_INSENSITIVE);
+ }
+ }
+
+ /**
+ * 获取浏览器版本
+ *
+ * @param userAgentString User-Agent字符串
+ * @return 版本
+ */
+ public String getVersion(String userAgentString) {
+ if (isUnknown()) {
+ return null;
+ }
+ return ReUtil.getGroup1(this.versionPattern, userAgentString);
+ }
+
+ /**
+ * 是否移动浏览器
+ *
+ * @return 是否移动浏览器
+ */
+ public boolean isMobile() {
+ final String name = this.getName();
+ return "PSP".equals(name) ||
+ "Yammer Mobile".equals(name) ||
+ "Android Browser".equals(name) ||
+ "IEMobile".equals(name) ||
+ "MicroMessenger".equals(name) ||
+ "miniProgram".equals(name) ||
+ "DingTalk".equals(name);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Engine.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Engine.java
new file mode 100755
index 0000000..c424852
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Engine.java
@@ -0,0 +1,62 @@
+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 Engine extends UserAgentInfo {
+ private static final long serialVersionUID = 1L;
+
+ /** 未知 */
+ public static final Engine Unknown = new Engine(NameUnknown, null);
+
+ /**
+ * 支持的引擎类型
+ */
+ public static final List engines = CollUtil.newArrayList(
+ new Engine("Trident", "trident"),
+ new Engine("Webkit", "webkit"),
+ new Engine("Chrome", "chrome"),
+ new Engine("Opera", "opera"),
+ new Engine("Presto", "presto"),
+ new Engine("Gecko", "gecko"),
+ new Engine("KHTML", "khtml"),
+ new Engine("Konqueror", "konqueror"),
+ new Engine("MIDP", "MIDP")
+ );
+
+ private final Pattern versionPattern;
+
+ /**
+ * 构造
+ *
+ * @param name 引擎名称
+ * @param regex 关键字或表达式
+ */
+ public Engine(String name, String regex) {
+ super(name, regex);
+ this.versionPattern = Pattern.compile(name + "[/\\- ]([\\d\\w.\\-]+)", Pattern.CASE_INSENSITIVE);
+ }
+
+ /**
+ * 获取引擎版本
+ *
+ * @param userAgentString User-Agent字符串
+ * @return 版本
+ * @since 5.7.4
+ */
+ public String getVersion(String userAgentString) {
+ if (isUnknown()) {
+ return null;
+ }
+ return ReUtil.getGroup1(this.versionPattern, userAgentString);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/OS.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/OS.java
new file mode 100755
index 0000000..8e7c753
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/OS.java
@@ -0,0 +1,107 @@
+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 OS extends UserAgentInfo {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 未知
+ */
+ public static final OS Unknown = new OS(NameUnknown, null);
+
+ /**
+ * 支持的引擎类型
+ */
+ public static final List oses = CollUtil.newArrayList(//
+ new OS("Windows 10 or Windows Server 2016", "windows nt 10\\.0", "windows nt (10\\.0)"),//
+ new OS("Windows 8.1 or Windows Server 2012R2", "windows nt 6\\.3", "windows nt (6\\.3)"),//
+ new OS("Windows 8 or Windows Server 2012", "windows nt 6\\.2", "windows nt (6\\.2)"),//
+ new OS("Windows Vista", "windows nt 6\\.0", "windows nt (6\\.0)"), //
+ new OS("Windows 7 or Windows Server 2008R2", "windows nt 6\\.1", "windows nt (6\\.1)"), //
+ new OS("Windows 2003", "windows nt 5\\.2", "windows nt (5\\.2)"), //
+ new OS("Windows XP", "windows nt 5\\.1", "windows nt (5\\.1)"), //
+ new OS("Windows 2000", "windows nt 5\\.0", "windows nt (5\\.0)"), //
+ new OS("Windows Phone", "windows (ce|phone|mobile)( os)?", "windows (?:ce|phone|mobile) (\\d+([._]\\d+)*)"), //
+ new OS("Windows", "windows"), //
+ new OS("OSX", "os x (\\d+)[._](\\d+)", "os x (\\d+([._]\\d+)*)"), //
+ new OS("Android", "Android", "Android (\\d+([._]\\d+)*)"),//
+ new OS("Android", "XiaoMi|MI\\s+", "\\(X(\\d+([._]\\d+)*)"),//
+ new OS("Linux", "linux"), //
+ new OS("Wii", "wii", "wii libnup/(\\d+([._]\\d+)*)"), //
+ new OS("PS3", "playstation 3", "playstation 3; (\\d+([._]\\d+)*)"), //
+ new OS("PSP", "playstation portable", "Portable\\); (\\d+([._]\\d+)*)"), //
+ new OS("iPad", "\\(iPad.*os (\\d+)[._](\\d+)", "\\(iPad.*os (\\d+([._]\\d+)*)"), //
+ new OS("iPhone", "\\(iPhone.*os (\\d+)[._](\\d+)", "\\(iPhone.*os (\\d+([._]\\d+)*)"), //
+ new OS("YPod", "iPod touch[\\s\\;]+iPhone.*os (\\d+)[._](\\d+)", "iPod touch[\\s\\;]+iPhone.*os (\\d+([._]\\d+)*)"), //
+ new OS("YPad", "iPad[\\s\\;]+iPhone.*os (\\d+)[._](\\d+)", "iPad[\\s\\;]+iPhone.*os (\\d+([._]\\d+)*)"), //
+ new OS("YPhone", "iPhone[\\s\\;]+iPhone.*os (\\d+)[._](\\d+)", "iPhone[\\s\\;]+iPhone.*os (\\d+([._]\\d+)*)"), //
+ new OS("Symbian", "symbian(os)?"), //
+ new OS("Darwin", "Darwin\\/([\\d\\w\\.\\-]+)", "Darwin\\/([\\d\\w\\.\\-]+)"), //
+ new OS("Adobe Air", "AdobeAir\\/([\\d\\w\\.\\-]+)", "AdobeAir\\/([\\d\\w\\.\\-]+)"), //
+ new OS("Java", "Java[\\s]+([\\d\\w\\.\\-]+)", "Java[\\s]+([\\d\\w\\.\\-]+)")//
+ );
+
+ /**
+ * 添加自定义的系统类型
+ *
+ * @param name 浏览器名称
+ * @param regex 关键字或表达式
+ * @param versionRegex 匹配版本的正则
+ * @since 5.7.4
+ */
+ synchronized public static void addCustomOs(String name, String regex, String versionRegex) {
+ oses.add(new OS(name, regex, versionRegex));
+ }
+
+ private Pattern versionPattern;
+
+ /**
+ * 构造
+ *
+ * @param name 系统名称
+ * @param regex 关键字或表达式
+ */
+ public OS(String name, String regex) {
+ this(name, regex, null);
+ }
+
+ /**
+ * 构造
+ *
+ * @param name 系统名称
+ * @param regex 关键字或表达式
+ * @param versionRegex 版本正则表达式
+ * @since 5.7.4
+ */
+ public OS(String name, String regex, String versionRegex) {
+ super(name, regex);
+ if (null != versionRegex) {
+ this.versionPattern = Pattern.compile(versionRegex, Pattern.CASE_INSENSITIVE);
+ }
+ }
+
+ /**
+ * 获取浏览器版本
+ *
+ * @param userAgentString User-Agent字符串
+ * @return 版本
+ */
+ public String getVersion(String userAgentString) {
+ if (isUnknown() || null == this.versionPattern) {
+ // 无版本信息
+ return null;
+ }
+ return ReUtil.getGroup1(this.versionPattern, userAgentString);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Platform.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Platform.java
new file mode 100644
index 0000000..4aed047
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/Platform.java
@@ -0,0 +1,147 @@
+package aiyh.utils.tool.cn.hutool.http.useragent;
+
+import aiyh.utils.tool.cn.hutool.core.collection.CollUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 平台对象
+ *
+ * @author looly
+ * @since 4.2.1
+ */
+public class Platform extends UserAgentInfo {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 未知
+ */
+ public static final Platform Unknown = new Platform(NameUnknown, null);
+
+ /**
+ * Iphone
+ */
+ public static final Platform IPHONE = new Platform("iPhone", "iphone");
+ /**
+ * ipod
+ */
+ public static final Platform IPOD = new Platform("iPod", "ipod");
+ /**
+ * ipad
+ */
+ public static final Platform IPAD = new Platform("iPad", "ipad");
+
+ /**
+ * android
+ */
+ public static final Platform ANDROID = new Platform("Android", "android");
+ /**
+ * android
+ */
+ public static final Platform GOOGLE_TV = new Platform("GoogleTV", "googletv");
+
+ /**
+ * Windows Phone
+ */
+ public static final Platform WINDOWS_PHONE = new Platform("Windows Phone", "windows (ce|phone|mobile)( os)?");
+
+ /**
+ * 支持的移动平台类型
+ */
+ public static final List mobilePlatforms = CollUtil.newArrayList(//
+ WINDOWS_PHONE, //
+ IPAD, //
+ IPOD, //
+ IPHONE, //
+ new Platform("Android", "XiaoMi|MI\\s+"), //
+ ANDROID, //
+ GOOGLE_TV, //
+ new Platform("htcFlyer", "htc_flyer"), //
+ new Platform("Symbian", "symbian(os)?"), //
+ new Platform("Blackberry", "blackberry") //
+ );
+
+ /**
+ * 支持的桌面平台类型
+ */
+ public static final List desktopPlatforms = CollUtil.newArrayList(//
+ new Platform("Windows", "windows"), //
+ new Platform("Mac", "(macintosh|darwin)"), //
+ new Platform("Linux", "linux"), //
+ new Platform("Wii", "wii"), //
+ new Platform("Playstation", "playstation"), //
+ new Platform("Java", "java") //
+ );
+
+ /**
+ * 支持的平台类型
+ */
+ public static final List platforms;
+
+ static {
+ platforms = new ArrayList<>(13);
+ platforms.addAll(mobilePlatforms);
+ platforms.addAll(desktopPlatforms);
+ }
+
+ /**
+ * 构造
+ *
+ * @param name 平台名称
+ * @param regex 关键字或表达式
+ */
+ public Platform(String name, String regex) {
+ super(name, regex);
+ }
+
+ /**
+ * 是否为移动平台
+ *
+ * @return 是否为移动平台
+ */
+ public boolean isMobile() {
+ return mobilePlatforms.contains(this);
+ }
+
+ /**
+ * 是否为Iphone或者iPod设备
+ *
+ * @return 是否为Iphone或者iPod设备
+ * @since 5.2.3
+ */
+ public boolean isIPhoneOrIPod() {
+ return this.equals(IPHONE) || this.equals(IPOD);
+ }
+
+ /**
+ * 是否为Iphone或者iPod设备
+ *
+ * @return 是否为Iphone或者iPod设备
+ * @since 5.2.3
+ */
+ public boolean isIPad() {
+ return this.equals(IPAD);
+ }
+
+ /**
+ * 是否为IOS平台,包括IPhone、IPod、IPad
+ *
+ * @return 是否为IOS平台,包括IPhone、IPod、IPad
+ * @since 5.2.3
+ */
+ public boolean isIos() {
+ return isIPhoneOrIPod() || isIPad();
+ }
+
+ /**
+ * 是否为Android平台,包括Android和Google TV
+ *
+ * @return 是否为Android平台,包括Android和Google TV
+ * @since 5.2.3
+ */
+ public boolean isAndroid() {
+ return this.equals(ANDROID) || this.equals(GOOGLE_TV);
+ }
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgent.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgent.java
new file mode 100644
index 0000000..6921422
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgent.java
@@ -0,0 +1,196 @@
+package aiyh.utils.tool.cn.hutool.http.useragent;
+
+import java.io.Serializable;
+
+/**
+ * User-Agent信息对象
+ *
+ * @author looly
+ * @since 4.2.1
+ */
+public class UserAgent implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 是否为移动平台
+ */
+ private boolean mobile;
+ /**
+ * 浏览器类型
+ */
+ private aiyh.utils.tool.cn.hutool.http.useragent.Browser browser;
+ /**
+ * 浏览器版本
+ */
+ private String version;
+
+ /**
+ * 平台类型
+ */
+ private aiyh.utils.tool.cn.hutool.http.useragent.Platform platform;
+
+ /**
+ * 系统类型
+ */
+ private aiyh.utils.tool.cn.hutool.http.useragent.OS os;
+ /**
+ * 系统版本
+ */
+ private String osVersion;
+
+ /**
+ * 引擎类型
+ */
+ private aiyh.utils.tool.cn.hutool.http.useragent.Engine engine;
+ /**
+ * 引擎版本
+ */
+ private String engineVersion;
+
+ /**
+ * 是否为移动平台
+ *
+ * @return 是否为移动平台
+ */
+ public boolean isMobile() {
+ return mobile;
+ }
+
+ /**
+ * 设置是否为移动平台
+ *
+ * @param mobile 是否为移动平台
+ */
+ public void setMobile(boolean mobile) {
+ this.mobile = mobile;
+ }
+
+ /**
+ * 获取浏览器类型
+ *
+ * @return 浏览器类型
+ */
+ public aiyh.utils.tool.cn.hutool.http.useragent.Browser getBrowser() {
+ return browser;
+ }
+
+ /**
+ * 设置浏览器类型
+ *
+ * @param browser 浏览器类型
+ */
+ public void setBrowser(Browser browser) {
+ this.browser = browser;
+ }
+
+ /**
+ * 获取平台类型
+ *
+ * @return 平台类型
+ */
+ public aiyh.utils.tool.cn.hutool.http.useragent.Platform getPlatform() {
+ return platform;
+ }
+
+ /**
+ * 设置平台类型
+ *
+ * @param platform 平台类型
+ */
+ public void setPlatform(Platform platform) {
+ this.platform = platform;
+ }
+
+ /**
+ * 获取系统类型
+ *
+ * @return 系统类型
+ */
+ public aiyh.utils.tool.cn.hutool.http.useragent.OS getOs() {
+ return os;
+ }
+
+ /**
+ * 设置系统类型
+ *
+ * @param os 系统类型
+ */
+ public void setOs(OS os) {
+ this.os = os;
+ }
+
+ /**
+ * 获取系统版本
+ *
+ * @return 系统版本
+ * @since 5.7.4
+ */
+ public String getOsVersion() {
+ return this.osVersion;
+ }
+
+ /**
+ * 设置系统版本
+ *
+ * @param osVersion 系统版本
+ * @since 5.7.4
+ */
+ public void setOsVersion(String osVersion) {
+ this.osVersion = osVersion;
+ }
+
+ /**
+ * 获取引擎类型
+ *
+ * @return 引擎类型
+ */
+ public aiyh.utils.tool.cn.hutool.http.useragent.Engine getEngine() {
+ return engine;
+ }
+
+ /**
+ * 设置引擎类型
+ *
+ * @param engine 引擎类型
+ */
+ public void setEngine(Engine engine) {
+ this.engine = engine;
+ }
+
+ /**
+ * 获取浏览器版本
+ *
+ * @return 浏览器版本
+ */
+ public String getVersion() {
+ return version;
+ }
+
+ /**
+ * 设置浏览器版本
+ *
+ * @param version 浏览器版本
+ */
+ public void setVersion(String version) {
+ this.version = version;
+ }
+
+ /**
+ * 获取引擎版本
+ *
+ * @return 引擎版本
+ */
+ public String getEngineVersion() {
+ return engineVersion;
+ }
+
+ /**
+ * 设置引擎版本
+ *
+ * @param engineVersion 引擎版本
+ */
+ public void setEngineVersion(String engineVersion) {
+ this.engineVersion = engineVersion;
+ }
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentInfo.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentInfo.java
new file mode 100755
index 0000000..c55e029
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentInfo.java
@@ -0,0 +1,114 @@
+package aiyh.utils.tool.cn.hutool.http.useragent;
+
+import aiyh.utils.tool.cn.hutool.core.util.ReUtil;
+
+import java.io.Serializable;
+import java.util.regex.Pattern;
+
+/**
+ * User-agent信息
+ *
+ * @author looly
+ * @since 4.2.1
+ */
+public class UserAgentInfo implements Serializable {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 未知类型
+ */
+ public static final String NameUnknown = "Unknown";
+
+ /** 信息名称 */
+ private final String name;
+ /** 信息匹配模式 */
+ private final Pattern pattern;
+
+ /**
+ * 构造
+ *
+ * @param name 名字
+ * @param regex 表达式
+ */
+ public UserAgentInfo(String name, String regex) {
+ this(name, (null == regex) ? null : Pattern.compile(regex, Pattern.CASE_INSENSITIVE));
+ }
+
+ /**
+ * 构造
+ *
+ * @param name 名字
+ * @param pattern 匹配模式
+ */
+ public UserAgentInfo(String name, Pattern pattern) {
+ this.name = name;
+ this.pattern = pattern;
+ }
+
+ /**
+ * 获取信息名称
+ *
+ * @return 信息名称
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * 获取匹配模式
+ *
+ * @return 匹配模式
+ */
+ public Pattern getPattern() {
+ return pattern;
+ }
+
+ /**
+ * 指定内容中是否包含匹配此信息的内容
+ *
+ * @param content User-Agent字符串
+ * @return 是否包含匹配此信息的内容
+ */
+ public boolean isMatch(String content) {
+ return ReUtil.contains(this.pattern, content);
+ }
+
+ /**
+ * 是否为Unknown
+ *
+ * @return 是否为Unknown
+ */
+ public boolean isUnknown() {
+ return NameUnknown.equals(this.name);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((name == null) ? 0 : name.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final UserAgentInfo other = (UserAgentInfo) obj;
+ if (name == null) {
+ return other.name == null;
+ } else return name.equals(other.name);
+ }
+
+ @Override
+ public String toString() {
+ return this.name;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentParser.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentParser.java
new file mode 100644
index 0000000..8285b9a
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentParser.java
@@ -0,0 +1,108 @@
+package aiyh.utils.tool.cn.hutool.http.useragent;
+
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+/**
+ * User-Agent解析器
+ *
+ * @author looly
+ * @since 4.2.1
+ */
+public class UserAgentParser {
+
+ /**
+ * 解析User-Agent
+ *
+ * @param userAgentString User-Agent字符串
+ * @return {@link UserAgent}
+ */
+ public static UserAgent parse(String userAgentString) {
+ if (StrUtil.isBlank(userAgentString)) {
+ return null;
+ }
+ final UserAgent userAgent = new UserAgent();
+
+ // 浏览器
+ final aiyh.utils.tool.cn.hutool.http.useragent.Browser browser = parseBrowser(userAgentString);
+ userAgent.setBrowser(browser);
+ userAgent.setVersion(browser.getVersion(userAgentString));
+
+ // 浏览器引擎
+ final aiyh.utils.tool.cn.hutool.http.useragent.Engine engine = parseEngine(userAgentString);
+ userAgent.setEngine(engine);
+ userAgent.setEngineVersion(engine.getVersion(userAgentString));
+
+ // 操作系统
+ final aiyh.utils.tool.cn.hutool.http.useragent.OS os = parseOS(userAgentString);
+ userAgent.setOs(os);
+ userAgent.setOsVersion(os.getVersion(userAgentString));
+
+ // 平台
+ final aiyh.utils.tool.cn.hutool.http.useragent.Platform platform = parsePlatform(userAgentString);
+ userAgent.setPlatform(platform);
+ userAgent.setMobile(platform.isMobile() || browser.isMobile());
+
+
+ return userAgent;
+ }
+
+ /**
+ * 解析浏览器类型
+ *
+ * @param userAgentString User-Agent字符串
+ * @return 浏览器类型
+ */
+ private static aiyh.utils.tool.cn.hutool.http.useragent.Browser parseBrowser(String userAgentString) {
+ for (aiyh.utils.tool.cn.hutool.http.useragent.Browser browser : aiyh.utils.tool.cn.hutool.http.useragent.Browser.browers) {
+ if (browser.isMatch(userAgentString)) {
+ return browser;
+ }
+ }
+ return Browser.Unknown;
+ }
+
+ /**
+ * 解析引擎类型
+ *
+ * @param userAgentString User-Agent字符串
+ * @return 引擎类型
+ */
+ private static aiyh.utils.tool.cn.hutool.http.useragent.Engine parseEngine(String userAgentString) {
+ for (aiyh.utils.tool.cn.hutool.http.useragent.Engine engine : aiyh.utils.tool.cn.hutool.http.useragent.Engine.engines) {
+ if (engine.isMatch(userAgentString)) {
+ return engine;
+ }
+ }
+ return Engine.Unknown;
+ }
+
+ /**
+ * 解析系统类型
+ *
+ * @param userAgentString User-Agent字符串
+ * @return 系统类型
+ */
+ private static aiyh.utils.tool.cn.hutool.http.useragent.OS parseOS(String userAgentString) {
+ for (aiyh.utils.tool.cn.hutool.http.useragent.OS os : aiyh.utils.tool.cn.hutool.http.useragent.OS.oses) {
+ if (os.isMatch(userAgentString)) {
+ return os;
+ }
+ }
+ return OS.Unknown;
+ }
+
+ /**
+ * 解析平台类型
+ *
+ * @param userAgentString User-Agent字符串
+ * @return 平台类型
+ */
+ private static aiyh.utils.tool.cn.hutool.http.useragent.Platform parsePlatform(String userAgentString) {
+ for (aiyh.utils.tool.cn.hutool.http.useragent.Platform platform : aiyh.utils.tool.cn.hutool.http.useragent.Platform.platforms) {
+ if (platform.isMatch(userAgentString)) {
+ return platform;
+ }
+ }
+ return Platform.Unknown;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentUtil.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentUtil.java
new file mode 100644
index 0000000..0590b4e
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/UserAgentUtil.java
@@ -0,0 +1,20 @@
+package aiyh.utils.tool.cn.hutool.http.useragent;
+
+/**
+ * User-Agent工具类
+ *
+ * @author looly
+ */
+public class UserAgentUtil {
+
+ /**
+ * 解析User-Agent
+ *
+ * @param userAgentString User-Agent字符串
+ * @return {@link UserAgent}
+ */
+ public static UserAgent parse(String userAgentString) {
+ return UserAgentParser.parse(userAgentString);
+ }
+
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/package-info.java
new file mode 100644
index 0000000..5a697db
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/useragent/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * User-Agent解析
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http.useragent;
\ No newline at end of file
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapClient.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapClient.java
new file mode 100644
index 0000000..51b91fd
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapClient.java
@@ -0,0 +1,654 @@
+package aiyh.utils.tool.cn.hutool.http.webservice;
+
+import aiyh.utils.tool.cn.hutool.core.collection.CollUtil;
+import aiyh.utils.tool.cn.hutool.core.io.IoUtil;
+import aiyh.utils.tool.cn.hutool.core.map.MapUtil;
+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.XmlUtil;
+import aiyh.utils.tool.cn.hutool.http.HttpBase;
+import aiyh.utils.tool.cn.hutool.http.HttpGlobalConfig;
+import aiyh.utils.tool.cn.hutool.http.HttpRequest;
+import aiyh.utils.tool.cn.hutool.http.HttpResponse;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.QName;
+import javax.xml.soap.*;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.charset.Charset;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+/**
+ * SOAP客户端
+ *
+ *
+ * 此对象用于构建一个SOAP消息,并通过HTTP接口发出消息内容。
+ * SOAP消息本质上是一个XML文本,可以通过调用{@link #getMsgStr(boolean)} 方法获取消息体
+ *
+ * 使用方法:
+ *
+ *
+ * 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 {
+
+ /**
+ * XML消息体的Content-Type
+ * soap1.1 : text/xml
+ * soap1.2 : application/soap+xml
+ * soap1.1与soap1.2区别: https://www.cnblogs.com/qlqwjy/p/7577147.html
+ */
+ private static final String CONTENT_TYPE_SOAP11_TEXT_XML = "text/xml;charset=";
+ private static final String CONTENT_TYPE_SOAP12_SOAP_XML = "application/soap+xml;charset=";
+
+ /**
+ * 请求的URL地址
+ */
+ private String url;
+
+ /**
+ * 默认连接超时
+ */
+ private int connectionTimeout = aiyh.utils.tool.cn.hutool.http.HttpGlobalConfig.getTimeout();
+ /**
+ * 默认读取超时
+ */
+ private int readTimeout = HttpGlobalConfig.getTimeout();
+
+ /**
+ * 消息工厂,用于创建消息
+ */
+ private MessageFactory factory;
+ /**
+ * SOAP消息
+ */
+ private SOAPMessage message;
+ /**
+ * 消息方法节点
+ */
+ private SOAPBodyElement methodEle;
+ /**
+ * 应用于方法上的命名空间URI
+ */
+ private final String namespaceURI;
+ /**
+ * Soap协议
+ * soap1.1 : text/xml
+ * soap1.2 : application/soap+xml
+ */
+ private final SoapProtocol protocol;
+
+ /**
+ * 创建SOAP客户端,默认使用soap1.1版本协议
+ *
+ * @param url WS的URL地址
+ * @return this
+ */
+ public static SoapClient create(String url) {
+ return new SoapClient(url);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @return this
+ */
+ public static SoapClient create(String url, SoapProtocol protocol) {
+ return new SoapClient(url, protocol);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @param namespaceURI 方法上的命名空间URI
+ * @return this
+ * @since 4.5.6
+ */
+ public static SoapClient create(String url, SoapProtocol protocol, String namespaceURI) {
+ return new SoapClient(url, protocol, namespaceURI);
+ }
+
+ /**
+ * 构造,默认使用soap1.1版本协议
+ *
+ * @param url WS的URL地址
+ */
+ public SoapClient(String url) {
+ this(url, SoapProtocol.SOAP_1_1);
+ }
+
+ /**
+ * 构造
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议版本,见{@link SoapProtocol}
+ */
+ public SoapClient(String url, SoapProtocol protocol) {
+ this(url, protocol, null);
+ }
+
+ /**
+ * 构造
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议版本,见{@link SoapProtocol}
+ * @param namespaceURI 方法上的命名空间URI
+ * @since 4.5.6
+ */
+ public SoapClient(String url, SoapProtocol protocol, String namespaceURI) {
+ this.url = url;
+ this.namespaceURI = namespaceURI;
+ this.protocol = protocol;
+ init(protocol);
+ }
+
+ /**
+ * 初始化
+ *
+ * @param protocol 协议版本枚举,见{@link SoapProtocol}
+ * @return this
+ */
+ public SoapClient init(SoapProtocol protocol) {
+ // 创建消息工厂
+ try {
+ this.factory = MessageFactory.newInstance(protocol.getValue());
+ // 根据消息工厂创建SoapMessage
+ this.message = factory.createMessage();
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ return this;
+ }
+
+ /**
+ * 重置SOAP客户端,用于客户端复用
+ *
+ *
+ * 重置后需调用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 params, boolean useMethodPrefix) {
+ return setMethod(new QName(name.getURI(), name.getLocalName(), name.getPrefix()), params, useMethodPrefix);
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param name 方法名及其命名空间
+ * @param params 参数
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ */
+ public SoapClient setMethod(QName name, Map params, boolean useMethodPrefix) {
+ setMethod(name);
+ final String prefix = useMethodPrefix ? name.getPrefix() : null;
+ final SOAPBodyElement methodEle = this.methodEle;
+ for (Entry entry : MapUtil.wrap(params)) {
+ setParam(methodEle, entry.getKey(), entry.getValue(), prefix);
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置请求方法
+ * 方法名自动识别前缀,前缀和方法名使用“:”分隔
+ * 当识别到前缀后,自动添加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 methodNameList = StrUtil.split(methodName, ':');
+ final QName qName;
+ if (2 == methodNameList.size()) {
+ qName = new QName(namespaceURI, methodNameList.get(1), methodNameList.get(0));
+ } else {
+ qName = new QName(namespaceURI, methodName);
+ }
+ return setMethod(qName);
+ }
+
+ /**
+ * 设置请求方法
+ *
+ * @param name 方法名及其命名空间
+ * @return this
+ */
+ public SoapClient setMethod(QName name) {
+ try {
+ this.methodEle = this.message.getSOAPBody().addBodyElement(name);
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ return this;
+ }
+
+ /**
+ * 设置方法参数,使用方法的前缀
+ *
+ * @param name 参数名
+ * @param value 参数值,可以是字符串或Map或{@link SOAPElement}
+ * @return this
+ */
+ public SoapClient setParam(String name, Object value) {
+ return setParam(name, value, true);
+ }
+
+ /**
+ * 设置方法参数
+ *
+ * @param name 参数名
+ * @param value 参数值,可以是字符串或Map或{@link SOAPElement}
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ */
+ public SoapClient setParam(String name, Object value, boolean useMethodPrefix) {
+ setParam(this.methodEle, name, value, useMethodPrefix ? this.methodEle.getPrefix() : null);
+ return this;
+ }
+
+ /**
+ * 批量设置参数,使用方法的前缀
+ *
+ * @param params 参数列表
+ * @return this
+ * @since 4.5.6
+ */
+ public SoapClient setParams(Map params) {
+ return setParams(params, true);
+ }
+
+ /**
+ * 批量设置参数
+ *
+ * @param params 参数列表
+ * @param useMethodPrefix 是否使用方法的命名空间前缀
+ * @return this
+ * @since 4.5.6
+ */
+ public SoapClient setParams(Map params, boolean useMethodPrefix) {
+ for (Entry entry : MapUtil.wrap(params)) {
+ setParam(entry.getKey(), entry.getValue(), useMethodPrefix);
+ }
+ return this;
+ }
+
+ /**
+ * 获取方法节点
+ * 用于创建子节点等操作
+ *
+ * @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> entry : res.headers().entrySet()) {
+ if (StrUtil.isNotEmpty(entry.getKey())) {
+ headers.setHeader(entry.getKey(), CollUtil.get(entry.getValue(), 0));
+ }
+ }
+ try {
+ return this.factory.createMessage(headers, res.bodyStream());
+ } catch (IOException | SOAPException e) {
+ throw new SoapRuntimeException(e);
+ } finally {
+ IoUtil.close(res);
+ }
+ }
+
+ /**
+ * 执行Webservice请求,即发送SOAP内容
+ *
+ * @return 返回结果
+ */
+ public String send() {
+ return send(false);
+ }
+
+ /**
+ * 执行Webservice请求,即发送SOAP内容
+ *
+ * @param pretty 是否格式化
+ * @return 返回结果
+ */
+ public String send(boolean pretty) {
+ final String body = sendForResponse().body();
+ return pretty ? XmlUtil.format(body) : body;
+ }
+
+ // -------------------------------------------------------------------------------------------------------- Private method start
+
+ /**
+ * 发送请求,获取异步响应
+ *
+ * @return 响应对象
+ */
+ public HttpResponse sendForResponse() {
+ return HttpRequest.post(this.url)//
+ .setFollowRedirects(true)//
+ .setConnectionTimeout(this.connectionTimeout)
+ .setReadTimeout(this.readTimeout)
+ .contentType(getXmlContentType())//
+ .header(this.headers())
+ .body(getMsgStr(false))//
+ .executeAsync();
+ }
+
+ /**
+ * 获取请求的Content-Type,附加编码信息
+ *
+ * @return 请求的Content-Type
+ */
+ private String getXmlContentType() {
+ switch (this.protocol) {
+ case SOAP_1_1:
+ return CONTENT_TYPE_SOAP11_TEXT_XML.concat(this.charset.toString());
+ case SOAP_1_2:
+ return CONTENT_TYPE_SOAP12_SOAP_XML.concat(this.charset.toString());
+ default:
+ throw new SoapRuntimeException("Unsupported protocol: {}", this.protocol);
+ }
+ }
+
+ /**
+ * 设置方法参数
+ *
+ * @param ele 方法节点
+ * @param name 参数名
+ * @param value 参数值
+ * @param prefix 命名空间前缀, {@code null}表示不使用前缀
+ * @return {@link SOAPElement}子节点
+ */
+ @SuppressWarnings("rawtypes")
+ private static SOAPElement setParam(SOAPElement ele, String name, Object value, String prefix) {
+ final SOAPElement childEle;
+ try {
+ if (StrUtil.isNotBlank(prefix)) {
+ childEle = ele.addChildElement(name, prefix);
+ } else {
+ childEle = ele.addChildElement(name);
+ }
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+
+ if (null != value) {
+ if (value instanceof SOAPElement) {
+ // 单个子节点
+ try {
+ ele.addChildElement((SOAPElement) value);
+ } catch (SOAPException e) {
+ throw new SoapRuntimeException(e);
+ }
+ } else if (value instanceof Map) {
+ // 多个字节点
+ Entry entry;
+ for (Object obj : ((Map) value).entrySet()) {
+ entry = (Entry) obj;
+ setParam(childEle, entry.getKey().toString(), entry.getValue(), prefix);
+ }
+ } else {
+ // 单个值
+ childEle.setValue(value.toString());
+ }
+ }
+
+ return childEle;
+ }
+ // -------------------------------------------------------------------------------------------------------- Private method end
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapProtocol.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapProtocol.java
new file mode 100644
index 0000000..6c0cbbc
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapProtocol.java
@@ -0,0 +1,35 @@
+package aiyh.utils.tool.cn.hutool.http.webservice;
+
+import javax.xml.soap.SOAPConstants;
+
+/**
+ * SOAP协议版本枚举
+ *
+ * @author looly
+ */
+public enum SoapProtocol {
+ /** SOAP 1.1协议 */
+ SOAP_1_1(SOAPConstants.SOAP_1_1_PROTOCOL),
+ /** SOAP 1.2协议 */
+ SOAP_1_2(SOAPConstants.SOAP_1_2_PROTOCOL);
+
+ /**
+ * 构造
+ *
+ * @param value {@link SOAPConstants} 中的协议版本值
+ */
+ SoapProtocol(String value) {
+ this.value = value;
+ }
+
+ private final String value;
+
+ /**
+ * 获取版本值信息
+ *
+ * @return 版本值信息
+ */
+ public String getValue() {
+ return this.value;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapRuntimeException.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapRuntimeException.java
new file mode 100644
index 0000000..eaf7cf9
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapRuntimeException.java
@@ -0,0 +1,32 @@
+package aiyh.utils.tool.cn.hutool.http.webservice;
+
+import aiyh.utils.tool.cn.hutool.core.util.StrUtil;
+
+/**
+ * SOAP异常
+ *
+ * @author xiaoleilu
+ */
+public class SoapRuntimeException extends RuntimeException {
+ private static final long serialVersionUID = 8247610319171014183L;
+
+ public SoapRuntimeException(Throwable e) {
+ super(e.getMessage(), e);
+ }
+
+ public SoapRuntimeException(String message) {
+ super(message);
+ }
+
+ public SoapRuntimeException(String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params));
+ }
+
+ public SoapRuntimeException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+
+ public SoapRuntimeException(Throwable throwable, String messageTemplate, Object... params) {
+ super(StrUtil.format(messageTemplate, params), throwable);
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapUtil.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapUtil.java
new file mode 100644
index 0000000..b64b294
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/SoapUtil.java
@@ -0,0 +1,91 @@
+package aiyh.utils.tool.cn.hutool.http.webservice;
+
+import aiyh.utils.tool.cn.hutool.core.exceptions.UtilException;
+import aiyh.utils.tool.cn.hutool.core.util.CharsetUtil;
+import aiyh.utils.tool.cn.hutool.core.util.XmlUtil;
+
+import javax.xml.soap.SOAPException;
+import javax.xml.soap.SOAPMessage;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+/**
+ * SOAP相关工具类
+ *
+ * @author looly
+ * @since 4.5.7
+ */
+public class SoapUtil {
+
+ /**
+ * 创建SOAP客户端,默认使用soap1.1版本协议
+ *
+ * @param url WS的URL地址
+ * @return {@link aiyh.utils.tool.cn.hutool.http.webservice.SoapClient}
+ */
+ public static aiyh.utils.tool.cn.hutool.http.webservice.SoapClient createClient(String url) {
+ return aiyh.utils.tool.cn.hutool.http.webservice.SoapClient.create(url);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @return {@link aiyh.utils.tool.cn.hutool.http.webservice.SoapClient}
+ */
+ public static aiyh.utils.tool.cn.hutool.http.webservice.SoapClient createClient(String url, SoapProtocol protocol) {
+ return aiyh.utils.tool.cn.hutool.http.webservice.SoapClient.create(url, protocol);
+ }
+
+ /**
+ * 创建SOAP客户端
+ *
+ * @param url WS的URL地址
+ * @param protocol 协议,见{@link SoapProtocol}
+ * @param namespaceURI 方法上的命名空间URI
+ * @return {@link aiyh.utils.tool.cn.hutool.http.webservice.SoapClient}
+ * @since 4.5.6
+ */
+ public static aiyh.utils.tool.cn.hutool.http.webservice.SoapClient createClient(String url, SoapProtocol protocol, String namespaceURI) {
+ return SoapClient.create(url, protocol, namespaceURI);
+ }
+
+ /**
+ * {@link SOAPMessage} 转为字符串
+ *
+ * @param message SOAP消息对象
+ * @param pretty 是否格式化
+ * @return SOAP XML字符串
+ */
+ public static String toString(SOAPMessage message, boolean pretty) {
+ return toString(message, pretty, CharsetUtil.CHARSET_UTF_8);
+ }
+
+ /**
+ * {@link SOAPMessage} 转为字符串
+ *
+ * @param message SOAP消息对象
+ * @param pretty 是否格式化
+ * @param charset 编码
+ * @return SOAP XML字符串
+ * @since 4.5.7
+ */
+ public static String toString(SOAPMessage message, boolean pretty, Charset charset) {
+ final ByteArrayOutputStream out = new ByteArrayOutputStream();
+ try {
+ message.writeTo(out);
+ } catch (SOAPException | IOException e) {
+ throw new SoapRuntimeException(e);
+ }
+ String messageToString;
+ try {
+ messageToString = out.toString(charset.toString());
+ } catch (UnsupportedEncodingException e) {
+ throw new UtilException(e);
+ }
+ return pretty ? XmlUtil.format(messageToString) : messageToString;
+ }
+}
diff --git a/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/package-info.java b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/package-info.java
new file mode 100644
index 0000000..5fa3834
--- /dev/null
+++ b/src/main/java/aiyh/utils/tool/cn/hutool/http/webservice/package-info.java
@@ -0,0 +1,6 @@
+/**
+ * Webservice客户端封装实现
+ *
+ * @author looly
+ */
+package aiyh.utils.tool.cn.hutool.http.webservice;
\ No newline at end of file
diff --git a/src/main/java/com/api/bokang/xiao/zscq/controller/ReserveSelectController.java b/src/main/java/com/api/bokang/xiao/zscq/controller/ReserveSelectController.java
index 3be017f..ee75f63 100644
--- a/src/main/java/com/api/bokang/xiao/zscq/controller/ReserveSelectController.java
+++ b/src/main/java/com/api/bokang/xiao/zscq/controller/ReserveSelectController.java
@@ -6,6 +6,7 @@ import com.api.bokang.xiao.zscq.service.ReserveService;
import com.api.bokang.xiao.zscq.service.impl.ReserveServiceImpl;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import org.apache.log4j.Logger;
+import weaver.bokang.xiao.zscq.store.TableNameStore;
import weaver.file.ImageFileManager;
import weaver.hrm.HrmUserVarify;
import weaver.hrm.User;
@@ -17,7 +18,9 @@ import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.StreamingOutput;
import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -48,77 +51,68 @@ public class ReserveSelectController {
@Produces(MediaType.APPLICATION_JSON)
public String reserveSelect(@Context HttpServletRequest request, @Context HttpServletResponse response, @RequestBody Map param) {
try{
- log.info("====== into getReportData success =======");
+ log.info("====== into reserveSelect success =======");
log.info("param:"+param);
User loginUser = HrmUserVarify.getUser(request, response);
- List