Compare commits

...

2 Commits

Author SHA1 Message Date
Looly
c9c4cf5ae3 fix test 2025-10-23 02:53:41 +08:00
Looly
0b2565f6aa add proxy support 2025-10-23 02:38:54 +08:00
24 changed files with 241 additions and 227 deletions

View File

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

View File

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

View File

@@ -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实例
*

View File

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

View File

@@ -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);
}
.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())
.header(HeaderName.CONTENT_TYPE, ContentType.JSON.getValue())
.header(HeaderName.ACCEPT, ContentType.JSON.getValue())
.bearerAuth(config.getApiKey())
.body(paramJson)
.send();
} catch (final AIException e) {
throw new AIException("Failed to send POST request" + e.getMessage(), e);
}
.send(this.clientEngine);
}
/**
@@ -103,19 +90,12 @@ public class BaseAIService {
* @return 请求响应
*/
protected Response sendFormData(final String endpoint, final Map<String, Object> paramMap) {
//链式构建请求
try {
//设置超时3分钟
HttpGlobalConfig.setTimeout(config.getTimeout());
return HttpUtil.createPost(config.getApiUrl() + endpoint)
//form表单中有file对象会自动将文件编码为 multipart/form-data 格式。不需要设置
// .header(HeaderName.CONTENT_TYPE, "multipart/form-data")
.header(HeaderName.AUTHORIZATION, "Bearer " + config.getApiKey())
.bearerAuth(config.getApiKey())
.form(paramMap)
.send();
} catch (final AIException e) {
throw new AIException("Failed to send POST request" + e.getMessage(), e);
}
.send(this.clientEngine);
}
/**
@@ -126,41 +106,22 @@ public class BaseAIService {
* @param callback 流式数据回调函数
*/
protected void sendPostStream(final String endpoint, final Map<String, Object> paramMap, final Consumer<String> 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()))) {
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();
}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,7 +92,7 @@ class OpenaiServiceTest {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.GPT_4O_MINI.getModel()).build(), OpenaiService.class);
String prompt = "图片上有些什么?";
List<String> images = Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800");
List<String> images = List.of("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544\",\"https://img2.baidu.com/it/u=1682510685,1244554634&fm=253&fmt=auto&app=138&f=JPEG?w=803&h=800");
// 使用AtomicBoolean作为结束标志
AtomicBoolean isDone = new AtomicBoolean(false);
@@ -155,10 +155,11 @@ class OpenaiServiceTest {
void textToSpeech() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.TTS_1_HD.getModel()).build(), OpenaiService.class);
final InputStream inputStream = openaiService.textToSpeech("万里山河一夜白,\n" +
"千峰尽染玉龙哀。\n" +
"长风卷起琼花碎,\n" +
"直上九霄揽月来。", OpenaiCommon.OpenaiSpeech.NOVA);
final InputStream inputStream = openaiService.textToSpeech("""
万里山河一夜白,
千峰尽染玉龙哀。
长风卷起琼花碎,
直上九霄揽月来。""", OpenaiCommon.OpenaiSpeech.NOVA);
final String filePath = "your filePath";
final Path path = Paths.get(filePath);
@@ -199,7 +200,7 @@ class OpenaiServiceTest {
void moderations() {
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
.setApiKey(key).setModel(Models.Openai.OMNI_MODERATION_LATEST.getModel()).build(), OpenaiService.class);
final String moderations = openaiService.moderations("你要杀人", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
final String moderations = openaiService.moderations("你要玩游戏", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
assertNotNull(moderations);
}

View File

@@ -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<String, Object> form, final Charset charset) {
return urlWithForm(url, form, charset, true);
}
/**
* 将表单数据加到URL中用于GET表单提交<br>
* 表单的键值对会被url编码但是url中原参数不会被编码
*
* @param url URL
* @param form 表单数据
* @param charset 编码
* @param isEncodeParams 是否对键和值做转义处理
* @return 合成后的URL
*/
public static String urlWithForm(final String url, final Map<String, Object> 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();
}
}

View File

@@ -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<CharSequence, CharSequence> strEditor, final CharSequence... strs){
final StringBuilder sb = new StringBuilder();
for (final CharSequence str : strs) {
sb.append(str);
sb.append(strEditor.apply( str));
}
return sb;
}

View File

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

View File

@@ -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<br>
* 见https://github.com/chinabugotech/hutool/issues/2042<br>
* 见:<a href="https://github.com/chinabugotech/hutool/issues/2042">issue#2042</a><br>
*
* @param customTrustAnyHost 如果设置为{@code false}则按照JDK默认验证机制验证目标服务器的证书host和请求host是否一致{@code true}表示不验证。
* @since 5.8.27

View File

@@ -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<String, Object> form, final Charset charset) {
return urlWithForm(url, form, charset, true);
}
/**
* 将表单数据加到URL中用于GET表单提交<br>
* 表单的键值对会被url编码但是url中原参数不会被编码
*
* @param url URL
* @param form 表单数据
* @param charset 编码
* @param isEncodeParams 是否对键和值做转义处理
* @return 合成后的URL
*/
public static String urlWithForm(final String url, final Map<String, Object> 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();
}
/**
* 创建客户端引擎
*

View File

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

View File

@@ -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) {

View File

@@ -31,26 +31,17 @@ import java.util.List;
*
* @author Looly
* @since 6.0.0
*/
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;
}

View File

@@ -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<String, Object> 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(){

View File

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

View File

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