写在前面
在前面的文章里,我们已经在 soul-bootstrap 网关里拿到了需要代理的接口数据,并且会存储到内存中。再加上 web socket 做数据同步的功能,能保证接口的变化会更新到 soul-bootstrap 网关上。
但是我们真正使用的时候是直接把接口请求发送给 soul-bootstrap 网关的,并没有直接去请求真实的服务。
那么今天我们就一起来看看 soul-bootstrap 是怎么代理接口的。
soul-bootstrap 代理接口的过程
这个项目用了 Spring Boot 的 WebFlux,跟一般的 Spring Mvc 请求过程有些区别。
引入了 soul-web 模块,这个模块会从加载「插件」开始。
public class SoulConfiguration {
@Bean("webHandler")
public SoulWebHandler soulWebHandler(final ObjectProvider<List<SoulPlugin>> plugins) {
List<SoulPlugin> pluginList = plugins.getIfAvailable(Collections::emptyList);
final List<SoulPlugin> soulPlugins = pluginList.stream()
.sorted(Comparator.comparingInt(SoulPlugin::getOrder)).collect(Collectors.toList());
soulPlugins.forEach(soulPlugin -> log.info("load plugin:[{}] [{}]", soulPlugin.named(), soulPlugin.getClass().getName()));
return new SoulWebHandler(soulPlugins);
}
}
然后会每当有请求发送过来的时候,就会去从插件里面去匹配一个来处理,http 接口的处理插件是 Divide,所以我们只关注 Divide 插件就行了。
我们先猜测下这个 Divide 插件处理的过程吧。
- 从缓存里拿到「选择器」数据
- 遍历「选择器」数据匹配请求的 uri 里面的 contextPath
- 匹配成功之后从缓存里拿到「规则」数据
- 遍历「规则」数据匹配 uri 里面的接口地址
- 从缓存里拿到真实接口服务的地址,发送新的请求
- 拿到返回值
大概的逻辑就是这样,但是中间过程会比这个复杂的多。
举个例子,「选择器」和「规则」匹配数据的时候,还会有各自的「匹配条件」。
什么是「匹配条件」呢?比如根据请求的方式(get 还是 post 或者 option 等)去匹配;比如根据携带的参数去匹配,参数 xxx == xxx,还是参数 xxx != xxx 等等。
还有,对真实服务的请求也会有很多的其他处理,比如同一个接口有多个服务,那么当代理这个接口的时候,最终请求到哪一个服务也是可以用网关控制的。也就是所谓的负载均衡,针对每个服务设置权重,控制对每个服务请求的压力。
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);
// 省略判断空的逻辑 ...
// 匹配选择器
final SelectorData selectorData = matchSelector(exchange, selectors);
// 省略判断空的逻辑 日志输出...
// 从缓存拿到规则数据
final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
// 匹配规则 省略部分代码 ...
RuleData rule;
// 省略判断空逻辑 日志输出 ...
return doExecute(exchange, chain, selectorData, rule);
}
return chain.execute(exchange);
}
}
在 AbstractSoulPlugin 这个通用的插件里面就做了上面我们分析的前 3 个步骤,剩下的步骤在哪呢?
就在每个具体的插件处理类里面,这些插件都实现了上面那个类的 doExecute 方法,我们还是只关注 Divide 插件继续看下去:
public class DividePlugin extends AbstractSoulPlugin {
@Override
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
// 加载上游的服务数据 也就是真实的服务数据
final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
// 负载均衡处理
final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
// 构造真实的服务地址
String domain = buildDomain(divideUpstream);
String realURL = buildRealURL(domain, soulContext, exchange);
exchange.getAttributes().put(Constants.HTTP_URL, realURL);
return chain.execute(exchange);
}
}
这个类里就完成了上面分析的第 4 第 5 两个步骤。最后一个步骤在 WebClientPlugin 类里面,这个类会在最后被执行,主要功能就是向真实的服务发起请求拿到响应。
public class WebClientPlugin implements SoulPlugin {
@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
// 拿到请求方法 url 等参数,发送请求处理返回值
HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);
return handleRequestBody(requestBodySpec, exchange, timeout, retryTimes, chain);
}
}
到了这里,一个完整的网关代理接口的过程就完成了。虽然过程有些简陋,但是核心的过程是完整的。
总结
我在写这篇文章的难点在于对 WebFlux 不熟悉,加上用了非 Spring Mvc 的接口处理过程,而是使用了 React Netty 的 WebHandler 去处理,一开始被这个绕晕了头。
后来尝试去忽略掉一些过程,通过调试和猜测去寻找整个流程,从中慢慢找到大概的流程之后,整体的逻辑也就满清晰了。
尽管还是对 WebFlux 一窍不通,但是并不影响整个流程的梳理。