feat: add 网关
This commit is contained in:
parent
fa4c571f6e
commit
bb9555a086
@ -37,6 +37,12 @@
|
||||
<artifactId>commons-collections4</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.jayway.jsonpath</groupId>
|
||||
<artifactId>json-path</artifactId>
|
||||
<version>2.9.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
|
||||
@ -0,0 +1,14 @@
|
||||
package cn.axzo.foundation.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum EntityStatusEnum {
|
||||
ENABLED("启用"),
|
||||
|
||||
DISABLED("禁用");
|
||||
|
||||
private String label;
|
||||
}
|
||||
@ -0,0 +1,190 @@
|
||||
package cn.axzo.foundation.util;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.serializer.SerializerFeature;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.jayway.jsonpath.Configuration;
|
||||
import com.jayway.jsonpath.JsonPath;
|
||||
import com.jayway.jsonpath.MapFunction;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* Helper类方便从集合或者bean对象过滤属性和聚合数据
|
||||
* 支持指定jsonpath去聚合数目. 通常使用在BFF的controller层.
|
||||
* https://github.com/json-path/JsonPath
|
||||
*/
|
||||
public class DataAssembleHelper {
|
||||
|
||||
/**
|
||||
* 一个Immutable的jsonarray, 防止内容被修改
|
||||
*/
|
||||
private final static JSONArray EMPTY_JSON_ARRAY = new JSONArray(Collections.emptyList());
|
||||
|
||||
/**
|
||||
* 给定一个对象集合, 过滤集合中每个对象的属性.
|
||||
*
|
||||
* @param data 对象集合
|
||||
* @param includeFields 仅仅包含的属性名称
|
||||
* @return 过滤了对象属性的集合
|
||||
*/
|
||||
public static JSONArray filterCollection(Collection data, Set<String> includeFields, boolean include) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return EMPTY_JSON_ARRAY;
|
||||
}
|
||||
|
||||
JSONArray jsonList = (JSONArray)toJSON(data);
|
||||
List collect = IntStream.range(0, jsonList.size()).mapToObj(i -> jsonList.getJSONObject(i))
|
||||
.map(e -> {
|
||||
return new JSONObject(Maps.filterEntries(e, entry -> include == includeFields.contains(entry.getKey())));
|
||||
}).collect(Collectors.toList());
|
||||
return new JSONArray(collect);
|
||||
}
|
||||
|
||||
public static JSONArray filterCollection(Collection data, Set<String> includeFields) {
|
||||
return filterCollection(data, includeFields, true);
|
||||
}
|
||||
|
||||
public static JSONArray filterCollectionWithPatterns(Collection data, Set<String> patterns) {
|
||||
if (data == null || data.isEmpty()) {
|
||||
return EMPTY_JSON_ARRAY;
|
||||
}
|
||||
String[] includeFieldArr = patterns.stream().toArray(String[]::new);
|
||||
|
||||
JSONArray jsonList = (JSONArray)toJSON(data);
|
||||
List collect = IntStream.range(0, jsonList.size()).mapToObj(i -> jsonList.getJSONObject(i))
|
||||
.map(e -> {
|
||||
return new JSONObject(Maps.filterEntries(e, entry -> PatternMatchUtils.simpleMatch(includeFieldArr, entry.getKey())));
|
||||
}).collect(Collectors.toList());
|
||||
return new JSONArray(collect);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤指定bean对象的
|
||||
*
|
||||
* @param bean
|
||||
* @param includeFields fields 名称可以嵌套名称比如 xxx.yyy 通过"."来分割
|
||||
* @return jsonobject, 内容是过滤后的bean对象的属性和属性值.
|
||||
*/
|
||||
public static JSONObject filterBean(Object bean, Set<String> includeFields) {
|
||||
return filterBean(bean, includeFields, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤指定bean对象的
|
||||
*
|
||||
* @param bean
|
||||
* @param filterFields fields 名称可以嵌套名称比如 xxx.yyy 通过"."来分割
|
||||
* @param include 包含or排除
|
||||
* @return jsonobject, 内容是过滤后的bean对象的属性和属性值.
|
||||
*/
|
||||
public static JSONObject filterBean(Object bean, Set<String> filterFields, boolean include) {
|
||||
Preconditions.checkArgument(!(bean instanceof Collection));
|
||||
|
||||
if (bean == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject json = (JSONObject) toJSON(bean);
|
||||
// 查找有子属性定义的 key, 并按照主属性分组. key -> [subkey1, subkey2, ...]
|
||||
// 如 a.b, a.c 转换成 a -> [b,c]
|
||||
final Map<String, Set<String>> filterSubKeys = filterFields.stream()
|
||||
.map(e -> Pair.of(StringUtils.substringBefore(e, "."), StringUtils.substringAfter(e, ".")))
|
||||
.filter(e-> !Strings.isNullOrEmpty(e.getValue()))
|
||||
.collect(Collectors.groupingBy(e -> e.getKey(), Collectors.mapping(e -> e.getValue(), Collectors.toSet())));
|
||||
|
||||
// 支持嵌套属性. 如 a.b
|
||||
Map<String, Object> res = json.entrySet().stream()
|
||||
.map(entry -> {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
final Set<String> subKeys = filterSubKeys.get(key);
|
||||
if (subKeys != null) {
|
||||
if (value instanceof Collection) {
|
||||
value = filterCollection((Collection) value, subKeys, include);
|
||||
} else {
|
||||
value = filterBean(value, subKeys, include);
|
||||
}
|
||||
} else {
|
||||
if (include != filterFields.contains(key)) {
|
||||
key = "";
|
||||
}
|
||||
}
|
||||
return Pair.of(key, value);
|
||||
})
|
||||
.filter(e -> !Strings.isNullOrEmpty(e.getKey()))
|
||||
.collect(HashMap::new, (m, v)->m.put(v.getKey(), v.getValue()), HashMap::putAll);
|
||||
|
||||
return new JSONObject(res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对bean对象, 根据指定jsonpath中的节点, 替换成mapper中返回的值
|
||||
*
|
||||
* @param jsonPath 节点路径. 参看 https://github.com/json-path/JsonPath
|
||||
* @param bean
|
||||
* @param mapper
|
||||
* @return 内容是替换后的bean对象的属性和属性值.
|
||||
*/
|
||||
public static JSONObject mapBean(String jsonPath, Object bean, Function<Object, Object> mapper) {
|
||||
Preconditions.checkArgument(!(bean instanceof Collection));
|
||||
|
||||
if (bean == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
JSONObject json = (JSONObject) toJSON(bean);
|
||||
MapFunction f = (target, conf) -> {
|
||||
return mapper.apply(target);
|
||||
};
|
||||
return JsonPath.compile(jsonPath).map(json, f, Configuration.defaultConfiguration());
|
||||
}
|
||||
|
||||
/**
|
||||
* 对bean对象集合, 根据指定jsonpath中的节点, 替换成mapper中返回的值
|
||||
*
|
||||
* @param jsonPath 节点路径. 参看 https://github.com/json-path/JsonPath
|
||||
* @param beans
|
||||
* @param mapper
|
||||
* @return 内容是替换后的集合列表.
|
||||
*/
|
||||
public static JSONArray mapBeans(String jsonPath, Collection beans, Function<Object, Object> mapper) {
|
||||
if (beans == null || beans.isEmpty()) {
|
||||
return EMPTY_JSON_ARRAY;
|
||||
}
|
||||
|
||||
JSONArray json = (JSONArray)toJSON(beans);
|
||||
MapFunction f = (target, conf) -> {
|
||||
return mapper.apply(target);
|
||||
};
|
||||
return JsonPath.compile(jsonPath).map(json, f, Configuration.defaultConfiguration());
|
||||
}
|
||||
|
||||
private static JSON toJSON(Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
if (obj instanceof JSON) {
|
||||
return (JSON) obj;
|
||||
} else if (obj instanceof Collection) {
|
||||
return JSON.parseArray(JSONObject.toJSONString(obj, SerializerFeature.WriteMapNullValue));
|
||||
} else {
|
||||
return JSON.parseObject(JSONObject.toJSONString(obj, SerializerFeature.WriteMapNullValue));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -17,4 +17,34 @@
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>cn.axzo.foundation</groupId>
|
||||
<artifactId>web-support-lib</artifactId>
|
||||
<version>2.0.0-SNAPSHOT</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>commons-beanutils</groupId>
|
||||
<artifactId>commons-beanutils</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.googlecode.aviator</groupId>
|
||||
<artifactId>aviator</artifactId>
|
||||
<version>4.2.8</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>commons-logging</groupId>
|
||||
<artifactId>commons-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@ -0,0 +1,24 @@
|
||||
package cn.axzo.foundation.gateway.support;
|
||||
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GlobalContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.proxy.Proxy;
|
||||
|
||||
public interface BizGateway {
|
||||
|
||||
/**
|
||||
* 根据请求url找到匹配的代理处理器
|
||||
*
|
||||
* @param context
|
||||
* @return
|
||||
*/
|
||||
Proxy findProxy(RequestContext context);
|
||||
|
||||
/**
|
||||
* 获取全局上下文
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
GlobalContext getGlobalContext();
|
||||
}
|
||||
@ -0,0 +1,548 @@
|
||||
package cn.axzo.foundation.gateway.support;
|
||||
|
||||
import cn.axzo.foundation.enums.AppEnvEnum;
|
||||
import cn.axzo.foundation.enums.EntityStatusEnum;
|
||||
import cn.axzo.foundation.gateway.support.entity.*;
|
||||
import cn.axzo.foundation.gateway.support.exception.ApiNotFoundException;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.plugin.impl.DebugProxyHook;
|
||||
import cn.axzo.foundation.gateway.support.plugin.impl.ServiceCodeHook;
|
||||
import cn.axzo.foundation.gateway.support.plugin.impl.StopWatchHook;
|
||||
import cn.axzo.foundation.gateway.support.proxy.Proxy;
|
||||
import cn.axzo.foundation.gateway.support.proxy.ProxyFeature;
|
||||
import cn.axzo.foundation.gateway.support.proxy.impl.AbstractProxy;
|
||||
import cn.axzo.foundation.gateway.support.proxy.impl.DefaultProxy;
|
||||
import cn.axzo.foundation.gateway.support.proxy.impl.NoContentProxy;
|
||||
import cn.axzo.foundation.gateway.support.utils.ParameterFilter;
|
||||
import cn.axzo.foundation.gateway.support.utils.RpcClientProvider;
|
||||
import cn.axzo.foundation.gateway.support.utils.ServiceResolver;
|
||||
import cn.axzo.foundation.web.support.AppRuntime;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONAware;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.*;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.axzo.foundation.gateway.support.plugin.impl.RequestFilterHook.*;
|
||||
import static cn.axzo.foundation.gateway.support.utils.ParameterFilter.*;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class BizGatewayImpl implements BizGateway {
|
||||
|
||||
private final static String DEFAULT_PROXY_PATTERN = "/**";
|
||||
private final static Set<String> NO_CONTENT_PATHS = ImmutableSet.of("/", "/favicon.ico");
|
||||
|
||||
private final GlobalContext globalContext;
|
||||
private final ServiceResolver serviceResolver;
|
||||
private final ImmutableMap<String, Proxy> preloadProxies;
|
||||
private final AntPathMatcher antPathMatcher;
|
||||
|
||||
// 创建Proxy失败时返回,用于过滤失败的case
|
||||
private static final Proxy NULL_PROXY = new Proxy() {
|
||||
@Override
|
||||
public ProxyContext getContext() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProxyHook> getProxyHooks() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse request(RequestContext context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
|
||||
// 已失效的代理,用于灰度代理配置标记已失效的配置
|
||||
private static final Proxy DISABLED_PROXY = new Proxy() {
|
||||
@Override
|
||||
public ProxyContext getContext() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProxyHook> getProxyHooks() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse request(RequestContext context) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
};
|
||||
|
||||
private BizGatewayImpl(GlobalContext globalContext) {
|
||||
this.globalContext = globalContext;
|
||||
this.serviceResolver = new ServiceResolver(globalContext);
|
||||
antPathMatcher = new AntPathMatcher();
|
||||
antPathMatcher.setCachePatterns(true);
|
||||
|
||||
// local proxies
|
||||
Map<String, Proxy> localProxies = getProxies(globalContext.getLocalProxies(), Collections.emptyMap());
|
||||
// default proxy
|
||||
Proxy defaultProxy = new DefaultProxy(ProxyContext.builder()
|
||||
.id(ProxyContext.DEFAULT_PROXY_ID)
|
||||
.proxyName(ProxyContext.DEFAULT_PROXY_NAME)
|
||||
.proxyParam(new JSONObject())
|
||||
.version(ProxyContext.DEFAULT_VERSION)
|
||||
.feature(ProxyContext.DEFAULT_FEATURE)
|
||||
.parameterFilter(ParameterFilter.buildFilter())
|
||||
.globalContext(globalContext)
|
||||
.serviceResolver(serviceResolver)
|
||||
.build(), globalContext.getProxyHookChain());
|
||||
|
||||
// 预加载的代理配置,包括本地配置及默认配置
|
||||
ImmutableMap.Builder<String, Proxy> proxyBuilder = ImmutableMap.<String, Proxy>builder().putAll(localProxies);
|
||||
if (!localProxies.containsKey(DEFAULT_PROXY_PATTERN)) {
|
||||
proxyBuilder.put(DEFAULT_PROXY_PATTERN, defaultProxy);
|
||||
}
|
||||
|
||||
// 返回空实现的代理
|
||||
NoContentProxy noContentProxy = new NoContentProxy(globalContext, serviceResolver);
|
||||
NO_CONTENT_PATHS.stream()
|
||||
.filter(p -> !localProxies.containsKey(p))
|
||||
.forEach(e -> proxyBuilder.put(e, noContentProxy));
|
||||
|
||||
preloadProxies = proxyBuilder.build();
|
||||
}
|
||||
|
||||
private Map<String, Proxy> getProxies(Map<String, GateSettingResp.Proxy> proxyConfigs, Map<String, Proxy> oldProxies) {
|
||||
Map<String, Proxy> proxies = proxyConfigs.keySet().stream()
|
||||
.collect(Collectors.toMap(pattern -> pattern, pattern -> {
|
||||
GateSettingResp.Proxy config = proxyConfigs.get(pattern);
|
||||
Proxy instance = oldProxies.get(pattern);
|
||||
|
||||
// 如果相同pattern的代理实例对应的proxyId与version一致,则重用该实例
|
||||
boolean instanceAvailable = Optional.ofNullable(instance)
|
||||
.map(i -> Objects.equals(i.getContext().getId(), config.getId())
|
||||
&& Objects.equals(i.getContext().getVersion(), config.getVersion()))
|
||||
.orElse(false);
|
||||
if (instanceAvailable) {
|
||||
return instance;
|
||||
} else {
|
||||
return createProxyFromConfig(config);
|
||||
}
|
||||
}));
|
||||
|
||||
// 过滤NULL_PROXY的结果
|
||||
return proxies.keySet().stream()
|
||||
.filter(k -> !Objects.equals(proxies.get(k), NULL_PROXY))
|
||||
.collect(Collectors.toMap(k -> k, proxies::get));
|
||||
}
|
||||
|
||||
private Proxy createProxyFromConfig(GateSettingResp.Proxy proxyConfig) {
|
||||
try {
|
||||
// 没有status字段时认为是ENABLED,兼容老的方式
|
||||
if (Optional.ofNullable(proxyConfig.getStatus()).orElse(EntityStatusEnum.ENABLED) == EntityStatusEnum.DISABLED) {
|
||||
return DISABLED_PROXY;
|
||||
}
|
||||
// 获取配置id
|
||||
if (Optional.ofNullable(proxyConfig.getId()).orElse(0L) < 1) {
|
||||
globalContext.postAlert("配置id不正确,请检查代理配置, 当前配置:{}", JSON.toJSONString(proxyConfig));
|
||||
return NULL_PROXY;
|
||||
}
|
||||
JSONObject proxyParam = proxyConfig.getParameter();
|
||||
if (Objects.isNull(proxyParam)) {
|
||||
globalContext.postAlert("没有代理参数,请检查代理配置, 当前配置:{}", JSON.toJSONString(proxyConfig));
|
||||
return NULL_PROXY;
|
||||
}
|
||||
ParameterFilter parameterFilter = ParameterFilter.buildFilter(proxyParam.getString("inFilter"),
|
||||
proxyParam.getString("outFilter"), ProxyFeature.needPrintLog(proxyConfig.getFeature()));
|
||||
if (Objects.isNull(parameterFilter)) {
|
||||
globalContext.postAlert("参数过滤规则不正确,请检查代理配置, 当前配置:{}", JSON.toJSONString(proxyConfig));
|
||||
return NULL_PROXY;
|
||||
}
|
||||
|
||||
// 获取代理实例
|
||||
AbstractProxy proxy = AbstractProxy.newProxy(ProxyContext.builder()
|
||||
.id(proxyConfig.getId())
|
||||
.proxyName(proxyConfig.getName())
|
||||
.proxyParam(proxyParam)
|
||||
.version(proxyConfig.getVersion())
|
||||
.feature(proxyConfig.getFeature())
|
||||
.parameterFilter(parameterFilter)
|
||||
.globalContext(globalContext)
|
||||
.serviceResolver(serviceResolver)
|
||||
.proxyResolver(proxyPath -> preloadProxies.get(proxyPath))
|
||||
.build(), globalContext.getProxyHookChain());
|
||||
if (Objects.isNull(proxy)) {
|
||||
globalContext.postAlert("找不到代理实现类,请检查代理配置, 当前配置:{}", JSON.toJSONString(proxyConfig));
|
||||
return NULL_PROXY;
|
||||
}
|
||||
|
||||
// 解析代理参数
|
||||
if (!proxy.parseParameter(proxyParam)) {
|
||||
globalContext.postAlert("代理参数有误,请检查代理配置, 当前配置:{}", JSON.toJSONString(proxyConfig));
|
||||
return NULL_PROXY;
|
||||
}
|
||||
log.info("创建代理, proxy - {}", proxy.getContext());
|
||||
|
||||
return proxy;
|
||||
} catch (Exception e) {
|
||||
// 捕获异常以确保某一条配置错误不影响其它配置
|
||||
globalContext.postAlert("创建代理失败,请检查代理配置, 当前配置:{}", JSON.toJSONString(proxyConfig), e);
|
||||
return NULL_PROXY;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeStopWatchHook() {
|
||||
ProxyHook hook = globalContext.getProxyHookChain().getHooks().get(0);
|
||||
((StopWatchHook) hook).initialize();
|
||||
}
|
||||
|
||||
private boolean isGrayStage(RequestContext requestContext) {
|
||||
// TODO: correct gray stage
|
||||
if (!CollectionUtils.isEmpty(requestContext.getHeaders())
|
||||
&& StringUtils.equals(requestContext.getHeaders().get("gray_stage"), "true")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Proxy findProxy(RequestContext context) {
|
||||
initializeStopWatchHook();
|
||||
|
||||
// 所有的proxy都是从本地预加载的
|
||||
Map<String, Proxy> proxies = preloadProxies;
|
||||
Comparator<String> comparator = antPathMatcher.getPatternComparator(context.getRequestURI());
|
||||
Proxy proxy = proxies.keySet().stream()
|
||||
.filter(pattern -> antPathMatcher.match(pattern, context.getRequestURI()))
|
||||
.min(comparator)
|
||||
.map(proxies::get).orElseThrow(() -> {
|
||||
String message = String.format("host: %s, requestURI: %s", context.getHost(), context.getRequestURI());
|
||||
globalContext.postAlert(message);
|
||||
return new ApiNotFoundException(message);
|
||||
});
|
||||
|
||||
ProxyHookChain hookChain = null;
|
||||
try {
|
||||
hookChain = new ProxyHookChain(proxy.getProxyHooks());
|
||||
hookChain.findProxy(context, proxy.getContext());
|
||||
return proxy;
|
||||
} catch (Throwable throwable) {
|
||||
if (Objects.isNull(hookChain)) {
|
||||
hookChain = globalContext.getProxyHookChain();
|
||||
}
|
||||
hookChain.onError(context, proxy.getContext(), throwable);
|
||||
throw throwable;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public GlobalContext getGlobalContext() {
|
||||
return globalContext;
|
||||
}
|
||||
|
||||
public static BizGatewayImpl.Builder builder() {
|
||||
return new BizGatewayImpl.Builder();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class Builder {
|
||||
private RpcClient defaultRpcClient;
|
||||
private List<ServiceRpcClient> serviceRpcClients;
|
||||
private AppRuntime appRuntime;
|
||||
private Long blockingMillis;
|
||||
private List<RouteRule> routeRules;
|
||||
private BiConsumer<String, Object[]> alertConsumer;
|
||||
private Function<AppEnvEnum, List<Service>> serviceSupplier;
|
||||
private ScheduledThreadPoolExecutor executor;
|
||||
|
||||
private final static List<ProxyHook> DEFAULT_HEAD_HOOKS = ImmutableList.of(
|
||||
new StopWatchHook(), new ServiceCodeHook());
|
||||
|
||||
private final static List<ProxyHook> DEFAULT_TAIL_HOOKS = ImmutableList.of(
|
||||
new DebugProxyHook());
|
||||
|
||||
@Setter(AccessLevel.NONE)
|
||||
@Getter(AccessLevel.PRIVATE)
|
||||
private List<ProxyHook> proxyHooks = Lists.newArrayList(DEFAULT_HEAD_HOOKS);
|
||||
|
||||
public Builder hook(ProxyHook hook) {
|
||||
proxyHooks.add(hook);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder defaultRpcClient(RpcClient defaultRpcClient) {
|
||||
this.defaultRpcClient = defaultRpcClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为指定的appId配置单独的rpc client
|
||||
*
|
||||
* @param serviceRpcClients
|
||||
* @return
|
||||
*/
|
||||
public Builder serviceRpcClients(List<ServiceRpcClient> serviceRpcClients) {
|
||||
this.serviceRpcClients = serviceRpcClients;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder alertConsumer(BiConsumer<String, Object[]> alertConsumer) {
|
||||
this.alertConsumer = alertConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder serviceSupplier(Function<AppEnvEnum, List<Service>> serviceSupplier) {
|
||||
this.serviceSupplier = serviceSupplier;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder appRuntime(AppRuntime appRuntime) {
|
||||
this.appRuntime = appRuntime;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder blockingMillis(Long blockingMillis) {
|
||||
this.blockingMillis = blockingMillis;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder executor(ScheduledThreadPoolExecutor executor) {
|
||||
this.executor = executor;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder routeRules(List<RouteRule> routeRules) {
|
||||
this.routeRules = routeRules;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
public BizGatewayImpl build() {
|
||||
Preconditions.checkArgument(this.defaultRpcClient != null, "缺少 defaultRpcClient");
|
||||
Preconditions.checkArgument(this.alertConsumer != null, "缺少 alertConsumer");
|
||||
Preconditions.checkArgument(this.serviceSupplier != null, "缺少 serviceSupplier");
|
||||
Preconditions.checkArgument(!CollectionUtils.isEmpty(proxyHooks)
|
||||
&& proxyHooks.get(0) instanceof StopWatchHook, "缺少 StopWatchHook");
|
||||
Preconditions.checkArgument(appRuntime != null, "缺少 appRuntime");
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(appRuntime.getAppName()), "缺少 appName");
|
||||
Preconditions.checkArgument(appRuntime.getEnv() != null, "缺少 appEnv");
|
||||
Preconditions.checkArgument(executor != null, "缺少 executor");
|
||||
|
||||
Map<String, RpcClient> serviceRpcClientMap = Collections.emptyMap();
|
||||
if (!CollectionUtils.isEmpty(serviceRpcClients)) {
|
||||
serviceRpcClientMap = serviceRpcClients.stream()
|
||||
.collect(Collectors.toMap(ServiceRpcClient::getAppName, ServiceRpcClient::getRpcClient));
|
||||
}
|
||||
|
||||
proxyHooks.addAll(DEFAULT_TAIL_HOOKS);
|
||||
GlobalContext globalContext = GlobalContext.builder()
|
||||
.gateAppName(appRuntime.getAppName())
|
||||
.gateEnv(appRuntime.getEnv())
|
||||
.rpcClientProvider(new RpcClientProvider(defaultRpcClient, serviceRpcClientMap))
|
||||
.alertConsumer(alertConsumer)
|
||||
.serviceSupplier(serviceSupplier)
|
||||
.proxyHookChain(new ProxyHookChain(proxyHooks))
|
||||
.version(0L)
|
||||
.debugURIs(ImmutableList.of())
|
||||
.debugStains(ImmutableMap.of())
|
||||
.services(ImmutableMap.of())
|
||||
.supportUnknownApp(false)
|
||||
.blockingMillis(blockingMillis)
|
||||
.localProxies(buildLocalProxies())
|
||||
.executor(executor)
|
||||
.appRuntime(appRuntime)
|
||||
.build();
|
||||
return new BizGatewayImpl(globalContext);
|
||||
}
|
||||
|
||||
private Map<String, GateSettingResp.Proxy> buildLocalProxies() {
|
||||
if (getRouteRules() == null) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
return getRouteRules().stream().map(e -> {
|
||||
return buildProxy(e);
|
||||
}).collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 根据rule构建proxy
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Pair<String, GateSettingResp.Proxy> buildProxy(RouteRule rule) {
|
||||
Preconditions.checkNotNull(rule.getService());
|
||||
|
||||
String proxyName = rule.getProxy().getName();
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(proxyName), "不支持的proxy " + rule.getProxy());
|
||||
|
||||
URI uri;
|
||||
try {
|
||||
uri = new URI(rule.getPath());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new RuntimeException("不支持的rule " + rule, e);
|
||||
}
|
||||
|
||||
String pattern = uri.getPath();
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(pattern));
|
||||
//对于带下划线的serviceCode. uri无法解析为host只能解析为authority. 因此需要多次尝试获取
|
||||
String serviceCode = rule.getService();
|
||||
|
||||
String inFilter = rule.getInFilter();
|
||||
if (!CollectionUtils.isEmpty(rule.getInFilters())) {
|
||||
inFilter = buildFilterString(rule.getInFilters());
|
||||
}
|
||||
String outFilter = rule.getOutFilter();
|
||||
if (!CollectionUtils.isEmpty(rule.getOutFilters())) {
|
||||
outFilter = buildFilterString(rule.getOutFilters());
|
||||
}
|
||||
|
||||
JSONObject params = new JSONObject(Maps.newHashMap(Optional.ofNullable(rule.params).orElseGet(Maps::newHashMap)))
|
||||
.fluentPut("inFilter", inFilter)
|
||||
.fluentPut("apiName", rule.getApiName())
|
||||
.fluentPut("desc", rule.getDesc())
|
||||
.fluentPut("outFilter", outFilter)
|
||||
.fluentPut("collectOperateLog", rule.getCollectOperateLog())
|
||||
.fluentPut("serviceCode", serviceCode)
|
||||
.fluentPut("port", uri.getPort())
|
||||
.fluentPut("host", uri.getHost())
|
||||
.fluentPut("authority", uri.getAuthority());
|
||||
|
||||
// id, version都必须有值,否则创建Proxy的时候会报错
|
||||
GateSettingResp.Proxy proxy = GateSettingResp.Proxy.builder()
|
||||
.id(Long.valueOf(999999L + Math.abs(rule.hashCode()))).version(1L)
|
||||
.feature(0L).name(proxyName)
|
||||
.parameter(params)
|
||||
.status(EntityStatusEnum.ENABLED).build();
|
||||
return Pair.of(pattern, proxy);
|
||||
}
|
||||
|
||||
private String buildFilterString(List<InOutFilter> filters) {
|
||||
String filterStr = filters.stream()
|
||||
.map(InOutFilter::getExpression)
|
||||
.map(this::reverseFilerExpression)
|
||||
.filter(e -> !Strings.isNullOrEmpty(e))
|
||||
.collect(Collectors.joining(FILTER_SEPARATOR));
|
||||
|
||||
String beanStr = filters.stream()
|
||||
.filter(e -> !Strings.isNullOrEmpty(e.getBean()))
|
||||
.map(e -> Optional.ofNullable(e.getConfig())
|
||||
.map(JSONAware::toJSONString)
|
||||
.map(config -> {
|
||||
try {
|
||||
return URLEncoder.encode(config, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
})
|
||||
.map(config -> e.getBean() + CONFIG_DELIMITER + config)
|
||||
.orElse(e.getBean()))
|
||||
.collect(Collectors.joining(BEAN_SEPARATOR));
|
||||
if (!Strings.isNullOrEmpty(beanStr)) {
|
||||
beanStr = EXTENDS_PREFIX + FILTER_BEAN + FILTER_DELIMITER + beanStr;
|
||||
}
|
||||
|
||||
return ImmutableList.of(filterStr, beanStr).stream()
|
||||
.filter(e -> !Strings.isNullOrEmpty(e))
|
||||
.collect(Collectors.joining(FILTER_SEPARATOR));
|
||||
}
|
||||
|
||||
/**
|
||||
* 老的filter的格式是src:target,不便于理解,特别是指定const的值时。
|
||||
* 使用新的{@link InOutFilter}方式配置时,格式为target:src,这里将其转为老的方式传入proxy
|
||||
*
|
||||
* @param expression
|
||||
* @return
|
||||
*/
|
||||
private String reverseFilerExpression(String expression) {
|
||||
if (Strings.isNullOrEmpty(expression)) {
|
||||
return expression;
|
||||
}
|
||||
return Splitter.on(FILTER_SEPARATOR).omitEmptyStrings().trimResults().splitToList(expression).stream()
|
||||
.map(segment -> {
|
||||
if (!StringUtils.contains(segment, FILTER_DELIMITER)
|
||||
|| StringUtils.startsWith(segment, EXTENDS_PREFIX)) {
|
||||
return segment;
|
||||
}
|
||||
String target = StringUtils.substringBefore(segment, FILTER_DELIMITER);
|
||||
String src = StringUtils.substringAfter(segment, FILTER_DELIMITER);
|
||||
return src + FILTER_DELIMITER + target;
|
||||
})
|
||||
.collect(Collectors.joining(FILTER_SEPARATOR));
|
||||
}
|
||||
}
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
@lombok.Builder(toBuilder = true)
|
||||
public final static class RouteRule {
|
||||
@NonNull
|
||||
String apiName;
|
||||
String desc;
|
||||
@NonNull
|
||||
ProxyEnum proxy;
|
||||
String service;
|
||||
@NonNull
|
||||
String path;
|
||||
String inFilter;
|
||||
List<InOutFilter> inFilters;
|
||||
String outFilter;
|
||||
List<InOutFilter> outFilters;
|
||||
Map<String, String> params;
|
||||
Boolean collectOperateLog;
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ProxyEnum {
|
||||
transform("TransformProxy"),
|
||||
black("BlacklistProxy"),
|
||||
regex("RegexProxy"),
|
||||
forward("ForwardProxy"),
|
||||
cachableTransform("CachableTransformProxy"),
|
||||
valueAsBody("ValueAsBodyProxy"),
|
||||
condition("ConditionProxy"),
|
||||
;
|
||||
private String name;
|
||||
}
|
||||
|
||||
@Data
|
||||
@lombok.Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public final static class InOutFilter {
|
||||
private String expression;
|
||||
private String bean;
|
||||
private JSONObject config;
|
||||
}
|
||||
|
||||
@lombok.Builder
|
||||
@Data
|
||||
public static class ServiceRpcClient {
|
||||
/**
|
||||
* 服务在AppCenter中的appName
|
||||
*/
|
||||
private String appName;
|
||||
private RpcClient rpcClient;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,40 @@
|
||||
package cn.axzo.foundation.gateway.support.entity;
|
||||
|
||||
import cn.axzo.foundation.enums.AppEnvEnum;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DebugLogReq {
|
||||
private String gateAppName;
|
||||
private AppEnvEnum gateEnv;
|
||||
private String host;
|
||||
private String requestURI;
|
||||
private String tag;
|
||||
private String message;
|
||||
private String errorStack;
|
||||
private Date timestamp;
|
||||
|
||||
public Map<String, String> toMap() {
|
||||
return ImmutableMap.<String, String>builder()
|
||||
.put("gateAppName", gateAppName)
|
||||
.put("gateEnv", gateEnv.name())
|
||||
.put("host", host)
|
||||
.put("requestURI", requestURI)
|
||||
.put("tag", tag)
|
||||
.put("message", Strings.nullToEmpty(message))
|
||||
.put("errorStack", Strings.nullToEmpty(errorStack))
|
||||
.put("timestamp", String.valueOf(timestamp.getTime()))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,133 @@
|
||||
package cn.axzo.foundation.gateway.support.entity;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Maps;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.MediaType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Data
|
||||
@Builder(toBuilder = true)
|
||||
public class GateResponse {
|
||||
|
||||
/**
|
||||
* HTTP Client返回的Header中已经处理为lower case
|
||||
*/
|
||||
private static final String CONTENT_TYPE = "content-type";
|
||||
private static final String CONTENT_LENGTH = "content-length";
|
||||
private static final String CONTENT_ENCODING = "content-encoding";
|
||||
private static final String TRANSFER_ENCODING = "transfer-encoding";
|
||||
private static final String CONTENT_DISPOSITION = "content-disposition";
|
||||
|
||||
/**
|
||||
* XXX 由于网关客户端使用的OkHttpClient会自动处理gzip并将trunked数据合并,因此网关在透传的数据已经不是gzip及trunked的,需要过滤
|
||||
* 掉后端服务返回的HTTP Header中的content-encoding及transfer-encoding
|
||||
*/
|
||||
private static final Set<String> IGNORE_RESPONSE_HEADERS = ImmutableSet.of(CONTENT_ENCODING, TRANSFER_ENCODING);
|
||||
|
||||
/**
|
||||
* 响应内容
|
||||
*/
|
||||
byte[] content;
|
||||
|
||||
/**
|
||||
* 后端服务response中的Http Header
|
||||
*/
|
||||
@Builder.Default
|
||||
Map<String, List<String>> headers = ImmutableMap.of();
|
||||
|
||||
/** 返回的默认状态码 */
|
||||
HttpStatus status;
|
||||
|
||||
public String getStringContent() {
|
||||
if (Objects.isNull(content)) {
|
||||
log.error("empty content in gate response");
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
Charset charset = Optional.ofNullable(headers.get(CONTENT_TYPE))
|
||||
.flatMap(l -> l.stream().findFirst())
|
||||
.map(MediaType::parse)
|
||||
.map(MediaType::charset)
|
||||
.orElse(Charset.defaultCharset());
|
||||
return new String(content, charset);
|
||||
}
|
||||
|
||||
public void setStringContent(String stringContent) {
|
||||
if (Strings.isNullOrEmpty(stringContent)) {
|
||||
content = new byte[0];
|
||||
return;
|
||||
}
|
||||
Charset charset = Optional.ofNullable(headers.get(CONTENT_TYPE))
|
||||
.flatMap(l -> l.stream().findFirst())
|
||||
.map(MediaType::parse)
|
||||
.map(MediaType::charset)
|
||||
.orElse(Charset.defaultCharset());
|
||||
content = stringContent.getBytes(charset);
|
||||
|
||||
// content改动后,需要更新content-length
|
||||
headers = Maps.newHashMap(headers);
|
||||
headers.put(CONTENT_LENGTH, ImmutableList.of(String.valueOf(content.length)));
|
||||
}
|
||||
|
||||
public boolean hasTextContent() {
|
||||
return Optional.ofNullable(headers.get(CONTENT_TYPE))
|
||||
.flatMap(l -> l.stream().findFirst())
|
||||
.map(MediaType::parse)
|
||||
.map(MediaType::subtype)
|
||||
.map(s -> s.equalsIgnoreCase("json")
|
||||
|| s.equalsIgnoreCase("plain"))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
public boolean isJson() {
|
||||
return Optional.ofNullable(headers.get(CONTENT_TYPE))
|
||||
.flatMap(l -> l.stream().findFirst())
|
||||
.map(MediaType::parse)
|
||||
.map(MediaType::subtype)
|
||||
.map(s -> s.equalsIgnoreCase("json"))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if (hasTextContent()) {
|
||||
return "GateResponse(content=" + getStringContent() + ", headers=" + headers + ")";
|
||||
}
|
||||
int contentLength = Objects.isNull(content) ? 0 : content.length;
|
||||
return "GateResponse(content=byte[" + contentLength + "]" + ", headers=" + headers + ")";
|
||||
}
|
||||
|
||||
public void writeTo(HttpServletResponse response) {
|
||||
try {
|
||||
headers.entrySet().stream()
|
||||
.filter(e -> !IGNORE_RESPONSE_HEADERS.contains(e.getKey().toLowerCase()))
|
||||
.flatMap(e -> e.getValue().stream().map(v -> Pair.of(e.getKey(), v)))
|
||||
.forEach(p -> response.setHeader(p.getLeft(), CONTENT_DISPOSITION.equalsIgnoreCase(p.getLeft()) ?
|
||||
// XXX content-disposition中可能包含非ASCII字符,此时OkHttpClient返回的String可能没有采用
|
||||
// UTF8编码,需要转为UTF8
|
||||
new String(p.getRight().getBytes(), StandardCharsets.UTF_8) : p.getRight()));
|
||||
//设置返回的code. 默认200
|
||||
response.setStatus(Optional.ofNullable(status).orElse(HttpStatus.OK).value());
|
||||
if (content != null) {
|
||||
response.getOutputStream().write(content);
|
||||
}
|
||||
response.flushBuffer();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("写入网关服务响应失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package cn.axzo.foundation.gateway.support.entity;
|
||||
|
||||
import cn.axzo.foundation.enums.EntityStatusEnum;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class GateSettingResp {
|
||||
|
||||
//使用网关配置的watermark作为版本
|
||||
private String gateAppName;
|
||||
private Long version;
|
||||
// 代理配置列表 {urlPattern:Proxy}
|
||||
private Map<String, Proxy> proxies;
|
||||
// 服务列表 {code:host}
|
||||
private Map<String, String> services;
|
||||
// 网关全局配置
|
||||
private Config config;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Proxy {
|
||||
private Long id;
|
||||
private String name;
|
||||
private JSONObject parameter;
|
||||
private Long version;
|
||||
private Long feature;
|
||||
private EntityStatusEnum status;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Config {
|
||||
/**
|
||||
* 需要debug的URI列表
|
||||
*/
|
||||
private List<String> debugURIs;
|
||||
/**
|
||||
* 需要debug的请求参数字段列表 {paramName:partialValue}
|
||||
*/
|
||||
private Map<String, String> debugStains;
|
||||
/**
|
||||
* 是否支持AppCenter中没有记录的app, 默认false, true表示将自动推导其host
|
||||
*/
|
||||
private Boolean supportUnknownApp;
|
||||
}
|
||||
|
||||
public GlobalContext toGlobalContext() {
|
||||
Optional<Config> configOpt = Optional.ofNullable(config);
|
||||
return GlobalContext.builder()
|
||||
.version(version)
|
||||
.debugURIs(configOpt.map(Config::getDebugURIs).orElse(ImmutableList.of()))
|
||||
.debugStains(configOpt.map(Config::getDebugStains).orElse(ImmutableMap.of()))
|
||||
.supportUnknownApp(configOpt.map(Config::getSupportUnknownApp).orElse(null))
|
||||
.build();
|
||||
}
|
||||
|
||||
public static GateSettingResp fromGlobalContext(GlobalContext context) {
|
||||
return GateSettingResp.builder()
|
||||
.services(context.getServices())
|
||||
.version(context.getVersion())
|
||||
.config(Config.builder()
|
||||
.debugURIs(context.getDebugURIs())
|
||||
.debugStains(context.getDebugStains())
|
||||
.supportUnknownApp(context.getSupportUnknownApp())
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
package cn.axzo.foundation.gateway.support.entity;
|
||||
|
||||
import cn.axzo.foundation.enums.AppEnvEnum;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.utils.RpcClientProvider;
|
||||
import cn.axzo.foundation.web.support.AppRuntime;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Builder
|
||||
@ToString(exclude = {"rpcClientProvider", "alertConsumer", "serviceSupplier", "executor", "localProxies",
|
||||
"appRuntime"})
|
||||
@Slf4j
|
||||
public class GlobalContext {
|
||||
|
||||
public static final String BIZ_GATE_CENTER_SERVICE_CODE = "biz-gate";
|
||||
|
||||
@Getter
|
||||
private String gateAppName;
|
||||
@Getter
|
||||
private AppEnvEnum gateEnv;
|
||||
@Getter
|
||||
final private RpcClientProvider rpcClientProvider;
|
||||
@Getter
|
||||
final private BiConsumer<String, Object[]> alertConsumer;
|
||||
@Getter
|
||||
final private Function<AppEnvEnum, List<Service>> serviceSupplier;
|
||||
@Getter
|
||||
/** 全局的代理Hook列表 */
|
||||
final private ProxyHookChain proxyHookChain;
|
||||
@Getter
|
||||
final private Long blockingMillis;
|
||||
@Getter
|
||||
final private Map<String, GateSettingResp.Proxy> localProxies;
|
||||
@Getter
|
||||
/** 网关代理配置最后更新时间 */
|
||||
private Long version;
|
||||
@Getter
|
||||
/** 需要debug的URI列表 */
|
||||
private List<String> debugURIs;
|
||||
@Getter
|
||||
/** 需要debug的请求参数字段列表 {paramName:partialValue} */
|
||||
private Map<String, String> debugStains;
|
||||
@Getter
|
||||
/** 是否支持AppCenter中没有记录的app, 默认false, true表示将自动推导其host */
|
||||
private Boolean supportUnknownApp;
|
||||
@Getter
|
||||
/** 服务配置列表 {appName: serviceHost} */
|
||||
private Map<String, String> services;
|
||||
@Getter
|
||||
/** 用于拉取proxies配置及appHosts配置的线程 */
|
||||
private ScheduledThreadPoolExecutor executor;
|
||||
@Getter
|
||||
private AppRuntime appRuntime;
|
||||
|
||||
public void update(GlobalContext context) {
|
||||
if (Objects.nonNull(context.version)) {
|
||||
version = context.version;
|
||||
}
|
||||
if (Objects.nonNull(context.debugURIs)) {
|
||||
debugURIs = ImmutableList.copyOf(context.debugURIs);
|
||||
}
|
||||
if (Objects.nonNull(context.debugStains)) {
|
||||
debugStains = ImmutableMap.copyOf(context.debugStains);
|
||||
}
|
||||
if (Objects.nonNull(context.services)) {
|
||||
services = ImmutableMap.copyOf(context.services);
|
||||
}
|
||||
if (Objects.nonNull(context.supportUnknownApp)) {
|
||||
supportUnknownApp = context.supportUnknownApp;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean versionChanged(GlobalContext context) {
|
||||
return !Objects.equals(version, context.version);
|
||||
}
|
||||
|
||||
public boolean needDebugURI(String requestURI) {
|
||||
if (Strings.isNullOrEmpty(requestURI)) {
|
||||
return false;
|
||||
}
|
||||
return debugURIs.stream().anyMatch(u -> StringUtils.contains(requestURI, u));
|
||||
}
|
||||
|
||||
public boolean hasDebugStain(Map<String, String> requestParams) {
|
||||
if (CollectionUtils.isEmpty(requestParams)) {
|
||||
return false;
|
||||
}
|
||||
return debugStains.keySet().stream().anyMatch(k -> StringUtils.contains(requestParams.get(k), debugStains.get(k)));
|
||||
}
|
||||
|
||||
public void postAlert(String message, Object... objects) {
|
||||
alertConsumer.accept(message, objects);
|
||||
log.error(message, objects);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package cn.axzo.foundation.gateway.support.entity;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.proxy.Proxy;
|
||||
import cn.axzo.foundation.gateway.support.utils.ParameterFilter;
|
||||
import cn.axzo.foundation.gateway.support.utils.ServiceResolver;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@Builder
|
||||
@ToString(exclude = {"parameterFilter", "serviceResolver"})
|
||||
public class ProxyContext {
|
||||
public static final Long DEFAULT_PROXY_ID = 0L;
|
||||
public static final String DEFAULT_PROXY_NAME = "DefaultProxy";
|
||||
public static final Long DEFAULT_VERSION = 0L;
|
||||
public static final Long DEFAULT_FEATURE = 0L;
|
||||
|
||||
@Getter
|
||||
private Long id;
|
||||
@Getter
|
||||
private String proxyName;
|
||||
@Getter
|
||||
private JSONObject proxyParam;
|
||||
@Getter
|
||||
private Long version;
|
||||
@Getter
|
||||
private Long feature;
|
||||
@Getter
|
||||
private ParameterFilter parameterFilter;
|
||||
@Getter
|
||||
private GlobalContext globalContext;
|
||||
@Getter
|
||||
private ServiceResolver serviceResolver;
|
||||
@Getter
|
||||
private Function<String, Proxy> proxyResolver;
|
||||
}
|
||||
@ -0,0 +1,249 @@
|
||||
package cn.axzo.foundation.gateway.support.entity;
|
||||
|
||||
import cn.axzo.foundation.caller.Caller;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Maps;
|
||||
import lombok.*;
|
||||
import lombok.experimental.Accessors;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.Part;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static cn.axzo.foundation.gateway.support.utils.UrlPathHelper.URI_SEPARATOR;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
/**
|
||||
* 请求网关的上下文信息.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Accessors(chain = true)
|
||||
public class RequestContext {
|
||||
|
||||
public static final String PROFILE_CODE_DELIMITER = "-";
|
||||
|
||||
/**
|
||||
* // XXX Need filter some headers here
|
||||
* 1. 需要过滤掉host, 否则不能转发到下游服务
|
||||
* 2. 需要去除accept-encoding,因为浏览器默认会传accept-encoding=gzip, deflate
|
||||
* 如果显示指定accept-encoding=gzip,那么需要手动处理okhttp3的response,不处理时将会乱码!
|
||||
* 正常情况下,不指定该header属性,okhttp3默认会带上accept-encoding=gzip,此时其会自动处理response。
|
||||
* 3. sso的token至此已使用完(转成session放在attribute中),需要移除;
|
||||
* 避免和请求其他内部服务的jwt token冲突(jwt token也是放在header的Authorization字段)
|
||||
* 4. 发送给后端服务请求的header中需要过滤content-length及content-type,由OkHttpClient来处理
|
||||
* 5. 部分header参考{@link jdk.internal.net.http.common.Utils.DISALLOWED_HEADERS_SET}
|
||||
*/
|
||||
private static final Set<String> IGNORE_REQUEST_HEADERS;
|
||||
|
||||
static {
|
||||
// A case insensitive TreeSet of strings.
|
||||
TreeSet<String> treeSet = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
|
||||
treeSet.addAll(ImmutableSet.of("connection", "content-length",
|
||||
"date", "expect", "from", "host", "upgrade", "via", "warning",
|
||||
"content-type", "accept-encoding", "authorization",
|
||||
"traceparent", "X-NT-Trace-Meta", "X-NT-App-Meta", "X-NT-Route-Context"));
|
||||
IGNORE_REQUEST_HEADERS = Collections.unmodifiableSet(treeSet);
|
||||
}
|
||||
|
||||
private String host;
|
||||
|
||||
private String requestURI;
|
||||
|
||||
private RequestMethod requestMethod;
|
||||
|
||||
private Map<String, String> headers;
|
||||
|
||||
private Map<String, String> requestParams;
|
||||
|
||||
private Map<String, byte[]> multiParts;
|
||||
|
||||
private String requestBody;
|
||||
|
||||
private HttpServletRequest originalRequest;
|
||||
|
||||
/*
|
||||
* 原始调用者信息. 如果是从frontend调用过来. authcaller为空.
|
||||
*/
|
||||
private Optional<Caller> authCaller;
|
||||
|
||||
@Builder.Default
|
||||
private Map<String, Object> env = Maps.newHashMap();
|
||||
|
||||
/**
|
||||
* 解析requestURI以获取serviceCode
|
||||
*/
|
||||
public String getServiceCode() {
|
||||
return Splitter.on(URI_SEPARATOR)
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(requestURI)
|
||||
.stream().findFirst().orElse(StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析requestURI以获取servicePath
|
||||
*/
|
||||
public String getServicePath() {
|
||||
return String.join(URI_SEPARATOR, Splitter.on(URI_SEPARATOR)
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(requestURI)
|
||||
.stream().skip(1).collect(toList()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取多域名部署时gateHost的profileCode, 如http://foo-1.xxx.com, 1则为profileCode.
|
||||
*
|
||||
* @return 没有profileCode则返回空字符串
|
||||
*/
|
||||
public String getProfileCode() {
|
||||
// Example: http://foo-1.xxx.com -> http://foo-1
|
||||
String startSegment = StringUtils.substringBefore(host, ".");
|
||||
if (Strings.isNullOrEmpty(startSegment)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
String customNum = StringUtils.substringAfterLast(startSegment, PROFILE_CODE_DELIMITER);
|
||||
return StringUtils.isNumeric(customNum) ? customNum : StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将HttpServletRequest包装成RequestContext对象, 但session需要手动设置.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static RequestContext wrap(HttpServletRequest request, Optional<Caller> caller) {
|
||||
Preconditions.checkArgument(caller != null);
|
||||
Preconditions.checkArgument(request != null);
|
||||
|
||||
RequestContext context = new RequestContext();
|
||||
context.setHost(request.getServerName());
|
||||
context.setRequestURI(request.getRequestURI());
|
||||
context.setRequestMethod(HttpMethod.fromHttpRequest(request).getRequestMethod());
|
||||
context.setAuthCaller(caller);
|
||||
|
||||
Map<String, String> heads = Maps.newHashMap();
|
||||
Enumeration<String> headerNames = request.getHeaderNames();
|
||||
while (Objects.nonNull(headerNames) && headerNames.hasMoreElements()) {
|
||||
String name = headerNames.nextElement();
|
||||
if (IGNORE_REQUEST_HEADERS.contains(name)) {
|
||||
continue;
|
||||
}
|
||||
heads.put(name, request.getHeader(name));
|
||||
}
|
||||
context.setHeaders(heads);
|
||||
|
||||
// request.getParameterMap()返回的参数值是String[], 需要进行转换
|
||||
Map<String, String> requestParams = Maps.newHashMap();
|
||||
Enumeration<String> paramNames = request.getParameterNames();
|
||||
while (Objects.nonNull(paramNames) && paramNames.hasMoreElements()) {
|
||||
String key = paramNames.nextElement();
|
||||
requestParams.put(key, request.getParameter(key));
|
||||
}
|
||||
context.setRequestParams(requestParams);
|
||||
|
||||
// 从HttpServletRequest读取multi-parts的字节流放入Map
|
||||
try {
|
||||
Collection<Part> parts = request.getParts();
|
||||
if (!CollectionUtils.isEmpty(parts)) {
|
||||
context.setMultiParts(parts.stream().collect(toMap(Part::getName, e -> {
|
||||
try {
|
||||
return toByteArray(e.getInputStream());
|
||||
} catch (IOException e1) {
|
||||
throw new RuntimeException(String.format("read multi-parts byte array error! host: %s, requestURI: %s",
|
||||
context.getHost(), context.getRequestURI()), e1);
|
||||
}
|
||||
})));
|
||||
// request.getParts()不为空时,说明时上传文件,之前已有spring的xxxMultiPartResolver读取了输入流(request.getInputStream()),
|
||||
// 所有信息(包括其他参数)都已放到multiParts中,无需执行后续逻辑,否则后续的request.getReader()将会报错
|
||||
return context.setOriginalRequest(request);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(String.format("get multi-parts error from HttpServletRequest! host: %s, requestURI: %s",
|
||||
context.getHost(), context.getRequestURI()), e);
|
||||
} catch (ServletException e) {
|
||||
// no multi-parts, ignore
|
||||
}
|
||||
|
||||
try {
|
||||
context.setRequestBody(Optional.ofNullable(request.getReader())
|
||||
.map(BufferedReader::lines)
|
||||
.map(s -> s.collect(Collectors.joining(System.lineSeparator())))
|
||||
.orElse(StringUtils.EMPTY));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(String.format("get request body error from HttpServletRequest! host: %s, requestURI: %s",
|
||||
context.getHost(), context.getRequestURI()), e);
|
||||
}
|
||||
|
||||
return context.setOriginalRequest(request);
|
||||
}
|
||||
|
||||
private static byte[] toByteArray(InputStream input) throws IOException {
|
||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[4096];
|
||||
int length = 0;
|
||||
while (length != -1) {
|
||||
length = input.read(buffer);
|
||||
if (length > 0) {
|
||||
output.write(buffer, 0, length);
|
||||
}
|
||||
}
|
||||
return output.toByteArray();
|
||||
}
|
||||
|
||||
public interface RequestMethod {
|
||||
GateResponse request(RpcClient client, String url, RequestParams params);
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum HttpMethod {
|
||||
GET((client, url, params) ->
|
||||
client.get(url, (c, h) -> GateResponse.builder().content(c).headers(lowerCaseHeaderName(h)).build(), params)
|
||||
),
|
||||
POST((client, url, params) ->
|
||||
client.post(url, (c, h) -> GateResponse.builder().content(c).headers(lowerCaseHeaderName(h)).build(), params)
|
||||
),
|
||||
PUT((client, url, params) ->
|
||||
client.put(url, (c, h) -> GateResponse.builder().content(c).headers(lowerCaseHeaderName(h)).build(), params)
|
||||
),
|
||||
DELETE((client, url, params) ->
|
||||
client.delete(url, (c, h) -> GateResponse.builder().content(c).headers(lowerCaseHeaderName(h)).build(), params)
|
||||
);
|
||||
|
||||
private RequestMethod requestMethod;
|
||||
|
||||
private static HttpMethod fromHttpRequest(HttpServletRequest request) {
|
||||
try {
|
||||
return HttpMethod.valueOf(request.getMethod());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(String.format("http method not support! host: %s, requestURI: %s",
|
||||
request.getServerName(), request.getRequestURI()), e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, List<String>> lowerCaseHeaderName(Map<String, List<String>> headers) {
|
||||
if (CollectionUtils.isEmpty(headers)) {
|
||||
return ImmutableMap.of();
|
||||
}
|
||||
return headers.entrySet().stream()
|
||||
.collect(Collectors.toMap(e -> StringUtils.lowerCase(e.getKey()), Map.Entry::getValue));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package cn.axzo.foundation.gateway.support.entity;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Data
|
||||
@Builder
|
||||
public class Service {
|
||||
/**
|
||||
* 服务在AppCenter中的appId
|
||||
*/
|
||||
private String appId;
|
||||
/**
|
||||
* 服务在AppCenter中的服务名
|
||||
*/
|
||||
private String appName;
|
||||
/**
|
||||
* 服务host的supplier
|
||||
*/
|
||||
private Supplier<String> hostSupplier;
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package cn.axzo.foundation.gateway.support.exception;
|
||||
|
||||
/**
|
||||
* API不存在导致代理失败的异常.
|
||||
* <p>
|
||||
* 实施时GateServer的ExceptionResolver可以拦截该异常做特殊处理, 如统一为HTTP标准状态码返回: ModelAndView.setStatus(HttpStatus.NOT_FOUND).
|
||||
* </p>
|
||||
*/
|
||||
public class ApiNotFoundException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8821155936941903240L;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public ApiNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public ApiNotFoundException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package cn.axzo.foundation.gateway.support.exception;
|
||||
|
||||
/**
|
||||
* API未经授权访问的异常.
|
||||
* <p>
|
||||
* 实施时GateServer的ExceptionResolver可以拦截该异常做特殊处理, 如统一为HTTP标准状态码返回: ModelAndView.setStatus(HttpStatus.UNAUTHORIZED).
|
||||
* </p>
|
||||
*/
|
||||
public class ApiUnauthorizedException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -7679511760689237798L;
|
||||
|
||||
public ApiUnauthorizedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public ApiUnauthorizedException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package cn.axzo.foundation.gateway.support.exception;
|
||||
|
||||
/**
|
||||
* ParameterFilter处理输入参数时输入参数中指定的字段不存在
|
||||
*/
|
||||
public class InputFieldAbsentException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = -5646417429309513569L;
|
||||
|
||||
public InputFieldAbsentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public InputFieldAbsentException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,17 @@
|
||||
package cn.axzo.foundation.gateway.support.exception;
|
||||
|
||||
/**
|
||||
* ParameterFilter处理返回对象时返回对象中指定的字段不存在
|
||||
*/
|
||||
public class OutputFieldAbsentException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 8830968633694688099L;
|
||||
|
||||
public OutputFieldAbsentException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public OutputFieldAbsentException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
package cn.axzo.foundation.gateway.support.plugin;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import lombok.NonNull;
|
||||
|
||||
/**
|
||||
* 针对每个Request, 从发现Proxy到Proxy内部调用的每个关键节点的Hook接口.
|
||||
*
|
||||
* <p>invoke chain: findProxy -> preFilterIn -> preRequest -> postResponse -> postFilterOut</p>
|
||||
* <p>当有多个Hook时, 以post开头的方法应按Hook注入的顺序反转执行, 其他方法则按Hook顺序正常执行.</p>
|
||||
* <p>假设按顺序注入了2个Hooks: A和B, 则实际执行的完整Chain应该如下所示:</p>
|
||||
* <pre>
|
||||
* A.findProxy -> B.findProxy ->
|
||||
* A.preFilterIn -> B.preFilterIn ->
|
||||
* A.preRequest -> B.preRequest ->
|
||||
* B.postResponse -> A.postResponse ->
|
||||
* B.postFilterOut -> A.postFilterOut(End)
|
||||
* </pre>
|
||||
*/
|
||||
public interface ProxyHook {
|
||||
|
||||
/**
|
||||
* 在发现代理类之后且调用request之前执行.
|
||||
*
|
||||
* @param reqContext 当前请求的上下文信息
|
||||
* @param proxyContext 当前代理的上下文信息
|
||||
*/
|
||||
default void findProxy(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在解析service之前执行, 通过该hook可以将serviceCode按自定义规则进行转换.
|
||||
* XXX 需要注意的是当有多个hook存在时, 每个hook处理后的结果会传递给下一个, 并使用最后一个hook返回的serviceCode.
|
||||
*
|
||||
* @param requestContext
|
||||
* @param proxyContext
|
||||
* @param serviceCode
|
||||
* @return
|
||||
*/
|
||||
default String preResolveService(RequestContext requestContext, ProxyContext proxyContext, String serviceCode) {
|
||||
return serviceCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对请求参数调用Filter之前执行.
|
||||
*
|
||||
* @param reqContext 当前请求的上下文信息
|
||||
* @param proxyContext 当前代理的上下文信息
|
||||
*/
|
||||
default void preFilterIn(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在Proxy调用业务方RPC之前执行.
|
||||
* @param reqContext 当前请求的上下文信息
|
||||
* @param proxyContext 当前代理的上下文信息
|
||||
* @param rpcClient 调用业务方RPC的使用的Client
|
||||
* @param postURL 调用业务方RPC的真实URL
|
||||
* @param postParams 调用业务方RPC的请求参数
|
||||
* @return
|
||||
*/
|
||||
default RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext,
|
||||
RpcClient rpcClient, String postURL, @NonNull final RequestParams postParams) {
|
||||
return postParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在Proxy调用业务方RPC之后执行.
|
||||
* @param reqContext 当前请求的上下文信息
|
||||
* @param proxyContext 当前代理的上下文信息
|
||||
* @param response RPC Response返回对象
|
||||
* @return
|
||||
*/
|
||||
default GateResponse postResponse(RequestContext reqContext, ProxyContext proxyContext, final GateResponse response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对返回结果调用Filter之后执行.
|
||||
*
|
||||
* @param reqContext 当前请求的上下文信息
|
||||
* @param proxyContext 当前代理的上下文信息
|
||||
* @param response RPC Response返回对象
|
||||
*/
|
||||
default void postFilterOut(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 在发现代理类之后到调用request当发生异常时执行.
|
||||
*
|
||||
* @param reqContext 当前请求的上下文信息
|
||||
* @param proxyContext 当前代理的上下文信息
|
||||
* @param throwable 捕获并将抛出的异常类(无论Hooks是否处理该异常, 它都应在onError调用后被抛出)
|
||||
*/
|
||||
default void onError(RequestContext reqContext, ProxyContext proxyContext, Throwable throwable) {
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package cn.axzo.foundation.gateway.support.plugin;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.Getter;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
/**
|
||||
* 提供对一组ProxyHook的钩子方法调用.
|
||||
*/
|
||||
public final class ProxyHookChain {
|
||||
|
||||
public static final ProxyHookChain EMPTY = new ProxyHookChain(null);
|
||||
|
||||
@Getter
|
||||
private List<ProxyHook> hooks;
|
||||
|
||||
public ProxyHookChain(List<ProxyHook> hooks) {
|
||||
if (CollectionUtils.isEmpty(hooks)) {
|
||||
this.hooks = Collections.EMPTY_LIST;
|
||||
} else {
|
||||
this.hooks = Lists.newArrayList(hooks);
|
||||
}
|
||||
}
|
||||
|
||||
public void findProxy(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
hooks.forEach(hook -> hook.findProxy(reqContext, proxyContext));
|
||||
}
|
||||
|
||||
public String preResolveService(RequestContext reqContext, ProxyContext proxyContext, String serviceCode) {
|
||||
for (ProxyHook hook : hooks) {
|
||||
serviceCode = hook.preResolveService(reqContext, proxyContext, serviceCode);
|
||||
}
|
||||
return serviceCode;
|
||||
}
|
||||
|
||||
public void preFilterIn(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
hooks.forEach(hook -> hook.preFilterIn(reqContext, proxyContext));
|
||||
}
|
||||
|
||||
public RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext,
|
||||
RpcClient rpcClient, String postURL, final RequestParams postParams) {
|
||||
RequestParams res = postParams;
|
||||
for (final ProxyHook hook : hooks) {
|
||||
res = hook.preRequest(reqContext, proxyContext, rpcClient, postURL, res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public GateResponse postResponse(RequestContext reqContext, ProxyContext proxyContext, final GateResponse response) {
|
||||
GateResponse res = response;
|
||||
for (ListIterator<ProxyHook> it = hooks.listIterator(hooks.size()); it.hasPrevious();) {
|
||||
res = it.previous().postResponse(reqContext, proxyContext, res);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public void postFilterOut(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
for (ListIterator<ProxyHook> it = hooks.listIterator(hooks.size()); it.hasPrevious();) {
|
||||
it.previous().postFilterOut(reqContext, proxyContext, response);
|
||||
}
|
||||
}
|
||||
|
||||
public void onError(RequestContext reqContext, ProxyContext proxyContext, Throwable throwable) {
|
||||
hooks.forEach(hook -> hook.onError(reqContext, proxyContext, throwable));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,155 @@
|
||||
package cn.axzo.foundation.gateway.support.plugin.impl;
|
||||
|
||||
import cn.axzo.foundation.exception.BusinessException;
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import cn.axzo.foundation.util.FastjsonUtils;
|
||||
import cn.axzo.foundation.util.TraceUtils;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 接口请求日志打印
|
||||
*/
|
||||
@Slf4j
|
||||
public class ApiLogHook implements ProxyHook {
|
||||
private static final ThreadLocal<RequestParamsHolder> REQUEST_PARAMS_THREAD_LOCAL = new ThreadLocal<>();
|
||||
private static final int API_RESPONSE_CONTENT_PRINT_LENGTH = 2048;
|
||||
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
|
||||
private static final Set<String> DEFAULT_IGNORE_HEADER_NAMES = ImmutableSet.of("User-Agent", "Authorization", "_EMPLOYEE_PRINCIPAL", "Bfs-Authorization");
|
||||
|
||||
private final int maxLogLength;
|
||||
private final Predicate<HookContext> logEnableHook;
|
||||
private final List<String> ignorePathPatterns;
|
||||
private final Set<String> ignoreHeaderNames;
|
||||
private final boolean printOriginalParams;
|
||||
private final Set<Class<? extends Exception>> warningExceptions;
|
||||
|
||||
public ApiLogHook() {
|
||||
this(null, null, null, null, false, null);
|
||||
}
|
||||
|
||||
@Builder
|
||||
public ApiLogHook(Predicate<HookContext> logEnableHook, Integer maxLogLength, List<String> ignorePathPatterns,
|
||||
Set<String> ignoreHeaderNames, Boolean printOriginalParams, Set<Class<? extends Exception>> warningExceptions) {
|
||||
this.logEnableHook = Optional.ofNullable(logEnableHook).orElse(hookContext -> true);
|
||||
this.maxLogLength = Optional.ofNullable(maxLogLength).orElse(API_RESPONSE_CONTENT_PRINT_LENGTH);
|
||||
this.ignorePathPatterns = Optional.ofNullable(ignorePathPatterns).orElse(ImmutableList.of());
|
||||
this.ignoreHeaderNames = Optional.ofNullable(ignoreHeaderNames).orElse(DEFAULT_IGNORE_HEADER_NAMES);
|
||||
this.printOriginalParams = BooleanUtils.isTrue(printOriginalParams);
|
||||
// 默认 BusinessException 异常不打印ERROR级别日志.
|
||||
this.warningExceptions = Optional.ofNullable(warningExceptions).orElse(ImmutableSet.of(BusinessException.class));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext,
|
||||
RpcClient rpcClient, String postURL, @NonNull RequestParams postParams) {
|
||||
REQUEST_PARAMS_THREAD_LOCAL.remove();
|
||||
HookContext hookContext = HookContext.builder()
|
||||
.postURL(postURL)
|
||||
.params(postParams)
|
||||
.context(reqContext)
|
||||
.build();
|
||||
REQUEST_PARAMS_THREAD_LOCAL.set(RequestParamsHolder.builder()
|
||||
.requestParams(postParams)
|
||||
.logEnabled(logEnableHook.test(hookContext))
|
||||
.build());
|
||||
return postParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postFilterOut(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
RequestParamsHolder requestParamsHolder = REQUEST_PARAMS_THREAD_LOCAL.get();
|
||||
if (requestParamsHolder == null || !requestParamsHolder.isLogEnabled()) {
|
||||
return;
|
||||
}
|
||||
if (ignorePathPatterns.stream().anyMatch(p -> antPathMatcher.match(p, reqContext.getRequestURI()))) {
|
||||
return;
|
||||
}
|
||||
|
||||
String param = StringUtils.EMPTY;
|
||||
if (requestParamsHolder.getRequestParams() != null && requestParamsHolder.getRequestParams().getData() != null) {
|
||||
param = FastjsonUtils.toJsonPettyLogString(requestParamsHolder.getRequestParams().getData());
|
||||
}
|
||||
|
||||
Map<String, String> logHeaders = reqContext.getHeaders().entrySet().stream()
|
||||
.filter(p -> !ignoreHeaderNames.contains(p.getKey()))
|
||||
.collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue()));
|
||||
|
||||
|
||||
if (printOriginalParams) {
|
||||
String originalParam = Optional.ofNullable(reqContext.getRequestBody())
|
||||
.orElseGet(() -> Optional.ofNullable(reqContext.getRequestParams()).map(String::valueOf).orElse(StringUtils.EMPTY));
|
||||
log.info("api log hook, url = {}, traceId = {}, originalParam = {}, param = {}, header = {}, result = {}",
|
||||
reqContext.getRequestURI(), TraceUtils.getTraceId(), originalParam, param, logHeaders,
|
||||
StringUtils.left(response.getStringContent(), maxLogLength));
|
||||
} else {
|
||||
log.info("api log hook, url = {}, traceId = {}, param = {}, header = {}, result = {}",
|
||||
reqContext.getRequestURI(), TraceUtils.getTraceId(), param, logHeaders,
|
||||
StringUtils.left(response.getStringContent(), maxLogLength));
|
||||
}
|
||||
REQUEST_PARAMS_THREAD_LOCAL.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(RequestContext reqContext, ProxyContext proxyContext, Throwable throwable) {
|
||||
Optional<RequestParams> requestParams = Optional.ofNullable(REQUEST_PARAMS_THREAD_LOCAL.get())
|
||||
.map(RequestParamsHolder::getRequestParams);
|
||||
String param = requestParams.map(RequestParams::getData).map(FastjsonUtils::toJsonPettyLogString)
|
||||
.orElse(StringUtils.EMPTY);
|
||||
Map<String, String> header = requestParams.map(RequestParams::getHeaders).orElse(ImmutableMap.of());
|
||||
|
||||
String originalParam = Optional.ofNullable(reqContext.getRequestBody())
|
||||
.orElseGet(() -> Optional.ofNullable(reqContext.getRequestParams()).map(String::valueOf)
|
||||
.orElse(StringUtils.EMPTY));
|
||||
Map<String, String> originalHeader = reqContext.getHeaders();
|
||||
|
||||
boolean warningLog = warningExceptions.stream().anyMatch(exceptionClz -> throwable.getClass().isAssignableFrom(exceptionClz));
|
||||
if (warningLog) {
|
||||
log.warn("api log hook, url = {}, traceId = {}, param = {}, header = {}, " +
|
||||
"originalParam = {}, originalHeader = {}, catch exception",
|
||||
reqContext.getRequestURI(), TraceUtils.getTraceId(), param, header,
|
||||
originalParam, originalHeader, throwable);
|
||||
} else {
|
||||
log.error("api log hook, url = {}, traceId = {}, param = {}, header = {}, " +
|
||||
"originalParam = {}, originalHeader = {}, catch exception",
|
||||
reqContext.getRequestURI(), TraceUtils.getTraceId(), param, header,
|
||||
originalParam, originalHeader, throwable);
|
||||
}
|
||||
REQUEST_PARAMS_THREAD_LOCAL.remove();
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
public static class HookContext {
|
||||
String postURL;
|
||||
RequestParams params;
|
||||
RequestContext context;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
private static class RequestParamsHolder {
|
||||
RequestParams requestParams;
|
||||
boolean logEnabled;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,215 @@
|
||||
package cn.axzo.foundation.gateway.support.plugin.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.*;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import cn.axzo.foundation.gateway.support.proxy.ProxyFeature;
|
||||
import cn.axzo.foundation.gateway.support.utils.ParameterFormatter;
|
||||
import cn.axzo.foundation.gateway.support.utils.UrlPathHelper;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.google.common.base.Throwables;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 用于调试Proxy的Hook.
|
||||
*/
|
||||
@Slf4j
|
||||
public class DebugProxyHook implements ProxyHook {
|
||||
|
||||
// 日志中支持的消息内容最大长度
|
||||
private static final int MAX_MESSAGE_LENGTH = 10240;
|
||||
private static final DebugTag DEFAULT_DEBUG_TAG = new DebugTag();
|
||||
private static final ThreadLocal<DebugTag> DEBUG_TAG_LOCAL = new ThreadLocal<>();
|
||||
private static final String POST_DEBUG_LOG_PATH = "/debug-log/post";
|
||||
private static final String PRINT_VERBOSE_PARAM_NAME = "__print_verbose";
|
||||
|
||||
private static class DebugTag {
|
||||
private String debugLogTag = StringUtils.EMPTY;
|
||||
private boolean needPrint = false;
|
||||
private boolean needCollect = false;
|
||||
}
|
||||
|
||||
//创建一个默认2个线程, 最大5个线程, 任务最多执行5秒, 最大容量2048的线程池
|
||||
private static ExecutorService executorService = new ThreadPoolExecutor(2, 5, 3L,
|
||||
TimeUnit.SECONDS,
|
||||
new LinkedBlockingQueue(2048),
|
||||
new ThreadPoolExecutor.DiscardPolicy() {
|
||||
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
|
||||
log.error("DebugProxyHook workQueue超过上限,丢弃任务。");
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
public void findProxy(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
initializeDebugTag(reqContext, proxyContext);
|
||||
if (needPrint() || needCollect()) {
|
||||
printOrCollectLog(reqContext, proxyContext,
|
||||
"[findProxy] host: {}, requestURI: {}, proxyContext: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), proxyContext);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String preResolveService(RequestContext reqContext, ProxyContext proxyContext, String serviceCode) {
|
||||
if (needPrint() || needCollect()) {
|
||||
printOrCollectLog(reqContext, proxyContext,
|
||||
"[preResolveService] host: {}, requestURI: {}, serviceCode: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), serviceCode);
|
||||
}
|
||||
return serviceCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preFilterIn(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
if (needPrint() || needCollect()) {
|
||||
printOrCollectLog(reqContext, proxyContext,
|
||||
"[preFilterIn] host: {}, requestURI: {}, requestParams: {}, inFilter: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), reqContext.getRequestParams(),
|
||||
proxyContext.getParameterFilter().getInFilters());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext,
|
||||
RpcClient rpcClient, String postURL, RequestParams postParams) {
|
||||
if (needPrint() || needCollect()) {
|
||||
printOrCollectLog(reqContext, proxyContext,
|
||||
"[preRequest] host: {}, requestURI: {}, rpcClient: {}, postURL: {}, postParams: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), rpcClient, postURL, postParams);
|
||||
}
|
||||
return postParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse postResponse(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
if (needPrint() || needCollect()) {
|
||||
printOrCollectLog(reqContext, proxyContext,
|
||||
"[postResponse] host: {}, requestURI: {}, response: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), response);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postFilterOut(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
if (needPrint() || needCollect()) {
|
||||
printOrCollectLog(reqContext, proxyContext,
|
||||
"[postFilterOut] host: {}, requestURI: {}, response: {}, outFilters: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), response,
|
||||
proxyContext.getParameterFilter().getOutFilters());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(RequestContext reqContext, ProxyContext proxyContext, Throwable throwable) {
|
||||
if (needPrint() || needCollect()) {
|
||||
printOrCollectLog(reqContext, proxyContext,
|
||||
"[onError] host: {}, requestURI: {}, proxyContext: {}, throwable: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), proxyContext, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
void initializeDebugTag(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
DebugTag tag = DEBUG_TAG_LOCAL.get();
|
||||
if (Objects.isNull(tag)) {
|
||||
tag = new DebugTag();
|
||||
DEBUG_TAG_LOCAL.set(tag);
|
||||
}
|
||||
tag.debugLogTag = UUID.randomUUID().toString().replace("-", "");
|
||||
tag.needPrint = ProxyFeature.needPrintLog(proxyContext.getFeature());
|
||||
tag.needCollect = ProxyFeature.needCollectLog(proxyContext.getFeature());
|
||||
|
||||
// 支持通过url parameter打开debug信息
|
||||
if (!CollectionUtils.isEmpty(reqContext.getRequestParams())
|
||||
&& reqContext.getRequestParams().containsKey(PRINT_VERBOSE_PARAM_NAME)) {
|
||||
tag.needPrint = true;
|
||||
}
|
||||
|
||||
if (tag.needPrint && tag.needCollect) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查全局配置,判断请求是否符合打印或收集条件
|
||||
GlobalContext globalContext = proxyContext.getGlobalContext();
|
||||
if (Objects.nonNull(globalContext)) {
|
||||
boolean needDebug = globalContext.needDebugURI(reqContext.getRequestURI())
|
||||
|| globalContext.hasDebugStain(reqContext.getRequestParams());
|
||||
tag.needPrint = tag.needPrint || needDebug;
|
||||
tag.needCollect = tag.needCollect || needDebug;
|
||||
}
|
||||
}
|
||||
|
||||
private String debugLogTag() {
|
||||
return Optional.ofNullable(DEBUG_TAG_LOCAL.get()).orElse(DEFAULT_DEBUG_TAG).debugLogTag;
|
||||
}
|
||||
|
||||
boolean needPrint() {
|
||||
return Optional.ofNullable(DEBUG_TAG_LOCAL.get()).orElse(DEFAULT_DEBUG_TAG).needPrint;
|
||||
}
|
||||
|
||||
boolean needCollect() {
|
||||
return Optional.ofNullable(DEBUG_TAG_LOCAL.get()).orElse(DEFAULT_DEBUG_TAG).needCollect;
|
||||
}
|
||||
|
||||
private void printOrCollectLog(RequestContext reqContext, ProxyContext proxyContext,
|
||||
String message, Object... objects) {
|
||||
if (needPrint()) {
|
||||
log.info(message, objects);
|
||||
}
|
||||
if (needCollect()) {
|
||||
DebugLogReq debugLog = buildFromParameter(reqContext, proxyContext, message, objects);
|
||||
executorService.submit(() -> postToServer(proxyContext, debugLog));
|
||||
}
|
||||
}
|
||||
|
||||
private DebugLogReq buildFromParameter(RequestContext reqContext, ProxyContext proxyContext,
|
||||
String message, Object... objects) {
|
||||
DebugLogReq.DebugLogReqBuilder builder = DebugLogReq.builder();
|
||||
builder.gateAppName(proxyContext.getGlobalContext().getGateAppName())
|
||||
.gateEnv(proxyContext.getGlobalContext().getGateEnv())
|
||||
.host(reqContext.getHost())
|
||||
.requestURI(reqContext.getRequestURI())
|
||||
.tag(debugLogTag())
|
||||
.timestamp(new Date());
|
||||
if (objects == null) {
|
||||
builder.message(message)
|
||||
.errorStack(null);
|
||||
} else {
|
||||
builder.message(formatMessage(message, objects));
|
||||
if (objects[objects.length - 1] instanceof Throwable) {
|
||||
Throwable throwable = (Throwable) objects[objects.length - 1];
|
||||
builder.errorStack(Throwables.getStackTraceAsString(throwable));
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private String formatMessage(String message, Object... objects) {
|
||||
String formattedMessage = ParameterFormatter.format(message, objects);
|
||||
return StringUtils.left(formattedMessage, MAX_MESSAGE_LENGTH);
|
||||
}
|
||||
|
||||
private void postToServer(ProxyContext proxyContext, DebugLogReq debugLog) {
|
||||
try {
|
||||
String host = proxyContext.getServiceResolver().getHost(proxyContext.getGlobalContext().getGateEnv(),
|
||||
GlobalContext.BIZ_GATE_CENTER_SERVICE_CODE);
|
||||
String url = UrlPathHelper.join(host, POST_DEBUG_LOG_PATH);
|
||||
|
||||
RpcClient rpcClient = proxyContext.getServiceResolver().getRpcClient(GlobalContext.BIZ_GATE_CENTER_SERVICE_CODE);
|
||||
rpcClient.post(url, debugLog, Object.class);
|
||||
} catch (Exception e) {
|
||||
log.error("post debug log error, debugLog = {}", debugLog, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,665 @@
|
||||
package cn.axzo.foundation.gateway.support.plugin.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import cn.axzo.foundation.page.IPageReq;
|
||||
import cn.axzo.foundation.page.PageResp;
|
||||
import cn.axzo.foundation.result.ResultCode;
|
||||
import cn.axzo.foundation.util.DataAssembleHelper;
|
||||
import cn.axzo.foundation.util.FastjsonUtils;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.JSONPath;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
/**
|
||||
* 对请求外部扩展插件。 使用如下:
|
||||
* 1. 在 rule 定义中 配置 inFilter=__extend.bean.beanName,param1:xx, param2:xx,
|
||||
* 2. 在 hook 中注册 transform resolver。 通过 applicationcontext 去获取 bean 对象实例
|
||||
* 3. 外部bean 实现 RequestFilter 接口。
|
||||
* <pre>
|
||||
* .hook(RequestFilterHook.builder().filterBeanResolver(bean->{
|
||||
* return applicationContext.getBean(bean, RequestFilterHook.RequestFilter.class);
|
||||
* }).build())
|
||||
*
|
||||
* @Bean
|
||||
* private RequestFilterHook.RequestFilter bulletinListFilter(){
|
||||
* return new RequestFilterHook.RequestFilter() {
|
||||
* @Override
|
||||
* public JSON filterIn(RequestContext reqContext, JSON params) {
|
||||
* return new JSONObject((JSONObject)params).fluentPut("__test", "test_req");
|
||||
* }
|
||||
*
|
||||
* @Override
|
||||
* public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
* return new JSONObject((JSONObject)response).fluentPut("__test", "test_resp");
|
||||
* }
|
||||
* };
|
||||
* }
|
||||
* </pre>
|
||||
*/
|
||||
@Slf4j
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class RequestFilterHook implements ProxyHook {
|
||||
public static final String FILTER_BEAN = "bean";
|
||||
public static final String BEAN_SEPARATOR = "|";
|
||||
public static final String CONFIG_DELIMITER = "?";
|
||||
|
||||
/**
|
||||
* key: bean 名称
|
||||
* value: transformer 对象。
|
||||
*/
|
||||
@NonNull
|
||||
private Function<String, RequestFilter> filterBeanResolver;
|
||||
|
||||
@Override
|
||||
public RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext, RpcClient rpcClient, String postURL, RequestParams postParams) {
|
||||
if (proxyContext.getParameterFilter() == null || proxyContext.getParameterFilter().getInFilterExtends().isEmpty()) {
|
||||
return postParams;
|
||||
}
|
||||
|
||||
List<FilterBean> beans = parseBeans(proxyContext.getParameterFilter().getInFilterExtends());
|
||||
if (beans.isEmpty()) {
|
||||
return postParams;
|
||||
}
|
||||
|
||||
if (!(postParams instanceof RequestParams.BodyParams)) {
|
||||
log.warn("postParam类型错误, classType: {}, postParam: {}",
|
||||
postParams.getClass().getSimpleName(), JSONObject.toJSONString(postParams));
|
||||
throw new IllegalArgumentException(String.format("postParam类型错误, classType: %s",
|
||||
postParams.getClass().getSimpleName()));
|
||||
}
|
||||
JSON requestBody = (JSON) postParams.getData();
|
||||
for (FilterBean bean : beans) {
|
||||
RequestFilter requestFilter = filterBeanResolver.apply(bean.getName());
|
||||
Preconditions.checkState(requestFilter != null, bean.getName() + " 没在系统注册");
|
||||
requestBody = requestFilter.filterIn(reqContext, requestBody, bean.getConfig());
|
||||
}
|
||||
|
||||
// 只支持 json 格式
|
||||
return ((RequestParams.BodyParams) postParams).toBuilder().content(requestBody).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse postResponse(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
if (proxyContext.getParameterFilter() == null || proxyContext.getParameterFilter().getOutFilterExtends().isEmpty()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
List<FilterBean> beans = parseBeans(proxyContext.getParameterFilter().getOutFilterExtends());
|
||||
if (beans.isEmpty()) {
|
||||
return response;
|
||||
}
|
||||
|
||||
String content = response.getStringContent();
|
||||
if (Strings.isNullOrEmpty(content)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
// 只支持 json 格式
|
||||
Preconditions.checkState(response.isJson(), "RequestTransformHook只支持 json 格式的应答 " + reqContext.getRequestURI());
|
||||
JSON responseBody = (JSON) JSON.parse(content);
|
||||
|
||||
for (FilterBean bean : beans) {
|
||||
RequestFilter requestFilter = filterBeanResolver.apply(bean.getName());
|
||||
Preconditions.checkState(requestFilter != null, bean.getName() + " 没在系统注册");
|
||||
responseBody = requestFilter.filterOut(reqContext, responseBody, bean.getConfig());
|
||||
}
|
||||
|
||||
GateResponse res = response.toBuilder().build();
|
||||
res.setStringContent(JSON.toJSONString(responseBody, FastjsonUtils.SERIALIZER_FEATURES));
|
||||
return res;
|
||||
}
|
||||
|
||||
private List<FilterBean> parseBeans(Map<String, String> filterExtends) {
|
||||
String beans = filterExtends.get(FILTER_BEAN);
|
||||
if (Strings.isNullOrEmpty(beans)) {
|
||||
return ImmutableList.of();
|
||||
}
|
||||
return Splitter.on(BEAN_SEPARATOR)
|
||||
.omitEmptyStrings()
|
||||
.trimResults()
|
||||
.splitToList(beans).stream()
|
||||
.map(bean -> {
|
||||
String name = StringUtils.substringBefore(bean, CONFIG_DELIMITER);
|
||||
String configStr = StringUtils.substringAfter(bean, CONFIG_DELIMITER);
|
||||
JSONObject config = Optional.ofNullable(configStr)
|
||||
.filter(e -> !Strings.isNullOrEmpty(e))
|
||||
.map(e -> {
|
||||
try {
|
||||
return URLDecoder.decode(e, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
})
|
||||
.map(JSON::parseObject)
|
||||
.orElseGet(JSONObject::new);
|
||||
return FilterBean.builder().name(name).config(config).build();
|
||||
})
|
||||
.filter(bean -> !Strings.isNullOrEmpty(bean.getName()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public interface RequestFilter {
|
||||
default JSON filterIn(RequestContext reqContext, JSON params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
default JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
return response;
|
||||
}
|
||||
|
||||
default JSON filterIn(RequestContext reqContext, JSON params, JSONObject config) {
|
||||
return filterIn(reqContext, params);
|
||||
}
|
||||
|
||||
default JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
|
||||
return filterOut(reqContext, response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台页面中通过关键字段查询时, 需要移除其他字段的查询场景. 添加通用的请求过滤器来处理此类需求.
|
||||
* 使用时, 需要单独注入.
|
||||
*/
|
||||
public final static class KeyNoQueryFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject config) {
|
||||
if (params == null) {
|
||||
return params;
|
||||
}
|
||||
|
||||
Set<String> keyNoFields = Splitter.on(",").omitEmptyStrings().trimResults()
|
||||
.splitToStream(Strings.nullToEmpty(config.getString("keyNoFields")))
|
||||
.collect(Collectors.toSet());
|
||||
Set<String> removeFields = Splitter.on(",").omitEmptyStrings().trimResults()
|
||||
.splitToStream(Strings.nullToEmpty(config.getString("removeFields")))
|
||||
.collect(Collectors.toSet());
|
||||
if (keyNoFields.isEmpty() || removeFields.isEmpty()) {
|
||||
return params;
|
||||
}
|
||||
|
||||
JSONObject paramsJson = (JSONObject) params;
|
||||
if (paramsJson.keySet().stream().noneMatch(keyNoFields::contains)) {
|
||||
return paramsJson;
|
||||
}
|
||||
|
||||
removeFields.forEach(paramsJson::remove);
|
||||
return paramsJson;
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class ListOrPageRecordsFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
if (!(response instanceof JSONObject)) {
|
||||
return response;
|
||||
}
|
||||
JSONObject jsonResp = (JSONObject) response;
|
||||
Object content = jsonResp.get("content");
|
||||
if (!(content instanceof JSONObject) && !(content instanceof JSONArray)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (content instanceof JSONArray) {
|
||||
List<JSONObject> records = ((JSONArray) content).toJavaList(JSONObject.class);
|
||||
JSONArray filtered = new JSONArray().fluentAddAll(filterRecords(reqContext, records));
|
||||
return jsonResp.fluentPut("content", filtered);
|
||||
}
|
||||
|
||||
JSONObject page = (JSONObject) content;
|
||||
JSONArray records = page.getJSONArray("records");
|
||||
if (CollectionUtils.isEmpty(records)) {
|
||||
return response;
|
||||
}
|
||||
List<JSONObject> filtered = filterRecords(reqContext, records.toJavaList(JSONObject.class));
|
||||
page.put("records", new JSONArray().fluentAddAll(filtered));
|
||||
return response;
|
||||
}
|
||||
|
||||
public abstract List<JSONObject> filterRecords(RequestContext reqContext, List<JSONObject> records);
|
||||
}
|
||||
|
||||
public abstract static class ListOrPageRecordsFilter2 implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
return filterOut(reqContext, response, new JSONObject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
|
||||
if (!(response instanceof JSONObject)) {
|
||||
return response;
|
||||
}
|
||||
JSONObject jsonResp = (JSONObject) response;
|
||||
Object content = jsonResp.get("content");
|
||||
if (!(content instanceof JSONObject) && !(content instanceof JSONArray)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (content instanceof JSONArray) {
|
||||
List<JSONObject> records = ((JSONArray) content).toJavaList(JSONObject.class);
|
||||
if (CollectionUtils.isEmpty(records)) {
|
||||
return response;
|
||||
}
|
||||
JSONArray filtered = new JSONArray().fluentAddAll(filterRecords(reqContext, records, config));
|
||||
return jsonResp.fluentPut("content", filtered);
|
||||
}
|
||||
|
||||
JSONObject page = (JSONObject) content;
|
||||
JSONArray records = page.getJSONArray("records");
|
||||
if (CollectionUtils.isEmpty(records)) {
|
||||
return response;
|
||||
}
|
||||
List<JSONObject> filtered = filterRecords(reqContext, records.toJavaList(JSONObject.class), config);
|
||||
page.put("records", new JSONArray().fluentAddAll(filtered));
|
||||
return response;
|
||||
}
|
||||
|
||||
public abstract List<JSONObject> filterRecords(RequestContext reqContext, List<JSONObject> records, JSONObject config);
|
||||
}
|
||||
|
||||
public abstract static class ConvertContentFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
return filterOut(reqContext, response, new JSONObject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
|
||||
if (!(response instanceof JSONObject)) {
|
||||
return response;
|
||||
}
|
||||
JSONObject jsonResp = (JSONObject) response;
|
||||
Object content = jsonResp.get("content");
|
||||
if (content == null) {
|
||||
return response;
|
||||
}
|
||||
Preconditions.checkArgument(content instanceof JSON, "ConvertContentFilter不支持原始response.content非json的情况");
|
||||
return jsonResp.fluentPut("content", filterContent(reqContext, (JSON) content, config));
|
||||
}
|
||||
|
||||
public abstract JSON filterContent(RequestContext reqContext, JSON content, JSONObject config);
|
||||
}
|
||||
|
||||
public static class ConvertPageAsListFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
return filterOut(reqContext, response, new JSONObject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
|
||||
if (!(response instanceof JSONObject)) {
|
||||
return response;
|
||||
}
|
||||
JSONObject jsonResp = (JSONObject) response;
|
||||
Object content = jsonResp.get("content");
|
||||
if (!(content instanceof JSONObject)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
JSONArray records = ((JSONObject) content).getJSONArray("records");
|
||||
return jsonResp.fluentPut("content", Optional.ofNullable(records).orElseGet(JSONArray::new));
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConvertListAsObjectFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
return filterOut(reqContext, response, new JSONObject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
|
||||
if (!(response instanceof JSONObject)) {
|
||||
return response;
|
||||
}
|
||||
JSONObject jsonResp = (JSONObject) response;
|
||||
Object content = jsonResp.get("content");
|
||||
|
||||
JSONArray arrayContent = null;
|
||||
if (content instanceof JSONArray) {
|
||||
arrayContent = (JSONArray) content;
|
||||
} else if (content instanceof JSONObject) {
|
||||
JSONArray records = ((JSONObject) content).getJSONArray("records");
|
||||
arrayContent = records == null ? new JSONArray() : records;
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
JSONObject obj = IntStream.range(0, arrayContent.size())
|
||||
.mapToObj(arrayContent::getJSONObject)
|
||||
.findFirst()
|
||||
.orElseGet(JSONObject::new);
|
||||
return jsonResp.fluentPut("content", filterObject(reqContext, obj, config));
|
||||
}
|
||||
|
||||
public JSONObject filterObject(RequestContext reqContext, JSONObject obj) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
public JSONObject filterObject(RequestContext reqContext, JSONObject obj, JSONObject config) {
|
||||
return filterObject(reqContext, obj);
|
||||
}
|
||||
}
|
||||
|
||||
public static class ConvertListAsPageFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
return filterOut(reqContext, response, new JSONObject());
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response, JSONObject config) {
|
||||
if (!(response instanceof JSONObject)) {
|
||||
return response;
|
||||
}
|
||||
JSONObject jsonResp = (JSONObject) response;
|
||||
Object content = jsonResp.get("content");
|
||||
if (!(content instanceof JSONArray)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
List<JSONObject> records = ((JSONArray) content).toJavaList(JSONObject.class);
|
||||
PageResp<JSONObject> page = PageResp.<JSONObject>builder().total(records.size()).build();
|
||||
|
||||
IPageReq pageParam = JSONObject.parseObject(reqContext.getRequestBody(), IPageReq.class);
|
||||
Optional.ofNullable(pageParam).map(IPageReq::getPage).ifPresent(page::setCurrent);
|
||||
Optional.ofNullable(pageParam).map(IPageReq::getPageSize).ifPresent(page::setSize);
|
||||
|
||||
page.setData(records.stream()
|
||||
.skip((page.getCurrent() - 1) * page.getSize())
|
||||
.limit(page.getSize())
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
return jsonResp.fluentPut("content", page);
|
||||
}
|
||||
}
|
||||
|
||||
public static class RequestParamCheckFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject config) {
|
||||
boolean isConfigRulePresent = config != null && !config.isEmpty() && config.containsKey("rules");
|
||||
Preconditions.checkArgument(isConfigRulePresent, "RequestParamCheckFilter必须指定检查规则");
|
||||
Preconditions.checkArgument(JSONObject.class.isAssignableFrom(params.getClass()),
|
||||
"RequestParamCheckFilter只能检查JSONObject类型参数");
|
||||
//spring list默认读取为 map(key=0...N). 将values转换为Rule
|
||||
List<Rule> rules = config.getJSONObject("rules").entrySet().stream()
|
||||
.map(e -> ((JSONObject) e.getValue()).toJavaObject(Rule.class))
|
||||
.collect(Collectors.toList());
|
||||
rules.stream().forEach(p ->
|
||||
Preconditions.checkArgument(!StringUtils.isAllBlank(p.getField(), p.getJsonPath()),
|
||||
"RequestParamCheckFilter规则错误field, jsonPath不能都为空"));
|
||||
Operator operator = Optional.ofNullable(config.getString("operator"))
|
||||
.map(e -> Operator.valueOf(e)).orElse(Operator.AND);
|
||||
|
||||
List<Optional<String>> errors = rules.stream()
|
||||
.map(rule -> rule.check((JSONObject) params))
|
||||
.collect(Collectors.toList());
|
||||
//如果operator=AND. & 任意一个检查错误 则告警
|
||||
if (operator == Operator.AND && errors.stream().anyMatch(Optional::isPresent)) {
|
||||
throw ResultCode.INVALID_PARAMS.toException(errors.stream().filter(Optional::isPresent).findFirst().get().get());
|
||||
}
|
||||
//如果operator=OR. & 所有检查都是错误 则告警
|
||||
if (operator == Operator.OR && errors.stream().allMatch(Optional::isPresent)) {
|
||||
throw ResultCode.INVALID_PARAMS.toException(errors.stream().filter(Optional::isPresent).findFirst().get().get());
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class Rule {
|
||||
String jsonPath;
|
||||
String field;
|
||||
boolean required;
|
||||
String regex;
|
||||
private transient Pattern pattern;
|
||||
|
||||
/** 检查值并返回错误信息, 正确则返回empty */
|
||||
protected Optional<String> check(JSONObject param) {
|
||||
Object value = getValue(param);
|
||||
if (required && value == null) {
|
||||
return Optional.of(field + "不能为空");
|
||||
}
|
||||
Optional<Pattern> pattern = tryGetPattern();
|
||||
if (pattern.isPresent() && value != null && !pattern.get().matcher(value.toString()).find()) {
|
||||
return Optional.of(field + "参数校验失败:" + regex);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/** 优先根据field获取value. 否则根据jsonPath */
|
||||
private Object getValue(JSONObject param) {
|
||||
if (!Strings.isNullOrEmpty(field)) {
|
||||
return param.get(field);
|
||||
}
|
||||
return JSONPath.eval(param, jsonPath);
|
||||
}
|
||||
|
||||
private Optional<Pattern> tryGetPattern() {
|
||||
if (Strings.isNullOrEmpty(regex)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
if (pattern == null) {
|
||||
pattern = Pattern.compile(regex);
|
||||
}
|
||||
return Optional.of(pattern);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Operator {
|
||||
AND,
|
||||
OR;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪返回的content内容
|
||||
* 仅支持:
|
||||
* 1.content是JSONObject
|
||||
* 2.content是JSONArray,且其中是JSONObject
|
||||
* 3.content是分页返回对象,即{"records":[{}]}
|
||||
*/
|
||||
public static class CropContentFilter extends ConvertContentFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterOut(RequestContext reqContext, JSON response) {
|
||||
throw new UnsupportedOperationException("使用CropContentFilter必须指定裁剪配置");
|
||||
}
|
||||
|
||||
@Override
|
||||
public JSON filterContent(RequestContext reqContext, JSON content, JSONObject config) {
|
||||
// see class: CropConfig
|
||||
if (CollectionUtils.isEmpty(config)) {
|
||||
throw new UnsupportedOperationException("使用CropContentFilter必须指定裁剪配置");
|
||||
}
|
||||
return cropContent(content, config);
|
||||
}
|
||||
|
||||
private JSON cropContent(JSON content, JSONObject config) {
|
||||
// content是json数组
|
||||
if (content instanceof JSONArray) {
|
||||
return cropJSONArrayContent((JSONArray) content, config);
|
||||
}
|
||||
// content是json对象
|
||||
if (content instanceof JSONObject) {
|
||||
JSONObject contentJSONObject = (JSONObject) content;
|
||||
// 可能是分页content,此时支持裁剪分页列表中的json对象
|
||||
if (contentJSONObject.containsKey("records")) {
|
||||
contentJSONObject.put("records",
|
||||
cropJSONArrayContent(contentJSONObject.getJSONArray("records"), config));
|
||||
}
|
||||
return doCrop(contentJSONObject, config);
|
||||
}
|
||||
return content;
|
||||
}
|
||||
|
||||
private JSONArray cropJSONArrayContent(JSONArray content, JSONObject config) {
|
||||
// 只考虑JSONArray中是JSONObject对象的情况
|
||||
List<JSONObject> contentList = content.stream()
|
||||
.filter(obj -> obj instanceof JSONObject)
|
||||
.map(obj -> JSONObject.parseObject(JSON.toJSONString(obj)))
|
||||
.collect(Collectors.toList());
|
||||
if (contentList.isEmpty()) {
|
||||
return content;
|
||||
}
|
||||
return JSON.parseArray(JSON.toJSONString(contentList.stream()
|
||||
.map(c -> doCrop(c, config))
|
||||
.collect(Collectors.toList())));
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪data中的字段
|
||||
*
|
||||
* @param data
|
||||
* @param config {@link CropConfig}
|
||||
* @return
|
||||
*/
|
||||
private static JSONObject doCrop(JSONObject data, JSONObject config) {
|
||||
if (CollectionUtils.isEmpty(config) || CollectionUtils.isEmpty(data)) {
|
||||
return data;
|
||||
}
|
||||
CropConfig cropConfig = config.toJavaObject(CropConfig.class);
|
||||
// 优先用includeKeys
|
||||
if (!CollectionUtils.isEmpty(cropConfig.getIncludeKeys())) {
|
||||
return DataAssembleHelper.filterBean(data, cropConfig.getIncludeKeys(), true);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(cropConfig.getExcludeKeys())) {
|
||||
return DataAssembleHelper.filterBean(data, cropConfig.getExcludeKeys(), false);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 裁剪content时的配置
|
||||
*
|
||||
* @see CropContentFilter
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class CropConfig {
|
||||
/**
|
||||
* 希望只包含这些key,只支持第一层key
|
||||
* 如果有值,将忽略excludeKeys
|
||||
*/
|
||||
private String includeKeys;
|
||||
/**
|
||||
* 希望排除的key,只支持第一层key
|
||||
*/
|
||||
private String excludeKeys;
|
||||
|
||||
public Set<String> getIncludeKeys() {
|
||||
if (Strings.isNullOrEmpty(includeKeys)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return ImmutableSet.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().splitToList(includeKeys));
|
||||
}
|
||||
|
||||
public Set<String> getExcludeKeys() {
|
||||
if (Strings.isNullOrEmpty(excludeKeys)) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return ImmutableSet.copyOf(Splitter.on(",").trimResults().omitEmptyStrings().splitToList(excludeKeys));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动请求输入参数中字段到指定字段下.
|
||||
*/
|
||||
public final static class MoveInputFieldFilter implements RequestFilter {
|
||||
@Override
|
||||
public JSON filterIn(RequestContext reqContext, JSON params, JSONObject configJSON) {
|
||||
if (params == null) {
|
||||
return params;
|
||||
}
|
||||
|
||||
Config config = configJSON.toJavaObject(Config.class);
|
||||
if (!config.isValid()) {
|
||||
throw new IllegalArgumentException("不正确的配置参数");
|
||||
}
|
||||
|
||||
Set<String> sourceFields = config.getSourceFields();
|
||||
JSONObject sourceValue = DataAssembleHelper.filterBean(params, sourceFields);
|
||||
JSONObject res = DataAssembleHelper.filterBean(params, sourceFields, false);
|
||||
Preconditions.checkArgument(!res.containsKey(config.getTargetField()), "目标字段已经存在, 不能被覆盖");
|
||||
|
||||
res.put(config.getTargetField(), config.getTargetFieldType() == Config.FieldType.PROPERTY
|
||||
// 如果是把字段move到listField作为它的第一个元素
|
||||
? sourceValue : Lists.newArrayList(sourceValue));
|
||||
return res;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
static class Config {
|
||||
private String targetField;
|
||||
private String sourceFields;
|
||||
private FieldType targetFieldType;
|
||||
|
||||
public Set<String> getSourceFields() {
|
||||
return Splitter.on(",").omitEmptyStrings().trimResults()
|
||||
.splitToStream(Strings.nullToEmpty(sourceFields))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
boolean isValid() {
|
||||
return !getSourceFields().isEmpty() && !StringUtils.isBlank(targetField);
|
||||
}
|
||||
|
||||
public FieldType getTargetFieldType() {
|
||||
if (targetFieldType == null) {
|
||||
return FieldType.PROPERTY;
|
||||
}
|
||||
return targetFieldType;
|
||||
}
|
||||
|
||||
enum FieldType {
|
||||
PROPERTY,
|
||||
/**
|
||||
* 将Field看做一个列表, 将 moveields 放在toField下作为第一个元素.
|
||||
*/
|
||||
LIST;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class FilterBean {
|
||||
private String name;
|
||||
private JSONObject config;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,22 @@
|
||||
package cn.axzo.foundation.gateway.support.plugin.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ServiceCodeHook implements ProxyHook {
|
||||
|
||||
/**
|
||||
* @param requestContext
|
||||
* @param proxyContext
|
||||
* @param serviceCode
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public String preResolveService(RequestContext requestContext, ProxyContext proxyContext, String serviceCode) {
|
||||
return Strings.isNullOrEmpty(serviceCode) ? requestContext.getServiceCode() : serviceCode;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
package cn.axzo.foundation.gateway.support.plugin.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.StopWatch;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public class StopWatchHook implements ProxyHook {
|
||||
|
||||
// 容忍请求处理的时间,超过该时间将发送告警日志,单位: 毫秒. 默认15秒
|
||||
private static final long TOLERANT_PROCESS_MILLIS = 15000;
|
||||
private static final ThreadLocal<StopWatch> STOP_WATCH_LOCAL = new ThreadLocal<>();
|
||||
|
||||
public void initialize() {
|
||||
STOP_WATCH_LOCAL.set(new StopWatch());
|
||||
start("FindProxy");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void findProxy(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
stop();
|
||||
start("WaitForRequest");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void preFilterIn(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
stop();
|
||||
start("FilterIn");
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext,
|
||||
RpcClient rpcClient, String postURL, RequestParams postParams) {
|
||||
stop();
|
||||
start("Request");
|
||||
return postParams;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse postResponse(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
stop();
|
||||
start("FilterOut");
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postFilterOut(RequestContext reqContext, ProxyContext proxyContext, GateResponse response) {
|
||||
stopAndCheck(reqContext, proxyContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(RequestContext reqContext, ProxyContext proxyContext, Throwable throwable) {
|
||||
stopAndCheck(reqContext, proxyContext);
|
||||
}
|
||||
|
||||
private void start(String taskName) {
|
||||
try {
|
||||
Optional<StopWatch> stopWatch = Optional.ofNullable(STOP_WATCH_LOCAL.get());
|
||||
stopWatch.ifPresent(w -> w.start(taskName));
|
||||
} catch (Exception e) {
|
||||
log.error("StopWatchHook start exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void stop() {
|
||||
try {
|
||||
Optional<StopWatch> stopWatch = Optional.ofNullable(STOP_WATCH_LOCAL.get());
|
||||
stopWatch.ifPresent(StopWatch::stop);
|
||||
} catch (Exception e) {
|
||||
log.error("StopWatchHook stop exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAndCheck(RequestContext reqContext, ProxyContext proxyContext) {
|
||||
try {
|
||||
Optional<StopWatch> stopWatch = Optional.ofNullable(STOP_WATCH_LOCAL.get());
|
||||
stopWatch.ifPresent(StopWatch::stop);
|
||||
|
||||
long elapsedTime = stopWatch.map(StopWatch::getTotalTimeMillis).orElse(0L);
|
||||
long tolerantTime = Optional.ofNullable(proxyContext.getProxyParam())
|
||||
.map(p -> Optional.ofNullable(p.getLong("readTimeoutMillis"))
|
||||
.orElseGet(() -> p.getLong("TolerantProcessTime")))
|
||||
.orElse(TOLERANT_PROCESS_MILLIS);
|
||||
if (elapsedTime > tolerantTime) {
|
||||
proxyContext.getGlobalContext().postAlert(
|
||||
"处理请求超时, host: {}, requestURI: {}, proxyContext: {}, details: {}",
|
||||
reqContext.getHost(), reqContext.getRequestURI(), proxyContext, stopWatch.get().prettyPrint());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("StopWatchHook stopAndCheck exception", e);
|
||||
}
|
||||
|
||||
STOP_WATCH_LOCAL.remove();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface Proxy {
|
||||
|
||||
/**
|
||||
* 获取Proxy的上下文信息.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
ProxyContext getContext();
|
||||
|
||||
/**
|
||||
* 获取注入到Proxy的Hooks.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
List<ProxyHook> getProxyHooks();
|
||||
|
||||
/**
|
||||
* Proxy执行请求并返回结果.
|
||||
*
|
||||
* @param context 当前请求的上下文信息
|
||||
* @return
|
||||
*/
|
||||
GateResponse request(RequestContext context);
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy;
|
||||
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public enum ProxyFeature {
|
||||
|
||||
PRINT_LOG(1L << 0, "打印日志"),
|
||||
COLLECT_LOG(1L << 1, "收集日志"),
|
||||
GRAY_STAGE(1L << 2, "灰度阶段");
|
||||
|
||||
private long code;
|
||||
private String description;
|
||||
|
||||
ProxyFeature(long code, String description) {
|
||||
this.code = code;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public static boolean needPrintLog(long feature) {
|
||||
return (feature & PRINT_LOG.code) > 0;
|
||||
}
|
||||
|
||||
public static boolean needCollectLog(long feature) {
|
||||
return (feature & COLLECT_LOG.code) > 0;
|
||||
}
|
||||
|
||||
public static boolean inGrayStage(long feature) {
|
||||
return (feature & GRAY_STAGE.code) > 0;
|
||||
}
|
||||
|
||||
public static Set<ProxyFeature> convertFeaturesToSet(Long features) {
|
||||
if (Optional.ofNullable(features).orElse(0L) <= 0) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
return Arrays.stream(values()).filter(f -> (features & f.code) > 0)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将ProxyFeature逗号分隔的字符串转为数值
|
||||
*
|
||||
* @param featureString 逗号分隔的字符串,如'PRINT_LOG, COLLECT_LOG'
|
||||
* @return
|
||||
*/
|
||||
public static Long convertStringToFeatures(String featureString) {
|
||||
if (Strings.isNullOrEmpty(featureString)) {
|
||||
return 0L;
|
||||
}
|
||||
|
||||
Long features = 0L;
|
||||
List<String> list = Splitter.on(",")
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(featureString);
|
||||
for (ProxyFeature feature : values()) {
|
||||
if (list.contains(feature.name())) {
|
||||
features |= feature.code;
|
||||
}
|
||||
}
|
||||
return features;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.proxy.Proxy;
|
||||
import cn.axzo.foundation.gateway.support.utils.ServiceResolver;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
public abstract class AbstractProxy implements Proxy {
|
||||
|
||||
@Getter
|
||||
private ProxyContext context;
|
||||
|
||||
@Getter(AccessLevel.PROTECTED)
|
||||
private ProxyHookChain hookChain;
|
||||
|
||||
public static AbstractProxy newProxy(ProxyContext context) {
|
||||
return newProxy(context, ProxyHookChain.EMPTY);
|
||||
}
|
||||
|
||||
public static AbstractProxy newProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
try {
|
||||
Class<?> proxyClz = Class.forName("cn.axzo.foundation.gateway.support.proxy.impl." + context.getProxyName());
|
||||
Constructor<?> constructor = proxyClz.getConstructor(ProxyContext.class, ProxyHookChain.class);
|
||||
return (AbstractProxy) constructor.newInstance(context, hookChain);
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected AbstractProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
Preconditions.checkArgument(Objects.nonNull(context)
|
||||
&& Objects.nonNull(context.getId())
|
||||
&& !Strings.isNullOrEmpty(context.getProxyName())
|
||||
&& Objects.nonNull(context.getProxyParam())
|
||||
&& Objects.nonNull(context.getVersion())
|
||||
&& Objects.nonNull(context.getFeature())
|
||||
&& Objects.nonNull(context.getParameterFilter())
|
||||
&& Objects.nonNull(context.getServiceResolver()));
|
||||
this.context = context;
|
||||
this.hookChain = Objects.nonNull(hookChain) ? hookChain : ProxyHookChain.EMPTY;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对BizGate的配置参数进行解析.
|
||||
*
|
||||
* @param params Routing.parameter
|
||||
* @return 解析成功返回true, 失败返回false
|
||||
*/
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
return Objects.nonNull(params);
|
||||
}
|
||||
|
||||
public ServiceResolver getServiceResolver() {
|
||||
return context.getServiceResolver();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProxyHook> getProxyHooks() {
|
||||
return ImmutableList.copyOf(hookChain.getHooks());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,30 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.exception.ApiUnauthorizedException;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
|
||||
/**
|
||||
* 黑名单Proxy, 请求的URI被禁止访问, 针对此类请求一律抛出{@link ApiUnauthorizedException}.
|
||||
*/
|
||||
public class BlacklistProxy extends AbstractProxy {
|
||||
|
||||
public BlacklistProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
// 该Proxy理应不需要任何参数
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse request(RequestContext context) {
|
||||
// 该Proxy直接抛出异常且不调用Hooks.onError
|
||||
throw new ApiUnauthorizedException(String.format("host: %s, requestURI: %s", context.getHost(), context.getRequestURI()));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,92 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.hash.Hashing;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 对 transform 返回的数据进行缓存5 分钟。
|
||||
* 不支持指定时间的缓存。
|
||||
*/
|
||||
@Slf4j
|
||||
public class CachableTransformProxy extends TransformProxy {
|
||||
|
||||
private final static Cache<Object, GateResponse> cache5 = CacheBuilder.newBuilder().maximumSize(5000)
|
||||
.expireAfterWrite(5, TimeUnit.MINUTES).build();
|
||||
private final static Cache<Object, GateResponse> cache30 = CacheBuilder.newBuilder().maximumSize(5000)
|
||||
.expireAfterWrite(30, TimeUnit.MINUTES).build();
|
||||
private final static Cache<Object, GateResponse> cache60 = CacheBuilder.newBuilder().maximumSize(5000)
|
||||
.expireAfterWrite(60, TimeUnit.MINUTES).build();
|
||||
private final static Map<String, Cache<Object, GateResponse>> CACHE_BUCKETS =
|
||||
ImmutableMap.of("5", cache5, "30", cache30, "60", cache60);
|
||||
|
||||
public CachableTransformProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
/**
|
||||
* 参考 DebugProxyHook.PRINT_VERBOSE_PARAM_NAME
|
||||
* DebugProxyHook是内部类不能访问。不想破坏类的封装,这里做了简单复制。
|
||||
*/
|
||||
private static final String PRINT_VERBOSE_PARAM_NAME = "__print_verbose";
|
||||
private String cacheMinutes;
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
if (!super.parseParameter(params)) {
|
||||
return false;
|
||||
}
|
||||
cacheMinutes = params.getString("cacheMinutes");
|
||||
if (Strings.isNullOrEmpty(cacheMinutes)) {
|
||||
cacheMinutes = "5";
|
||||
}
|
||||
Preconditions.checkState(CACHE_BUCKETS.containsKey(cacheMinutes), "不支持的缓存分钟 " + cacheMinutes);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected GateResponse doRequest(RequestContext reqContext, String resolvedUrl, RequestParams requestParams, String hookedCode) {
|
||||
boolean needPrint = false;
|
||||
// 支持通过url parameter打开debug信息
|
||||
if (!CollectionUtils.isEmpty(reqContext.getRequestParams())
|
||||
&& reqContext.getRequestParams().containsKey(PRINT_VERBOSE_PARAM_NAME)) {
|
||||
needPrint = true;
|
||||
}
|
||||
|
||||
String reqHash = Hashing.murmur3_128().newHasher()
|
||||
.putString(JSONObject.toJSONString(requestParams.getData()), Charset.defaultCharset())
|
||||
.putString(resolvedUrl, Charset.defaultCharset())
|
||||
.hash().toString();
|
||||
Cache<Object, GateResponse> cache = CACHE_BUCKETS.get(cacheMinutes);
|
||||
GateResponse resp = cache.getIfPresent(reqHash);
|
||||
if (resp == null) {
|
||||
resp = super.doRequest(reqContext, resolvedUrl, requestParams, hookedCode);
|
||||
if (needPrint) {
|
||||
log.info("[CachableTransformProxy] PUT response to cache. host: {}, resolvedUrl: {}, requestParams: {}",
|
||||
reqContext.getHost(), resolvedUrl, JSONObject.toJSONString(requestParams.getData()));
|
||||
}
|
||||
cache.put(reqHash, resp);
|
||||
} else {
|
||||
if (needPrint) {
|
||||
log.info("[CachableTransformProxy] LOAD response from cache. host: {}, resolvedUrl: {}, requestParams: {}",
|
||||
reqContext.getHost(), resolvedUrl, JSONObject.toJSONString(requestParams.getData()));
|
||||
}
|
||||
}
|
||||
return resp;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.GlobalContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.proxy.Proxy;
|
||||
import cn.axzo.foundation.result.ResultCode;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Strings;
|
||||
import com.googlecode.aviator.AviatorEvaluator;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 条件Proxy, 根据条件使用不同的proxy对请求进行路由
|
||||
*/
|
||||
public class ConditionProxy extends AbstractProxy {
|
||||
|
||||
private List<Condition> conditions;
|
||||
|
||||
public ConditionProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
if (!super.parseParameter(params)) {
|
||||
return false;
|
||||
}
|
||||
conditions = Optional.ofNullable(params.getJSONArray("conditions"))
|
||||
.map(e -> e.toJavaList(Condition.class))
|
||||
.orElse(null);
|
||||
|
||||
return !CollectionUtils.isEmpty(conditions) && conditions.stream()
|
||||
.allMatch(c -> c.check(getContext().getGlobalContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse request(RequestContext context) {
|
||||
JSONObject env = new JSONObject();
|
||||
Condition condition = conditions.stream()
|
||||
.filter(c -> (Boolean) AviatorEvaluator.execute(c.getCondition(), env, true))
|
||||
.findFirst()
|
||||
.orElseThrow(() -> ResultCode.INVALID_PARAMS.toException("找不到对应路径"));
|
||||
Proxy targetProxy = getContext().getProxyResolver().apply(condition.getProxyPath());
|
||||
if (targetProxy == null) {
|
||||
throw ResultCode.INVALID_PARAMS.toException("找不到对应路径");
|
||||
}
|
||||
return targetProxy.request(context);
|
||||
}
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
private static class Condition {
|
||||
private String condition;
|
||||
private String proxyPath;
|
||||
|
||||
public boolean check(GlobalContext globalContext) {
|
||||
return !Strings.isNullOrEmpty(condition) && globalContext.getLocalProxies().containsKey(proxyPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.utils.UrlPathHelper;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 默认的Proxy, 根据GateServer的host(product/dev/qa)推导出业务方的域名, 并拼接业务方接口的URI后进行调用.
|
||||
*
|
||||
* <p>假设业务方的域名为: foo.xxx.com, 映射到Gate的某个Server接口的URI为: /foo/resource</p>
|
||||
* <p>当GateServer运行在dev环境下时, 该Server接口经推导后的URL为: https://foo.dev.xxx.com/resource</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class DefaultProxy extends SimpleProxy {
|
||||
|
||||
public DefaultProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
// 该Proxy理应不需要任何参数
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveServiceCode(RequestContext context) {
|
||||
return context.getServiceCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveRequestUrl(RequestContext context, String serviceCode) {
|
||||
String host = getServiceResolver().getHost(context, serviceCode);
|
||||
String servicePath = context.getServicePath();
|
||||
return !Strings.isNullOrEmpty(servicePath) ? UrlPathHelper.join(host, servicePath) : null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.utils.UrlPathHelper;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
/**
|
||||
* 请求转发的proxy, 根据配置中的host, 并拼接业务方接口的URI后进行调用.
|
||||
*
|
||||
* <p>假设调用Gate的某个Server接口的URI为: /search_news</p>
|
||||
* <p>当该URL配置为ForwardProxy且配置中服务编码对应为: forward://https%3A%2F%2Fwww.baidu.com/search_news?targetPath=search</p>
|
||||
* <p>则经过Proxy处理后实际请求为: https://www.baidu.com/search</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class ForwardProxy extends SimpleProxy {
|
||||
@Getter
|
||||
private String authority;
|
||||
@Getter
|
||||
private String targetPath;
|
||||
|
||||
public ForwardProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
if (!super.parseParameter(params)) {
|
||||
return false;
|
||||
}
|
||||
authority = params.getString("authority");
|
||||
targetPath = params.getString("targetPath");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveServiceCode(RequestContext context) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveRequestUrl(RequestContext context, String serviceCode) {
|
||||
String servicePath = !Strings.isNullOrEmpty(targetPath) ? targetPath : context.getServicePath();
|
||||
return !Strings.isNullOrEmpty(servicePath) ? UrlPathHelper.join(authority, servicePath) : null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.GlobalContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.utils.ParameterFilter;
|
||||
import cn.axzo.foundation.gateway.support.utils.ServiceResolver;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* 针对特殊的请求. 如根路径, /favicon.ico等返回NoContent的httpCode
|
||||
*/
|
||||
public class NoContentProxy extends AbstractProxy {
|
||||
|
||||
private static final Long DEFAULT_PROXY_ID = Long.valueOf(999999L + Math.abs(NoContentProxy.class.hashCode()));
|
||||
|
||||
public NoContentProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
public NoContentProxy(GlobalContext globalContext, ServiceResolver serviceResolver) {
|
||||
super(ProxyContext.builder()
|
||||
.id(DEFAULT_PROXY_ID)
|
||||
.proxyName("NoContentProxy")
|
||||
.proxyParam(new JSONObject())
|
||||
.version(ProxyContext.DEFAULT_VERSION)
|
||||
.feature(ProxyContext.DEFAULT_FEATURE)
|
||||
.parameterFilter(ParameterFilter.buildFilter())
|
||||
.globalContext(globalContext)
|
||||
.serviceResolver(serviceResolver)
|
||||
.build(),
|
||||
globalContext.getProxyHookChain());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
// 该Proxy理应不需要任何参数
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GateResponse request(RequestContext context) {
|
||||
GateResponse response = GateResponse.builder()
|
||||
.status(HttpStatus.NO_CONTENT)
|
||||
.build();
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.utils.UrlPathHelper;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
/**
|
||||
* 正则表达式域名地址转换Proxy, 根据配置中的正则表达式将域名, 及请求URI转换后进行调用.
|
||||
*
|
||||
* <p>假设调用Gate的某个Server接口的URI为: /groupA/resource</p>
|
||||
* <p>当该URL配置为RegexProxy且配置中服务编码对应的域名为: https://foo.xxx.com</p>
|
||||
* <p>且配置的sourcePattern为"^/groupA/(?<tail>.+$)", replacePattern为"/groupB/${tail}"</p>
|
||||
* <p>则经过Proxy处理后实际请求为: https://foo.xxx.com/groupB/resource</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class RegexProxy extends SimpleProxy {
|
||||
|
||||
private Pattern sourcePattern;
|
||||
|
||||
private String replacePattern;
|
||||
|
||||
@Getter
|
||||
private String serviceCode;
|
||||
|
||||
public RegexProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
if (!super.parseParameter(params)) {
|
||||
return false;
|
||||
}
|
||||
// 读取routing中配置的serviceCode, 但可以为空
|
||||
serviceCode = params.getString("serviceCode");
|
||||
|
||||
String sourcePatternStr = params.getString("sourcePattern");
|
||||
if (Strings.isNullOrEmpty(sourcePatternStr)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
sourcePattern = Pattern.compile(sourcePatternStr);
|
||||
} catch (PatternSyntaxException e) {
|
||||
return false;
|
||||
}
|
||||
replacePattern = params.getString("replacePattern");
|
||||
return !Strings.isNullOrEmpty(replacePattern);
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveServiceCode(RequestContext context) {
|
||||
return serviceCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveRequestUrl(RequestContext context, String serviceCode) {
|
||||
String host = getServiceResolver().getHost(context, serviceCode);
|
||||
Matcher matcher = sourcePattern.matcher(context.getRequestURI());
|
||||
String servicePath = matcher.replaceFirst(replacePattern);
|
||||
return !Strings.isNullOrEmpty(servicePath) ? UrlPathHelper.join(host, servicePath) : null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.GateResponse;
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONException;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.net.UrlEscapers;
|
||||
import okhttp3.MediaType;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.exception.ApiNotFoundException;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
|
||||
/**
|
||||
* 简单请求Proxy, 根据配置中的服务编码对应的域名, 在具体实现代理中进行URL及参数转换.
|
||||
*/
|
||||
public abstract class SimpleProxy extends AbstractProxy {
|
||||
|
||||
abstract String resolveServiceCode(RequestContext context);
|
||||
|
||||
abstract String resolveRequestUrl(RequestContext context, String serviceCode);
|
||||
|
||||
protected Long resolveReadTimeoutMillis() {
|
||||
return getContext().getProxyParam().getLong("readTimeoutMillis");
|
||||
}
|
||||
|
||||
SimpleProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
final static Set<String> JSON_YML_SUBTYPES = ImmutableSet.of("yml", "yaml", "json");
|
||||
|
||||
@Override
|
||||
public GateResponse request(RequestContext requestContext) {
|
||||
try {
|
||||
String serviceCode = resolveServiceCode(requestContext);
|
||||
String hookedCode = getHookChain().preResolveService(requestContext, getContext(), serviceCode);
|
||||
|
||||
String resolvedUrl = resolveRequestUrl(requestContext, hookedCode);
|
||||
if (Strings.isNullOrEmpty(resolvedUrl)) {
|
||||
throw new ApiNotFoundException(String.format("host: %s, requestURI: %s",
|
||||
requestContext.getHost(), requestContext.getRequestURI()));
|
||||
}
|
||||
|
||||
getHookChain().preFilterIn(requestContext, getContext());
|
||||
RequestParams resolvedParams = resolveRequestParams(requestContext);
|
||||
if (resolvedParams instanceof RequestParams.BodyParams) {
|
||||
// json body方式的请求,将request中的query parameter添加在url中
|
||||
resolvedUrl = addQueryParameters(requestContext, resolvedUrl);
|
||||
}
|
||||
return doRequest(requestContext, resolvedUrl, resolvedParams, hookedCode);
|
||||
} catch (Throwable throwable) {
|
||||
getHookChain().onError(requestContext, getContext(), throwable);
|
||||
throw throwable;
|
||||
}
|
||||
}
|
||||
|
||||
protected GateResponse doRequest(RequestContext requestContext, String resolvedUrl, RequestParams requestParams, String hookedCode) {
|
||||
RpcClient rpcClient = getServiceResolver().getRpcClient(requestContext, hookedCode);
|
||||
RequestParams resolvedParams = getHookChain().preRequest(requestContext, getContext(), rpcClient, resolvedUrl, requestParams);
|
||||
|
||||
GateResponse response = requestContext.getRequestMethod().request(
|
||||
rpcClient, resolvedUrl, resolvedParams);
|
||||
GateResponse res = getHookChain().postResponse(requestContext, getContext(), response);
|
||||
|
||||
filterResponse(requestContext, res);
|
||||
getHookChain().postFilterOut(requestContext, getContext(), res);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
private String addQueryParameters(RequestContext requestContext, String url) {
|
||||
Map<String, String> params = requestContext.getRequestParams();
|
||||
if (CollectionUtils.isEmpty(params)) {
|
||||
return url;
|
||||
}
|
||||
Function<String, String> escape = UrlEscapers.urlPathSegmentEscaper().asFunction();
|
||||
String queryString = params.entrySet().stream()
|
||||
.map(p -> escape.apply(p.getKey() + "=" + p.getValue()))
|
||||
.collect(Collectors.joining("&"));
|
||||
return url.contains("?")
|
||||
? url + "&" + queryString
|
||||
: url + "?" + queryString;
|
||||
}
|
||||
|
||||
RequestParams resolveRequestParams(RequestContext context) {
|
||||
Optional<String> subtypeOptional = Optional.ofNullable(context.getOriginalRequest())
|
||||
.map(HttpServletRequest::getContentType)
|
||||
.map(MediaType::parse)
|
||||
.map(e->e.subtype().toLowerCase());
|
||||
if (subtypeOptional.isPresent() && JSON_YML_SUBTYPES.contains(subtypeOptional.get())) {
|
||||
String requestBody = StringUtils.firstNonBlank(context.getRequestBody(), "{}");
|
||||
Object body;
|
||||
String subtype = subtypeOptional.get();
|
||||
if (subtype.equals("yml") || subtype.equals("yaml")) {
|
||||
body = readYmlAsJson(requestBody);
|
||||
} else {
|
||||
body = JSON.parse(requestBody);
|
||||
}
|
||||
if (getContext().getParameterFilter().hasInputFilter() && body instanceof JSONObject) {
|
||||
Map filtered = getContext().getParameterFilter().filterInputParams((JSONObject) body, context.getEnv());
|
||||
return RequestParams.BodyParams.builder().headers(context.getHeaders()).content(filtered)
|
||||
.readTimeoutMillis(resolveReadTimeoutMillis())
|
||||
.logEnabled(false).build();
|
||||
} else {
|
||||
return RequestParams.BodyParams.builder().headers(context.getHeaders()).content(body)
|
||||
.readTimeoutMillis(resolveReadTimeoutMillis())
|
||||
.logEnabled(false).build();
|
||||
}
|
||||
}
|
||||
|
||||
// form params
|
||||
Map<String, Object> params = Maps.newHashMap();
|
||||
// 如果请求是上传文件, 在调用RPC之前将parts填充到params
|
||||
if (!CollectionUtils.isEmpty(context.getMultiParts())) {
|
||||
params.putAll(context.getMultiParts());
|
||||
}
|
||||
// XXX 需要放在putAll(context.getMultiParts())之后,用以覆盖非byte[]类型的参数
|
||||
params.putAll(context.getRequestParams());
|
||||
Map filtered = getContext().getParameterFilter().filterInputParams(params, context.getEnv());
|
||||
return RequestParams.FormParams.builder().headers(context.getHeaders()).params(filtered)
|
||||
.logEnable(false).build();
|
||||
}
|
||||
|
||||
void filterResponse(RequestContext requestContext, GateResponse response) {
|
||||
if (getContext().getParameterFilter().hasOutputFilter() && Objects.nonNull(response) && response.hasTextContent()) {
|
||||
try {
|
||||
JSONObject jsonObject = JSON.parseObject(response.getStringContent());
|
||||
Map filtered = getContext().getParameterFilter().filterOutputObject(
|
||||
jsonObject.getInnerMap(), requestContext.getEnv());
|
||||
response.setStringContent(JSON.toJSONString(filtered));
|
||||
} catch (JSONException e) {
|
||||
// 不是json对象,不需要过滤
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected JSON readYmlAsJson(String body) {
|
||||
Yaml yaml = new Yaml();
|
||||
Iterable<Object> itr = yaml.loadAll(body);
|
||||
|
||||
Object itrNext = itr.iterator().next();
|
||||
if (itrNext instanceof Collection) {
|
||||
return new JSONArray().fluentAddAll((Collection) itrNext);
|
||||
}
|
||||
|
||||
// find the root which is a map
|
||||
return new JSONObject((Map) itrNext);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.gateway.support.utils.UrlPathHelper;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Strings;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 服务域名转换Proxy, 根据配置中的服务编码对应的域名, 并拼接业务方接口的URI后进行调用.
|
||||
*
|
||||
* <p>假设调用Gate的某个Server接口的URI为: /foo/resource</p>
|
||||
* <p>当该URL配置为TransformProxy且配置中服务编码对应的域名为: https://bar.xxx.com</p>
|
||||
* <p>则经过Proxy处理后实际请求为: https://bar.xxx.com/resource</p>
|
||||
*/
|
||||
@Slf4j
|
||||
public class TransformProxy extends SimpleProxy {
|
||||
|
||||
@Getter
|
||||
private String serviceCode;
|
||||
|
||||
@Getter
|
||||
private String targetPath;
|
||||
|
||||
public TransformProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
if (!super.parseParameter(params)) {
|
||||
return false;
|
||||
}
|
||||
// 读取routing中配置的serviceCode, 但可以为空
|
||||
serviceCode = params.getString("serviceCode");
|
||||
targetPath = params.getString("targetPath");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveServiceCode(RequestContext context) {
|
||||
return serviceCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
String resolveRequestUrl(RequestContext context, String serviceCode) {
|
||||
String host = getServiceResolver().getHost(context, serviceCode);
|
||||
String servicePath = !Strings.isNullOrEmpty(targetPath) ? targetPath : context.getServicePath();
|
||||
return !Strings.isNullOrEmpty(servicePath) ? UrlPathHelper.join(host, servicePath) : null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package cn.axzo.foundation.gateway.support.proxy.impl;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.entity.ProxyContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHook;
|
||||
import cn.axzo.foundation.gateway.support.plugin.ProxyHookChain;
|
||||
import cn.axzo.foundation.web.support.rpc.RequestParams;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 对于前端或经过filter处理后的JSONObject格式的参数,将指定key对应的value作为请求body,目的是兼容某些服务特殊的请求body格式
|
||||
* 比如: 前端请求: {"accountId": 123123}, bodyKey: accountId, 则请求至后端的body内容为: 123123
|
||||
*/
|
||||
@Slf4j
|
||||
public class ValueAsBodyProxy extends TransformProxy {
|
||||
|
||||
private String bodyKey;
|
||||
private ProxyHookChain withValueAsBodyHookChain;
|
||||
|
||||
public ValueAsBodyProxy(ProxyContext context, ProxyHookChain hookChain) {
|
||||
super(context, hookChain);
|
||||
|
||||
// 将ValueAsBodyHook插入在chain末尾,确保在请求后端服务之前进行hook
|
||||
List<ProxyHook> withValueAsBodyHooks = Optional.ofNullable(hookChain)
|
||||
.map(ProxyHookChain::getHooks)
|
||||
.map(Lists::newArrayList)
|
||||
.orElseGet(Lists::newArrayList);
|
||||
bodyKey = context.getProxyParam().getString("bodyKey");
|
||||
withValueAsBodyHooks.add(new ValueAsBodyHook(bodyKey));
|
||||
withValueAsBodyHookChain = new ProxyHookChain(withValueAsBodyHooks);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean parseParameter(JSONObject params) {
|
||||
if (!super.parseParameter(params)) {
|
||||
return false;
|
||||
}
|
||||
return !Strings.isNullOrEmpty(bodyKey);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProxyHookChain getHookChain() {
|
||||
return withValueAsBodyHookChain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定key对应的value作为请求body,必须放在ProxyHookChain的末尾
|
||||
*/
|
||||
public static class ValueAsBodyHook implements ProxyHook {
|
||||
private String bodyKey;
|
||||
|
||||
public ValueAsBodyHook(String bodyKey) {
|
||||
this.bodyKey = bodyKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestParams preRequest(RequestContext reqContext, ProxyContext proxyContext,
|
||||
RpcClient rpcClient, String postURL, @NonNull final RequestParams params) {
|
||||
Preconditions.checkArgument(params instanceof RequestParams.BodyParams,
|
||||
"ValueAsBodyProxy的请求参数不是BodyParams");
|
||||
RequestParams.BodyParams bodyParams = (RequestParams.BodyParams) params;
|
||||
Preconditions.checkArgument(bodyParams.getData() instanceof JSONObject,
|
||||
"ValueAsBodyProxy的请求参数不是JSONObject");
|
||||
String bodyValue = ((JSONObject) bodyParams.getData()).getString(bodyKey);
|
||||
return bodyParams.toBuilder().content(bodyValue).build();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,514 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.exception.InputFieldAbsentException;
|
||||
import cn.axzo.foundation.gateway.support.exception.OutputFieldAbsentException;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.beanutils.PropertyUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 参数过滤器,根据过滤规则对函数的输入参数或返回对象的属性进行过滤处理
|
||||
*/
|
||||
@Slf4j
|
||||
public class ParameterFilter {
|
||||
|
||||
public static final String FILTER_SEPARATOR = ",";
|
||||
public static final String FILTER_DELIMITER = ":";
|
||||
public static final String EXTENDS_PREFIX = "__extend.";
|
||||
|
||||
private static final String ENV_PREFIX = "__";
|
||||
private static final String CONSTANT_PREFIX = "__constant.";
|
||||
/**
|
||||
* 参数过滤器目前有两个情况包含'|',一种是常量值,表示转换后的属性值是一个或多个常量组成的ArrayList
|
||||
* 另一种是'|'分隔的属性值,此时将使用'|'分隔的一个或多个属性值构成的ArrayList作为转换后的属性值
|
||||
*/
|
||||
private static final String ARRAY_SEPARATOR = "|";
|
||||
|
||||
private Context context;
|
||||
|
||||
private ParameterFilter(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取参数过滤器
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public static ParameterFilter buildFilter() {
|
||||
return buildFilter(null, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表达式获取参数过滤器
|
||||
*
|
||||
* @param inFilterExpression 不支持嵌套,支持格式如: "prop1:renamed1, prop2"
|
||||
* @param outFilterExpression 支持嵌套,支持格式如: "prop1.sub1.sub2:renamed1.renamed2, prop2.sub3"
|
||||
* @return
|
||||
*/
|
||||
public static ParameterFilter buildFilter(String inFilterExpression, String outFilterExpression) {
|
||||
return buildFilter(inFilterExpression, outFilterExpression, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表达式获取参数过滤器
|
||||
*
|
||||
* @param inFilterExpression 不支持嵌套,支持格式如: "prop1:renamed1, prop2"
|
||||
* @param outFilterExpression 支持嵌套,支持格式如: "prop1.sub1.sub2:renamed1.renamed2, prop2.sub3"
|
||||
* @param logEnabled 是否打开debug
|
||||
* @return
|
||||
*/
|
||||
public static ParameterFilter buildFilter(String inFilterExpression, String outFilterExpression, boolean logEnabled) {
|
||||
Context context = buildContext(inFilterExpression, outFilterExpression, logEnabled);
|
||||
if (Objects.isNull(context)) {
|
||||
return null;
|
||||
}
|
||||
return new ParameterFilter(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据表达式获取参数过滤器上下文
|
||||
*
|
||||
* @param inFilterExpression 不支持嵌套,支持格式如: "prop1:renamed1, prop2"
|
||||
* @param outFilterExpression 支持嵌套,支持格式如: "prop1.sub1.sub2:renamed1.renamed2, prop2.sub3"
|
||||
* @return
|
||||
*/
|
||||
private static Context buildContext(String inFilterExpression, String outFilterExpression, boolean logEnabled) {
|
||||
return Context.build(inFilterExpression, outFilterExpression, logEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤输入参数后执行函数,并返回过滤后的对象,直接对返回对象进行过滤处理
|
||||
*
|
||||
* @param params 输入参数
|
||||
* @param function 需要执行的函数,接收Map类型的输入参数, 返回Java Bean对象
|
||||
* @return
|
||||
*/
|
||||
public <T> T invoke(Map params, Function<Map, T> function) {
|
||||
Preconditions.checkArgument(Objects.nonNull(function));
|
||||
return filterOutputObject(function.apply(filterInputParams(params)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤输入参数后执行函数,并返回过滤后的对象(仅对返回对象中指定的属性进行过滤处理)
|
||||
*
|
||||
* @param params 输入参数
|
||||
* @param outputPropertyName 指定返回对象中需要过滤处理的属性名
|
||||
* @param function 需要执行的函数,接收Map类型的输入参数, 返回Java Bean对象
|
||||
* @return
|
||||
*/
|
||||
public <T> T invoke(Map params, String outputPropertyName, Function<Map, T> function) {
|
||||
if (context.logEnabled) {
|
||||
log.info("ParameterFilter invoke({}, {}), context={}", params, outputPropertyName, context);
|
||||
}
|
||||
Preconditions.checkArgument(!Strings.isNullOrEmpty(outputPropertyName) && Objects.nonNull(function));
|
||||
|
||||
T output = function.apply(filterInputParams(params));
|
||||
if (context.logEnabled) {
|
||||
log.info("function apply, output={}, context={}", output, context);
|
||||
}
|
||||
if (Objects.isNull(output) || context.outFilters.isEmpty()) {
|
||||
return output;
|
||||
}
|
||||
|
||||
Object outputProperty;
|
||||
try {
|
||||
outputProperty = PropertyUtils.getProperty(output, outputPropertyName);
|
||||
} catch (Exception e) {
|
||||
log.error("get output property exception, output={}, outputPropertyName={}, context={}",
|
||||
output, outputPropertyName, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
Object filteredOutput = filterOutputObject(outputProperty);
|
||||
try {
|
||||
PropertyUtils.setProperty(output, outputPropertyName, filteredOutput);
|
||||
} catch (Exception e) {
|
||||
log.error("set output property exception, output={}, outputPropertyName={}, filteredOutput={}, context={}",
|
||||
output, filteredOutput, outputPropertyName, filteredOutput, context, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否需要过滤输入参数
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean hasInputFilter() {
|
||||
return !CollectionUtils.isEmpty(context.inFilters);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回是否需要过滤返回参数
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public boolean hasOutputFilter() {
|
||||
return !CollectionUtils.isEmpty(context.outFilters);
|
||||
}
|
||||
|
||||
public Map filterInputParams(Map params) {
|
||||
return filterInputParams(params, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤输入参数
|
||||
*
|
||||
* @param params
|
||||
* @return
|
||||
*/
|
||||
public Map filterInputParams(Map params, Map env) {
|
||||
return filterObject(params, context.inFilters, context.inputPrototypeStr, env,
|
||||
context.inFilterExtensionFields, context.logEnabled, s -> new InputFieldAbsentException(String.format("请求属性%s缺失", s)));
|
||||
}
|
||||
|
||||
public <T> T filterOutputObject(T output) {
|
||||
return filterOutputObject(output, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤返回对象
|
||||
*
|
||||
* @param output
|
||||
* @return
|
||||
*/
|
||||
public <T> T filterOutputObject(T output, Map env) {
|
||||
return filterObject(output, context.outFilters, context.outputPrototypeStr, env,
|
||||
context.outFilterExtensionFields, context.logEnabled, s -> new OutputFieldAbsentException(String.format("返回属性%s缺失", s)));
|
||||
}
|
||||
|
||||
private static <T> T filterObject(T obj, Map<String, String> filters, String prototype,
|
||||
Map env, Set<ExtensionField> extensionFields, boolean logEnabled,
|
||||
Function<String, ? extends RuntimeException> fieldAbsentExceptionFunc) {
|
||||
if (logEnabled) {
|
||||
log.info("filterObject({}), filters={}, prototype={}, extensions={}", obj, filters, prototype, extensionFields);
|
||||
}
|
||||
|
||||
if (CollectionUtils.isEmpty(filters)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
JSONObject prototypeJson = JSONObject.parseObject(prototype);
|
||||
if (extensionFields.contains(ExtensionField.INCLUDE_OTHER_FIELD)) {
|
||||
prototypeJson.putAll(JSON.parseObject(JSON.toJSONString(obj)));
|
||||
if (logEnabled) {
|
||||
log.info("include other field obj={}, prototypeJson={}", obj, prototypeJson);
|
||||
}
|
||||
}
|
||||
|
||||
boolean validate = extensionFields.contains(ExtensionField.VALIDATE_FIELD);
|
||||
applyFilter(prototypeJson, obj, env, filters, validate, logEnabled, fieldAbsentExceptionFunc);
|
||||
|
||||
if (logEnabled) {
|
||||
log.info("filterObject property processed, {} --> {}, filters={}, prototype={}", obj, prototypeJson, filters, prototype);
|
||||
}
|
||||
|
||||
T filtered;
|
||||
if (Map.class.isAssignableFrom(obj.getClass())) {
|
||||
// XXX 解决反序列化不支持HashMap: can not get javaBeanDeserializer. java.util.HashMap
|
||||
filtered = prototypeJson.<T>toJavaObject(Map.class);
|
||||
} else {
|
||||
filtered = prototypeJson.<T>toJavaObject(obj.getClass());
|
||||
}
|
||||
if (logEnabled) {
|
||||
log.info("filterObject finished {} --> {}, filters={}, prototype={}", obj, filtered, filters, prototype);
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private static void applyFilter(JSONObject prototypeJson, Object obj, Map env,
|
||||
Map<String, String> filters, boolean validate, boolean logEnabled,
|
||||
Function<String, ? extends RuntimeException> fieldAbsentExceptionFunc) {
|
||||
filters.forEach((filteredProperty, property) -> {
|
||||
Object source = obj;
|
||||
Object value = null;
|
||||
|
||||
// 标识是常量则直接赋值,example:__constant.abc:code,表示设置request body中固定code字段的值为abc
|
||||
if (property.startsWith(CONSTANT_PREFIX)) {
|
||||
value = StringUtils.substringAfter(property, CONSTANT_PREFIX);
|
||||
|
||||
// 如果常量值中包含|, 则将值解析为数组
|
||||
if (value.toString().contains(ARRAY_SEPARATOR)) {
|
||||
value = Splitter.on(ARRAY_SEPARATOR)
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(value.toString());
|
||||
}
|
||||
} else if (property.startsWith(ENV_PREFIX)) {
|
||||
source = env;
|
||||
property = StringUtils.substringAfter(property, ENV_PREFIX);
|
||||
} else if (property.contains(ARRAY_SEPARATOR)) {
|
||||
value = Splitter.on(ARRAY_SEPARATOR)
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(property)
|
||||
.stream()
|
||||
.map(p -> {
|
||||
try {
|
||||
return PropertyUtils.getProperty(obj, p);
|
||||
} catch (Exception e) {
|
||||
if (validate) {
|
||||
if (logEnabled) {
|
||||
log.info("对象{}缺少字段{}", obj, p);
|
||||
}
|
||||
throw fieldAbsentExceptionFunc.apply(String.format("对象缺少字段: %s", p));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (Objects.isNull(value)) {
|
||||
try {
|
||||
value = PropertyUtils.getProperty(source, property);
|
||||
} catch (Exception e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
if (Objects.isNull(value) && validate) {
|
||||
if (logEnabled) {
|
||||
log.info("对象{}缺少字段{}", source, property);
|
||||
}
|
||||
throw fieldAbsentExceptionFunc.apply(String.format("对象缺少字段: %s", property));
|
||||
}
|
||||
|
||||
try {
|
||||
PropertyUtils.setProperty(prototypeJson, filteredProperty, value);
|
||||
if (logEnabled) {
|
||||
log.info("filterObject convert {} to {}, value={}", property, filteredProperty, value);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("set property exception, prototypeJson={}, property={}, value={}, filters={}",
|
||||
prototypeJson, property, value, filters, e);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过滤器中的输入参数过滤规则
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getInFilters() {
|
||||
return context.inFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过滤器中的输入参数过滤规则扩展信息定义
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getInFilterExtends() {
|
||||
return context.inFilterExtends;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过滤器中返回对象过滤参数
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getOutFilters() {
|
||||
return context.outFilters;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过滤器中返回对象过滤规则扩展信息定义
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Map<String, String> getOutFilterExtends() {
|
||||
return context.outFilterExtends;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过滤器中输入参数过滤规则的特殊扩展字段
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Set<ExtensionField> getInFilterExtensionFields() {
|
||||
return context.inFilterExtensionFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取过滤器中返回对象过滤规则的特殊扩展字段
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Set<ExtensionField> getOUtFilterExtensionFields() {
|
||||
return context.outFilterExtensionFields;
|
||||
}
|
||||
|
||||
@ToString
|
||||
static class Context {
|
||||
|
||||
static final Pattern FILTER_PATTERN = Pattern.compile(Regex.PROXY_PARAM_FILTER_REGEX);
|
||||
private static final String PROPERTY_DELIMITER = ".";
|
||||
|
||||
private final Map<String, String> inFilters;
|
||||
private final Map<String, String> inFilterExtends;
|
||||
private final Map<String, String> outFilters;
|
||||
private final Map<String, String> outFilterExtends;
|
||||
private final String inputPrototypeStr;
|
||||
private final String outputPrototypeStr;
|
||||
private final boolean logEnabled;
|
||||
private final Set<ExtensionField> inFilterExtensionFields;
|
||||
private final Set<ExtensionField> outFilterExtensionFields;
|
||||
|
||||
private Context(Map<String, String> inFilters, Map<String, String> inFilterExtends,
|
||||
Map<String, String> outFilters, Map<String, String> outFilterExtends,
|
||||
Set<ExtensionField> inFilterExtensionFields, Set<ExtensionField> outFilterExtensionFields,
|
||||
String inputPrototypeStr, String outputPrototypeStr,
|
||||
boolean logEnabled) {
|
||||
this.inFilters = ImmutableMap.copyOf(inFilters);
|
||||
this.inFilterExtends = inFilterExtends;
|
||||
|
||||
this.outFilters = ImmutableMap.copyOf(outFilters);
|
||||
this.outFilterExtends = outFilterExtends;
|
||||
|
||||
this.inFilterExtensionFields = inFilterExtensionFields;
|
||||
this.outFilterExtensionFields = outFilterExtensionFields;
|
||||
this.inputPrototypeStr = inputPrototypeStr;
|
||||
this.outputPrototypeStr = outputPrototypeStr;
|
||||
this.logEnabled = logEnabled;
|
||||
}
|
||||
|
||||
private static Context build(String inFilterExpression, String outFilterExpression, boolean logEnabled) {
|
||||
Map<String, String> inFilters = parseFilter(inFilterExpression, FILTER_PATTERN);
|
||||
Map<String, String> outFilters = parseFilter(outFilterExpression, FILTER_PATTERN);
|
||||
Map<String, String> inFilterExtends = extractFilterExtends(inFilterExpression);
|
||||
Map<String, String> outFilterExtends = extractFilterExtends(outFilterExpression);
|
||||
if (Objects.isNull(inFilters) || Objects.isNull(outFilters)) {
|
||||
return null;
|
||||
}
|
||||
Set<ExtensionField> inFilterExtensionFields = ExtensionField.resolveExtensions(inFilters);
|
||||
Set<ExtensionField> outFilterExtensionFields = ExtensionField.resolveExtensions(outFilters);
|
||||
String inputPrototypeStr = constructPrototype(inFilters.keySet());
|
||||
String outputPrototypeStr = constructPrototype(outFilters.keySet());
|
||||
return new Context(inFilters, inFilterExtends, outFilters, outFilterExtends, inFilterExtensionFields, outFilterExtensionFields,
|
||||
inputPrototypeStr, outputPrototypeStr, logEnabled);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param expression 表达式格式如下 "输入字段1:输出字段1, 输入字段2:输出字段2, 输入字段3"
|
||||
* @return {"输出字段1":"输入字段1", "输出字段2":"输入字段2", "输入字段3:"输入字段3""}
|
||||
*/
|
||||
static Map<String, String> parseFilter(String expression, Pattern pattern) {
|
||||
if (Strings.isNullOrEmpty(expression)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
List<String> filters = Splitter.on(FILTER_SEPARATOR)
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(expression)
|
||||
.stream().filter(e -> !e.startsWith(EXTENDS_PREFIX)).collect(Collectors.toList());
|
||||
if (filters.stream()
|
||||
.filter(f -> !f.startsWith(CONSTANT_PREFIX))
|
||||
.map(f -> StringUtils.remove(f, ARRAY_SEPARATOR))
|
||||
.anyMatch(f -> !pattern.matcher(f).matches())) {
|
||||
// 格式错误,返回null以便错误处理
|
||||
return null;
|
||||
}
|
||||
return filters.stream().collect(Collectors.toMap(
|
||||
f -> f.contains(FILTER_DELIMITER) ? StringUtils.substringAfter(f, FILTER_DELIMITER) : f,
|
||||
f -> StringUtils.substringBefore(f, FILTER_DELIMITER),
|
||||
(left, right) -> right));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param expression 表达式格式如下 "__extend.bean:xxx, __extend.disableHook"
|
||||
* @return {"bean":"xxx", "disableHook":""}
|
||||
*/
|
||||
static Map<String, String> extractFilterExtends(String expression) {
|
||||
if (Strings.isNullOrEmpty(expression)) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
|
||||
List<String> filters = Splitter.on(FILTER_SEPARATOR)
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(expression)
|
||||
.stream().filter(e -> e.startsWith(EXTENDS_PREFIX)).collect(Collectors.toList());
|
||||
return filters.stream().map(e -> StringUtils.substringAfter(e, EXTENDS_PREFIX))
|
||||
.collect(Collectors.toMap(
|
||||
f -> f.contains(FILTER_DELIMITER) ? StringUtils.substringBefore(f, FILTER_DELIMITER) : f,
|
||||
f -> StringUtils.substringAfter(f, FILTER_DELIMITER),
|
||||
(left, right) -> right));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据返回对象的属性列表构造返回对象
|
||||
*
|
||||
* @param propertyList 属性列表,支持嵌套格式,比如['a.b.c', 'e',' f.g']
|
||||
* @return
|
||||
*/
|
||||
static String constructPrototype(Collection<String> propertyList) {
|
||||
if (CollectionUtils.isEmpty(propertyList)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, Object> prototype = Maps.newHashMap();
|
||||
propertyList.forEach(p -> {
|
||||
List<String> nestedProperties = Splitter.on(PROPERTY_DELIMITER)
|
||||
.trimResults()
|
||||
.omitEmptyStrings()
|
||||
.splitToList(p);
|
||||
resolveNestedProperty(prototype, nestedProperties);
|
||||
});
|
||||
return JSONObject.toJSONString(prototype);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用该方法通过嵌套的属性名称生成对应的Map并填入object中
|
||||
*
|
||||
* @param map Map对象
|
||||
* @param nestedProperties 嵌套的属性名称列表,如['a', 'b', 'c']
|
||||
*/
|
||||
static void resolveNestedProperty(Map map, List<String> nestedProperties) {
|
||||
if (Optional.ofNullable(nestedProperties).map(List::size).orElse(0) <= 1) {
|
||||
// 最后一层嵌套属性不需要处理成Map
|
||||
return;
|
||||
}
|
||||
|
||||
Object next = map.computeIfAbsent(nestedProperties.get(0), (k) -> Maps.<String, Object>newHashMap());
|
||||
resolveNestedProperty((Map) next, nestedProperties.stream().skip(1).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ExtensionField {
|
||||
INCLUDE_OTHER_FIELD("*", "过滤时需包含Filter以外的字段"),
|
||||
VALIDATE_FIELD("__validate", "Filter中的字段不存在则报错");
|
||||
|
||||
private String flag;
|
||||
private String description;
|
||||
|
||||
static Set<ExtensionField> resolveExtensions(Map<String, String> filters) {
|
||||
return Arrays.stream(values())
|
||||
.filter(e -> Objects.nonNull(filters.remove(e.flag)))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,637 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
*
|
||||
* 根据参数格式化message
|
||||
* {@link org.apache.logging.log4j.message}
|
||||
*/
|
||||
public class ParameterFormatter {
|
||||
/**
|
||||
* Prefix for recursion.
|
||||
*/
|
||||
static final String RECURSION_PREFIX = "[...";
|
||||
/**
|
||||
* Suffix for recursion.
|
||||
*/
|
||||
static final String RECURSION_SUFFIX = "...]";
|
||||
|
||||
/**
|
||||
* Prefix for errors.
|
||||
*/
|
||||
static final String ERROR_PREFIX = "[!!!";
|
||||
/**
|
||||
* Separator for errors.
|
||||
*/
|
||||
static final String ERROR_SEPARATOR = "=>";
|
||||
/**
|
||||
* Separator for error messages.
|
||||
*/
|
||||
static final String ERROR_MSG_SEPARATOR = ":";
|
||||
/**
|
||||
* Suffix for errors.
|
||||
*/
|
||||
static final String ERROR_SUFFIX = "!!!]";
|
||||
|
||||
private static final char DELIM_START = '{';
|
||||
private static final char DELIM_STOP = '}';
|
||||
private static final char ESCAPE_CHAR = '\\';
|
||||
|
||||
private static ThreadLocal<SimpleDateFormat> threadLocalSimpleDateFormat = new ThreadLocal<>();
|
||||
|
||||
private ParameterFormatter() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of unescaped placeholders in the given messagePattern.
|
||||
*
|
||||
* @param messagePattern the message pattern to be analyzed.
|
||||
* @return the number of unescaped placeholders.
|
||||
*/
|
||||
static int countArgumentPlaceholders(final String messagePattern) {
|
||||
if (messagePattern == null) {
|
||||
return 0;
|
||||
}
|
||||
final int length = messagePattern.length();
|
||||
int result = 0;
|
||||
boolean isEscaped = false;
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
final char curChar = messagePattern.charAt(i);
|
||||
if (curChar == ESCAPE_CHAR) {
|
||||
isEscaped = !isEscaped;
|
||||
} else if (curChar == DELIM_START) {
|
||||
if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
|
||||
result++;
|
||||
i++;
|
||||
}
|
||||
isEscaped = false;
|
||||
} else {
|
||||
isEscaped = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of unescaped placeholders in the given messagePattern.
|
||||
*
|
||||
* @param messagePattern the message pattern to be analyzed.
|
||||
* @return the number of unescaped placeholders.
|
||||
*/
|
||||
static int countArgumentPlaceholders2(final String messagePattern, final int[] indices) {
|
||||
if (messagePattern == null) {
|
||||
return 0;
|
||||
}
|
||||
final int length = messagePattern.length();
|
||||
int result = 0;
|
||||
boolean isEscaped = false;
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
final char curChar = messagePattern.charAt(i);
|
||||
if (curChar == ESCAPE_CHAR) {
|
||||
isEscaped = !isEscaped;
|
||||
indices[0] = -1; // escaping means fast path is not available...
|
||||
result++;
|
||||
} else if (curChar == DELIM_START) {
|
||||
if (!isEscaped && messagePattern.charAt(i + 1) == DELIM_STOP) {
|
||||
indices[result] = i;
|
||||
result++;
|
||||
i++;
|
||||
}
|
||||
isEscaped = false;
|
||||
} else {
|
||||
isEscaped = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of unescaped placeholders in the given messagePattern.
|
||||
*
|
||||
* @param messagePattern the message pattern to be analyzed.
|
||||
* @return the number of unescaped placeholders.
|
||||
*/
|
||||
static int countArgumentPlaceholders3(final char[] messagePattern, final int length, final int[] indices) {
|
||||
int result = 0;
|
||||
boolean isEscaped = false;
|
||||
for (int i = 0; i < length - 1; i++) {
|
||||
final char curChar = messagePattern[i];
|
||||
if (curChar == ESCAPE_CHAR) {
|
||||
isEscaped = !isEscaped;
|
||||
} else if (curChar == DELIM_START) {
|
||||
if (!isEscaped && messagePattern[i + 1] == DELIM_STOP) {
|
||||
indices[result] = i;
|
||||
result++;
|
||||
i++;
|
||||
}
|
||||
isEscaped = false;
|
||||
} else {
|
||||
isEscaped = false;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholders in the given messagePattern with arguments.
|
||||
*
|
||||
* @param messagePattern the message pattern containing placeholders.
|
||||
* @param arguments the arguments to be used to replace placeholders.
|
||||
* @return the formatted message.
|
||||
*/
|
||||
public static String format(final String messagePattern, final Object[] arguments) {
|
||||
final StringBuilder result = new StringBuilder();
|
||||
final int argCount = arguments == null ? 0 : arguments.length;
|
||||
formatMessage(result, messagePattern, arguments, argCount);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholders in the given messagePattern with arguments.
|
||||
*
|
||||
* @param buffer the buffer to write the formatted message into
|
||||
* @param messagePattern the message pattern containing placeholders.
|
||||
* @param arguments the arguments to be used to replace placeholders.
|
||||
*/
|
||||
static void formatMessage2(final StringBuilder buffer, final String messagePattern,
|
||||
final Object[] arguments, final int argCount, final int[] indices) {
|
||||
if (messagePattern == null || arguments == null || argCount == 0) {
|
||||
buffer.append(messagePattern);
|
||||
return;
|
||||
}
|
||||
int previous = 0;
|
||||
for (int i = 0; i < argCount; i++) {
|
||||
buffer.append(messagePattern, previous, indices[i]);
|
||||
previous = indices[i] + 2;
|
||||
recursiveDeepToString(arguments[i], buffer, null);
|
||||
}
|
||||
buffer.append(messagePattern, previous, messagePattern.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholders in the given messagePattern with arguments.
|
||||
*
|
||||
* @param buffer the buffer to write the formatted message into
|
||||
* @param messagePattern the message pattern containing placeholders.
|
||||
* @param arguments the arguments to be used to replace placeholders.
|
||||
*/
|
||||
static void formatMessage3(final StringBuilder buffer, final char[] messagePattern, final int patternLength,
|
||||
final Object[] arguments, final int argCount, final int[] indices) {
|
||||
if (messagePattern == null) {
|
||||
return;
|
||||
}
|
||||
if (arguments == null || argCount == 0) {
|
||||
buffer.append(messagePattern);
|
||||
return;
|
||||
}
|
||||
int previous = 0;
|
||||
for (int i = 0; i < argCount; i++) {
|
||||
buffer.append(messagePattern, previous, indices[i]);
|
||||
previous = indices[i] + 2;
|
||||
recursiveDeepToString(arguments[i], buffer, null);
|
||||
}
|
||||
buffer.append(messagePattern, previous, patternLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace placeholders in the given messagePattern with arguments.
|
||||
*
|
||||
* @param buffer the buffer to write the formatted message into
|
||||
* @param messagePattern the message pattern containing placeholders.
|
||||
* @param arguments the arguments to be used to replace placeholders.
|
||||
*/
|
||||
static void formatMessage(final StringBuilder buffer, final String messagePattern,
|
||||
final Object[] arguments, final int argCount) {
|
||||
if (messagePattern == null || arguments == null || argCount == 0) {
|
||||
buffer.append(messagePattern);
|
||||
return;
|
||||
}
|
||||
int escapeCounter = 0;
|
||||
int currentArgument = 0;
|
||||
int i = 0;
|
||||
final int len = messagePattern.length();
|
||||
for (; i < len - 1; i++) { // last char is excluded from the loop
|
||||
final char curChar = messagePattern.charAt(i);
|
||||
if (curChar == ESCAPE_CHAR) {
|
||||
escapeCounter++;
|
||||
} else {
|
||||
if (isDelimPair(curChar, messagePattern, i)) { // looks ahead one char
|
||||
i++;
|
||||
|
||||
// write escaped escape chars
|
||||
writeEscapedEscapeChars(escapeCounter, buffer);
|
||||
|
||||
if (isOdd(escapeCounter)) {
|
||||
// i.e. escaped: write escaped escape chars
|
||||
writeDelimPair(buffer);
|
||||
} else {
|
||||
// unescaped
|
||||
writeArgOrDelimPair(arguments, argCount, currentArgument, buffer);
|
||||
currentArgument++;
|
||||
}
|
||||
} else {
|
||||
handleLiteralChar(buffer, escapeCounter, curChar);
|
||||
}
|
||||
escapeCounter = 0;
|
||||
}
|
||||
}
|
||||
handleRemainingCharIfAny(messagePattern, len, buffer, escapeCounter, i);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the specified char and the char at {@code curCharIndex + 1} in the specified message
|
||||
* pattern together form a "{}" delimiter pair, returns {@code false} otherwise.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 22 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static boolean isDelimPair(final char curChar, final String messagePattern, final int curCharIndex) {
|
||||
return curChar == DELIM_START && messagePattern.charAt(curCharIndex + 1) == DELIM_STOP;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects whether the message pattern has been fully processed or if an unprocessed character remains and processes
|
||||
* it if necessary, returning the resulting position in the result char array.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static void handleRemainingCharIfAny(final String messagePattern, final int len,
|
||||
final StringBuilder buffer, final int escapeCounter, final int i) {
|
||||
if (i == len - 1) {
|
||||
final char curChar = messagePattern.charAt(i);
|
||||
handleLastChar(buffer, escapeCounter, curChar);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the last unprocessed character and returns the resulting position in the result char array.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 28 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static void handleLastChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
|
||||
if (curChar == ESCAPE_CHAR) {
|
||||
writeUnescapedEscapeChars(escapeCounter + 1, buffer);
|
||||
} else {
|
||||
handleLiteralChar(buffer, escapeCounter, curChar);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a literal char (neither an '\' escape char nor a "{}" delimiter pair) and returns the resulting
|
||||
* position.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 16 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static void handleLiteralChar(final StringBuilder buffer, final int escapeCounter, final char curChar) {
|
||||
// any other char beside ESCAPE or DELIM_START/STOP-combo
|
||||
// write unescaped escape chars
|
||||
writeUnescapedEscapeChars(escapeCounter, buffer);
|
||||
buffer.append(curChar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes "{}" to the specified result array at the specified position and returns the resulting position.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 18 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static void writeDelimPair(final StringBuilder buffer) {
|
||||
buffer.append(DELIM_START);
|
||||
buffer.append(DELIM_STOP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the specified parameter is odd.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static boolean isOdd(final int number) {
|
||||
return (number & 1) == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a '\' char to the specified result array (starting at the specified position) for each <em>pair</em> of
|
||||
* '\' escape chars encountered in the message format and returns the resulting position.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 11 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static void writeEscapedEscapeChars(final int escapeCounter, final StringBuilder buffer) {
|
||||
final int escapedEscapes = escapeCounter >> 1; // divide by two
|
||||
writeUnescapedEscapeChars(escapedEscapes, buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the specified number of '\' chars to the specified result array (starting at the specified position) and
|
||||
* returns the resulting position.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 20 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static void writeUnescapedEscapeChars(int escapeCounter, final StringBuilder buffer) {
|
||||
while (escapeCounter > 0) {
|
||||
buffer.append(ESCAPE_CHAR);
|
||||
escapeCounter--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the argument at the specified argument index (or, if no such argument exists, the "{}" delimiter pair) to
|
||||
* the specified result char array at the specified position and returns the resulting position.
|
||||
*/
|
||||
// Profiling showed this method is important to log4j performance. Modify with care!
|
||||
// 25 bytes (allows immediate JVM inlining: < 35 bytes) LOG4J2-1096
|
||||
private static void writeArgOrDelimPair(final Object[] arguments, final int argCount, final int currentArgument,
|
||||
final StringBuilder buffer) {
|
||||
if (currentArgument < argCount) {
|
||||
recursiveDeepToString(arguments[currentArgument], buffer, null);
|
||||
} else {
|
||||
writeDelimPair(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method performs a deep toString of the given Object.
|
||||
* Primitive arrays are converted using their respective Arrays.toString methods while
|
||||
* special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
|
||||
* contain themselves.
|
||||
* <p>
|
||||
* It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
|
||||
* behavior. They only check if the container is directly contained in itself, but not if a contained container
|
||||
* contains the original one. Because of that, Arrays.toString(Object[]) isn't safe either.
|
||||
* Confusing? Just read the last paragraph again and check the respective toString() implementation.
|
||||
* </p>
|
||||
* <p>
|
||||
* This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
|
||||
* would produce a relatively hard-to-debug StackOverflowError.
|
||||
* </p>
|
||||
*
|
||||
* @param o The object.
|
||||
* @return The String representation.
|
||||
*/
|
||||
static String deepToString(final Object o) {
|
||||
if (o == null) {
|
||||
return null;
|
||||
}
|
||||
if (o instanceof String) {
|
||||
return (String) o;
|
||||
}
|
||||
final StringBuilder str = new StringBuilder();
|
||||
final Set<String> dejaVu = new HashSet<>(); // that's actually a neat name ;)
|
||||
recursiveDeepToString(o, str, dejaVu);
|
||||
return str.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method performs a deep toString of the given Object.
|
||||
* Primitive arrays are converted using their respective Arrays.toString methods while
|
||||
* special handling is implemented for "container types", i.e. Object[], Map and Collection because those could
|
||||
* contain themselves.
|
||||
* <p>
|
||||
* dejaVu is used in case of those container types to prevent an endless recursion.
|
||||
* </p>
|
||||
* <p>
|
||||
* It should be noted that neither AbstractMap.toString() nor AbstractCollection.toString() implement such a
|
||||
* behavior.
|
||||
* They only check if the container is directly contained in itself, but not if a contained container contains the
|
||||
* original one. Because of that, Arrays.toString(Object[]) isn't safe either.
|
||||
* Confusing? Just read the last paragraph again and check the respective toString() implementation.
|
||||
* </p>
|
||||
* <p>
|
||||
* This means, in effect, that logging would produce a usable output even if an ordinary System.out.println(o)
|
||||
* would produce a relatively hard-to-debug StackOverflowError.
|
||||
* </p>
|
||||
*
|
||||
* @param o the Object to convert into a String
|
||||
* @param str the StringBuilder that o will be appended to
|
||||
* @param dejaVu a list of container identities that were already used.
|
||||
*/
|
||||
private static void recursiveDeepToString(final Object o, final StringBuilder str, final Set<String> dejaVu) {
|
||||
if (appendSpecialTypes(o, str)) {
|
||||
return;
|
||||
}
|
||||
if (isMaybeRecursive(o)) {
|
||||
appendPotentiallyRecursiveValue(o, str, dejaVu);
|
||||
} else {
|
||||
tryObjectToString(o, str);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean appendSpecialTypes(final Object o, final StringBuilder str) {
|
||||
if (o == null || o instanceof String) {
|
||||
str.append((String) o);
|
||||
return true;
|
||||
} else if (o instanceof CharSequence) {
|
||||
str.append((CharSequence) o);
|
||||
return true;
|
||||
} else if (o instanceof Integer) {
|
||||
str.append(((Integer) o).intValue());
|
||||
return true;
|
||||
} else if (o instanceof Long) {
|
||||
str.append(((Long) o).longValue());
|
||||
return true;
|
||||
} else if (o instanceof Double) {
|
||||
str.append(((Double) o).doubleValue());
|
||||
return true;
|
||||
} else if (o instanceof Boolean) {
|
||||
str.append(((Boolean) o).booleanValue());
|
||||
return true;
|
||||
} else if (o instanceof Character) {
|
||||
str.append(((Character) o).charValue());
|
||||
return true;
|
||||
} else if (o instanceof Short) {
|
||||
str.append(((Short) o).shortValue());
|
||||
return true;
|
||||
} else if (o instanceof Float) {
|
||||
str.append(((Float) o).floatValue());
|
||||
return true;
|
||||
}
|
||||
return appendDate(o, str);
|
||||
}
|
||||
|
||||
private static boolean appendDate(final Object o, final StringBuilder str) {
|
||||
if (!(o instanceof Date)) {
|
||||
return false;
|
||||
}
|
||||
final Date date = (Date) o;
|
||||
final SimpleDateFormat format = getSimpleDateFormat();
|
||||
str.append(format.format(date));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static SimpleDateFormat getSimpleDateFormat() {
|
||||
SimpleDateFormat result = threadLocalSimpleDateFormat.get();
|
||||
if (result == null) {
|
||||
result = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
|
||||
threadLocalSimpleDateFormat.set(result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the specified object is an array, a Map or a Collection.
|
||||
*/
|
||||
private static boolean isMaybeRecursive(final Object o) {
|
||||
return o.getClass().isArray() || o instanceof Map || o instanceof Collection;
|
||||
}
|
||||
|
||||
private static void appendPotentiallyRecursiveValue(final Object o, final StringBuilder str,
|
||||
final Set<String> dejaVu) {
|
||||
final Class<?> oClass = o.getClass();
|
||||
if (oClass.isArray()) {
|
||||
appendArray(o, str, dejaVu, oClass);
|
||||
} else if (o instanceof Map) {
|
||||
appendMap(o, str, dejaVu);
|
||||
} else if (o instanceof Collection) {
|
||||
appendCollection(o, str, dejaVu);
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendArray(final Object o, final StringBuilder str, Set<String> dejaVu,
|
||||
final Class<?> oClass) {
|
||||
if (oClass == byte[].class) {
|
||||
str.append(Arrays.toString((byte[]) o));
|
||||
} else if (oClass == short[].class) {
|
||||
str.append(Arrays.toString((short[]) o));
|
||||
} else if (oClass == int[].class) {
|
||||
str.append(Arrays.toString((int[]) o));
|
||||
} else if (oClass == long[].class) {
|
||||
str.append(Arrays.toString((long[]) o));
|
||||
} else if (oClass == float[].class) {
|
||||
str.append(Arrays.toString((float[]) o));
|
||||
} else if (oClass == double[].class) {
|
||||
str.append(Arrays.toString((double[]) o));
|
||||
} else if (oClass == boolean[].class) {
|
||||
str.append(Arrays.toString((boolean[]) o));
|
||||
} else if (oClass == char[].class) {
|
||||
str.append(Arrays.toString((char[]) o));
|
||||
} else {
|
||||
if (dejaVu == null) {
|
||||
dejaVu = new HashSet<>();
|
||||
}
|
||||
// special handling of container Object[]
|
||||
final String id = identityToString(o);
|
||||
if (dejaVu.contains(id)) {
|
||||
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
|
||||
} else {
|
||||
dejaVu.add(id);
|
||||
final Object[] oArray = (Object[]) o;
|
||||
str.append('[');
|
||||
boolean first = true;
|
||||
for (final Object current : oArray) {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
str.append(", ");
|
||||
}
|
||||
recursiveDeepToString(current, str, new HashSet<>(dejaVu));
|
||||
}
|
||||
str.append(']');
|
||||
}
|
||||
//str.append(Arrays.deepToString((Object[]) o));
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendMap(final Object o, final StringBuilder str, Set<String> dejaVu) {
|
||||
// special handling of container Map
|
||||
if (dejaVu == null) {
|
||||
dejaVu = new HashSet<>();
|
||||
}
|
||||
final String id = identityToString(o);
|
||||
if (dejaVu.contains(id)) {
|
||||
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
|
||||
} else {
|
||||
dejaVu.add(id);
|
||||
final Map<?, ?> oMap = (Map<?, ?>) o;
|
||||
str.append('{');
|
||||
boolean isFirst = true;
|
||||
for (final Object o1 : oMap.entrySet()) {
|
||||
final Map.Entry<?, ?> current = (Map.Entry<?, ?>) o1;
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
} else {
|
||||
str.append(", ");
|
||||
}
|
||||
final Object key = current.getKey();
|
||||
final Object value = current.getValue();
|
||||
recursiveDeepToString(key, str, new HashSet<>(dejaVu));
|
||||
str.append('=');
|
||||
recursiveDeepToString(value, str, new HashSet<>(dejaVu));
|
||||
}
|
||||
str.append('}');
|
||||
}
|
||||
}
|
||||
|
||||
private static void appendCollection(final Object o, final StringBuilder str, Set<String> dejaVu) {
|
||||
// special handling of container Collection
|
||||
if (dejaVu == null) {
|
||||
dejaVu = new HashSet<>();
|
||||
}
|
||||
final String id = identityToString(o);
|
||||
if (dejaVu.contains(id)) {
|
||||
str.append(RECURSION_PREFIX).append(id).append(RECURSION_SUFFIX);
|
||||
} else {
|
||||
dejaVu.add(id);
|
||||
final Collection<?> oCol = (Collection<?>) o;
|
||||
str.append('[');
|
||||
boolean isFirst = true;
|
||||
for (final Object anOCol : oCol) {
|
||||
if (isFirst) {
|
||||
isFirst = false;
|
||||
} else {
|
||||
str.append(", ");
|
||||
}
|
||||
recursiveDeepToString(anOCol, str, new HashSet<>(dejaVu));
|
||||
}
|
||||
str.append(']');
|
||||
}
|
||||
}
|
||||
|
||||
private static void tryObjectToString(final Object o, final StringBuilder str) {
|
||||
// it's just some other Object, we can only use toString().
|
||||
try {
|
||||
str.append(o.toString());
|
||||
} catch (final Throwable t) {
|
||||
handleErrorInObjectToString(o, str, t);
|
||||
}
|
||||
}
|
||||
|
||||
private static void handleErrorInObjectToString(final Object o, final StringBuilder str, final Throwable t) {
|
||||
str.append(ERROR_PREFIX);
|
||||
str.append(identityToString(o));
|
||||
str.append(ERROR_SEPARATOR);
|
||||
final String msg = t.getMessage();
|
||||
final String className = t.getClass().getName();
|
||||
str.append(className);
|
||||
if (!className.equals(msg)) {
|
||||
str.append(ERROR_MSG_SEPARATOR);
|
||||
str.append(msg);
|
||||
}
|
||||
str.append(ERROR_SUFFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method returns the same as if Object.toString() would not have been
|
||||
* overridden in obj.
|
||||
* <p>
|
||||
* Note that this isn't 100% secure as collisions can always happen with hash codes.
|
||||
* </p>
|
||||
* <p>
|
||||
* Copied from Object.hashCode():
|
||||
* </p>
|
||||
* <blockquote>
|
||||
* As much as is reasonably practical, the hashCode method defined by
|
||||
* class {@code Object} does return distinct integers for distinct
|
||||
* objects. (This is typically implemented by converting the internal
|
||||
* address of the object into an integer, but this implementation
|
||||
* technique is not required by the Java™ programming language.)
|
||||
* </blockquote>
|
||||
*
|
||||
* @param obj the Object that is to be converted into an identity string.
|
||||
* @return the identity string as also defined in Object.toString()
|
||||
*/
|
||||
static String identityToString(final Object obj) {
|
||||
if (obj == null) {
|
||||
return null;
|
||||
}
|
||||
return obj.getClass().getName() + '@' + Integer.toHexString(System.identityHashCode(obj));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
public class Regex {
|
||||
|
||||
/**
|
||||
* 匹配http(s)开头的host,支持'-'连接及端口,如: http://foo.xxx.com:8080
|
||||
*/
|
||||
public static final String SERVICE_HOST_REGEX = "^https?://(\\w+(-\\w+)*)+(.(\\w+(-\\w+)*))*(:\\d{1,5})?$";
|
||||
|
||||
/**
|
||||
* 匹配代理配置参数中的返回对象转换及过滤规则,支持嵌套的属性名称
|
||||
* 支持的规则包括:
|
||||
* 'a:x' 表示将后端服务返回对象中属性名为a的值设置为网关返回对象属性名为x的值
|
||||
* 'a.b:x.y.z' 表示将返回对象中的嵌套属性a.b的值设置为网关返回对象嵌套属性名为x.y.z的值
|
||||
* 多条规则以','分隔,如:
|
||||
* 'a.b:x.y.z, c' 表示将返回对象中a.b的值设置为网关返回对象x.y.z的值,返回对象中的c仍设置为网关返回对象中的c,
|
||||
* 当存在过滤规则时,没有在规则中出现的字段将被忽略
|
||||
* 嵌套属性的规则参考:https://commons.apache.org/proper/commons-beanutils/javadocs/v1.9.0/apidocs/org/apache/commons/beanutils/PropertyUtilsBean.html
|
||||
* 暂时仅支持Simple及Nested,不支持Indexed,Mapped,Combined
|
||||
* Extensions:
|
||||
* '*' 表示将包含没有在规则中出现的字段及其对应的值
|
||||
* '__validate' 表示将校验规则中出现的字段,不存在则报错
|
||||
*/
|
||||
public static final String PROXY_PARAM_FILTER_REGEX = "^(\\w+(\\.\\w+)*):(\\w+(\\.\\w+)*)|(\\w+(\\.\\w+)*)|\\*$";
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.google.common.base.Preconditions;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@Slf4j
|
||||
public class RpcClientProvider {
|
||||
|
||||
@Getter
|
||||
private final RpcClient defaultRpcClient;
|
||||
/** 为service指定的rpc client, key: appName */
|
||||
private final Map<String, RpcClient> serviceRpcClientMap;
|
||||
|
||||
public RpcClientProvider(RpcClient defaultRpcClient, Map<String, RpcClient> serviceRpcClientMap) {
|
||||
Preconditions.checkNotNull(defaultRpcClient);
|
||||
log.info("RpcClientProvider, defaultRpcClient = {}, serviceRpcClientMap = {}", defaultRpcClient, serviceRpcClientMap);
|
||||
|
||||
this.defaultRpcClient = defaultRpcClient;
|
||||
this.serviceRpcClientMap = Optional.ofNullable(serviceRpcClientMap).orElseGet(Collections::emptyMap);
|
||||
}
|
||||
|
||||
public RpcClient getRpcClient(String appId, String appName) {
|
||||
// FIXME: 为了兼容仍使用appId作为RpcClientConfig中的bean名称的方式
|
||||
if (!serviceRpcClientMap.containsKey(appName)) {
|
||||
return serviceRpcClientMap.getOrDefault(appId, defaultRpcClient);
|
||||
}
|
||||
return serviceRpcClientMap.getOrDefault(appName, defaultRpcClient);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,219 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
import cn.axzo.foundation.caller.Caller;
|
||||
import cn.axzo.foundation.enums.AppEnvEnum;
|
||||
import cn.axzo.foundation.gateway.support.entity.GlobalContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.RequestContext;
|
||||
import cn.axzo.foundation.gateway.support.entity.Service;
|
||||
import cn.axzo.foundation.gateway.support.exception.ApiNotFoundException;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Strings;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.ListeningExecutorService;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.BooleanUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 提供后端服务的域名, appId等
|
||||
*/
|
||||
@Slf4j
|
||||
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class ServiceResolver {
|
||||
|
||||
// 默认10分钟
|
||||
private static final Long REFRESH_INTERVAL_MILLIS = TimeUnit.MINUTES.toMillis(10);
|
||||
|
||||
private GlobalContext globalContext;
|
||||
|
||||
private RpcClientProvider rpcClientProvider;
|
||||
|
||||
/**
|
||||
* 缓存app列表, key: appName, value: Service
|
||||
*/
|
||||
private LoadingCache<AppEnvEnum, Map<String, Service>> appCache;
|
||||
|
||||
private ListeningExecutorService backgroundRefreshPools;
|
||||
|
||||
public ServiceResolver(GlobalContext globalContext) {
|
||||
Preconditions.checkNotNull(globalContext);
|
||||
this.globalContext = globalContext;
|
||||
this.rpcClientProvider = globalContext.getRpcClientProvider();
|
||||
|
||||
backgroundRefreshPools = MoreExecutors.listeningDecorator(globalContext.getExecutor());
|
||||
|
||||
appCache = CacheBuilder
|
||||
.newBuilder()
|
||||
.maximumSize(10)
|
||||
.refreshAfterWrite(REFRESH_INTERVAL_MILLIS, TimeUnit.MILLISECONDS)
|
||||
.build(new CacheLoader<AppEnvEnum, Map<String, Service>>() {
|
||||
@Override
|
||||
public Map<String, Service> load(AppEnvEnum appEnv) {
|
||||
return loadApps(appEnv, ImmutableMap.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Map<String, Service>> reload(AppEnvEnum appEnv, Map<String, Service> old) {
|
||||
return backgroundRefreshPools.submit(() -> loadApps(appEnv, old));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制一个ServiceResolver, 新的resolver使用指定的rpcClientProvider
|
||||
*
|
||||
* @param rpcClientProvider
|
||||
* @return
|
||||
*/
|
||||
public ServiceResolver clone(RpcClientProvider rpcClientProvider) {
|
||||
Preconditions.checkNotNull(rpcClientProvider);
|
||||
return new ServiceResolver(this.globalContext, rpcClientProvider, this.appCache, this.backgroundRefreshPools);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取后端服务的Host.
|
||||
*
|
||||
* @param context 当前请求的上下文信息
|
||||
* @param serviceCode 服务编码, 即AppCenter中的appName
|
||||
* @return
|
||||
*/
|
||||
public String getHost(RequestContext context, String serviceCode) {
|
||||
Preconditions.checkNotNull(context);
|
||||
// serviceCode可以为空, 为空时通过解析requestURI来获取
|
||||
serviceCode = !Strings.isNullOrEmpty(serviceCode) ? serviceCode : context.getServiceCode();
|
||||
|
||||
// 网关配置中有host则使用代理配置中的host
|
||||
String host = fromGlobalContext(context, serviceCode);
|
||||
if (!Strings.isNullOrEmpty(host)) {
|
||||
return host;
|
||||
}
|
||||
|
||||
// AppCenter中配置了服务及host则使用AppCenter中配置的host
|
||||
host = getHost(context.getAuthCaller().map(Caller::getAppEnv).orElse(globalContext.getGateEnv()), serviceCode);
|
||||
if (!Strings.isNullOrEmpty(host)) {
|
||||
return host;
|
||||
}
|
||||
|
||||
if (BooleanUtils.isTrue(globalContext.getSupportUnknownApp())) {
|
||||
// 自动推导
|
||||
Profile profile = Profile.getByEnv(globalContext.getGateEnv());
|
||||
return profile.formatDomain(serviceCode);
|
||||
}
|
||||
|
||||
if (getRpcClient(context, serviceCode) == rpcClientProvider.getDefaultRpcClient()) {
|
||||
// 默认的rpc client不允许没有host
|
||||
String message = String.format("请求路径不存在. host: %s, requestURI: %s", context.getHost(), context.getRequestURI());
|
||||
throw new ApiNotFoundException(message);
|
||||
}
|
||||
|
||||
// 引入ServiceRpcClient后,不同的rpc client实现会自行去获取host,或者根本就不使用host(如通过stein的dependency时)
|
||||
// 因此找不到host时允许返回一个空的host
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
|
||||
public String getHost(AppEnvEnum appEnv, String serviceCode) {
|
||||
return Optional.ofNullable(appCache.getUnchecked(appEnv))
|
||||
.map(m -> m.get(serviceCode))
|
||||
.flatMap(s -> Optional.ofNullable(s.getHostSupplier()).map(Supplier::get))
|
||||
.orElse(StringUtils.EMPTY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访问后端服务的RpcClient.
|
||||
*
|
||||
* @param context 当前请求的上下文信息
|
||||
* @param serviceCode 服务编码, 即AppCenter中的appName
|
||||
* @return
|
||||
*/
|
||||
public RpcClient getRpcClient(RequestContext context, String serviceCode) {
|
||||
Preconditions.checkNotNull(context);
|
||||
// serviceCode可以为空, 为空时通过解析requestURI来获取
|
||||
String code = !Strings.isNullOrEmpty(serviceCode) ? serviceCode : context.getServiceCode();
|
||||
AppEnvEnum env = context.getAuthCaller().map(Caller::getAppEnv).orElse(globalContext.getGateEnv());
|
||||
String appId = Optional.ofNullable(appCache.getUnchecked(env))
|
||||
.map(m -> m.get(code)).map(Service::getAppId).orElse(StringUtils.EMPTY);
|
||||
|
||||
return rpcClientProvider.getRpcClient(appId, code);
|
||||
}
|
||||
|
||||
public RpcClient getRpcClient(String serviceCode) {
|
||||
String appId = Optional.ofNullable(appCache.getUnchecked(globalContext.getGateEnv()))
|
||||
.map(m -> m.get(serviceCode)).map(Service::getAppId).orElse(StringUtils.EMPTY);
|
||||
return rpcClientProvider.getRpcClient(appId, serviceCode);
|
||||
}
|
||||
|
||||
private String fromGlobalContext(RequestContext context, String serviceCode) {
|
||||
if (CollectionUtils.isEmpty(globalContext.getServices())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, String> services = Optional.ofNullable(globalContext.getServices())
|
||||
.orElse(Collections.emptyMap());
|
||||
String profileCode = context.getProfileCode();
|
||||
if (!Strings.isNullOrEmpty(profileCode)) {
|
||||
String host = services.get(serviceCode + RequestContext.PROFILE_CODE_DELIMITER + profileCode);
|
||||
if (!Strings.isNullOrEmpty(host)) {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
return services.get(serviceCode);
|
||||
}
|
||||
|
||||
private Map<String, Service> loadApps(AppEnvEnum appEnv, Map<String, Service> oldValues) {
|
||||
try {
|
||||
List<Service> services = globalContext.getServiceSupplier().apply(appEnv);
|
||||
return Maps.uniqueIndex(services, Service::getAppName);
|
||||
} catch (Exception e) {
|
||||
log.error("ServiceResolver拉取app异常, gateway-env={} request-env={}", globalContext.getGateEnv(), appEnv, e);
|
||||
globalContext.postAlert(" ServiceResolver拉取app异常, gateway-env={} request-env={}",
|
||||
globalContext.getGateEnv(), appEnv, e);
|
||||
return oldValues;
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Deprecated
|
||||
private enum Profile {
|
||||
|
||||
PRODUCT("https://%s.com.axzo/", AppEnvEnum.prd),
|
||||
|
||||
QA("https://%s.qa.com.axzo/", AppEnvEnum.test),
|
||||
|
||||
DEV("https://%s.dev.com.axzo/", AppEnvEnum.dev);
|
||||
|
||||
private final String domain;
|
||||
private final AppEnvEnum env;
|
||||
|
||||
String formatDomain(String serviceCode) {
|
||||
return String.format(domain, serviceCode);
|
||||
}
|
||||
|
||||
static Profile getByEnv(AppEnvEnum env) {
|
||||
return Stream.of(values())
|
||||
.filter(p -> p.getEnv() == env)
|
||||
.findFirst()
|
||||
.orElse(DEV);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,116 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
import cn.axzo.foundation.gateway.support.exception.ApiUnauthorizedException;
|
||||
import cn.axzo.foundation.web.support.rpc.RpcClient;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import com.google.common.cache.CacheLoader;
|
||||
import com.google.common.cache.LoadingCache;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 提供按租户过滤的查询条件
|
||||
*/
|
||||
@Slf4j
|
||||
public class TenantQueryProvider {
|
||||
private static final String TENANT_LIST_PATH = "/tenant/list";
|
||||
private static final String SUBORDINATE_LEVEL = "subordinate_level";
|
||||
|
||||
private RpcClient bfsUserRpcClient;
|
||||
|
||||
// 租户缓存
|
||||
private LoadingCache<String, Optional<Tenant>> tenantCache = CacheBuilder.newBuilder()
|
||||
// 30分钟刷新一次
|
||||
.expireAfterWrite(30, TimeUnit.MINUTES)
|
||||
.maximumSize(2000)
|
||||
.build(new CacheLoader<String, Optional<Tenant>>() {
|
||||
@Override
|
||||
public Optional<Tenant> load(String tenantId) throws Exception {
|
||||
return getTenantFromUserClient(tenantId);
|
||||
}
|
||||
});
|
||||
|
||||
// 租户缓存失效(用户服务不可用)时用于降级处理
|
||||
private Cache<String, Tenant> failoverCache = CacheBuilder.newBuilder().build();
|
||||
|
||||
@Builder
|
||||
public TenantQueryProvider(RpcClient bfsUserRpcClient) {
|
||||
Preconditions.checkArgument(bfsUserRpcClient != null, "bfsUserRpcClient不能为空");
|
||||
this.bfsUserRpcClient = bfsUserRpcClient;
|
||||
}
|
||||
|
||||
public Tenant getTenant(String tenantId) {
|
||||
Optional<Tenant> tenantOpt;
|
||||
try {
|
||||
tenantOpt = tenantCache.getUnchecked(tenantId);
|
||||
} catch (Exception e) {
|
||||
tenantOpt = Optional.ofNullable(failoverCache.getIfPresent(tenantId));
|
||||
}
|
||||
return tenantOpt.orElseThrow(() -> new ApiUnauthorizedException("租户不存在"));
|
||||
}
|
||||
|
||||
private Optional<Tenant> getTenantFromUserClient(String tenantId) {
|
||||
List<Tenant> tenants = bfsUserRpcClient.request()
|
||||
.url(TENANT_LIST_PATH)
|
||||
.content(new JSONObject().fluentPut("tenantId", tenantId))
|
||||
.clz(Tenant.class)
|
||||
.postAndGetList();
|
||||
Optional<Tenant> tenantOpt = tenants.stream().findFirst();
|
||||
tenantOpt.ifPresent(value -> failoverCache.put(tenantId, value));
|
||||
return tenantOpt;
|
||||
}
|
||||
|
||||
@Builder
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class Tenant {
|
||||
private String id;
|
||||
private String tenantId;
|
||||
private String tenantName;
|
||||
private Integer tenantType;
|
||||
private String contacts;
|
||||
private String telPhone;
|
||||
private String unifyCode;
|
||||
private Integer status;
|
||||
private String provinceCode;
|
||||
private String provinceName;
|
||||
private String cityCode;
|
||||
private String cityName;
|
||||
private String countyCode;
|
||||
private String countyName;
|
||||
private String address;
|
||||
private Double longitude;
|
||||
private Double latitude;
|
||||
private Boolean admin;
|
||||
private String courseNumber;
|
||||
private String secret;
|
||||
private String contractName;
|
||||
private String tenantContractUrl;
|
||||
private String taxIdentifyNo;
|
||||
private String baoFooAccount;
|
||||
private String email;
|
||||
private String parentTenantId;
|
||||
private String dataAuthLevel;
|
||||
private String identification;
|
||||
private Integer company;
|
||||
private Integer freshCompany;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
public boolean canAccessSubordinate() {
|
||||
return SUBORDINATE_LEVEL.equals(getDataAuthLevel());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.cache.Cache;
|
||||
import com.google.common.cache.CacheBuilder;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* 间隔一定时间刷新的缓存。
|
||||
* <pre>
|
||||
* 1、初次调用时,直接获取需要缓存的内容,并缓存到cache中
|
||||
* 2、每间隔intervalMillis尝试刷新缓存,
|
||||
* 如果刷新缓存成功,更新cache缓存的值
|
||||
* 如果刷新缓存失败,则不更新
|
||||
* </pre>
|
||||
*
|
||||
* @param <T>
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
@Slf4j
|
||||
public class TimerRefreshCache<T> {
|
||||
private ScheduledThreadPoolExecutor executor;
|
||||
private String name;
|
||||
private Long initialDelayMillis;
|
||||
private Long intervalMillis;
|
||||
private Supplier<T> refresher;
|
||||
private Cache<String, T> cache;
|
||||
|
||||
public TimerRefreshCache(String name, Long initialDelayMillis, Long intervalMillis,
|
||||
ScheduledThreadPoolExecutor executor, Supplier<T> refresher, T initData) {
|
||||
Preconditions.checkArgument(executor != null);
|
||||
Preconditions.checkArgument(refresher != null);
|
||||
Preconditions.checkArgument(intervalMillis != null);
|
||||
|
||||
this.name = name;
|
||||
this.initialDelayMillis = initialDelayMillis;
|
||||
this.intervalMillis = intervalMillis;
|
||||
this.refresher = refresher;
|
||||
// 该方法获取的,往往是完整的缓存内容。所以maxSize暂定为2即可
|
||||
long maximumSize = 2L;
|
||||
cache = CacheBuilder.newBuilder()
|
||||
.maximumSize(maximumSize)
|
||||
.build();
|
||||
if (initData != null) {
|
||||
cache.put(name, initData);
|
||||
}
|
||||
this.executor = executor;
|
||||
startTimer();
|
||||
}
|
||||
|
||||
private void startTimer() {
|
||||
executor.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
T value = refresher.get();
|
||||
// 当返回为null,表示Value没有变化,这时不用刷新Cache
|
||||
if (value == null) {
|
||||
log.debug("{} already has latest value.", name);
|
||||
return;
|
||||
}
|
||||
|
||||
cache.put(name, value);
|
||||
log.info("{} refreshed, new value={}", name, value);
|
||||
} catch (Exception e) {
|
||||
log.error("TimerRefreshCache get fail.name={}", name, e);
|
||||
}
|
||||
}, initialDelayMillis, intervalMillis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public T get() {
|
||||
return cache.getIfPresent(name);
|
||||
}
|
||||
|
||||
|
||||
// XXX: for unittest
|
||||
public void put(T configs) {
|
||||
log.info("{},set->config={}", TimerRefreshCache.this, configs);
|
||||
cache.put(name, configs);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package cn.axzo.foundation.gateway.support.utils;
|
||||
|
||||
import com.google.common.base.Preconditions;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 适用于BizGate的URL路径拼接工具类.
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class UrlPathHelper {
|
||||
|
||||
public static final String URI_SEPARATOR = "/";
|
||||
|
||||
/**
|
||||
* 将host和servicePath拼接成URL
|
||||
*/
|
||||
public static String join(String host, String servicePath) {
|
||||
Preconditions.checkNotNull(host);
|
||||
Preconditions.checkNotNull(servicePath);
|
||||
if (host.endsWith(URI_SEPARATOR) || servicePath.startsWith(URI_SEPARATOR)) {
|
||||
return host + servicePath;
|
||||
} else {
|
||||
return host + URI_SEPARATOR + servicePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user