Merge branch 'master' into feature/crypto-module
This commit is contained in:
@@ -0,0 +1,319 @@
|
||||
package com.eactive.eai.adapter.handler;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Properties;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.message.StandardItem;
|
||||
import com.eactive.eai.message.StandardMessage;
|
||||
|
||||
/**
|
||||
* 템플릿 기반 어댑터 에러 메시지 핸들러.
|
||||
*
|
||||
* [프로퍼티 설정]
|
||||
* AdapterPropManager 의 "AdapterErrorMessageHandler" 프로퍼티 그룹에서
|
||||
* "{adapterGroupId}.template" 키로 등록된 템플릿 문자열을 읽어
|
||||
* StandardMessage 의 필드 값으로 치환한 결과를 반환한다.
|
||||
*
|
||||
* [변수 치환 규칙]
|
||||
* 1. "${callprop.키}" → callProp.getProperty("키") 로 치환
|
||||
* "${callprop.키:기본값}" → 키 없거나 callProp null 이면 기본값 반환
|
||||
* 2. "${callprop[경로]}" → StandardMessage 경로 값을 키로 callProp 간접 조회
|
||||
* "${callprop[경로]:기본값}" → 키 결정 실패 또는 키 없으면 기본값 반환
|
||||
* 3. "${경로}" → StandardMessage.findItemValue("경로") 로 치환
|
||||
* "${경로:기본값}" → 경로 값이 null/공백이면 기본값 반환
|
||||
* 모든 형태에서 기본값 미지정 시 빈 문자열을 반환한다.
|
||||
*
|
||||
* [배열 반복 문법] (MSG_LIST 등 GRID 타입)
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* { "code":"${outp_msg_cd}", "msg":"${outp_msg_ctnt}" }
|
||||
* {{/foreach}}
|
||||
* → MSG_LIST 각 행을 반복하며 행 내 필드명(접두어 없이)으로 치환,
|
||||
* 반복 결과를 쉼표(,)로 이어 붙임
|
||||
*
|
||||
* [사용 예 – JSON 템플릿]
|
||||
* {
|
||||
* "adapterName": "${callprop.ADAPTER_NAME}",
|
||||
* "resultCode": "${MSG.MAIN_MSG.outp_msg_cd}",
|
||||
* "resultMsg": "${MSG.MAIN_MSG.outp_msg_ctnt}",
|
||||
* "errors": [
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* {
|
||||
* "code": "${outp_msg_cd}",
|
||||
* "msg": "${outp_msg_ctnt}",
|
||||
* "desc": "${outp_msg_desc}",
|
||||
* "field": "${err_occu_item_nm}"
|
||||
* }
|
||||
* {{/foreach}}
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* [사용 예 – XML 템플릿]
|
||||
* <error>
|
||||
* <adapter>${callprop.ADAPTER_NAME}</adapter>
|
||||
* <code>${MSG.MAIN_MSG.outp_msg_cd}</code>
|
||||
* <errors>
|
||||
* {{#foreach MSG.MSG_LIST}}
|
||||
* <item><code>${outp_msg_cd}</code><msg>${outp_msg_ctnt}</msg></item>
|
||||
* {{/foreach}}
|
||||
* </errors>
|
||||
* </error>
|
||||
*
|
||||
* [callProp 중첩 Properties 참조]
|
||||
* callProp 의 값이 Properties 인 경우 점(.) 으로 체인 탐색이 가능하다.
|
||||
* callProp.put("OUTBOUND", subProps) // subProps.setProperty("IF_ID", "SVC001")
|
||||
* → 템플릿에서 ${callprop.OUTBOUND.IF_ID} 로 참조
|
||||
* 중간 값이 Properties 가 아닌 경우 전체 keyPath 를 키로 폴백 조회한다.
|
||||
*/
|
||||
public class TemplateAdapterErrorMsgHandler implements AdapterErrorMessageHandler {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_ADAPTER);
|
||||
|
||||
/** AdapterPropManager 에서 템플릿을 조회할 프로퍼티 그룹 이름 */
|
||||
static final String PROP_GROUP = "AdapterErrorMessageHandler";
|
||||
|
||||
/** callProp 참조 변수 접두어: ${callprop.키} */
|
||||
static final String CALLPROP_PREFIX = "callprop.";
|
||||
|
||||
/** callProp 간접 참조 접두어: ${callprop[표준전문경로]} */
|
||||
static final String CALLPROP_INDIRECT_PREFIX = "callprop[";
|
||||
|
||||
/** ${path} 또는 ${callprop.key} 스칼라 변수 패턴 */
|
||||
private static final Pattern VAR_PATTERN =
|
||||
Pattern.compile("\\$\\{([^}]+)\\}");
|
||||
|
||||
/** {{#foreach path}}...{{/foreach}} 배열 반복 블록 패턴 */
|
||||
private static final Pattern FOREACH_PATTERN =
|
||||
Pattern.compile("\\{\\{#foreach\\s+([^}]+)\\}\\}(.*?)\\{\\{/foreach\\}\\}",
|
||||
Pattern.DOTALL);
|
||||
|
||||
@Override
|
||||
public Object generateNonStandardErrorResponseMessage(
|
||||
String inboundAdapterGroupName,
|
||||
String inboundAdapterName,
|
||||
Properties callProp,
|
||||
Object outboundRequestData,
|
||||
StandardMessage resStandardMessage) throws Exception {
|
||||
|
||||
String templateKey = inboundAdapterGroupName + ".template";
|
||||
String template = PropManager.getInstance().getProperty(PROP_GROUP, templateKey);
|
||||
|
||||
if (StringUtils.isBlank(template)) {
|
||||
if (logger.isWarn()) {
|
||||
logger.warn("TemplateAdapterErrorMsgHandler] template not found. group=" + PROP_GROUP
|
||||
+ ", key=" + templateKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return render(template, resStandardMessage, callProp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 템플릿에 StandardMessage 및 callProp 값을 치환해 최종 문자열을 반환한다.
|
||||
* package-private: 단위 테스트에서 직접 호출 가능
|
||||
*/
|
||||
String render(String template, StandardMessage msg, Properties callProp) {
|
||||
// 1단계: {{#foreach path}}...{{/foreach}} 배열 반복 처리
|
||||
StringBuffer sb = new StringBuffer();
|
||||
Matcher foreachMatcher = FOREACH_PATTERN.matcher(template);
|
||||
while (foreachMatcher.find()) {
|
||||
String arrayPath = foreachMatcher.group(1).trim();
|
||||
String blockContent = foreachMatcher.group(2);
|
||||
String rendered = renderForeach(arrayPath, blockContent, msg);
|
||||
foreachMatcher.appendReplacement(sb, Matcher.quoteReplacement(rendered));
|
||||
}
|
||||
foreachMatcher.appendTail(sb);
|
||||
|
||||
// 2단계: 나머지 ${path} / ${callprop.키} 스칼라 변수 치환
|
||||
String intermediate = sb.toString();
|
||||
StringBuffer result = new StringBuffer();
|
||||
Matcher varMatcher = VAR_PATTERN.matcher(intermediate);
|
||||
while (varMatcher.find()) {
|
||||
String expr = varMatcher.group(1).trim();
|
||||
String value = resolveScalar(expr, msg, callProp);
|
||||
varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
|
||||
}
|
||||
varMatcher.appendTail(result);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 변수 표현식을 해석해 값을 반환한다.
|
||||
* 모든 형태에서 ':기본값' 접미사를 지원한다. 기본값 미지정 시 빈 문자열.
|
||||
* - "callprop[경로][:기본값]" → 표준전문 경로 값을 키로 callProp 간접 조회
|
||||
* - "callprop.키[:기본값]" → callProp 직접/중첩 조회
|
||||
* - "경로[:기본값]" → StandardMessage.findItemValue(경로)
|
||||
*/
|
||||
private String resolveScalar(String expr, StandardMessage msg, Properties callProp) {
|
||||
if (expr.startsWith(CALLPROP_INDIRECT_PREFIX)) {
|
||||
return resolveIndirectCallProp(expr, msg, callProp);
|
||||
}
|
||||
if (expr.startsWith(CALLPROP_PREFIX)) {
|
||||
String rest = expr.substring(CALLPROP_PREFIX.length());
|
||||
int colonIdx = rest.indexOf(':');
|
||||
String keyPath = colonIdx >= 0 ? rest.substring(0, colonIdx) : rest;
|
||||
String defaultVal = colonIdx >= 0 ? rest.substring(colonIdx + 1) : "";
|
||||
if (callProp == null) return defaultVal;
|
||||
String value = resolveCallProp(callProp, keyPath);
|
||||
return value != null ? value : defaultVal;
|
||||
}
|
||||
int colonIdx = expr.indexOf(':');
|
||||
String path = colonIdx >= 0 ? expr.substring(0, colonIdx) : expr;
|
||||
String defaultVal = colonIdx >= 0 ? expr.substring(colonIdx + 1) : "";
|
||||
String value = resolveMessagePath(msg, path);
|
||||
return StringUtils.isNotEmpty(value) ? value : defaultVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* StandardMessage 경로를 해석한다.
|
||||
* 1. findItemValue(path) 로 직접 조회한다.
|
||||
* 2. null 이면 경로를 뒤에서부터 분리하며, 부모 경로 값이 JSON 문자열인 경우
|
||||
* 나머지 경로(subPath)를 키로 JSON 필드를 추출한다.
|
||||
* 예) DATA.BIZDATA.acctNo → findItemValue("DATA.BIZDATA") → JSON 파싱 → acctNo
|
||||
* DATA.BIZDATA.addr.city → findItemValue("DATA.BIZDATA") → JSON 파싱 → addr.city
|
||||
*/
|
||||
private String resolveMessagePath(StandardMessage msg, String path) {
|
||||
String value = msg.findItemValue(path);
|
||||
if (value != null) return value;
|
||||
|
||||
int dotIdx = path.lastIndexOf('.');
|
||||
while (dotIdx > 0) {
|
||||
String parentPath = path.substring(0, dotIdx);
|
||||
String subPath = path.substring(dotIdx + 1);
|
||||
String parentVal = msg.findItemValue(parentPath);
|
||||
if (StringUtils.isNotBlank(parentVal)) {
|
||||
String extracted = extractFromJson(parentVal, subPath);
|
||||
if (extracted != null) return extracted;
|
||||
}
|
||||
dotIdx = parentPath.lastIndexOf('.');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 문자열에서 dot 구분 경로로 필드 값을 추출한다.
|
||||
* 중간 경로가 JSON 오브젝트면 계속 탐색하고, 최종 값이 primitive 면 문자열로,
|
||||
* 오브젝트/배열이면 JSON 문자열 그대로 반환한다. 파싱 실패 시 null 반환.
|
||||
*/
|
||||
private String extractFromJson(String json, String fieldPath) {
|
||||
try {
|
||||
JsonElement el = new JsonParser().parse(json);
|
||||
for (String part : fieldPath.split("\\.", -1)) {
|
||||
if (!el.isJsonObject()) return null;
|
||||
el = el.getAsJsonObject().get(part);
|
||||
if (el == null) return null;
|
||||
}
|
||||
return el.isJsonPrimitive() ? el.getAsString() : el.toString();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ${callprop[경로]} 또는 ${callprop[경로]:기본값} 처리.
|
||||
* 1. 표준전문 경로로 callProp 키를 동적으로 결정한다.
|
||||
* 2. 결정된 키로 callProp 에서 값을 조회한다.
|
||||
* 3. 키 또는 값을 찾지 못하면 기본값(없으면 빈 문자열)을 반환한다.
|
||||
*/
|
||||
private String resolveIndirectCallProp(String expr, StandardMessage msg, Properties callProp) {
|
||||
int closeIdx = expr.indexOf(']');
|
||||
if (closeIdx < 0) return "";
|
||||
|
||||
String msgPath = expr.substring(CALLPROP_INDIRECT_PREFIX.length(), closeIdx);
|
||||
String defaultVal = (closeIdx + 1 < expr.length() && expr.charAt(closeIdx + 1) == ':')
|
||||
? expr.substring(closeIdx + 2)
|
||||
: "";
|
||||
|
||||
String key = msg.findItemValue(msgPath);
|
||||
if (StringUtils.isBlank(key)) return defaultVal;
|
||||
if (callProp == null) return defaultVal;
|
||||
|
||||
String value = resolveCallProp(callProp, key);
|
||||
return value != null ? value : defaultVal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties 체인을 점(.) 구분자로 재귀 탐색한다.
|
||||
* - 첫 세그먼트의 값이 Properties 이면 나머지 경로로 재귀
|
||||
* - String 이면 반환, 그 외 Object 이면 toString() 반환
|
||||
* - 첫 세그먼트 값이 Properties 가 아닌 경우 전체 keyPath 로 폴백 조회
|
||||
*/
|
||||
private String resolveCallProp(Properties props, String keyPath) {
|
||||
int dotIdx = keyPath.indexOf('.');
|
||||
if (dotIdx < 0) {
|
||||
Object val = props.get(keyPath);
|
||||
if (val == null) return null;
|
||||
if (val instanceof String) return (String) val;
|
||||
return val.toString();
|
||||
}
|
||||
|
||||
String first = keyPath.substring(0, dotIdx);
|
||||
String rest = keyPath.substring(dotIdx + 1);
|
||||
Object val = props.get(first);
|
||||
if (val instanceof Properties) {
|
||||
return resolveCallProp((Properties) val, rest);
|
||||
}
|
||||
|
||||
// 중간 값이 Properties 가 아닌 경우: 전체 keyPath 를 키로 폴백
|
||||
Object direct = props.get(keyPath);
|
||||
if (direct == null) return null;
|
||||
if (direct instanceof String) return (String) direct;
|
||||
return direct.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* GRID 타입 배열 아이템을 반복하며 blockContent 를 각 행으로 치환,
|
||||
* 결과를 쉼표로 이어 반환한다. (foreach 블록 내부는 callProp 참조 미지원)
|
||||
*/
|
||||
private String renderForeach(String arrayPath, String blockContent, StandardMessage msg) {
|
||||
StandardItem arrayItem = msg.findItem(arrayPath);
|
||||
if (arrayItem == null) {
|
||||
if (logger.isWarn()) logger.warn("TemplateAdapterErrorMsgHandler] foreach path not found: " + arrayPath);
|
||||
return "";
|
||||
}
|
||||
|
||||
List<LinkedHashMap<String, StandardItem>> rows = arrayItem.getList();
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
List<String> renderedItems = new ArrayList<>();
|
||||
for (LinkedHashMap<String, StandardItem> row : rows) {
|
||||
String rowText = renderRow(blockContent, row);
|
||||
if (StringUtils.isNotBlank(rowText)) {
|
||||
renderedItems.add(rowText);
|
||||
}
|
||||
}
|
||||
return String.join(",", renderedItems);
|
||||
}
|
||||
|
||||
/**
|
||||
* 배열 한 행(LinkedHashMap) 기준으로 blockContent 의 ${fieldName} 을 치환한다.
|
||||
* fieldName 은 경로 없이 해당 행의 컬럼명만 사용한다.
|
||||
*/
|
||||
private String renderRow(String blockContent, LinkedHashMap<String, StandardItem> row) {
|
||||
StringBuffer result = new StringBuffer();
|
||||
Matcher varMatcher = VAR_PATTERN.matcher(blockContent);
|
||||
while (varMatcher.find()) {
|
||||
String fieldName = varMatcher.group(1).trim();
|
||||
String value = "";
|
||||
StandardItem item = row.get(fieldName);
|
||||
if (item != null) {
|
||||
value = StringUtils.defaultString(item.getValue());
|
||||
}
|
||||
varMatcher.appendReplacement(result, Matcher.quoteReplacement(value));
|
||||
}
|
||||
varMatcher.appendTail(result);
|
||||
return result.toString().trim();
|
||||
}
|
||||
}
|
||||
+8
@@ -40,7 +40,9 @@ import com.eactive.eai.adapter.AdapterManager;
|
||||
import com.eactive.eai.adapter.Keys;
|
||||
import com.eactive.eai.adapter.http.secure.HttpClient5SSLContextFactory;
|
||||
import com.eactive.eai.common.TransactionContextKeys;
|
||||
import com.eactive.eai.common.message.EAIMessageKeys;
|
||||
import com.eactive.eai.common.property.PropManager;
|
||||
import com.eactive.eai.common.server.EAIServerManager;
|
||||
import com.eactive.eai.common.util.HttpAdapterExtraLogUtil;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
import com.eactive.eai.httpouttlsinfo.HttpOutTlsInfoVO;
|
||||
@@ -566,4 +568,10 @@ public abstract class HttpClient5AdapterServiceSupport implements HttpClientAdap
|
||||
}
|
||||
|
||||
public abstract Object execute(Properties prop, Object message, Properties tempProp) throws Exception;
|
||||
|
||||
protected boolean isTas(Properties tempProp) {
|
||||
String simYn = tempProp.getProperty("SIM_YN");
|
||||
return EAIServerManager.getInstance().isTASEnabledEAIServer()
|
||||
&& StringUtils.equals(simYn, EAIMessageKeys.TRANTYPE_TAS);
|
||||
}
|
||||
}
|
||||
+2
-5
@@ -41,14 +41,11 @@ public class HttpClient5AdapterServiceBody extends HttpClient5AdapterServiceSupp
|
||||
boolean useForwardProxy = StringUtils.equalsIgnoreCase(prop.getProperty("FORWARD_PROXY_USE_YN", "N"), "Y");
|
||||
String forwardProxyUrl = prop.getProperty("FORWARD_PROXY_URL");
|
||||
|
||||
if(useForwardProxy) {
|
||||
if(useForwardProxy && !isTas(tempProp)) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol,host, port);
|
||||
HttpHost proxy = new HttpHost(url.getProtocol(), url.getHost(), url.getPort());
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
|
||||
+15
-10
@@ -11,6 +11,7 @@ import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -294,14 +295,11 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
|
||||
RequestConfig.Builder requestConfigBuilder = RequestConfig.custom();
|
||||
|
||||
if(useForwardProxy) {
|
||||
if(useForwardProxy && !isTas(tempProp)) {
|
||||
java.net.URL url = new java.net.URL(forwardProxyUrl);
|
||||
|
||||
// 프로토콜, 호스트, 포트 추출
|
||||
String protocol = url.getProtocol();
|
||||
String host = url.getHost();
|
||||
int port = url.getPort();
|
||||
HttpHost proxy = new HttpHost(protocol,host, port);
|
||||
HttpHost proxy = new HttpHost(url.getProtocol(), url.getHost(), url.getPort());
|
||||
requestConfigBuilder.setProxy(proxy);
|
||||
}
|
||||
|
||||
@@ -708,12 +706,19 @@ public class HttpClient5AdapterServiceRest extends HttpClient5AdapterServiceSupp
|
||||
return new String(responseMessage, encode);
|
||||
}
|
||||
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
|
||||
JSONObject headerJson = new JSONObject();
|
||||
for (String key : relayKeyArr) {
|
||||
String value = responseHeaderProp.getProperty(key);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
headerJson.put(key, value);
|
||||
if (StringUtils.equalsIgnoreCase(relayHeaderKeys, "ALL")) {
|
||||
for (Enumeration<Object> e = responseHeaderProp.keys(); e.hasMoreElements(); ) {
|
||||
String key = (String)e.nextElement();
|
||||
headerJson.put(StringUtils.lowerCase(key), responseHeaderProp.getProperty(key));
|
||||
}
|
||||
} else {
|
||||
String[] relayKeyArr = org.springframework.util.StringUtils.tokenizeToStringArray(relayHeaderKeys, ",");
|
||||
for (String key : relayKeyArr) {
|
||||
String value = responseHeaderProp.getProperty(key);
|
||||
if (StringUtils.isNotBlank(value)) {
|
||||
headerJson.put(StringUtils.lowerCase(key), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class ReloadInflowClientControlCommand extends Command {
|
||||
@@ -31,10 +31,10 @@ public class ReloadInflowClientControlCommand extends Command {
|
||||
String keyName = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager();
|
||||
if (!(base instanceof DualInflowControlManager)) {
|
||||
throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다.");
|
||||
if (!(base instanceof ClientDualInflowControlManager)) {
|
||||
throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다.");
|
||||
}
|
||||
DualInflowControlManager manager = (DualInflowControlManager) base;
|
||||
ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base;
|
||||
|
||||
if (keyName != null) {
|
||||
if ("ALL".equals(keyName)) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import com.eactive.eai.agent.command.Command;
|
||||
import com.eactive.eai.agent.command.CommandException;
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.InflowControlUtil;
|
||||
import com.eactive.eai.common.inflow.dual.DualInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
public class RemoveInflowClientControlCommand extends Command {
|
||||
@@ -31,10 +31,10 @@ public class RemoveInflowClientControlCommand extends Command {
|
||||
String key = (String) args;
|
||||
try {
|
||||
AbstractInflowControlManager base = InflowControlUtil.getInflowControlManager();
|
||||
if (!(base instanceof DualInflowControlManager)) {
|
||||
throw new IllegalStateException("DualInflowControlManager 가 활성화되지 않았습니다.");
|
||||
if (!(base instanceof ClientDualInflowControlManager)) {
|
||||
throw new IllegalStateException("ClientDualInflowControlManager 가 활성화되지 않았습니다.");
|
||||
}
|
||||
DualInflowControlManager manager = (DualInflowControlManager) base;
|
||||
ClientDualInflowControlManager manager = (ClientDualInflowControlManager) base;
|
||||
manager.removeClient(key);
|
||||
if (logger.isWarn())
|
||||
logger.warn(this.name + "] " + key + " removed.");
|
||||
|
||||
@@ -21,6 +21,7 @@ import com.eactive.eai.common.util.Logger;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.Refill;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
import io.github.bucket4j.local.LocalBucketBuilder;
|
||||
|
||||
@@ -345,12 +346,14 @@ public abstract class AbstractInflowControlManager implements Lifecycle, Bucket
|
||||
if (AbstractInflowControlManager.isInflowTarget(inflowTarget)) {
|
||||
LocalBucketBuilder builder = Bucket4j.builder();
|
||||
if (inflowTarget.getThresholdPerSecond() > 0) {
|
||||
builder.addLimit(Bandwidth.simple(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1)));
|
||||
builder.addLimit(Bandwidth.classic(inflowTarget.getThresholdPerSecond(),
|
||||
Refill.greedy(inflowTarget.getThresholdPerSecond(), Duration.ofSeconds(1))));
|
||||
}
|
||||
|
||||
if (inflowTarget.getThreshold() > 0 && StringUtils.isNotBlank(inflowTarget.getThresholdTimeUnit())) {
|
||||
builder.addLimit(Bandwidth.simple(inflowTarget.getThreshold(),
|
||||
AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit())));
|
||||
builder.addLimit(Bandwidth.classic(inflowTarget.getThreshold(),
|
||||
Refill.intervally(inflowTarget.getThreshold(),
|
||||
AbstractInflowControlManager.getPeriod(inflowTarget.getThresholdTimeUnit()))));
|
||||
}
|
||||
|
||||
return new CustomBucket(builder.build(), inflowTarget);
|
||||
@@ -369,14 +372,16 @@ public abstract class AbstractInflowControlManager implements Lifecycle, Bucket
|
||||
|
||||
if (groupVo.getThresholdPerSecond() > 0) {
|
||||
perSecondBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1)))
|
||||
.addLimit(Bandwidth.classic(groupVo.getThresholdPerSecond(),
|
||||
Refill.greedy(groupVo.getThresholdPerSecond(), Duration.ofSeconds(1))))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (groupVo.getThreshold() > 0 && StringUtils.isNotBlank(groupVo.getThresholdTimeUnit())) {
|
||||
thresholdBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(groupVo.getThreshold(),
|
||||
AbstractInflowControlManager.getPeriod(groupVo.getThresholdTimeUnit())))
|
||||
.addLimit(Bandwidth.classic(groupVo.getThreshold(),
|
||||
Refill.intervally(groupVo.getThreshold(),
|
||||
AbstractInflowControlManager.getPeriod(groupVo.getThresholdTimeUnit()))))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
/**
|
||||
* DualBucket에 클라이언트 유량제어 메서드를 추가한 인터페이스.
|
||||
* {@link ClientDualInflowControlManager}가 구현한다.
|
||||
*/
|
||||
public interface ClientDualBucket extends DualBucket {
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 체크 — 차단 원인 포함.
|
||||
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
|
||||
*/
|
||||
String isClientPassDetail(String clientId);
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용.
|
||||
* @return 등록된 설정이 없으면 null
|
||||
*/
|
||||
InflowTargetVO getClientInflowThreshold(String clientId);
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
import com.eactive.eai.common.inflow.InflowType;
|
||||
import com.eactive.eai.common.lifecycle.LifecycleException;
|
||||
import com.eactive.eai.common.util.ApplicationContextProvider;
|
||||
import com.eactive.eai.common.util.Logger;
|
||||
|
||||
/**
|
||||
* 어댑터/인터페이스 이중 버킷(DualInflowControlManager)에 클라이언트 유량제어를 추가한 관리자.
|
||||
*
|
||||
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
|
||||
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
|
||||
*/
|
||||
@Component
|
||||
public class ClientDualInflowControlManager extends DualInflowControlManager implements ClientDualBucket {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
|
||||
public static synchronized Bucket getBucket() {
|
||||
return ApplicationContextProvider.getContext().getBean(ClientDualInflowControlManager.class);
|
||||
}
|
||||
|
||||
private volatile Map<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> clientMetricsMap = new ConcurrentHashMap<>();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void start() throws LifecycleException {
|
||||
super.start();
|
||||
try {
|
||||
initClients();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException("ClientDualInflowControlManager init failed: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 초기화 / 리로드
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private void initClients() throws Exception {
|
||||
this.clientBucketMap = loadBucketMap(InflowType.CLIENT);
|
||||
if (logger.isInfo()) logger.info("ClientDualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets");
|
||||
}
|
||||
|
||||
public synchronized void reloadClient() throws Exception {
|
||||
initClients();
|
||||
clientMetricsMap.clear();
|
||||
}
|
||||
|
||||
public synchronized void reloadClient(String clientId) throws Exception {
|
||||
reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId);
|
||||
TargetMetrics m = clientMetricsMap.get(clientId);
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
public synchronized void removeClient(String clientId) {
|
||||
clientBucketMap.remove(clientId);
|
||||
clientMetricsMap.remove(clientId);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 클라이언트 메트릭 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public TargetMetrics getClientMetrics(String clientId) { return clientMetricsMap.get(clientId); }
|
||||
public Map<String, TargetMetrics> getAllClientMetrics() { return Collections.unmodifiableMap(clientMetricsMap); }
|
||||
public void resetClientMetrics(String clientId) { TargetMetrics m = clientMetricsMap.get(clientId); if (m != null) m.reset(); }
|
||||
public void resetAllClientMetrics() { clientMetricsMap.values().forEach(TargetMetrics::reset); }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ClientDualBucket — 클라이언트 유량제어
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public String isClientPassDetail(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
if (b == null) return DualCustomBucket.RESULT_PASS;
|
||||
String result = b.tryConsume();
|
||||
recordMetrics(clientMetricsMap, clientId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getClientInflowThreshold(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
return (b == null) ? null : b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 모니터링용 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public DualCustomBucket getClientBucket(String clientId) {
|
||||
return clientBucketMap.get(clientId);
|
||||
}
|
||||
|
||||
public Map<String, DualCustomBucket> getClientBucketMap() {
|
||||
return clientBucketMap;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
package com.eactive.eai.common.inflow.dual;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
|
||||
/**
|
||||
* Bucket 인터페이스 확장.
|
||||
* 기존 isAdapterPass/isInterfacePass(boolean)에 더해
|
||||
* 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지 반환하는 메서드 추가.
|
||||
* 클라이언트별 유량제어 메서드도 포함.
|
||||
*
|
||||
* <p>클라이언트 유량제어는 {@link ClientDualBucket}으로 분리됨.
|
||||
*/
|
||||
public interface DualBucket extends Bucket {
|
||||
|
||||
@@ -22,16 +22,4 @@ public interface DualBucket extends Bucket {
|
||||
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
|
||||
*/
|
||||
String isInterfacePassDetail(String inter);
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 체크 — 차단 원인 포함.
|
||||
* @return null이면 통과, "PER_SECOND" 또는 "THRESHOLD"이면 해당 버킷에서 차단
|
||||
*/
|
||||
String isClientPassDetail(String clientId);
|
||||
|
||||
/**
|
||||
* 클라이언트 유량제어 설정 조회 — 에러 메시지 생성용.
|
||||
* @return 등록된 설정이 없으면 null
|
||||
*/
|
||||
InflowTargetVO getClientInflowThreshold(String clientId);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.eactive.eai.common.inflow.AbstractInflowControlManager;
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
@@ -22,21 +21,17 @@ import com.eactive.eai.common.util.Logger;
|
||||
|
||||
import io.github.bucket4j.Bandwidth;
|
||||
import io.github.bucket4j.Bucket4j;
|
||||
import io.github.bucket4j.Refill;
|
||||
import io.github.bucket4j.local.LocalBucket;
|
||||
|
||||
/**
|
||||
* 어댑터/인터페이스/클라이언트 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자.
|
||||
* 어댑터/인터페이스 버킷을 이중 구조(perSecond + threshold)로 관리하는 유량제어 관리자.
|
||||
*
|
||||
* <p>기존 InflowControlManager는 어댑터/인터페이스에 단일 LocalBucket(복수 Bandwidth)을 사용하여
|
||||
* 차단 시 어느 한도(초당/기간)를 초과했는지 알 수 없었다.
|
||||
* 이 클래스는 그룹 버킷(CustomGroupBucket)과 동일하게 두 버킷을 분리하고
|
||||
* {@link DualBucket#isAdapterPassDetail}/{@link DualBucket#isInterfacePassDetail}로
|
||||
* 차단 원인을 반환한다. 그룹 버킷 통과 여부는 {@link TargetMetrics}로 카운팅된다.
|
||||
* <p>클라이언트 유량제어는 {@link ClientDualInflowControlManager}로 분리됨.
|
||||
*
|
||||
* <p>적용 방법: INFLOW.properties에 아래 한 줄 추가.
|
||||
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager</pre>
|
||||
* <pre>inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.ClientDualInflowControlManager</pre>
|
||||
*/
|
||||
@Component
|
||||
public class DualInflowControlManager extends AbstractInflowControlManager implements DualBucket {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(Logger.LOGGER_DEFAULT);
|
||||
@@ -48,11 +43,9 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
|
||||
private volatile Map<String, DualCustomBucket> adapterBucketMap = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, DualCustomBucket> interfaceBucketMap = new ConcurrentHashMap<>();
|
||||
private volatile Map<String, DualCustomBucket> clientBucketMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final ConcurrentHashMap<String, TargetMetrics> adapterMetricsMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> interfaceMetricsMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> clientMetricsMap = new ConcurrentHashMap<>();
|
||||
private final ConcurrentHashMap<String, TargetMetrics> groupMetricsMap = new ConcurrentHashMap<>();
|
||||
|
||||
public static class TargetMetrics {
|
||||
@@ -79,7 +72,6 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
try {
|
||||
initAdapters();
|
||||
initInterfaces();
|
||||
initClients();
|
||||
} catch (Exception e) {
|
||||
throw new LifecycleException("DualInflowControlManager init failed: " + e.getMessage());
|
||||
}
|
||||
@@ -99,12 +91,7 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
if (logger.isInfo()) logger.info("DualInflowControlManager] initInterfaces: " + interfaceBucketMap.size() + " buckets");
|
||||
}
|
||||
|
||||
private void initClients() throws Exception {
|
||||
this.clientBucketMap = loadBucketMap(InflowType.CLIENT);
|
||||
if (logger.isInfo()) logger.info("DualInflowControlManager] initClients: " + clientBucketMap.size() + " buckets");
|
||||
}
|
||||
|
||||
private Map<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
|
||||
protected Map<String, DualCustomBucket> loadBucketMap(InflowType type) throws Exception {
|
||||
Map<String, DualCustomBucket> newMap = new HashMap<>();
|
||||
for (InflowTargetVO vo : inflowControlDAO.getInflowList(type)) {
|
||||
DualCustomBucket bucket = makeBucket(vo);
|
||||
@@ -139,22 +126,6 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
public synchronized void reloadClient() throws Exception {
|
||||
initClients();
|
||||
clientMetricsMap.clear();
|
||||
}
|
||||
|
||||
public synchronized void reloadClient(String clientId) throws Exception {
|
||||
reloadBucketMap(clientBucketMap, InflowType.CLIENT, clientId);
|
||||
TargetMetrics m = clientMetricsMap.get(clientId);
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
public synchronized void removeClient(String clientId) {
|
||||
clientBucketMap.remove(clientId);
|
||||
clientMetricsMap.remove(clientId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void reloadGroup() throws Exception {
|
||||
super.reloadGroup();
|
||||
@@ -168,7 +139,7 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
if (m != null) m.reset();
|
||||
}
|
||||
|
||||
private void reloadBucketMap(Map<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
|
||||
protected void reloadBucketMap(Map<String, DualCustomBucket> targetMap, InflowType type, String name) throws Exception {
|
||||
Map<String, InflowTargetVO> updated = inflowControlDAO.getInflowMap(type, name);
|
||||
if (updated == null) return;
|
||||
for (InflowTargetVO vo : updated.values()) {
|
||||
@@ -179,24 +150,20 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 어댑터/인터페이스/클라이언트 메트릭 접근자
|
||||
// 어댑터/인터페이스 메트릭 접근자
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
public TargetMetrics getAdapterMetrics(String adapter) { return adapterMetricsMap.get(adapter); }
|
||||
public TargetMetrics getInterfaceMetrics(String inter) { return interfaceMetricsMap.get(inter); }
|
||||
public TargetMetrics getClientMetrics(String clientId) { return clientMetricsMap.get(clientId); }
|
||||
|
||||
public Map<String, TargetMetrics> getAllAdapterMetrics() { return Collections.unmodifiableMap(adapterMetricsMap); }
|
||||
public Map<String, TargetMetrics> getAllInterfaceMetrics() { return Collections.unmodifiableMap(interfaceMetricsMap); }
|
||||
public Map<String, TargetMetrics> getAllClientMetrics() { return Collections.unmodifiableMap(clientMetricsMap); }
|
||||
|
||||
public void resetAdapterMetrics(String adapter) { TargetMetrics m = adapterMetricsMap.get(adapter); if (m != null) m.reset(); }
|
||||
public void resetInterfaceMetrics(String inter) { TargetMetrics m = interfaceMetricsMap.get(inter); if (m != null) m.reset(); }
|
||||
public void resetClientMetrics(String clientId) { TargetMetrics m = clientMetricsMap.get(clientId); if (m != null) m.reset(); }
|
||||
|
||||
public void resetAllAdapterMetrics() { adapterMetricsMap.values().forEach(TargetMetrics::reset); }
|
||||
public void resetAllInterfaceMetrics() { interfaceMetricsMap.values().forEach(TargetMetrics::reset); }
|
||||
public void resetAllClientMetrics() { clientMetricsMap.values().forEach(TargetMetrics::reset); }
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 그룹 메트릭 — isGroupPass() 카운팅 및 접근자
|
||||
@@ -255,32 +222,15 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String isClientPassDetail(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
if (b == null) return DualCustomBucket.RESULT_PASS;
|
||||
String result = b.tryConsume();
|
||||
recordMetrics(clientMetricsMap, clientId, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void recordMetrics(ConcurrentHashMap<String, TargetMetrics> metricsMap, String key, String result) {
|
||||
protected void recordMetrics(ConcurrentHashMap<String, TargetMetrics> metricsMap, String key, String result) {
|
||||
TargetMetrics m = metricsMap.computeIfAbsent(key, k -> new TargetMetrics());
|
||||
if (result == null) m.allowed.incrementAndGet();
|
||||
else if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(result)) m.rejectedPerSecond.incrementAndGet();
|
||||
else m.rejectedThreshold.incrementAndGet();
|
||||
}
|
||||
|
||||
@Override
|
||||
public InflowTargetVO getClientInflowThreshold(String clientId) {
|
||||
DualCustomBucket b = clientBucketMap.get(clientId);
|
||||
return (b == null) ? null : b.getInflowTargetVo();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Bucket 인터페이스 오버라이드 — 이중 버킷으로 교체 (boolean 반환 유지)
|
||||
// isAdapterPassDetail/isInterfacePassDetail이 토큰을 소비하므로
|
||||
// 두 메서드를 동일 요청에서 중복 호출하면 토큰이 이중 소비됨에 주의.
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
@@ -295,7 +245,6 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 어댑터/인터페이스 키·VO·삭제 — Dual 맵 기준으로 오버라이드
|
||||
// (base의 adapterBucketList / interfaceBucketList 는 읽지 않아 DAO 이중 호출 제거)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
@@ -356,19 +305,16 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
return interfaceBucketMap;
|
||||
}
|
||||
|
||||
public DualCustomBucket getClientBucket(String clientId) {
|
||||
return clientBucketMap.get(clientId);
|
||||
}
|
||||
|
||||
public Map<String, DualCustomBucket> getClientBucketMap() {
|
||||
return clientBucketMap;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// 버킷 팩토리
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private DualCustomBucket makeBucket(InflowTargetVO vo) {
|
||||
/**
|
||||
* perSecond 버킷: greedy refill — 초당 N개 요청을 균등하게 허용.
|
||||
* threshold 버킷: intervally refill — 기간(시/일/월 등) 단위 쿼터를 기간 시작 시 일괄 충전.
|
||||
* Bandwidth.simple은 두 버킷 모두 greedy로 생성되어 시간 단위 구분이 없어지므로 classic으로 교체.
|
||||
*/
|
||||
protected DualCustomBucket makeBucket(InflowTargetVO vo) {
|
||||
if (!AbstractInflowControlManager.isInflowTarget(vo)) {
|
||||
return null;
|
||||
}
|
||||
@@ -378,14 +324,16 @@ public class DualInflowControlManager extends AbstractInflowControlManager imple
|
||||
|
||||
if (vo.getThresholdPerSecond() > 0) {
|
||||
perSecondBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(vo.getThresholdPerSecond(), Duration.ofSeconds(1)))
|
||||
.addLimit(Bandwidth.classic(vo.getThresholdPerSecond(),
|
||||
Refill.greedy(vo.getThresholdPerSecond(), Duration.ofSeconds(1))))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (vo.getThreshold() > 0 && StringUtils.isNotBlank(vo.getThresholdTimeUnit())) {
|
||||
Duration period = AbstractInflowControlManager.getPeriod(vo.getThresholdTimeUnit());
|
||||
thresholdBucket = Bucket4j.builder()
|
||||
.addLimit(Bandwidth.simple(vo.getThreshold(),
|
||||
AbstractInflowControlManager.getPeriod(vo.getThresholdTimeUnit())))
|
||||
.addLimit(Bandwidth.classic(vo.getThreshold(),
|
||||
Refill.intervally(vo.getThreshold(), period)))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
package com.eactive.eai.inbound.processor;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.eactive.eai.common.inflow.Bucket;
|
||||
import com.eactive.eai.common.inflow.InflowTargetVO;
|
||||
import com.eactive.eai.common.inflow.dual.DualBucket;
|
||||
import com.eactive.eai.common.inflow.dual.DualCustomBucket;
|
||||
|
||||
/**
|
||||
* 어댑터/인터페이스 유량제어 차단 시 어느 버킷(PER_SECOND/THRESHOLD)에서 차단됐는지
|
||||
* 에러 메시지에 포함하는 RequestProcessor 확장.
|
||||
*
|
||||
* <p>DualInflowControlManager와 함께 사용:
|
||||
* <pre>
|
||||
* inflow.control.bucket.className=com.eactive.eai.common.inflow.dual.DualInflowControlManager
|
||||
* </pre>
|
||||
*
|
||||
* <p>Spring Bean 등록 후 기존 RequestProcessor 대신 이 클래스를 어댑터에 설정한다.
|
||||
*/
|
||||
public class DualRequestProcessor extends RequestProcessor {
|
||||
|
||||
@Override
|
||||
protected String checkClientInflow(Bucket bucket, String clientId) {
|
||||
if (StringUtils.isBlank(clientId)) return null;
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).isClientPassDetail(clientId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InflowTargetVO resolveClientInflowThreshold(Bucket bucket, String clientId) {
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).getClientInflowThreshold(clientId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String checkAdapterInflow(Bucket bucket, String adapterGroupName) {
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).isAdapterPassDetail(adapterGroupName);
|
||||
}
|
||||
return super.checkAdapterInflow(bucket, adapterGroupName);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String checkInterfaceInflow(Bucket bucket, String eaiSvcCd) {
|
||||
if (bucket instanceof DualBucket) {
|
||||
return ((DualBucket) bucket).isInterfacePassDetail(eaiSvcCd);
|
||||
}
|
||||
return super.checkInterfaceInflow(bucket, eaiSvcCd);
|
||||
}
|
||||
|
||||
/**
|
||||
* 차단 원인에 따라 에러 메시지에 한도 정보를 포함.
|
||||
* <ul>
|
||||
* <li>PER_SECOND: "API 호출 한도 초과 [name: Nreq/sec]"</li>
|
||||
* <li>THRESHOLD: "API 호출 한도 초과 [name: Nreq/UNIT]"</li>
|
||||
* <li>그 외: "API 호출 한도 초과 [name]" (기존 동작)</li>
|
||||
* </ul>
|
||||
*/
|
||||
@Override
|
||||
protected String buildInflowTargetErrorMsg(InflowTargetVO inflowTargetVO, String blockedType) {
|
||||
if (DualCustomBucket.RESULT_BLOCKED_PER_SECOND.equals(blockedType)) {
|
||||
return String.format("API 호출 한도 초과 [%s: %dreq/sec]",
|
||||
inflowTargetVO.getName(),
|
||||
inflowTargetVO.getThresholdPerSecond());
|
||||
}
|
||||
if (DualCustomBucket.RESULT_BLOCKED_THRESHOLD.equals(blockedType)) {
|
||||
return String.format("API 호출 한도 초과 [%s: %dreq/%s]",
|
||||
inflowTargetVO.getName(),
|
||||
inflowTargetVO.getThreshold(),
|
||||
inflowTargetVO.getThresholdTimeUnit());
|
||||
}
|
||||
return super.buildInflowTargetErrorMsg(inflowTargetVO, blockedType);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import com.eactive.eai.adapter.AdapterPropManager;
|
||||
import com.eactive.eai.adapter.AdapterVO;
|
||||
import com.eactive.eai.adapter.http.HttpStatusException;
|
||||
import com.eactive.eai.adapter.http.client.HttpClientAdapterServiceKey;
|
||||
import com.eactive.eai.adapter.http.dynamic.HttpAdapterServiceKey;
|
||||
import com.eactive.eai.common.EAIKeys;
|
||||
import com.eactive.eai.common.exception.ExceptionUtil;
|
||||
import com.eactive.eai.common.header.HeaderAction;
|
||||
@@ -352,24 +353,26 @@ public class HTTPProcess extends DefaultProcess {
|
||||
this.tempProp.put(HttpClientAdapterServiceKey.INSTANCE_ID, instanceId);
|
||||
}
|
||||
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp());
|
||||
if (eaiServerManager.isTASEnabledEAIServer() && EAIMessageKeys.TRANTYPE_TAS.equals(reqEaiMsg.getTranType())
|
||||
// RESTAdapter는 교체하지 않는다
|
||||
&& !StringUtils.equals(adptGrpVO.getType(), com.eactive.eai.adapter.Keys.TYPE_REST)) {
|
||||
PropManager manager = PropManager.getInstance();
|
||||
Properties prop = manager.getProperties(RouteKeys.SIM);
|
||||
|
||||
if (reqEaiMsg.getCurrentSvcMsg().isSyncItfTp()) {
|
||||
this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_SYNC, "_SIM_OU_HTT_SyC");
|
||||
} else {
|
||||
this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_ASYNC, "_SIM_OU_HTT_AsC");
|
||||
}
|
||||
this.reqEaiMsg.getSvcMsg(0).setPsvSysItfTp(this.adapterGroupName);
|
||||
} else {
|
||||
this.adapterGroupName = reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp();
|
||||
}
|
||||
// EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
// AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
// AdapterGroupVO adptGrpVO = adapterManager.getAdapterGroupVO(reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp());
|
||||
// if (eaiServerManager.isTASEnabledEAIServer() && EAIMessageKeys.TRANTYPE_TAS.equals(reqEaiMsg.getTranType())
|
||||
// // RESTAdapter는 교체하지 않는다
|
||||
// && !StringUtils.equals(adptGrpVO.getType(), com.eactive.eai.adapter.Keys.TYPE_REST)) {
|
||||
// PropManager manager = PropManager.getInstance();
|
||||
// Properties prop = manager.getProperties(RouteKeys.SIM);
|
||||
//
|
||||
// if (reqEaiMsg.getCurrentSvcMsg().isSyncItfTp()) {
|
||||
// this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_SYNC, "_SIM_OU_HTT_SyC");
|
||||
// } else {
|
||||
// this.adapterGroupName = prop.getProperty(RouteKeys.SIM_ADAPTER_ASYNC, "_SIM_OU_HTT_AsC");
|
||||
// }
|
||||
// this.reqEaiMsg.getSvcMsg(0).setPsvSysItfTp(this.adapterGroupName);
|
||||
// } else {
|
||||
// this.adapterGroupName = reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp();
|
||||
// }
|
||||
|
||||
this.adapterGroupName = reqEaiMsg.getCurrentSvcMsg().getPsvSysItfTp();
|
||||
|
||||
this.svcTsmtUsgTp = this.reqEaiMsg.getSvcTsmtUsgTp(); // Inbound 호출방식
|
||||
this.psvItfTp = this.reqEaiMsg.getCurrentSvcMsg().getPsvItfTp(); // 호출방식
|
||||
@@ -404,6 +407,7 @@ public class HTTPProcess extends DefaultProcess {
|
||||
}
|
||||
|
||||
// Inbound Adapter mesageType
|
||||
AdapterManager adapterManager = AdapterManager.getInstance();
|
||||
AdapterGroupVO inAdptGrpVO = adapterManager.getAdapterGroupVO(this.reqEaiMsg.getSngSysItfTp());
|
||||
this.inAdapterMsgType = MessageType.ASC;
|
||||
|
||||
@@ -592,6 +596,22 @@ public class HTTPProcess extends DefaultProcess {
|
||||
logger.debug(guidLogPrefix + " HTTP NONSTANDARD Request Message : "
|
||||
+ MessageUtil.toAdapterTypeString(this.reqObject, this.inboundCharset));
|
||||
}
|
||||
// TAS 선택시 시뮬레이터 url로 변경, HTT 어댑터도 RST 처럼 방식을 변경하였다.
|
||||
EAIServerManager eaiServerManager = EAIServerManager.getInstance();
|
||||
if (eaiServerManager.isTASEnabledEAIServer() && EAIMessageKeys.TRANTYPE_TAS.equals(reqEaiMsg.getTranType())) {
|
||||
PropManager manager = PropManager.getInstance();
|
||||
Properties prop = manager.getProperties(RouteKeys.SIM);
|
||||
String simAddr = prop.getProperty(RouteKeys.SIM_REST_MOCKSERVER);
|
||||
if (StringUtils.isBlank(simAddr)) {
|
||||
throw new Exception(String.format("Properties %s > %s not setted", RouteKeys.SIM,
|
||||
RouteKeys.SIM_REST_MOCKSERVER));
|
||||
}
|
||||
|
||||
String url = StringUtils.removeEnd(simAddr, "/") + "/mockapi/" + this.reqEaiMsg.getEAISvcCd();
|
||||
this.outboundProp.put(HttpClientAdapterServiceKey.URL, url);
|
||||
this.tempProp.remove(HttpAdapterServiceKey.INBOUND_REWRITE_PATH);
|
||||
logger.debug("simUrl=" + url);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
String[] msgArgs = new String[1];
|
||||
msgArgs[0] = this.reqEaiMsg.getEAISvcCd();
|
||||
|
||||
Reference in New Issue
Block a user