feat: add 网关

This commit is contained in:
zengxiaobo 2024-04-25 09:38:30 +08:00
parent fa4c571f6e
commit bb9555a086
45 changed files with 5442 additions and 0 deletions

View File

@ -37,6 +37,12 @@
<artifactId>commons-collections4</artifactId> <artifactId>commons-collections4</artifactId>
</dependency> </dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<version>2.9.0</version>
</dependency>
<dependency> <dependency>
<groupId>org.slf4j</groupId> <groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId> <artifactId>slf4j-api</artifactId>

View File

@ -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;
}

View File

@ -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));
}
}
}

View File

@ -17,4 +17,34 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </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> </project>

View File

@ -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();
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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));
}
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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) {
}
}

View File

@ -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));
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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()));
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}
}

View File

@ -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&#8482; 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));
}
}

View File

@ -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不支持IndexedMappedCombined
* Extensions:
* '*' 表示将包含没有在规则中出现的字段及其对应的值
* '__validate' 表示将校验规则中出现的字段不存在则报错
*/
public static final String PROXY_PARAM_FILTER_REGEX = "^(\\w+(\\.\\w+)*):(\\w+(\\.\\w+)*)|(\\w+(\\.\\w+)*)|\\*$";
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}
}