mirror of
https://github.com/qaiu/netdisk-fast-download.git
synced 2026-04-22 08:36:54 +00:00
Compare commits
3 Commits
copilot/fi
...
v3.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2161190d9a | ||
|
|
aaae301cbc | ||
|
|
9ca6511235 |
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"java.compile.nullAnalysis.mode": "automatic",
|
||||
"java.configuration.updateBuildConfiguration": "interactive"
|
||||
"java.configuration.updateBuildConfiguration": "interactive",
|
||||
"java.debug.settings.onBuildFailureProceed": true
|
||||
}
|
||||
@@ -73,6 +73,12 @@
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -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<JsonObject> 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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,6 +9,7 @@ import java.lang.annotation.*;
|
||||
public @interface HandleSortFilter {
|
||||
/**
|
||||
* 注册顺序,数字越大越先注册<br>
|
||||
* 前置拦截器会先执行后注册即数字小的, 后置拦截器会先执行先注册的即数字大的<br>
|
||||
* 值<0时会过滤掉该处理器
|
||||
*/
|
||||
int value() default 0;
|
||||
|
||||
12
core/src/main/java/cn/qaiu/vx/core/base/AppRun.java
Normal file
12
core/src/main/java/cn/qaiu/vx/core/base/AppRun.java
Normal file
@@ -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);
|
||||
}
|
||||
@@ -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 <T> void doFireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult, int statusCode) {
|
||||
if (!ctx.response().ended()) {
|
||||
fireJsonResultResponse(ctx, jsonResult, statusCode);
|
||||
}
|
||||
handleAfterInterceptor(ctx, jsonResult.toJsonObject());
|
||||
}
|
||||
|
||||
default Set<AfterInterceptor> getAfterInterceptor() {
|
||||
|
||||
|
||||
23
core/src/main/java/cn/qaiu/vx/core/base/DefaultAppRun.java
Normal file
23
core/src/main/java/cn/qaiu/vx/core/base/DefaultAppRun.java
Normal file
@@ -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实现示例
|
||||
* <br>Create date 2024-01-01 00:00:00
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
@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());
|
||||
}
|
||||
}
|
||||
@@ -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<Handler<RoutingContext>> 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<Handler<RoutingContext>> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 前置拦截器接口
|
||||
* <p>
|
||||
* 注意:Vert.x是异步非阻塞框架,不能在Event Loop中使用synchronized等阻塞操作!
|
||||
* 所有操作都应该是非阻塞的,使用Vert.x的上下文数据存储机制保证线程安全。
|
||||
* </p>
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
@@ -14,28 +16,25 @@ public interface BeforeInterceptor extends Handler<RoutingContext> {
|
||||
String IS_NEXT = "RoutingContextIsNext";
|
||||
|
||||
default Handler<RoutingContext> 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); // 实现具体的拦截处理逻辑
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class JsonResult<T> implements Serializable {
|
||||
|
||||
private int code = SUCCESS_CODE;//状态码
|
||||
|
||||
private String msg = SUCCESS_MESSAGE; //消息
|
||||
private String msg = SUCCESS_MESSAGE;//消息
|
||||
|
||||
private boolean success = true; //是否成功
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 异步读取配置工具类
|
||||
* <br>Create date 2021/9/2 1:23
|
||||
@@ -24,7 +28,29 @@ public class ConfigUtil {
|
||||
* @return JsonObject的Future
|
||||
*/
|
||||
public static Future<JsonObject> readConfig(String format, String path, Vertx vertx) {
|
||||
// 读取yml配置
|
||||
// 支持 classpath: 前缀从类路径读取,否则从文件系统读取
|
||||
if (path != null && path.startsWith("classpath:")) {
|
||||
String resource = path.substring("classpath:".length());
|
||||
// 使用 executeBlocking(Callable) 直接返回 Future<JsonObject>
|
||||
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<JsonObject> 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配置文件
|
||||
*
|
||||
|
||||
20
core/src/main/java/cn/qaiu/vx/core/util/FutureUtils.java
Normal file
20
core/src/main/java/cn/qaiu/vx/core/util/FutureUtils.java
Normal file
@@ -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> T getResult(Future<T> future) {
|
||||
try {
|
||||
return future.toCompletionStage().toCompletableFuture().get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
public static <T> T getResult(Promise<T> promise) {
|
||||
return promise.future().toCompletionStage().toCompletableFuture().join();
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
* Create at 2023/10/14 9:07
|
||||
* @date 2023/10/14 9:07
|
||||
*/
|
||||
public class JacksonConfig {
|
||||
|
||||
|
||||
@@ -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<String, Reflections> 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<String> 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<String> 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<String> packageAddresses) {
|
||||
ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
|
||||
FilterBuilder filterBuilder = new FilterBuilder();
|
||||
packageAddresses.forEach(str -> {
|
||||
Collection<URL> 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<String> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <T> void fireJsonResultResponse(RoutingContext ctx, JsonResult<T> jsonResult, int statusCode) {
|
||||
fireJsonObjectResponse(ctx, jsonResult.toJsonObject(), statusCode);
|
||||
}
|
||||
|
||||
public static <T> void fireJsonResultResponse(HttpServerResponse ctx, JsonResult<T> jsonResult) {
|
||||
fireJsonObjectResponse(ctx, jsonResult.toJsonObject());
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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实现
|
||||
* <br>Create date 2024-01-01 00:00:00
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
*/
|
||||
public class PostExecVerticle extends AbstractVerticle {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(PostExecVerticle.class);
|
||||
private static final Set<AppRun> appRunImplementations;
|
||||
private static final AtomicBoolean lock = new AtomicBoolean(false);
|
||||
|
||||
static {
|
||||
Reflections reflections = ReflectionUtil.getReflections();
|
||||
Set<Class<? extends AppRun>> 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<Void> 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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* <p>反向代理服务</p>
|
||||
* <p>可以根据配置文件自动生成代理服务</p>
|
||||
* <p>可以配置多个服务, 配置文件见示例</p>
|
||||
* <p>【优化】支持高并发场景,连接池复用,避免线程阻塞</p>
|
||||
* <br>Create date 2021/9/2 0:41
|
||||
*
|
||||
* @author <a href="https://qaiu.top">QAIU</a>
|
||||
@@ -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<String, HttpClient> 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<Void> 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<Void> 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<String> 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<ProxyResponse> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -29,20 +29,23 @@ public class ServiceVerticle extends AbstractVerticle {
|
||||
Reflections reflections = ReflectionUtil.getReflections();
|
||||
handlers = reflections.getTypesAnnotatedWith(Service.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start(Promise<Void> 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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,2 @@
|
||||
app.version=${project.version}
|
||||
build=${maven.build.timestamp}
|
||||
build=${build.timestamp}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.<clinit> 需要 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();
|
||||
}
|
||||
}
|
||||
36
core/src/test/java/cn/qaiu/vx/core/test/TestJsonHandler.java
Normal file
36
core/src/test/java/cn/qaiu/vx/core/test/TestJsonHandler.java
Normal file
@@ -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<JsonResult> 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<JsonResult> 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)));
|
||||
}
|
||||
}
|
||||
@@ -1,105 +1,105 @@
|
||||
// ==UserScript==
|
||||
// @name Fetch API示例解析器
|
||||
// @type fetch_demo
|
||||
// @displayName Fetch演示
|
||||
// @description 演示如何在ES5环境中使用fetch API和async/await
|
||||
// @match https?://example\.com/s/(?<KEY>\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/(?<KEY>\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);
|
||||
// });
|
||||
// }
|
||||
|
||||
54
web-service/src/main/resources/server-proxy-local.yml
Normal file
54
web-service/src/main/resources/server-proxy-local.yml
Normal file
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user