mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-08-18 20:38:02 +08:00
Compare commits
45 Commits
6fb471f2f9
...
v6-dev
Author | SHA1 | Date | |
---|---|---|---|
|
fa8f1f2091 | ||
|
f5910f9db4 | ||
|
6f7d4f1402 | ||
|
b05f45a76f | ||
|
9fbf8d85d1 | ||
|
667915d05c | ||
|
6574dd010b | ||
|
90b83d16a5 | ||
|
8aff7bfb66 | ||
|
d2d5e3092f | ||
|
bf7777cdc9 | ||
|
c64e48e037 | ||
|
04a0b342e4 | ||
|
8be799e685 | ||
|
ea09b76332 | ||
|
8595c0a988 | ||
|
b0b44e2ec6 | ||
|
cdfdf6f5bd | ||
|
d98cded437 | ||
|
7c74c789e1 | ||
|
115b4b91c4 | ||
|
e019d98e3e | ||
|
fbd6821280 | ||
|
cf58e5e41e | ||
|
9f76238137 | ||
|
b9cedd8798 | ||
|
e91dc5b756 | ||
|
4f68e8f83e | ||
|
4aea2ef471 | ||
|
3658d9f7ca | ||
|
49e9ede07b | ||
|
58789b61f5 | ||
|
761c8c1757 | ||
|
b0e37e3ef3 | ||
|
ca4cdcb80a | ||
|
aa13f98934 | ||
|
1cfd6c7a5c | ||
|
c7c4457deb | ||
|
a0eab6fb11 | ||
|
4960c77a34 | ||
|
1011c5ec60 | ||
|
79485aca23 | ||
|
deee1f8d09 | ||
|
58d44d6086 | ||
|
581a5dd0fd |
@@ -3,11 +3,7 @@
|
||||
|
||||
-------------------------------------------------------------------------------------------------------------
|
||||
|
||||
# 6.0.0-M17 (2024-10-09)
|
||||
|
||||
### 计划实现
|
||||
* 【db 】 增加DDL封装
|
||||
* 【db 】 Entity数据量大时占用较多内存,考虑共享meta信息
|
||||
# 6.0.0-M22 (2025-05-07)
|
||||
|
||||
### ❌不兼容特性
|
||||
|
||||
|
@@ -128,18 +128,18 @@ Each module can be introduced individually, or all modules can be introduced by
|
||||
<dependency>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 🍐Gradle
|
||||
```
|
||||
implementation 'org.dromara.hutool:hutool-all:6.0.0-M22'
|
||||
implementation 'org.dromara.hutool:hutool-all:6.0.0-M23'
|
||||
```
|
||||
|
||||
## 📥Download
|
||||
|
||||
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/6.0.0-M22/)
|
||||
- [Maven Repo](https://repo1.maven.org/maven2/cn/hutool/hutool-all/6.0.0-M23/)
|
||||
|
||||
> 🔔️note:
|
||||
> Hutool 5.x supports JDK8+ and is not tested on Android platforms, and cannot guarantee that all tool classes or tool methods are available.
|
||||
|
@@ -130,21 +130,21 @@ Hutool目前主要版本4.x、5.x、6.x,选择如下:
|
||||
<dependency>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 🍐Gradle
|
||||
|
||||
```
|
||||
implementation 'org.dromara.hutool:hutool-all:6.0.0-M22'
|
||||
implementation 'org.dromara.hutool:hutool-all:6.0.0-M23'
|
||||
```
|
||||
|
||||
### 📥下载jar
|
||||
|
||||
点击以下链接,下载`hutool-all-X.X.X.jar`即可:
|
||||
|
||||
- [Maven中央库](https://repo1.maven.org/maven2/org/dromara/hutool/hutool-all/6.0.0-M22/)
|
||||
- [Maven中央库](https://repo1.maven.org/maven2/org/dromara/hutool/hutool-all/6.0.0-M23/)
|
||||
|
||||
> 🔔️注意
|
||||
> Hutool 6.x支持JDK8+,对Android平台没有测试,不能保证所有工具类或工具方法可用。
|
||||
|
@@ -1 +1 @@
|
||||
6.0.0-M22
|
||||
6.0.0-M23
|
||||
|
@@ -1 +1 @@
|
||||
var version = '6.0.0-M22'
|
||||
var version = '6.0.0-M23'
|
@@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-ai</artifactId>
|
||||
|
@@ -22,6 +22,7 @@ import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.ai.model.deepseek.DeepSeekService;
|
||||
import org.dromara.hutool.ai.model.doubao.DoubaoService;
|
||||
import org.dromara.hutool.ai.model.grok.GrokService;
|
||||
import org.dromara.hutool.ai.model.hutool.HutoolService;
|
||||
import org.dromara.hutool.ai.model.openai.OpenaiService;
|
||||
|
||||
import java.util.List;
|
||||
@@ -58,6 +59,17 @@ public class AIUtil {
|
||||
return getAIService(config, AIService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Hutool-AI服务
|
||||
*
|
||||
* @param config 创建的AI服务模型的配置
|
||||
* @return HutoolService
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public static HutoolService getHutoolService(final AIConfig config) {
|
||||
return getAIService(config, HutoolService.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取DeepSeek模型服务
|
||||
*
|
||||
|
@@ -23,6 +23,10 @@ package org.dromara.hutool.ai;
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public enum ModelName {
|
||||
/**
|
||||
* hutool
|
||||
*/
|
||||
HUTOOL("hutool"),
|
||||
/**
|
||||
* deepSeek
|
||||
*/
|
||||
|
@@ -24,6 +24,21 @@ package org.dromara.hutool.ai;
|
||||
*/
|
||||
public class Models {
|
||||
|
||||
// Hutool的模型
|
||||
public enum Hutool {
|
||||
HUTOOL("hutool");
|
||||
|
||||
private final String model;
|
||||
|
||||
Hutool(String model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
// DeepSeek的模型
|
||||
public enum DeepSeek {
|
||||
DEEPSEEK_CHAT("deepseek-chat"),
|
||||
@@ -123,7 +138,12 @@ public class Models {
|
||||
DOUBAO_VISION_LITE_32K("doubao-vision-lite-32k-241015"),
|
||||
DOUBAO_EMBEDDING_LARGE("doubao-embedding-large-text-240915"),
|
||||
DOUBAO_EMBEDDING_TEXT_240715("doubao-embedding-text-240715"),
|
||||
DOUBAO_EMBEDDING_VISION("doubao-embedding-vision-241215");
|
||||
DOUBAO_EMBEDDING_VISION("doubao-embedding-vision-241215"),
|
||||
DOUBAO_SEEDREAM_3_0_T2I("doubao-seedream-3-0-t2i-250415"),
|
||||
Doubao_Seedance_1_0_lite_t2v("doubao-seedance-1-0-lite-t2v-250428"),
|
||||
Doubao_Seedance_1_0_lite_i2v("doubao-seedance-1-0-lite-i2v-250428"),
|
||||
Wan2_1_14B_t2v("wan2-1-14b-t2v-250225"),
|
||||
Wan2_1_14B_i2v("wan2-1-14b-i2v-250225");
|
||||
|
||||
private final String model;
|
||||
|
||||
@@ -138,6 +158,23 @@ public class Models {
|
||||
|
||||
// Grok的模型
|
||||
public enum Grok {
|
||||
GROK_3_BETA_LATEST("grok-3-beta"),
|
||||
GROK_3_BETA("grok-3-beta"),
|
||||
GROK_3("grok-3-beta"),
|
||||
GROK_3_MINI_FAST_LATEST("grok-3-mini-fast-beta"),
|
||||
GROK_3_MINI_FAST_BETA("grok-3-mini-fast-beta"),
|
||||
GROK_3_MINI_FAST("grok-3-mini-fast-beta"),
|
||||
GROK_3_FAST_LATEST("grok-3-fast-beta"),
|
||||
GROK_3_FAST_BETA("grok-3-fast-beta"),
|
||||
GROK_3_FAST("grok-3-fast-beta"),
|
||||
GROK_3_MINI_LATEST("grok-3-mini-beta"),
|
||||
GROK_3_MINI_BETA("grok-3-mini-beta"),
|
||||
GROK_3_MINI("grok-3-mini-beta"),
|
||||
GROK_2_IMAGE_LATEST("grok-2-image-1212"),
|
||||
GROK_2_IMAGE("grok-2-image-1212"),
|
||||
GROK_2_IMAGE_1212("grok-2-image-1212"),
|
||||
grok_2_latest("grok-2-1212"),
|
||||
GROK_2("grok-2-1212"),
|
||||
GROK_2_1212("grok-2-1212"),
|
||||
GROK_2_VISION_1212("grok-2-vision-1212"),
|
||||
GROK_BETA("grok-beta"),
|
||||
|
@@ -110,4 +110,36 @@ public interface AIConfig {
|
||||
*/
|
||||
Map<String, Object> getAdditionalConfigMap();
|
||||
|
||||
/**
|
||||
* 设置连接超时时间
|
||||
*
|
||||
* @param timeout 连接超时时间
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void setTimeout(int timeout);
|
||||
|
||||
/**
|
||||
* 获取连接超时时间
|
||||
*
|
||||
* @return timeout
|
||||
* @since 6.0.0
|
||||
*/
|
||||
int getTimeout();
|
||||
|
||||
/**
|
||||
* 设置读取超时时间
|
||||
*
|
||||
* @param readTimeout 连接超时时间
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void setReadTimeout(int readTimeout);
|
||||
|
||||
/**
|
||||
* 获取读取超时时间
|
||||
*
|
||||
* @return readTimeout
|
||||
* @since 6.0.0
|
||||
*/
|
||||
int getReadTimeout();
|
||||
|
||||
}
|
||||
|
@@ -106,6 +106,34 @@ public class AIConfigBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置连接超时时间,不设置为默认值
|
||||
*
|
||||
* @param timeout 超时时间
|
||||
* @return config
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public synchronized AIConfigBuilder setTimout(final int timeout) {
|
||||
if (timeout > 0) {
|
||||
config.setTimeout(timeout);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置读取超时时间,不设置为默认值
|
||||
*
|
||||
* @param readTimout 取超时时间
|
||||
* @return config
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public synchronized AIConfigBuilder setReadTimout(final int readTimout) {
|
||||
if (readTimout > 0) {
|
||||
config.setReadTimeout(readTimout);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回config实例
|
||||
*
|
||||
|
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.dromara.hutool.ai.core;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 模型公共的API功能,特有的功能在model.xx.XXService下定义
|
||||
@@ -33,7 +35,25 @@ public interface AIService {
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String chat(String prompt);
|
||||
default String chat(String prompt){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话-SSE流式输出
|
||||
* @param prompt user题词
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chat(String prompt, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
chat(messages, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话
|
||||
@@ -44,4 +64,12 @@ public interface AIService {
|
||||
*/
|
||||
String chat(final List<Message> messages);
|
||||
|
||||
/**
|
||||
* 对话-SSE流式输出
|
||||
* @param messages 由目前为止的对话组成的消息列表,可以设置role,content。详细参考官方文档
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void chat(final List<Message> messages, final Consumer<String> callback);
|
||||
|
||||
}
|
||||
|
@@ -22,8 +22,15 @@ import org.dromara.hutool.http.HttpUtil;
|
||||
import org.dromara.hutool.http.client.Response;
|
||||
import org.dromara.hutool.http.meta.HeaderName;
|
||||
import org.dromara.hutool.http.meta.Method;
|
||||
import org.dromara.hutool.json.JSONUtil;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* 基础AIService,包含基公共参数和公共方法
|
||||
@@ -52,8 +59,8 @@ public class BaseAIService {
|
||||
protected Response sendGet(final String endpoint) {
|
||||
//链式构建请求
|
||||
try {
|
||||
//设置超时3分钟
|
||||
HttpGlobalConfig.setTimeout(180000);
|
||||
//设置超时
|
||||
HttpGlobalConfig.setTimeout(config.getTimeout());
|
||||
return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.GET)
|
||||
.header(HeaderName.ACCEPT, "application/json")
|
||||
.header(HeaderName.AUTHORIZATION, "Bearer " + config.getApiKey())
|
||||
@@ -73,7 +80,7 @@ public class BaseAIService {
|
||||
//链式构建请求
|
||||
try {
|
||||
//设置超时3分钟
|
||||
HttpGlobalConfig.setTimeout(180000);
|
||||
HttpGlobalConfig.setTimeout(config.getTimeout());
|
||||
return HttpUtil.createRequest(config.getApiUrl() + endpoint, Method.POST)
|
||||
.header(HeaderName.CONTENT_TYPE, "application/json")
|
||||
.header(HeaderName.ACCEPT, "application/json")
|
||||
@@ -96,7 +103,7 @@ public class BaseAIService {
|
||||
//链式构建请求
|
||||
try {
|
||||
//设置超时3分钟
|
||||
HttpGlobalConfig.setTimeout(180000);
|
||||
HttpGlobalConfig.setTimeout(config.getTimeout());
|
||||
return HttpUtil.createPost(config.getApiUrl() + endpoint)
|
||||
//form表单中有file对象会自动将文件编码为 multipart/form-data 格式。不需要设置
|
||||
// .header(HeaderName.CONTENT_TYPE, "multipart/form-data")
|
||||
@@ -107,4 +114,50 @@ public class BaseAIService {
|
||||
throw new AIException("Failed to send POST request:" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持流式返回的 POST 请求
|
||||
*
|
||||
* @param endpoint 请求地址
|
||||
* @param paramMap 请求参数
|
||||
* @param callback 流式数据回调函数
|
||||
*/
|
||||
protected void sendPostStream(final String endpoint, final Map<String, Object> paramMap, Consumer<String> callback) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// 创建连接
|
||||
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 (OutputStream os = connection.getOutputStream()) {
|
||||
String jsonInputString = JSONUtil.toJsonStr(paramMap);
|
||||
os.write(jsonInputString.getBytes());
|
||||
os.flush();
|
||||
}
|
||||
|
||||
// 读取流式响应
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
// 调用回调函数处理每一行数据
|
||||
callback.accept(line);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
callback.accept("{\"error\": \"" + e.getMessage() + "\"}");
|
||||
} finally {
|
||||
// 关闭连接
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -36,6 +36,10 @@ public class BaseConfig implements AIConfig {
|
||||
protected volatile String model;
|
||||
//动态扩展字段
|
||||
protected Map<String, Object> additionalConfig = new SafeConcurrentHashMap<>();
|
||||
//连接超时时间
|
||||
protected volatile int timeout = 180000;
|
||||
//读取超时时间
|
||||
protected volatile int readTimeout = 300000;
|
||||
|
||||
@Override
|
||||
public void setApiKey(final String apiKey) {
|
||||
@@ -82,4 +86,23 @@ public class BaseConfig implements AIConfig {
|
||||
return new SafeConcurrentHashMap<>(additionalConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getTimeout() {
|
||||
return timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setTimeout(final int timeout) {
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getReadTimeout() {
|
||||
return readTimeout;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadTimeout(final int readTimeout) {
|
||||
this.readTimeout = readTimeout;
|
||||
}
|
||||
}
|
||||
|
@@ -18,6 +18,8 @@ package org.dromara.hutool.ai.model.deepseek;
|
||||
|
||||
import org.dromara.hutool.ai.core.AIService;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* deepSeek支持的扩展接口
|
||||
*
|
||||
@@ -35,6 +37,14 @@ public interface DeepSeekService extends AIService {
|
||||
*/
|
||||
String beta(String prompt);
|
||||
|
||||
/**
|
||||
* 模型beta功能-SSE流式输出
|
||||
* @param prompt 题词
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void beta(String prompt, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 列出所有模型列表
|
||||
*
|
||||
|
@@ -19,13 +19,14 @@ package org.dromara.hutool.ai.model.deepseek;
|
||||
import org.dromara.hutool.ai.core.AIConfig;
|
||||
import org.dromara.hutool.ai.core.BaseAIService;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.http.client.Response;
|
||||
import org.dromara.hutool.json.JSONUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* DeepSeek服务,AI具体功能的实现
|
||||
@@ -54,15 +55,6 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final String prompt) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
final String paramJson = buildChatRequestBody(messages);
|
||||
@@ -70,6 +62,12 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(List<Message> messages, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "deepseek-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String beta(final String prompt) {
|
||||
final String paramJson = buildBetaRequestBody(prompt);
|
||||
@@ -77,6 +75,12 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beta(String prompt, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildBetaStreamRequestBody(prompt);
|
||||
ThreadUtil.newThread(() -> sendPostStream(BETA_ENDPOINT, paramMap, callback::accept), "deepseek-beta-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String models() {
|
||||
final Response response = sendGet(MODELS_ENDPOINT);
|
||||
@@ -101,6 +105,19 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建chatStream请求体
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
// 构建beta请求体
|
||||
private String buildBetaRequestBody(final String prompt) {
|
||||
// 定义消息结构
|
||||
@@ -114,4 +131,17 @@ public class DeepSeekServiceImpl extends BaseAIService implements DeepSeekServic
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建betaStream请求体
|
||||
private Map<String, Object> buildBetaStreamRequestBody(final String prompt) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -19,7 +19,9 @@ package org.dromara.hutool.ai.model.doubao;
|
||||
import org.dromara.hutool.ai.core.AIService;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* doubao支持的扩展接口
|
||||
@@ -29,17 +31,6 @@ import java.util.List;
|
||||
*/
|
||||
public interface DoubaoService extends AIService {
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
@@ -52,9 +43,43 @@ public interface DoubaoService extends AIService {
|
||||
return chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback) {
|
||||
chatVision(prompt, images, DoubaoCommon.DoubaoVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 提问
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 创建视频生成任务
|
||||
* 注意:调用该方法时,配置config中的model为您创建的推理接入点(Endpoint)ID。详细参考官方文档
|
||||
* 注意:调用该方法时,配置config中的model为生成视频的模型或者您创建的推理接入点(Endpoint)ID。详细参考官方文档
|
||||
*
|
||||
* @param text 文本提示词
|
||||
* @param image 图片/或者图片Base64编码图片(URI形式)
|
||||
@@ -114,6 +139,15 @@ public interface DoubaoService extends AIService {
|
||||
*/
|
||||
String botsChat(final List<Message> messages);
|
||||
|
||||
/**
|
||||
* 应用(Bot)-SSE流式输出 config中model设置为您创建的应用ID
|
||||
*
|
||||
* @param messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void botsChat(final List<Message> messages, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 分词:可以将文本转换为模型可理解的 token id,并返回文本的 tokens 数量、token id、 token 在原始文本中的偏移量等信息
|
||||
*
|
||||
@@ -132,7 +166,12 @@ public interface DoubaoService extends AIService {
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String batchChat(String prompt);
|
||||
default String batchChat(String prompt){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return batchChat(messages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量推理 Chat
|
||||
@@ -179,7 +218,26 @@ public interface DoubaoService extends AIService {
|
||||
* @return AI的回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String chatContext(String prompt, String contextId);
|
||||
default String chatContext(String prompt, String contextId){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user", prompt));
|
||||
return chatContext(messages, contextId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下文缓存对话-SSE流式输出
|
||||
* 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model
|
||||
*
|
||||
* @param prompt 对话的内容题词
|
||||
* @param contextId 创建上下文缓存后获取的缓存id
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatContext(String prompt, String contextId, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user", prompt));
|
||||
chatContext(messages, contextId, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上下文缓存对话: 向大模型发起带上下文缓存的请求
|
||||
@@ -192,4 +250,24 @@ public interface DoubaoService extends AIService {
|
||||
*/
|
||||
String chatContext(final List<Message> messages, String contextId);
|
||||
|
||||
/**
|
||||
* 上下文缓存对话-SSE流式输出
|
||||
* 注意:配置config中的model可以为您创建的推理接入点(Endpoint)ID,也可以是支持chat的model
|
||||
*
|
||||
* @param messages 对话的信息 不支持最后一个元素的role设置为assistant。如使用session 缓存(mode设置为session)传入最新一轮对话的信息,无需传入历史信息
|
||||
* @param contextId 创建上下文缓存后获取的缓存id
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void chatContext(final List<Message> messages, String contextId, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 文生图
|
||||
* 请设置config中model为支持图片功能的模型,目前支持Doubao-Seedream-3.0-t2i
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return 包含生成图片的url
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String imagesGenerations(String prompt);
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import org.dromara.hutool.ai.core.AIConfig;
|
||||
import org.dromara.hutool.ai.core.BaseAIService;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.text.StrUtil;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.http.client.Response;
|
||||
import org.dromara.hutool.json.JSONUtil;
|
||||
|
||||
@@ -27,6 +28,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Doubao服务,AI具体功能的实现
|
||||
@@ -54,21 +56,14 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
private final String CHAT_CONTEXT = "/context/chat/completions";
|
||||
//创建视频生成任务
|
||||
private final String CREATE_VIDEO = "/contents/generations/tasks";
|
||||
//文生图
|
||||
private final String IMAGES_GENERATIONS = "/images/generations";
|
||||
|
||||
public DoubaoServiceImpl(final AIConfig config) {
|
||||
//初始化doubao客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(String prompt) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
@@ -76,6 +71,12 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(List<Message> messages, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
@@ -83,6 +84,12 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "doubao-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String videoTasks(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams) {
|
||||
String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams);
|
||||
@@ -118,6 +125,12 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void botsChat(List<Message> messages, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildBotsChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(BOTS_CHAT, paramMap, callback::accept), "doubao-botsChat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String tokenization(String[] text) {
|
||||
String paramJson = buildTokenizationRequestBody(text);
|
||||
@@ -125,15 +138,6 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String batchChat(String prompt) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return batchChat(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String batchChat(final List<Message> messages) {
|
||||
String paramJson = buildBatchChatRequestBody(messages);
|
||||
@@ -148,14 +152,6 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatContext(String prompt, String contextId) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user", prompt));
|
||||
return chatContext(messages, contextId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatContext(final List<Message> messages, String contextId) {
|
||||
String paramJson = buildChatContentRequestBody(messages, contextId);
|
||||
@@ -163,6 +159,19 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatContext(List<Message> messages, String contextId, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatContentStreamRequestBody(messages, contextId);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_CONTEXT, paramMap, callback::accept), "doubao-chatContext-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
Response response = sendPost(IMAGES_GENERATIONS, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
@@ -175,6 +184,19 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
// 构建chatStream请求体
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
@@ -206,6 +228,37 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建文本向量化请求体
|
||||
private String buildEmbeddingTextRequestBody(String[] input) {
|
||||
//使用JSON工具
|
||||
@@ -253,6 +306,10 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return buildChatRequestBody(messages);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildBotsChatStreamRequestBody(final List<Message> messages) {
|
||||
return buildChatStreamRequestBody(messages);
|
||||
}
|
||||
|
||||
//构建分词请求体
|
||||
private String buildTokenizationRequestBody(String[] text) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
@@ -266,6 +323,10 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return buildChatRequestBody(messages);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildBatchChatStreamRequestBody(final List<Message> messages) {
|
||||
return buildChatStreamRequestBody(messages);
|
||||
}
|
||||
|
||||
//构建创建上下文缓存请求体
|
||||
private String buildCreateContextRequest(final List<Message> messages, String mode) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
@@ -291,6 +352,19 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatContentStreamRequestBody(final List<Message> messages, String contextId) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("context_id", contextId);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建创建视频任务请求体
|
||||
private String buildGenerationsTasksRequestBody(String text, String image, final List<DoubaoCommon.DoubaoVideo> videoParams) {
|
||||
//使用JSON工具
|
||||
@@ -306,7 +380,7 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
content.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isNotBlank(image)) {
|
||||
if (!StrUtil.isBlank(image)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
@@ -351,4 +425,15 @@ public class DoubaoServiceImpl extends BaseAIService implements DoubaoService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -17,8 +17,11 @@
|
||||
package org.dromara.hutool.ai.model.grok;
|
||||
|
||||
import org.dromara.hutool.ai.core.AIService;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* grok支持的扩展接口
|
||||
@@ -36,7 +39,48 @@ public interface GrokService extends AIService {
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String message(String prompt, int maxToken);
|
||||
default String message(String prompt, int maxToken){
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return message(messages, maxToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息回复-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param maxToken 最大token
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void message(String prompt, int maxToken, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
message(messages, maxToken, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息回复
|
||||
*
|
||||
* @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @param maxToken 最大token
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String message(List<Message> messages, int maxToken);
|
||||
|
||||
/**
|
||||
* 创建消息回复-SSE流式输出
|
||||
*
|
||||
* @param messages messages 由对话组成的消息列表。如系统人设,背景信息等,用户自定义的信息
|
||||
* @param maxToken 最大token
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void message(List<Message> messages, int maxToken, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
@@ -49,6 +93,17 @@ public interface GrokService extends AIService {
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
@@ -61,6 +116,18 @@ public interface GrokService extends AIService {
|
||||
return chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback){
|
||||
chatVision(prompt, images, GrokCommon.GrokVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出所有model列表
|
||||
*
|
||||
@@ -112,4 +179,14 @@ public interface GrokService extends AIService {
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String deferredCompletion(String requestId);
|
||||
|
||||
/**
|
||||
* 文生图
|
||||
* 请设置config中model为支持图片功能的模型,目前支持GROK_2_IMAGE
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return 包含生成图片的url
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String imagesGenerations(String prompt);
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ package org.dromara.hutool.ai.model.grok;
|
||||
import org.dromara.hutool.ai.core.AIConfig;
|
||||
import org.dromara.hutool.ai.core.BaseAIService;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.http.client.Response;
|
||||
import org.dromara.hutool.json.JSONUtil;
|
||||
|
||||
@@ -26,6 +27,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Grok服务,AI具体功能的实现
|
||||
@@ -47,21 +49,14 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
private final String TOKENIZE_TEXT = "/tokenize-text";
|
||||
//获取延迟对话
|
||||
private final String DEFERRED_COMPLETION = "/chat/deferred-completion";
|
||||
//文生图
|
||||
private final String IMAGES_GENERATIONS = "/images/generations";
|
||||
|
||||
public GrokServiceImpl(final AIConfig config) {
|
||||
//初始化grok客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(String prompt) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
@@ -70,16 +65,24 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public String message(String prompt, int maxToken) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
public void chat(List<Message> messages, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String message(final List<Message> messages, int maxToken) {
|
||||
String paramJson = buildMessageRequestBody(messages, maxToken);
|
||||
Response response = sendPost(MESSAGES, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void message(List<Message> messages, int maxToken, final Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildMessageStreamRequestBody(messages, maxToken);
|
||||
ThreadUtil.newThread(() -> sendPostStream(MESSAGES, paramMap, callback::accept), "grok-message-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
@@ -87,6 +90,12 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "grok-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String models() {
|
||||
Response response = sendGet(MODELS_ENDPOINT);
|
||||
@@ -124,6 +133,13 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
Response response = sendPost(IMAGES_GENERATIONS, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
@@ -136,6 +152,18 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
@@ -167,6 +195,37 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建消息回复请求体
|
||||
private String buildMessageRequestBody(final List<Message> messages, int maxToken) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
@@ -179,6 +238,18 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildMessageStreamRequestBody(final List<Message> messages, int maxToken) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("max_tokens", maxToken);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建分词请求体
|
||||
private String buildTokenizeRequestBody(String text) {
|
||||
//使用JSON工具
|
||||
@@ -190,4 +261,15 @@ public class GrokServiceImpl extends BaseAIService implements GrokService {
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.ai.model.hutool;
|
||||
|
||||
/**
|
||||
* hutool公共类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public class HutoolCommon {
|
||||
|
||||
//hutool视觉参数
|
||||
public enum HutoolVision {
|
||||
|
||||
AUTO("auto"),
|
||||
LOW("low"),
|
||||
HIGH("high");
|
||||
|
||||
private final String detail;
|
||||
|
||||
HutoolVision(String detail) {
|
||||
this.detail = detail;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
|
||||
//hutool音频参数
|
||||
public enum HutoolSpeech {
|
||||
|
||||
ALLOY("alloy"),
|
||||
ASH("ash"),
|
||||
CORAL("coral"),
|
||||
ECHO("echo"),
|
||||
FABLE("fable"),
|
||||
ONYX("onyx"),
|
||||
NOVA("nova"),
|
||||
SAGE("sage"),
|
||||
SHIMMER("shimmer");
|
||||
|
||||
private final String voice;
|
||||
|
||||
HutoolSpeech(String voice) {
|
||||
this.voice = voice;
|
||||
}
|
||||
|
||||
public String getVoice() {
|
||||
return voice;
|
||||
}
|
||||
}
|
||||
|
||||
//hutool视频生成参数
|
||||
public enum HutoolVideo {
|
||||
|
||||
//宽高比例
|
||||
RATIO_16_9("--rt", "16:9"),//[1280, 720]
|
||||
RATIO_4_3("--rt", "4:3"),//[960, 720]
|
||||
RATIO_1_1("--rt", "1:1"),//[720, 720]
|
||||
RATIO_3_4("--rt", "3:4"),//[720, 960]
|
||||
RATIO_9_16("--rt", "9:16"),//[720, 1280]
|
||||
RATIO_21_9("--rt", "21:9"),//[1280, 544]
|
||||
|
||||
//生成视频时长
|
||||
DURATION_5("--dur", 5),//文生视频,图生视频
|
||||
DURATION_10("--dur", 10),//文生视频
|
||||
|
||||
//帧率,即一秒时间内视频画面数量
|
||||
FPS_5("--fps", 24),
|
||||
|
||||
//视频分辨率
|
||||
RESOLUTION_5("--rs", "720p"),
|
||||
|
||||
//生成视频是否包含水印
|
||||
WATERMARK_TRUE("--wm", true),
|
||||
WATERMARK_FALSE("--wm", false);
|
||||
|
||||
private final String type;
|
||||
private final Object value;
|
||||
|
||||
HutoolVideo(String type, Object value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public Object getValue() {
|
||||
if (value instanceof String) {
|
||||
return (String) value;
|
||||
} else if (value instanceof Integer) {
|
||||
return (Integer) value;
|
||||
} else if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.ai.model.hutool;
|
||||
|
||||
import org.dromara.hutool.ai.Models;
|
||||
import org.dromara.hutool.ai.core.BaseConfig;
|
||||
|
||||
/**
|
||||
* Hutool配置类,初始化API接口地址,设置默认的模型
|
||||
*
|
||||
* @author elichow
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public class HutoolConfig extends BaseConfig {
|
||||
|
||||
private final String API_URL = "https://api.hutool.cn/ai/api";
|
||||
|
||||
private final String DEFAULT_MODEL = Models.Hutool.HUTOOL.getModel();
|
||||
|
||||
public HutoolConfig() {
|
||||
setApiUrl(API_URL);
|
||||
setModel(DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public HutoolConfig(String apiKey) {
|
||||
this();
|
||||
setApiKey(apiKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getModelName() {
|
||||
return "hutool";
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.ai.model.hutool;
|
||||
|
||||
import org.dromara.hutool.ai.core.AIConfig;
|
||||
import org.dromara.hutool.ai.core.AIServiceProvider;
|
||||
|
||||
/**r
|
||||
* 创建Hutool服务实现类
|
||||
*
|
||||
* @author elichow
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public class HutoolProvider implements AIServiceProvider {
|
||||
|
||||
@Override
|
||||
public String getServiceName() {
|
||||
return "hutool";
|
||||
}
|
||||
|
||||
@Override
|
||||
public HutoolService create(final AIConfig config) {
|
||||
return new HutoolServiceImpl(config);
|
||||
}
|
||||
}
|
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.ai.model.hutool;
|
||||
|
||||
import org.dromara.hutool.ai.core.AIService;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* hutool支持的扩展接口
|
||||
*
|
||||
* @author elichow
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public interface HutoolService extends AIService {
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default String chatVision(String prompt, final List<String> images) {
|
||||
return chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入|的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback){
|
||||
chatVision(prompt, images, HutoolCommon.HutoolVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分词:可以将文本转换为模型可理解的 token 信息
|
||||
*
|
||||
* @param text 需要分词的内容
|
||||
* @return 分词结果
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String tokenizeText(String text);
|
||||
|
||||
/**
|
||||
* 文生图
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @return 包含生成图片的url
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String imagesGenerations(String prompt);
|
||||
|
||||
/**
|
||||
* 图文向量化:仅支持单一文本、单张图片或文本与图片的组合输入(即一段文本 + 一张图片),暂不支持批量文本 / 图片的同时处理
|
||||
*
|
||||
* @param text 需要向量化的内容
|
||||
* @param image 需要向量化的图片地址/或者图片Base64编码图片(URI形式)
|
||||
* @return 处理后的向量信息
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String embeddingVision(String text, String image);
|
||||
|
||||
/**
|
||||
* TTS文本转语音
|
||||
*
|
||||
* @param input 需要转成语音的文本
|
||||
* @param voice AI的音色
|
||||
* @return 返回的音频mp3文件流
|
||||
* @since 6.0.0
|
||||
*/
|
||||
InputStream tts(String input, final HutoolCommon.HutoolSpeech voice);
|
||||
|
||||
/**
|
||||
* TTS文本转语音
|
||||
*
|
||||
* @param input 需要转成语音的文本
|
||||
* @return 返回的音频mp3文件流
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default InputStream tts(String input) {
|
||||
return tts(input, HutoolCommon.HutoolSpeech.ALLOY);
|
||||
}
|
||||
|
||||
/**
|
||||
* STT音频转文本
|
||||
*
|
||||
* @param file 需要转成文本的音频文件
|
||||
* @return 返回的文本内容
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String stt(final File file);
|
||||
|
||||
/**
|
||||
* 创建视频生成任务
|
||||
*
|
||||
* @param text 文本提示词
|
||||
* @param image 图片/或者图片Base64编码图片(URI形式)
|
||||
* @param videoParams 视频参数列表
|
||||
* @return 生成任务id
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams);
|
||||
|
||||
/**
|
||||
* 创建视频生成任务
|
||||
*
|
||||
* @param text 文本提示词
|
||||
* @param image 图片/或者图片Base64编码图片(URI形式)
|
||||
* @return 生成任务id
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default String videoTasks(String text, String image) {
|
||||
return videoTasks(text, image, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询视频生成任务信息
|
||||
*
|
||||
* @param taskId 通过创建生成视频任务返回的生成任务id
|
||||
* @return 生成任务信息
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String getVideoTasksInfo(String taskId);
|
||||
|
||||
}
|
@@ -0,0 +1,380 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.ai.model.hutool;
|
||||
|
||||
import org.dromara.hutool.ai.AIException;
|
||||
import org.dromara.hutool.ai.core.AIConfig;
|
||||
import org.dromara.hutool.ai.core.BaseAIService;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.text.StrUtil;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.http.client.Response;
|
||||
import org.dromara.hutool.json.JSONUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Hutool服务,AI具体功能的实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public class HutoolServiceImpl extends BaseAIService implements HutoolService {
|
||||
|
||||
//对话补全
|
||||
private final String CHAT_ENDPOINT = "/chat/completions";
|
||||
//分词
|
||||
private final String TOKENIZE_TEXT = "/tokenize/text";
|
||||
//文生图
|
||||
private final String IMAGES_GENERATIONS = "/images/generations";
|
||||
//图文向量化
|
||||
private final String EMBEDDING_VISION = "/embeddings/multimodal";
|
||||
//文本转语音
|
||||
private final String TTS = "/audio/tts";
|
||||
//语音转文本
|
||||
private final String STT = "/audio/stt";
|
||||
//创建视频生成任务
|
||||
private final String CREATE_VIDEO = "/video/generations";
|
||||
|
||||
public HutoolServiceImpl(final AIConfig config) {
|
||||
//初始化hutool客户端
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
Response response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(List<Message> messages,Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
Response response = sendPost(CHAT_ENDPOINT, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
System.out.println(JSONUtil.toJsonStr(paramMap));
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "hutool-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String tokenizeText(String text) {
|
||||
String paramJson = buildTokenizeRequestBody(text);
|
||||
Response response = sendPost(TOKENIZE_TEXT, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
Response response = sendPost(IMAGES_GENERATIONS, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String embeddingVision(String text, String image) {
|
||||
String paramJson = buildEmbeddingVisionRequestBody(text, image);
|
||||
Response response = sendPost(EMBEDDING_VISION, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream tts(String input, final HutoolCommon.HutoolSpeech voice) {
|
||||
try {
|
||||
String paramJson = buildTTSRequestBody(input, voice.getVoice());
|
||||
Response response = sendPost(TTS, paramJson);
|
||||
|
||||
// 检查响应内容类型
|
||||
String contentType = response.header("Content-Type");
|
||||
if (contentType != null && contentType.startsWith("application/json")) {
|
||||
// 如果是JSON响应,说明有错误
|
||||
String errorBody = response.bodyStr();
|
||||
throw new AIException("TTS请求失败: " + errorBody);
|
||||
}
|
||||
// 默认返回音频流
|
||||
return response.bodyStream();
|
||||
} catch (Exception e) {
|
||||
throw new AIException("TTS处理失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String stt(final File file) {
|
||||
final Map<String, Object> paramMap = buildSTTRequestBody(file);
|
||||
Response response = sendFormData(STT, paramMap);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String videoTasks(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {
|
||||
String paramJson = buildGenerationsTasksRequestBody(text, image, videoParams);
|
||||
Response response = sendPost(CREATE_VIDEO, paramJson);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getVideoTasksInfo(String taskId) {
|
||||
Response response = sendGet(CREATE_VIDEO + "/" + taskId);
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
|
||||
//构建分词请求体
|
||||
private String buildTokenizeRequestBody(String text) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("text", text);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("prompt", prompt);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建图文向量化请求体
|
||||
private String buildEmbeddingVisionRequestBody(String text, String image) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
|
||||
final List<Object> input = new ArrayList<>();
|
||||
//添加文本参数
|
||||
if (!StrUtil.isBlank(text)) {
|
||||
final Map<String, String> textMap = new HashMap<>();
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", text);
|
||||
input.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isBlank(image)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", image);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
input.add(imgUrlMap);
|
||||
}
|
||||
|
||||
paramMap.put("input", input);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
System.out.println(JSONUtil.toJsonStr(paramMap));
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
|
||||
//构建TTS请求体
|
||||
private String buildTTSRequestBody(String input, String voice) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("input", input);
|
||||
paramMap.put("voice", voice);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
//构建STT请求体
|
||||
private Map<String, Object> buildSTTRequestBody(final File file) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("file", file);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建创建视频任务请求体
|
||||
private String buildGenerationsTasksRequestBody(String text, String image, final List<HutoolCommon.HutoolVideo> videoParams) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("model", config.getModel());
|
||||
|
||||
final List<Object> content = new ArrayList<>();
|
||||
//添加文本参数
|
||||
final Map<String, String> textMap = new HashMap<>();
|
||||
if (!StrUtil.isBlank(text)) {
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", text);
|
||||
content.add(textMap);
|
||||
}
|
||||
//添加图片参数
|
||||
if (!StrUtil.isBlank(image)) {
|
||||
final Map<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
final Map<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", image);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
//添加视频参数
|
||||
if (videoParams != null && !videoParams.isEmpty()) {
|
||||
//如果有文本参数就加在后面
|
||||
if (textMap != null && !textMap.isEmpty()) {
|
||||
int textIndex = content.indexOf(textMap);
|
||||
StringBuilder textBuilder = new StringBuilder(text);
|
||||
for (HutoolCommon.HutoolVideo videoParam : videoParams) {
|
||||
textBuilder.append(" ").append(videoParam.getType()).append(" ").append(videoParam.getValue());
|
||||
}
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", textBuilder.toString());
|
||||
|
||||
if (textIndex != -1) {
|
||||
content.set(textIndex, textMap);
|
||||
} else {
|
||||
content.add(textMap);
|
||||
}
|
||||
} else {
|
||||
//如果没有文本参数就重新增加
|
||||
StringBuilder textBuilder = new StringBuilder();
|
||||
for (HutoolCommon.HutoolVideo videoParam : videoParams) {
|
||||
textBuilder.append(videoParam.getType()).append(videoParam.getValue()).append(" ");
|
||||
}
|
||||
textMap.put("type", "text");
|
||||
textMap.put("text", textBuilder.toString());
|
||||
content.add(textMap);
|
||||
}
|
||||
}
|
||||
|
||||
paramMap.put("content", content);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
System.out.println(JSONUtil.toJsonStr(paramMap));
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 对hutool的封装实现
|
||||
*
|
||||
* @author elichow
|
||||
* @since 6.0.0
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.ai.model.hutool;
|
@@ -21,7 +21,9 @@ import org.dromara.hutool.ai.core.Message;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* openai支持的扩展接口
|
||||
@@ -42,6 +44,17 @@ public interface OpenaiService extends AIService {
|
||||
*/
|
||||
String chatVision(String prompt, final List<String> images, String detail);
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 图片列表/或者图片Base64编码图片列表(URI形式)
|
||||
* @param detail 手动设置图片的质量,取值范围high、low、auto,默认为auto
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void chatVision(String prompt, final List<String> images, String detail,final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 图像理解:模型会依据传入的图片信息以及问题,给出回复。
|
||||
*
|
||||
@@ -54,6 +67,18 @@ public interface OpenaiService extends AIService {
|
||||
return chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail());
|
||||
}
|
||||
|
||||
/**
|
||||
* 图像理解-SSE流式输出
|
||||
*
|
||||
* @param prompt 题词
|
||||
* @param images 传入的图片列表地址/或者图片Base64编码图片列表(URI形式)
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatVision(String prompt, final List<String> images, final Consumer<String> callback){
|
||||
chatVision(prompt, images, OpenaiCommon.OpenaiVision.AUTO.getDetail(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 文生图 请设置config中model为支持图片功能的模型 DALL·E系列
|
||||
*
|
||||
@@ -166,7 +191,28 @@ public interface OpenaiService extends AIService {
|
||||
* @return AI回答
|
||||
* @since 6.0.0
|
||||
*/
|
||||
String chatReasoning(String prompt, String reasoningEffort);
|
||||
default String chatReasoning(String prompt, String reasoningEffort){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chatReasoning(messages, reasoningEffort);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @param reasoningEffort 推理程度
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatReasoning(String prompt, String reasoningEffort, final Consumer<String> callback){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
chatReasoning(messages, reasoningEffort, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat
|
||||
@@ -180,6 +226,18 @@ public interface OpenaiService extends AIService {
|
||||
return chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort());
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param prompt 对话题词
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatReasoning(String prompt, final Consumer<String> callback) {
|
||||
chatReasoning(prompt, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat
|
||||
* 支持o3-mini和o1
|
||||
@@ -191,6 +249,17 @@ public interface OpenaiService extends AIService {
|
||||
*/
|
||||
String chatReasoning(final List<Message> messages, String reasoningEffort);
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @param reasoningEffort 推理程度
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
void chatReasoning(final List<Message> messages, String reasoningEffort, final Consumer<String> callback);
|
||||
|
||||
/**
|
||||
* 推理chat
|
||||
* 支持o3-mini和o1
|
||||
@@ -203,4 +272,16 @@ public interface OpenaiService extends AIService {
|
||||
return chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort());
|
||||
}
|
||||
|
||||
/**
|
||||
* 推理chat-SSE流式输出
|
||||
* 支持o3-mini和o1
|
||||
*
|
||||
* @param messages 消息列表
|
||||
* @param callback 流式数据回调函数
|
||||
* @since 6.0.0
|
||||
*/
|
||||
default void chatReasoning(final List<Message> messages, final Consumer<String> callback) {
|
||||
chatReasoning(messages, OpenaiCommon.OpenaiReasoning.MEDIUM.getEffort(), callback);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ import org.dromara.hutool.ai.core.AIConfig;
|
||||
import org.dromara.hutool.ai.core.BaseAIService;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.text.StrUtil;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.http.client.Response;
|
||||
import org.dromara.hutool.json.JSONUtil;
|
||||
|
||||
@@ -29,6 +30,7 @@ import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* openai服务,AI具体功能的实现
|
||||
@@ -60,15 +62,6 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
super(config);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(String prompt) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chat(final List<Message> messages) {
|
||||
String paramJson = buildChatRequestBody(messages);
|
||||
@@ -76,6 +69,12 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chat(List<Message> messages, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatStreamRequestBody(messages);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chat-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatVision(String prompt, final List<String> images, String detail) {
|
||||
String paramJson = buildChatVisionRequestBody(prompt, images, detail);
|
||||
@@ -83,6 +82,12 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatVision(String prompt, List<String> images, String detail, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatVisionStreamRequestBody(prompt, images, detail);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatVision-sse").start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String imagesGenerations(String prompt) {
|
||||
String paramJson = buildImagesGenerationsRequestBody(prompt);
|
||||
@@ -132,15 +137,6 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatReasoning(String prompt, String reasoningEffort) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system", "You are a helpful assistant"));
|
||||
messages.add(new Message("user", prompt));
|
||||
return chat(messages);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String chatReasoning(final List<Message> messages, String reasoningEffort) {
|
||||
String paramJson = buildChatReasoningRequestBody(messages, reasoningEffort);
|
||||
@@ -148,6 +144,12 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
return response.bodyStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void chatReasoning(List<Message> messages, String reasoningEffort, Consumer<String> callback) {
|
||||
Map<String, Object> paramMap = buildChatReasoningStreamRequestBody(messages, reasoningEffort);
|
||||
ThreadUtil.newThread(() -> sendPostStream(CHAT_ENDPOINT, paramMap, callback::accept), "openai-chatReasoning-sse").start();
|
||||
}
|
||||
|
||||
// 构建chat请求体
|
||||
private String buildChatRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
@@ -160,6 +162,18 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatStreamRequestBody(final List<Message> messages) {
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建chatVision请求体
|
||||
private String buildChatVisionRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
@@ -191,6 +205,37 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatVisionStreamRequestBody(String prompt, final List<String> images, String detail) {
|
||||
// 定义消息结构
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
final List<Object> content = new ArrayList<>();
|
||||
|
||||
final Map<String, String> contentMap = new HashMap<>();
|
||||
contentMap.put("type", "text");
|
||||
contentMap.put("text", prompt);
|
||||
content.add(contentMap);
|
||||
for (String img : images) {
|
||||
HashMap<String, Object> imgUrlMap = new HashMap<>();
|
||||
imgUrlMap.put("type", "image_url");
|
||||
HashMap<String, String> urlMap = new HashMap<>();
|
||||
urlMap.put("url", img);
|
||||
urlMap.put("detail", detail);
|
||||
imgUrlMap.put("image_url", urlMap);
|
||||
content.add(imgUrlMap);
|
||||
}
|
||||
|
||||
messages.add(new Message("user", content));
|
||||
|
||||
//使用JSON工具
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
//构建文生图请求体
|
||||
private String buildImagesGenerationsRequestBody(String prompt) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
@@ -305,4 +350,16 @@ public class OpenaiServiceImpl extends BaseAIService implements OpenaiService {
|
||||
return JSONUtil.toJsonStr(paramMap);
|
||||
}
|
||||
|
||||
private Map<String, Object> buildChatReasoningStreamRequestBody(final List<Message> messages, String reasoningEffort) {
|
||||
final Map<String, Object> paramMap = new HashMap<>();
|
||||
paramMap.put("stream", true);
|
||||
paramMap.put("model", config.getModel());
|
||||
paramMap.put("messages", messages);
|
||||
paramMap.put("reasoning_effort", reasoningEffort);
|
||||
//合并其他参数
|
||||
paramMap.putAll(config.getAdditionalConfigMap());
|
||||
|
||||
return paramMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,3 +1,4 @@
|
||||
org.dromara.hutool.ai.model.hutool.HutoolConfig
|
||||
org.dromara.hutool.ai.model.deepseek.DeepSeekConfig
|
||||
org.dromara.hutool.ai.model.openai.OpenaiConfig
|
||||
org.dromara.hutool.ai.model.doubao.DoubaoConfig
|
||||
|
@@ -1,3 +1,4 @@
|
||||
org.dromara.hutool.ai.model.hutool.HutoolProvider
|
||||
org.dromara.hutool.ai.model.deepseek.DeepSeekProvider
|
||||
org.dromara.hutool.ai.model.openai.OpenaiProvider
|
||||
org.dromara.hutool.ai.model.doubao.DoubaoProvider
|
||||
|
@@ -22,6 +22,7 @@ import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.ai.model.deepseek.DeepSeekService;
|
||||
import org.dromara.hutool.ai.model.doubao.DoubaoService;
|
||||
import org.dromara.hutool.ai.model.grok.GrokService;
|
||||
import org.dromara.hutool.ai.model.hutool.HutoolService;
|
||||
import org.dromara.hutool.ai.model.openai.OpenaiService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -46,6 +47,12 @@ class AIUtilTest {
|
||||
assertNotNull(aiService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getHutoolService() {
|
||||
final HutoolService hutoolService = AIUtil.getHutoolService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build());
|
||||
assertNotNull(hutoolService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDeepSeekService() {
|
||||
final DeepSeekService deepSeekService = AIUtil.getDeepSeekService(new AIConfigBuilder(ModelName.DEEPSEEK.getValue()).setApiKey(key).build());
|
||||
|
@@ -20,11 +20,15 @@ import org.dromara.hutool.ai.AIServiceFactory;
|
||||
import org.dromara.hutool.ai.ModelName;
|
||||
import org.dromara.hutool.ai.core.AIConfigBuilder;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class DeepSeekServiceTest {
|
||||
|
||||
@@ -35,7 +39,30 @@ class DeepSeekServiceTest {
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = deepSeekService.chat("写一个疯狂星期四广告词");
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
deepSeekService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -45,27 +72,50 @@ class DeepSeekServiceTest {
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = deepSeekService.chat(messages);
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void beta() {
|
||||
final String beta = deepSeekService.beta("写一个疯狂星期四广告词");
|
||||
System.out.println(beta);
|
||||
assertNotNull(beta);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void betaStream() {
|
||||
String beta = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
deepSeekService.beta(beta, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void models() {
|
||||
final String models = deepSeekService.models();
|
||||
System.out.println(models);
|
||||
assertNotNull(models);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void balance() {
|
||||
final String balance = deepSeekService.balance();
|
||||
System.out.println(balance);
|
||||
assertNotNull(balance);
|
||||
}
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import org.dromara.hutool.ai.ModelName;
|
||||
import org.dromara.hutool.ai.Models;
|
||||
import org.dromara.hutool.ai.core.AIConfigBuilder;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.swing.img.ImgUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -29,6 +30,9 @@ import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class DoubaoServiceTest {
|
||||
|
||||
@@ -39,7 +43,30 @@ class DoubaoServiceTest {
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = doubaoService.chat("写一个疯狂星期四广告词");
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
doubaoService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,7 +76,7 @@ class DoubaoServiceTest {
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = doubaoService.chat(messages);
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -59,7 +86,34 @@ class DoubaoServiceTest {
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
|
||||
final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
|
||||
final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList(base64));
|
||||
System.out.println(chatVision);
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.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");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
doubaoService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,17 +122,17 @@ class DoubaoServiceTest {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_1_5_VISION_PRO_32K.getModel()).build(), DoubaoService.class);
|
||||
final String chatVision = doubaoService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"),DoubaoCommon.DoubaoVision.HIGH.getDetail());
|
||||
System.out.println(chatVision);
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void videoTasks() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
.setApiKey(key).setModel(Models.Doubao.Doubao_Seedance_1_0_lite_i2v.getModel()).build(), DoubaoService.class);
|
||||
final String videoTasks = doubaoService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," +
|
||||
"画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
System.out.println(videoTasks);//cgt-20250306170051-6r9gk
|
||||
assertNotNull(videoTasks);//cgt-20250306170051-6r9gk
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -88,7 +142,7 @@ class DoubaoServiceTest {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).build(), DoubaoService.class);
|
||||
final String videoTasksInfo = doubaoService.getVideoTasksInfo("cgt-20250306170051-6r9gk");
|
||||
System.out.println(videoTasksInfo);
|
||||
assertNotNull(videoTasksInfo);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -97,7 +151,7 @@ class DoubaoServiceTest {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_TEXT_240715.getModel()).build(), DoubaoService.class);
|
||||
final String embeddingText = doubaoService.embeddingText(new String[]{"阿斯顿", "马丁"});
|
||||
System.out.println(embeddingText);
|
||||
assertNotNull(embeddingText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,7 +160,7 @@ class DoubaoServiceTest {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_EMBEDDING_VISION.getModel()).build(), DoubaoService.class);
|
||||
final String embeddingVision = doubaoService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
System.out.println(embeddingVision);
|
||||
assertNotNull(embeddingVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -118,14 +172,41 @@ class DoubaoServiceTest {
|
||||
messages.add(new Message("system","你是什么都可以"));
|
||||
messages.add(new Message("user","你想做些什么"));
|
||||
final String botsChat = doubaoService.botsChat(messages);
|
||||
System.out.println(botsChat);
|
||||
assertNotNull(botsChat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void botsChatStream() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your bots id").build(), DoubaoService.class);
|
||||
final ArrayList<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是什么都可以"));
|
||||
messages.add(new Message("user","你想做些什么"));
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
doubaoService.botsChat(messages, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void tokenization() {
|
||||
final String tokenization = doubaoService.tokenization(new String[]{"阿斯顿", "马丁"});
|
||||
System.out.println(tokenization);
|
||||
assertNotNull(tokenization);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -134,7 +215,7 @@ class DoubaoServiceTest {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final String batchChat = doubaoService.batchChat("写首歌词");
|
||||
System.out.println(batchChat);
|
||||
assertNotNull(batchChat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,7 +227,7 @@ class DoubaoServiceTest {
|
||||
messages.add(new Message("system","你是个抽象大师"));
|
||||
messages.add(new Message("user","写一个KFC的抽象广告"));
|
||||
final String batchChat = doubaoService.batchChat(messages);
|
||||
System.out.println(batchChat);
|
||||
assertNotNull(batchChat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -157,7 +238,7 @@ class DoubaoServiceTest {
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,你真的很抽象"));
|
||||
final String context = doubaoService.createContext(messages);//ctx-20250307092153-cvslm
|
||||
System.out.println(context);
|
||||
assertNotNull(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -168,17 +249,16 @@ class DoubaoServiceTest {
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,你真的很抽象"));
|
||||
final String context = doubaoService.createContext(messages,DoubaoCommon.DoubaoContext.COMMON_PREFIX.getMode());
|
||||
System.out.println(context);//ctx-20250307092153-cvslm
|
||||
assertNotNull(context);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatContext() {
|
||||
//ctx-20250307092153-cvslm
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("eyour Endpoint ID").build(), DoubaoService.class);
|
||||
final String chatContext = doubaoService.chatContext("你是谁?", "ctx-20250307092153-cvslm");
|
||||
System.out.println(chatContext);
|
||||
final String chatContext = doubaoService.chatContext("你是谁?", "your contextId");
|
||||
assertNotNull(chatContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -188,7 +268,43 @@ class DoubaoServiceTest {
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user","你怎么看待意大利面拌水泥?"));
|
||||
final String chatContext = doubaoService.chatContext(messages, "ctx-20250307092153-cvslm");
|
||||
System.out.println(chatContext);
|
||||
final String chatContext = doubaoService.chatContext(messages, "your contextId");
|
||||
assertNotNull(chatContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatContextStream() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel("your Endpoint ID").build(), DoubaoService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("user","你怎么看待意大利面拌水泥?"));
|
||||
String contextId = "your contextId";
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
doubaoService.chatContext(messages,contextId, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesGenerations() {
|
||||
final DoubaoService doubaoService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.DOUBAO.getValue())
|
||||
.setApiKey(key).setModel(Models.Doubao.DOUBAO_SEEDREAM_3_0_T2I.getModel()).build(), DoubaoService.class);
|
||||
final String imagesGenerations = doubaoService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
assertNotNull(imagesGenerations);
|
||||
}
|
||||
}
|
||||
|
@@ -21,6 +21,7 @@ import org.dromara.hutool.ai.ModelName;
|
||||
import org.dromara.hutool.ai.Models;
|
||||
import org.dromara.hutool.ai.core.AIConfigBuilder;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.swing.img.ImgUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -29,6 +30,7 @@ import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -42,7 +44,30 @@ class GrokServiceTest {
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = grokService.chat("写一个疯狂星期四广告词");
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
grokService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -52,14 +77,37 @@ class GrokServiceTest {
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = grokService.chat(messages);
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void message() {
|
||||
final String message = grokService.message("给我一个KFC的广告词", 4096);
|
||||
System.out.println(message);
|
||||
assertNotNull(message);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void messageStream() {
|
||||
String prompt = "给我一个KFC的广告词";
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
grokService.message(prompt, 4096, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -68,7 +116,32 @@ class GrokServiceTest {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);
|
||||
final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
|
||||
final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList(base64));
|
||||
System.out.println(chatVision);
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.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");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
grokService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -76,7 +149,7 @@ class GrokServiceTest {
|
||||
void testChatVision() {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue()).setModel(Models.Grok.GROK_2_VISION_1212.getModel()).setApiKey(key).build(), GrokService.class);
|
||||
final String chatVision = grokService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"));
|
||||
System.out.println(chatVision);
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -120,4 +193,13 @@ class GrokServiceTest {
|
||||
final String deferred = grokService.deferredCompletion(key);
|
||||
assertNotNull(deferred);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesGenerations() {
|
||||
final GrokService grokService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.GROK.getValue())
|
||||
.setApiKey(key).setModel(Models.Grok.GROK_2_IMAGE.getModel()).build(), GrokService.class);
|
||||
final String imagesGenerations = grokService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
assertNotNull(imagesGenerations);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,193 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.ai.model.hutool;
|
||||
|
||||
import org.dromara.hutool.ai.AIException;
|
||||
import org.dromara.hutool.ai.AIServiceFactory;
|
||||
import org.dromara.hutool.ai.ModelName;
|
||||
import org.dromara.hutool.ai.core.AIConfigBuilder;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.io.file.FileUtil;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.dromara.hutool.swing.img.ImgUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class HutoolServiceTest {
|
||||
|
||||
String key = "请前往Hutool-AI官网:https://ai.hutool.cn 获取";
|
||||
HutoolService hutoolService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.HUTOOL.getValue()).setApiKey(key).build(), HutoolService.class);
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = hutoolService.chat("写一个疯狂星期四广告词");
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
hutoolService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChat(){
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = hutoolService.chat(messages);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatVision() {
|
||||
final String base64 = ImgUtil.toBase64DataUri(Toolkit.getDefaultToolkit().createImage("your imageUrl"), "png");
|
||||
final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList(base64));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
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");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
hutoolService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVision() {
|
||||
final String chatVision = hutoolService.chatVision("图片上有些什么?", Arrays.asList("https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544"));
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void tokenizeText() {
|
||||
final String tokenizeText = hutoolService.tokenizeText(key);
|
||||
assertNotNull(tokenizeText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void imagesGenerations() {
|
||||
final String imagesGenerations = hutoolService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
assertNotNull(imagesGenerations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void embeddingVision() {
|
||||
final String embeddingVision = hutoolService.embeddingVision("天空好难", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
System.out.println(embeddingVision);
|
||||
assertNotNull(embeddingVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void textToSpeech() {
|
||||
try {
|
||||
// 测试正常音频流返回
|
||||
final InputStream inputStream = hutoolService.tts("万里山河一夜白,\n" +
|
||||
"千峰尽染玉龙哀。\n" +
|
||||
"长风卷起琼花碎,\n" +
|
||||
"直上九霄揽月来。", HutoolCommon.HutoolSpeech.NOVA);
|
||||
assertNotNull(inputStream);
|
||||
|
||||
// 保存音频文件
|
||||
final String filePath = "your filePath";
|
||||
FileUtil.writeFromStream(inputStream, new File(filePath));
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new AIException("TTS测试失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void speechToText() {
|
||||
final File file = FileUtil.file("your filePath");
|
||||
final String speechToText = hutoolService.stt(file);
|
||||
assertNotNull(speechToText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void videoTasks() {
|
||||
final String videoTasks = hutoolService.videoTasks("生成一段动画视频,主角是大耳朵图图,一个活泼可爱的小男孩。视频中图图在公园里玩耍," +
|
||||
"画面采用明亮温暖的卡通风格,色彩鲜艳,动作流畅。背景音乐轻快活泼,带有冒险感,音效包括鸟叫声、欢笑声和山洞回声。", "https://img2.baidu.com/it/u=862000265,4064861820&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=1544");
|
||||
assertNotNull(videoTasks);//cgt-20250529154621-d7dq9
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void getVideoTasksInfo() {
|
||||
final String videoTasksInfo = hutoolService.getVideoTasksInfo("cgt-20250529154621-d7dq9");
|
||||
System.out.println(videoTasksInfo);
|
||||
assertNotNull(videoTasksInfo);
|
||||
}
|
||||
|
||||
}
|
@@ -22,6 +22,7 @@ import org.dromara.hutool.ai.Models;
|
||||
import org.dromara.hutool.ai.core.AIConfigBuilder;
|
||||
import org.dromara.hutool.ai.core.Message;
|
||||
import org.dromara.hutool.core.io.file.FileUtil;
|
||||
import org.dromara.hutool.core.thread.ThreadUtil;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -35,6 +36,9 @@ import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
|
||||
class OpenaiServiceTest {
|
||||
|
||||
@@ -46,7 +50,30 @@ class OpenaiServiceTest {
|
||||
@Disabled
|
||||
void chat(){
|
||||
final String chat = openaiService.chat("写一个疯狂星期四广告词");
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatStream() {
|
||||
String prompt = "写一个疯狂星期四广告词";
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
|
||||
openaiService.chat(prompt, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -56,7 +83,33 @@ class OpenaiServiceTest {
|
||||
messages.add(new Message("system","你是个抽象大师,会说很抽象的话,最擅长说抽象的笑话"));
|
||||
messages.add(new Message("user","给我说一个笑话"));
|
||||
final String chat = openaiService.chat(messages);
|
||||
System.out.println(chat);
|
||||
assertNotNull(chat);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void testChatVisionStream() {
|
||||
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");
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
openaiService.chatVision(prompt,images, data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -65,7 +118,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);
|
||||
final String chatVision = openaiService.chatVision("图片上有些什么?", 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"));
|
||||
System.out.println(chatVision);
|
||||
assertNotNull(chatVision);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -74,8 +127,7 @@ class OpenaiServiceTest {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.DALL_E_3.getModel()).build(), OpenaiService.class);
|
||||
final String imagesGenerations = openaiService.imagesGenerations("一位年轻的宇航员站在未来感十足的太空站内,透过巨大的弧形落地窗凝望浩瀚宇宙。窗外,璀璨的星河与五彩斑斓的星云交织,远处隐约可见未知星球的轮廓,仿佛在召唤着探索的脚步。宇航服上的呼吸灯与透明显示屏上的星图交相辉映,象征着人类科技与宇宙奥秘的碰撞。画面深邃而神秘,充满对未知的渴望与无限可能的想象。");
|
||||
System.out.println(imagesGenerations);
|
||||
//https://oaidalleapiprodscus.blob.core.windows.net/private/org-l99H6T0zCZejctB2TqdYrXFB/user-LilDVU1V8cUxJYwVAGRkUwYd/img-yA9kNatHnBiUHU5lZGim1hP2.png?st=2025-03-07T01%3A04%3A18Z&se=2025-03-07T03%3A04%3A18Z&sp=r&sv=2024-08-04&sr=b&rscd=inline&rsct=image/png&skoid=d505667d-d6c1-4a0a-bac7-5c84a87759f8&sktid=a48cca56-e6da-484e-a814-9c849652bcb3&skt=2025-03-06T15%3A04%3A42Z&ske=2025-03-07T15%3A04%3A42Z&sks=b&skv=2024-08-04&sig=rjcRzC5U7Y3pEDZ4ME0CiviAPdIpoGO2rRTXw3m8rHw%3D
|
||||
assertNotNull(imagesGenerations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -85,7 +137,7 @@ class OpenaiServiceTest {
|
||||
.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);
|
||||
final File file = FileUtil.file("your imgUrl");
|
||||
final String imagesEdits = openaiService.imagesEdits("茂密的森林中,有一只九色鹿若隐若现",file);
|
||||
System.out.println(imagesEdits);
|
||||
assertNotNull(imagesEdits);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -95,7 +147,7 @@ class OpenaiServiceTest {
|
||||
.setApiKey(key).setModel(Models.Openai.DALL_E_2.getModel()).build(), OpenaiService.class);
|
||||
final File file = FileUtil.file("your imgUrl");
|
||||
final String imagesVariations = openaiService.imagesVariations(file);
|
||||
System.out.println(imagesVariations);
|
||||
assertNotNull(imagesVariations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -130,7 +182,7 @@ class OpenaiServiceTest {
|
||||
.setApiKey(key).setModel(Models.Openai.WHISPER_1.getModel()).build(), OpenaiService.class);
|
||||
final File file = FileUtil.file("your filePath");
|
||||
final String speechToText = openaiService.speechToText(file);
|
||||
System.out.println(speechToText);
|
||||
assertNotNull(speechToText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,7 +191,7 @@ class OpenaiServiceTest {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.TEXT_EMBEDDING_3_SMALL.getModel()).build(), OpenaiService.class);
|
||||
final String embeddingText = openaiService.embeddingText("萬里山河一夜白,千峰盡染玉龍哀,長風捲起瓊花碎,直上九霄闌月來");
|
||||
System.out.println(embeddingText);
|
||||
assertNotNull(embeddingText);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -148,7 +200,7 @@ class OpenaiServiceTest {
|
||||
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");
|
||||
System.out.println(moderations);
|
||||
assertNotNull(moderations);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -160,6 +212,33 @@ class OpenaiServiceTest {
|
||||
messages.add(new Message("system","你是现代抽象家"));
|
||||
messages.add(new Message("user","给我一个KFC疯狂星期四的文案"));
|
||||
final String chatReasoning = openaiService.chatReasoning(messages, OpenaiCommon.OpenaiReasoning.HIGH.getEffort());
|
||||
System.out.println(chatReasoning);
|
||||
assertNotNull(chatReasoning);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
void chatReasoningStream() {
|
||||
final OpenaiService openaiService = AIServiceFactory.getAIService(new AIConfigBuilder(ModelName.OPENAI.getValue())
|
||||
.setApiKey(key).setModel(Models.Openai.O3_MINI.getModel()).build(), OpenaiService.class);
|
||||
final List<Message> messages = new ArrayList<>();
|
||||
messages.add(new Message("system","你是现代抽象家"));
|
||||
messages.add(new Message("user","给我一个KFC疯狂星期四的文案"));
|
||||
|
||||
// 使用AtomicBoolean作为结束标志
|
||||
AtomicBoolean isDone = new AtomicBoolean(false);
|
||||
openaiService.chatReasoning(messages,OpenaiCommon.OpenaiReasoning.HIGH.getEffort(), data -> {
|
||||
assertNotNull(data);
|
||||
if (data.contains("[DONE]")) {
|
||||
// 设置结束标志
|
||||
isDone.set(true);
|
||||
} else if (data.contains("\"error\"")) {
|
||||
isDone.set(true);
|
||||
}
|
||||
|
||||
});
|
||||
// 轮询检查结束标志
|
||||
while (!isDone.get()) {
|
||||
ThreadUtil.sleep(100);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-all</artifactId>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-bom</artifactId>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-core</artifactId>
|
||||
|
@@ -153,7 +153,7 @@ public class ArrayUtil extends PrimitiveArrayUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
|
||||
* 是否全都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
|
||||
*
|
||||
* @param args 被检查的对象,一个或者多个
|
||||
* @return 是否都不为空
|
||||
|
@@ -750,11 +750,12 @@ public class BeanUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为Bean对象<br>
|
||||
* 判定方法是否存在只有无参数的getXXX方法或者isXXX方法
|
||||
* 判断是否存在getXXX或者isXXX方法<br>
|
||||
* 判定方法是否存在只有无参数的getXXX方法或者isXXX方法<br>
|
||||
* 需要注意String对象有isEmpty方法,此处返回true
|
||||
*
|
||||
* @param clazz 待测试类
|
||||
* @return 是否为Bean对象
|
||||
* @return 判断是否存在getXXX或者isXXX方法
|
||||
* @since 4.2.2
|
||||
*/
|
||||
public static boolean hasGetter(final Class<?> clazz) {
|
||||
|
@@ -107,7 +107,9 @@ public abstract class AbstractCache<K, V> implements Cache<K, V> {
|
||||
final MutableObj<K> mKey = MutableObj.of(key);
|
||||
|
||||
// issue#3618 对于替换的键值对,不做满队列检查和清除
|
||||
if (cacheMap.containsKey(mKey)) {
|
||||
final CacheObj<K, V> oldObj = cacheMap.get(mKey);
|
||||
if (null != oldObj) {
|
||||
onRemove(oldObj.key, oldObj.obj);
|
||||
// 存在相同key,覆盖之
|
||||
cacheMap.put(mKey, co);
|
||||
} else {
|
||||
|
@@ -17,8 +17,11 @@
|
||||
package org.dromara.hutool.core.cache.impl;
|
||||
|
||||
import org.dromara.hutool.core.collection.iter.CopiedIter;
|
||||
import org.dromara.hutool.core.collection.set.SetUtil;
|
||||
import org.dromara.hutool.core.lang.mutable.Mutable;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
@@ -98,7 +101,14 @@ public abstract class LockedCache<K, V> extends AbstractCache<K, V> {
|
||||
public void clear() {
|
||||
lock.lock();
|
||||
try {
|
||||
cacheMap.clear();
|
||||
// 获取所有键的副本
|
||||
final Set<Mutable<K>> keys = SetUtil.of(cacheMap.keySet());
|
||||
for (final Mutable<K> key : keys) {
|
||||
final CacheObj<K, V> co = removeWithoutLock(key.get());
|
||||
if (co != null) {
|
||||
onRemove(co.key, co.obj); // 触发资源释放
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
|
@@ -942,7 +942,7 @@ public class ZipUtil {
|
||||
*/
|
||||
public static byte[] zlib(final InputStream in, final int level, final int length) {
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
|
||||
Deflate.of(in, out, false).deflater(level);
|
||||
Deflate.of(in, out, false).deflater(level).close();
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@@ -994,7 +994,7 @@ public class ZipUtil {
|
||||
*/
|
||||
public static byte[] unZlib(final InputStream in, final int length) {
|
||||
final ByteArrayOutputStream out = new ByteArrayOutputStream(length);
|
||||
Deflate.of(in, out, false).inflater();
|
||||
Deflate.of(in, out, false).inflater().close();
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
|
@@ -45,6 +45,7 @@ import java.util.function.Function;
|
||||
* <li>{@code java.lang.Short}</li>
|
||||
* <li>{@code java.lang.Integer}</li>
|
||||
* <li>{@code java.util.concurrent.atomic.AtomicInteger}</li>
|
||||
*
|
||||
* <li>{@code java.lang.Long}</li>
|
||||
* <li>{@code java.util.concurrent.atomic.AtomicLong}</li>
|
||||
* <li>{@code java.lang.Float}</li>
|
||||
|
@@ -0,0 +1,162 @@
|
||||
package org.dromara.hutool.core.data;
|
||||
|
||||
import org.dromara.hutool.core.data.masking.RichTextMaskingProcessor;
|
||||
import org.dromara.hutool.core.data.masking.RichTextMaskingRule;
|
||||
|
||||
/**
|
||||
* 富文本脱敏工具类,提供对富文本内容的脱敏处理功能
|
||||
*
|
||||
* @author xjf
|
||||
*/
|
||||
public class RichTextMaskingUtil {
|
||||
|
||||
/**
|
||||
* 默认的富文本脱敏处理器
|
||||
*/
|
||||
private static final RichTextMaskingProcessor DEFAULT_PROCESSOR = createDefaultProcessor();
|
||||
|
||||
/**
|
||||
* 创建默认的富文本脱敏处理器
|
||||
*
|
||||
* @return 默认的富文本脱敏处理器
|
||||
*/
|
||||
private static RichTextMaskingProcessor createDefaultProcessor() {
|
||||
final RichTextMaskingProcessor processor = new RichTextMaskingProcessor(true);
|
||||
|
||||
// 添加一些常用的脱敏规则
|
||||
|
||||
// 邮箱脱敏规则
|
||||
processor.addRule(new RichTextMaskingRule(
|
||||
"邮箱",
|
||||
"[\\w.-]+@[\\w.-]+\\.\\w+",
|
||||
RichTextMaskingRule.MaskType.PARTIAL,
|
||||
"[邮箱已隐藏]")
|
||||
.setPreserveLeft(1)
|
||||
.setPreserveRight(0)
|
||||
.setMaskChar('*'));
|
||||
|
||||
// 网址脱敏规则
|
||||
processor.addRule(new RichTextMaskingRule(
|
||||
"网址",
|
||||
"https?://[\\w.-]+(?:/[\\w.-]*)*",
|
||||
RichTextMaskingRule.MaskType.REPLACE,
|
||||
"[网址已隐藏]"));
|
||||
|
||||
// 敏感词脱敏规则(示例)
|
||||
processor.addRule(new RichTextMaskingRule(
|
||||
"敏感词",
|
||||
"(机密|绝密|内部资料|秘密|保密)",
|
||||
RichTextMaskingRule.MaskType.FULL,
|
||||
"***")
|
||||
.setMaskChar('*'));
|
||||
|
||||
return processor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对富文本内容进行脱敏处理
|
||||
*
|
||||
* @param text 富文本内容
|
||||
* @return 脱敏后的文本
|
||||
*/
|
||||
public static String mask(final String text) {
|
||||
return DEFAULT_PROCESSOR.mask(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用自定义处理器对富文本内容进行脱敏处理
|
||||
*
|
||||
* @param text 富文本内容
|
||||
* @param processor 自定义处理器
|
||||
* @return 脱敏后的文本
|
||||
*/
|
||||
public static String mask(final String text, final RichTextMaskingProcessor processor) {
|
||||
return processor.mask(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的富文本脱敏处理器
|
||||
*
|
||||
* @param preserveHtmlTags 是否保留HTML标签
|
||||
* @return 富文本脱敏处理器
|
||||
*/
|
||||
public static RichTextMaskingProcessor createProcessor(final boolean preserveHtmlTags) {
|
||||
return new RichTextMaskingProcessor(preserveHtmlTags);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个邮箱脱敏规则
|
||||
*
|
||||
* @return 邮箱脱敏规则
|
||||
*/
|
||||
public static RichTextMaskingRule createEmailRule() {
|
||||
return new RichTextMaskingRule(
|
||||
"邮箱",
|
||||
"[\\w.-]+@[\\w.-]+\\.\\w+",
|
||||
RichTextMaskingRule.MaskType.PARTIAL,
|
||||
null)
|
||||
.setPreserveLeft(1)
|
||||
.setPreserveRight(0)
|
||||
.setMaskChar('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个网址脱敏规则
|
||||
*
|
||||
* @param replacement 替换文本
|
||||
* @return 网址脱敏规则
|
||||
*/
|
||||
public static RichTextMaskingRule createUrlRule(final String replacement) {
|
||||
return new RichTextMaskingRule(
|
||||
"网址",
|
||||
"https?://[\\w.-]+(?:/[\\w.-]*)*",
|
||||
RichTextMaskingRule.MaskType.REPLACE,
|
||||
replacement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个敏感词脱敏规则
|
||||
*
|
||||
* @param pattern 敏感词正则表达式
|
||||
* @return 敏感词脱敏规则
|
||||
*/
|
||||
public static RichTextMaskingRule createSensitiveWordRule(final String pattern) {
|
||||
return new RichTextMaskingRule(
|
||||
"敏感词",
|
||||
pattern,
|
||||
RichTextMaskingRule.MaskType.FULL,
|
||||
null)
|
||||
.setMaskChar('*');
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个自定义脱敏规则
|
||||
*
|
||||
* @param name 规则名称
|
||||
* @param pattern 匹配模式(正则表达式)
|
||||
* @param maskType 脱敏类型
|
||||
* @param replacement 替换内容
|
||||
* @return 自定义脱敏规则
|
||||
*/
|
||||
public static RichTextMaskingRule createCustomRule(final String name, final String pattern,
|
||||
final RichTextMaskingRule.MaskType maskType,
|
||||
final String replacement) {
|
||||
return new RichTextMaskingRule(name, pattern, maskType, replacement);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个部分脱敏规则
|
||||
*
|
||||
* @param name 规则名称
|
||||
* @param pattern 匹配模式(正则表达式)
|
||||
* @param preserveLeft 保留左侧字符数
|
||||
* @param preserveRight 保留右侧字符数
|
||||
* @param maskChar 脱敏字符
|
||||
* @return 部分脱敏规则
|
||||
*/
|
||||
public static RichTextMaskingRule createPartialMaskRule(final String name, final String pattern,
|
||||
final int preserveLeft, final int preserveRight,
|
||||
final char maskChar) {
|
||||
return new RichTextMaskingRule(name, pattern, preserveLeft, preserveRight, maskChar);
|
||||
}
|
||||
}
|
@@ -73,7 +73,7 @@ import java.util.concurrent.atomic.AtomicLong;
|
||||
*
|
||||
* @since 4.1.11
|
||||
*/
|
||||
public class UUID implements java.io.Serializable, Comparable<UUID> {
|
||||
public final class UUID implements java.io.Serializable, Comparable<UUID> {
|
||||
private static final long serialVersionUID = -1185015143654744140L;
|
||||
|
||||
/**
|
||||
|
@@ -0,0 +1,296 @@
|
||||
package org.dromara.hutool.core.data.masking;
|
||||
|
||||
import org.dromara.hutool.core.text.StrUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 富文本脱敏处理器,用于对富文本内容进行脱敏处理
|
||||
*
|
||||
* @author xjf
|
||||
*/
|
||||
public class RichTextMaskingProcessor {
|
||||
|
||||
/**
|
||||
* 脱敏规则列表
|
||||
*/
|
||||
private final List<RichTextMaskingRule> rules = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 是否保留HTML标签
|
||||
*/
|
||||
private boolean preserveHtmlTags = true;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public RichTextMaskingProcessor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param preserveHtmlTags 是否保留HTML标签
|
||||
*/
|
||||
public RichTextMaskingProcessor(final boolean preserveHtmlTags) {
|
||||
this.preserveHtmlTags = preserveHtmlTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加脱敏规则
|
||||
*
|
||||
* @param rule 脱敏规则
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingProcessor addRule(final RichTextMaskingRule rule) {
|
||||
this.rules.add(rule);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对文本内容进行脱敏处理
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @return 脱敏后的文本
|
||||
*/
|
||||
public String mask(final String text) {
|
||||
if (StrUtil.isBlank(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
// 如果是HTML内容,则需要特殊处理
|
||||
if (preserveHtmlTags && isHtmlContent(text)) {
|
||||
return maskHtmlContent(text);
|
||||
} else {
|
||||
// 普通文本直接处理
|
||||
return maskPlainText(text);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为HTML内容
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @return 是否为HTML内容
|
||||
*/
|
||||
private boolean isHtmlContent(final String text) {
|
||||
// 简单判断是否包含HTML标签
|
||||
return text.contains("<") && text.contains(">") &&
|
||||
(text.contains("</") || text.contains("/>"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 对HTML内容进行脱敏处理
|
||||
*
|
||||
* @param html HTML内容
|
||||
* @return 脱敏后的HTML
|
||||
*/
|
||||
private String maskHtmlContent(final String html) {
|
||||
final StringBuilder result = new StringBuilder();
|
||||
int lastIndex = 0;
|
||||
boolean inTag = false;
|
||||
String currentTag = null;
|
||||
|
||||
for (int i = 0; i < html.length(); i++) {
|
||||
final char c = html.charAt(i);
|
||||
|
||||
if (c == '<') {
|
||||
// 处理标签前的文本内容
|
||||
if (!inTag && i > lastIndex) {
|
||||
final String textContent = html.substring(lastIndex, i);
|
||||
result.append(processTextContentWithContext(textContent, currentTag));
|
||||
}
|
||||
|
||||
inTag = true;
|
||||
lastIndex = i;
|
||||
|
||||
// 尝试获取当前标签名
|
||||
int tagNameStart = i + 1;
|
||||
if (tagNameStart < html.length()) {
|
||||
// 跳过结束标签的斜杠
|
||||
if (html.charAt(tagNameStart) == '/') {
|
||||
tagNameStart++;
|
||||
}
|
||||
|
||||
// 查找标签名结束位置
|
||||
int tagNameEnd = html.indexOf(' ', tagNameStart);
|
||||
if (tagNameEnd == -1) {
|
||||
tagNameEnd = html.indexOf('>', tagNameStart);
|
||||
}
|
||||
|
||||
if (tagNameEnd > tagNameStart) {
|
||||
currentTag = html.substring(tagNameStart, tagNameEnd).toLowerCase();
|
||||
}
|
||||
}
|
||||
} else if (c == '>' && inTag) {
|
||||
inTag = false;
|
||||
result.append(html, lastIndex, i + 1); // 保留标签
|
||||
lastIndex = i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最后一部分
|
||||
if (lastIndex < html.length()) {
|
||||
if (inTag) {
|
||||
// 如果还在标签内,直接添加剩余部分
|
||||
result.append(html.substring(lastIndex));
|
||||
} else {
|
||||
// 处理最后的文本内容
|
||||
final String textContent = html.substring(lastIndex);
|
||||
result.append(processTextContentWithContext(textContent, currentTag));
|
||||
}
|
||||
}
|
||||
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据上下文处理文本内容
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @param tagName 当前所在的标签名
|
||||
* @return 处理后的文本
|
||||
*/
|
||||
private String processTextContentWithContext(final String text, final String tagName) {
|
||||
if (StrUtil.isBlank(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
String result = text;
|
||||
|
||||
for (final RichTextMaskingRule rule : rules) {
|
||||
// 检查是否需要根据标签进行过滤
|
||||
if (tagName != null) {
|
||||
// 如果设置了只包含特定标签且当前标签不在列表中,则跳过
|
||||
if (!rule.getIncludeTags().isEmpty() && !rule.getIncludeTags().contains(tagName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果当前标签在排除列表中,则跳过
|
||||
if (rule.getExcludeTags().contains(tagName)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 应用脱敏规则
|
||||
result = applyMaskingRule(result, rule);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对普通文本进行脱敏处理
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @return 脱敏后的文本
|
||||
*/
|
||||
private String maskPlainText(final String text) {
|
||||
String result = text;
|
||||
|
||||
for (final RichTextMaskingRule rule : rules) {
|
||||
result = applyMaskingRule(result, rule);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用脱敏规则
|
||||
*
|
||||
* @param text 文本内容
|
||||
* @param rule 脱敏规则
|
||||
* @return 脱敏后的文本
|
||||
*/
|
||||
private String applyMaskingRule(final String text, final RichTextMaskingRule rule) {
|
||||
if (StrUtil.isBlank(text) || StrUtil.isBlank(rule.getPattern())) {
|
||||
return text;
|
||||
}
|
||||
|
||||
final Pattern pattern = Pattern.compile(rule.getPattern());
|
||||
final Matcher matcher = pattern.matcher(text);
|
||||
|
||||
final StringBuffer sb = new StringBuffer();
|
||||
|
||||
while (matcher.find()) {
|
||||
final String matched = matcher.group();
|
||||
final String replacement;
|
||||
|
||||
switch (rule.getMaskType()) {
|
||||
case FULL:
|
||||
// 完全脱敏,用脱敏字符替换整个匹配内容
|
||||
replacement = StrUtil.repeat(rule.getMaskChar(), matched.length());
|
||||
break;
|
||||
|
||||
case PARTIAL:
|
||||
// 部分脱敏,保留部分原始内容
|
||||
replacement = partialMask(matched, rule.getPreserveLeft(), rule.getPreserveRight(), rule.getMaskChar());
|
||||
break;
|
||||
|
||||
case REPLACE:
|
||||
// 替换脱敏,用指定文本替换
|
||||
replacement = rule.getReplacement();
|
||||
break;
|
||||
|
||||
default:
|
||||
replacement = matched;
|
||||
break;
|
||||
}
|
||||
|
||||
// 处理正则表达式中的特殊字符
|
||||
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement));
|
||||
}
|
||||
|
||||
matcher.appendTail(sb);
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 部分脱敏,保留部分原始内容
|
||||
*
|
||||
* @param text 原文本
|
||||
* @param preserveLeft 保留左侧字符数
|
||||
* @param preserveRight 保留右侧字符数
|
||||
* @param maskChar 脱敏字符
|
||||
* @return 脱敏后的文本
|
||||
*/
|
||||
private String partialMask(final String text, int preserveLeft, int preserveRight, final char maskChar) {
|
||||
if (StrUtil.isBlank(text)) {
|
||||
return text;
|
||||
}
|
||||
|
||||
final int length = text.length();
|
||||
|
||||
// 调整保留字符数,确保不超过文本长度
|
||||
preserveLeft = Math.min(preserveLeft, length);
|
||||
preserveRight = Math.min(preserveRight, length - preserveLeft);
|
||||
|
||||
// 计算需要脱敏的字符数
|
||||
final int maskLength = length - preserveLeft - preserveRight;
|
||||
|
||||
if (maskLength <= 0) {
|
||||
return text;
|
||||
}
|
||||
|
||||
final StringBuilder sb = new StringBuilder(length);
|
||||
|
||||
// 添加左侧保留的字符
|
||||
if (preserveLeft > 0) {
|
||||
sb.append(text, 0, preserveLeft);
|
||||
}
|
||||
|
||||
// 添加脱敏字符
|
||||
sb.append(StrUtil.repeat(maskChar, maskLength));
|
||||
|
||||
// 添加右侧保留的字符
|
||||
if (preserveRight > 0) {
|
||||
sb.append(text, length - preserveRight, length);
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
@@ -0,0 +1,344 @@
|
||||
package org.dromara.hutool.core.data.masking;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 富文本脱敏规则,用于配置如何对富文本内容进行脱敏处理
|
||||
*
|
||||
* @author xjf
|
||||
*/
|
||||
public class RichTextMaskingRule {
|
||||
|
||||
/**
|
||||
* 脱敏类型枚举
|
||||
*/
|
||||
public enum MaskType {
|
||||
/**
|
||||
* 完全脱敏,将匹配的内容完全替换为指定字符
|
||||
*/
|
||||
FULL,
|
||||
|
||||
/**
|
||||
* 部分脱敏,保留部分原始内容
|
||||
*/
|
||||
PARTIAL,
|
||||
|
||||
/**
|
||||
* 替换脱敏,将匹配的内容替换为指定的替换文本
|
||||
*/
|
||||
REPLACE
|
||||
}
|
||||
|
||||
/**
|
||||
* 规则名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 匹配模式(正则表达式)
|
||||
*/
|
||||
private String pattern;
|
||||
|
||||
/**
|
||||
* 脱敏类型
|
||||
*/
|
||||
private MaskType maskType;
|
||||
|
||||
/**
|
||||
* 替换内容
|
||||
*/
|
||||
private String replacement;
|
||||
|
||||
/**
|
||||
* 保留左侧字符数(用于PARTIAL类型)
|
||||
*/
|
||||
private int preserveLeft;
|
||||
|
||||
/**
|
||||
* 保留右侧字符数(用于PARTIAL类型)
|
||||
*/
|
||||
private int preserveRight;
|
||||
|
||||
/**
|
||||
* 脱敏字符
|
||||
*/
|
||||
private char maskChar = '*';
|
||||
|
||||
/**
|
||||
* 是否处理HTML标签内容
|
||||
*/
|
||||
private boolean processHtmlTags = false;
|
||||
|
||||
/**
|
||||
* 需要排除的HTML标签
|
||||
*/
|
||||
private Set<String> excludeTags = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 仅处理指定的HTML标签
|
||||
*/
|
||||
private Set<String> includeTags = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*/
|
||||
public RichTextMaskingRule() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param name 规则名称
|
||||
* @param pattern 匹配模式(正则表达式)
|
||||
* @param maskType 脱敏类型
|
||||
* @param replacement 替换内容
|
||||
*/
|
||||
public RichTextMaskingRule(final String name, final String pattern, final MaskType maskType, final String replacement) {
|
||||
this.name = name;
|
||||
this.pattern = pattern;
|
||||
this.maskType = maskType;
|
||||
this.replacement = replacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数,用于部分脱敏
|
||||
*
|
||||
* @param name 规则名称
|
||||
* @param pattern 匹配模式(正则表达式)
|
||||
* @param preserveLeft 保留左侧字符数
|
||||
* @param preserveRight 保留右侧字符数
|
||||
* @param maskChar 脱敏字符
|
||||
*/
|
||||
public RichTextMaskingRule(final String name, final String pattern, final int preserveLeft, final int preserveRight, final char maskChar) {
|
||||
this.name = name;
|
||||
this.pattern = pattern;
|
||||
this.maskType = MaskType.PARTIAL;
|
||||
this.preserveLeft = preserveLeft;
|
||||
this.preserveRight = preserveRight;
|
||||
this.maskChar = maskChar;
|
||||
}
|
||||
|
||||
// Getter and Setter methods
|
||||
|
||||
/**
|
||||
* 获取规则名称
|
||||
*
|
||||
* @return 规则名称
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置规则名称
|
||||
*
|
||||
* @param name 名称
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setName(final String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取匹配模式(正则表达式)
|
||||
* @return 匹配模式(正则表达式)
|
||||
*/
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置匹配模式(正则表达式)
|
||||
* @param pattern 匹配模式(正则表达式)
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setPattern(final String pattern) {
|
||||
this.pattern = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取脱敏类型
|
||||
*
|
||||
* @return 脱敏类型
|
||||
*/
|
||||
public MaskType getMaskType() {
|
||||
return maskType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置脱敏类型
|
||||
*
|
||||
* @param maskType 脱敏类型
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setMaskType(final MaskType maskType) {
|
||||
this.maskType = maskType;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取替换内容
|
||||
*
|
||||
* @return 替换内容
|
||||
*/
|
||||
public String getReplacement() {
|
||||
return replacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置替换内容
|
||||
*
|
||||
* @param replacement 替换内容
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setReplacement(final String replacement) {
|
||||
this.replacement = replacement;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保留左侧字符数
|
||||
*
|
||||
* @return 保留左侧字符数
|
||||
*/
|
||||
public int getPreserveLeft() {
|
||||
return preserveLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置保留左侧字符数
|
||||
*
|
||||
* @param preserveLeft 保留左侧字符数
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setPreserveLeft(final int preserveLeft) {
|
||||
this.preserveLeft = preserveLeft;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取保留右侧字符数
|
||||
*
|
||||
* @return 保留右侧字符数
|
||||
*/
|
||||
public int getPreserveRight() {
|
||||
return preserveRight;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置保留右侧字符数
|
||||
*
|
||||
* @param preserveRight 保留右侧字符数
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setPreserveRight(final int preserveRight) {
|
||||
this.preserveRight = preserveRight;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取脱敏字符
|
||||
*
|
||||
* @return 脱敏字符
|
||||
*/
|
||||
public char getMaskChar() {
|
||||
return maskChar;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置脱敏字符
|
||||
*
|
||||
* @param maskChar 脱敏字符
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setMaskChar(final char maskChar) {
|
||||
this.maskChar = maskChar;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取是否处理HTML标签内容
|
||||
*
|
||||
* @return 是否处理HTML标签内容
|
||||
*/
|
||||
public boolean isProcessHtmlTags() {
|
||||
return processHtmlTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否处理HTML标签内容
|
||||
*
|
||||
* @param processHtmlTags 是否处理HTML标签内容
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setProcessHtmlTags(final boolean processHtmlTags) {
|
||||
this.processHtmlTags = processHtmlTags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要排除的HTML标签
|
||||
*
|
||||
* @return 需要排除的HTML标签
|
||||
*/
|
||||
public Set<String> getExcludeTags() {
|
||||
return excludeTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置需要排除的HTML标签
|
||||
*
|
||||
* @param excludeTags 需要排除的HTML标签
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setExcludeTags(final Set<String> excludeTags) {
|
||||
this.excludeTags = excludeTags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加需要排除的HTML标签
|
||||
*
|
||||
* @param tag 需要排除的HTML标签
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule addExcludeTag(final String tag) {
|
||||
this.excludeTags.add(tag.toLowerCase());
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取仅处理指定的HTML标签
|
||||
*
|
||||
* @return 仅处理指定的HTML标签
|
||||
*/
|
||||
public Set<String> getIncludeTags() {
|
||||
return includeTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置仅处理指定的HTML标签
|
||||
*
|
||||
* @param includeTags 仅处理指定的HTML标签
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule setIncludeTags(final Set<String> includeTags) {
|
||||
this.includeTags = includeTags;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加仅处理指定的HTML标签
|
||||
*
|
||||
* @param tag 仅处理指定的HTML标签
|
||||
* @return this
|
||||
*/
|
||||
public RichTextMaskingRule addIncludeTag(final String tag) {
|
||||
this.includeTags.add(tag.toLowerCase());
|
||||
return this;
|
||||
}
|
||||
}
|
@@ -46,7 +46,7 @@ import java.util.stream.Collectors;
|
||||
* 日期时间工具类
|
||||
*
|
||||
* @author Looly
|
||||
* @see TimeUtil java8日志工具类
|
||||
* @see TimeUtil java8日期工具类
|
||||
* @see DateFormatPool 日期常用格式工具类
|
||||
*/
|
||||
public class DateUtil {
|
||||
|
@@ -345,7 +345,7 @@ public class ChineseDate {
|
||||
* @return 获得农历节日
|
||||
*/
|
||||
public String getFestivals() {
|
||||
return StrUtil.join(",", LunarFestival.getFestivals(this.year, this.month, day));
|
||||
return StrUtil.join(",", LunarFestival.getFestivals(this.year, this.isLeapMonth ? this.month - 1 : this.month, day));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -314,8 +314,8 @@ public class FileUtil {
|
||||
}
|
||||
|
||||
// 如果用户需要相对项目路径,则使用project:前缀
|
||||
if (path.startsWith("project:")) {
|
||||
return new File(path);
|
||||
if (path.startsWith(UrlUtil.PROJECT_URL_PREFIX)) {
|
||||
return new File(StrUtil.subSuf(path, UrlUtil.PROJECT_URL_PREFIX.length()));
|
||||
}
|
||||
|
||||
return new File(getAbsolutePath(path));
|
||||
|
@@ -243,7 +243,7 @@ public class ResourceUtil {
|
||||
*/
|
||||
public static Resource getResource(final String path) {
|
||||
if (StrUtil.isNotBlank(path)) {
|
||||
if (path.startsWith(UrlUtil.FILE_URL_PREFIX) || FileUtil.isAbsolutePath(path)) {
|
||||
if (StrUtil.startWithAny(path, UrlUtil.FILE_URL_PREFIX, UrlUtil.PROJECT_URL_PREFIX) || FileUtil.isAbsolutePath(path)) {
|
||||
return new FileResource(path);
|
||||
}
|
||||
}
|
||||
|
@@ -310,7 +310,7 @@ public class Money implements Serializable, Comparable<Money> {
|
||||
*/
|
||||
public void setAmount(final BigDecimal amount) {
|
||||
if (amount != null) {
|
||||
cent = rounding(amount.movePointRight(2), DEFAULT_ROUNDING_MODE);
|
||||
cent = rounding(amount.movePointRight(currency.getDefaultFractionDigits()), DEFAULT_ROUNDING_MODE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -742,7 +742,7 @@ public class Money implements Serializable, Comparable<Money> {
|
||||
final Money lowResult = newMoneyWithSameCurrency(cent / targets);
|
||||
final Money highResult = newMoneyWithSameCurrency(lowResult.cent + 1);
|
||||
|
||||
final int remainder = (int) cent % targets;
|
||||
final int remainder = (int) (cent % targets);
|
||||
|
||||
for (int i = 0; i < remainder; i++) {
|
||||
results[i] = highResult;
|
||||
|
@@ -55,6 +55,10 @@ public class UrlUtil {
|
||||
* 针对ClassPath路径的伪协议前缀(兼容Spring): "classpath:"
|
||||
*/
|
||||
public static final String CLASSPATH_URL_PREFIX = "classpath:";
|
||||
/**
|
||||
* 针对project路径的伪协议前缀: "project:"
|
||||
*/
|
||||
public static final String PROJECT_URL_PREFIX = "project:";
|
||||
/**
|
||||
* URL 前缀表示文件: "file:"
|
||||
*/
|
||||
|
@@ -105,8 +105,6 @@ public class PatternPool {
|
||||
public final static Pattern TEL = Pattern.compile(RegexPool.TEL);
|
||||
/**
|
||||
* 座机号码+400+800电话
|
||||
*
|
||||
* @see <a href="https://baike.baidu.com/item/800">800</a>
|
||||
*/
|
||||
public final static Pattern TEL_400_800 = Pattern.compile(RegexPool.TEL_400_800);
|
||||
/**
|
||||
|
@@ -99,10 +99,8 @@ public interface RegexPool {
|
||||
String TEL = "(010|02\\d|0[3-9]\\d{2})-?(\\d{6,8})";
|
||||
/**
|
||||
* 座机号码+400+800电话
|
||||
*
|
||||
* @see <a href="https://baike.baidu.com/item/800">800</a>
|
||||
*/
|
||||
String TEL_400_800 = "0\\d{2,3}[\\- ]?[1-9]\\d{6,7}|[48]00[\\- ]?[1-9]\\d{2}[\\- ]?\\d{4}";
|
||||
String TEL_400_800 = "0\\d{2,3}[\\- ]?[0-9]\\d{6,7}|[48]00[\\- ]?[0-9]\\d{2}[\\- ]?\\d{4}";
|
||||
/**
|
||||
* 18位身份证号码
|
||||
*/
|
||||
|
@@ -472,7 +472,7 @@ public class CollectorUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将一个{@code Collection<T>}两个属性分流至两个ArrayList,并使用Pair收集。
|
||||
* 将一个{@code Collection<T>}两个属性分流至两个List,并使用Pair收集。
|
||||
*
|
||||
* @param lMapper 左属性收集方法
|
||||
* @param rMapper 右属性收集方法
|
||||
@@ -482,53 +482,72 @@ public class CollectorUtil {
|
||||
* @return {@code Pair<List<L>,List<R>>} Pair收集的两个List
|
||||
* @author Tanglt
|
||||
*/
|
||||
public static <T, L, R> Collector<T, Pair<List<L>, List<R>>, Pair<List<L>, List<R>>> toPairList(final Function<T, L> lMapper,
|
||||
final Function<T, R> rMapper) {
|
||||
return toPairCollection(lMapper, rMapper, ArrayList::new, ArrayList::new);
|
||||
public static <T, L, R> Collector<T, ?, Pair<List<L>, List<R>>> toPairList(final Function<? super T, ? extends L> lMapper,
|
||||
final Function<? super T, ? extends R> rMapper) {
|
||||
return toPair(lMapper, rMapper, Collectors.toList(), Collectors.toList());
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将一个{@code Collection<T>}两个属性分流至两个Collection,并使用Pair收集。需要指定Collection类型
|
||||
* 将一个{@code Collection<T>}两个属性分流至两个Collection,并使用Pair收集。
|
||||
*
|
||||
* @param lMapper 左属性收集方法
|
||||
* @param rMapper 右属性收集方法
|
||||
* @param newCollectionL 左属性Collection创建方法
|
||||
* @param newCollectionR 右属性Collection创建方法
|
||||
* @param <T> 元素类型
|
||||
* @param <L> 左属性类型
|
||||
* @param <R> 右属性类型
|
||||
* @param <LC> 左分流Collection类型
|
||||
* @param <RC> 右分流Collection类型
|
||||
* @return {@code Pair<C<L>,C<R>>} Pair收集的两个List
|
||||
* @param lMapper 左属性收集方法
|
||||
* @param rMapper 右属性收集方法
|
||||
* @param lDownstream 左属性下游操作
|
||||
* @param rDownstream 右属性下游操作
|
||||
* @param <T> 元素类型
|
||||
* @param <LU> 左属性类型
|
||||
* @param <LA> 左属性收集类型
|
||||
* @param <LR> 左属性收集最终类型
|
||||
* @param <RU> 左属性类型
|
||||
* @param <RA> 左属性收集类型
|
||||
* @param <RR> 左属性收集最终类型
|
||||
* @return {@code Pair<LR,RR>} Pair收集的结果
|
||||
* @author Tanglt
|
||||
*/
|
||||
public static <T, L, R, LC extends Collection<L>, RC extends Collection<R>>
|
||||
Collector<T, Pair<LC, RC>, Pair<LC, RC>> toPairCollection(final Function<T, L> lMapper,
|
||||
final Function<T, R> rMapper,
|
||||
final Supplier<LC> newCollectionL,
|
||||
final Supplier<RC> newCollectionR) {
|
||||
return new SimpleCollector<>(() -> Pair.of(newCollectionL.get(), newCollectionR.get()),
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T, LU, LA, LR, RU, RA, RR>
|
||||
Collector<T, ?, Pair<LR, RR>> toPair(final Function<? super T, ? extends LU> lMapper,
|
||||
final Function<? super T, ? extends RU> rMapper,
|
||||
final Collector<? super LU, LA, LR> lDownstream,
|
||||
final Collector<? super RU, RA, RR> rDownstream
|
||||
) {
|
||||
return new SimpleCollector<>(
|
||||
() -> Pair.of(lDownstream.supplier().get(), rDownstream.supplier().get()),
|
||||
|
||||
(listPair, element) -> {
|
||||
final L lValue = lMapper.apply(element);
|
||||
if (lValue != null) {
|
||||
listPair.getLeft().add(lValue);
|
||||
}
|
||||
final R rValue = rMapper.apply(element);
|
||||
if (rValue != null) {
|
||||
listPair.getRight().add(rValue);
|
||||
}
|
||||
lDownstream.accumulator().accept(listPair.getLeft(), lMapper.apply(element));
|
||||
rDownstream.accumulator().accept(listPair.getRight(), rMapper.apply(element));
|
||||
},
|
||||
(listPair1, listPair2) -> {
|
||||
listPair1.getLeft().addAll(listPair2.getLeft());
|
||||
listPair1.getRight().addAll(listPair2.getRight());
|
||||
return listPair1;
|
||||
|
||||
(listPair1, listPair2) ->
|
||||
Pair.of(lDownstream.combiner().apply(listPair1.getLeft(), listPair2.getLeft()),
|
||||
rDownstream.combiner().apply(listPair1.getRight(), listPair2.getRight())),
|
||||
|
||||
finisherPair -> {
|
||||
final LR finisherLeftValue;
|
||||
if (lDownstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
|
||||
finisherLeftValue = (LR) finisherPair.getLeft();
|
||||
} else {
|
||||
finisherLeftValue = lDownstream.finisher().apply(finisherPair.getLeft());
|
||||
}
|
||||
|
||||
final RR finisherRightValue;
|
||||
if (lDownstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
|
||||
finisherRightValue = (RR) finisherPair.getRight();
|
||||
} else {
|
||||
finisherRightValue = rDownstream.finisher().apply(finisherPair.getRight());
|
||||
}
|
||||
|
||||
return Pair.of(finisherLeftValue, finisherRightValue);
|
||||
},
|
||||
CH_ID);
|
||||
CH_NOID);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将一个{@code Collection<T>}三个属性分流至三个ArrayList,并使用Triple收集。
|
||||
* 将一个{@code Collection<T>}三个属性分流至三个List,并使用Triple收集。
|
||||
*
|
||||
* @param lMapper 左属性收集方法
|
||||
* @param mMapper 中属性收集方法
|
||||
@@ -541,64 +560,83 @@ public class CollectorUtil {
|
||||
* @author Tanglt
|
||||
*/
|
||||
public static <T, L, M, R>
|
||||
Collector<T, Triple<List<L>, List<M>, List<R>>, Triple<List<L>, List<M>, List<R>>> toTripleList(final Function<T, L> lMapper,
|
||||
final Function<T, M> mMapper,
|
||||
final Function<T, R> rMapper) {
|
||||
return toTripleCollection(lMapper, mMapper, rMapper, ArrayList::new, ArrayList::new, ArrayList::new);
|
||||
Collector<T, ?, Triple<List<L>, List<M>, List<R>>> toTripleList(final Function<? super T, ? extends L> lMapper,
|
||||
final Function<? super T, ? extends M> mMapper,
|
||||
final Function<? super T, ? extends R> rMapper) {
|
||||
return toTriple(lMapper, mMapper, rMapper, Collectors.toList(), Collectors.toList(), Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 将一个{@code Collection<T>}两个属性分流至两个Collection,并使用Triple收集。需要指定Collection类型
|
||||
* 将一个{@code Collection<T>}两个属性分流至两个Collection,并使用Pair收集。
|
||||
*
|
||||
* @param lMapper 左属性收集方法
|
||||
* @param mMapper 中属性收集方法
|
||||
* @param rMapper 右属性收集方法
|
||||
* @param newCollectionL 左属性Collection创建方法
|
||||
* @param newCollectionM 中属性Collection创建方法
|
||||
* @param newCollectionR 右属性Collection创建方法
|
||||
* @param <T> 元素类型
|
||||
* @param <L> 左属性类型
|
||||
* @param <M> 中属性类型
|
||||
* @param <R> 右属性类型
|
||||
* @param <LC> 左分流Collection类型
|
||||
* @param <MC> 中分流Collection类型
|
||||
* @param <RC> 右分流Collection类型
|
||||
* @return {@code Triple<LC<L>,MC<M>,RC<R>>} Triple收集的三个List
|
||||
* @param lMapper 左元素收集方法
|
||||
* @param mMapper 中元素收集方法
|
||||
* @param rMapper 右元素收集方法
|
||||
* @param lDownstream 左元素下游操作
|
||||
* @param mDownstream 中元素下游操作
|
||||
* @param rDownstream 右元素下游操作
|
||||
* @param <T> 元素类型
|
||||
* @param <LU> 左属性类型
|
||||
* @param <LA> 左属性收集类型
|
||||
* @param <LR> 左属性收集最终类型
|
||||
* @param <MU> 中属性类型
|
||||
* @param <MA> 中属性收集类型
|
||||
* @param <MR> 中属性收集最终类型
|
||||
* @param <RU> 左属性类型
|
||||
* @param <RA> 左属性收集类型
|
||||
* @param <RR> 左属性收集最终类型
|
||||
* @return {@code Triple<LR,MR,RR>} Triple收集的结果
|
||||
* @author Tanglt
|
||||
*/
|
||||
public static <T, L, M, R,
|
||||
LC extends Collection<L>,
|
||||
MC extends Collection<M>,
|
||||
RC extends Collection<R>>
|
||||
Collector<T, Triple<LC, MC, RC>, Triple<LC, MC, RC>> toTripleCollection(final Function<T, L> lMapper,
|
||||
final Function<T, M> mMapper,
|
||||
final Function<T, R> rMapper,
|
||||
final Supplier<LC> newCollectionL,
|
||||
final Supplier<MC> newCollectionM,
|
||||
final Supplier<RC> newCollectionR) {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <T, LU, LA, LR, MU, MA, MR, RU, RA, RR>
|
||||
Collector<T, ?, Triple<LR, MR, RR>> toTriple(final Function<? super T, ? extends LU> lMapper,
|
||||
final Function<? super T, ? extends MU> mMapper,
|
||||
final Function<? super T, ? extends RU> rMapper,
|
||||
final Collector<? super LU, LA, LR> lDownstream,
|
||||
final Collector<? super MU, MA, MR> mDownstream,
|
||||
final Collector<? super RU, RA, RR> rDownstream
|
||||
) {
|
||||
return new SimpleCollector<>(
|
||||
() -> Triple.of(newCollectionL.get(), newCollectionM.get(), newCollectionR.get()),
|
||||
() -> Triple.of(lDownstream.supplier().get(), mDownstream.supplier().get(), rDownstream.supplier().get()),
|
||||
|
||||
(listTriple, element) -> {
|
||||
final L lValue = lMapper.apply(element);
|
||||
if (lValue != null) {
|
||||
listTriple.getLeft().add(lValue);
|
||||
}
|
||||
final M mValue = mMapper.apply(element);
|
||||
if (mValue != null) {
|
||||
listTriple.getMiddle().add(mValue);
|
||||
}
|
||||
final R rValue = rMapper.apply(element);
|
||||
if (rValue != null) {
|
||||
listTriple.getRight().add(rValue);
|
||||
}
|
||||
lDownstream.accumulator().accept(listTriple.getLeft(), lMapper.apply(element));
|
||||
mDownstream.accumulator().accept(listTriple.getMiddle(), mMapper.apply(element));
|
||||
rDownstream.accumulator().accept(listTriple.getRight(), rMapper.apply(element));
|
||||
},
|
||||
(listTriple1, listTriple2) -> {
|
||||
listTriple1.getLeft().addAll(listTriple2.getLeft());
|
||||
listTriple1.getMiddle().addAll(listTriple2.getMiddle());
|
||||
listTriple1.getRight().addAll(listTriple2.getRight());
|
||||
return listTriple1;
|
||||
|
||||
(listTriple1, listTriple2) ->
|
||||
Triple.of(lDownstream.combiner().apply(listTriple1.getLeft(), listTriple2.getLeft()),
|
||||
mDownstream.combiner().apply(listTriple1.getMiddle(), listTriple2.getMiddle()),
|
||||
rDownstream.combiner().apply(listTriple1.getRight(), listTriple2.getRight())),
|
||||
|
||||
finisherTriple -> {
|
||||
final LR finisherLeftValue;
|
||||
if (lDownstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
|
||||
finisherLeftValue = (LR) finisherTriple.getLeft();
|
||||
} else {
|
||||
finisherLeftValue = lDownstream.finisher().apply(finisherTriple.getLeft());
|
||||
}
|
||||
|
||||
final MR finisherMiddleValue;
|
||||
if (mDownstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
|
||||
finisherMiddleValue = (MR) finisherTriple.getMiddle();
|
||||
} else {
|
||||
finisherMiddleValue = mDownstream.finisher().apply(finisherTriple.getMiddle());
|
||||
}
|
||||
|
||||
final RR finisherRightValue;
|
||||
if (lDownstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) {
|
||||
finisherRightValue = (RR) finisherTriple.getRight();
|
||||
} else {
|
||||
finisherRightValue = rDownstream.finisher().apply(finisherTriple.getRight());
|
||||
}
|
||||
|
||||
return Triple.of(finisherLeftValue, finisherMiddleValue, finisherRightValue);
|
||||
},
|
||||
CH_ID);
|
||||
CH_NOID);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -3664,6 +3664,68 @@ public class CharSequenceUtil extends StrValidator {
|
||||
|
||||
// region ----- lower and upper
|
||||
|
||||
/**
|
||||
* 将字符串转为小写
|
||||
*
|
||||
* @param str 被转的字符串
|
||||
* @return 转换后的字符串
|
||||
* @see String#toLowerCase()
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static String toLowerCase(final CharSequence str) {
|
||||
return toLowerCase(str, Locale.getDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转为小写
|
||||
*
|
||||
* @param str 被转的字符串
|
||||
* @param locale Locale
|
||||
* @return 转换后的字符串
|
||||
* @see String#toLowerCase()
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public static String toLowerCase(final CharSequence str, final Locale locale) {
|
||||
if (null == str) {
|
||||
return null;
|
||||
}
|
||||
if(0 == str.length()){
|
||||
return EMPTY;
|
||||
}
|
||||
return str.toString().toLowerCase(locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转为大写
|
||||
*
|
||||
* @param str 被转的字符串
|
||||
* @return 转换后的字符串
|
||||
* @see String#toUpperCase()
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public static String toUpperCase(final CharSequence str) {
|
||||
return toUpperCase(str, Locale.getDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将字符串转为大写
|
||||
*
|
||||
* @param str 被转的字符串
|
||||
* @param locale Locale
|
||||
* @return 转换后的字符串
|
||||
* @see String#toUpperCase()
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public static String toUpperCase(final CharSequence str, final Locale locale) {
|
||||
if (null == str) {
|
||||
return null;
|
||||
}
|
||||
if(0 == str.length()){
|
||||
return EMPTY;
|
||||
}
|
||||
return str.toString().toUpperCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* 原字符串首字母大写并在其首部添加指定字符串 例如:str=name, preString=get =》 return getName
|
||||
*
|
||||
|
@@ -213,7 +213,7 @@ public class StrValidator {
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否存都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
|
||||
* 是否全都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
|
||||
*
|
||||
* @param args 被检查的对象,一个或者多个
|
||||
* @return 是否都不为空
|
||||
|
@@ -37,7 +37,7 @@ public class RetryUtil {
|
||||
* 没有返回值,重试执行方法
|
||||
*
|
||||
* @param run 执行方法
|
||||
* @param maxAttempts 最大的重试次数
|
||||
* @param maxAttempts 最大的重试次数, 小于1不会重试, 但任务至少会被执行1次
|
||||
* @param delay 重试间隔
|
||||
* @param recover 达到最大重试次数后执行的备用方法
|
||||
* @param exs 指定的异常类型需要重试
|
||||
@@ -63,7 +63,7 @@ public class RetryUtil {
|
||||
* 有返回值,重试执行方法
|
||||
*
|
||||
* @param sup 执行方法
|
||||
* @param maxAttempts 最大的重试次数
|
||||
* @param maxAttempts 最大的重试次数, 小于1不会重试, 但任务至少会被执行1次
|
||||
* @param delay 重试间隔
|
||||
* @param recover 达到最大重试次数后执行的备用方法
|
||||
* @param exs 指定的异常类型需要重试
|
||||
@@ -88,10 +88,10 @@ public class RetryUtil {
|
||||
* 没有返回值,重试执行方法
|
||||
*
|
||||
* @param run 执行方法
|
||||
* @param maxAttempts 最大的重试次数
|
||||
* @param maxAttempts 最大的重试次数, 小于1不会重试, 但任务至少会被执行1次
|
||||
* @param delay 重试间隔
|
||||
* @param recover 达到最大重试次数后执行的备用方法
|
||||
* @param predicate 自定义重试条件
|
||||
* @param predicate 自定义重试条件, 返回true时表示重试
|
||||
*/
|
||||
public static void ofPredicate(final Runnable run, final long maxAttempts, final Duration delay,
|
||||
final Supplier<Void> recover, final BiPredicate<Void, Throwable> predicate) {
|
||||
@@ -105,14 +105,14 @@ public class RetryUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 根据异常信息进行重试
|
||||
* 根据自定义结果进行重试
|
||||
* 有返回值,重试执行方法
|
||||
*
|
||||
* @param sup 执行方法
|
||||
* @param maxAttempts 最大的重试次数
|
||||
* @param maxAttempts 最大的重试次数, 小于1不会重试, 但任务至少会被执行1次
|
||||
* @param delay 重试间隔
|
||||
* @param recover 达到最大重试次数后执行的备用方法
|
||||
* @param predicate 自定义重试条件
|
||||
* @param predicate 自定义重试条件, 返回true时表示重试
|
||||
* @param <T> 结果类型
|
||||
* @return 执行结果
|
||||
*/
|
||||
|
@@ -109,11 +109,11 @@ public class RetryableTask<T> {
|
||||
*/
|
||||
private T result;
|
||||
/**
|
||||
* 执行法方法
|
||||
* 执行方法
|
||||
*/
|
||||
private final Supplier<T> sup;
|
||||
/**
|
||||
* 重试策略
|
||||
* 重试策略, 返回true时表示重试
|
||||
*/
|
||||
private final BiPredicate<T, Throwable> predicate;
|
||||
/**
|
||||
@@ -150,8 +150,6 @@ public class RetryableTask<T> {
|
||||
* @return 当前对象
|
||||
*/
|
||||
public RetryableTask<T> maxAttempts(final long maxAttempts) {
|
||||
Assert.isTrue(this.maxAttempts > 0, "maxAttempts must be greater than 0");
|
||||
|
||||
this.maxAttempts = maxAttempts;
|
||||
return this;
|
||||
}
|
||||
@@ -223,7 +221,8 @@ public class RetryableTask<T> {
|
||||
private RetryableTask<T> doExecute() {
|
||||
Throwable th = null;
|
||||
|
||||
while (--this.maxAttempts >= 0) {
|
||||
// 任务至少被执行一次
|
||||
do {
|
||||
try {
|
||||
this.result = this.sup.get();
|
||||
} catch (final Throwable t) {
|
||||
@@ -236,8 +235,11 @@ public class RetryableTask<T> {
|
||||
break;
|
||||
}
|
||||
|
||||
ThreadUtil.sleep(delay.toMillis());
|
||||
}
|
||||
// 避免最后一次任务执行时的线程睡眠
|
||||
if (this.maxAttempts > 0) {
|
||||
ThreadUtil.sleep(delay.toMillis());
|
||||
}
|
||||
} while (--this.maxAttempts >= 0);
|
||||
|
||||
this.throwable = th;
|
||||
return this;
|
||||
|
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.dromara.hutool.core.thread.lock;
|
||||
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
|
||||
@@ -48,6 +51,105 @@ public class LockUtil {
|
||||
return new ReentrantReadWriteLock(fair);
|
||||
}
|
||||
|
||||
// region ----- SegmentLock
|
||||
/**
|
||||
* 创建分段锁(强引用),使用 ReentrantLock
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @return 分段锁实例
|
||||
*/
|
||||
public static SegmentLock<Lock> createSegmentLock(final int segments) {
|
||||
return SegmentLock.lock(segments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分段读写锁(强引用),使用 ReentrantReadWriteLock
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @return 分段读写锁实例
|
||||
*/
|
||||
public static SegmentLock<ReadWriteLock> createSegmentReadWriteLock(final int segments) {
|
||||
return SegmentLock.readWriteLock(segments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分段信号量(强引用)
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @param permits 每个信号量的许可数
|
||||
* @return 分段信号量实例
|
||||
*/
|
||||
public static SegmentLock<Semaphore> createSegmentSemaphore(final int segments, final int permits) {
|
||||
return SegmentLock.semaphore(segments, permits);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建弱引用分段锁,使用 ReentrantLock,懒加载
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @return 弱引用分段锁实例
|
||||
*/
|
||||
public static SegmentLock<Lock> createLazySegmentLock(final int segments) {
|
||||
return SegmentLock.lazyWeakLock(segments);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取分段锁(强引用)
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @param key 用于映射分段的 key
|
||||
* @return 对应的 Lock 实例
|
||||
*/
|
||||
public static Lock getSegmentLock(final int segments, final Object key) {
|
||||
return SegmentLock.lock(segments).get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取分段读锁(强引用)
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @param key 用于映射分段的 key
|
||||
* @return 对应的读锁实例
|
||||
*/
|
||||
public static Lock getSegmentReadLock(final int segments, final Object key) {
|
||||
return SegmentLock.readWriteLock(segments).get(key).readLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取分段写锁(强引用)
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @param key 用于映射分段的 key
|
||||
* @return 对应的写锁实例
|
||||
*/
|
||||
public static Lock getSegmentWriteLock(final int segments, final Object key) {
|
||||
return SegmentLock.readWriteLock(segments).get(key).writeLock();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取分段信号量(强引用)
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @param permits 每个信号量的许可数
|
||||
* @param key 用于映射分段的 key
|
||||
* @return 对应的 Semaphore 实例
|
||||
*/
|
||||
public static Semaphore getSegmentSemaphore(final int segments, final int permits, final Object key) {
|
||||
return SegmentLock.semaphore(segments, permits).get(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 key 获取弱引用分段锁,懒加载
|
||||
*
|
||||
* @param segments 分段数量,必须大于 0
|
||||
* @param key 用于映射分段的 key
|
||||
* @return 对应的 Lock 实例
|
||||
*/
|
||||
public static Lock getLazySegmentLock(final int segments, final Object key) {
|
||||
return SegmentLock.lazyWeakLock(segments).get(key);
|
||||
}
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* 获取单例的无锁对象
|
||||
*
|
||||
|
@@ -0,0 +1,523 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.core.thread.lock;
|
||||
|
||||
import org.dromara.hutool.core.collection.CollUtil;
|
||||
import org.dromara.hutool.core.collection.ListUtil;
|
||||
import org.dromara.hutool.core.lang.Assert;
|
||||
|
||||
import java.lang.ref.Reference;
|
||||
import java.lang.ref.ReferenceQueue;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicReferenceArray;
|
||||
import java.util.concurrent.locks.*;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 分段锁工具类,支持 Lock、Semaphore 和 ReadWriteLock 的分段实现。
|
||||
* <p>
|
||||
* 通过将锁分成多个段(segments),不同的操作可以并发使用不同的段,避免所有线程竞争同一把锁。
|
||||
* 相等的 key 保证映射到同一段锁(如 key1.equals(key2) 时,get(key1) 和 get(key2) 返回相同对象)。
|
||||
* 但不同 key 可能因哈希冲突映射到同一段,段数越少冲突概率越高。
|
||||
* <p>
|
||||
* 支持两种实现:
|
||||
* <ul>
|
||||
* <li>强引用:创建时初始化所有段,内存占用稳定。</li>
|
||||
* <li>弱引用:懒加载,首次使用时创建段,未使用时可被垃圾回收,适合大量段但使用较少的场景。</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param <L>
|
||||
* @author Guava,dakuo
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public abstract class SegmentLock<L> {
|
||||
|
||||
/** 当段数大于此阈值时,使用 ConcurrentMap 替代大数组以节省内存(适用于懒加载场景) */
|
||||
private static final int LARGE_LAZY_CUTOFF = 1024;
|
||||
|
||||
private SegmentLock() {}
|
||||
|
||||
/**
|
||||
* 根据 key 获取对应的锁段,保证相同 key 返回相同对象。
|
||||
*
|
||||
* @param key 非空 key
|
||||
* @return 对应的锁段
|
||||
*/
|
||||
public abstract L get(Object key);
|
||||
|
||||
/**
|
||||
* 根据索引获取锁段,索引范围为 [0, size())。
|
||||
*
|
||||
* @param index 索引
|
||||
* @return 指定索引的锁段
|
||||
*/
|
||||
public abstract L getAt(int index);
|
||||
|
||||
/**
|
||||
* 计算 key 对应的段索引。
|
||||
*
|
||||
* @param key 非空 key
|
||||
* @return 段索引
|
||||
*/
|
||||
abstract int indexFor(Object key);
|
||||
|
||||
/**
|
||||
* 获取总段数。
|
||||
*
|
||||
* @return 段数
|
||||
*/
|
||||
public abstract int size();
|
||||
|
||||
/**
|
||||
* 批量获取多个 key 对应的锁段列表,按索引升序排列,避免死锁。
|
||||
*
|
||||
* @param keys 非空 key 集合
|
||||
* @return 锁段列表(可能有重复)
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public Iterable<L> bulkGet(final Iterable<?> keys) {
|
||||
final List<Object> result = (List<Object>) ListUtil.of(keys);
|
||||
if (CollUtil.isEmpty(result)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final int[] stripes = new int[result.size()];
|
||||
for (int i = 0; i < result.size(); i++) {
|
||||
stripes[i] = indexFor(result.get(i));
|
||||
}
|
||||
Arrays.sort(stripes);
|
||||
int previousStripe = stripes[0];
|
||||
result.set(0, getAt(previousStripe));
|
||||
for (int i = 1; i < result.size(); i++) {
|
||||
final int currentStripe = stripes[i];
|
||||
if (currentStripe == previousStripe) {
|
||||
result.set(i, result.get(i - 1));
|
||||
} else {
|
||||
result.set(i, getAt(currentStripe));
|
||||
previousStripe = currentStripe;
|
||||
}
|
||||
}
|
||||
final List<L> asStripes = (List<L>) result;
|
||||
return Collections.unmodifiableList(asStripes);
|
||||
}
|
||||
|
||||
// 静态工厂方法
|
||||
|
||||
/**
|
||||
* 创建强引用的分段锁,所有段在创建时初始化。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @param supplier 锁提供者
|
||||
* @param <L> 锁类型
|
||||
* @return 分段锁实例
|
||||
*/
|
||||
public static <L> SegmentLock<L> custom(final int stripes, final Supplier<L> supplier) {
|
||||
return new CompactSegmentLock<>(stripes, supplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建强引用的可重入锁分段实例。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @return 分段锁实例
|
||||
*/
|
||||
public static SegmentLock<Lock> lock(final int stripes) {
|
||||
return custom(stripes, PaddedLock::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建弱引用的可重入锁分段实例,懒加载。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @return 分段锁实例
|
||||
*/
|
||||
public static SegmentLock<Lock> lazyWeakLock(final int stripes) {
|
||||
return lazyWeakCustom(stripes, () -> new ReentrantLock(false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建弱引用的分段锁,懒加载。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @param supplier 锁提供者
|
||||
* @param <L> 锁类型
|
||||
* @return 分段锁实例
|
||||
*/
|
||||
private static <L> SegmentLock<L> lazyWeakCustom(final int stripes, final Supplier<L> supplier) {
|
||||
return stripes < LARGE_LAZY_CUTOFF
|
||||
? new SmallLazySegmentLock<>(stripes, supplier)
|
||||
: new LargeLazySegmentLock<>(stripes, supplier);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建强引用的信号量分段实例。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @param permits 每个信号量的许可数
|
||||
* @return 分段信号量实例
|
||||
*/
|
||||
public static SegmentLock<Semaphore> semaphore(final int stripes, final int permits) {
|
||||
return custom(stripes, () -> new PaddedSemaphore(permits));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建弱引用的信号量分段实例,懒加载。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @param permits 每个信号量的许可数
|
||||
* @return 分段信号量实例
|
||||
*/
|
||||
public static SegmentLock<Semaphore> lazyWeakSemaphore(final int stripes, final int permits) {
|
||||
return lazyWeakCustom(stripes, () -> new Semaphore(permits, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建强引用的读写锁分段实例。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @return 分段读写锁实例
|
||||
*/
|
||||
public static SegmentLock<ReadWriteLock> readWriteLock(final int stripes) {
|
||||
return custom(stripes, ReentrantReadWriteLock::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建弱引用的读写锁分段实例,懒加载。
|
||||
*
|
||||
* @param stripes 段数
|
||||
* @return 分段读写锁实例
|
||||
*/
|
||||
public static SegmentLock<ReadWriteLock> lazyWeakReadWriteLock(final int stripes) {
|
||||
return lazyWeakCustom(stripes, WeakSafeReadWriteLock::new);
|
||||
}
|
||||
|
||||
// 内部实现类
|
||||
|
||||
/**
|
||||
* 弱引用安全的读写锁实现,确保读锁和写锁持有对自身的强引用。
|
||||
*/
|
||||
private static final class WeakSafeReadWriteLock implements ReadWriteLock {
|
||||
private final ReadWriteLock delegate;
|
||||
|
||||
WeakSafeReadWriteLock() {
|
||||
this.delegate = new ReentrantReadWriteLock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Lock readLock() {
|
||||
return new WeakSafeLock(delegate.readLock(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Lock writeLock() {
|
||||
return new WeakSafeLock(delegate.writeLock(), this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弱引用安全的锁包装类,确保持有强引用。
|
||||
*/
|
||||
private static final class WeakSafeLock implements Lock {
|
||||
private final Lock delegate;
|
||||
private final WeakSafeReadWriteLock strongReference;
|
||||
|
||||
WeakSafeLock(final Lock delegate, final WeakSafeReadWriteLock strongReference) {
|
||||
this.delegate = delegate;
|
||||
this.strongReference = strongReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lock() {
|
||||
delegate.lock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void lockInterruptibly() throws InterruptedException {
|
||||
delegate.lockInterruptibly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock() {
|
||||
return delegate.tryLock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean tryLock(final long time, final java.util.concurrent.TimeUnit unit) throws InterruptedException {
|
||||
return delegate.tryLock(time, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unlock() {
|
||||
delegate.unlock();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Condition newCondition() {
|
||||
return new WeakSafeCondition(delegate.newCondition(), strongReference);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 弱引用安全的条件包装类。
|
||||
*/
|
||||
private static final class WeakSafeCondition implements Condition {
|
||||
private final Condition delegate;
|
||||
|
||||
/** 防止垃圾回收 */
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final WeakSafeReadWriteLock strongReference;
|
||||
|
||||
WeakSafeCondition(final Condition delegate, final WeakSafeReadWriteLock strongReference) {
|
||||
this.delegate = delegate;
|
||||
this.strongReference = strongReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void await() throws InterruptedException {
|
||||
delegate.await();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void awaitUninterruptibly() {
|
||||
delegate.awaitUninterruptibly();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long awaitNanos(final long nanosTimeout) throws InterruptedException {
|
||||
return delegate.awaitNanos(nanosTimeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean await(final long time, final TimeUnit unit) throws InterruptedException {
|
||||
return delegate.await(time, unit);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitUntil(final Date deadline) throws InterruptedException {
|
||||
return delegate.awaitUntil(deadline);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signal() {
|
||||
delegate.signal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signalAll() {
|
||||
delegate.signalAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽象基类,确保段数为 2 的幂。
|
||||
*/
|
||||
private abstract static class PowerOfTwoSegmentLock<L> extends SegmentLock<L> {
|
||||
final int mask;
|
||||
|
||||
PowerOfTwoSegmentLock(final int stripes) {
|
||||
Assert.isTrue(stripes > 0, "Segment count must be positive");
|
||||
this.mask = stripes > Integer.MAX_VALUE / 2 ? ALL_SET : ceilToPowerOfTwo(stripes) - 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
final int indexFor(final Object key) {
|
||||
final int hash = smear(key.hashCode());
|
||||
return hash & mask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final L get(final Object key) {
|
||||
return getAt(indexFor(key));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强引用实现,使用固定数组存储段。
|
||||
*/
|
||||
private static class CompactSegmentLock<L> extends PowerOfTwoSegmentLock<L> {
|
||||
private final Object[] array;
|
||||
|
||||
CompactSegmentLock(final int stripes, final Supplier<L> supplier) {
|
||||
super(stripes);
|
||||
Assert.isTrue(stripes <= Integer.MAX_VALUE / 2, "Segment count must be <= 2^30");
|
||||
this.array = new Object[mask + 1];
|
||||
for (int i = 0; i < array.length; i++) {
|
||||
array[i] = supplier.get();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public L getAt(final int index) {
|
||||
if (index < 0 || index >= array.length) {
|
||||
throw new IllegalArgumentException("Index " + index + " out of bounds for size " + array.length);
|
||||
}
|
||||
return (L) array[index];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return array.length;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 小规模弱引用实现,使用 AtomicReferenceArray 存储段。
|
||||
*/
|
||||
private static class SmallLazySegmentLock<L> extends PowerOfTwoSegmentLock<L> {
|
||||
final AtomicReferenceArray<ArrayReference<? extends L>> locks;
|
||||
final Supplier<L> supplier;
|
||||
final int size;
|
||||
final ReferenceQueue<L> queue = new ReferenceQueue<>();
|
||||
|
||||
SmallLazySegmentLock(final int stripes, final Supplier<L> supplier) {
|
||||
super(stripes);
|
||||
this.size = (mask == ALL_SET) ? Integer.MAX_VALUE : mask + 1;
|
||||
this.locks = new AtomicReferenceArray<>(size);
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public L getAt(final int index) {
|
||||
if (size != Integer.MAX_VALUE) {
|
||||
Assert.isTrue(index >= 0 && index < size, "Index out of bounds");
|
||||
}
|
||||
ArrayReference<? extends L> existingRef = locks.get(index);
|
||||
L existing = existingRef == null ? null : existingRef.get();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
final L created = supplier.get();
|
||||
final ArrayReference<L> newRef = new ArrayReference<>(created, index, queue);
|
||||
while (!locks.compareAndSet(index, existingRef, newRef)) {
|
||||
existingRef = locks.get(index);
|
||||
existing = existingRef == null ? null : existingRef.get();
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
}
|
||||
drainQueue();
|
||||
return created;
|
||||
}
|
||||
|
||||
private void drainQueue() {
|
||||
Reference<? extends L> ref;
|
||||
while ((ref = queue.poll()) != null) {
|
||||
final ArrayReference<? extends L> arrayRef = (ArrayReference<? extends L>) ref;
|
||||
locks.compareAndSet(arrayRef.index, arrayRef, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
|
||||
private static final class ArrayReference<L> extends WeakReference<L> {
|
||||
final int index;
|
||||
|
||||
ArrayReference(final L referent, final int index, final ReferenceQueue<L> queue) {
|
||||
super(referent, queue);
|
||||
this.index = index;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 大规模弱引用实现,使用 ConcurrentMap 存储段。
|
||||
*/
|
||||
private static class LargeLazySegmentLock<L> extends PowerOfTwoSegmentLock<L> {
|
||||
final ConcurrentMap<Integer, L> locks;
|
||||
final Supplier<L> supplier;
|
||||
final int size;
|
||||
|
||||
LargeLazySegmentLock(final int stripes, final Supplier<L> supplier) {
|
||||
super(stripes);
|
||||
this.size = (mask == ALL_SET) ? Integer.MAX_VALUE : mask + 1;
|
||||
this.locks = new ConcurrentHashMap<>();
|
||||
this.supplier = supplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public L getAt(final int index) {
|
||||
if (size != Integer.MAX_VALUE) {
|
||||
Assert.isTrue(index >= 0 && index < size, "Index out of bounds");
|
||||
}
|
||||
L existing = locks.get(index);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
final L created = supplier.get();
|
||||
existing = locks.putIfAbsent(index, created);
|
||||
return existing != null ? existing : created;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return size;
|
||||
}
|
||||
}
|
||||
|
||||
private static final int ALL_SET = ~0;
|
||||
|
||||
private static int ceilToPowerOfTwo(final int x) {
|
||||
return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1));
|
||||
}
|
||||
|
||||
private static int smear(int hashCode) {
|
||||
hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);
|
||||
return hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充锁,避免缓存行干扰。
|
||||
*/
|
||||
private static class PaddedLock extends ReentrantLock {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
long unused1;
|
||||
long unused2;
|
||||
long unused3;
|
||||
|
||||
PaddedLock() {
|
||||
super(false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充信号量,避免缓存行干扰。
|
||||
*/
|
||||
private static class PaddedSemaphore extends Semaphore {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
long unused1;
|
||||
long unused2;
|
||||
long unused3;
|
||||
|
||||
PaddedSemaphore(final int permits) {
|
||||
super(permits, false);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -164,4 +164,22 @@ public class CacheTest {
|
||||
assertFalse(ALARM_CACHE.containsKey(1));
|
||||
assertEquals(1, counter.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* ReentrantCache类clear()方法、AbstractCache.putWithoutLock方法可能导致资源泄露
|
||||
* https://github.com/chinabugotech/hutool/issues/3957
|
||||
*/
|
||||
@Test
|
||||
public void reentrantCache_clear_Method_Test() {
|
||||
final AtomicInteger removeCount = new AtomicInteger();
|
||||
final Cache<String, String> lruCache = CacheUtil.newLRUCache(4);
|
||||
lruCache.setListener((key, cachedObject) -> removeCount.getAndIncrement());
|
||||
lruCache.put("key1","String1");
|
||||
lruCache.put("key2","String2");
|
||||
lruCache.put("key3","String3");
|
||||
lruCache.put("key1","String4");//key已经存在,原始putWithoutLock方法存在资源泄露
|
||||
lruCache.put("key4","String5");
|
||||
lruCache.clear();//ReentrantCache类clear()方法存在资源泄露
|
||||
Assertions.assertEquals(5, removeCount.get());
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,202 @@
|
||||
package org.dromara.hutool.core.data;
|
||||
|
||||
import org.dromara.hutool.core.data.masking.RichTextMaskingProcessor;
|
||||
import org.dromara.hutool.core.data.masking.RichTextMaskingRule;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 富文本脱敏工具类测试
|
||||
*
|
||||
* @author xjf
|
||||
*/
|
||||
public class RichTextMaskingUtilTest {
|
||||
|
||||
@Test
|
||||
public void testDefaultMask() {
|
||||
// 测试默认脱敏功能
|
||||
final String html = "这是一封邮件,联系人:test@example.com,网址:https://www.example.com,包含机密信息。";
|
||||
final String masked = RichTextMaskingUtil.mask(html);
|
||||
|
||||
// 验证邮箱被脱敏
|
||||
Assertions.assertFalse(masked.contains("test@example.com"));
|
||||
Assertions.assertTrue(masked.contains("t***"));
|
||||
|
||||
// 验证网址被脱敏
|
||||
Assertions.assertFalse(masked.contains("https://www.example.com"));
|
||||
Assertions.assertTrue(masked.contains("[网址已隐藏]"));
|
||||
|
||||
// 验证敏感词被脱敏
|
||||
Assertions.assertFalse(masked.contains("机密"));
|
||||
Assertions.assertTrue(masked.contains("**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHtmlContentMask() {
|
||||
// 测试HTML内容脱敏
|
||||
final String html = "<p>这是一封邮件,联系人:<a href='mailto:testA@example.com'>test@example.com</a>," +
|
||||
"网址:<a href='https://www.aexample.com'>https://www.example.com</a>," +
|
||||
"包含<span style='color:red'>机密</span>信息。</p>";
|
||||
final String masked = RichTextMaskingUtil.mask(html);
|
||||
|
||||
// 验证HTML标签被保留
|
||||
Assertions.assertTrue(masked.contains("<p>"));
|
||||
Assertions.assertTrue(masked.contains("</p>"));
|
||||
Assertions.assertTrue(masked.contains("<a href='mailto:"));
|
||||
Assertions.assertTrue(masked.contains("<span style='color:red'>"));
|
||||
|
||||
// 验证邮箱被脱敏
|
||||
Assertions.assertFalse(masked.contains("test@example.com"));
|
||||
Assertions.assertTrue(masked.contains("t***"));
|
||||
|
||||
// 验证网址被脱敏
|
||||
Assertions.assertFalse(masked.contains("https://www.example.com"));
|
||||
Assertions.assertTrue(masked.contains("[网址已隐藏]"));
|
||||
|
||||
// 验证敏感词被脱敏
|
||||
Assertions.assertFalse(masked.contains("机密"));
|
||||
Assertions.assertTrue(masked.contains("**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCustomProcessor() {
|
||||
// 创建自定义处理器
|
||||
final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
|
||||
|
||||
// 添加自定义规则 - 手机号码
|
||||
processor.addRule(RichTextMaskingUtil.createPartialMaskRule(
|
||||
"手机号",
|
||||
"1[3-9]\\d{9}",
|
||||
3,
|
||||
4,
|
||||
'*'));
|
||||
|
||||
// 添加自定义规则 - 公司名称
|
||||
processor.addRule(RichTextMaskingUtil.createCustomRule(
|
||||
"公司名称",
|
||||
"XX科技有限公司",
|
||||
RichTextMaskingRule.MaskType.REPLACE,
|
||||
"[公司名称已隐藏]"));
|
||||
|
||||
// 测试文本
|
||||
final String text = "联系电话:13812345678,公司名称:XX科技有限公司";
|
||||
final String masked = RichTextMaskingUtil.mask(text, processor);
|
||||
|
||||
// 验证手机号被脱敏
|
||||
Assertions.assertFalse(masked.contains("13812345678"));
|
||||
Assertions.assertTrue(masked.contains("138****5678"));
|
||||
|
||||
// 验证公司名称被脱敏
|
||||
Assertions.assertFalse(masked.contains("XX科技有限公司"));
|
||||
Assertions.assertTrue(masked.contains("[公司名称已隐藏]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTagFiltering() {
|
||||
// 创建自定义处理器
|
||||
final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
|
||||
|
||||
// 创建只在特定标签中生效的规则
|
||||
final RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule(
|
||||
"标签内敏感信息",
|
||||
"敏感信息",
|
||||
RichTextMaskingRule.MaskType.REPLACE,
|
||||
"[已隐藏]");
|
||||
|
||||
// 设置只在div标签中生效
|
||||
final Set<String> includeTags = new HashSet<>();
|
||||
includeTags.add("div");
|
||||
rule.setIncludeTags(includeTags);
|
||||
|
||||
processor.addRule(rule);
|
||||
|
||||
// 测试HTML
|
||||
final String html = "<p>这是一段敏感信息</p><div>这也是一段敏感信息</div>";
|
||||
final String masked = RichTextMaskingUtil.mask(html, processor);
|
||||
|
||||
// 验证只有div标签中的敏感信息被脱敏
|
||||
Assertions.assertTrue(masked.contains("<p>这是一段敏感信息</p>"));
|
||||
Assertions.assertTrue(masked.contains("<div>这也是一段[已隐藏]</div>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExcludeTags() {
|
||||
// 创建自定义处理器
|
||||
final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
|
||||
|
||||
// 创建排除特定标签的规则
|
||||
final RichTextMaskingRule rule = RichTextMaskingUtil.createCustomRule(
|
||||
"排除标签内敏感信息",
|
||||
"敏感信息",
|
||||
RichTextMaskingRule.MaskType.REPLACE,
|
||||
"[已隐藏]");
|
||||
|
||||
// 设置排除code标签
|
||||
rule.addExcludeTag("code");
|
||||
|
||||
processor.addRule(rule);
|
||||
|
||||
// 测试HTML
|
||||
final String html = "<p>这是一段敏感信息</p><code>这是代码中的敏感信息</code>";
|
||||
final String masked = RichTextMaskingUtil.mask(html, processor);
|
||||
|
||||
// 验证code标签中的敏感信息不被脱敏
|
||||
Assertions.assertTrue(masked.contains("<p>这是一段[已隐藏]</p>"));
|
||||
Assertions.assertTrue(masked.contains("<code>这是代码中的敏感信息</code>"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComplexHtml() {
|
||||
// 测试复杂HTML内容
|
||||
final String html = "<div class='content'>" +
|
||||
"<h1>公司内部文档</h1>" +
|
||||
"<p>联系人:张三 <a href='mailto:zhangsan@example.com'>zhangsan@example.com</a></p>" +
|
||||
"<p>电话:13812345678</p>" +
|
||||
"<div class='secret'>这是一段机密信息,请勿外传</div>" +
|
||||
"<pre><code>// 这是一段代码\nString password = \"123456\";</code></pre>" +
|
||||
"<p>公司网址:<a href='https://www.example.com'>https://www.example.com</a></p>" +
|
||||
"</div>";
|
||||
|
||||
// 创建自定义处理器
|
||||
final RichTextMaskingProcessor processor = RichTextMaskingUtil.createProcessor(true);
|
||||
|
||||
// 添加邮箱脱敏规则
|
||||
processor.addRule(RichTextMaskingUtil.createEmailRule());
|
||||
|
||||
// 添加手机号脱敏规则
|
||||
processor.addRule(RichTextMaskingUtil.createPartialMaskRule(
|
||||
"手机号",
|
||||
"1[3-9]\\d{9}",
|
||||
3,
|
||||
4,
|
||||
'*'));
|
||||
|
||||
// 添加敏感词脱敏规则
|
||||
processor.addRule(RichTextMaskingUtil.createSensitiveWordRule("机密|内部"));
|
||||
|
||||
// 添加网址脱敏规则
|
||||
processor.addRule(RichTextMaskingUtil.createUrlRule("[网址已隐藏]"));
|
||||
|
||||
// 添加密码脱敏规则,但排除code标签
|
||||
final RichTextMaskingRule passwordRule = RichTextMaskingUtil.createCustomRule(
|
||||
"密码",
|
||||
"password = \"[^\"]+\"",
|
||||
RichTextMaskingRule.MaskType.REPLACE,
|
||||
"password = \"******\"");
|
||||
passwordRule.addExcludeTag("code");
|
||||
processor.addRule(passwordRule);
|
||||
|
||||
final String masked = RichTextMaskingUtil.mask(html, processor);
|
||||
|
||||
// 验证结果
|
||||
Assertions.assertTrue(masked.contains("<h1>公司**文档</h1>"));
|
||||
Assertions.assertTrue(masked.contains("z***"));
|
||||
Assertions.assertTrue(masked.contains("138****5678"));
|
||||
Assertions.assertTrue(masked.contains("这是一段**信息"));
|
||||
Assertions.assertFalse(masked.contains("String password = \"123456\""));
|
||||
Assertions.assertTrue(masked.contains("[网址已隐藏]"));
|
||||
}
|
||||
}
|
@@ -516,7 +516,11 @@ public class FileUtilTest {
|
||||
final String parseSmbPath = FileUtil.getAbsolutePath(smbPath);
|
||||
assertEquals(smbPath, parseSmbPath);
|
||||
assertTrue(FileUtil.isAbsolutePath(smbPath));
|
||||
assertTrue(Paths.get(smbPath).isAbsolute());
|
||||
if(FileUtil.isWindows()){
|
||||
// 在Windows下`\`路径是绝对路径,也表示SMB路径
|
||||
// 但是在Linux下,`\`表示转义字符,并不被识别为路径
|
||||
assertTrue(Paths.get(smbPath).isAbsolute());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@@ -59,6 +59,6 @@ public class ResourceUtilTest {
|
||||
@Test
|
||||
void getResourceTest2() {
|
||||
// project:开头表示基于项目的相对路径,此处无文件报错
|
||||
Assertions.assertThrows(NoResourceException.class, () -> ResourceUtil.getResource("project:test.xml"));
|
||||
Assertions.assertThrows(NoResourceException.class, () -> ResourceUtil.getResource("project:test.xml").getStream());
|
||||
}
|
||||
}
|
||||
|
@@ -144,14 +144,14 @@ public class CollectorUtilTest {
|
||||
final Pair<List<Integer>, List<String>> pairList = list.stream()
|
||||
.collect(CollectorUtil.toPairList(Pair::getLeft, Pair::getRight));
|
||||
|
||||
Assertions.assertEquals(pairList.getLeft().size(),list.size());
|
||||
Assertions.assertEquals(pairList.getRight().size(),list.size());
|
||||
Assertions.assertEquals(list.size(),pairList.getLeft().size());
|
||||
Assertions.assertEquals(list.size(),pairList.getRight().size());
|
||||
|
||||
final Pair<HashSet<Integer>, ArrayList<String>> pairMixed = list.stream()
|
||||
.collect(CollectorUtil.toPairCollection(Pair::getLeft, Pair::getRight, HashSet::new, ArrayList::new));
|
||||
final Pair<Set<Integer>, List<String>> pairMixed = list.stream()
|
||||
.collect(CollectorUtil.toPair(Pair::getLeft, Pair::getRight, Collectors.toSet(), Collectors.toList()));
|
||||
|
||||
Assertions.assertEquals(pairMixed.getLeft().size(),list.size());
|
||||
Assertions.assertEquals(pairMixed.getRight().size(),list.size());
|
||||
Assertions.assertEquals(list.size(),pairMixed.getLeft().size());
|
||||
Assertions.assertEquals(list.size(),pairMixed.getRight().size());
|
||||
|
||||
}
|
||||
|
||||
@@ -163,16 +163,17 @@ public class CollectorUtilTest {
|
||||
final Triple<List<Integer>, List<Long>, List<String>> tripleList = list.stream()
|
||||
.collect(CollectorUtil.toTripleList(Triple::getLeft, Triple::getMiddle, Triple::getRight));
|
||||
|
||||
Assertions.assertEquals(tripleList.getLeft().size(),list.size());
|
||||
Assertions.assertEquals(tripleList.getMiddle().size(),list.size());
|
||||
Assertions.assertEquals(tripleList.getRight().size(),list.size());
|
||||
Assertions.assertEquals(list.size(),tripleList.getLeft().size());
|
||||
Assertions.assertEquals(list.size(),tripleList.getMiddle().size());
|
||||
Assertions.assertEquals(list.size(),tripleList.getRight().size());
|
||||
|
||||
final Triple<HashSet<Integer>, HashSet<Long>, ArrayList<String>> tripleMixed = list.stream()
|
||||
.collect(CollectorUtil.toTripleCollection(Triple::getLeft, Triple::getMiddle, Triple::getRight, HashSet::new, HashSet::new, ArrayList::new));
|
||||
final Triple<Integer, List<Long>, String> tripleMixed = list.stream()
|
||||
.collect(CollectorUtil.toTriple(Triple::getLeft, Triple::getMiddle, Triple::getRight,
|
||||
Collectors.summingInt(s->s), Collectors.toList(), Collectors.joining()));
|
||||
|
||||
Assertions.assertEquals(tripleMixed.getLeft().size(),list.size());
|
||||
Assertions.assertEquals(tripleMixed.getMiddle().size(),list.size());
|
||||
Assertions.assertEquals(tripleMixed.getRight().size(),list.size());
|
||||
Assertions.assertEquals(3,tripleMixed.getLeft());
|
||||
Assertions.assertEquals(list.size(),tripleMixed.getMiddle().size());
|
||||
Assertions.assertEquals(6,tripleMixed.getRight().length());
|
||||
|
||||
}
|
||||
|
||||
|
@@ -0,0 +1,203 @@
|
||||
package org.dromara.hutool.core.thread.lock;
|
||||
|
||||
import org.dromara.hutool.core.collection.ListUtil;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReadWriteLock;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
public class SegmentLockTest {
|
||||
private static final int SEGMENT_COUNT = 4;
|
||||
private SegmentLock<Lock> strongLock;
|
||||
private SegmentLock<Lock> weakLock;
|
||||
private SegmentLock<Semaphore> semaphore;
|
||||
private SegmentLock<ReadWriteLock> readWriteLock;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
strongLock = SegmentLock.lock(SEGMENT_COUNT);
|
||||
weakLock = SegmentLock.lazyWeakLock(SEGMENT_COUNT);
|
||||
semaphore = SegmentLock.semaphore(SEGMENT_COUNT, 2);
|
||||
readWriteLock = SegmentLock.readWriteLock(SEGMENT_COUNT);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSize() {
|
||||
assertEquals(SEGMENT_COUNT, strongLock.size());
|
||||
assertEquals(SEGMENT_COUNT, weakLock.size());
|
||||
assertEquals(SEGMENT_COUNT, semaphore.size());
|
||||
assertEquals(SEGMENT_COUNT, readWriteLock.size());
|
||||
}
|
||||
|
||||
@SuppressWarnings("StringOperationCanBeSimplified")
|
||||
@Test
|
||||
public void testGetWithSameKey() {
|
||||
// 相同 key 应返回相同锁
|
||||
final String key1 = "testKey";
|
||||
final String key2 = new String("testKey"); // equals 但不同对象
|
||||
final Lock lock1 = strongLock.get(key1);
|
||||
final Lock lock2 = strongLock.get(key2);
|
||||
assertSame(lock1, lock2, "相同 key 应返回同一锁对象");
|
||||
|
||||
final Lock weakLock1 = weakLock.get(key1);
|
||||
final Lock weakLock2 = weakLock.get(key2);
|
||||
assertSame(weakLock1, weakLock2, "弱引用锁相同 key 应返回同一锁对象");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetAt() {
|
||||
for (int i = 0; i < SEGMENT_COUNT; i++) {
|
||||
final Lock lock = strongLock.getAt(i);
|
||||
assertNotNull(lock, "getAt 返回的锁不应为 null");
|
||||
}
|
||||
assertThrows(IllegalArgumentException.class, () -> strongLock.getAt(SEGMENT_COUNT),
|
||||
"超出段数的索引应抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBulkGet() {
|
||||
final List<String> keys = ListUtil.of("key1", "key2", "key3");
|
||||
final Iterable<Lock> locks = strongLock.bulkGet(keys);
|
||||
final List<Lock> lockList = ListUtil.of(locks);
|
||||
|
||||
assertEquals(3, lockList.size(), "bulkGet 返回的锁数量应与 key 数量一致");
|
||||
|
||||
// 检查顺序性
|
||||
int prevIndex = -1;
|
||||
for (final Lock lock : lockList) {
|
||||
final int index = findIndex(strongLock, lock);
|
||||
assertTrue(index >= prevIndex, "bulkGet 返回的锁应按索引升序");
|
||||
prevIndex = index;
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockConcurrency() throws InterruptedException {
|
||||
final int threadCount = SEGMENT_COUNT * 2;
|
||||
final CountDownLatch startLatch = new CountDownLatch(1);
|
||||
final CountDownLatch endLatch = new CountDownLatch(threadCount);
|
||||
final ExecutorService executor = Executors.newFixedThreadPool(threadCount);
|
||||
final List<String> keys = new ArrayList<>();
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
keys.add("key" + i);
|
||||
}
|
||||
|
||||
for (int i = 0; i < threadCount; i++) {
|
||||
final String key = keys.get(i);
|
||||
executor.submit(() -> {
|
||||
try {
|
||||
startLatch.await();
|
||||
final Lock lock = strongLock.get(key);
|
||||
lock.lock();
|
||||
try {
|
||||
Thread.sleep(100); // 模拟工作
|
||||
} finally {
|
||||
lock.unlock();
|
||||
}
|
||||
} catch (final InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
} finally {
|
||||
endLatch.countDown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
startLatch.countDown();
|
||||
assertTrue(endLatch.await(2000, java.util.concurrent.TimeUnit.MILLISECONDS),
|
||||
"并发锁测试应在 2 秒内完成");
|
||||
executor.shutdown();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSemaphore() {
|
||||
final Semaphore sem = semaphore.get("testKey");
|
||||
assertEquals(2, sem.availablePermits(), "信号量初始许可应为 2");
|
||||
|
||||
sem.acquireUninterruptibly(2);
|
||||
assertEquals(0, sem.availablePermits(), "获取所有许可后应为 0");
|
||||
|
||||
sem.release(1);
|
||||
assertEquals(1, sem.availablePermits(), "释放一个许可后应为 1");
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@Test
|
||||
public void testReadWriteLock() throws InterruptedException {
|
||||
final ReadWriteLock rwLock = readWriteLock.get("testKey");
|
||||
final Lock readLock = rwLock.readLock();
|
||||
final Lock writeLock = rwLock.writeLock();
|
||||
|
||||
// 测试读锁可重入
|
||||
readLock.lock();
|
||||
assertTrue(readLock.tryLock(), "读锁应允许多个线程同时持有");
|
||||
readLock.unlock();
|
||||
readLock.unlock();
|
||||
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
final AtomicBoolean readLockAcquired = new AtomicBoolean(false);
|
||||
|
||||
writeLock.lock();
|
||||
executor.submit(() -> {
|
||||
readLockAcquired.set(readLock.tryLock());
|
||||
latch.countDown();
|
||||
});
|
||||
|
||||
latch.await(500, TimeUnit.MILLISECONDS);
|
||||
assertFalse(readLockAcquired.get(), "写锁持有时读锁应失败");
|
||||
writeLock.unlock();
|
||||
|
||||
executor.shutdown();
|
||||
executor.awaitTermination(1, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWeakReferenceCleanup() throws InterruptedException {
|
||||
final SegmentLock<Lock> weakLockLarge = SegmentLock.lazyWeakLock(1024); // 超过 LARGE_LAZY_CUTOFF
|
||||
final Lock lock = weakLockLarge.get("testKey");
|
||||
|
||||
System.gc();
|
||||
Thread.sleep(100);
|
||||
|
||||
// 弱引用锁未被其他引用,应仍可获取
|
||||
final Lock lockAgain = weakLockLarge.get("testKey");
|
||||
assertSame(lock, lockAgain, "弱引用锁未被回收时应返回同一对象");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInvalidSegmentCount() {
|
||||
assertThrows(IllegalArgumentException.class, () -> SegmentLock.lock(0),
|
||||
"段数为 0 应抛出异常");
|
||||
assertThrows(IllegalArgumentException.class, () -> SegmentLock.lock(-1),
|
||||
"负段数应抛出异常");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHashDistribution() {
|
||||
final SegmentLock<Lock> lock = SegmentLock.lock(4);
|
||||
final int[] counts = new int[4];
|
||||
for (int i = 0; i < 100; i++) {
|
||||
final int index = findIndex(lock, lock.get("key" + i));
|
||||
counts[index]++;
|
||||
}
|
||||
for (final int count : counts) {
|
||||
assertTrue(count > 0, "每个段都应至少被分配到一个 key");
|
||||
}
|
||||
}
|
||||
|
||||
private int findIndex(final SegmentLock<Lock> lock, final Lock target) {
|
||||
for (int i = 0; i < lock.size(); i++) {
|
||||
if (lock.getAt(i) == target) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-cron</artifactId>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
|
@@ -0,0 +1,168 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.crypto.digest;
|
||||
|
||||
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
|
||||
import org.bouncycastle.crypto.params.Argon2Parameters;
|
||||
|
||||
/**
|
||||
* Argon2加密实现
|
||||
*
|
||||
* @author changhr2013
|
||||
* @author Looly
|
||||
* @since 5.8.38
|
||||
*/
|
||||
public class Argon2 {
|
||||
|
||||
/**
|
||||
* 默认hash长度
|
||||
*/
|
||||
public static final int DEFAULT_HASH_LENGTH = 32;
|
||||
|
||||
private int hashLength = DEFAULT_HASH_LENGTH;
|
||||
private final Argon2Parameters.Builder paramsBuilder;
|
||||
|
||||
/**
|
||||
* 构造,默认使用{@link Argon2Parameters#ARGON2_id}类型
|
||||
*/
|
||||
public Argon2(){
|
||||
this(Argon2Parameters.ARGON2_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param type {@link Argon2Parameters#ARGON2_d}、{@link Argon2Parameters#ARGON2_i}、{@link Argon2Parameters#ARGON2_id}
|
||||
*/
|
||||
public Argon2(final int type){
|
||||
this(new Argon2Parameters.Builder(type));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param paramsBuilder 参数构造器
|
||||
*/
|
||||
public Argon2(final Argon2Parameters.Builder paramsBuilder){
|
||||
this.paramsBuilder = paramsBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置hash长度
|
||||
*
|
||||
* @param hashLength hash长度
|
||||
* @return this
|
||||
*/
|
||||
public Argon2 setHashLength(final int hashLength){
|
||||
this.hashLength = hashLength;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置版本
|
||||
*
|
||||
* @param version 版本
|
||||
* @return this
|
||||
* @see Argon2Parameters#ARGON2_VERSION_10
|
||||
* @see Argon2Parameters#ARGON2_VERSION_13
|
||||
*/
|
||||
public Argon2 setVersion(final int version){
|
||||
this.paramsBuilder.withVersion(version);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置盐
|
||||
*
|
||||
* @param salt 盐
|
||||
* @return this
|
||||
*/
|
||||
public Argon2 setSalt(final byte[] salt){
|
||||
this.paramsBuilder.withSalt(salt);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置可选的密钥数据,用于增加哈希的复杂性
|
||||
*
|
||||
* @param secret 密钥
|
||||
* @return this
|
||||
*/
|
||||
public Argon2 setSecret(final byte[] secret){
|
||||
this.paramsBuilder.withSecret(secret);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param additional 附加数据
|
||||
* @return this
|
||||
*/
|
||||
public Argon2 setAdditional(final byte[] additional){
|
||||
this.paramsBuilder.withAdditional(additional);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置迭代次数<br>
|
||||
* 迭代次数越多,生成哈希的时间就越长,破解哈希就越困难
|
||||
*
|
||||
* @param iterations 迭代次数
|
||||
* @return this
|
||||
*/
|
||||
public Argon2 setIterations(final int iterations){
|
||||
this.paramsBuilder.withIterations(iterations);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置内存,单位KB<br>
|
||||
* 内存越大,生成哈希的时间就越长,破解哈希就越困难
|
||||
*
|
||||
* @param memoryAsKB 内存,单位KB
|
||||
* @return this
|
||||
*/
|
||||
public Argon2 setMemoryAsKB(final int memoryAsKB){
|
||||
this.paramsBuilder.withMemoryAsKB(memoryAsKB);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置并行度,即同时使用的核心数<br>
|
||||
* 值越高,生成哈希的时间就越长,破解哈希就越困难
|
||||
*
|
||||
* @param parallelism 并行度
|
||||
* @return this
|
||||
*/
|
||||
public Argon2 setParallelism(final int parallelism){
|
||||
this.paramsBuilder.withParallelism(parallelism);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成hash值
|
||||
*
|
||||
* @param password 密码
|
||||
* @return hash值
|
||||
*/
|
||||
public byte[] digest(final char[] password){
|
||||
final Argon2BytesGenerator generator = new Argon2BytesGenerator();
|
||||
generator.init(paramsBuilder.build());
|
||||
final byte[] result = new byte[hashLength];
|
||||
generator.generateBytes(password, result);
|
||||
return result;
|
||||
}
|
||||
}
|
@@ -0,0 +1,22 @@
|
||||
package org.dromara.hutool.crypto.digest;
|
||||
|
||||
import org.dromara.hutool.core.codec.binary.Base64;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class Argon2Test {
|
||||
@Test
|
||||
public void argon2Test() {
|
||||
final Argon2 argon2 = new Argon2();
|
||||
final byte[] digest = argon2.digest("123456".toCharArray());
|
||||
Assertions.assertEquals("wVGMOdzf5EdKGANPeHjaUnaFEJA0BnAq6HcF2psFmFo=", Base64.encode(digest));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void argon2WithSaltTest() {
|
||||
final Argon2 argon2 = new Argon2();
|
||||
argon2.setSalt("123456".getBytes());
|
||||
final byte[] digest = argon2.digest("123456".toCharArray());
|
||||
Assertions.assertEquals("sEpbXTdMWra36JXPVxrZMm3xyoR5GkMlLhtW0Kwp9Ag=", Base64.encode(digest));
|
||||
}
|
||||
}
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-db</artifactId>
|
||||
@@ -164,5 +164,11 @@
|
||||
<version>2.4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.sap.cloud.db.jdbc</groupId>
|
||||
<artifactId>ngdbc</artifactId>
|
||||
<version>2.24.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
|
@@ -89,6 +89,9 @@ public class DialectFactory {
|
||||
} else if (DriverNames.DRIVER_GOLDENDB.equalsIgnoreCase(driverName)) {
|
||||
// MySQL兼容
|
||||
return new MysqlDialect(dbConfig);
|
||||
} else if (DriverNames.DRIVER_HANA.equalsIgnoreCase(driverName)) {
|
||||
// SAP HANA
|
||||
return new HanaDialect(dbConfig);
|
||||
}
|
||||
}
|
||||
// 无法识别可支持的数据库类型默认使用ANSI方言,可兼容大部分SQL语句
|
||||
|
@@ -25,7 +25,7 @@ import org.dromara.hutool.core.text.StrUtil;
|
||||
* @author Looly
|
||||
*/
|
||||
public enum DialectName {
|
||||
ANSI, MYSQL, ORACLE, POSTGRESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012, PHOENIX, DM;
|
||||
ANSI, MYSQL, ORACLE, POSTGRESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012, PHOENIX, DM, HANA;
|
||||
|
||||
/**
|
||||
* 是否为指定数据库方言,检查时不分区大小写
|
||||
|
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Hutool Team and hutool.cn
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.dromara.hutool.db.dialect.impl;
|
||||
|
||||
import org.dromara.hutool.core.text.StrUtil;
|
||||
import org.dromara.hutool.db.DbException;
|
||||
import org.dromara.hutool.db.Entity;
|
||||
import org.dromara.hutool.db.config.DbConfig;
|
||||
import org.dromara.hutool.db.dialect.DialectName;
|
||||
import org.dromara.hutool.db.sql.QuoteWrapper;
|
||||
import org.dromara.hutool.db.sql.SqlBuilder;
|
||||
import org.dromara.hutool.db.sql.StatementUtil;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Hana数据库方言
|
||||
*
|
||||
* @author daoyou.dev
|
||||
*/
|
||||
public class HanaDialect extends AnsiSqlDialect {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param config 数据库配置对象
|
||||
*/
|
||||
public HanaDialect(final DbConfig config) {
|
||||
super(config);
|
||||
quoteWrapper = new QuoteWrapper('"');
|
||||
}
|
||||
|
||||
@Override
|
||||
public String dialectName() {
|
||||
return DialectName.HANA.name();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用于upsert的{@link PreparedStatement}。
|
||||
* SAP HANA 使用 MERGE INTO 语法来实现 UPSERT 操作。
|
||||
* <p>
|
||||
* 生成 SQL 语法为:
|
||||
* <pre>{@code
|
||||
* MERGE INTO demo AS target
|
||||
* USING (SELECT ? AS a, ? AS b, ? AS c FROM DUMMY) AS source
|
||||
* ON target.id = source.id
|
||||
* WHEN MATCHED THEN
|
||||
* UPDATE SET target.a = source.a, target.b = source.b, target.c = source.c
|
||||
* WHEN NOT MATCHED THEN
|
||||
* INSERT (a, b, c) VALUES (source.a, source.b, source.c);
|
||||
* }</pre>
|
||||
*
|
||||
* @param conn 数据库连接对象
|
||||
* @param entity 数据实体类(包含表名)
|
||||
* @param keys 主键字段数组,通常用于确定匹配条件(联合主键)
|
||||
* @return PreparedStatement
|
||||
* @throws DbException SQL 执行异常
|
||||
*/
|
||||
@Override
|
||||
public PreparedStatement psForUpsert(final Connection conn, final Entity entity, final String... keys) throws DbException {
|
||||
SqlBuilder.validateEntity(entity);
|
||||
final SqlBuilder builder = SqlBuilder.of(quoteWrapper);
|
||||
|
||||
final List<String> columns = new ArrayList<>();
|
||||
|
||||
// 构建字段部分和参数占位符部分
|
||||
entity.forEach((field, value) -> {
|
||||
if (StrUtil.isNotBlank(field)) {
|
||||
columns.add(quoteWrapper != null ? quoteWrapper.wrap(field) : field);
|
||||
builder.addParams(value);
|
||||
}
|
||||
});
|
||||
|
||||
String tableName = entity.getTableName();
|
||||
if (quoteWrapper != null) {
|
||||
tableName = quoteWrapper.wrap(tableName);
|
||||
}
|
||||
|
||||
// 构建 UPSERT 语句
|
||||
builder.append("UPSERT ").append(tableName).append(" (");
|
||||
builder.append(String.join(", ", columns));
|
||||
builder.append(") VALUES (");
|
||||
builder.append(String.join(", ", Collections.nCopies(columns.size(), "?")));
|
||||
builder.append(") WITH PRIMARY KEY");
|
||||
|
||||
return StatementUtil.prepareStatement(false, this.dbConfig, conn, builder.build(), builder.getParamValueArray());
|
||||
}
|
||||
}
|
@@ -33,7 +33,7 @@ import java.util.List;
|
||||
* @author Looly
|
||||
* @since 6.0.0
|
||||
*/
|
||||
public class DriverIdentifier implements DriverNames{
|
||||
public class DriverIdentifier implements DriverNames {
|
||||
|
||||
/**
|
||||
* 单例驱动识别器
|
||||
@@ -131,7 +131,9 @@ public class DriverIdentifier implements DriverNames{
|
||||
new StartsWithDriverMatcher(DRIVER_GAUSS, "jdbc:zenith:"),
|
||||
new StartsWithDriverMatcher(DRIVER_OPENGAUSS, "jdbc:opengauss:"),
|
||||
// 中兴GoldenDB
|
||||
new StartsWithDriverMatcher(DRIVER_GOLDENDB, "jdbc:goldendb:")
|
||||
new StartsWithDriverMatcher(DRIVER_GOLDENDB, "jdbc:goldendb:"),
|
||||
// SAP HANA
|
||||
new StartsWithDriverMatcher(DRIVER_HANA, "jdbc:sap:")
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -252,9 +252,12 @@ public interface DriverNames {
|
||||
* JDBC 驱动 Greenplum
|
||||
*/
|
||||
String DRIVER_GREENPLUM = "com.pivotal.jdbc.GreenplumDriver";
|
||||
|
||||
/**
|
||||
* JDBC 驱动 GoldenDB
|
||||
*/
|
||||
String DRIVER_GOLDENDB = "com.goldendb.jdbc.Driver";
|
||||
/**
|
||||
* JDBC 驱动 Sap Hana
|
||||
*/
|
||||
String DRIVER_HANA = "com.sap.db.jdbc.Driver";
|
||||
}
|
||||
|
@@ -99,6 +99,16 @@ public class PooledConnection extends ConnectionWrapper {
|
||||
return this.isClosed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开连接
|
||||
*
|
||||
* @return this
|
||||
*/
|
||||
PooledConnection open() {
|
||||
this.isClosed = false;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁连接,即彻底关闭并丢弃连接
|
||||
*/
|
||||
|
@@ -86,7 +86,7 @@ public class PooledDataSource extends AbstractDataSource {
|
||||
|
||||
@Override
|
||||
public PooledConnection getConnection() throws SQLException {
|
||||
return connPool.borrowObject();
|
||||
return connPool.borrowObject().open();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@@ -643,7 +643,9 @@ public class StatementWrapper extends SimpleWrapper<PreparedStatement> implement
|
||||
final ParameterMetaData pmd;
|
||||
try {
|
||||
pmd = this.raw.getParameterMetaData();
|
||||
sqlType = pmd.getParameterType(paramIndex);
|
||||
if(null != pmd){
|
||||
sqlType = pmd.getParameterType(paramIndex);
|
||||
}
|
||||
} catch (final SQLException ignore) {
|
||||
// ignore
|
||||
// log.warn("Null param of index [{}] type get failed, by: {}", paramIndex, e.getMessage());
|
||||
|
82
hutool-db/src/test/java/org/dromara/hutool/db/HanaTest.java
Normal file
82
hutool-db/src/test/java/org/dromara/hutool/db/HanaTest.java
Normal file
@@ -0,0 +1,82 @@
|
||||
package org.dromara.hutool.db;
|
||||
|
||||
import org.dromara.hutool.core.lang.Console;
|
||||
import org.junit.jupiter.api.Disabled;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
public class HanaTest {
|
||||
//@BeforeAll
|
||||
public static void createTable() {
|
||||
final Db db = Db.of("hana");
|
||||
final long count = db.count("SELECT * FROM SYS.TABLES WHERE TABLE_NAME = ? AND SCHEMA_NAME = CURRENT_SCHEMA", "user");
|
||||
if (count > 0) {
|
||||
db.execute("drop table \"user\"");
|
||||
}
|
||||
db.execute("CREATE COLUMN TABLE \"user\" (\"id\" INT NOT NULL, \"account\" VARCHAR(255), \"name\" VARCHAR(255), \"text\" VARCHAR(255), \"test1\" VARCHAR(255), \"pass\" VARCHAR(255), PRIMARY KEY (\"id\"))");
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void insertTest() {
|
||||
for (int id = 100; id < 200; id++) {
|
||||
Db.of("hana").insert(Entity.of("user")//
|
||||
.set("id", id)//
|
||||
.set("name", "测试用户" + id)//
|
||||
.set("text", "描述" + id)//
|
||||
.set("test1", "t" + id)//
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务测试<br>
|
||||
* 更新三条信息,低2条后抛出异常,正常情况下三条都应该不变
|
||||
*
|
||||
* @throws SQLException SQL异常
|
||||
*/
|
||||
@Test
|
||||
@Disabled
|
||||
public void txTest() throws SQLException {
|
||||
Db.of("hana").tx(db -> {
|
||||
final int update = db.update(Entity.of("user").set("text", "描述100"), Entity.of().set("id", 100));
|
||||
db.update(Entity.of("user").set("text", "描述101"), Entity.of().set("id", 101));
|
||||
if (1 == update) {
|
||||
// 手动指定异常,然后测试回滚触发
|
||||
throw new RuntimeException("Error");
|
||||
}
|
||||
db.update(Entity.of("user").set("text", "描述102"), Entity.of().set("id", 102));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void pageTest() {
|
||||
final PageResult<Entity> result = Db.of("hana").page(Entity.of("\"user\""), new Page(2, 10));
|
||||
for (final Entity entity : result) {
|
||||
Console.log(entity.get("id"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void getTimeStampTest() {
|
||||
final List<Entity> all = Db.of("hana").findAll("user");
|
||||
Console.log(all);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Disabled
|
||||
public void upsertTest() {
|
||||
final Db db = Db.of("hana");
|
||||
db.insert(Entity.of("user").set("id", 1).set("account", "ice").set("pass", "123456"));
|
||||
db.upsert(Entity.of("user").set("id", 1).set("account", "daoyou").set("pass", "a123456").set("name", "道友"));
|
||||
final Entity user = db.get(Entity.of("user").set("id", 1));
|
||||
System.out.println("user=======" + user.getStr("account") + "___" + user.getStr("pass"));
|
||||
assertEquals("daoyou", user.getStr("account"));
|
||||
}
|
||||
}
|
@@ -108,3 +108,9 @@ url = jdbc:oceanbase://localhost:2881/test
|
||||
user = root
|
||||
pass = 123456
|
||||
remarks = true
|
||||
|
||||
[hana]
|
||||
url = jdbc:sap://localhost:30015/HAP_CONN?autoReconnect=true
|
||||
user = DB
|
||||
pass = 123456
|
||||
remarks = true
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-extra</artifactId>
|
||||
|
@@ -68,7 +68,10 @@ public class ValidationUtil {
|
||||
|
||||
/**
|
||||
* 校验对象,校验不通过,直接抛出给调用者
|
||||
* 说明:如果Bean对象内部有非基本类型对象,需要把内部对象取出,进行手动多次调用,本方法
|
||||
*
|
||||
* <p>说明:如果Bean对象内部有非基本类型对象, 可以如下操作</p>
|
||||
* <p>方法一: 把内部对象取出,进行手动多次调用本方法</p>
|
||||
* <p>方法二: 在内部对象上注解 {@link Valid} 即可嵌套校验. (issue#3953@Github)</p>
|
||||
*
|
||||
* @param object 待校验对象
|
||||
* @param groups 待校验的组
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-http</artifactId>
|
||||
|
@@ -96,7 +96,7 @@ public class HttpClient5Response extends SimpleWrapper<ClassicHttpResponse> impl
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return this.entity.getContentLength();
|
||||
return null == this.entity ? -1 : this.entity.getContentLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -106,6 +106,9 @@ public class HttpClient5Response extends SimpleWrapper<ClassicHttpResponse> impl
|
||||
|
||||
@Override
|
||||
public InputStream bodyStream() {
|
||||
if(null == this.entity){
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return this.entity.getContent();
|
||||
} catch (final IOException e) {
|
||||
@@ -130,6 +133,9 @@ public class HttpClient5Response extends SimpleWrapper<ClassicHttpResponse> impl
|
||||
|
||||
@Override
|
||||
public String bodyStr() throws HttpException {
|
||||
if(null == this.entity){
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return EntityUtils.toString(this.entity, charset());
|
||||
} catch (final IOException e) {
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-json</artifactId>
|
||||
|
@@ -48,7 +48,7 @@ import java.util.Map;
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 详细介绍见;<a href="https://www.jianshu.com/p/576dbf44b2ae">https://www.jianshu.com/p/576dbf44b2ae</a>
|
||||
* 详细介绍见:<a href="https://www.jianshu.com/p/576dbf44b2ae">https://www.jianshu.com/p/576dbf44b2ae</a>
|
||||
* </p>
|
||||
*
|
||||
* @author Looly
|
||||
|
@@ -30,9 +30,9 @@ import javax.xml.xpath.XPathConstants;
|
||||
public class IssueI676ITTest {
|
||||
@Test
|
||||
public void parseXMLTest() {
|
||||
final JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.readUtf8Str("issueI676IT.json"));
|
||||
final JSONObject jsonObject = JSONUtil.parseObj(ResourceUtil.readUtf8Str("IssueI676IT.json"));
|
||||
final String xmlStr = JSONXMLSerializer.toXml(jsonObject, null, (String) null);
|
||||
final String content = String.valueOf(XPathUtil.getByXPath("/page/orderItems[1]/content", XmlUtil.readXml(xmlStr), XPathConstants.STRING));
|
||||
Assertions.assertEquals(content, "bar1");
|
||||
Assertions.assertEquals("bar1", content);
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-log</artifactId>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-poi</artifactId>
|
||||
|
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.dromara.hutool.poi.csv;
|
||||
|
||||
import org.dromara.hutool.core.io.file.FileUtil;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -36,8 +35,9 @@ public class Issue3705Test {
|
||||
csvWriter.flush();
|
||||
}
|
||||
|
||||
// CsvWriteConfig中默认为`\r\n`
|
||||
Assertions.assertEquals(
|
||||
"\"2024-08-20 14:24:35,\"" + FileUtil.getLineSeparator() + "最后一行",
|
||||
"\"2024-08-20 14:24:35,\"\r\n最后一行",
|
||||
stringWriter.toString());
|
||||
}
|
||||
}
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-setting</artifactId>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-socket</artifactId>
|
||||
|
@@ -25,7 +25,7 @@
|
||||
<parent>
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-swing</artifactId>
|
||||
|
12
pom.xml
12
pom.xml
@@ -24,7 +24,7 @@
|
||||
|
||||
<groupId>org.dromara.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>6.0.0-M22</version>
|
||||
<version>6.0.0-M23</version>
|
||||
<name>hutool</name>
|
||||
<description>
|
||||
Hutool是一个功能丰富且易用的Java工具库,通过诸多实用工具类的使用,旨在帮助开发者快速、便捷地完成各类开发任务。这些封装的工具涵盖了字符串、数字、集合、编码、日期、文件、IO、加密、数据库JDBC、JSON、HTTP客户端等一系列操作,可以满足各种不同的开发需求。
|
||||
@@ -59,7 +59,7 @@
|
||||
<junit.version>5.11.3</junit.version>
|
||||
<lombok.version>1.18.36</lombok.version>
|
||||
<kotlin-version>2.0.20</kotlin-version>
|
||||
<bouncycastle.version>1.79</bouncycastle.version>
|
||||
<bouncycastle.version>1.81</bouncycastle.version>
|
||||
|
||||
<!-- version for json and jmh -->
|
||||
<jackson.version>2.18.1</jackson.version>
|
||||
@@ -241,6 +241,12 @@
|
||||
<generateBackupPoms>false</generateBackupPoms>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<!-- 单元测试 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>3.5.3</version>
|
||||
</plugin>
|
||||
<!-- 测试覆盖度 -->
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
@@ -306,7 +312,7 @@
|
||||
<plugin>
|
||||
<groupId>org.sonatype.central</groupId>
|
||||
<artifactId>central-publishing-maven-plugin</artifactId>
|
||||
<version>0.7.0</version>
|
||||
<version>0.8.0</version>
|
||||
<extensions>true</extensions>
|
||||
<configuration>
|
||||
<publishingServerId>central6</publishingServerId>
|
||||
|
Reference in New Issue
Block a user