This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
package com.eactive.eai.rms.onl.apim.portalmenu;
|
package com.eactive.eai.rms.onl.apim.portalmenu;
|
||||||
|
|
||||||
|
import com.eactive.apim.portal.common.internal.InternalApiTokenService;
|
||||||
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
|
||||||
import org.apache.http.client.HttpClient;
|
import org.apache.http.client.HttpClient;
|
||||||
import org.apache.http.client.config.RequestConfig;
|
import org.apache.http.client.config.RequestConfig;
|
||||||
import org.apache.http.impl.client.HttpClientBuilder;
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
import org.apache.log4j.Logger;
|
import org.apache.log4j.Logger;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@@ -33,8 +37,12 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* (기본 {@code http://127.0.0.1:39130/internal/menu/reload}).
|
* (기본 {@code http://127.0.0.1:39130/internal/menu/reload}).
|
||||||
* 포탈이 다중 인스턴스로 기동되는 환경을 위해 <b>콤마/세미콜론/줄바꿈</b> 으로 여러 URL 을 나열할 수 있으며,
|
* 포탈이 다중 인스턴스로 기동되는 환경을 위해 <b>콤마/세미콜론/줄바꿈</b> 으로 여러 URL 을 나열할 수 있으며,
|
||||||
* 나열된 모든 서버로 병렬 호출한다. 각 호출의 connect/read 타임아웃은 {@value #TIMEOUT_MS}ms 이다.
|
* 나열된 모든 서버로 병렬 호출한다. 각 호출의 connect/read 타임아웃은 {@value #TIMEOUT_MS}ms 이다.
|
||||||
* portal 측은 {@code menu.internal.allow-ips} 허용 IP 목록으로 요청을 검증한다.
|
|
||||||
* 오류는 결과 Map 으로 감싸 화면에 서버별 사유를 표시한다 — 예외를 전파하지 않는다.</p>
|
* 오류는 결과 Map 으로 감싸 화면에 서버별 사유를 표시한다 — 예외를 전파하지 않는다.</p>
|
||||||
|
*
|
||||||
|
* <p>portal 측 검증은 두 겹이다: {@code menu.internal.allow-ips} 허용 IP 목록과
|
||||||
|
* {@link InternalApiTokenService} 공유 토큰 헤더. 토큰/헤더명은 PTL_PROPERTY 로 portal 과 공유하므로
|
||||||
|
* 이 클래스는 <b>읽기만</b> 한다(생성 주체는 portal). 값이 아직 없으면 헤더 없이 호출하게 되어 portal 이
|
||||||
|
* 401 로 거부하며, 이때는 portal 을 먼저 기동해 토큰을 생성해야 한다.</p>
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class PortalMenuCacheClient {
|
public class PortalMenuCacheClient {
|
||||||
@@ -55,16 +63,21 @@ public class PortalMenuCacheClient {
|
|||||||
private static final int MAX_PARALLELISM = 10;
|
private static final int MAX_PARALLELISM = 10;
|
||||||
|
|
||||||
private final PortalPropertyService portalPropertyService;
|
private final PortalPropertyService portalPropertyService;
|
||||||
|
private final InternalApiTokenService internalApiTokenService;
|
||||||
private final RestTemplate restTemplate;
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public PortalMenuCacheClient(PortalPropertyService portalPropertyService) {
|
public PortalMenuCacheClient(PortalPropertyService portalPropertyService,
|
||||||
this(portalPropertyService, createRestTemplateWithTimeout(TIMEOUT_MS));
|
InternalApiTokenService internalApiTokenService) {
|
||||||
|
this(portalPropertyService, internalApiTokenService, createRestTemplateWithTimeout(TIMEOUT_MS));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 테스트용 — 타임아웃이 설정된 RestTemplate 을 직접 주입한다. */
|
/** 테스트용 — 타임아웃이 설정된 RestTemplate 을 직접 주입한다. */
|
||||||
PortalMenuCacheClient(PortalPropertyService portalPropertyService, RestTemplate restTemplate) {
|
PortalMenuCacheClient(PortalPropertyService portalPropertyService,
|
||||||
|
InternalApiTokenService internalApiTokenService,
|
||||||
|
RestTemplate restTemplate) {
|
||||||
this.portalPropertyService = portalPropertyService;
|
this.portalPropertyService = portalPropertyService;
|
||||||
|
this.internalApiTokenService = internalApiTokenService;
|
||||||
this.restTemplate = restTemplate;
|
this.restTemplate = restTemplate;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +111,8 @@ public class PortalMenuCacheClient {
|
|||||||
*/
|
*/
|
||||||
public Map<String, Object> reload() {
|
public Map<String, Object> reload() {
|
||||||
List<String> urls = resolveUrls();
|
List<String> urls = resolveUrls();
|
||||||
List<Map<String, Object>> servers = callAll(urls);
|
// 토큰/헤더명 조회는 리로드 1회당 1번만 (서버 수만큼 DB 를 때리지 않는다)
|
||||||
|
List<Map<String, Object>> servers = callAll(urls, buildRequest());
|
||||||
|
|
||||||
int successCount = 0;
|
int successCount = 0;
|
||||||
for (Map<String, Object> server : servers) {
|
for (Map<String, Object> server : servers) {
|
||||||
@@ -140,13 +154,28 @@ public class PortalMenuCacheClient {
|
|||||||
return "포탈 메뉴 캐시 리로드 일부 실패 (" + successCount + "/" + total + " 서버 성공)";
|
return "포탈 메뉴 캐시 리로드 일부 실패 (" + successCount + "/" + total + " 서버 성공)";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 내부 API 인증 헤더를 실은 빈 본문 요청. 토큰이 아직 없으면 헤더 없이 나가고 포탈이 401 로 거부한다.
|
||||||
|
*/
|
||||||
|
private HttpEntity<Void> buildRequest() {
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
String token = internalApiTokenService.findToken();
|
||||||
|
if (token == null) {
|
||||||
|
logger.warn("포탈 내부 API 토큰 없음 - PTL_PROPERTY " + InternalApiTokenService.PROP_GROUP + "/"
|
||||||
|
+ InternalApiTokenService.PROP_TOKEN + " (포탈 최초 기동 시 자동 생성)");
|
||||||
|
} else {
|
||||||
|
headers.set(internalApiTokenService.findHeaderName(), token);
|
||||||
|
}
|
||||||
|
return new HttpEntity<Void>(headers);
|
||||||
|
}
|
||||||
|
|
||||||
/** 모든 URL 로 병렬 호출. 반환 순서는 {@code urls} 순서와 동일하다. */
|
/** 모든 URL 로 병렬 호출. 반환 순서는 {@code urls} 순서와 동일하다. */
|
||||||
private List<Map<String, Object>> callAll(List<String> urls) {
|
private List<Map<String, Object>> callAll(List<String> urls, final HttpEntity<Void> request) {
|
||||||
if (urls.isEmpty()) {
|
if (urls.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
if (urls.size() == 1) {
|
if (urls.size() == 1) {
|
||||||
return Collections.singletonList(callOne(urls.get(0)));
|
return Collections.singletonList(callOne(urls.get(0), request));
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Callable<Map<String, Object>>> tasks = new ArrayList<>(urls.size());
|
List<Callable<Map<String, Object>>> tasks = new ArrayList<>(urls.size());
|
||||||
@@ -154,7 +183,7 @@ public class PortalMenuCacheClient {
|
|||||||
tasks.add(new Callable<Map<String, Object>>() {
|
tasks.add(new Callable<Map<String, Object>>() {
|
||||||
@Override
|
@Override
|
||||||
public Map<String, Object> call() {
|
public Map<String, Object> call() {
|
||||||
return callOne(url);
|
return callOne(url, request);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -193,11 +222,11 @@ public class PortalMenuCacheClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 서버 1대 호출 — 예외를 전파하지 않고 실패 Map 으로 감싼다. */
|
/** 서버 1대 호출 — 예외를 전파하지 않고 실패 Map 으로 감싼다. */
|
||||||
private Map<String, Object> callOne(String url) {
|
private Map<String, Object> callOne(String url, HttpEntity<Void> request) {
|
||||||
long startedAt = System.currentTimeMillis();
|
long startedAt = System.currentTimeMillis();
|
||||||
try {
|
try {
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("rawtypes")
|
||||||
ResponseEntity<Map> response = restTemplate.postForEntity(url, null, Map.class);
|
ResponseEntity<Map> response = restTemplate.exchange(url, HttpMethod.POST, request, Map.class);
|
||||||
long elapsed = System.currentTimeMillis() - startedAt;
|
long elapsed = System.currentTimeMillis() - startedAt;
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
|
|||||||
Reference in New Issue
Block a user