文章目录
概要
上一篇我们介绍了如何使用WebFlux快速的搭建一个请求转发的demo,了解一些基本的概念。这一篇我们来分析soul网关的设计思路,了解soul网关是如何做到将多个插件加载到webflux并进行请求转发的。我们以最简单的divide插件来进行http请求的分析。
我们从以下流程分析整个调用链。
-
一切的开始——SoulConfiguration
-
请求的核心处理入口——SoulWebHandler
-
Http请求插件——WebClientPlugin
-
Http响应处理插件——WebClientResponsePlugin
基础知识
开始之前需要了解,SpringWebFlux的工作原理,不然会很懵逼。
参考:
Soul网关源码阅读(六)—— Soul网关之WebFlux
Soul网关http转发流程分析
SoulConfiguration
以上我们分析了如何将divide插件配置到我们的spring IOC容器,那么我们的soul网关又是如何加载他们到我们的请求处理链中的呢?
我们启动bootstrap服务,控制台打印了以下日志:
可以看到通过SoulConfiguration
配置了类加载了很多不同的插件到Spring服务。
我们的分析就从SoulConfiguration
开始,查看以下代码。
/**
* Init SoulWebHandler.
*
* @param plugins this plugins is All impl SoulPlugin.
* @return {@linkplain SoulWebHandler}
*/
@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);
}
可以看到这里从上下文中将所有插件对象的集合plugins作为参数参入了SoulWebHandler,使SoulWebHandler具有处理所有插件的能力。
上面分析到SoulConfiguration
配置文件将插件集合plugins配置到了SoulWebHandler
,那么可以断定SoulWebHandler具备了我们网关所有插件的业务处理能力。
SoulWebHandler
查看SoulWebHandler的构造方法:
public final class SoulWebHandler implements WebHandler {
private final List<SoulPlugin> plugins;
private final Scheduler scheduler;
/**
* Instantiates a new Soul web handler.
*
* @param plugins the plugins
*/
public SoulWebHandler(final List<SoulPlugin> plugins) {
this.plugins = plugins;
String schedulerType = System.getProperty("soul.scheduler.type", "fixed");
if (Objects.equals(schedulerType, "fixed")) {
int threads = Integer.parseInt(System.getProperty(
"soul.work.threads", "" + Math.max((Runtime.getRuntime().availableProcessors() << 1) + 1, 16)));
scheduler = Schedulers.newParallel("soul-work-threads", threads);
} else {
scheduler = Schedulers.elastic();
}
}
//...
}
可以看到SoulWebHandler集成了WebHandler,该类的作用就是处理WebFlux的请求的(回想以下我们上一篇中的GreetingWebHandler,这里两者是同一个东西)。从构造函数式中我们可以看到这个handler持有了一个SoulPlugin类型的集合,并且初始化了一个Scheduler类型的成员变量。
Scheduler
Reactor的Scheduler调度器相当于Java中的ExecutorService,不同的调度器定义不同的线程执行环境。Schedulers
工具类提供的静态方法可搭建不同的线程执行环境。
Schedulers
类已经预先创建了几种常用的不同线程池模型的调度器:使用single()
、elastic()
和parallel()
方法创建的调度器可以分别使用内置的单线程、弹性线程池和固定大小线程池。如果想创建新的调度器,可以使用newSingle()
、newElastic()
和newParallel()
方法。这些方法都是返回一个Scheduler
的具体实现。
这里根据配置属性"fix",选择创建了一个固定大小的调度器,线程数默认为Cpu个数*2+1,最小为16个;或者是弹性调度器。
handle方法
@Override
public Mono<Void> handle(@NonNull final ServerWebExchange exchange) {
MetricsTrackerFacade.getInstance().counterInc(MetricsLabelEnum.REQUEST_TOTAL.getName());
Optional<HistogramMetricsTrackerDelegate> startTimer = MetricsTrackerFacade.getInstance().histogramStartTimer(MetricsLabelEnum.REQUEST_LATENCY.getName());
return new DefaultSoulPluginChain(plugins).execute(exchange).subscribeOn(scheduler)
.doOnSuccess(t -> startTimer.ifPresent(time -> MetricsTrackerFacade.getInstance().histogramObserveDuration(time)));
}
从构造方法我们知道了,SoulWebHandler具备了处理所有插件逻辑的能力,整个Soul网关只配置了这么一个WebHandler,也就是说这个handler对应的handle方法是soul网关处理所有请求的地方。
这么做的好处很明显,我们只需要配置插件到Spring上下文,然后WebFlux去上下文获取就行了,扩展起来比较方便,而不是我们为每一个插件都配置一个WebHandler。
查看具体的handle方法,我们可以看到核心逻辑是有两个:
- 创建了一个HistogramMetricsTrackerDelegate类型的startTimer对象
- 执行DefaultSoulPluginChain的execute方法,加入scheduler线程模型,以及成功回调的处理逻辑(看起来和监控相关,后面分析)
详细说一下DefaultSoulPluginChain
的execute方法,它是负责处理具体插件逻辑的。
@Override
public Mono<Void> execute(final ServerWebExchange exchange) {
return Mono.defer(() -> {
if (this.index < plugins.size()) {
SoulPlugin plugin = plugins.get(this.index++);
Boolean skip = plugin.skip(exchange);
if (skip) {
return this.execute(exchange);
}
return plugin.execute(exchange, this);
}
return Mono.empty();
});
}
整体结构就是遍历所有插件类型,通过skip
方法判断exchange中带有插件类型的参数是否跟plugin匹配,如果匹配则执行对应plugin的execute方法,如果不匹配则往下迭代。
skip方法
从exchange请求中获取插件参数判断,以此判断是否执行当前plugin的execute方法。
@Override
public Boolean skip(final ServerWebExchange exchange) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
return !Objects.equals(Objects.requireNonNull(soulContext).getRpcType(), RpcTypeEnum.HTTP.getName());
}
WebClientPlugin
上面我们了解到了SoulWebHandler
将请求委托给具体的插件进行执行,这里我们的http请求会委托给WebClientPlugin
。查看WebClientPlugin的execute实现,其核心逻辑则是从请求的exchange中获取http请求的参数,然后设置一些请求属性,最后重点来了——使用WebClient将http请求转发到我们的后端服务!(和我们上一篇的demo逻辑是一样的!)
@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assert soulContext != null;
String urlPath = exchange.getAttribute(Constants.HTTP_URL);
if (StringUtils.isEmpty(urlPath)) {
Object error = SoulResultWrap.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
long timeout = (long) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_TIME_OUT)).orElse(3000L);
int retryTimes = (int) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_RETRY)).orElse(0);
log.info("The request urlPath is {}, retryTimes is {}", urlPath, retryTimes);
HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());
WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);
return handleRequestBody(requestBodySpec, exchange, timeout, retryTimes, chain);
}
//...
private Mono<Void> handleRequestBody(final WebClient.RequestBodySpec requestBodySpec,
final ServerWebExchange exchange,
final long timeout,
final int retryTimes,
final SoulPluginChain chain) {
return requestBodySpec.headers(httpHeaders -> {
httpHeaders.addAll(exchange.getRequest().getHeaders());
httpHeaders.remove(HttpHeaders.HOST);
})
.contentType(buildMediaType(exchange)) //设置contentType
.body(BodyInserters.fromDataBuffers(exchange.getRequest().getBody())) //设置请求体
.exchange() //发送请求
.doOnError(e -> log.error(e.getMessage())) //异常处理
.timeout(Duration.ofMillis(timeout)) //超时设置
.retryWhen(Retry.onlyIf(x -> x.exception() instanceof ConnectTimeoutException) //重试处理
.retryMax(retryTimes)
.backoff(Backoff.exponential(Duration.ofMillis(200), Duration.ofSeconds(20), 2, true)))
.flatMap(e -> doNext(e, exchange, chain));
}
//将请求响应传入设置参数,传入WebClientResponsePlugin插件进行处理
private Mono<Void> doNext(final ClientResponse res, final ServerWebExchange exchange, final SoulPluginChain chain) {
if (res.statusCode().is2xxSuccessful()) {
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName());
} else {
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.ERROR.getName());
}
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_ATTR, res);
return chain.execute(exchange);
}
这里面需要注意的是handleRequestBody
方法,利用链式调用许多逻辑 ,包括:
设置请求头参数->设置contentType->设置请求体->发送请求->异常处理->超时设置->重试处理->请求响应转换处理->返回Mono
这里需要注意几个WebClient的对象方法:
- 方法exchange 的作用是发送请求并得到以 Mono表示的 HTTP 响应。
- 方法flatMap是作用是处理响应并返回Mono对象。
WebClientResponsePlugin
最终处理后的响应落到WebClientResponsePlugin
插件上,由该插件将返回值写入ServerHttpResponse
, 返回给前端,由此整个http请求转发调用结束。
public class WebClientResponsePlugin implements SoulPlugin {
@Override
public Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {
return chain.execute(exchange).then(Mono.defer(() -> {
ServerHttpResponse response = exchange.getResponse();
ClientResponse clientResponse = exchange.getAttribute(Constants.CLIENT_RESPONSE_ATTR);
if (Objects.isNull(clientResponse)
|| response.getStatusCode() == HttpStatus.BAD_GATEWAY
|| response.getStatusCode() == HttpStatus.INTERNAL_SERVER_ERROR) {
Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_RESULT_ERROR.getCode(), SoulResultEnum.SERVICE_RESULT_ERROR.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
if (response.getStatusCode() == HttpStatus.GATEWAY_TIMEOUT) {
Object error = SoulResultWrap.error(SoulResultEnum.SERVICE_TIMEOUT.getCode(), SoulResultEnum.SERVICE_TIMEOUT.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
response.setStatusCode(clientResponse.statusCode());
response.getCookies().putAll(clientResponse.cookies());
response.getHeaders().putAll(clientResponse.headers().asHttpHeaders());
return response.writeWith(clientResponse.body(BodyExtractors.toDataBuffers()));
}));
}
//...
}
思考总结
今天我们详细分析了一个从前端http请求到soul网关的调用流程,大概逻辑如下:
- SoulConfiguration将我们所有装配的SoulPlugin加载到WebFlux上下文。
- 配置我们的WebHandler,并让它持有所有SoulPlugin对象。
- 通过请求url的contextPath参数来匹配对应的的rpc协议,并委托给响应的插件进行处理。
- 通过对应的rpc协议插件与后端进行请求调用。
- 将结果返回值转换成对应的Response写回客户端。
明天,我们具体分析一下整个Soul网关插件的调用顺序是怎么设计的。