DEFAULT spec 서버 치환 제거:
- GW/sentinel 치환 로직 삭제 및 serverRewriter 호출 제거 - mock path 분리/servers 덮어쓰기 로직 및 공통 유틸 추가
This commit is contained in:
+4
-4
@@ -71,11 +71,11 @@ public class DjbTestbedSpecController {
|
||||
*/
|
||||
private String buildSpecJson(String id, HttpServletRequest request, boolean alwaysGateway) throws IOException {
|
||||
if (DEFAULT_TOKEN_API_ID.equals(id)) {
|
||||
// 클래스패스 기본 토큰 spec 은 서버 치환 대상이 아니다. path 가 포탈 mock 토큰 경로
|
||||
// (PORTAL_MOCK_TOKEN_PATH)라 GW 호스트를 붙이면 실재하지 않는 주소가 되고, servers 를
|
||||
// 비워 두면 Swagger UI 가 문서 origin(포탈)을 사용해 프록시가 mock 토큰을 발급한다.
|
||||
Resource resource = new ClassPathResource(DEFAULT_SPEC_PATH);
|
||||
String content = new String(FileCopyUtils.copyToByteArray(resource.getInputStream()), StandardCharsets.UTF_8);
|
||||
return alwaysGateway
|
||||
? serverRewriter.rewriteServerToGateway(content, request)
|
||||
: serverRewriter.rewriteServer(content, null, request);
|
||||
return new String(FileCopyUtils.copyToByteArray(resource.getInputStream()), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
Optional<ApiSpecInfo> spec = apiSpecInfoService.findById(id);
|
||||
|
||||
+116
-35
@@ -2,7 +2,12 @@ package com.eactive.apim.portal.djb.testbed.service;
|
||||
|
||||
import com.eactive.apim.portal.apispec.entity.ApiSpecInfo;
|
||||
import com.eactive.apim.portal.djb.testbed.config.DjbTestbedGatewayProperty;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import java.net.URI;
|
||||
import java.util.Iterator;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -12,58 +17,62 @@ import org.yaml.snakeyaml.DumperOptions;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
/**
|
||||
* testbed spec(JSON)에 baking 된 서버 sentinel({@value #SERVER_SENTINEL})을
|
||||
* API SPEC 설정(responseType)에 따른 실주소로 치환한다.
|
||||
* testbed spec(JSON)의 서버 주소를 제공 시점에 실주소로 치환한다.
|
||||
*
|
||||
* <ul>
|
||||
* <li>gw : {@link DjbTestbedGatewayProperty#resolveApiBaseUrl}(GW base URL, 단일 기준)</li>
|
||||
* <li>mock : {@code ApiSpecInfo.mockUrl}</li>
|
||||
* <li>mock : {@code ApiSpecInfo.mockUrl} 을 origin/path 로 분해 — origin 은 서버, path 는 paths 키로
|
||||
* 교체해 어댑터/오퍼레이션 경로가 뒤에 덧붙지 않게 한다</li>
|
||||
* <li>sample(기본) : 요청 포탈 origin(scheme://host)</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>spec 의 서버주소는 Swagger UI path 표시 + cURL 스니펫용이다(실호출은 {@code /api/call-api} 프록시).
|
||||
* admin(app.js buildOpenApiSpec)은 env별 실주소를 저장시 굳히지 않고 sentinel 만 path 앞에 baking 하며,
|
||||
* 실제 치환은 이 서비스가 spec 제공 시점에 수행한다. 또한 동일 spec 을 YAML 로 변환 제공한다.
|
||||
* admin(app.js buildOpenApiSpec)은 저장 시 서버를 {@value #SERVER_SENTINEL} 로만 남기고 실주소를 굳히지
|
||||
* 않는다 — 저장본을 환경 독립으로 유지하기 위함이다. 실주소 결정은 이 서비스가 전담한다.
|
||||
*
|
||||
* <p>sentinel 문자열은 유일하므로 문자열 치환으로 처리한다(없으면 원본 유지, 멱등).
|
||||
* <p>치환은 <b>서버 값 유무와 무관하게</b> 트리 조작으로 덮어쓴다. 과거 admin 이 실주소를 굳혀 저장한
|
||||
* spec(sentinel 없음)도 현재 환경 기준으로 정정되며, 저장 환경(dev)의 GW 주소가 따라오지 않는다.
|
||||
* 다만 그 시절 mock 저장본은 {@code paths} 키가 이미 mock 경로로 바뀌어 있어 다운로드용
|
||||
* ({@link #rewriteServerToGateway}) 경로는 재저장 전까지 어댑터경로로 복원되지 않는다.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class DjbTestbedSpecServerRewriter {
|
||||
|
||||
/** admin(app.js buildOpenApiSpec)이 path 앞에 baking 하는 고정 서버 sentinel. */
|
||||
/** admin(app.js buildOpenApiSpec)이 저장본 서버에 남기는 고정 sentinel. */
|
||||
public static final String SERVER_SENTINEL = "http://swagger-server-url";
|
||||
|
||||
private final DjbTestbedGatewayProperty gatewayProperty;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/** sentinel → 응답유형별 실주소 치환한 spec JSON 반환. sentinel 없거나 실주소 미확정 시 원본 유지. */
|
||||
/** 응답유형(sample/mock/gw)별 실주소로 서버를 치환한 spec JSON 반환. */
|
||||
public String rewriteServer(String specJson, ApiSpecInfo spec, HttpServletRequest request) {
|
||||
if (specJson == null || !specJson.contains(SERVER_SENTINEL)) {
|
||||
return specJson;
|
||||
String responseType = (spec == null || !StringUtils.hasText(spec.getResponseType()))
|
||||
? "sample" : spec.getResponseType().trim();
|
||||
|
||||
if ("gw".equalsIgnoreCase(responseType)) {
|
||||
return applyServer(specJson, gatewayProperty.resolveApiBaseUrl(originOf(request)), null);
|
||||
}
|
||||
String base = stripTrailingSlash(resolveBase(spec, request));
|
||||
if (!StringUtils.hasText(base)) {
|
||||
return specJson; // 실주소 미확정 시 sentinel 유지
|
||||
if ("mock".equalsIgnoreCase(responseType)) {
|
||||
String mockUrl = (spec == null) ? null : spec.getMockUrl();
|
||||
if (!StringUtils.hasText(mockUrl)) {
|
||||
// mock 인데 주소 미입력 — 포탈 origin 으로 폴백(기존 동작)
|
||||
return applyServer(specJson, originOf(request), null);
|
||||
}
|
||||
String[] parts = splitFullUrl(mockUrl.trim());
|
||||
return applyServer(specJson, parts[0], parts[1]);
|
||||
}
|
||||
return specJson.replace(SERVER_SENTINEL, base);
|
||||
// sample(기본): 포탈 origin
|
||||
return applyServer(specJson, originOf(request), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* sentinel → GW 주소({@link DjbTestbedGatewayProperty#resolveApiBaseUrl}) 치환한 spec JSON 반환.
|
||||
* responseType 을 무시하고 항상 GW 기준으로 치환한다 — 외부 공개/다운로드용 spec(swagger.json/yaml)
|
||||
* 단일 기준. (Swagger UI 표시용은 {@link #rewriteServer} 의 responseType 분기를 그대로 사용.)
|
||||
* responseType 을 무시하고 항상 GW 주소로 치환한 spec JSON 반환 — 외부 공개/다운로드용
|
||||
* (swagger.json/yaml) 단일 기준. (Swagger UI 표시용은 {@link #rewriteServer} 의 응답유형 분기 사용.)
|
||||
*/
|
||||
public String rewriteServerToGateway(String specJson, HttpServletRequest request) {
|
||||
if (specJson == null || !specJson.contains(SERVER_SENTINEL)) {
|
||||
return specJson;
|
||||
}
|
||||
String base = stripTrailingSlash(gatewayProperty.resolveApiBaseUrl(originOf(request)));
|
||||
if (!StringUtils.hasText(base)) {
|
||||
return specJson; // 실주소 미확정 시 sentinel 유지
|
||||
}
|
||||
return specJson.replace(SERVER_SENTINEL, base);
|
||||
return applyServer(specJson, gatewayProperty.resolveApiBaseUrl(originOf(request)), null);
|
||||
}
|
||||
|
||||
/** spec JSON → YAML 문자열. 변환 실패 시 JSON 원본 반환. */
|
||||
@@ -80,18 +89,90 @@ public class DjbTestbedSpecServerRewriter {
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveBase(ApiSpecInfo spec, HttpServletRequest request) {
|
||||
String rt = (spec == null || !StringUtils.hasText(spec.getResponseType()))
|
||||
? "sample" : spec.getResponseType().trim();
|
||||
if ("gw".equalsIgnoreCase(rt)) {
|
||||
return gatewayProperty.resolveApiBaseUrl(originOf(request));
|
||||
/**
|
||||
* {@code servers} 를 지정 주소로 덮어쓰고, {@code newPathKey} 가 있으면 단일 path 키를 교체한다.
|
||||
*
|
||||
* @param serverUrl 서버 주소. 비어 있으면 {@code servers} 를 제거해 Swagger UI 가 문서 origin 을 쓰게 한다.
|
||||
* @param newPathKey mock 처럼 경로까지 치환해야 할 때의 새 path 키. null 이면 경로 유지.
|
||||
* @return 치환된 JSON. 파싱 실패·형태 불일치 시 원본 유지(멱등).
|
||||
*/
|
||||
private String applyServer(String specJson, String serverUrl, String newPathKey) {
|
||||
if (specJson == null) {
|
||||
return null;
|
||||
}
|
||||
if ("mock".equalsIgnoreCase(rt)) {
|
||||
String mock = (spec == null) ? null : spec.getMockUrl();
|
||||
return StringUtils.hasText(mock) ? mock.trim() : originOf(request);
|
||||
try {
|
||||
JsonNode parsed = objectMapper.readTree(specJson);
|
||||
if (!parsed.isObject()) {
|
||||
return specJson;
|
||||
}
|
||||
ObjectNode root = (ObjectNode) parsed;
|
||||
|
||||
String base = stripTrailingSlash(serverUrl);
|
||||
if (StringUtils.hasText(base)) {
|
||||
ArrayNode servers = objectMapper.createArrayNode();
|
||||
servers.add(objectMapper.createObjectNode().put("url", base));
|
||||
root.set("servers", servers);
|
||||
} else {
|
||||
root.remove("servers");
|
||||
}
|
||||
|
||||
if (newPathKey != null) {
|
||||
renameSinglePath(root, newPathKey);
|
||||
}
|
||||
return objectMapper.writeValueAsString(root);
|
||||
} catch (Exception e) {
|
||||
log.error("spec 서버 치환 실패, 원본 유지 - serverUrl={}, newPathKey={}", serverUrl, newPathKey, e);
|
||||
return specJson;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code paths} 의 유일한 키를 {@code newPathKey} 로 교체한다.
|
||||
*
|
||||
* <p>testbed spec 은 오퍼레이션 1건(= path 1건) 기준으로 생성되므로 단일 키만 다룬다.
|
||||
* 키가 없거나 2개 이상이면 어느 것을 mock 경로에 대응시킬지 알 수 없어 원본을 유지한다.</p>
|
||||
*/
|
||||
private void renameSinglePath(ObjectNode root, String newPathKey) {
|
||||
JsonNode paths = root.get("paths");
|
||||
if (paths == null || !paths.isObject()) {
|
||||
return;
|
||||
}
|
||||
ObjectNode pathsNode = (ObjectNode) paths;
|
||||
if (pathsNode.size() != 1) {
|
||||
log.warn("paths 가 단일 키가 아니어서 mock 경로 치환 생략 - size={}", pathsNode.size());
|
||||
return;
|
||||
}
|
||||
Iterator<String> names = pathsNode.fieldNames();
|
||||
String oldKey = names.next();
|
||||
if (oldKey.equals(newPathKey)) {
|
||||
return;
|
||||
}
|
||||
JsonNode pathItem = pathsNode.get(oldKey);
|
||||
pathsNode.remove(oldKey);
|
||||
pathsNode.set(newPathKey, pathItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* 전체 URL → {@code [origin, path]}. mock 주소를 서버/경로로 분해해 경로가 중복 부착되지 않게 한다.
|
||||
* 파싱 실패(스킴 누락 등)면 통째로 origin 취급하고 경로는 {@code "/"}.
|
||||
*/
|
||||
private static String[] splitFullUrl(String url) {
|
||||
try {
|
||||
URI uri = new URI(url);
|
||||
if (uri.getScheme() == null || uri.getHost() == null) {
|
||||
return new String[]{stripTrailingSlash(url), "/"};
|
||||
}
|
||||
String origin = uri.getScheme() + "://" + uri.getHost()
|
||||
+ (uri.getPort() < 0 ? "" : ":" + uri.getPort());
|
||||
String path = StringUtils.hasText(uri.getRawPath()) ? uri.getRawPath() : "/";
|
||||
if (StringUtils.hasText(uri.getRawQuery())) {
|
||||
path = path + "?" + uri.getRawQuery();
|
||||
}
|
||||
return new String[]{origin, path};
|
||||
} catch (Exception e) {
|
||||
log.warn("mock URL 파싱 실패, 통째로 서버 취급 - url={}, cause={}", url, e.getMessage());
|
||||
return new String[]{stripTrailingSlash(url), "/"};
|
||||
}
|
||||
// sample(기본): 포탈 origin
|
||||
return originOf(request);
|
||||
}
|
||||
|
||||
/** 요청 기준 포탈 origin(scheme://host[:port]) — 리버스 프록시 X-Forwarded-* 우선. */
|
||||
|
||||
Reference in New Issue
Block a user