entry : parameterValueList.entrySet()) {
- LOGGER.debug("Param [{}]: {} = {}", i++, entry.getKey(),
- entry.getValue() != null ? entry.getValue().toString() : "null");
- }
- }
-
try {
// 反射调用
Object data = ReflectionUtil.invokeWithArguments(method, instance, parameterValueArray);
if (data != null) {
- if (data instanceof JsonResult) {
- doFireJsonResultResponse(ctx, (JsonResult>) data);
+ if (data instanceof JsonResult jsonResult) {
+ doFireJsonResultResponse(ctx, (JsonResult>) data, jsonResult.getCode());
}
if (data instanceof JsonObject) {
doFireJsonObjectResponse(ctx, ((JsonObject) data));
} else if (data instanceof Future) { // 处理异步响应
((Future>) data).onSuccess(res -> {
- if (res instanceof JsonResult) {
- doFireJsonResultResponse(ctx, (JsonResult>) res);
+ if (res instanceof JsonResult jsonResult) {
+ doFireJsonResultResponse(ctx, jsonResult, jsonResult.getCode());
}
if (res instanceof JsonObject) {
doFireJsonObjectResponse(ctx, ((JsonObject) res));
} else if (res != null) {
doFireJsonResultResponse(ctx, JsonResult.data(res));
} else {
- handleAfterInterceptor(ctx, null);
+ doFireJsonResultResponse(ctx, JsonResult.data(null));
}
- }).onFailure(e -> doFireJsonResultResponse(ctx, JsonResult.error(e.getMessage())));
+ }).onFailure(e -> doFireJsonResultResponse(ctx, JsonResult.error(e.getMessage()), 500));
} else {
doFireJsonResultResponse(ctx, JsonResult.data(data));
}
@@ -434,7 +423,7 @@ public class RouterHandlerFactory implements BaseHttpApi {
err = e.getCause().getMessage();
}
}
- doFireJsonResultResponse(ctx, JsonResult.error(err));
+ doFireJsonResultResponse(ctx, JsonResult.error(err), 500);
}
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/interceptor/BeforeInterceptor.java b/core/src/main/java/cn/qaiu/vx/core/interceptor/BeforeInterceptor.java
index 4714a9d..a1f156b 100644
--- a/core/src/main/java/cn/qaiu/vx/core/interceptor/BeforeInterceptor.java
+++ b/core/src/main/java/cn/qaiu/vx/core/interceptor/BeforeInterceptor.java
@@ -3,10 +3,12 @@ package cn.qaiu.vx.core.interceptor;
import io.vertx.core.Handler;
import io.vertx.ext.web.RoutingContext;
-import static cn.qaiu.vx.core.util.ResponseUtil.sendError;
-
/**
* 前置拦截器接口
+ *
+ * 注意:Vert.x是异步非阻塞框架,不能在Event Loop中使用synchronized等阻塞操作!
+ * 所有操作都应该是非阻塞的,使用Vert.x的上下文数据存储机制保证线程安全。
+ *
*
* @author QAIU
*/
@@ -14,28 +16,25 @@ public interface BeforeInterceptor extends Handler {
String IS_NEXT = "RoutingContextIsNext";
default Handler doHandle() {
-
return ctx -> {
- // 加同步锁
- synchronized (BeforeInterceptor.class) {
- ctx.put(IS_NEXT, false);
- BeforeInterceptor.this.handle(ctx);
- if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
- sendError(ctx, 403);
- }
- }
+ // 【优化】移除synchronized锁,Vert.x的RoutingContext本身就是线程安全的
+ // 每个请求都有独立的RoutingContext,不需要额外加锁
+ ctx.put(IS_NEXT, false);
+ handle(ctx); // 调用具体的处理逻辑
+ // 确保如果没有调用doNext()并且响应未结束,则返回错误
+ // if (!(Boolean) ctx.get(IS_NEXT) && !ctx.response().ended()) {
+ // sendError(ctx, 403);
+ // }
};
}
default void doNext(RoutingContext context) {
- // 设置上下文状态为可以继续执行
- // 添加同步锁保障多线程下执行时序
- synchronized (BeforeInterceptor.class) {
- context.put(IS_NEXT, true);
- context.next();
- }
+ // 【优化】移除synchronized锁
+ // RoutingContext的put和next操作是线程安全的,不需要额外同步
+ context.put(IS_NEXT, true);
+ context.next(); // 继续执行下一个处理器
}
- void handle(RoutingContext context);
-
+ void handle(RoutingContext context); // 实现具体的拦截处理逻辑
}
+
diff --git a/core/src/main/java/cn/qaiu/vx/core/model/JsonResult.java b/core/src/main/java/cn/qaiu/vx/core/model/JsonResult.java
index 3d4cda7..b4422de 100644
--- a/core/src/main/java/cn/qaiu/vx/core/model/JsonResult.java
+++ b/core/src/main/java/cn/qaiu/vx/core/model/JsonResult.java
@@ -30,7 +30,7 @@ public class JsonResult implements Serializable {
private int code = SUCCESS_CODE;//状态码
- private String msg = SUCCESS_MESSAGE; //消息
+ private String msg = SUCCESS_MESSAGE;//消息
private boolean success = true; //是否成功
diff --git a/core/src/main/java/cn/qaiu/vx/core/package-info.java b/core/src/main/java/cn/qaiu/vx/core/package-info.java
index 1a13278..640890c 100644
--- a/core/src/main/java/cn/qaiu/vx/core/package-info.java
+++ b/core/src/main/java/cn/qaiu/vx/core/package-info.java
@@ -1,7 +1,7 @@
/**
* ModuleGen cn.qaiu.vx.core
*/
-@ModuleGen(name = "vertx-http-proxy", groupPackage = "cn.qaiu.vx.core", useFutures = true)
+@ModuleGen(name = "vertx-http-proxy", groupPackage = "cn.qaiu.vx.core")
package cn.qaiu.vx.core;
import io.vertx.codegen.annotations.ModuleGen;
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/AsyncServiceUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/AsyncServiceUtil.java
index 5c893b2..42572d3 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/AsyncServiceUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/AsyncServiceUtil.java
@@ -5,7 +5,7 @@ import io.vertx.serviceproxy.ServiceProxyBuilder;
/**
* @author Xu Haidong
- * Create at 2018/8/15
+ * @date 2018/8/15
*/
public final class AsyncServiceUtil {
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java
index dd423e2..d9da4ff 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/CommonUtil.java
@@ -13,6 +13,7 @@ import java.net.Socket;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.List;
+import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
@@ -117,7 +118,7 @@ public class CommonUtil {
return set.stream().filter(c1 -> {
HandleSortFilter s1 = c1.getAnnotation(HandleSortFilter.class);
if (s1 != null) {
- return s1.value() > 0;
+ return s1.value() >= 0;
} else {
return true;
}
@@ -138,7 +139,7 @@ public class CommonUtil {
} catch (Exception e) {
throw new RuntimeException(e);
}
- }).collect(Collectors.toSet());
+ }).collect(Collectors.toCollection(LinkedHashSet::new));
}
private static String appVersion;
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java
index 43ebb66..1d5cb50 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/ConfigUtil.java
@@ -4,9 +4,13 @@ import io.vertx.config.ConfigRetriever;
import io.vertx.config.ConfigRetrieverOptions;
import io.vertx.config.ConfigStoreOptions;
import io.vertx.core.Future;
+import io.vertx.core.Promise;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
/**
* 异步读取配置工具类
*
Create date 2021/9/2 1:23
@@ -24,7 +28,29 @@ public class ConfigUtil {
* @return JsonObject的Future
*/
public static Future readConfig(String format, String path, Vertx vertx) {
- // 读取yml配置
+ // 支持 classpath: 前缀从类路径读取,否则从文件系统读取
+ if (path != null && path.startsWith("classpath:")) {
+ String resource = path.substring("classpath:".length());
+ // 使用 executeBlocking(Callable) 直接返回 Future
+ return vertx.executeBlocking(() -> {
+ InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
+ if (is == null) {
+ throw new RuntimeException("classpath resource not found: " + resource);
+ }
+ try (InputStream in = is) {
+ byte[] bytes = in.readAllBytes();
+ String content = new String(bytes, StandardCharsets.UTF_8);
+ if ("json".equalsIgnoreCase(format)) {
+ return new JsonObject(content);
+ } else {
+ throw new RuntimeException("unsupported classpath format: " + format);
+ }
+ }
+ });
+ }
+
+ Promise promise = Promise.promise();
+
ConfigStoreOptions store = new ConfigStoreOptions()
.setType("file")
.setFormat(format)
@@ -33,10 +59,22 @@ public class ConfigUtil {
ConfigRetriever retriever = ConfigRetriever
.create(vertx, new ConfigRetrieverOptions().addStore(store));
- return retriever.getConfig();
+ // 异步获取配置
+ // 成功直接完成 promise
+ retriever.getConfig()
+ .onSuccess(promise::complete)
+ .onFailure(err -> {
+ // 配置读取失败,直接返回失败 Future
+ promise.fail(new RuntimeException(
+ "读取配置文件失败: " + path, err));
+ retriever.close();
+ });
+
+ return promise.future();
}
+
/**
* 异步读取Yaml配置文件
*
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/FutureUtils.java b/core/src/main/java/cn/qaiu/vx/core/util/FutureUtils.java
new file mode 100644
index 0000000..f008994
--- /dev/null
+++ b/core/src/main/java/cn/qaiu/vx/core/util/FutureUtils.java
@@ -0,0 +1,20 @@
+package cn.qaiu.vx.core.util;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+
+import java.util.concurrent.ExecutionException;
+
+public class FutureUtils {
+
+ public static T getResult(Future future) {
+ try {
+ return future.toCompletionStage().toCompletableFuture().get();
+ } catch (InterruptedException | ExecutionException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ public static T getResult(Promise promise) {
+ return promise.future().toCompletionStage().toCompletableFuture().join();
+ }
+}
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/JacksonConfig.java b/core/src/main/java/cn/qaiu/vx/core/util/JacksonConfig.java
index d164269..b9508d3 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/JacksonConfig.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/JacksonConfig.java
@@ -16,7 +16,7 @@ import java.time.format.DateTimeFormatter;
/**
* @author QAIU
- * Create at 2023/10/14 9:07
+ * @date 2023/10/14 9:07
*/
public class JacksonConfig {
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
index 65bdd6c..89a48ba 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/ReflectionUtil.java
@@ -36,6 +36,8 @@ import static cn.qaiu.vx.core.util.ConfigConstant.BASE_LOCATIONS;
*/
public final class ReflectionUtil {
+ // 缓存Reflections实例,避免重复扫描(每次扫描约35K+值,耗时1-3秒,占用大量内存)
+ private static final Map REFLECTIONS_CACHE = new java.util.concurrent.ConcurrentHashMap<>();
/**
* 以默认配置的基础包路径获取反射器
@@ -47,52 +49,48 @@ public final class ReflectionUtil {
}
/**
- * 获取反射器
+ * 获取反射器(带缓存)
*
* @param packageAddress Package address String
* @return Reflections object
*/
public static Reflections getReflections(String packageAddress) {
- List packageAddressList;
- if (packageAddress.contains(",")) {
- packageAddressList = Arrays.asList(packageAddress.split(","));
- } else if (packageAddress.contains(";")) {
- packageAddressList = Arrays.asList(packageAddress.split(";"));
- } else {
- packageAddressList = Collections.singletonList(packageAddress);
- }
-
- return getReflections(packageAddressList);
+ return REFLECTIONS_CACHE.computeIfAbsent(packageAddress, key -> {
+ List packageAddressList;
+ if (key.contains(",")) {
+ packageAddressList = Arrays.asList(key.split(","));
+ } else if (key.contains(";")) {
+ packageAddressList = Arrays.asList(key.split(";"));
+ } else {
+ packageAddressList = Collections.singletonList(key);
+ }
+ return createReflections(packageAddressList);
+ });
}
/**
- * 获取反射器
+ * 获取反射器(带缓存)
*
* @param packageAddresses Package address List
* @return Reflections object
*/
public static Reflections getReflections(List packageAddresses) {
- ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
- FilterBuilder filterBuilder = new FilterBuilder();
- packageAddresses.forEach(str -> {
- Collection urls = ClasspathHelper.forPackage(str.trim());
- configurationBuilder.addUrls(urls);
- filterBuilder.includePackage(str.trim());
- });
+ String cacheKey = String.join(",", packageAddresses);
+ return REFLECTIONS_CACHE.computeIfAbsent(cacheKey, key -> createReflections(packageAddresses));
+ }
- // 采坑记录 2021-05-08
- // 发现注解api层 没有继承父类时 这里反射一直有问题(Scanner SubTypesScanner was not configured)
- // 因此这里需要手动配置各种Scanner扫描器 -- https://blog.csdn.net/qq_29499107/article/details/106889781
- configurationBuilder.setScanners(
- Scanners.SubTypes.filterResultsBy(s -> true), //允许getAllTypes获取所有Object的子类, 不设置为false则 getAllTypes
- // 会报错.默认为true.
- new MethodParameterNamesScanner(), //设置方法参数名称 扫描器,否则调用getConstructorParamNames 会报错
- Scanners.MethodsAnnotated, //设置方法注解 扫描器, 否则getConstructorsAnnotatedWith,getMethodsAnnotatedWith 会报错
- new MemberUsageScanner(), //设置 member 扫描器,否则 getMethodUsage 会报错
- Scanners.TypesAnnotated //设置类注解 扫描器 ,否则 getTypesAnnotatedWith 会报错
- );
-
- configurationBuilder.filterInputsBy(filterBuilder);
+ private static Reflections createReflections(List packageAddresses) {
+ ConfigurationBuilder configurationBuilder = new ConfigurationBuilder()
+ .addClassLoaders(Thread.currentThread().getContextClassLoader())
+ .forPackages(packageAddresses.toArray(new String[0]))
+ .setScanners(
+ Scanners.SubTypes.filterResultsBy(s -> true), //允许getAllTypes获取所有Object的子类, 不设置为false则 getAllTypes
+ // 会报错.默认为true.
+ new MethodParameterNamesScanner(), //设置方法参数名称 扫描器,否则调用getConstructorParamNames 会报错
+ Scanners.MethodsAnnotated, //设置方法注解 扫描器, 否则getConstructorsAnnotatedWith,getMethodsAnnotatedWith 会报错
+ new MemberUsageScanner(), //设置 member 扫描器,否则 getMethodUsage 会报错
+ Scanners.TypesAnnotated //设置类注解 扫描器 ,否则 getTypesAnnotatedWith 会报错
+ );
return new Reflections(configurationBuilder);
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/util/ResponseUtil.java b/core/src/main/java/cn/qaiu/vx/core/util/ResponseUtil.java
index fad0314..e6f259a 100644
--- a/core/src/main/java/cn/qaiu/vx/core/util/ResponseUtil.java
+++ b/core/src/main/java/cn/qaiu/vx/core/util/ResponseUtil.java
@@ -13,6 +13,7 @@ public class ResponseUtil {
public static void redirect(HttpServerResponse response, String url) {
response.putHeader(CONTENT_TYPE, "text/html; charset=utf-8")
+ .putHeader("Referrer-Policy", "no-referrer")
.putHeader(HttpHeaders.LOCATION, url).setStatusCode(302).end();
}
@@ -22,14 +23,22 @@ public class ResponseUtil {
}
public static void fireJsonObjectResponse(RoutingContext ctx, JsonObject jsonObject) {
- ctx.response().putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
- .setStatusCode(200)
- .end(jsonObject.encode());
+ fireJsonObjectResponse(ctx, jsonObject, 200);
}
public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject) {
+ fireJsonObjectResponse(ctx, jsonObject, 200);
+ }
+
+ public static void fireJsonObjectResponse(RoutingContext ctx, JsonObject jsonObject, int statusCode) {
+ ctx.response().putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
+ .setStatusCode(statusCode)
+ .end(jsonObject.encode());
+ }
+
+ public static void fireJsonObjectResponse(HttpServerResponse ctx, JsonObject jsonObject, int statusCode) {
ctx.putHeader(CONTENT_TYPE, "application/json; charset=utf-8")
- .setStatusCode(200)
+ .setStatusCode(statusCode)
.end(jsonObject.encode());
}
@@ -37,6 +46,10 @@ public class ResponseUtil {
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
}
+ public static void fireJsonResultResponse(RoutingContext ctx, JsonResult jsonResult, int statusCode) {
+ fireJsonObjectResponse(ctx, jsonResult.toJsonObject(), statusCode);
+ }
+
public static void fireJsonResultResponse(HttpServerResponse ctx, JsonResult jsonResult) {
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
index 3c4b4f3..64fd990 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/HttpProxyVerticle.java
@@ -1,50 +1,77 @@
package cn.qaiu.vx.core.verticle;
import io.vertx.core.AbstractVerticle;
-import io.vertx.core.Vertx;
-import io.vertx.core.VertxOptions;
-import io.vertx.core.dns.AddressResolverOptions;
import io.vertx.core.http.*;
+import io.vertx.core.json.JsonObject;
import io.vertx.core.net.NetClient;
import io.vertx.core.net.NetClientOptions;
-import io.vertx.core.net.NetSocket;
import io.vertx.core.net.ProxyOptions;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.net.URI;
import java.util.Base64;
+import static cn.qaiu.vx.core.util.ConfigConstant.GLOBAL_CONFIG;
+import static cn.qaiu.vx.core.util.ConfigConstant.LOCAL;
+
/**
*
*/
public class HttpProxyVerticle extends AbstractVerticle {
+ private static final Logger LOGGER = LoggerFactory.getLogger(HttpProxyVerticle.class);
private HttpClient httpClient;
private NetClient netClient;
+ private JsonObject proxyPreConf;
+ private JsonObject proxyServerConf;
+
+
@Override
public void start() {
- ProxyOptions proxyOptions = new ProxyOptions().setHost("127.0.0.1").setPort(7890);
+ proxyServerConf = ((JsonObject)vertx.sharedData().getLocalMap(LOCAL).get(GLOBAL_CONFIG)).getJsonObject("proxy-server");
+ proxyPreConf = ((JsonObject)vertx.sharedData().getLocalMap(LOCAL).get(GLOBAL_CONFIG)).getJsonObject("proxy-pre");
+ Integer serverPort = proxyServerConf.getInteger("port");
+
+ ProxyOptions proxyOptions = null;
+ if (proxyPreConf != null && StringUtils.isNotBlank(proxyPreConf.getString("ip"))) {
+ proxyOptions = new ProxyOptions(proxyPreConf);
+ }
+
// 初始化 HTTP 客户端,用于向目标服务器发送 HTTP 请求
HttpClientOptions httpClientOptions = new HttpClientOptions();
- httpClient = vertx.createHttpClient(httpClientOptions.setProxyOptions(proxyOptions));
+ if (proxyOptions != null) {
+ httpClientOptions.setProxyOptions(proxyOptions);
+ }
+ httpClient = vertx.createHttpClient(httpClientOptions);
// 创建并启动 HTTP 代理服务器,监听指定端口
- HttpServer server = vertx.createHttpServer(new HttpServerOptions().setClientAuth(ClientAuth.REQUIRED));
+ HttpServerOptions httpServerOptions = new HttpServerOptions();
+ if (proxyServerConf.containsKey("username") &&
+ StringUtils.isNotBlank(proxyServerConf.getString("username"))) {
+ httpServerOptions.setClientAuth(ClientAuth.REQUIRED);
+ }
+
+ HttpServer server = vertx.createHttpServer();
server.requestHandler(this::handleClientRequest);
// 初始化 NetClient,用于在 CONNECT 请求中建立 TCP 连接隧道
- netClient = vertx.createNetClient(new NetClientOptions()
- .setProxyOptions(proxyOptions)
+ NetClientOptions netClientOptions = new NetClientOptions();
+
+ if (proxyOptions != null) {
+ httpClientOptions.setProxyOptions(proxyOptions);
+ }
+
+ netClient = vertx.createNetClient(netClientOptions
.setConnectTimeout(15000)
.setTrustAll(true));
// 启动 HTTP 代理服务器
- server.listen(7891, ar -> {
- if (ar.succeeded()) {
- System.out.println("HTTP Proxy server started on port 7891");
- } else {
- System.err.println("Failed to start HTTP Proxy server: " + ar.cause());
- }
- });
+ server.listen(serverPort)
+ .onSuccess(res-> LOGGER.info("HTTP Proxy server started on port {}", serverPort))
+ .onFailure(err-> LOGGER.error("Failed to start HTTP Proxy server: " + err.getMessage()));
}
// 处理 HTTP CONNECT 请求,用于代理 HTTPS 流量
@@ -66,49 +93,54 @@ public class HttpProxyVerticle extends AbstractVerticle {
}
clientRequest.pause();
// 通过 NetClient 连接目标服务器并创建隧道
- netClient.connect(targetPort, targetHost, connectionAttempt -> {
- if (connectionAttempt.succeeded()) {
- NetSocket targetSocket = connectionAttempt.result();
+ netClient.connect(targetPort, targetHost)
+ .onSuccess(targetSocket -> {
+ // Upgrade client connection to NetSocket and implement bidirectional data flow
+ clientRequest.toNetSocket()
+ .onSuccess(clientSocket -> {
+ // Set up bidirectional data forwarding
+ clientSocket.handler(targetSocket::write);
+ targetSocket.handler(clientSocket::write);
- // 升级客户端连接到 NetSocket 并实现双向数据流
- clientRequest.toNetSocket().onComplete(clientSocketAttempt -> {
- if (clientSocketAttempt.succeeded()) {
- NetSocket clientSocket = clientSocketAttempt.result();
-
- // 设置双向数据流转发
- clientSocket.handler(targetSocket::write);
- targetSocket.handler(clientSocket::write);
-
- // 关闭其中一方时关闭另一方
- clientSocket.closeHandler(v -> targetSocket.close());
- targetSocket.closeHandler(v -> clientSocket.close());
- } else {
- System.err.println("Failed to upgrade client connection to socket: " + clientSocketAttempt.cause().getMessage());
- targetSocket.close();
- clientRequest.response().setStatusCode(500).end("Internal Server Error");
- }
+ // Close the other socket when one side closes
+ clientSocket.closeHandler(v -> targetSocket.close());
+ targetSocket.closeHandler(v -> clientSocket.close());
+ })
+ .onFailure(clientSocketAttempt -> {
+ System.err.println("Failed to upgrade client connection to socket: " + clientSocketAttempt.getMessage());
+ targetSocket.close();
+ clientRequest.response().setStatusCode(500).end("Internal Server Error");
+ });
+ })
+ .onFailure(connectionAttempt -> {
+ System.err.println("Failed to connect to target: " + connectionAttempt.getMessage());
+ clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to connect to target");
});
- } else {
- System.err.println("Failed to connect to target: " + connectionAttempt.cause().getMessage());
- clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to connect to target");
- }
- });
}
// 处理客户端的 HTTP 请求
private void handleClientRequest(HttpServerRequest clientRequest) {
- String s = clientRequest.headers().get("Proxy-Authorization");
- if (s == null) {
- clientRequest.response().setStatusCode(403).end();
- return;
+ // 打印来源ip和访问目标URI
+ LOGGER.debug("source: {}, target: {}", clientRequest.remoteAddress().toString(), clientRequest.uri());
+ if (proxyServerConf.containsKey("username") &&
+ StringUtils.isNotBlank(proxyServerConf.getString("username"))) {
+ String s = clientRequest.headers().get("Proxy-Authorization");
+ if (s == null) {
+ clientRequest.response().setStatusCode(403).end();
+ return;
+ }
+ String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
+ if (split.length > 1) {
+ // TODO
+ String username = proxyServerConf.getString("username");
+ String password = proxyServerConf.getString("password");
+ if (!split[0].equals(username) || !split[1].equals(password)) {
+ LOGGER.info("-----auth failed------\nusername: {}\npassword: {}", username, password);
+ clientRequest.response().setStatusCode(403).end();
+ return;
+ }
+ }
}
- String[] split = new String(Base64.getDecoder().decode(s.replace("Basic ", ""))).split(":");
- if (split.length > 1) {
- System.out.println(split[0]);
- System.out.println(split[1]);
- // TODO
- }
-
if (clientRequest.method() == HttpMethod.CONNECT) {
// 处理 CONNECT 请求
@@ -129,7 +161,7 @@ public class HttpProxyVerticle extends AbstractVerticle {
}
String targetHost = hostHeader.split(":")[0];
- int targetPort = 80; // 默认为 HTTP 的端口
+ int targetPort = extractPortFromUrl(clientRequest.uri()); // 默认为 HTTP 的端口
clientRequest.pause(); // 暂停客户端请求的读取,避免数据丢失
httpClient.request(clientRequest.method(), targetPort, targetHost, clientRequest.uri())
@@ -140,16 +172,19 @@ public class HttpProxyVerticle extends AbstractVerticle {
clientRequest.headers().forEach(header -> request.putHeader(header.getKey(), header.getValue()));
// 将客户端请求的 body 转发给目标服务器
- clientRequest.bodyHandler(body -> request.send(body, ar -> {
- if (ar.succeeded()) {
- var response = ar.result();
- clientRequest.response().setStatusCode(response.statusCode());
- clientRequest.response().headers().setAll(response.headers());
- response.body().onSuccess(b-> clientRequest.response().end(b));
- } else {
- clientRequest.response().setStatusCode(502).end("Bad Gateway: Unable to reach target");
- }
- }));
+ clientRequest.bodyHandler(body ->
+ request.send(body)
+ .onSuccess(response -> {
+ clientRequest.response().setStatusCode(response.statusCode());
+ clientRequest.response().headers().setAll(response.headers());
+ response.body()
+ .onSuccess(b -> clientRequest.response().end(b))
+ .onFailure(err -> clientRequest.response()
+ .setStatusCode(502).end("Bad Gateway: Unable to reach target"));
+ })
+ .onFailure(err -> clientRequest.response()
+ .setStatusCode(502).end("Bad Gateway: Unable to reach target"))
+ );
})
.onFailure(err -> {
err.printStackTrace();
@@ -157,28 +192,43 @@ public class HttpProxyVerticle extends AbstractVerticle {
});
}
+
+ /**
+ * 从 URL 中提取端口号
+ *
+ * @param urlString URL 字符串
+ * @return 提取的端口号,如果没有指定端口,则返回默认端口
+ */
+ public static int extractPortFromUrl(String urlString) {
+ try {
+ URI uri = new URI(urlString);
+ int port = uri.getPort();
+ // 如果 URL 没有指定端口,使用默认端口
+ if (port == -1) {
+ if ("https".equalsIgnoreCase(uri.getScheme())) {
+ port = 443; // HTTPS 默认端口
+ } else {
+ port = 80; // HTTP 默认端口
+ }
+ }
+ return port;
+ } catch (Exception e) {
+ e.printStackTrace();
+ // 出现异常时返回 -1,表示提取失败
+ return -1;
+ }
+ }
+
+
@Override
public void stop() {
// 停止 HTTP 客户端以释放资源
if (httpClient != null) {
httpClient.close();
}
+ if (netClient != null) {
+ netClient.close();
+ }
}
- /**
- * TODO add Deploy
- * @param args
- */
- public static void main(String[] args) {
- // 配置 DNS 解析器,使用多个 DNS 服务器来提升解析速度
- Vertx vertx = Vertx.vertx(new VertxOptions()
- .setAddressResolverOptions(new AddressResolverOptions()
- .addServer("114.114.114.114")
- .addServer("114.114.115.115")
- .addServer("8.8.8.8")
- .addServer("8.8.4.4")));
-
- // 部署 Verticle 并启动动态 HTTP 代理服务器
- vertx.deployVerticle(new HttpProxyVerticle());
- }
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java
new file mode 100644
index 0000000..8cfa6d4
--- /dev/null
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/PostExecVerticle.java
@@ -0,0 +1,68 @@
+package cn.qaiu.vx.core.verticle;
+
+import cn.qaiu.vx.core.base.AppRun;
+import cn.qaiu.vx.core.base.DefaultAppRun;
+import cn.qaiu.vx.core.util.CommonUtil;
+import cn.qaiu.vx.core.util.ReflectionUtil;
+import cn.qaiu.vx.core.util.SharedDataUtil;
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.core.json.JsonObject;
+import org.reflections.Reflections;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 后置执行Verticle - 在core启动后立即执行AppRun实现
+ *
Create date 2024-01-01 00:00:00
+ *
+ * @author QAIU
+ */
+public class PostExecVerticle extends AbstractVerticle {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(PostExecVerticle.class);
+ private static final Set appRunImplementations;
+ private static final AtomicBoolean lock = new AtomicBoolean(false);
+
+ static {
+ Reflections reflections = ReflectionUtil.getReflections();
+ Set> subTypesOf = reflections.getSubTypesOf(AppRun.class);
+ subTypesOf.add(DefaultAppRun.class);
+ appRunImplementations = CommonUtil.sortClassSet(subTypesOf);
+ if (appRunImplementations.isEmpty()) {
+ LOGGER.warn("未找到 AppRun 接口的实现类");
+ } else {
+ LOGGER.info("找到 {} 个 AppRun 接口的实现类", appRunImplementations.size());
+ }
+ }
+
+ @Override
+ public void start(Promise startPromise) {
+ if (!lock.compareAndSet(false, true)) {
+ return;
+ }
+ LOGGER.info("PostExecVerticle 开始执行...");
+
+ if (appRunImplementations != null && !appRunImplementations.isEmpty()) {
+ appRunImplementations.forEach(appRun -> {
+ try {
+ LOGGER.info("执行 AppRun 实现: {}", appRun.getClass().getName());
+ JsonObject globalConfig = SharedDataUtil.getJsonConfig("globalConfig");
+ appRun.execute(globalConfig);
+ LOGGER.info("AppRun 实现 {} 执行完成", appRun.getClass().getName());
+ } catch (Exception e) {
+ LOGGER.error("执行 AppRun 实现 {} 时发生错误",appRun.getClass().getName(), e);
+ }
+ });
+ } else {
+ LOGGER.info("未找到 AppRun 接口的实现类");
+ }
+
+ LOGGER.info("PostExecVerticle 执行完成");
+ startPromise.complete();
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java
index a2e7b6d..977c4d7 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ReverseProxyVerticle.java
@@ -5,8 +5,10 @@ import io.vertx.core.AbstractVerticle;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.core.http.HttpClient;
+import io.vertx.core.http.HttpClientOptions;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
+import io.vertx.core.http.HttpServerRequest;
import io.vertx.core.json.JsonArray;
import io.vertx.core.json.JsonObject;
import io.vertx.core.net.PemKeyCertOptions;
@@ -15,6 +17,9 @@ import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.StaticHandler;
import io.vertx.ext.web.proxy.handler.ProxyHandler;
import io.vertx.httpproxy.HttpProxy;
+import io.vertx.httpproxy.ProxyContext;
+import io.vertx.httpproxy.ProxyInterceptor;
+import io.vertx.httpproxy.ProxyResponse;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,13 +27,16 @@ import org.slf4j.LoggerFactory;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
-import java.nio.file.Path;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
/**
* 反向代理服务
* 可以根据配置文件自动生成代理服务
* 可以配置多个服务, 配置文件见示例
+ * 【优化】支持高并发场景,连接池复用,避免线程阻塞
*
Create date 2021/9/2 0:41
*
* @author QAIU
@@ -47,14 +55,83 @@ public class ReverseProxyVerticle extends AbstractVerticle {
public static String REROUTE_PATH_PREFIX = "/__rrvpspp"; //re_route_vert_proxy_server_path_prefix 硬编码
+ /**
+ * 【优化】HttpClient连接池,按host:port缓存复用,避免每个请求都创建新连接
+ */
+ private final Map httpClientPool = new ConcurrentHashMap<>();
+
+ /**
+ * 【优化】高并发场景下的HttpClient配置
+ */
+ private static final int MAX_POOL_SIZE = 100; // 最大连接池大小
+ private static final int MAX_WAIT_QUEUE_SIZE = 500; // 最大等待队列大小
+ private static final int CONNECT_TIMEOUT = 30000; // 连接超时30秒
+ private static final int IDLE_TIMEOUT = 60; // 空闲超时60秒
+ private static final boolean KEEP_ALIVE = true; // 启用Keep-Alive
+ private static final boolean PIPELINING = true; // 启用HTTP管线化
+
@Override
public void start(Promise startPromise) {
- CONFIG.onSuccess(this::handleProxyConfList);
+ CONFIG.onSuccess(this::handleProxyConfList).onFailure(e -> {
+ LOGGER.info("web代理配置已禁用,当前仅支持API调用");
+ });
// createFileListener
startPromise.complete();
}
+ /**
+ * 【优化】Verticle停止时清理HttpClient连接池
+ */
+ @Override
+ public void stop(Promise stopPromise) {
+ LOGGER.info("Stopping ReverseProxyVerticle, closing {} HttpClient connections...", httpClientPool.size());
+ httpClientPool.values().forEach(client -> {
+ try {
+ client.close();
+ } catch (Exception e) {
+ LOGGER.warn("Error closing HttpClient: {}", e.getMessage());
+ }
+ });
+ httpClientPool.clear();
+ stopPromise.complete();
+ }
+
+ /**
+ * 【优化】获取或创建HttpClient,实现连接池复用
+ * @param host 目标主机
+ * @param port 目标端口
+ * @return HttpClient实例
+ */
+ private HttpClient getOrCreateHttpClient(String host, int port) {
+ String key = host + ":" + port;
+ return httpClientPool.computeIfAbsent(key, k -> {
+ LOGGER.info("Creating new HttpClient for {}", key);
+ HttpClientOptions options = new HttpClientOptions()
+ .setMaxPoolSize(MAX_POOL_SIZE) // 连接池大小
+ .setMaxWaitQueueSize(MAX_WAIT_QUEUE_SIZE) // 等待队列大小
+ .setConnectTimeout(CONNECT_TIMEOUT) // 连接超时
+ .setIdleTimeout(IDLE_TIMEOUT) // 空闲超时
+ .setKeepAlive(KEEP_ALIVE) // Keep-Alive
+ .setKeepAliveTimeout(120) // Keep-Alive超时120秒
+ .setPipelining(PIPELINING) // HTTP管线化
+ .setPipeliningLimit(10) // 管线化限制
+ .setDecompressionSupported(true) // 支持解压响应
+ .setTcpKeepAlive(true) // TCP Keep-Alive
+ .setTcpNoDelay(true) // 禁用Nagle算法,降低延迟
+ .setTcpFastOpen(true) // 启用TCP Fast Open
+ .setTcpQuickAck(true) // 启用TCP Quick ACK
+ .setReuseAddress(true) // 允许地址重用
+ .setReusePort(true); // 允许端口重用
+ return vertx.createHttpClient(options);
+ });
+ }
+
+ /**
+ * 全局可信上游代理 IP 集合(如 nginx),仅这些 IP 的 X-Forwarded-For 会被信任
+ */
+ private Set globalTrustedProxies = new HashSet<>();
+
/**
* 获取主配置文件
*
@@ -62,6 +139,15 @@ public class ReverseProxyVerticle extends AbstractVerticle {
*/
private void handleProxyConfList(JsonObject config) {
serverName = config.getString("server-name");
+ // 解析全局 trusted-proxies
+ JsonArray trustedArr = config.getJsonArray("trusted-proxies");
+ if (trustedArr != null) {
+ trustedArr.forEach(ip -> {
+ if (ip instanceof String) {
+ globalTrustedProxies.add(((String) ip).trim());
+ }
+ });
+ }
JsonArray proxyConfList = config.getJsonArray("proxy");
if (proxyConfList != null) {
proxyConfList.forEach(proxyConf -> {
@@ -72,32 +158,89 @@ public class ReverseProxyVerticle extends AbstractVerticle {
}
}
+ /**
+ * 解析真实客户端 IP。
+ * 若直连来源在可信代理列表中,优先取 X-Real-IP,其次取 X-Forwarded-For 第一个值;
+ * 否则直接使用直连对端地址。
+ */
+ private String resolveClientIp(HttpServerRequest request) {
+ String peerIp = request.remoteAddress().host();
+ if (globalTrustedProxies.contains(peerIp)) {
+ String realIp = request.getHeader("X-Real-IP");
+ if (StringUtils.isNotBlank(realIp)) {
+ return realIp.trim();
+ }
+ String xff = request.getHeader("X-Forwarded-For");
+ if (StringUtils.isNotBlank(xff)) {
+ return xff.split(",")[0].trim();
+ }
+ }
+ return peerIp;
+ }
+
+ /**
+ * 解析 proxy-set-headers 中的 nginx 风格变量。
+ * 支持:$remote_addr、$proxy_add_x_forwarded_for、$scheme、$host;
+ * 其他值作为字面量直接使用。
+ */
+ private String resolveHeaderVariable(String tpl, HttpServerRequest req, String clientIp) {
+ return switch (tpl) {
+ case "$remote_addr" -> clientIp;
+ case "$proxy_add_x_forwarded_for" -> {
+ String existing = req.getHeader("X-Forwarded-For");
+ yield StringUtils.isNotBlank(existing) ? existing + ", " + clientIp : clientIp;
+ }
+ case "$scheme" -> req.isSSL() ? "https" : "http";
+ case "$host" -> req.getHeader("Host");
+ default -> tpl;
+ };
+ }
+
/**
* 处理单个反向代理配置
*
* @param proxyConf 代理配置
*/
private void handleProxyConf(JsonObject proxyConf) {
- // page404 path: 兼容不同启动目录(根目录或子模块目录)
- String configured404 = proxyConf.getString("page404");
- String resolved404 = resolveExistingPath(configured404, false);
- if (resolved404 == null) {
- resolved404 = resolveExistingPath(DEFAULT_PATH_404, false);
- }
- proxyConf.put("page404", resolved404 == null ? DEFAULT_PATH_404 : resolved404);
+ // page404 path
+ if (proxyConf.containsKey(
+
+ "page404")) {
+ System.getProperty("user.dir");
+ String path = proxyConf.getString("page404");
+ if (StringUtils.isEmpty(path)) {
+ proxyConf.put("page404", DEFAULT_PATH_404);
+ } else {
+ if (!path.startsWith("/")) {
+ path = "/" + path;
+ }
+ if (!new File(System.getProperty("user.dir") + path).exists()) {
+ proxyConf.put("page404", DEFAULT_PATH_404);
+ }
+ }
+ } else {
+ proxyConf.put("page404", DEFAULT_PATH_404);
+ }
- final HttpClient httpClient = VertxHolder.getVertxInstance().createHttpClient();
Router proxyRouter = Router.router(vertx);
// Add Server name header
proxyRouter.route().handler(ctx -> {
+ String realPath = ctx.request().uri();
+ if (realPath.startsWith(REROUTE_PATH_PREFIX)) {
+ // vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
+ String rePath = realPath.replace(REROUTE_PATH_PREFIX, "");
+ ctx.reroute(rePath);
+ return;
+ }
+
ctx.response().putHeader("Server", serverName);
ctx.next();
});
// http api proxy
if (proxyConf.containsKey("location")) {
- handleLocation(proxyConf.getJsonArray("location"), httpClient, proxyRouter);
+ handleLocation(proxyConf.getJsonArray("location"), proxyRouter);
}
// static server
@@ -106,7 +249,9 @@ public class ReverseProxyVerticle extends AbstractVerticle {
}
// Send page404 page
- proxyRouter.errorHandler(404, ctx -> ctx.response().sendFile(proxyConf.getString("page404")));
+ proxyRouter.errorHandler(404, ctx -> {
+ ctx.response().sendFile(proxyConf.getString("page404"));
+ });
HttpServer server = getHttpsServer(proxyConf);
server.requestHandler(proxyRouter);
@@ -118,8 +263,16 @@ public class ReverseProxyVerticle extends AbstractVerticle {
private HttpServer getHttpsServer(JsonObject proxyConf) {
HttpServerOptions httpServerOptions = new HttpServerOptions()
- .setCompressionSupported(true);
-
+ // 【优化】高并发服务器配置
+ .setTcpKeepAlive(true) // TCP Keep-Alive
+ .setTcpNoDelay(true) // 禁用Nagle算法
+ .setCompressionSupported(true) // 启用压缩
+ .setAcceptBacklog(50000) // 增加积压队列到50000
+ .setIdleTimeout(120) // 空闲超时120秒
+ .setTcpFastOpen(true) // 启用TCP Fast Open
+ .setTcpQuickAck(true) // 启用TCP Quick ACK
+ .setReuseAddress(true) // 允许地址重用
+ .setReusePort(true); // 允许端口重用
if (proxyConf.containsKey("ssl")) {
JsonObject sslConfig = proxyConf.getJsonObject("ssl");
@@ -169,18 +322,10 @@ public class ReverseProxyVerticle extends AbstractVerticle {
StaticHandler staticHandler;
if (staticConf.containsKey("root")) {
- String configuredRoot = staticConf.getString("root");
- String resolvedRoot = resolveStaticRoot(configuredRoot);
- if (resolvedRoot != null) {
- staticHandler = StaticHandler.create(resolvedRoot);
- } else {
- LOGGER.warn("static root not found, fallback to configured path: {}", configuredRoot);
- staticHandler = StaticHandler.create(configuredRoot);
- }
+ staticHandler = StaticHandler.create(staticConf.getString("root"));
} else {
staticHandler = StaticHandler.create();
}
-
if (staticConf.containsKey("directory-listing")) {
staticHandler.setDirectoryListing(staticConf.getBoolean("directory-listing"));
} else if (staticConf.containsKey("index")) {
@@ -193,10 +338,9 @@ public class ReverseProxyVerticle extends AbstractVerticle {
* 处理Location配置 代理请求Location(和nginx类似?)
*
* @param locationsConf location配置
- * @param httpClient 客户端
* @param proxyRouter 代理路由
*/
- private void handleLocation(JsonArray locationsConf, HttpClient httpClient, Router proxyRouter) {
+ private void handleLocation(JsonArray locationsConf, Router proxyRouter) {
locationsConf.stream().map(e -> (JsonObject) e).forEach(location -> {
// 代理规则
@@ -212,9 +356,33 @@ public class ReverseProxyVerticle extends AbstractVerticle {
String originPath = url.getPath();
LOGGER.info("path {}, originPath {}, to {}:{}", path, originPath, host, port);
- // 注意这里不能origin多个代理地址, 一个实例只能代理一个origin
+ // 【优化】使用连接池获取HttpClient,避免每个location都创建新连接
+ final HttpClient httpClient = getOrCreateHttpClient(host, port);
final HttpProxy httpProxy = HttpProxy.reverseProxy(httpClient);
httpProxy.origin(port, host);
+
+ // proxy-set-headers 支持(nginx 风格变量替换)
+ if (location.containsKey("proxy-set-headers")) {
+ final JsonObject headerConf = location.getJsonObject("proxy-set-headers");
+ httpProxy.addInterceptor(new ProxyInterceptor() {
+ @Override
+ public Future handleProxyRequest(ProxyContext ctx) {
+ HttpServerRequest incoming = ctx.request().proxiedRequest();
+ String clientIp = resolveClientIp(incoming);
+ headerConf.forEach(entry -> {
+ Object val = entry.getValue();
+ if (val != null) {
+ String resolved = resolveHeaderVariable(val.toString(), incoming, clientIp);
+ if (resolved != null) {
+ ctx.request().putHeader(entry.getKey(), resolved);
+ }
+ }
+ });
+ return ProxyInterceptor.super.handleProxyRequest(ctx);
+ }
+ });
+ }
+
if (StringUtils.isEmpty(path)) {
return;
}
@@ -223,24 +391,65 @@ public class ReverseProxyVerticle extends AbstractVerticle {
if (StringUtils.isEmpty(originPath) || path.equals(originPath)) {
Route route = path.startsWith("~") ? proxyRouter.routeWithRegex(path.substring(1))
: proxyRouter.route(path);
+ // 【优化】为代理处理器添加超时
route.handler(ProxyHandler.create(httpProxy));
} else {
// 配置 /api/, / => 请求 /api/test 代理后 /test
// 配置 /api/, /xxx => 请求 /api/test 代理后 /xxx/test
- final String path0 = path;
- final String originPath0 = REROUTE_PATH_PREFIX + originPath;
+ final String path0 = path;
+ final String originPath0 = REROUTE_PATH_PREFIX + originPath;
- proxyRouter.route(originPath0 + "*").handler(ProxyHandler.create(httpProxy));
- proxyRouter.route(path0 + "*").handler(ctx -> {
- String realPath = ctx.request().uri();
- if (realPath.startsWith(path0)) {
- // vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
- String rePath = realPath.replaceAll("^" + path0, originPath0);
- ctx.reroute(rePath);
- } else {
- ctx.next();
- }
- });
+ proxyRouter.route(originPath0 + "*").handler(ProxyHandler.create(httpProxy));
+ proxyRouter.route(path0 + "*").handler(ctx -> {
+ String realPath = ctx.request().uri();
+ if (realPath.startsWith(path0)) {
+ // vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath
+ String rePath = realPath.replaceAll("^" + path0, originPath0);
+ ctx.reroute(rePath);
+ } else {
+ ctx.next();
+ }
+ });
+ // 计算唯一后缀,避免多个 location 冲突
+// String uniqueKey = (host + ":" + port + "|" + path).replaceAll("[^a-zA-Z0-9:_|/]", "");
+// String uniqueSuffix = Integer.toHexString(uniqueKey.hashCode());
+//
+//// 规格化 originPath
+// //String originPath = url.getPath(); // 原值
+// if (StringUtils.isBlank(originPath)) originPath = "/";
+//
+//// 处理 index.html 的情况:用于首页兜底,其它子路径仍按目录穿透
+// String indexFile;
+// if (originPath.endsWith(".html")) {
+// indexFile = originPath; // 例如 /index.html
+// originPath = "/"; // 目录穿透基准改为根
+// } else {
+// indexFile = null;
+// }
+//
+//// 唯一内部挂载前缀
+// final String originMount = REROUTE_PATH_PREFIX + uniqueSuffix + originPath;
+//
+//// 1) 目标挂载:所有被重写的请求最终到这里走 ProxyHandler
+// proxyRouter.route(originMount + "*").handler(ProxyHandler.create(httpProxy));
+//
+//// 2) 从外部前缀 -> 内部挂载 的重写
+// final String path0 = path;
+// proxyRouter.route(path0 + "*").handler(ctx -> {
+// String uri = ctx.request().uri();
+// if (!uri.startsWith(path0)) { ctx.next(); return; }
+//
+// // 首页兜底:访问 /n2 或 /n2/ 时,重写到 index.html(如果配置了)
+// if (indexFile != null && (uri.equals(path0) || uri.equals(path0.substring(0, path0.length()-1)))) {
+// String rePath = originMount.endsWith("/") ? (originMount + indexFile.substring(1)) : (originMount + indexFile);
+// ctx.reroute(rePath);
+// return;
+// }
+//
+// // 一般穿透:/n2/xxx -> originMount + xxx
+// String rePath = uri.replaceFirst("^" + path0, originMount);
+// ctx.reroute(rePath);
+// });
}
} catch (MalformedURLException e) {
@@ -249,77 +458,4 @@ public class ReverseProxyVerticle extends AbstractVerticle {
});
}
-
- /**
- * 解析配置路径: 优先绝对路径, 否则尝试 user.dir 和 user.dir/..。
- */
- private String resolveExistingPath(String path, boolean directory) {
- if (StringUtils.isBlank(path)) {
- return null;
- }
-
- File directFile = new File(path);
- if (existsByType(directFile, directory)) {
- return directFile.getAbsolutePath();
- }
-
- String userDir = System.getProperty("user.dir");
- File inUserDir = new File(userDir, path);
- if (existsByType(inUserDir, directory)) {
- return inUserDir.getAbsolutePath();
- }
-
- File inParentDir = new File(new File(userDir).getParentFile(), path);
- if (existsByType(inParentDir, directory)) {
- return inParentDir.getAbsolutePath();
- }
-
- return null;
- }
-
- /**
- * StaticHandler 只接受相对 web root,不接受以 / 开头的绝对路径。
- */
- private String resolveStaticRoot(String path) {
- if (StringUtils.isBlank(path)) {
- return null;
- }
-
- File directFile = new File(path);
- if (existsByType(directFile, true)) {
- return path;
- }
-
- String userDir = System.getProperty("user.dir");
- File inUserDir = new File(userDir, path);
- if (existsByType(inUserDir, true)) {
- return relativizePath(new File(userDir), inUserDir);
- }
-
- File userDirFile = new File(userDir);
- File parentDir = userDirFile.getParentFile();
- File inParentDir = parentDir == null ? null : new File(parentDir, path);
- if (existsByType(inParentDir, true)) {
- return relativizePath(userDirFile, inParentDir);
- }
-
- return null;
- }
-
- private String relativizePath(File baseDir, File target) {
- try {
- Path basePath = baseDir.toPath().toAbsolutePath().normalize();
- Path targetPath = target.toPath().toAbsolutePath().normalize();
- return basePath.relativize(targetPath).toString().replace(File.separatorChar, '/');
- } catch (IllegalArgumentException ignored) {
- return target.getPath().replace(File.separatorChar, '/');
- }
- }
-
- private boolean existsByType(File file, boolean directory) {
- if (file == null || !file.exists()) {
- return false;
- }
- return directory ? file.isDirectory() : file.isFile();
- }
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java
index 46d477d..acc0c7e 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/RouterVerticle.java
@@ -48,10 +48,19 @@ public class RouterVerticle extends AbstractVerticle {
} else {
options = new HttpServerOptions();
}
-
- // 绑定到 0.0.0.0 以允许外部访问
- options.setHost("0.0.0.0");
options.setPort(port);
+
+ // 【优化】高并发服务器配置
+ options.setTcpKeepAlive(true) // TCP Keep-Alive
+ .setTcpNoDelay(true) // 禁用Nagle算法,降低延迟
+ .setCompressionSupported(true) // 启用压缩
+ .setAcceptBacklog(50000) // 增加积压队列到50000,防止高并发时连接被拒绝
+ .setIdleTimeout(120) // 空闲超时120秒
+ .setTcpFastOpen(true) // 启用TCP Fast Open
+ .setTcpQuickAck(true) // 启用TCP Quick ACK
+ .setReuseAddress(true) // 允许地址重用
+ .setReusePort(true); // 允许端口重用
+
server = vertx.createHttpServer(options);
server.requestHandler(router).webSocketHandler(s->{}).listen()
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java
index 8b2df50..946b339 100644
--- a/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/ServiceVerticle.java
@@ -29,20 +29,23 @@ public class ServiceVerticle extends AbstractVerticle {
Reflections reflections = ReflectionUtil.getReflections();
handlers = reflections.getTypesAnnotatedWith(Service.class);
}
-
@Override
public void start(Promise startPromise) {
ServiceBinder binder = new ServiceBinder(vertx);
if (null != handlers && handlers.size() > 0) {
+ // handlers转为拼接类列表,xxx,yyy,zzz
+ StringBuilder serviceNames = new StringBuilder();
handlers.forEach(asyncService -> {
try {
+ serviceNames.append(asyncService.getName()).append("|");
BaseAsyncService asInstance = (BaseAsyncService) ReflectionUtil.newWithNoParam(asyncService);
binder.setAddress(asInstance.getAddress()).register(asInstance.getAsyncInterfaceClass(), asInstance);
} catch (Exception e) {
- LOGGER.error(e.getMessage());
+ LOGGER.error("Failed to register service: {}", asyncService.getName(), e);
}
});
- LOGGER.info("registered async services -> id: {}", ID.getAndIncrement());
+
+ LOGGER.info("registered async services -> id: {}, name: {}", ID.getAndIncrement(), serviceNames.toString());
}
startPromise.complete();
}
diff --git a/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java
new file mode 100644
index 0000000..c8c4d5d
--- /dev/null
+++ b/core/src/main/java/cn/qaiu/vx/core/verticle/conf/HttpProxyConf.java
@@ -0,0 +1,89 @@
+package cn.qaiu.vx.core.verticle.conf;
+
+import io.vertx.codegen.annotations.DataObject;
+import io.vertx.codegen.json.annotations.JsonGen;
+import io.vertx.core.json.JsonObject;
+import io.vertx.core.net.ProxyOptions;
+
+import java.util.UUID;
+
+@DataObject
+//@JsonGen(publicConverter = false)
+public class HttpProxyConf {
+
+ public static final String DEFAULT_USERNAME = UUID.randomUUID().toString();
+
+ public static final String DEFAULT_PASSWORD = UUID.randomUUID().toString();
+
+ public static final Integer DEFAULT_PORT = 6432;
+
+ public static final Integer DEFAULT_TIMEOUT = 15000;
+
+ Integer timeout;
+
+ String username;
+
+ String password;
+
+ Integer port;
+
+ ProxyOptions preProxyOptions;
+
+ public HttpProxyConf() {
+ this.username = DEFAULT_USERNAME;
+ this.password = DEFAULT_PASSWORD;
+ this.timeout = DEFAULT_PORT;
+ this.timeout = DEFAULT_TIMEOUT;
+ this.preProxyOptions = new ProxyOptions();
+ }
+
+ public HttpProxyConf(JsonObject json) {
+ this();
+ }
+
+
+ public Integer getTimeout() {
+ return timeout;
+ }
+
+ public HttpProxyConf setTimeout(Integer timeout) {
+ this.timeout = timeout;
+ return this;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public HttpProxyConf setUsername(String username) {
+ this.username = username;
+ return this;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public HttpProxyConf setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Integer getPort() {
+ return port;
+ }
+
+ public HttpProxyConf setPort(Integer port) {
+ this.port = port;
+ return this;
+ }
+
+ public ProxyOptions getPreProxyOptions() {
+ return preProxyOptions;
+ }
+
+ public HttpProxyConf setPreProxyOptions(ProxyOptions preProxyOptions) {
+ this.preProxyOptions = preProxyOptions;
+ return this;
+ }
+}
diff --git a/core/src/main/resources/app.properties b/core/src/main/resources/app.properties
index e91d53d..a6bbd66 100644
--- a/core/src/main/resources/app.properties
+++ b/core/src/main/resources/app.properties
@@ -1,2 +1,2 @@
app.version=${project.version}
-build=${maven.build.timestamp}
+build=${build.timestamp}
diff --git a/core/src/test/java/cn/qaiu/vx/core/test/JsonBodyBindingLogicTest.java b/core/src/test/java/cn/qaiu/vx/core/test/JsonBodyBindingLogicTest.java
new file mode 100644
index 0000000..f9aab4b
--- /dev/null
+++ b/core/src/test/java/cn/qaiu/vx/core/test/JsonBodyBindingLogicTest.java
@@ -0,0 +1,134 @@
+package cn.qaiu.vx.core.test;
+
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * 单元测试:验证 RouterHandlerFactory 关于 JsonObject/JsonArray 参数绑定的核心分支逻辑是否正确
+ * (不启动整个 Vert.x 服务器,直接用 Vert.x JsonObject/JsonArray API 模拟验证关键逻辑)
+ */
+public class JsonBodyBindingLogicTest {
+
+ // === 模拟 handlerMethod 中的 JSON body 绑定逻辑 ===
+
+ /**
+ * 模拟:content-type = application/json,body 是 JsonObject
+ * 期望:JsonObject 类型参数被正确绑定
+ */
+ @Test
+ public void testJsonObjectBinding() {
+ String bodyStr = "{\"name\":\"test\",\"value\":123}";
+
+ // 模拟 ctx.body().asJsonObject()
+ JsonObject body = parseAsJsonObject(bodyStr);
+ Assert.assertNotNull("body 应能解析为 JsonObject", body);
+
+ // 模拟绑定逻辑中的类型判断
+ String targetType = JsonObject.class.getName();
+ boolean matched = JsonObject.class.getName().equals(targetType);
+ Assert.assertTrue("JsonObject 类型应命中绑定分支", matched);
+
+ // 模拟结果
+ Object bound = body; // parameterValueList.put(k, body)
+ Assert.assertNotNull("JsonObject 参数应被绑定(非null)", bound);
+ Assert.assertEquals("name字段应为test", "test", ((JsonObject) bound).getString("name"));
+ Assert.assertEquals("value字段应为123", 123, (int) ((JsonObject) bound).getInteger("value"));
+
+ System.out.println("[PASS] testJsonObjectBinding: JsonObject 绑定成功 -> " + bound);
+ }
+
+ /**
+ * 模拟:content-type = application/json,body 是 JsonArray
+ * 期望:JsonArray 类型参数被正确绑定
+ */
+ @Test
+ public void testJsonArrayBinding() {
+ String bodyStr = "[1,2,3]";
+
+ // body 解析为 JsonObject 应返回 null
+ JsonObject bodyAsObj = parseAsJsonObject(bodyStr);
+ Assert.assertNull("JsonArray body 解析为 JsonObject 应为 null", bodyAsObj);
+
+ // 进入 else 分支,解析为 JsonArray
+ JsonArray bodyArr = parseAsJsonArray(bodyStr);
+ Assert.assertNotNull("body 应能解析为 JsonArray", bodyArr);
+
+ String targetType = JsonArray.class.getName();
+ boolean matched = JsonArray.class.getName().equals(targetType);
+ Assert.assertTrue("JsonArray 类型应命中绑定分支", matched);
+
+ Object bound = bodyArr;
+ Assert.assertNotNull("JsonArray 参数应被绑定(非null)", bound);
+ Assert.assertEquals("数组大小应为3", 3, ((JsonArray) bound).size());
+
+ System.out.println("[PASS] testJsonArrayBinding: JsonArray 绑定成功, size=" + ((JsonArray) bound).size());
+ }
+
+ /**
+ * 验证旧代码的 bug:条件 ctx.body().asJsonObject() != null 会把 JsonArray body 排除在外
+ * 新代码只判断 content-type,在 body==null 时才进 else 分支处理 JsonArray
+ */
+ @Test
+ public void testOldConditionBug() {
+ String jsonArrayBody = "[1,2,3]";
+
+ // 旧代码条件:content-type==json && asJsonObject()!=null
+ // 对于 JsonArray body,asJsonObject() 返回 null,整个 if 跳过
+ JsonObject wrongParsed = parseAsJsonObject(jsonArrayBody);
+ boolean oldConditionPassed = wrongParsed != null; // 旧代码的第二个条件
+ Assert.assertFalse("旧代码 bug: JsonArray body 会导致 asJsonObject()==null,整个分支跳过", oldConditionPassed);
+
+ // 新代码:先进 if,body==null 再走 else 解析 JsonArray
+ boolean newConditionFirst = true; // content-type 匹配
+ JsonObject newBody = parseAsJsonObject(jsonArrayBody);
+ boolean newBodyIsNull = newBody == null; // null -> 进 else
+ Assert.assertTrue("新代码: body 解析为 null 时应走 else 分支解析 JsonArray", newBodyIsNull);
+
+ JsonArray newArr = parseAsJsonArray(jsonArrayBody);
+ Assert.assertNotNull("新代码: else 分支正确解析出 JsonArray", newArr);
+
+ System.out.println("[PASS] testOldConditionBug: 修复验证通过,新代码正确处理 JsonArray body");
+ }
+
+ /**
+ * 验证:JsonObject 参数旧代码没有绑定分支(只处理实体类)
+ */
+ @Test
+ public void testOldMissingJsonObjectBranch() {
+ String bodyStr = "{\"key\":\"value\"}";
+ JsonObject body = parseAsJsonObject(bodyStr);
+
+ // 旧代码只调用 matchRegList(entityPackagesReg, typeName)
+ // 对于 io.vertx.core.json.JsonObject,该方法返回 false,不会被绑定
+ String typeName = JsonObject.class.getName(); // "io.vertx.core.json.JsonObject"
+ // entityPackagesReg 一般是 "cn.qaiu.*" 这类,不会匹配 io.vertx
+ boolean oldWouldBind = typeName.startsWith("cn.qaiu"); // 模拟旧代码逻辑
+ Assert.assertFalse("旧代码 bug: JsonObject 参数不会被绑定", oldWouldBind);
+
+ // 新代码:增加了 JsonObject 类型判断
+ boolean newWouldBind = JsonObject.class.getName().equals(typeName);
+ Assert.assertTrue("新代码: JsonObject 参数应能被绑定", newWouldBind);
+
+ System.out.println("[PASS] testOldMissingJsonObjectBranch: 修复验证通过");
+ }
+
+ // ===== 辅助方法:模拟 Vert.x RequestBody 的 asJsonObject/asJsonArray 行为 =====
+
+ private JsonObject parseAsJsonObject(String str) {
+ try {
+ return new JsonObject(str);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private JsonArray parseAsJsonArray(String str) {
+ try {
+ return new JsonArray(str);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/core/src/test/java/cn/qaiu/vx/core/test/RouterHandlerBindingTest.java b/core/src/test/java/cn/qaiu/vx/core/test/RouterHandlerBindingTest.java
new file mode 100644
index 0000000..1d6eb8c
--- /dev/null
+++ b/core/src/test/java/cn/qaiu/vx/core/test/RouterHandlerBindingTest.java
@@ -0,0 +1,125 @@
+package cn.qaiu.vx.core.test;
+
+import cn.qaiu.vx.core.util.VertxHolder;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 集成测试: 验证 RouterHandlerFactory 对 JsonObject/JsonArray 参数绑定逻辑是否正确
+ *
+ * 运行方式: mvn test-compile -pl core && java -cp "core/target/test-classes:core/target/classes:..." \
+ * cn.qaiu.vx.core.test.RouterHandlerBindingTest
+ *
+ * 或直接在 IDE 中运行 main 方法。
+ */
+public class RouterHandlerBindingTest {
+
+ static final int TEST_PORT = 18989;
+
+ public static void main(String[] args) throws Exception {
+ System.out.println("=== RouterHandler JsonObject/JsonArray 绑定测试 ===\n");
+
+ // 1. 先初始化 Vert.x 与 VertxHolder ——必须在加载 RouterHandlerFactory 之前
+ Vertx vertx = Vertx.vertx();
+ VertxHolder.init(vertx);
+
+ // 2. 向 SharedData 注入最小化配置
+ // baseLocations 指向测试包,使 Reflections 只扫描 TestJsonHandler
+ vertx.sharedData().getLocalMap("local").put("customConfig", new JsonObject()
+ .put("baseLocations", "cn.qaiu.vx.core.test")
+ .put("routeTimeOut", 30000)
+ .put("entityPackagesReg", new JsonArray()));
+ // ReverseProxyVerticle. 需要 globalConfig.proxyConf(非空字符串即可)
+ vertx.sharedData().getLocalMap("local").put("globalConfig", new JsonObject()
+ .put("proxyConf", "proxy.yml"));
+
+ // 3. 创建 Router(此时才触发 BaseHttpApi.reflections 静态字段初始化)
+ // 用反射延迟加载,确保上面的 SharedData 已就绪
+ cn.qaiu.vx.core.handlerfactory.RouterHandlerFactory factory =
+ new cn.qaiu.vx.core.handlerfactory.RouterHandlerFactory("api");
+ io.vertx.ext.web.Router router = factory.createRouter();
+
+ // 4. 启动 HTTP 服务器
+ CountDownLatch latch = new CountDownLatch(1);
+ vertx.createHttpServer()
+ .requestHandler(router)
+ .listen(TEST_PORT, res -> {
+ if (res.succeeded()) {
+ System.out.println("✔ 测试服务器启动成功 port=" + TEST_PORT);
+ } else {
+ System.err.println("✘ 服务器启动失败: " + res.cause().getMessage());
+ }
+ latch.countDown();
+ });
+
+ if (!latch.await(5, TimeUnit.SECONDS)) {
+ System.err.println("服务器启动超时");
+ vertx.close();
+ System.exit(1);
+ }
+ Thread.sleep(100); // 等 Vert.x 就绪
+
+ // 5. 执行测试
+ boolean allPassed = true;
+ allPassed &= testJsonObject();
+ allPassed &= testJsonArray();
+
+ // 6. 关闭
+ CountDownLatch closeLatch = new CountDownLatch(1);
+ vertx.close(v -> closeLatch.countDown());
+ closeLatch.await(3, TimeUnit.SECONDS);
+
+ System.out.println("\n" + (allPassed ? "✅ 全部测试通过!" : "❌ 存在测试失败!"));
+ System.exit(allPassed ? 0 : 1);
+ }
+
+ // ---------- 子测试 ----------
+
+ private static boolean testJsonObject() throws Exception {
+ String bodyStr = "{\"name\":\"test\",\"value\":123}";
+ String respBody = post("/api/test/json-object", bodyStr);
+ System.out.println("[JsonObject] 响应: " + respBody);
+
+ JsonObject result = new JsonObject(respBody);
+ JsonObject data = result.getJsonObject("data");
+ boolean bound = data != null && Boolean.TRUE.equals(data.getBoolean("bound"));
+ System.out.println("[JsonObject] " + (bound
+ ? "PASS ✅ body 正确绑定为 JsonObject"
+ : "FAIL ❌ body 未绑定 (null)"));
+ return bound;
+ }
+
+ private static boolean testJsonArray() throws Exception {
+ String bodyStr = "[1,2,3]";
+ String respBody = post("/api/test/json-array", bodyStr);
+ System.out.println("[JsonArray] 响应: " + respBody);
+
+ JsonObject result = new JsonObject(respBody);
+ JsonObject data = result.getJsonObject("data");
+ boolean bound = data != null
+ && Boolean.TRUE.equals(data.getBoolean("bound"))
+ && Integer.valueOf(3).equals(data.getInteger("size"));
+ System.out.println("[JsonArray] " + (bound
+ ? "PASS ✅ body 正确绑定为 JsonArray, size=3"
+ : "FAIL ❌ body 未绑定 或 size 不对"));
+ return bound;
+ }
+
+ private static String post(String path, String body) throws Exception {
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest req = HttpRequest.newBuilder()
+ .uri(URI.create("http://localhost:" + TEST_PORT + path))
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(body))
+ .build();
+ return client.send(req, HttpResponse.BodyHandlers.ofString()).body();
+ }
+}
diff --git a/core/src/test/java/cn/qaiu/vx/core/test/TestJsonHandler.java b/core/src/test/java/cn/qaiu/vx/core/test/TestJsonHandler.java
new file mode 100644
index 0000000..2fad6ab
--- /dev/null
+++ b/core/src/test/java/cn/qaiu/vx/core/test/TestJsonHandler.java
@@ -0,0 +1,36 @@
+package cn.qaiu.vx.core.test;
+
+import cn.qaiu.vx.core.annotaions.RouteHandler;
+import cn.qaiu.vx.core.annotaions.RouteMapping;
+import cn.qaiu.vx.core.enums.MIMEType;
+import cn.qaiu.vx.core.enums.RouteMethod;
+import cn.qaiu.vx.core.model.JsonResult;
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+
+/**
+ * 用于测试 RouterHandlerFactory 对 JsonObject/JsonArray 参数绑定的测试 Handler
+ */
+@RouteHandler("test")
+public class TestJsonHandler {
+
+ /** POST /api/test/json-object Body: {"name":"test","value":123} */
+ @RouteMapping(value = "/json-object", method = RouteMethod.POST, requestMIMEType = MIMEType.APPLICATION_JSON)
+ public Future testJsonObject(JsonObject body) {
+ // 只返回是否绑定成功及已知字段值,不嵌套原始 body 避免 toJsonObject() 循环
+ boolean bound = body != null;
+ String nameVal = bound ? body.getString("name", "") : "";
+ return Future.succeededFuture(JsonResult.data(new io.vertx.core.json.JsonObject()
+ .put("bound", bound)
+ .put("name", nameVal)));
+ }
+
+ /** POST /api/test/json-array Body: [1,2,3] */
+ @RouteMapping(value = "/json-array", method = RouteMethod.POST, requestMIMEType = MIMEType.APPLICATION_JSON)
+ public Future testJsonArray(JsonArray body) {
+ return Future.succeededFuture(JsonResult.data(new io.vertx.core.json.JsonObject()
+ .put("bound", body != null)
+ .put("size", body != null ? body.size() : -1)));
+ }
+}
diff --git a/parser/src/main/resources/custom-parsers/fetch-demo.js b/parser/src/main/resources/custom-parsers/fetch-demo.js
index d1e2021..2d59b04 100644
--- a/parser/src/main/resources/custom-parsers/fetch-demo.js
+++ b/parser/src/main/resources/custom-parsers/fetch-demo.js
@@ -1,105 +1,105 @@
-// ==UserScript==
-// @name Fetch API示例解析器
-// @type fetch_demo
-// @displayName Fetch演示
-// @description 演示如何在ES5环境中使用fetch API和async/await
-// @match https?://example\.com/s/(?\w+)
-// @author QAIU
-// @version 1.0.0
-// ==/UserScript==
+// // ==UserScript==
+// // @name Fetch API示例解析器
+// // @type fetch_demo
+// // @displayName Fetch演示
+// // @description 演示如何在ES5环境中使用fetch API和async/await
+// // @match https?://example\.com/s/(?\w+)
+// // @author QAIU
+// // @version 1.0.0
+// // ==/UserScript==
-// 使用require导入类型定义(仅用于IDE类型提示)
-var types = require('./types');
-/** @typedef {types.ShareLinkInfo} ShareLinkInfo */
-/** @typedef {types.JsHttpClient} JsHttpClient */
-/** @typedef {types.JsLogger} JsLogger */
+// // 使用require导入类型定义(仅用于IDE类型提示)
+// var types = require('./types');
+// /** @typedef {types.ShareLinkInfo} ShareLinkInfo */
+// /** @typedef {types.JsHttpClient} JsHttpClient */
+// /** @typedef {types.JsLogger} JsLogger */
-/**
- * 演示使用fetch API的解析器
- * 注意:虽然源码中使用了ES6+语法(async/await),但在浏览器中会被编译为ES5
- *
- * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
- * @param {JsHttpClient} http - HTTP客户端(传统方式)
- * @param {JsLogger} logger - 日志对象
- * @returns {string} 下载链接
- */
-function parse(shareLinkInfo, http, logger) {
- logger.info("=== Fetch API Demo ===");
+// /**
+// * 演示使用fetch API的解析器
+// * 注意:虽然源码中使用了ES6+语法(async/await),但在浏览器中会被编译为ES5
+// *
+// * @param {ShareLinkInfo} shareLinkInfo - 分享链接信息
+// * @param {JsHttpClient} http - HTTP客户端(传统方式)
+// * @param {JsLogger} logger - 日志对象
+// * @returns {string} 下载链接
+// */
+// function parse(shareLinkInfo, http, logger) {
+// logger.info("=== Fetch API Demo ===");
- // 方式1:使用传统的http对象(同步)
- logger.info("方式1: 使用传统http对象");
- var response1 = http.get("https://httpbin.org/get");
- logger.info("状态码: " + response1.statusCode());
+// // 方式1:使用传统的http对象(同步)
+// logger.info("方式1: 使用传统http对象");
+// var response1 = http.get("https://httpbin.org/get");
+// logger.info("状态码: " + response1.statusCode());
- // 方式2:使用fetch API(基于Promise)
- logger.info("方式2: 使用fetch API");
+// // 方式2:使用fetch API(基于Promise)
+// logger.info("方式2: 使用fetch API");
- // 注意:在ES5环境中,我们需要手动处理Promise
- // 这个示例展示了如何在ES5中使用fetch
- var fetchPromise = fetch("https://httpbin.org/get");
+// // 注意:在ES5环境中,我们需要手动处理Promise
+// // 这个示例展示了如何在ES5中使用fetch
+// var fetchPromise = fetch("https://httpbin.org/get");
- // 等待Promise完成(同步等待模拟)
- var result = null;
- var error = null;
+// // 等待Promise完成(同步等待模拟)
+// var result = null;
+// var error = null;
- fetchPromise
- .then(function(response) {
- logger.info("Fetch响应状态: " + response.status);
- return response.text();
- })
- .then(function(text) {
- logger.info("Fetch响应内容: " + text.substring(0, 100) + "...");
- result = "https://example.com/download/demo.file";
- })
- ['catch'](function(err) {
- logger.error("Fetch失败: " + err.message);
- error = err;
- });
+// fetchPromise
+// .then(function(response) {
+// logger.info("Fetch响应状态: " + response.status);
+// return response.text();
+// })
+// .then(function(text) {
+// logger.info("Fetch响应内容: " + text.substring(0, 100) + "...");
+// result = "https://example.com/download/demo.file";
+// })
+// ['catch'](function(err) {
+// logger.error("Fetch失败: " + err.message);
+// error = err;
+// });
- // 简单的等待循环(实际场景中不推荐,这里仅作演示)
- var timeout = 5000; // 5秒超时
- var start = Date.now();
- while (result === null && error === null && (Date.now() - start) < timeout) {
- // 等待Promise完成
- java.lang.Thread.sleep(10);
- }
+// // 简单的等待循环(实际场景中不推荐,这里仅作演示)
+// var timeout = 5000; // 5秒超时
+// var start = Date.now();
+// while (result === null && error === null && (Date.now() - start) < timeout) {
+// // 等待Promise完成
+// java.lang.Thread.sleep(10);
+// }
- if (error !== null) {
- throw error;
- }
+// if (error !== null) {
+// throw error;
+// }
- if (result === null) {
- throw new Error("Fetch超时");
- }
+// if (result === null) {
+// throw new Error("Fetch超时");
+// }
- return result;
-}
+// return result;
+// }
-/**
- * 演示POST请求
- */
-function demonstratePost(logger) {
- logger.info("=== 演示POST请求 ===");
+// /**
+// * 演示POST请求
+// */
+// function demonstratePost(logger) {
+// logger.info("=== 演示POST请求 ===");
- var postPromise = fetch("https://httpbin.org/post", {
- method: "POST",
- headers: {
- "Content-Type": "application/json"
- },
- body: JSON.stringify({
- key: "value",
- demo: true
- })
- });
+// var postPromise = fetch("https://httpbin.org/post", {
+// method: "POST",
+// headers: {
+// "Content-Type": "application/json"
+// },
+// body: JSON.stringify({
+// key: "value",
+// demo: true
+// })
+// });
- postPromise
- .then(function(response) {
- return response.json();
- })
- .then(function(data) {
- logger.info("POST响应: " + JSON.stringify(data));
- })
- ['catch'](function(err) {
- logger.error("POST失败: " + err.message);
- });
-}
+// postPromise
+// .then(function(response) {
+// return response.json();
+// })
+// .then(function(data) {
+// logger.info("POST响应: " + JSON.stringify(data));
+// })
+// ['catch'](function(err) {
+// logger.error("POST失败: " + err.message);
+// });
+// }
diff --git a/web-service/src/main/resources/app.yml b/web-service/src/main/resources/app.yml
index 3947d90..c4a7112 100644
--- a/web-service/src/main/resources/app.yml
+++ b/web-service/src/main/resources/app.yml
@@ -1,4 +1,4 @@
# 要激活的配置: app-配置名称.yml
-active: dev
+active: local
# 控制台输出的版权文字
copyright: QAIU
diff --git a/web-service/src/main/resources/server-proxy-local.yml b/web-service/src/main/resources/server-proxy-local.yml
new file mode 100644
index 0000000..4fcc15a
--- /dev/null
+++ b/web-service/src/main/resources/server-proxy-local.yml
@@ -0,0 +1,54 @@
+# 反向代理
+server-name: Vert.x-proxy-server(v4.1.2)
+
+proxy:
+ - listen: 16401
+ # 404的路径
+ page404: webroot/nfd-front/index.html
+ static:
+ path: /
+ add-headers:
+ x-token: ABC
+ root: webroot/nfd-front/
+# index: index.html
+ # ~开头(没有空格)表示正则匹配否则为前缀匹配, 当origin带子路径时进行路由重写,
+ # 1.origin代理地址端口后有目录(包括 / ),转发后地址:代理地址+访问URL目录部分去除location匹配目录
+ # 2.origin代理地址端口后无任何,转发后地址:代理地址+访问URL目录部
+ location:
+ - path: ~^/(json/|v2/|d/|parser|ye/|lz/|cow/|ec/|fj/|fc/|le/|qq/|ws/|iz/|ce/).*
+ origin: 127.0.0.1:16400
+
+ # json/parser -> xxx/parser
+# - path: /json/
+# origin: 127.0.0.1:16400/
+ - path: /n1/
+ origin: 127.0.0.1:16400/v2/
+
+# # SSL HTTPS配置
+ ssl:
+ enable: false
+ # 强制https 暂不支持
+ #ssl_force: true
+ # SSL 协议版本
+ ssl_protocols: TLSv1.2
+ # 证书
+ ssl_certificate: ssl/server.pem
+ # 私钥
+ ssl_certificate_key: ssl/privkey.key
+ # 加密套件 ssl_ciphers 暂不支持
+ # ssl_ciphers: AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
+
+# - listen: 8086
+# static:
+# path: /t2/
+# root: webroot/test/
+# index: sockTest.html
+# location:
+# - path: /real/
+# origin: 127.0.0.1:8088
+# sock:
+# - path: /real/
+# origin: 127.0.0.1:8088
+
+
+