1. 程式人生 > 實用技巧 >Soul閘道器原始碼閱讀(八)路由匹配初探

Soul閘道器原始碼閱讀(八)路由匹配初探

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 = /more
realURL = /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

掘金