前置:
1.部署Dify,见官方教程及介绍https://docs.dify.ai/zh-hans,本文主要讲基于部署完之后的java实现的调用它的接口实现AI智能聊天,其他AI功能后续有用到再补充,没有就看缘分
2.什么是Dify?可以简单理解为集成了各类AI大模型的一个中转平台,你的服务请求它的接口,再通过在Dify上配置好的应用尽情请求获取到回复内容
3.用到了Dify,但其他相关的提供大模型平台的用法可能都差不多,可做参考
4.用到springboot/java
AI机器人创建
1.先在Dify上创建一个聊天应用
选择聊天助手
创建完毕
获取请求地址跟API密钥,写入配置文件
开始开发
1.pom文件新增依赖
<dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>retrofit</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>converter-jackson</artifactId> <version>2.9.0</version> </dependency> <dependency> <groupId>com.squareup.retrofit2</groupId> <artifactId>adapter-rxjava2</artifactId> <version>2.9.0</version> </dependency>
2.yml配置文件
dify:
#复制的apiKey,以下为假数据
apiKey: 'app-ugR39FF8DDDAOaY4yBZDq4ba'
#apiHost: 'http://localhost/v1/' 记得需要/结尾
apiHost: 'http://api-test.xxxxxx.cn/v1/'
3.控制层
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
@RequiredArgsConstructor
@RestController
@Slf4j
@RequestMapping("/ai")
public class PsyAiChatController {
//流式模式
private final static String STREAMING_MODE = "streaming";
//阻塞模式
private final static String BLOCKING_MODE = "blocking";
@Resource
private IPsyAiChatService difyChatService;
/**
* AI陪聊
* @param bo
* @param response
* @return
*/
@PostMapping("/chat")
@ResponseBody
public ResponseBodyEmitter chatSeeMessage(@RequestBody @Valid AiChatBo bo, HttpServletResponse response) {
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
DifyRequest difyRequest = new DifyRequest();
difyRequest.setResponseMode(STREAMING_MODE);
difyRequest.setQuery(bo.getQuery());
difyRequest.setUser(bo.getUserId().toString());
if(StringUtils.isNotEmpty(bo.getConversationId())){
difyRequest.setConversationId(bo.getConversationId());
}
return difyChatService.sseChatPrompt(difyRequest);
}
}
4.前端请求实体
@Data
public class AiChatBo implements Serializable {
/**
* 用户id
*/
private Long userId;
/**
* 聊天内容
*/
private String query;
/**
* (选填)会话id,若基于之前的聊天记录继续对话,必传之前消息的 conversation_id
*/
private String conversationId;
}
5.请求Dify请求体
package com.xmzs.common.edu.domain.dify.bo;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
import java.util.Map;
/**
* dify聊天请求体
* @Author: zyt
* @CreateTime: 2024-11-05
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DifyRequest {
/**
* 输入提问内容
*/
private String query;
/**
* (选填)允许传入 App 定义的各变量值
*/
private Map<String,String> inputs;
/**
* 回复模式:streaming流式模式,blocking阻塞模式
*/
@JsonProperty("response_mode")
private String responseMode;
/**
* (选填)会话id,需要基于之前的聊天记录继续对话,必须传之前消息的 conversation_id
* */
@JsonProperty("conversation_id")
private String conversationId;
/**
* 用户标识,用于定义终端用户的身份,方便检索、统计。 由开发者定义规则,需保证用户标识在应用内唯一。
* */
private String user="";
/**
* (选填)自动生成标题,默认 false。 可通过调用会话重命名接口并设置 auto_generate 为 true 实现异步生成标题
* */
@JsonProperty("autoGenerate_name")
private boolean autoGenerateName=false;
private List<UploadFile> files;
@Data
public class UploadFile{
/**
* 支持类型:图片 image(目前仅支持图片格式)
* */
private String type="image";
/**
* remote_url: 图片地址
* local_file: 上传文件
* */
@JsonProperty("transfer_method")
private String transferMethod;
/**
*
* */
private String url;
/**
* 上传文件 ID。(仅当传递方式为 local_file 时)
* */
@JsonProperty("upload_file_id")
private String uploadFileId;
}
}
6.service
接口
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
public interface IPsyAiChatService {
/**
* AI陪聊
* @param chatRequest
* @return
*/
ResponseBodyEmitter sseChatPrompt(DifyRequest chatRequest);
}
实现类
import cn.hutool.core.date.DateUnit;
import jakarta.annotation.Resource;
import jodd.cache.TimedCache;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
/**
* AI相关service
* @Author: zyt
* @CreateTime: 2024-11-05
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class PsyAiChatServiceImpl implements IPsyAiChatService {
/**
* 设置sse链接时长缓存
*/
public static final long TIMEOUT = 30 * DateUnit.MINUTE.getMillis();
public static final TimedCache<String, Object> LOCAL_CACHE = new TimedCache<>(TIMEOUT);
@Resource
private DifyApiClient difyApiClient;
/**
* AI陪聊
* @param chatRequest
* @return
*/
@Override
public ResponseBodyEmitter sseChatPrompt(DifyRequest chatRequest) {
ResponseBodyEmitter sseEmitter = this.getResponseBodyEmitter(chatRequest);
DifySseEventSourceListener listener = new DifySseEventSourceListener(sseEmitter);
difyApiClient.streamChatCompletion(chatRequest,listener);
return sseEmitter;
}
/**
* 创建sse连接
* @param chatRequest
* @return
*/
private ResponseBodyEmitter getResponseBodyEmitter(DifyRequest chatRequest) {
//0L设置允许超时
ResponseBodyEmitter sseEmitter = new ResponseBodyEmitter(0L);
sseEmitter.onCompletion(() -> {
log.info("会话[{}]sse结束连接......", chatRequest.getConversationId());
LOCAL_CACHE.remove(chatRequest.getConversationId());
});
//超时回调
sseEmitter.onTimeout(() -> {
log.error("会话[{}]sse连接超时......", chatRequest.getConversationId());
});
//异常回调
sseEmitter.onError(
throwable -> {
log.error("会话[{}]sse连接失败......", chatRequest.getConversationId());
}
);
LOCAL_CACHE.put(chatRequest.getConversationId(), sseEmitter);
log.info("会话[{}]创建sse连接成功!", chatRequest.getConversationId());
return sseEmitter;
}
}
事件监听器
用于流式模式,监听dify返回的多个结果,模拟ai问答一个字一个字回答打印出来的效果,如果是阻塞模式则需要dify处理完回复把完整结果返回,则用不到监听器
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import java.util.Objects;
/**
* dify sse事件监听器
* @Author: zyt
* @CreateTime: 2024-11-05
*/
@Slf4j
@AllArgsConstructor
public class DifySseEventSourceListener extends EventSourceListener {
private static final String DONE_SIGNAL = "[DONE]";
private final ResponseBodyEmitter emitter;
/**
* {@inheritDoc}
*/
@Override
public void onOpen(EventSource eventSource, Response response) {
log.info("Dify建立sse连接...");
}
/**
* {@inheritDoc}
*/
@SneakyThrows
@Override
public void onEvent(EventSource eventSource, String id, String type, String data) {
log.debug("DifyEventSourceListener data : {}",data);
if (data.equals(DONE_SIGNAL)) {
// 成功响应
emitter.complete();
return;
}
ObjectMapper mapper = new ObjectMapper();
ChunkChatCompletionResponse completionResponse = mapper.readValue(data, ChunkChatCompletionResponse.class);
if(completionResponse == null){
return;
}
String content = completionResponse.getAnswer();
if(StringUtils.isEmpty(content)){
return;
}
try {
emitter.send(content);
} catch (Exception e) {
log.error("sse信息推送失败!",e);
eventSource.cancel();
}
}
@Override
public void onClosed(EventSource eventSource) {
log.info("Dify关闭sse连接...");
}
@SneakyThrows
@Override
public void onFailure(EventSource eventSource, Throwable t, Response response) {
if (Objects.isNull(response)) {
return;
}
ResponseBody body = response.body();
if (Objects.nonNull(body)) {
log.error("Dify sse连接异常data:{},异常:{}", body.string(), t);
} else {
log.error("Dify sse连接异常data:{},异常:{}", response, t);
}
eventSource.cancel();
}
}
其他实体
@Data
public class Usage {
@JsonProperty("prompt_tokens")
private int promptTokens;
@JsonProperty("prompt_unit_price")
private double promptNnitPrice;
@JsonProperty("prompt_price_unit")
private double promptPriceUnit;
@JsonProperty("prompt_price")
private double promptPrice;
@JsonProperty("completion_tokens")
private int completionTokens;
@JsonProperty("completion_unit_price")
private double completionUnitPrice;//": "0.002",
@JsonProperty("completion_price_unit")
private double completionPriceUnit;//": "0.001",
@JsonProperty("completion_price")
private double completionPrice;//": "0.0002560",
@JsonProperty("total_tokens")
private int totalTokens;//": 1161,
@JsonProperty("total_price")
private double totalPrice;//": "0.0012890",
@JsonProperty("currency")
private String currency= "USD";
@JsonProperty("latency")
private double latency;
}
@Data
public class RetrieverResources {
//": 1
@JsonProperty("position")
private int position;
//": "101b4c97-fc2e-463c-90b1-5261a4cdcafb",
@JsonProperty("dataset_id")
private String datasetId;
//": "iPhone",
@JsonProperty("dataset_name")
private String datasetName;
//": "8dd1ad74-0b5f-4175-b735-7d98bbbb4e00",
@JsonProperty("document_id")
private String documentId;
//": "iPhone List",
@JsonProperty("document_name")
private String documentName;
//": "ed599c7f-2766-4294-9d1d-e5235a61270a",
@JsonProperty("segment_id")
private String segmentId;
//": 0.98457545,
@JsonProperty("score")
private String score;
//": "\"Model\",\"Release Date\",\"Display Size\",\"Resolution\",\"Processor\",\"RAM\",\"Storage\",\"Camera\",\"Battery\",\"Operating System\"\n\"iPhone 13 Pro Max\",\"September 24, 2021\",\"6.7 inch\",\"1284 x 2778\",\"Hexa-core (2x3.23 GHz Avalanche + 4x1.82 GHz Blizzard)\",\"6 GB\",\"128, 256, 512 GB, 1TB\",\"12 MP\",\"4352 mAh\",\"iOS 15\""
@JsonProperty("content")
private String content;
}
@Data
public class Metadata {
@JsonProperty("usage")
private Usage usage;
@JsonProperty("retriever_resources")
private List<RetrieverResources> retrieverResources;
}
/**
* dify聊天请求体
* @Author: zyt
* @CreateTime: 2024-11-05
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class DifyResponse {
/**
* 消息id
*/
@JsonProperty("message_id")
private String messageId;
/**
* 事件
*/
private String event;
/**
* 会话id
*/
@JsonProperty("conversation_id")
private String conversationId;
/**
* 创建时间戳
*/
@JsonProperty("created_at")
private int createdAt;
}
/**
*
*流式消息时返回对象
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChunkChatCompletionResponse extends DifyResponse{
//每一轮Agent迭代都会有一个唯一的id
private String id;
//任务 ID,用于请求跟踪和下方的停止响应接口
@JsonProperty("task_id")
private String taskId;
//LLM 返回文本块内容
private String answer;
//agent_thought在消息中的位置,如第一轮迭代position为1
private int position;
//agent的思考内容
private String thought;
//工具调用的返回结果
private String observation;
//使用的工具列表,以 ; 分割多个工具
private String tool;
//工具的输入,JSON格式的字符串(object)。如:{"dalle3": {"prompt": "a cute cat"}}
@JsonProperty("tool_input")
private String toolInput;
//当前 agent_thought 关联的文件ID
@JsonProperty("message_files")
private List<String> messageFiles;
//event: message_file时,文件类型,目前仅为image
private String type;
// (string) 文件归属,user或assistant,该接口返回仅为 assistant
@JsonProperty("belongs_to")
private String belongsTo;
//文件访问地址
private String url;
//元数据
private Metadata metadata;
//HTTP 状态码
private int status;
//错误码
private String code;
//错误消息
private String message;
}
/**
* 阻塞式消息时返回对象
*
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class ChatCompletionResponse extends DifyResponse{
/**App 模式,固定为 chat*/
private String mode="chat";
/**完整回复内容*/
/** LLM 返回文本全部内容*/
private String answer;
@JsonProperty("task_id")
private String taskId;
/**元数据*/
private Metadata metadata;
}
Dify客户端请求类及配置文件
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.Resource;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.sse.EventSource;
import okhttp3.sse.EventSourceListener;
import okhttp3.sse.EventSources;
import org.jetbrains.annotations.NotNull;
import retrofit2.Retrofit;
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
import retrofit2.converter.jackson.JacksonConverterFactory;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* @Author: linjinde
* @CreateTime: 2024-11-05
*/
@Slf4j
public class DifyApiClient {
//自定义api host使用builder的方式构造client
@Getter
private String apiHost;
@Getter
private List<String> apiKey;
@Getter
private DifyApi difyApi;
// 自定义okHttpClient,非自定义为sdk默认OkHttpClient实例
@Getter
private OkHttpClient okHttpClient;
// api key的获取策略
@Getter
private KeyStrategyFunction<List<String>, String> keyStrategy;
/**
* 构造器
* @return OpenAiClient.Builder
*/
public static Builder builder() {
return new Builder();
}
/**
* 构造
* @param builder
*/
private DifyApiClient(Builder builder) {
if (StrUtil.isBlank(builder.apiHost)) {
builder.apiHost = DifyConst.OPENAI_HOST;
}
apiHost = builder.apiHost;
apiKey = builder.apiKey;
if (Objects.isNull(builder.okHttpClient)) {
builder.okHttpClient = this.okHttpClient();
} else {
//自定义的okhttpClient 需要增加api keys
builder.okHttpClient = builder.okHttpClient
.newBuilder()
.build();
}
okHttpClient = builder.okHttpClient;
this.difyApi = new Retrofit.Builder()
.baseUrl(apiHost)
.client(okHttpClient)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.addConverterFactory(JacksonConverterFactory.create())
.build().create(DifyApi.class);
}
/**
* 创建默认OkHttpClient
* @return
*/
private OkHttpClient okHttpClient() {
return new OkHttpClient
.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS).build();
}
public static final class Builder {
//api keys
private @NotNull List<String> apiKey;
//api请求地址,结尾处有斜杠
private String apiHost;
//自定义OkhttpClient
private OkHttpClient okHttpClient;
// api key的获取策略
private KeyStrategyFunction keyStrategy;
public Builder() {
}
/**
* @param val api请求地址,结尾处有斜杠
* @return Builder对象
*/
public Builder apiHost(String val) {
apiHost = val;
return this;
}
public Builder apiKey(@NotNull List<String> val) {
apiKey = val;
return this;
}
public Builder keyStrategy(KeyStrategyFunction val) {
keyStrategy = val;
return this;
}
public Builder okHttpClient(OkHttpClient val) {
okHttpClient = val;
return this;
}
public DifyApiClient build() {
return new DifyApiClient(this);
}
}
/**
* 流式输出
* @param difyRequest
* @param eventSourceListener
* @param <T>
*/
public <T extends DifyResponse> void streamChatCompletion(DifyRequest difyRequest, EventSourceListener eventSourceListener) {
if (Objects.isNull(eventSourceListener)) {
log.info("EventSourceListener为空");
throw new EduException("300001");
}
try {
if(CollectionUtil.isNotEmpty(difyRequest.getInputs())){
difyRequest.setInputs(new HashMap<>());
}
if(CollectionUtil.isNotEmpty(difyRequest.getFiles())){
difyRequest.setFiles(new ArrayList<>());
}
//构建请求参数json数据
ObjectMapper mapper = new ObjectMapper();
String requestBody = mapper.writeValueAsString(difyRequest);
log.debug("请求参数:{}",requestBody);
//创建事件工厂
EventSource.Factory factory = EventSources.createFactory(this.okHttpClient);
Request request = new Request.Builder()
.url(this.apiHost + "chat-messages")
.addHeader(Header.AUTHORIZATION.getValue(), "Bearer " + apiKey.get(0))
.post(RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBody))
.build();
factory.newEventSource(request, eventSourceListener);
} catch (Exception e) {
log.error("请求参数解析异常:{}", e.getMessage());
}
}
/**
* 阻塞式问答
* @param difyRequest chat completion
* @return 返回答案
*/
public ChatCompletionResponse chatMessages(@Body DifyRequest difyRequest, String serverKey){
if(difyRequest.getInputs()==null){
difyRequest.setInputs(new HashMap<>());
}
if(difyRequest.getFiles() ==null){
difyRequest.setFiles(new ArrayList<>());
}
log.debug(JsonUtils.toJsonString(difyRequest));
// 序列化请求体
ObjectMapper mapper = new ObjectMapper();
String requestBodyJson = "";
try {
requestBodyJson = mapper.writeValueAsString(difyRequest);
} catch (Exception e) {
log.error("请求体序列化失败:{}", e.getMessage());
throw new EduException("300001");
}
// 创建请求体
RequestBody requestBody = RequestBody.create(MediaType.parse(ContentType.JSON.getValue()), requestBodyJson);
// 创建请求对象,这里动态地将API Key设置到请求头中
Request request = new Request.Builder()
.url(this.apiHost + "chat-messages") // 此处路径根据实际需要进行调整
.addHeader("Authorization", "Bearer " + serverKey) // 设置动态API Key
.post(requestBody)
.build();
ChatCompletionResponse response;
try {
// 执行同步请求并获取响应
okhttp3.Response okHttpResponse = okHttpClient.newCall(request).execute();
if (!okHttpResponse.isSuccessful() || okHttpResponse.body() == null) {
log.error("请求失败:HTTP {},message: {}", okHttpResponse.code(),okHttpResponse.message());
throw new BaseException("请求失败:HTTP " + okHttpResponse.code()+" "+okHttpResponse.message());
}
// 反序列化响应体
String responseBody = okHttpResponse.body().string();
response = mapper.readValue(responseBody, ChatCompletionResponse.class);
} catch (Exception e) {
log.error("请求异常:{}", e.getMessage());
throw new EduException("300001");
}
// 返回结果
return response;
}
}
import io.reactivex.Single;
import retrofit2.http.Body;
import retrofit2.http.POST;
/**
* @Author: zyt
* @CreateTime: 2024-11-05
*/
public interface DifyApi {
/**
* 发送对话消息
*
* @param difyRequest chat completion
* @return 返回答案
*/
@POST("chat-messages")
Single<ChatCompletionResponse> chatMessages(@Body DifyRequest difyRequest);
}
import com.xmzs.edu.client.dify.DifyApiClient;
import okhttp3.OkHttpClient;
import okhttp3.logging.HttpLoggingInterceptor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* dify配置类
* @Author: linjinde
* @CreateTime: 2024-11-05
*/
@Configuration
public class DifyConfig {
@Value("${dify.apiKey}")
private List<String> apiKey;
@Value("${dify.apiHost}")
private String apiHost;
// @Bean(name = "openAiStreamClient")
// public DifyStreamClient DifyStreamClient() {
//
// OkHttpClient okHttpClient;
// HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new OpenAILogger());
// httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
// okHttpClient = new OkHttpClient
// .Builder()
// .addInterceptor(httpLoggingInterceptor)
// .connectTimeout(30, TimeUnit.SECONDS)
// .writeTimeout(600, TimeUnit.SECONDS)
// .readTimeout(600, TimeUnit.SECONDS)
// .build();
//
// return OpenAiStreamClient
// .builder()
// .apiHost(apiHost)
// .apiKey(apiKey)
// //自定义key使用策略 默认随机策略
// .keyStrategy(new KeyRandomStrategy())
// .okHttpClient(okHttpClient)
// .build();
// }
@Bean(name = "difyApiClient")
public DifyApiClient difyApiClient() {
OkHttpClient okHttpClient;
HttpLoggingInterceptor httpLoggingInterceptor = new HttpLoggingInterceptor(new DifyLogger());
httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS);
okHttpClient = new OkHttpClient
.Builder()
.addInterceptor(httpLoggingInterceptor)
.connectTimeout(30, TimeUnit.SECONDS)
.writeTimeout(600, TimeUnit.SECONDS)
.readTimeout(600, TimeUnit.SECONDS)
.build();
return DifyApiClient
.builder()
.apiHost(apiHost)
.apiKey(apiKey)
//自定义key使用策略 默认随机策略
.keyStrategy(new KeyRandomStrategy())
.okHttpClient(okHttpClient)
.build();
}
}
public class DifyConst {
public final static String OPENAI_HOST = "https://api.openai.com/";
public final static int SUCCEED_CODE = 200;
/** DALL3绘图费用 */
public final static double DALL3_COST = 0.5;
public final static double GPT3_IN = 0.01;
public final static double GPT3_OUT = 0.02;
/**
* 以毫厘为单位,0.1元=1000
* 毫厘(0.0001)
* **/
public final static long GPT4_IN = 1000;
/**
* 以毫厘为单位,0.3元=3000
* 毫厘(0.0001)
* **/
public final static long GPT4_OUT = 3000;
/** 默认账户余额 */
public final static double USER_BALANCE = 5;
}
/**
* * @Author: linjinde
* * @CreateTime: 2024-11-05
* dify请求日志打印
*/
@Slf4j
public class DifyLogger implements HttpLoggingInterceptor.Logger {
@Override
public void log(String message) {
log.info("Dify数据请求中:{}", message);
}
}
/**
* 随机策略
* @Author: linjinde
* @CreateTime: 2024-11-05
*/
public class KeyRandomStrategy implements KeyStrategyFunction<List<String>, String> {
@Override
public String apply(List<String> apiKeys) {
return RandomUtil.randomEle(apiKeys);
}
}
@FunctionalInterface
public interface KeyStrategyFunction<T, R> {
/**
* Applies this function to the given argument.
*
* @param t the function argument
* @return the function result
*/
R apply(T t);
}
测试
over 后续有再补充