perf: 收敛工作流状态与高 IO 节点开销
- 落地 Redis 版本状态、触发租约和定义缓存 - 优化数据批写、插件请求、文件下载与审计日志 - 补齐循环范围校验、轮询兼容和专项测试
This commit is contained in:
@@ -1,182 +1,461 @@
|
||||
package tech.easyflow.common.ai.plugin;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.http.*;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import okhttp3.Dispatcher;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.BufferedSink;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class PluginHttpClient {
|
||||
/**
|
||||
* 使用共享连接池、有界并发和实际响应字节限制调用 HTTP 插件。
|
||||
*/
|
||||
public final class PluginHttpClient {
|
||||
|
||||
private static final int TIMEOUT = 10_000;
|
||||
private static final int TIMEOUT_MILLIS = 10_000;
|
||||
private static final long DEFAULT_MAX_RESPONSE_BYTES =
|
||||
64L * 1024L * 1024L;
|
||||
private static final int MAX_TRACKED_HOSTS = 512;
|
||||
private static final Semaphore GLOBAL_PERMITS =
|
||||
new Semaphore(32, true);
|
||||
private static final Semaphore OVERFLOW_HOST_PERMITS =
|
||||
new Semaphore(8, true);
|
||||
private static final Map<String, Semaphore> HOST_PERMITS =
|
||||
new ConcurrentHashMap<>();
|
||||
private static final Object HOST_REGISTRY_LOCK = new Object();
|
||||
private static final OkHttpClient CLIENT = buildClient(true);
|
||||
private static final OkHttpClient NO_RETRY_CLIENT = buildClient(false);
|
||||
|
||||
public static JSONObject sendRequest(String url, String method,
|
||||
Map<String, Object> headers,
|
||||
List<PluginParam> pluginParams) {
|
||||
// 1. 处理路径参数
|
||||
String processedUrl = replacePathVariables(url, pluginParams);
|
||||
|
||||
// 2. 初始化请求
|
||||
Method httpMethod = Method.valueOf(method.toUpperCase());
|
||||
HttpRequest request = HttpRequest.of(processedUrl)
|
||||
.method(httpMethod)
|
||||
.timeout(TIMEOUT);
|
||||
// 3. 处理请求头(合并默认头和参数头)
|
||||
processHeaders(request, headers, pluginParams);
|
||||
|
||||
// 4. 处理查询参数和请求体
|
||||
processQueryAndBodyParams(request, httpMethod, pluginParams);
|
||||
|
||||
// 5. 执行请求
|
||||
HttpResponse response = request.execute();
|
||||
return JSONUtil.parseObj(response.body());
|
||||
private PluginHttpClient() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理请求头(合并默认头和参数头)
|
||||
* 发送插件请求。
|
||||
*
|
||||
* @param url 插件 URL
|
||||
* @param method HTTP 方法
|
||||
* @param headers 默认请求头
|
||||
* @param pluginParams 插件参数
|
||||
* @return JSON 响应
|
||||
*/
|
||||
private static void processHeaders(HttpRequest request,
|
||||
Map<String, Object> defaultHeaders,
|
||||
List<PluginParam> params) {
|
||||
// 添加默认头
|
||||
if (ObjectUtil.isNotEmpty(defaultHeaders)) {
|
||||
defaultHeaders.forEach((k, v) -> request.header(k, v.toString()));
|
||||
}
|
||||
public static JSONObject sendRequest(
|
||||
String url,
|
||||
String method,
|
||||
Map<String, Object> headers,
|
||||
List<PluginParam> pluginParams) {
|
||||
String normalizedMethod = method == null
|
||||
? "GET"
|
||||
: method.trim().toUpperCase();
|
||||
List<PluginParam> safeParams = pluginParams == null
|
||||
? List.of()
|
||||
: pluginParams;
|
||||
HttpUrl processedUrl = buildUrl(url, safeParams);
|
||||
Request.Builder requestBuilder = new Request.Builder().url(processedUrl);
|
||||
applyHeaders(requestBuilder, headers, safeParams);
|
||||
RequestBody requestBody = buildRequestBody(
|
||||
normalizedMethod, safeParams);
|
||||
applyMethod(requestBuilder, normalizedMethod, requestBody);
|
||||
|
||||
// 添加参数中指定的头
|
||||
params.stream()
|
||||
.filter(p -> "header".equalsIgnoreCase(p.getMethod()) && p.isEnabled())
|
||||
.forEach(p -> request.header(p.getName(), p.getDefaultValue().toString()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理查询参数和请求体
|
||||
*/
|
||||
/**
|
||||
* 处理查询参数和请求体(新增文件参数支持)
|
||||
*/
|
||||
private static void processQueryAndBodyParams(HttpRequest request,
|
||||
Method httpMethod,
|
||||
List<PluginParam> params) {
|
||||
Map<String, Object> queryParams = new HashMap<>();
|
||||
Map<String, Object> bodyParams = new HashMap<>();
|
||||
// 标记是否包含文件参数
|
||||
AtomicBoolean hasMultipartFile = new AtomicBoolean(false);
|
||||
|
||||
// 分类参数(同时检测是否有文件)
|
||||
params.stream()
|
||||
.filter(PluginParam::isEnabled)
|
||||
.forEach(p -> {
|
||||
String methodType = p.getMethod().toLowerCase();
|
||||
Object paramValue = buildNestedParamValue(p);
|
||||
|
||||
// 检测是否为文件参数(MultipartFile 类型)
|
||||
if (paramValue instanceof org.springframework.web.multipart.MultipartFile) {
|
||||
hasMultipartFile.set(true);
|
||||
}
|
||||
|
||||
switch (methodType) {
|
||||
case "query":
|
||||
queryParams.put(p.getName(), paramValue);
|
||||
break;
|
||||
case "body":
|
||||
bodyParams.put(p.getName(), paramValue);
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
// 1. 设置查询参数(原有逻辑不变)
|
||||
if (!queryParams.isEmpty()) {
|
||||
request.form(queryParams);
|
||||
}
|
||||
|
||||
// 2. 设置请求体(分两种情况:有文件 vs 无文件)
|
||||
if (!bodyParams.isEmpty() && (httpMethod == Method.POST || httpMethod == Method.PUT)) {
|
||||
if (hasMultipartFile.get()) {
|
||||
// 2.1 包含文件参数 → 用 multipart/form-data 格式
|
||||
processMultipartBody(request, bodyParams);
|
||||
} else {
|
||||
// 2.2 无文件参数 → 保持原有 JSON 格式
|
||||
request.body(JSONUtil.toJsonStr(bodyParams))
|
||||
.header(Header.CONTENT_TYPE, ContentType.JSON.getValue());
|
||||
Semaphore hostPermits = hostSemaphore(processedUrl.host());
|
||||
boolean hostAcquired = false;
|
||||
boolean globalAcquired = false;
|
||||
try {
|
||||
hostAcquired = hostPermits.tryAcquire(
|
||||
1L, TimeUnit.SECONDS);
|
||||
if (!hostAcquired) {
|
||||
throw new IllegalStateException(
|
||||
"插件目标并发已达上限: " + processedUrl.host());
|
||||
}
|
||||
globalAcquired = GLOBAL_PERMITS.tryAcquire(
|
||||
1L, TimeUnit.SECONDS);
|
||||
if (!globalAcquired) {
|
||||
throw new IllegalStateException(
|
||||
"插件 HTTP 并发已达上限");
|
||||
}
|
||||
OkHttpClient client = isIdempotent(normalizedMethod)
|
||||
? CLIENT
|
||||
: NO_RETRY_CLIENT;
|
||||
try (Response response =
|
||||
client.newCall(requestBuilder.build()).execute()) {
|
||||
ResponseBody body = response.body();
|
||||
if (body == null) {
|
||||
throw new IllegalStateException("插件响应内容为空");
|
||||
}
|
||||
long maxBytes = Long.getLong(
|
||||
"easyflow.plugin.http.max-response-bytes",
|
||||
DEFAULT_MAX_RESPONSE_BYTES);
|
||||
String responseText = readBounded(body, maxBytes);
|
||||
return JSONUtil.parseObj(responseText);
|
||||
}
|
||||
} catch (InterruptedException error) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IllegalStateException(
|
||||
"插件 HTTP 请求被中断", error);
|
||||
} catch (IOException error) {
|
||||
throw new IllegalStateException(
|
||||
"插件 HTTP 请求失败", error);
|
||||
} finally {
|
||||
if (globalAcquired) {
|
||||
GLOBAL_PERMITS.release();
|
||||
}
|
||||
if (hostAcquired) {
|
||||
hostPermits.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构建嵌套参数值
|
||||
* @param param 当前参数
|
||||
* @return 如果是 Object 类型,返回 Map;否则返回 defaultValue
|
||||
* 创建共享 OkHttp 客户端。
|
||||
*
|
||||
* @param retryOnConnectionFailure 是否允许连接级自动恢复
|
||||
* @return 共享客户端
|
||||
*/
|
||||
private static Object buildNestedParamValue(PluginParam param) {
|
||||
// 如果不是 Object 类型,直接返回默认值
|
||||
private static OkHttpClient buildClient(
|
||||
boolean retryOnConnectionFailure) {
|
||||
Dispatcher dispatcher = new Dispatcher();
|
||||
dispatcher.setMaxRequests(32);
|
||||
dispatcher.setMaxRequestsPerHost(8);
|
||||
return new OkHttpClient.Builder()
|
||||
.dispatcher(dispatcher)
|
||||
.connectTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.readTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.writeTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.callTimeout(TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.retryOnConnectionFailure(retryOnConnectionFailure)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带路径参数和查询参数的 URL。
|
||||
*
|
||||
* @param url 原始 URL
|
||||
* @param params 插件参数
|
||||
* @return 完整 URL
|
||||
*/
|
||||
private static HttpUrl buildUrl(
|
||||
String url, List<PluginParam> params) {
|
||||
String processed = replacePathVariables(url, params);
|
||||
HttpUrl parsed = HttpUrl.parse(processed);
|
||||
if (parsed == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"插件 URL 不合法: " + processed);
|
||||
}
|
||||
HttpUrl.Builder builder = parsed.newBuilder();
|
||||
for (PluginParam param : params) {
|
||||
if (param.isEnabled()
|
||||
&& "query".equalsIgnoreCase(param.getMethod())
|
||||
&& param.getDefaultValue() != null) {
|
||||
builder.addQueryParameter(
|
||||
param.getName(),
|
||||
stringify(param.getDefaultValue()));
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并默认请求头和参数请求头。
|
||||
*
|
||||
* @param builder 请求构造器
|
||||
* @param headers 默认请求头
|
||||
* @param params 插件参数
|
||||
*/
|
||||
private static void applyHeaders(
|
||||
Request.Builder builder,
|
||||
Map<String, Object> headers,
|
||||
List<PluginParam> params) {
|
||||
if (headers != null) {
|
||||
headers.forEach((name, value) -> {
|
||||
if (name != null && value != null) {
|
||||
builder.header(name, String.valueOf(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
for (PluginParam param : params) {
|
||||
if (param.isEnabled()
|
||||
&& "header".equalsIgnoreCase(param.getMethod())
|
||||
&& param.getDefaultValue() != null) {
|
||||
builder.header(
|
||||
param.getName(),
|
||||
String.valueOf(param.getDefaultValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造 JSON 或 multipart 请求体。
|
||||
*
|
||||
* @param method HTTP 方法
|
||||
* @param params 插件参数
|
||||
* @return 请求体;无请求体时为 {@code null}
|
||||
*/
|
||||
private static RequestBody buildRequestBody(
|
||||
String method, List<PluginParam> params) {
|
||||
if (!supportsRequestBody(method)) {
|
||||
return null;
|
||||
}
|
||||
Map<String, Object> bodyValues = new HashMap<>();
|
||||
boolean multipart = false;
|
||||
for (PluginParam param : params) {
|
||||
if (!param.isEnabled()
|
||||
|| !"body".equalsIgnoreCase(param.getMethod())) {
|
||||
continue;
|
||||
}
|
||||
Object value = buildNestedParamValue(param);
|
||||
bodyValues.put(param.getName(), value);
|
||||
multipart |= value instanceof MultipartFile;
|
||||
}
|
||||
if (bodyValues.isEmpty()) {
|
||||
return RequestBody.create(
|
||||
new byte[0], null);
|
||||
}
|
||||
if (!multipart) {
|
||||
return RequestBody.create(
|
||||
JSONUtil.toJsonStr(bodyValues),
|
||||
MediaType.parse("application/json; charset=utf-8"));
|
||||
}
|
||||
MultipartBody.Builder builder =
|
||||
new MultipartBody.Builder().setType(MultipartBody.FORM);
|
||||
for (Map.Entry<String, Object> entry : bodyValues.entrySet()) {
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof MultipartFile) {
|
||||
MultipartFile file = (MultipartFile) value;
|
||||
MediaType contentType = MediaType.parse(
|
||||
file.getContentType() == null
|
||||
? "application/octet-stream"
|
||||
: file.getContentType());
|
||||
builder.addFormDataPart(
|
||||
entry.getKey(),
|
||||
Objects.toString(
|
||||
file.getOriginalFilename(),
|
||||
entry.getKey()),
|
||||
streamingFileBody(file, contentType));
|
||||
} else {
|
||||
builder.addFormDataPart(
|
||||
entry.getKey(), stringify(value));
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建从 MultipartFile 流式读取的请求体,避免 getBytes 二次复制。
|
||||
*
|
||||
* @param file 文件参数
|
||||
* @param mediaType 内容类型
|
||||
* @return 流式请求体
|
||||
*/
|
||||
private static RequestBody streamingFileBody(
|
||||
MultipartFile file, MediaType mediaType) {
|
||||
return new RequestBody() {
|
||||
@Override
|
||||
public MediaType contentType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() {
|
||||
return file.getSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeTo(BufferedSink sink) throws IOException {
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) != -1) {
|
||||
sink.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用 HTTP 方法。
|
||||
*
|
||||
* @param builder 请求构造器
|
||||
* @param method HTTP 方法
|
||||
* @param body 请求体
|
||||
*/
|
||||
private static void applyMethod(
|
||||
Request.Builder builder,
|
||||
String method,
|
||||
RequestBody body) {
|
||||
if ("GET".equals(method)) {
|
||||
builder.get();
|
||||
} else if ("HEAD".equals(method)) {
|
||||
builder.head();
|
||||
} else if ("DELETE".equals(method) && body == null) {
|
||||
builder.delete();
|
||||
} else {
|
||||
builder.method(method, body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按实际字节读取响应。
|
||||
*
|
||||
* @param body 响应体
|
||||
* @param maxBytes 最大字节数
|
||||
* @return UTF-8 响应
|
||||
* @throws IOException 读取失败或响应超限
|
||||
*/
|
||||
private static String readBounded(
|
||||
ResponseBody body, long maxBytes) throws IOException {
|
||||
if (maxBytes > 0L
|
||||
&& body.contentLength() > maxBytes) {
|
||||
throw new IOException(
|
||||
"插件响应超过字节上限: " + maxBytes);
|
||||
}
|
||||
try (InputStream inputStream = body.byteStream();
|
||||
ByteArrayOutputStream outputStream =
|
||||
new ByteArrayOutputStream(8 * 1024)) {
|
||||
byte[] buffer = new byte[64 * 1024];
|
||||
long total = 0L;
|
||||
int read;
|
||||
while ((read = inputStream.read(buffer)) != -1) {
|
||||
total += read;
|
||||
if (maxBytes > 0L && total > maxBytes) {
|
||||
throw new IOException(
|
||||
"插件响应超过字节上限: " + maxBytes);
|
||||
}
|
||||
outputStream.write(buffer, 0, read);
|
||||
}
|
||||
return outputStream.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归构造对象参数。
|
||||
*
|
||||
* @param param 参数
|
||||
* @return 参数值
|
||||
*/
|
||||
private static Object buildNestedParamValue(
|
||||
PluginParam param) {
|
||||
if (!"Object".equalsIgnoreCase(param.getType())) {
|
||||
return param.getDefaultValue();
|
||||
}
|
||||
|
||||
// 如果是 Object 类型,递归处理子参数
|
||||
Map<String, Object> nestedParams = new HashMap<>();
|
||||
Map<String, Object> nested = new HashMap<>();
|
||||
if (param.getChildren() != null) {
|
||||
param.getChildren().stream()
|
||||
.filter(PluginParam::isEnabled)
|
||||
.forEach(child -> {
|
||||
Object childValue = buildNestedParamValue(child); // 递归处理子参数
|
||||
nestedParams.put(child.getName(), childValue);
|
||||
});
|
||||
for (PluginParam child : param.getChildren()) {
|
||||
if (child.isEnabled()) {
|
||||
nested.put(
|
||||
child.getName(),
|
||||
buildNestedParamValue(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
return nestedParams;
|
||||
return nested;
|
||||
}
|
||||
|
||||
/**
|
||||
* 替换URL中的路径变量 {xxx}
|
||||
* 替换 URL 路径变量。
|
||||
*
|
||||
* @param url 原始 URL
|
||||
* @param params 插件参数
|
||||
* @return 替换后的 URL
|
||||
*/
|
||||
private static String replacePathVariables(String url, List<PluginParam> params) {
|
||||
String result = url;
|
||||
|
||||
// 收集路径参数
|
||||
Map<String, Object> pathParams = new HashMap<>();
|
||||
params.stream()
|
||||
.filter(p -> "path".equalsIgnoreCase(p.getMethod()) && p.isEnabled())
|
||||
.forEach(p -> pathParams.put(p.getName(), p.getDefaultValue()));
|
||||
|
||||
// 替换变量
|
||||
for (Map.Entry<String, Object> entry : pathParams.entrySet()) {
|
||||
result = result.replaceAll("\\{" + entry.getKey() + "\\}",
|
||||
entry.getValue().toString());
|
||||
private static String replacePathVariables(
|
||||
String url, List<PluginParam> params) {
|
||||
String result = Objects.requireNonNull(
|
||||
url, "插件 URL 不能为空");
|
||||
for (PluginParam param : params) {
|
||||
if (param.isEnabled()
|
||||
&& "path".equalsIgnoreCase(param.getMethod())
|
||||
&& param.getDefaultValue() != null) {
|
||||
result = result.replace(
|
||||
"{" + param.getName() + "}",
|
||||
String.valueOf(param.getDefaultValue()));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void processMultipartBody(HttpRequest request, Map<String, Object> bodyParams) {
|
||||
// 手动设置 Content-Type 为 multipart/form-data
|
||||
request.header(Header.CONTENT_TYPE, "multipart/form-data");
|
||||
for (Map.Entry<String, Object> entry : bodyParams.entrySet()) {
|
||||
String paramName = entry.getKey();
|
||||
Object paramValue = entry.getValue();
|
||||
|
||||
if (paramValue instanceof MultipartFile) {
|
||||
MultipartFile file = (MultipartFile) paramValue;
|
||||
try {
|
||||
request.form(paramName, file.getBytes(), file.getOriginalFilename());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("文件参数处理失败:参数名=%s,文件名=%s",
|
||||
paramName, file.getOriginalFilename()), e);
|
||||
}
|
||||
} else {
|
||||
// 处理普通参数
|
||||
String valueStr;
|
||||
if (paramValue instanceof String || paramValue instanceof Number || paramValue instanceof Boolean) {
|
||||
valueStr = paramValue.toString();
|
||||
} else {
|
||||
valueStr = JSONUtil.toJsonStr(paramValue);
|
||||
}
|
||||
request.form(paramName, valueStr);
|
||||
/**
|
||||
* 获取主机隔离许可并严格限制注册表体积。
|
||||
*
|
||||
* @param host 主机名
|
||||
* @return 主机许可
|
||||
*/
|
||||
private static Semaphore hostSemaphore(String host) {
|
||||
Semaphore existing = HOST_PERMITS.get(host);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
synchronized (HOST_REGISTRY_LOCK) {
|
||||
existing = HOST_PERMITS.get(host);
|
||||
if (existing != null) {
|
||||
return existing;
|
||||
}
|
||||
if (HOST_PERMITS.size() >= MAX_TRACKED_HOSTS) {
|
||||
return OVERFLOW_HOST_PERMITS;
|
||||
}
|
||||
Semaphore created = new Semaphore(8, true);
|
||||
HOST_PERMITS.put(host, created);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断方法是否幂等。
|
||||
*
|
||||
* @param method HTTP 方法
|
||||
* @return 是否允许连接级恢复
|
||||
*/
|
||||
private static boolean isIdempotent(String method) {
|
||||
return "GET".equals(method)
|
||||
|| "HEAD".equals(method)
|
||||
|| "OPTIONS".equals(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断方法是否支持请求体。
|
||||
*
|
||||
* @param method HTTP 方法
|
||||
* @return 是否支持请求体
|
||||
*/
|
||||
private static boolean supportsRequestBody(String method) {
|
||||
return "POST".equals(method)
|
||||
|| "PUT".equals(method)
|
||||
|| "PATCH".equals(method);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将普通或嵌套值转换为表单字符串。
|
||||
*
|
||||
* @param value 参数值
|
||||
* @return 表单值
|
||||
*/
|
||||
private static String stringify(Object value) {
|
||||
if (value == null) {
|
||||
return "";
|
||||
}
|
||||
if (value instanceof String
|
||||
|| value instanceof Number
|
||||
|| value instanceof Boolean) {
|
||||
return String.valueOf(value);
|
||||
}
|
||||
return JSONUtil.toJsonStr(value);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user