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引數請求錯誤
首先打上在類的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閘道器選擇器和規則的路徑設定存在高度的關聯性,字首可以說必須進行繼承
瞭解了匹配的一些細節,有助於寫匹配