diff --git a/.vscode/settings.json b/.vscode/settings.json index e012065..1b5ba53 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "java.compile.nullAnalysis.mode": "automatic", - "java.configuration.updateBuildConfiguration": "interactive" + "java.configuration.updateBuildConfiguration": "interactive", + "java.debug.settings.onBuildFailureProceed": true } \ No newline at end of file diff --git a/core/pom.xml b/core/pom.xml index 6aa0f4e..044a2ce 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -73,6 +73,12 @@ ${jackson.version} + + junit + junit + ${junit.version} + test + diff --git a/core/src/main/java/cn/qaiu/vx/core/Deploy.java b/core/src/main/java/cn/qaiu/vx/core/Deploy.java index 459e2d4..e6717be 100644 --- a/core/src/main/java/cn/qaiu/vx/core/Deploy.java +++ b/core/src/main/java/cn/qaiu/vx/core/Deploy.java @@ -3,12 +3,13 @@ package cn.qaiu.vx.core; import cn.qaiu.vx.core.util.CommonUtil; import cn.qaiu.vx.core.util.ConfigUtil; import cn.qaiu.vx.core.util.VertxHolder; +import cn.qaiu.vx.core.verticle.HttpProxyVerticle; +import cn.qaiu.vx.core.verticle.PostExecVerticle; import cn.qaiu.vx.core.verticle.ReverseProxyVerticle; import cn.qaiu.vx.core.verticle.RouterVerticle; import cn.qaiu.vx.core.verticle.ServiceVerticle; import io.vertx.core.*; import io.vertx.core.dns.AddressResolverOptions; -import io.vertx.core.impl.launcher.commands.VersionCommand; import io.vertx.core.json.JsonObject; import io.vertx.core.shareddata.LocalMap; import org.slf4j.Logger; @@ -17,6 +18,7 @@ import org.slf4j.LoggerFactory; import java.lang.management.ManagementFactory; import java.util.Calendar; import java.util.Date; +import java.util.UUID; import java.util.concurrent.locks.LockSupport; import static cn.qaiu.vx.core.util.ConfigConstant.*; @@ -54,6 +56,7 @@ public final class Deploy { public void start(String[] args, Handler handle) { this.mainThread = Thread.currentThread(); this.handle = handle; + if (args.length > 0 && args[0].startsWith("app-")) { // 启动参数dev或者prod path.append("-").append(args[0].replace("app-","")); @@ -104,7 +107,7 @@ public final class Deploy { System.out.printf(logoTemplate, CommonUtil.getAppVersion(), - VersionCommand.getVersion(), + "4x", conf.getString("copyright"), year ); @@ -123,12 +126,12 @@ public final class Deploy { var vertxOptions = vertxConfigELPS == 0 ? new VertxOptions() : new VertxOptions(vertxConfig); - vertxOptions.setAddressResolverOptions( - new AddressResolverOptions(). - addServer("114.114.114.114"). - addServer("114.114.115.115"). - addServer("8.8.8.8"). - addServer("8.8.4.4")); +// vertxOptions.setAddressResolverOptions( +// new AddressResolverOptions(). +// addServer("114.114.114.114"). +// addServer("114.114.115.115"). +// addServer("8.8.8.8"). +// addServer("8.8.4.4")); LOGGER.info("vertxConfigEventLoopPoolSize: {}, eventLoopPoolSize: {}, workerPoolSize: {}", vertxConfigELPS, vertxOptions.getEventLoopPoolSize(), vertxOptions.getWorkerPoolSize()); @@ -153,12 +156,39 @@ public final class Deploy { var future2 = vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service")); var future3 = vertx.deployVerticle(ReverseProxyVerticle.class, getWorkDeploymentOptions("proxy")); - Future.all(future1, future2, future3) - .onSuccess(this::deployWorkVerticalSuccess) - .onFailure(this::deployVerticalFailed); + + JsonObject jsonObject = ((JsonObject) localMap.get(GLOBAL_CONFIG)).getJsonObject("proxy-server"); + if (jsonObject != null) { + genPwd(jsonObject); + var future4 = vertx.deployVerticle(HttpProxyVerticle.class, getWorkDeploymentOptions("proxy")); + future4.onSuccess(LOGGER::info); + future4.onFailure(e -> LOGGER.error("Other handle error", e)); + Future.all(future1, future2, future3, future4) + .onSuccess(this::deployWorkVerticalSuccess) + .onFailure(this::deployVerticalFailed); + } else { + Future.all(future1, future2, future3) + .onSuccess(this::deployWorkVerticalSuccess) + .onFailure(this::deployVerticalFailed); + } + }).onFailure(e -> LOGGER.error("Other handle error", e)); } + private static void genPwd(JsonObject jsonObject) { + if (jsonObject.getBoolean("randUserPwd")) { + var username = UUID.randomUUID().toString().replace("-", "").substring(0, 16); + var password = UUID.randomUUID().toString().replace("-", "").substring(0, 16); + jsonObject.put("username", username); + jsonObject.put("password", password); + } + LOGGER.info("=============server info================="); + LOGGER.info("\nport: {}\nusername: {}\npassword: {}", + jsonObject.getString("port"), + jsonObject.getString("username"), + jsonObject.getString("password")); + LOGGER.info("==============server info================"); + } /** * 部署失败 * @@ -178,6 +208,42 @@ public final class Deploy { var t1 = ((double) (System.currentTimeMillis() - startTime)) / 1000; var t2 = ((double) System.currentTimeMillis() - ManagementFactory.getRuntimeMXBean().getStartTime()) / 1000; LOGGER.info("web服务启动成功 -> 用时: {}s, jvm启动用时: {}s", t1, t2); + + // 检查是否处于安装引导模式(数据库未配置) + Object installMode = VertxHolder.getVertxInstance().sharedData() + .getLocalMap(LOCAL).get("installMode"); + if (Boolean.TRUE.equals(installMode)) { + LOGGER.info("系统处于安装引导模式,等待用户完成数据库配置后再启动后置初始化..."); + return; + } + + // 正常模式:部署 PostExecVerticle 执行 AppRun 实现 + deployPostExec(); + } + + /** + * 部署 PostExecVerticle(执行所有 AppRun 实现) + * 安装引导完成后也可手动调用此方法触发后置初始化 + */ + public void deployPostExec() { + var vertx = VertxHolder.getVertxInstance(); + var postExecFuture = vertx.deployVerticle(PostExecVerticle.class, getWorkDeploymentOptions("postExec", 2)); + postExecFuture.onSuccess(id -> { + LOGGER.info("PostExecVerticle 部署成功,AppRun 实现执行完成"); + }).onFailure(e -> { + LOGGER.error("PostExecVerticle 部署失败", e); + }); + } + + /** + * 重新部署 ServiceVerticle,重新注册因 DB 未就绪而失败的服务到 EventBus + * 安装引导完成、DB 初始化后调用 + */ + public void redeployServices() { + var vertx = VertxHolder.getVertxInstance(); + vertx.deployVerticle(ServiceVerticle.class, getWorkDeploymentOptions("Service")) + .onSuccess(id -> LOGGER.info("ServiceVerticle 重新部署成功,DB 相关服务已注册")) + .onFailure(e -> LOGGER.error("ServiceVerticle 重新部署失败", e)); } /** diff --git a/core/src/main/java/cn/qaiu/vx/core/annotaions/HandleSortFilter.java b/core/src/main/java/cn/qaiu/vx/core/annotaions/HandleSortFilter.java index 1457498..d4bd1fe 100644 --- a/core/src/main/java/cn/qaiu/vx/core/annotaions/HandleSortFilter.java +++ b/core/src/main/java/cn/qaiu/vx/core/annotaions/HandleSortFilter.java @@ -9,6 +9,7 @@ import java.lang.annotation.*; public @interface HandleSortFilter { /** * 注册顺序,数字越大越先注册
+ * 前置拦截器会先执行后注册即数字小的, 后置拦截器会先执行先注册的即数字大的
* 值<0时会过滤掉该处理器 */ int value() default 0; diff --git a/core/src/main/java/cn/qaiu/vx/core/base/AppRun.java b/core/src/main/java/cn/qaiu/vx/core/base/AppRun.java new file mode 100644 index 0000000..63e62f9 --- /dev/null +++ b/core/src/main/java/cn/qaiu/vx/core/base/AppRun.java @@ -0,0 +1,12 @@ +package cn.qaiu.vx.core.base; + +import io.vertx.core.json.JsonObject; + +public interface AppRun { + + /** + * 执行方法 + * @param config 启动配置文件 + */ + void execute(JsonObject config); +} diff --git a/core/src/main/java/cn/qaiu/vx/core/base/BaseHttpApi.java b/core/src/main/java/cn/qaiu/vx/core/base/BaseHttpApi.java index 2e324e7..0b8261c 100644 --- a/core/src/main/java/cn/qaiu/vx/core/base/BaseHttpApi.java +++ b/core/src/main/java/cn/qaiu/vx/core/base/BaseHttpApi.java @@ -38,6 +38,20 @@ public interface BaseHttpApi { handleAfterInterceptor(ctx, jsonResult.toJsonObject()); } + default void doFireJsonObjectResponse(RoutingContext ctx, JsonObject jsonObject, int statusCode) { + if (!ctx.response().ended()) { + fireJsonObjectResponse(ctx, jsonObject, statusCode); + } + handleAfterInterceptor(ctx, jsonObject); + } + + + default void doFireJsonResultResponse(RoutingContext ctx, JsonResult jsonResult, int statusCode) { + if (!ctx.response().ended()) { + fireJsonResultResponse(ctx, jsonResult, statusCode); + } + handleAfterInterceptor(ctx, jsonResult.toJsonObject()); + } default Set getAfterInterceptor() { diff --git a/core/src/main/java/cn/qaiu/vx/core/base/DefaultAppRun.java b/core/src/main/java/cn/qaiu/vx/core/base/DefaultAppRun.java new file mode 100644 index 0000000..b6092b6 --- /dev/null +++ b/core/src/main/java/cn/qaiu/vx/core/base/DefaultAppRun.java @@ -0,0 +1,23 @@ +package cn.qaiu.vx.core.base; + +import cn.qaiu.vx.core.annotaions.HandleSortFilter; +import io.vertx.core.json.JsonObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 默认的AppRun实现示例 + *
Create date 2024-01-01 00:00:00 + * + * @author QAIU + */ +@HandleSortFilter +public class DefaultAppRun implements AppRun { + + private static final Logger LOGGER = LoggerFactory.getLogger(DefaultAppRun.class); + + @Override + public void execute(JsonObject config) { + LOGGER.info("======> AppRun实现类开始执行,配置数: {}", config.size()); + } +} diff --git a/core/src/main/java/cn/qaiu/vx/core/handlerfactory/RouterHandlerFactory.java b/core/src/main/java/cn/qaiu/vx/core/handlerfactory/RouterHandlerFactory.java index 1794a39..aca7f7b 100644 --- a/core/src/main/java/cn/qaiu/vx/core/handlerfactory/RouterHandlerFactory.java +++ b/core/src/main/java/cn/qaiu/vx/core/handlerfactory/RouterHandlerFactory.java @@ -23,8 +23,6 @@ import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.handler.*; import io.vertx.ext.web.handler.sockjs.SockJSHandler; import io.vertx.ext.web.handler.sockjs.SockJSHandlerOptions; -import io.vertx.ext.web.sstore.LocalSessionStore; -import io.vertx.ext.web.sstore.SessionStore; import javassist.CtClass; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; @@ -76,15 +74,15 @@ public class RouterHandlerFactory implements BaseHttpApi { // 主路由 Router mainRouter = Router.router(VertxHolder.getVertxInstance()); mainRouter.route().handler(ctx -> { - String realPath = ctx.request().uri();; + String realPath = ctx.request().uri(); if (realPath.startsWith(REROUTE_PATH_PREFIX)) { // vertx web proxy暂不支持rewrite, 所以这里进行手动替换, 请求地址中的请求path前缀替换为originPath - String rePath = realPath.substring(REROUTE_PATH_PREFIX.length()); + String rePath = realPath.replace(REROUTE_PATH_PREFIX, ""); ctx.reroute(rePath); return; } - LOGGER.debug("The HTTP service request address information ===>path:{}, uri:{}, method:{}", + LOGGER.debug("New request:{}, {}, {}", ctx.request().path(), ctx.request().absoluteURI(), ctx.request().method()); ctx.response().headers().add(ACCESS_CONTROL_ALLOW_ORIGIN, "*"); ctx.response().headers().add(DATE, LocalDateTime.now().format(ISO_LOCAL_DATE_TIME)); @@ -100,16 +98,6 @@ public class RouterHandlerFactory implements BaseHttpApi { // 配置文件上传路径 mainRouter.route().handler(BodyHandler.create().setUploadsDirectory("uploads")); - // 配置Session管理 - 用于演练场登录状态持久化 - // 30天过期时间(毫秒) - SessionStore sessionStore = LocalSessionStore.create(VertxHolder.getVertxInstance()); - SessionHandler sessionHandler = SessionHandler.create(sessionStore) - .setSessionTimeout(30L * 24 * 60 * 60 * 1000) // 30天 - .setSessionCookieName("SESSIONID") // Cookie名称 - .setCookieHttpOnlyFlag(true) // 防止XSS攻击 - .setCookieSecureFlag(false); // 非HTTPS环境设置为false - mainRouter.route().handler(sessionHandler); - // 拦截器 Set> interceptorSet = getInterceptorSet(); Route route0 = mainRouter.route("/*"); @@ -189,10 +177,10 @@ public class RouterHandlerFactory implements BaseHttpApi { if (ctx.response().ended()) return; // 超时处理器状态码503 if (ctx.statusCode() == 503 || ctx.failure() == null) { - doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员", 500)); + doFireJsonResultResponse(ctx, JsonResult.error("未知异常, 请联系管理员"), 503); } else { ctx.failure().printStackTrace(); - doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage(), 500)); + doFireJsonResultResponse(ctx, JsonResult.error(ctx.failure().getMessage()), 500); } }); } else if (method.isAnnotationPresent(SockRouteMapper.class)) { @@ -246,7 +234,7 @@ public class RouterHandlerFactory implements BaseHttpApi { */ private Set> getInterceptorSet() { // 配置拦截 - return getBeforeInterceptor().stream().map(BeforeInterceptor::doHandle).collect(Collectors.toSet()); + return getBeforeInterceptor().stream().map(BeforeInterceptor::doHandle).collect(Collectors.toCollection(LinkedHashSet::new)); } /** @@ -315,19 +303,19 @@ public class RouterHandlerFactory implements BaseHttpApi { final MultiMap queryParams = ctx.queryParams(); // 解析body-json参数 - // 只处理POST/PUT/PATCH等有body的请求方法,避免GET请求读取body导致"Request has already been read"错误 - String httpMethod = ctx.request().method().name(); - if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod)) - && ctx.parsedHeaders() != null && ctx.parsedHeaders().contentType() != null - && HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value()) - && ctx.body() != null && ctx.body().asJsonObject() != null) { + if (HttpHeaderValues.APPLICATION_JSON.toString().equals(ctx.parsedHeaders().contentType().value())) { JsonObject body = ctx.body().asJsonObject(); if (body != null) { methodParametersTemp.forEach((k, v) -> { + String typeName = v.getRight().getName(); + // 直接绑定 JsonObject 类型参数 + if (JsonObject.class.getName().equals(typeName)) { + parameterValueList.put(k, body); + } // 只解析已配置包名前缀的实体类 - if (CommonUtil.matchRegList(entityPackagesReg.getList(), v.getRight().getName())) { + else if (CommonUtil.matchRegList(entityPackagesReg.getList(), typeName)) { try { - Class aClass = Class.forName(v.getRight().getName()); + Class aClass = Class.forName(typeName); JsonObject data = CommonUtil.getSubJsonForEntity(body, aClass); if (!data.isEmpty()) { Object entity = data.mapTo(aClass); @@ -336,17 +324,21 @@ public class RouterHandlerFactory implements BaseHttpApi { } catch (ClassNotFoundException e) { e.printStackTrace(); } - } }); + } else { + // body 可能是 JsonArray + JsonArray bodyArray = ctx.body().asJsonArray(); + if (bodyArray != null) { + methodParametersTemp.forEach((k, v) -> { + if (JsonArray.class.getName().equals(v.getRight().getName())) { + parameterValueList.put(k, bodyArray); + } + }); + } } - } else if (("POST".equals(httpMethod) || "PUT".equals(httpMethod) || "PATCH".equals(httpMethod)) - && ctx.body() != null && ctx.body().length() > 0) { - try { - queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString())); - } catch (Exception e) { - LOGGER.debug("Failed to parse body as params: {}", e.getMessage()); - } + } else if (ctx.body() != null) { + queryParams.addAll(ParamUtil.paramsToMap(ctx.body().asString())); } // 解析其他参数 @@ -365,12 +357,6 @@ public class RouterHandlerFactory implements BaseHttpApi { parameterValueList.put(k, ctx.request()); } else if (HttpServerResponse.class.getName().equals(v.getRight().getName())) { parameterValueList.put(k, ctx.response()); - } else if (JsonObject.class.getName().equals(v.getRight().getName())) { - if (ctx.body() != null && ctx.body().asJsonObject() != null) { - parameterValueList.put(k, ctx.body().asJsonObject()); - } else { - parameterValueList.put(k, new JsonObject()); - } } else if (parameterValueList.get(k) == null && CommonUtil.matchRegList(entityPackagesReg.getList(), v.getRight().getName())) { // 绑定实体类 @@ -381,45 +367,48 @@ public class RouterHandlerFactory implements BaseHttpApi { } catch (Exception e) { e.printStackTrace(); } + } else if (parameterValueList.get(k) == null + && JsonObject.class.getName().equals(v.getRight().getName())) { + // 兜底: content-type 非 application/json 时尝试从 body 解析 JsonObject + if (ctx.body() != null) { + JsonObject jo = ctx.body().asJsonObject(); + if (jo != null) parameterValueList.put(k, jo); + } + } else if (parameterValueList.get(k) == null + && JsonArray.class.getName().equals(v.getRight().getName())) { + // 兜底: content-type 非 application/json 时尝试从 body 解析 JsonArray + if (ctx.body() != null) { + JsonArray ja = ctx.body().asJsonArray(); + if (ja != null) parameterValueList.put(k, ja); + } } }); // 调用handle 获取响应对象 Object[] parameterValueArray = parameterValueList.values().toArray(new Object[0]); - - // 打印调试信息,确认参数注入的情况 - if (LOGGER.isDebugEnabled() && method.getName().equals("donateAccount")) { - LOGGER.debug("donateAccount parameter list:"); - int i = 0; - for (Map.Entry 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 + + +