mirror of
https://gitee.com/dromara/hutool.git
synced 2025-11-24 08:33:22 +08:00
Compare commits
2 Commits
9ab781090d
...
c9c4cf5ae3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9c4cf5ae3 | ||
|
|
0b2565f6aa |
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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实例
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<String, Object> 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<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()))) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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请求
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建客户端引擎
|
||||
*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<CookieSpi> cookieSpis = this.cookieStore.get(httpUrl.uri());
|
||||
final List<Cookie> 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<Cookie> list) {
|
||||
if(CollUtil.isEmpty(list)){
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(){
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user