diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/AIException.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/AIException.java index 44afe00eb4..cafb1aae87 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/AIException.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/AIException.java @@ -18,10 +18,13 @@ package cn.hutool.v7.ai; import cn.hutool.v7.core.exception.HutoolException; +import java.io.Serial; + /** * 异常处理类 */ public class AIException extends HutoolException { + @Serial private static final long serialVersionUID = 1L; /** diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfig.java index c5f7fd4d07..ed17011683 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfig.java @@ -16,6 +16,8 @@ package cn.hutool.v7.ai.core; +import cn.hutool.v7.http.proxy.ProxyInfo; + import java.util.Map; /** @@ -142,4 +144,19 @@ public interface AIConfig { */ int getReadTimeout(); + /** + * 获取http代理 + * + * @return http代理 + * @since 7.0.0 + */ + ProxyInfo getProxy(); + + /** + * 设置代理配置 + * + * @param proxy 连接超时时间 + * @since 7.0.0 + */ + void setProxy(ProxyInfo proxy); } diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfigBuilder.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfigBuilder.java index 345624a7d2..028208f74f 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfigBuilder.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/AIConfigBuilder.java @@ -16,6 +16,8 @@ package cn.hutool.v7.ai.core; +import cn.hutool.v7.http.proxy.ProxyInfo; + import java.lang.reflect.Constructor; /** @@ -134,6 +136,18 @@ public class AIConfigBuilder { return this; } + /** + * 设置代理 + * + * @param proxy 代理 + * @return config + * @since 7.0.0 + */ + public AIConfigBuilder setProxy(final ProxyInfo proxy) { + config.setProxy(proxy); + return this; + } + /** * 返回config实例 * diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseAIConfig.java similarity index 88% rename from hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseConfig.java rename to hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseAIConfig.java index 1e6384a5b6..956d7334f9 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseAIConfig.java @@ -16,6 +16,8 @@ package cn.hutool.v7.ai.core; +import cn.hutool.v7.http.proxy.ProxyInfo; + import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -25,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap; * @author elichow * @since 6.0.0 */ -public class BaseConfig implements AIConfig { +public class BaseAIConfig implements AIConfig { /** * API Key @@ -51,6 +53,10 @@ public class BaseConfig implements AIConfig { * 读取超时 */ protected volatile int readTimeout = 300000; + /** + * 代理 + */ + private volatile ProxyInfo proxy; @Override public void setApiKey(final String apiKey) { @@ -116,4 +122,14 @@ public class BaseConfig implements AIConfig { public void setReadTimeout(final int readTimeout) { this.readTimeout = readTimeout; } + + @Override + public ProxyInfo getProxy() { + return proxy; + } + + @Override + public void setProxy(final ProxyInfo proxy) { + this.proxy = proxy; + } } diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseAIService.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseAIService.java index 79ecfd2378..308fad8ab6 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseAIService.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/BaseAIService.java @@ -16,19 +16,18 @@ package cn.hutool.v7.ai.core; -import cn.hutool.v7.ai.AIException; -import cn.hutool.v7.http.HttpGlobalConfig; +import cn.hutool.v7.core.io.IoUtil; import cn.hutool.v7.http.HttpUtil; +import cn.hutool.v7.http.client.ClientConfig; import cn.hutool.v7.http.client.Response; +import cn.hutool.v7.http.client.engine.ClientEngine; +import cn.hutool.v7.http.client.engine.ClientEngineFactory; +import cn.hutool.v7.http.meta.ContentType; import cn.hutool.v7.http.meta.HeaderName; import cn.hutool.v7.http.meta.Method; -import cn.hutool.v7.json.JSONUtil; import java.io.BufferedReader; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; +import java.io.IOException; import java.util.Map; import java.util.function.Consumer; @@ -44,6 +43,7 @@ public class BaseAIService { * AI配置 */ protected final AIConfig config; + private final ClientEngine clientEngine; /** * 构造方法 @@ -52,6 +52,8 @@ public class BaseAIService { */ public BaseAIService(final AIConfig config) { this.config = config; + this.clientEngine = ClientEngineFactory.createEngine( + ClientConfig.of().setTimeout(config.getTimeout()).setProxy(config.getProxy())); } /** @@ -60,17 +62,10 @@ public class BaseAIService { * @return 请求响应 */ protected Response sendGet(final String endpoint) { - //链式构建请求 - try { - //设置超时 - HttpGlobalConfig.setTimeout(config.getTimeout()); - return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.GET) - .header(HeaderName.ACCEPT, "application/json") - .header(HeaderName.AUTHORIZATION, "Bearer " + config.getApiKey()) - .send(); - } catch (final AIException e) { - throw new AIException("Failed to send GET request: " + e.getMessage(), e); - } + return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.GET) + .header(HeaderName.ACCEPT, ContentType.JSON.getValue()) + .bearerAuth(config.getApiKey()) + .send(this.clientEngine); } /** @@ -80,20 +75,12 @@ public class BaseAIService { * @return 请求响应 */ protected Response sendPost(final String endpoint, final String paramJson) { - //链式构建请求 - try { - //设置超时3分钟 - HttpGlobalConfig.setTimeout(config.getTimeout()); - return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.POST) - .header(HeaderName.CONTENT_TYPE, "application/json") - .header(HeaderName.ACCEPT, "application/json") - .header(HeaderName.AUTHORIZATION, "Bearer " + config.getApiKey()) - .body(paramJson) - .send(); - } catch (final AIException e) { - throw new AIException("Failed to send POST request:" + e.getMessage(), e); - } - + return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.POST) + .header(HeaderName.CONTENT_TYPE, ContentType.JSON.getValue()) + .header(HeaderName.ACCEPT, ContentType.JSON.getValue()) + .bearerAuth(config.getApiKey()) + .body(paramJson) + .send(this.clientEngine); } /** @@ -103,19 +90,12 @@ public class BaseAIService { * @return 请求响应 */ protected Response sendFormData(final String endpoint, final Map paramMap) { - //链式构建请求 - try { - //设置超时3分钟 - HttpGlobalConfig.setTimeout(config.getTimeout()); - return HttpUtil.createPost(config.getApiUrl() + endpoint) - //form表单中有file对象会自动将文件编码为 multipart/form-data 格式。不需要设置 + return HttpUtil.createPost(config.getApiUrl() + endpoint) + //form表单中有file对象会自动将文件编码为 multipart/form-data 格式。不需要设置 // .header(HeaderName.CONTENT_TYPE, "multipart/form-data") - .header(HeaderName.AUTHORIZATION, "Bearer " + config.getApiKey()) - .form(paramMap) - .send(); - } catch (final AIException e) { - throw new AIException("Failed to send POST request:" + e.getMessage(), e); - } + .bearerAuth(config.getApiKey()) + .form(paramMap) + .send(this.clientEngine); } /** @@ -126,41 +106,22 @@ public class BaseAIService { * @param callback 流式数据回调函数 */ protected void sendPostStream(final String endpoint, final Map paramMap, final Consumer callback) { - HttpURLConnection connection = null; - try { - // 创建连接 - final URL apiUrl = new URL(config.getApiUrl() + endpoint); - connection = (HttpURLConnection) apiUrl.openConnection(); - connection.setRequestMethod(Method.POST.name()); - connection.setRequestProperty(HeaderName.CONTENT_TYPE.getValue(), "application/json"); - connection.setRequestProperty(HeaderName.AUTHORIZATION.getValue(), "Bearer " + config.getApiKey()); - connection.setDoOutput(true); - //设置读取超时 - connection.setReadTimeout(config.getReadTimeout()); - //设置连接超时 - connection.setConnectTimeout(config.getTimeout()); - // 发送请求体 - try (final OutputStream os = connection.getOutputStream()) { - final String jsonInputString = JSONUtil.toJsonStr(paramMap); - os.write(jsonInputString.getBytes()); - os.flush(); - } + final Response response = HttpUtil.createPost(config.getApiUrl() + endpoint) + //form表单中有file对象会自动将文件编码为 multipart/form-data 格式。不需要设置 +// .header(HeaderName.CONTENT_TYPE, "multipart/form-data") + .bearerAuth(config.getApiKey()) + .form(paramMap) + .send(this.clientEngine); - // 读取流式响应 - try (final BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - // 调用回调函数处理每一行数据 - callback.accept(line); - } + // 读取流式响应 + try (final BufferedReader reader = IoUtil.toUtf8Reader(response.bodyStream())) { + String line; + while ((line = reader.readLine()) != null) { + // 调用回调函数处理每一行数据 + callback.accept(line); } - } catch (final Exception e) { + } catch (final IOException e){ callback.accept("{\"error\": \"" + e.getMessage() + "\"}"); - } finally { - // 关闭连接 - if (connection != null) { - connection.disconnect(); - } } } } diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/Message.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/Message.java index 1b4e6d17ff..c63e41ef34 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/core/Message.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/core/Message.java @@ -23,6 +23,7 @@ package cn.hutool.v7.ai.core; * @since 6.0.0 */ public class Message { + //角色 注意:如果设置系统消息,请放在messages列表的第一位 private String role; //内容 @@ -49,9 +50,11 @@ public class Message { * 设置角色 * * @param role 角色 + * @return this */ - public void setRole(final String role) { + public Message setRole(final String role) { this.role = role; + return this; } /** @@ -76,8 +79,10 @@ public class Message { * 设置内容 * * @param content 内容 + * @return this */ - public void setContent(final Object content) { + public Message setContent(final Object content) { this.content = content; + return this; } } diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/deepseek/DeepSeekConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/deepseek/DeepSeekConfig.java index a9b7e08968..40eb6edf68 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/deepseek/DeepSeekConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/deepseek/DeepSeekConfig.java @@ -17,7 +17,7 @@ package cn.hutool.v7.ai.model.deepseek; import cn.hutool.v7.ai.Models; -import cn.hutool.v7.ai.core.BaseConfig; +import cn.hutool.v7.ai.core.BaseAIConfig; /** * DeepSeek配置类,初始化API接口地址,设置默认的模型 @@ -25,7 +25,7 @@ import cn.hutool.v7.ai.core.BaseConfig; * @author elichow * @since 6.0.0 */ -public class DeepSeekConfig extends BaseConfig { +public class DeepSeekConfig extends BaseAIConfig { /** * 定义API的基础URL,用于后续的所有API请求 diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/doubao/DoubaoConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/doubao/DoubaoConfig.java index af59ac6155..595801bb26 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/doubao/DoubaoConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/doubao/DoubaoConfig.java @@ -17,7 +17,7 @@ package cn.hutool.v7.ai.model.doubao; import cn.hutool.v7.ai.Models; -import cn.hutool.v7.ai.core.BaseConfig; +import cn.hutool.v7.ai.core.BaseAIConfig; /** * Doubao配置类,初始化API接口地址,设置默认的模型 @@ -25,7 +25,7 @@ import cn.hutool.v7.ai.core.BaseConfig; * @author elichow * @since 6.0.0 */ -public class DoubaoConfig extends BaseConfig { +public class DoubaoConfig extends BaseAIConfig { // 定义API的基础URL,用于和服务器通信 private static final String API_URL = "https://ark.cn-beijing.volces.com/api/v3"; diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/grok/GrokConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/grok/GrokConfig.java index 31b8b021a7..4a8584cf4f 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/grok/GrokConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/grok/GrokConfig.java @@ -17,7 +17,7 @@ package cn.hutool.v7.ai.model.grok; import cn.hutool.v7.ai.Models; -import cn.hutool.v7.ai.core.BaseConfig; +import cn.hutool.v7.ai.core.BaseAIConfig; /** * Grok配置类,初始化API接口地址,设置默认的模型 @@ -25,7 +25,7 @@ import cn.hutool.v7.ai.core.BaseConfig; * @author elichow * @since 6.0.0 */ -public class GrokConfig extends BaseConfig { +public class GrokConfig extends BaseAIConfig { private final String API_URL = "https://api.x.ai/v1"; diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/hutool/HutoolConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/hutool/HutoolConfig.java index 9bd04daf66..dbc16dd2e7 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/hutool/HutoolConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/hutool/HutoolConfig.java @@ -17,7 +17,7 @@ package cn.hutool.v7.ai.model.hutool; import cn.hutool.v7.ai.Models; -import cn.hutool.v7.ai.core.BaseConfig; +import cn.hutool.v7.ai.core.BaseAIConfig; /** * Hutool配置类,初始化API接口地址,设置默认的模型 @@ -25,7 +25,7 @@ import cn.hutool.v7.ai.core.BaseConfig; * @author elichow * @since 6.0.0 */ -public class HutoolConfig extends BaseConfig { +public class HutoolConfig extends BaseAIConfig { private final String API_URL = "https://api.hutool.cn/ai/api"; diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/ollama/OllamaConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/ollama/OllamaConfig.java index 5e0275c418..fdbb60db6c 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/ollama/OllamaConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/ollama/OllamaConfig.java @@ -17,7 +17,7 @@ package cn.hutool.v7.ai.model.ollama; import cn.hutool.v7.ai.Models; -import cn.hutool.v7.ai.core.BaseConfig; +import cn.hutool.v7.ai.core.BaseAIConfig; /** * Ollama配置类,初始化API接口地址,设置默认的模型 @@ -25,7 +25,7 @@ import cn.hutool.v7.ai.core.BaseConfig; * @author yangruoyu-yumeisoft * @since 5.8.40 */ -public class OllamaConfig extends BaseConfig { +public class OllamaConfig extends BaseAIConfig { private final String API_URL = "http://localhost:11434"; diff --git a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/openai/OpenaiConfig.java b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/openai/OpenaiConfig.java index 7779c24cc6..009ebe8bb7 100644 --- a/hutool-ai/src/main/java/cn/hutool/v7/ai/model/openai/OpenaiConfig.java +++ b/hutool-ai/src/main/java/cn/hutool/v7/ai/model/openai/OpenaiConfig.java @@ -17,7 +17,7 @@ package cn.hutool.v7.ai.model.openai; import cn.hutool.v7.ai.Models; -import cn.hutool.v7.ai.core.BaseConfig; +import cn.hutool.v7.ai.core.BaseAIConfig; /** * openai配置类,初始化API接口地址,设置默认的模型 @@ -25,7 +25,7 @@ import cn.hutool.v7.ai.core.BaseConfig; * @author elichow * @since 6.0.0 */ -public class OpenaiConfig extends BaseConfig { +public class OpenaiConfig extends BaseAIConfig { private final String API_URL = "https://api.openai.com/v1"; diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/net/url/UrlUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/net/url/UrlUtil.java index a1f26a8db5..2058ee6d11 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/net/url/UrlUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/net/url/UrlUtil.java @@ -662,8 +662,7 @@ public class UrlUtil { try { conn = url.openConnection(); useCachesIfNecessary(conn); - if (conn instanceof HttpURLConnection) { - final HttpURLConnection httpCon = (HttpURLConnection) conn; + if (conn instanceof HttpURLConnection httpCon) { httpCon.setRequestMethod("HEAD"); } return conn.getContentLengthLong(); @@ -686,4 +685,73 @@ public class UrlUtil { public static void useCachesIfNecessary(final URLConnection con) { con.setUseCaches(con.getClass().getSimpleName().startsWith("JNLP")); } + + /** + * 将表单数据加到URL中(用于GET表单提交) + * 表单的键值对会被url编码,但是url中原参数不会被编码 + * 且对form参数进行 FormUrlEncoded ,x-www-form-urlencoded模式,此模式下空格会编码为'+' + * + * @param url URL + * @param form 表单数据 + * @param charset 编码 null表示不encode键值对 + * @return 合成后的URL + */ + public static String urlWithFormUrlEncoded(final String url, final Map form, final Charset charset) { + return urlWithForm(url, form, charset, true); + } + + /** + * 将表单数据加到URL中(用于GET表单提交)
+ * 表单的键值对会被url编码,但是url中原参数不会被编码 + * + * @param url URL + * @param form 表单数据 + * @param charset 编码 + * @param isEncodeParams 是否对键和值做转义处理 + * @return 合成后的URL + */ + public static String urlWithForm(final String url, final Map form, final Charset charset, final boolean isEncodeParams) { + return urlWithForm(url, UrlQueryUtil.toQuery(form, null), charset, isEncodeParams); + } + + /** + * 将表单数据字符串加到URL中(用于GET表单提交) + * + * @param url URL + * @param queryString 表单数据字符串 + * @param charset 编码 + * @param isEncode 是否对键和值做转义处理 + * @return 拼接后的字符串 + */ + public static String urlWithForm(final String url, final String queryString, final Charset charset, final boolean isEncode) { + if (StrUtil.isBlank(queryString)) { + // 无额外参数 + if (StrUtil.contains(url, '?')) { + // url中包含参数 + return isEncode ? UrlQueryUtil.encodeQuery(url, charset) : url; + } + return url; + } + + // 始终有参数 + final StringBuilder urlBuilder = new StringBuilder(url.length() + queryString.length() + 16); + final int qmIndex = url.indexOf('?'); + if (qmIndex > 0) { + // 原URL带参数,则对这部分参数单独编码(如果选项为进行编码) + urlBuilder.append(isEncode ? UrlQueryUtil.encodeQuery(url, charset) : url); + if (!StrUtil.endWith(url, '&')) { + // 已经带参数的情况下追加参数 + urlBuilder.append('&'); + } + } else { + // 原url无参数,则不做编码 + urlBuilder.append(url); + if (qmIndex < 0) { + // 无 '?' 追加之 + urlBuilder.append('?'); + } + } + urlBuilder.append(isEncode ? UrlQueryUtil.encodeQuery(queryString, charset) : queryString); + return urlBuilder.toString(); + } } diff --git a/hutool-core/src/main/java/cn/hutool/v7/core/text/CharSequenceUtil.java b/hutool-core/src/main/java/cn/hutool/v7/core/text/CharSequenceUtil.java index 15859344c4..e443dd4a02 100644 --- a/hutool-core/src/main/java/cn/hutool/v7/core/text/CharSequenceUtil.java +++ b/hutool-core/src/main/java/cn/hutool/v7/core/text/CharSequenceUtil.java @@ -3943,9 +3943,21 @@ public class CharSequenceUtil extends StrValidator { * @return StringBuilder对象 */ public static StringBuilder builder(final CharSequence... strs) { + return builder(Function.identity(), strs); + } + + /** + * 创建StringBuilder对象 + * + * @param strEditor 编辑器,用于对每个字符串进行编辑 + * @param strs 待处理的字符串列表 + * @return StringBuilder对象 + * @since 5.8.42 + */ + public static StringBuilder builder(Function strEditor, final CharSequence... strs){ final StringBuilder sb = new StringBuilder(); for (final CharSequence str : strs) { - sb.append(str); + sb.append(strEditor.apply( str)); } return sb; } diff --git a/hutool-core/src/test/java/cn/hutool/v7/core/net/URLUtilTest.java b/hutool-core/src/test/java/cn/hutool/v7/core/net/URLUtilTest.java index ff1cb7657f..815a75f869 100644 --- a/hutool-core/src/test/java/cn/hutool/v7/core/net/URLUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/v7/core/net/URLUtilTest.java @@ -17,11 +17,15 @@ package cn.hutool.v7.core.net; import cn.hutool.v7.core.net.url.UrlUtil; +import cn.hutool.v7.core.util.CharsetUtil; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import java.net.MalformedURLException; import java.net.URI; import java.net.URL; +import java.util.LinkedHashMap; +import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -107,4 +111,28 @@ public class URLUtilTest { final URI resolve = uri.resolve("."); assertEquals("/Uploads/20240601/", resolve.toString()); } + + @Test + public void urlWithFormTest() { + final Map param = new LinkedHashMap<>(); + param.put("AccessKeyId", "123"); + param.put("Action", "DescribeDomainRecords"); + param.put("Format", "date"); + param.put("DomainName", "lesper.cn"); // 域名地址 + param.put("SignatureMethod", "POST"); + param.put("SignatureNonce", "123"); + param.put("SignatureVersion", "4.3.1"); + param.put("Timestamp", 123432453); + param.put("Version", "1.0"); + + String urlWithForm = UrlUtil.urlWithForm("http://api.hutool.cn/login?type=aaa", param, CharsetUtil.UTF_8, false); + Assertions.assertEquals( + "http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0", + urlWithForm); + + urlWithForm = UrlUtil.urlWithForm("http://api.hutool.cn/login?type=aaa", param, CharsetUtil.UTF_8, false); + Assertions.assertEquals( + "http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0", + urlWithForm); + } } diff --git a/hutool-http/src/main/java/cn/hutool/v7/http/HttpGlobalConfig.java b/hutool-http/src/main/java/cn/hutool/v7/http/HttpGlobalConfig.java index 7c2fc318d8..c72b305317 100644 --- a/hutool-http/src/main/java/cn/hutool/v7/http/HttpGlobalConfig.java +++ b/hutool-http/src/main/java/cn/hutool/v7/http/HttpGlobalConfig.java @@ -18,6 +18,7 @@ package cn.hutool.v7.http; import cn.hutool.v7.core.util.RandomUtil; +import java.io.Serial; import java.io.Serializable; import java.net.HttpURLConnection; @@ -28,6 +29,7 @@ import java.net.HttpURLConnection; * @since 4.6.2 */ public class HttpGlobalConfig implements Serializable { + @Serial private static final long serialVersionUID = 1L; /** @@ -168,7 +170,7 @@ public class HttpGlobalConfig implements Serializable { /** * 是否信任所有Host
- * 见:https://github.com/chinabugotech/hutool/issues/2042
+ * 见:issue#2042
* * @param customTrustAnyHost 如果设置为{@code false},则按照JDK默认验证机制,验证目标服务器的证书host和请求host是否一致,{@code true}表示不验证。 * @since 5.8.27 diff --git a/hutool-http/src/main/java/cn/hutool/v7/http/HttpUtil.java b/hutool-http/src/main/java/cn/hutool/v7/http/HttpUtil.java index 06e6d9d65e..b097a51291 100644 --- a/hutool-http/src/main/java/cn/hutool/v7/http/HttpUtil.java +++ b/hutool-http/src/main/java/cn/hutool/v7/http/HttpUtil.java @@ -19,7 +19,6 @@ package cn.hutool.v7.http; import cn.hutool.v7.core.collection.CollUtil; import cn.hutool.v7.core.map.CaseInsensitiveMap; import cn.hutool.v7.core.map.MapUtil; -import cn.hutool.v7.core.net.url.UrlQueryUtil; import cn.hutool.v7.core.text.StrUtil; import cn.hutool.v7.http.client.ClientConfig; import cn.hutool.v7.http.client.Request; @@ -208,75 +207,6 @@ public class HttpUtil { return ClientEngineFactory.getEngine().send(request); } - /** - * 将表单数据加到URL中(用于GET表单提交) - * 表单的键值对会被url编码,但是url中原参数不会被编码 - * 且对form参数进行 FormUrlEncoded ,x-www-form-urlencoded模式,此模式下空格会编码为'+' - * - * @param url URL - * @param form 表单数据 - * @param charset 编码 null表示不encode键值对 - * @return 合成后的URL - */ - public static String urlWithFormUrlEncoded(final String url, final Map form, final Charset charset) { - return urlWithForm(url, form, charset, true); - } - - /** - * 将表单数据加到URL中(用于GET表单提交)
- * 表单的键值对会被url编码,但是url中原参数不会被编码 - * - * @param url URL - * @param form 表单数据 - * @param charset 编码 - * @param isEncodeParams 是否对键和值做转义处理 - * @return 合成后的URL - */ - public static String urlWithForm(final String url, final Map form, final Charset charset, final boolean isEncodeParams) { - return urlWithForm(url, UrlQueryUtil.toQuery(form, null), charset, isEncodeParams); - } - - /** - * 将表单数据字符串加到URL中(用于GET表单提交) - * - * @param url URL - * @param queryString 表单数据字符串 - * @param charset 编码 - * @param isEncode 是否对键和值做转义处理 - * @return 拼接后的字符串 - */ - public static String urlWithForm(final String url, final String queryString, final Charset charset, final boolean isEncode) { - if (StrUtil.isBlank(queryString)) { - // 无额外参数 - if (StrUtil.contains(url, '?')) { - // url中包含参数 - return isEncode ? UrlQueryUtil.encodeQuery(url, charset) : url; - } - return url; - } - - // 始终有参数 - final StringBuilder urlBuilder = new StringBuilder(url.length() + queryString.length() + 16); - final int qmIndex = url.indexOf('?'); - if (qmIndex > 0) { - // 原URL带参数,则对这部分参数单独编码(如果选项为进行编码) - urlBuilder.append(isEncode ? UrlQueryUtil.encodeQuery(url, charset) : url); - if (!StrUtil.endWith(url, '&')) { - // 已经带参数的情况下追加参数 - urlBuilder.append('&'); - } - } else { - // 原url无参数,则不做编码 - urlBuilder.append(url); - if (qmIndex < 0) { - // 无 '?' 追加之 - urlBuilder.append('?'); - } - } - urlBuilder.append(isEncode ? UrlQueryUtil.encodeQuery(queryString, charset) : queryString); - return urlBuilder.toString(); - } - /** * 创建客户端引擎 * diff --git a/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/jdk11/Jdk11ClientEngine.java b/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/jdk11/Jdk11ClientEngine.java index d4a7f902ae..bed574c64d 100644 --- a/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/jdk11/Jdk11ClientEngine.java +++ b/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/jdk11/Jdk11ClientEngine.java @@ -28,7 +28,6 @@ import cn.hutool.v7.http.ssl.SSLInfo; import java.io.IOException; import java.io.InputStream; -import java.net.ProxySelector; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; diff --git a/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/BasicProxyAuthenticator.java b/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/BasicProxyAuthenticator.java index eb2d0ff4fc..d507b14428 100644 --- a/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/BasicProxyAuthenticator.java +++ b/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/BasicProxyAuthenticator.java @@ -30,19 +30,9 @@ import java.net.PasswordAuthentication; * * @author Looly * @since 6.0.0 + * @param auth 账号密码 */ -public class BasicProxyAuthenticator implements Authenticator { - - private final PasswordAuthentication auth; - - /** - * 构造 - * - * @param passwordAuthentication 账号密码对 - */ - public BasicProxyAuthenticator(final PasswordAuthentication passwordAuthentication) { - auth = passwordAuthentication; - } +public record BasicProxyAuthenticator(PasswordAuthentication auth) implements Authenticator { @Override public Request authenticate(final Route route, final Response response) { diff --git a/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/CookieJarImpl.java b/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/CookieJarImpl.java index 8f78d7a88e..3ac73cbdb3 100644 --- a/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/CookieJarImpl.java +++ b/hutool-http/src/main/java/cn/hutool/v7/http/client/engine/okhttp/CookieJarImpl.java @@ -31,26 +31,17 @@ import java.util.List; * * @author Looly * @since 6.0.0 + * @param cookieStore Cookie存储器,用于自定义Cookie存储实现 */ -public class CookieJarImpl implements CookieJar { - - private final CookieStoreSpi cookieStore; - - /** - * 构造 - * - * @param cookieStore Cookie存储器,用于自定义Cookie存储实现 - */ - public CookieJarImpl(final CookieStoreSpi cookieStore) { - this.cookieStore = cookieStore; - } +public record CookieJarImpl(CookieStoreSpi cookieStore) implements CookieJar { /** * 获取Cookie存储器 * * @return Cookie存储器 */ - public CookieStoreSpi getCookieStore() { + @Override + public CookieStoreSpi cookieStore() { return this.cookieStore; } @@ -59,14 +50,14 @@ public class CookieJarImpl implements CookieJar { final List cookieSpis = this.cookieStore.get(httpUrl.uri()); final List cookies = new ArrayList<>(cookieSpis.size()); for (final CookieSpi cookieSpi : cookieSpis) { - cookies.add(((OkCookie)cookieSpi).getRaw()); + cookies.add(((OkCookie) cookieSpi).getRaw()); } return cookies; } @Override public void saveFromResponse(final HttpUrl httpUrl, final List list) { - if(CollUtil.isEmpty(list)){ + if (CollUtil.isEmpty(list)) { return; } diff --git a/hutool-http/src/test/java/cn/hutool/v7/http/HttpUtilTest.java b/hutool-http/src/test/java/cn/hutool/v7/http/HttpUtilTest.java index 7668e04722..87cec6a1b0 100644 --- a/hutool-http/src/test/java/cn/hutool/v7/http/HttpUtilTest.java +++ b/hutool-http/src/test/java/cn/hutool/v7/http/HttpUtilTest.java @@ -28,7 +28,6 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -131,30 +130,6 @@ public class HttpUtilTest { Console.log(body); } - @Test - public void urlWithFormTest() { - final Map param = new LinkedHashMap<>(); - param.put("AccessKeyId", "123"); - param.put("Action", "DescribeDomainRecords"); - param.put("Format", "date"); - param.put("DomainName", "lesper.cn"); // 域名地址 - param.put("SignatureMethod", "POST"); - param.put("SignatureNonce", "123"); - param.put("SignatureVersion", "4.3.1"); - param.put("Timestamp", 123432453); - param.put("Version", "1.0"); - - String urlWithForm = HttpUtil.urlWithForm("http://api.hutool.cn/login?type=aaa", param, CharsetUtil.UTF_8, false); - Assertions.assertEquals( - "http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0", - urlWithForm); - - urlWithForm = HttpUtil.urlWithForm("http://api.hutool.cn/login?type=aaa", param, CharsetUtil.UTF_8, false); - Assertions.assertEquals( - "http://api.hutool.cn/login?type=aaa&AccessKeyId=123&Action=DescribeDomainRecords&Format=date&DomainName=lesper.cn&SignatureMethod=POST&SignatureNonce=123&SignatureVersion=4.3.1&Timestamp=123432453&Version=1.0", - urlWithForm); - } - @Test @Disabled public void getWeixinTest(){ diff --git a/hutool-http/src/test/java/cn/hutool/v7/http/Issue3536Test.java b/hutool-http/src/test/java/cn/hutool/v7/http/Issue3536Test.java index a6bb1d58bd..73cd54f265 100644 --- a/hutool-http/src/test/java/cn/hutool/v7/http/Issue3536Test.java +++ b/hutool-http/src/test/java/cn/hutool/v7/http/Issue3536Test.java @@ -18,6 +18,7 @@ package cn.hutool.v7.http; import cn.hutool.v7.core.net.url.UrlQuery; import cn.hutool.v7.core.net.url.UrlQueryUtil; +import cn.hutool.v7.core.net.url.UrlUtil; import cn.hutool.v7.core.util.CharsetUtil; import org.junit.jupiter.api.Test; @@ -35,7 +36,7 @@ public class Issue3536Test { paramMap.put("redirect_uri", "https://api.hutool.cn/v1/test"); paramMap.put("scope", "a,b,c你"); - final String s = HttpUtil.urlWithFormUrlEncoded(url, paramMap, CharsetUtil.UTF_8); + final String s = UrlUtil.urlWithFormUrlEncoded(url, paramMap, CharsetUtil.UTF_8); assertEquals("https://hutool.cn/test?scope=a,b,c%E4%BD%A0&redirect_uri=https://api.hutool.cn/v1/test", s); } diff --git a/hutool-http/src/test/java/cn/hutool/v7/http/IssueIAFKWPTest.java b/hutool-http/src/test/java/cn/hutool/v7/http/IssueIAFKWPTest.java index 9de765b569..768150c50e 100644 --- a/hutool-http/src/test/java/cn/hutool/v7/http/IssueIAFKWPTest.java +++ b/hutool-http/src/test/java/cn/hutool/v7/http/IssueIAFKWPTest.java @@ -18,6 +18,7 @@ package cn.hutool.v7.http; import cn.hutool.v7.core.collection.ListUtil; import cn.hutool.v7.core.net.url.UrlQuery; +import cn.hutool.v7.core.net.url.UrlUtil; import cn.hutool.v7.core.util.CharsetUtil; import cn.hutool.v7.json.JSONObject; import cn.hutool.v7.json.JSONUtil; @@ -39,12 +40,12 @@ public class IssueIAFKWPTest { // form-url-encoded模式下所有字符转义 String build = UrlQuery.of(params, UrlQuery.EncodeMode.FORM_URL_ENCODED).build(CharsetUtil.UTF_8); - String s = HttpUtil.urlWithForm("https://hutool.cn", build, CharsetUtil.UTF_8, false); + String s = UrlUtil.urlWithForm("https://hutool.cn", build, CharsetUtil.UTF_8, false); Assertions.assertEquals("https://hutool.cn?query=%7B%22fields%22%3A%5B%221%22%2C%222%22%2C%22good%22%5D%7D", s); // 标准模式下只转义特定字符 build = UrlQuery.of(params, UrlQuery.EncodeMode.NORMAL).build(CharsetUtil.UTF_8); - s = HttpUtil.urlWithForm("https://hutool.cn", build, CharsetUtil.UTF_8, false); + s = UrlUtil.urlWithForm("https://hutool.cn", build, CharsetUtil.UTF_8, false); Assertions.assertEquals("https://hutool.cn?query=%7B%22fields%22:%5B%221%22,%222%22,%22good%22%5D%7D", s); } }