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 {......@Overridepublic 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 {@Overridepublic SoulContext build(final ServerWebExchange exchange) {final ServerHttpRequest request = exchange.getRequest();// path = /http/order/findByIdString 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/findByIdString 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, findByIdString[] splitList = StringUtils.split(path, "/");if (splitList.length != 0) {// 这个应该是前缀的意思,并且只取第一个,值是:/httpcontextPath = contextPath.concat(splitList[0]);}// 取后面的字符串,得到:/order/findByIdString 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 = /more
realURL = /prefix/baidu

     下面我继续看下,DividePlugin的匹配详情,首先打入断点在 AbstractSoulPlugin,执行匹配逻辑

public abstract class AbstractSoulPlugin implements SoulPlugin {......@Overridepublic 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 lastrule = 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策略类,进行匹配;继续跟进matchString 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 {@Overridepublic 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 {@Overridepublic 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/findByIdreturn Splitter.on(",").omitEmptyStrings().trimResults().splitToList(matchUrls).stream().anyMatch(url -> reg(url, path));}}

     选择器的匹配大致就是这些,可以但到进行匹配,其中的过程还挺复杂的,隐约能感受到一点设计的思想,有点逐步拆分的感觉。这块具体的分析,后面有时间再看看

     选择器匹配上以后,就进行到规则的匹配了,规则的匹配和选择器的匹配都是使用的这个匹配策略类进行匹配的,就是换行匹配的字符串罢了,这里就不详述了

     需要注意的一点是,规则匹配是使用请求的完整路径和规则的完整路径进行匹配的,没有截取之类的,也就是选择器和规则的路径设置存在高度的关联性,前缀可以说必须进行继承,这样感觉可能会导致一些灵活性的丧失

     继续来看 DividePlugin 插件,在下面的注释中可以看到 domain + readUrl 组合成了针对后端服务请求的url

public class DividePlugin extends AbstractSoulPlugin {@Overrideprotected 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:8188String domain = buildDomain(divideUpstream);// get real url : http://192.168.101.104:8188/order/findById?id=1111String 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

  • Soul源码阅读(一) 概览

  • Soul源码阅读(二)代码初步运行

  • Soul源码阅读(三)HTTP请求处理概览

  • Soul网关源码阅读(四)Dubbo请求概览

  • Soul网关源码阅读(五)请求类型探索

  • Soul网关源码阅读(六)Sofa请求处理概览

  • Soul网关源码阅读(七)限流插件初探

  • Soul网关源码阅读番外篇(一) HTTP参数请求错误

掘金

  • Soul网关源码阅读(一) 概览

  • Soul网关源码阅读(二)代码初步运行

  • Soul网关源码阅读(三)请求处理概览

  • Soul网关源码阅读(四)Dubbo请求概览

  • Soul网关源码阅读(五)请求类型探索

  • Soul网关源码阅读(六)Sofa请求处理概览

  • Soul网关源码阅读(七)限流插件初探

  • Soul网关源码阅读番外篇(一) HTTP参数请求错误

Soul网关源码阅读(八)路由匹配初探相关推荐

  1. Soul网关源码阅读(十)自定义简单插件编写

    Soul网关源码阅读(十)自定义简单插件编写 简介     综合前面所分析的插件处理流程相关知识,此次我们来编写自定义的插件:统计请求在插件链中的经历时长 编写准备     首先我们先探究一下,一个P ...

  2. Soul网关源码阅读(九)插件配置加载初探

    Soul网关源码阅读(九)插件配置加载初探 简介     今日来探索一下插件的初始化,及相关的配置的加载 源码Debug 插件初始化     首先来到我们非常熟悉的插件链调用的类: SoulWebHa ...

  3. Soul网关源码阅读(七)限流插件初探

    Soul网关源码阅读(七)限流插件初探 简介     前面的文章中对处理流程探索的差不多了,今天来探索下限流插件:resilience4j 示例运行 环境配置     启动下MySQL和redis d ...

  4. Soul 网关源码阅读(六)Sofa请求处理概览

    Soul 网关源码阅读(六)Sofa请求处理概览 简介     今天来探索一下Sofa请求处理流程,看看和前面的HTTP.Dubbo有什么异同 Sofa示例运行 PS:如果请求加上参数运行不成功,请更 ...

  5. Soul网关源码阅读番外篇(一) HTTP参数请求错误

    Soul网关源码阅读番外篇(一) HTTP参数请求错误 共同作者:石立 萧 * 简介     在Soul网关2.2.1版本源码阅读中,遇到了HTTP请求加上参数返回404的错误,此篇文章基于此进行探索 ...

  6. Soul网关源码阅读(六)请求类型探索

    Soul网关源码阅读(六)请求类型探索 简介     在上几篇文章中分析了请求的处理流程,HTTP和RPC请求处理是互斥的,通过请求类型来判断,这篇文章来探索下请求类型的前世今生 源码分析     通 ...

  7. Soul 网关源码阅读(四)Dubbo请求概览

    Soul 网关源码阅读(四)Dubbo请求概览 简介     本次启动一个dubbo服务示例,初步探索Soul网关源码的Dubbo请求处理流程 示例运行 环境配置     在Soul源码clone下来 ...

  8. Soul 网关源码阅读(三)请求处理概览

    Soul 源码阅读(三)请求处理概览 简介     基于上篇:Soul 源码阅读(二)代码初步运行的配置,这次debug下请求处理的大致路径,验证网关模型的路径 详细流程记录 查看运行日志,寻找切入点 ...

  9. Soul 网关源码阅读(一) 概览

    Soul 源码阅读(一) 概览 简介     阅读soul的官方文档,大致了解soul的功能和相关概念 心得     需要对网关的功能有个大致的了解,把soul官方文档读两遍(第一遍通读,能看懂多少是 ...

最新文章

  1. 想学习测试人必看的5本好书,没看过你就吃亏啦
  2. 利用cmake查看OpenCV的源码的方法
  3. html table相关标签和属性
  4. Knative 实战:基于 Knative Serverless 技术实现天气服务-下篇
  5. list redis 怎样做排行_redis实现商品销量排行榜
  6. 【Day12】整个前端性能提升大致分几类
  7. Arbitrage(判断正环 spfa写法)
  8. python跟php服务器对比_python学习笔记一和PHP的一些对比
  9. 聚合(Aggregation)和组合(Composition)的区别
  10. java复制数组函数_java 数组复制:System.arrayCopy 深入解析
  11. CAN、CAN FD
  12. 对区块链撒谎:将“垃圾进,垃圾出”问题应用在去中心化网络上
  13. 对.Net 垃圾回收的C#编程相关方面(Finalize 和Dispose(bool disposing)和 Dispose())的一些理解体会(转)...
  14. 谷歌地球Google Earth打不开的解决办法
  15. Admob的PIN码接收教程
  16. Verilog(2):与或非运算
  17. mysql退出安全模式_MySQL数据库之mysql 解除安全模式
  18. 计算机考试题库access,计算机二级access真题题库试题精选(附答案)-20210623194307.docx-原创力文档...
  19. 软件测试资质有哪些?具备CNAS资质和具备CMA资质的区别是什么?
  20. 云服务器环境安装、卸载与配置:mysql

热门文章

  1. [转]httpclient 上传文件、下载文件
  2. java中的char类型所占空间
  3. JAVA的嵌入式脚本开发(上)
  4. 创业的一些挫见之第二家公司失败记录
  5. WinForm 中自定义文件与自己的应用程序相关联
  6. Ubuntu 11.10 make menuconfig 失败的解决方法
  7. 【报告分享】2022中国职业教育行业报告-多鲸资本.pdf(附下载链接)
  8. 【干货】美团大脑系列之商品知识图谱的构建及应用.pdf(附下载链接)
  9. 【白皮书分享】2020用户生命周期运营白皮书2.0.pdf(附下载链接)
  10. 推荐系统的公平性:用户侧与生产侧