T0.12: GatewayProperties 路由表 + TargetRouter(/{target}/oapi/** 路由判定与 forwardUrl 换主机不换路径计算)
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package com.yunhai.platform.gateway.route;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 网关配置(T0.12 起;环境值走平台配置层,jar 内 yml 只留占位)。
|
||||
* <ul>
|
||||
* <li>routes:target → base-url(容器网内服务直连 URL,如 http://userbase-service:8080;
|
||||
* target = F11 七个生产 ctx);</li>
|
||||
* <li>observe:观察模式(T1.6 消费;默认 true = 只记不拦,R4)。</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "yh.gateway")
|
||||
public class GatewayProperties {
|
||||
|
||||
/** target → base-url(值不含路径;转发 = base-url + 完整原始 URI,换主机不换路径) */
|
||||
private Map<String, String> routes = new LinkedHashMap<>();
|
||||
|
||||
/** 观察模式:true = 全判定只记不拦(P1 只观察,R4) */
|
||||
private boolean observe = true;
|
||||
|
||||
public Map<String, String> getRoutes() {
|
||||
return routes;
|
||||
}
|
||||
|
||||
public void setRoutes(Map<String, String> routes) {
|
||||
this.routes = routes;
|
||||
}
|
||||
|
||||
public boolean isObserve() {
|
||||
return observe;
|
||||
}
|
||||
|
||||
public void setObserve(boolean observe) {
|
||||
this.observe = observe;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.yunhai.platform.gateway.route;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 目标路由与转发 URL 计算(T0.12):{@code /{target}/oapi/**} → 提供方 base-url 的映射。
|
||||
* <p>
|
||||
* <b>换主机不换路径</b>:forwardUrl = base-url + 完整原始 URI(含 query)。target 首段仅用于
|
||||
* 路由选择、注册表匹配与 canonical 剥离,不参与转发路径改写——提供方自带 context-path,
|
||||
* URI 原样送达即天然对齐,签名 canonical 逐字节不变。<br>
|
||||
* 网关只做 oapi:未知 target / 单段 URI / 第二段非 oapi(如 /{target}/api/x)一律返回 null
|
||||
* = <b>裸 404</b>({@code /api} 与页面不属于网关职责)。
|
||||
*/
|
||||
@Component
|
||||
public class TargetRouter {
|
||||
|
||||
/** 路由产物:判定字段 + 转发 URL */
|
||||
public static class Routing {
|
||||
public final String target;
|
||||
/** 剥掉 target 首段后的路径(如 /oapi/message/list)——RouteRegistry 与签名 canonical 同一坐标系 */
|
||||
public final String pathAfterStrip;
|
||||
/** base-url + 完整原始 URI(含 query),逐字符不改写 */
|
||||
public final String forwardUrl;
|
||||
|
||||
Routing(String target, String pathAfterStrip, String forwardUrl) {
|
||||
this.target = target;
|
||||
this.pathAfterStrip = pathAfterStrip;
|
||||
this.forwardUrl = forwardUrl;
|
||||
}
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private GatewayProperties properties;
|
||||
|
||||
/** 测试/显式装配入口 */
|
||||
void setProperties(GatewayProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param requestUri 原始请求 URI(未解码,如 /userbase/oapi/x)
|
||||
* @param queryString 原始 query(无 '?' 前缀;null = 无)
|
||||
* @return 路由产物;不匹配(未知 target / 单段 / 非 oapi 第二段)返回 null = 裸 404
|
||||
*/
|
||||
public Routing route(String requestUri, String queryString) {
|
||||
if (requestUri == null || requestUri.isEmpty() || requestUri.equals("/")) {
|
||||
return null;
|
||||
}
|
||||
// 剥首段 target:/userbase/oapi/x → target=userbase, rest=/oapi/x
|
||||
int secondSlash = requestUri.indexOf('/', 1);
|
||||
String target;
|
||||
String rest;
|
||||
if (secondSlash < 0) {
|
||||
return null; // 单段 URI(/foo)——无 oapi 第二段
|
||||
} else {
|
||||
target = requestUri.substring(1, secondSlash);
|
||||
rest = requestUri.substring(secondSlash);
|
||||
}
|
||||
if (target.isEmpty()) {
|
||||
return null; // 双斜杠开头等畸形
|
||||
}
|
||||
// 网关只做 oapi:第二段必须恰为 oapi(/oapi、/oapi/xxx 均可)
|
||||
if (!rest.equals("/oapi") && !rest.startsWith("/oapi/")) {
|
||||
return null;
|
||||
}
|
||||
String baseUrl = properties.getRoutes().get(target);
|
||||
if (baseUrl == null || baseUrl.trim().isEmpty()) {
|
||||
return null; // 未知 target = 裸 404
|
||||
}
|
||||
String normalizedBase = baseUrl.endsWith("/")
|
||||
? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
String url = normalizedBase + requestUri
|
||||
+ (queryString == null || queryString.isEmpty() ? "" : "?" + queryString);
|
||||
return new Routing(target, rest, url);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.yunhai.platform.gateway.route;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* TargetRouter 用例(T0.12 TDD):已知 target forwardUrl 逐字符断言(换主机不换路径)、
|
||||
* 未知 target / 单段 / 非 oapi 第二段一律裸 404(null)、base-url 尾斜杠归一化。
|
||||
*/
|
||||
class TargetRouterTest {
|
||||
|
||||
private TargetRouter router;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
router = new TargetRouter();
|
||||
GatewayProperties props = new GatewayProperties();
|
||||
Map<String, String> routes = new LinkedHashMap<>();
|
||||
routes.put("userbase", "http://userbase-service:8080");
|
||||
routes.put("notice", "http://notice-service:8080/");
|
||||
props.setRoutes(routes);
|
||||
router.setProperties(props);
|
||||
}
|
||||
|
||||
@Test
|
||||
void 已知target_forwardUrl逐字符断言() {
|
||||
TargetRouter.Routing r = router.route("/userbase/oapi/x", "a=1");
|
||||
assertThat(r).isNotNull();
|
||||
assertThat(r.forwardUrl).as("换主机不换路径:base-url + 完整原始 URI 含 query,逐字符")
|
||||
.isEqualTo("http://userbase-service:8080/userbase/oapi/x?a=1");
|
||||
assertThat(r.target).isEqualTo("userbase");
|
||||
assertThat(r.pathAfterStrip).as("剥段路径 = 注册表/签名坐标系").isEqualTo("/oapi/x");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 无query的URI不带问号() {
|
||||
TargetRouter.Routing r = router.route("/notice/oapi/notice/list", null);
|
||||
assertThat(r.forwardUrl).isEqualTo("http://notice-service:8080/notice/oapi/notice/list");
|
||||
assertThat(r.pathAfterStrip).isEqualTo("/oapi/notice/list");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 未知target_裸404() {
|
||||
assertThat(router.route("/nobody/oapi/x", null)).as("未配置的 target = 裸 404").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 单段URI_裸404() {
|
||||
assertThat(router.route("/", null)).isNull();
|
||||
assertThat(router.route("/foo", null)).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 非oapi第二段_裸404() {
|
||||
assertThat(router.route("/userbase/api/x", null)).as("网关只做 oapi,/api 一律 404").isNull();
|
||||
assertThat(router.route("/userbase/oapix", null)).as("前缀相同不算 oapi 段").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void 尾斜杠base归一化_无双斜杠() {
|
||||
TargetRouter.Routing r = router.route("/notice/oapi/x", null);
|
||||
assertThat(r.forwardUrl).as("base-url 尾斜杠不得产生双斜杠")
|
||||
.isEqualTo("http://notice-service:8080/notice/oapi/x");
|
||||
}
|
||||
|
||||
@Test
|
||||
void 裸oapi路径可路由() {
|
||||
// /{target}/oapi(无尾路径)属 /{target}/oapi/** 匹配范围
|
||||
TargetRouter.Routing r = router.route("/userbase/oapi", null);
|
||||
assertThat(r).isNotNull();
|
||||
assertThat(r.forwardUrl).isEqualTo("http://userbase-service:8080/userbase/oapi");
|
||||
assertThat(r.pathAfterStrip).isEqualTo("/oapi");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user