Soul 网关源码阅读(八)路由匹配初探
简介
今日看看路由的匹配相关代码,查看 HTTP 的 DividePlugin 匹配
示例运行
使用 HTTP 的示例,运行 Soul-Admin,Soul-Bootstrap,Soul-Example-HTTP
记得启动数据库
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:latest
复制代码
其他的就不再赘述了,有问题可以参照前面的文章,看看有没有啥借鉴的
源码 Debug
在番外篇:Soul网关源码阅读番外篇(一) HTTP参数请求错误,我们知道了 GlobalPlugin 的重要性,其会将请求对应的真实是后台服务器路径写入 Exchange 中,我们先来摸一摸其具体细节:
首先打上在类的 execute 中打上断点,访问:http://127.0.0.1:9195/http/order/findById?id=1111
进入断点后,继续跟入
public class GlobalPlugin implements SoulPlugin { ...... @Override public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) { final ServerHttpRequest request = exchange.getRequest(); final HttpHeaders headers = request.getHeaders(); final String upgrade = headers.getFirst("Upgrade"); SoulContext soulContext; if (StringUtils.isBlank(upgrade) || !"websocket".equals(upgrade)) { // 进入build函数,进行操作 soulContext = builder.build(exchange); } else { final MultiValueMap<String, String> queryParams = request.getQueryParams(); soulContext = transformMap(queryParams); } exchange.getAttributes().put(Constants.CONTEXT, soulContext); return chain.execute(exchange); } ......}
复制代码
跟进 build 里面去,里面首先获取了路径,进行请求类型判断,没有元数据则走到了默认的 HTTP
public class DefaultSoulContextBuilder implements SoulContextBuilder { @Override public SoulContext build(final ServerWebExchange exchange) { final ServerHttpRequest request = exchange.getRequest(); // path = /http/order/findById String path = request.getURI().getPath(); MetaData metaData = MetaDataCache.getInstance().obtain(path); if (Objects.nonNull(metaData) && metaData.getEnabled()) { exchange.getAttributes().put(Constants.META_DATA, metaData); } // 进入 transform 函数 return transform(request, metaData); } private SoulContext transform(final ServerHttpRequest request, final MetaData metaData) { final String appKey = request.getHeaders().getFirst(Constants.APP_KEY); final String sign = request.getHeaders().getFirst(Constants.SIGN); final String timestamp = request.getHeaders().getFirst(Constants.TIMESTAMP); SoulContext soulContext = new SoulContext(); // path = /http/order/findById String path = request.getURI().getPath(); soulContext.setPath(path); if (Objects.nonNull(metaData) && metaData.getEnabled()) { if (RpcTypeEnum.SPRING_CLOUD.getName().equals(metaData.getRpcType())) { setSoulContextByHttp(soulContext, path); soulContext.setRpcType(metaData.getRpcType()); } else if (RpcTypeEnum.DUBBO.getName().equals(metaData.getRpcType())) { setSoulContextByDubbo(soulContext, metaData); } else if (RpcTypeEnum.SOFA.getName().equals(metaData.getRpcType())) { setSoulContextBySofa(soulContext, metaData); } else if (RpcTypeEnum.TARS.getName().equals(metaData.getRpcType())) { setSoulContextByTars(soulContext, metaData); } else { setSoulContextByHttp(soulContext, path); soulContext.setRpcType(RpcTypeEnum.HTTP.getName()); } } else { // 来打这,进行HTTP设置 setSoulContextByHttp(soulContext, path); soulContext.setRpcType(RpcTypeEnum.HTTP.getName()); } soulContext.setAppKey(appKey); soulContext.setSign(sign); soulContext.setTimestamp(timestamp); soulContext.setStartDateTime(LocalDateTime.now()); Optional.ofNullable(request.getMethod()).ifPresent(httpMethod -> soulContext.setHttpMethod(httpMethod.name())); return soulContext; } private void setSoulContextByHttp(final SoulContext soulContext, final String path) { String contextPath = "/"; // 是一个列表,值是:http, order, findById String[] splitList = StringUtils.split(path, "/"); if (splitList.length != 0) { // 这个应该是前缀的意思,并且只取第一个,值是:/http contextPath = contextPath.concat(splitList[0]); } // 取后面的字符串,得到:/order/findById String realUrl = path.substring(contextPath.length()); soulContext.setContextPath(contextPath); soulContext.setModule(contextPath); soulContext.setMethod(realUrl); soulContext.setRealUrl(realUrl); }}
复制代码
在最后一个函数中,我们看到了具体设置 realURL 的代码,其大致思路,如上面代码总描述的一样
这里就有个小疑问,前缀也就是只能是 /xxx 之类的,如果是 /xxx/xxx 那请求后面是否会出问题
我们做了一个小实验,设置一个选择器为条件为:/more/prefix,一个规则为:/more/prefix/baidu,都是相等条件
下面 Debug 来看下 GlobalPlugin 的解析结果,如下,明显不是我们想要的,所有这里初步猜测不能选择器不能使用两级前缀,不然可能会出问题
contextPath = /morerealURL = /prefix/baidu
复制代码
下面我继续看下,DividePlugin 的匹配详情,首先打入断点在 AbstractSoulPlugin,执行匹配逻辑
public abstract class AbstractSoulPlugin implements SoulPlugin { ...... @Override public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) { String pluginName = named(); final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName); if (pluginData != null && pluginData.getEnabled()) { final Collection<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName); if (CollectionUtils.isEmpty(selectors)) { return handleSelectorIsNull(pluginName, exchange, chain); } // use /http/order/findById // 这里首先进行选择器的匹配,我们看下选择器如果的匹配细节 final SelectorData selectorData = matchSelector(exchange, selectors); if (Objects.isNull(selectorData)) { return handleSelectorIsNull(pluginName, exchange, chain); } selectorLog(selectorData, pluginName); final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId()); if (CollectionUtils.isEmpty(rules)) { return handleRuleIsNull(pluginName, exchange, chain); } RuleData rule; if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) { //get last rule = rules.get(rules.size() - 1); } else { rule = matchRule(exchange, rules); } if (Objects.isNull(rule)) { return handleRuleIsNull(pluginName, exchange, chain); } ruleLog(rule, pluginName); return doExecute(exchange, chain, selectorData, rule); } return chain.execute(exchange); }
private SelectorData matchSelector(final ServerWebExchange exchange, final Collection<SelectorData> selectors) { // 循环每个选择器,看是否能匹配得上,findFirst的意思是否多个匹配上就要第一个? return selectors.stream() .filter(selector -> selector.getEnabled() && filterSelector(selector, exchange)) .findFirst().orElse(null); }
private Boolean filterSelector(final SelectorData selector, final ServerWebExchange exchange) { if (selector.getType() == SelectorTypeEnum.CUSTOM_FLOW.getCode()) { if (CollectionUtils.isEmpty(selector.getConditionList())) { return false; } // 使用匹配策略工具进行匹配,我们进行跟下去 return MatchStrategyUtils.match(selector.getMatchMode(), selector.getConditionList(), exchange); } return true; }
private RuleData matchRule(final ServerWebExchange exchange, final Collection<RuleData> rules) { return rules.stream() .filter(rule -> filterRule(rule, exchange)) .findFirst().orElse(null); }
private Boolean filterRule(final RuleData ruleData, final ServerWebExchange exchange) { return ruleData.getEnabled() && MatchStrategyUtils.match(ruleData.getMatchMode(), ruleData.getConditionDataList(), exchange); } ......}
复制代码
继续跟到匹配策略工具的类中,它有 and 和 or 的匹配策略,判断策略,构造相关策略类后进行调用
public class MatchStrategyUtils {
public static boolean match(final Integer strategy, final List<ConditionData> conditionDataList, final ServerWebExchange exchange) { // and 策略,构造and策略类,进行匹配;继续跟进match String matchMode = MatchModeEnum.getMatchModeByCode(strategy); MatchStrategy matchStrategy = ExtensionLoader.getExtensionLoader(MatchStrategy.class).getJoin(matchMode); return matchStrategy.match(conditionDataList, exchange); }}
复制代码
进行跟到 judge 函数中
public class AndMatchStrategy extends AbstractMatchStrategy implements MatchStrategy {
@Override public Boolean match(final List<ConditionData> conditionDataList, final ServerWebExchange exchange) { return conditionDataList .stream() .allMatch(condition -> OperatorJudgeFactory.judge(condition, buildRealData(condition, exchange))); }}
复制代码
再根据 judge,有点复杂感觉......
public class OperatorJudgeFactory {
public static Boolean judge(final ConditionData conditionData, final String realData) { if (Objects.isNull(conditionData) || StringUtils.isBlank(realData)) { return false; } return OPERATOR_JUDGE_MAP.get(conditionData.getOperator()).judge(conditionData, realData); }}
复制代码
一层又一层,继续跟进 match 函数中
public class MatchOperatorJudge implements OperatorJudge {
@Override public Boolean judge(final ConditionData conditionData, final String realData) { if (Objects.equals(ParamTypeEnum.URI.getName(), conditionData.getParamType())) { return PathMatchUtils.match(conditionData.getParamValue().trim(), realData); } return realData.contains(conditionData.getParamValue().trim()); }}
复制代码
在这终于看到了具体的逻辑实现了,大致可以看出这是个字符串匹配
public class PathMatchUtils {
private static final AntPathMatcher MATCHER = new AntPathMatcher();
public static boolean match(final String matchUrls, final String path) { // matchUrls = /http/** , path = /http/order/findById return Splitter.on(",").omitEmptyStrings().trimResults().splitToList(matchUrls).stream().anyMatch(url -> reg(url, path)); }
}
复制代码
选择器的匹配大致就是这些,可以但到进行匹配,其中的过程还挺复杂的,隐约能感受到一点设计的思想,有点逐步拆分的感觉。这块具体的分析,后面有时间再看看
选择器匹配上以后,就进行到规则的匹配了,规则的匹配和选择器的匹配都是使用的这个匹配策略类进行匹配的,就是换行匹配的字符串罢了,这里就不详述了
需要注意的一点是,规则匹配是使用请求的完整路径和规则的完整路径进行匹配的,没有截取之类的,也就是选择器和规则的路径设置存在高度的关联性,前缀可以说必须进行继承,这样感觉可能会导致一些灵活性的丧失
继续来看 DividePlugin 插件,在下面的注释中可以看到 domain + readUrl 组合成了针对后端服务请求的 url
public class DividePlugin extends AbstractSoulPlugin {
@Override protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) { final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT); assert soulContext != null; final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class); final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId()); if (CollectionUtils.isEmpty(upstreamList)) { log.error("divide upstream configuration error: {}", rule.toString()); Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null); return WebFluxResultUtils.result(exchange, error); } final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress(); DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip); if (Objects.isNull(divideUpstream)) { log.error("divide has no upstream"); Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null); return WebFluxResultUtils.result(exchange, error); } // set the http url : http://192.168.101.104:8188 String domain = buildDomain(divideUpstream); // get real url : http://192.168.101.104:8188/order/findById?id=1111 String realURL = buildRealURL(domain, soulContext, exchange); exchange.getAttributes().put(Constants.HTTP_URL, realURL); exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout()); exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry()); return chain.execute(exchange); }}
复制代码
不知道是不是平时使用的是 NGINX,所有感觉 Soul 网关的转发好像不是那么灵活
比如我们配置: http://test/baidu ,转发到百度后端服务器: http://baidu.com
如果我们按照两级来配置的话,那真实的 url 就会变成 http://baidu.com/baidu
使用一级前缀配置能达到目的,都使用 match,选择器配置一级前缀,规则配置 /** ,这样前缀为 test 的请求都会转到百度
上面转发成功还是因为 规则: /** 能匹配 /test/ ,感觉没有 NGINX 类似的截取之类
总结
通过分析 Soul 的匹配算法,对如果写配置有了更深的了解,下面两点是需要注意的:
1.Soul 网关只支持一级前缀,因为在设置 RealURL 的时候,分隔字符串后定时取取 str[0]为前缀
2.Soul 网关选择器和规则的路径设置存在高度的关联性,前缀可以说必须进行继承
了解了匹配的一些细节,有助于写匹配
Soul 网关源码分析文章列表
Github
掘金
评论