Soul 网关源码学习(12) - Soul 网关 soul bootstrap 运行时插件拦截

昨天的源码学习,我们知道了 soul 网关是如何实现插件的热插拔的,可以看到 soul 网关插件和数据同步的对象设计做得是比较好的,我们如果要新增插件或者新增数据同步的方式只需要按照插件体系和数据同步体系的设计新增实现类就好了。

soul 网关运行时接收流量的组件是 soul bootstrap, 这个工程是基于 spring-webflux 开发的响应式的 Java Web 应用, soul 网关的插件通过组成插件链的形式, 让所有进入 web 应用的请求都经过一次插件链

soul bootstrap 插件链的组织形式

通过昨天的源码学习,我们了解到了 soul bootstrap 的工程依赖中加入了所有 starter 形式的可用的插件信息,通过运行时数据同步的方式从 soul admin 信息中同步影响插件行为的数据,这些数据存放在了 JVM 缓存当中。

soul bootstrap 运行时相关的配置在 soul-web 模块中,这个模块定义了 soul bootstrap 运行时的相关功能,比如统一异常处理CORS 请求处理部分 RPC 类型参数转化组织 soul bootstrap web 特性相关的配置等功能。soul-web 包含了一些配置类,
其中比较关键的类是 org.dromara.soul.web.configuration.SoulConfiguration

@Configuration
@ComponentScan("org.dromara.soul")
@Import(value = {ErrorHandlerConfiguration.class, SoulExtConfiguration.class, SpringExtConfiguration.class})
@Slf4j
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);
}

@Bean("dispatcherHandler")
public DispatcherHandler dispatcherHandler() {
return new DispatcherHandler();
}

@Bean
public PluginDataSubscriber pluginDataSubscriber(final ObjectProvider<List<PluginDataHandler>> pluginDataHandlerList) {
return new CommonPluginDataSubscriber(pluginDataHandlerList.getIfAvailable(Collections::emptyList));
}

@Bean
@ConditionalOnMissingBean(value = DubboParamResolveService.class, search = SearchStrategy.ALL)
public DubboParamResolveService defaultDubboParamResolveService() {
return new DefaultDubboParamResolveServiceImpl();
}

@Bean
@ConditionalOnMissingBean(value = SofaParamResolveService.class, search = SearchStrategy.ALL)
public DefaultSofaParamResolveServiceImpl defaultSofaParamResolveService() {
return new DefaultSofaParamResolveServiceImpl();
}

@Bean
@ConditionalOnMissingBean(RemoteAddressResolver.class)
public RemoteAddressResolver remoteAddressResolver() {
return new ForwardedRemoteAddressResolver(1);
}

@Bean
@Order(-100)
@ConditionalOnProperty(name = "soul.cross.enabled", havingValue = "true")
public WebFilter crossFilter() {
return new CrossFilter();
}

@Bean
@Order(-10)
@ConditionalOnProperty(name = "soul.file.enabled", havingValue = "true")
public WebFilter fileSizeFilter(final SoulConfig soulConfig) {
return new FileSizeFilter(soulConfig.getFileMaxSize());
}

@Bean
@Order(-5)
@ConditionalOnProperty(name = "soul.exclude.enabled", havingValue = "true", matchIfMissing = false)
public WebFilter excludeFilter(final ExcludePathProperties excludePathProperties) {
return new ExcludeFilter(excludePathProperties);
}

@Bean
@ConfigurationProperties(prefix = "soul")
public SoulConfig soulConfig() {
return new SoulConfig();
}

@Bean
@Order(30)
@ConditionalOnProperty(name = "soul.filterTimeEnable")
public WebFilter timeWebFilter(final SoulConfig soulConfig) {
return new TimeWebFilter(soulConfig);
}

@Bean
@Order(4)
public WebFilter webSocketWebFilter() {
return new WebSocketParamFilter();
}
}

这个 Configuration 类上面标准了 @ComponentScan 注解, 里面的参数传入了 com.dromara.soul, 指明了 Spring 容器加载 classpath 中 com.dromara.soul 包路径下的所有 Bean, 这样就把 soul 运行时的所需要的
Spring Bean 全部加载到容器中, 配置中还有一些 Filter 的定义, 这些 Filter 定义是部署级别的可选用, 即通过 Spring 应用的配置可以选择开启部分 Filter 的功能。

上面比较重要的配置 Bean 是 SoulWebHandler 的配置,可以看到这个类的构造函数需要传入 SoulPlugin 的列表对象:

public final class SoulWebHandler implements WebHandler {

private final List<SoulPlugin> plugins;

private final Scheduler scheduler;

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();
}
}

@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)));
}

private static class DefaultSoulPluginChain implements SoulPluginChain {

private int index;

private final List<SoulPlugin> plugins;

DefaultSoulPluginChain(final List<SoulPlugin> plugins) {
this.plugins = plugins;
}

@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();
});
}
}
}

可以看到 SoulPlugin 的列表对象最终用于构建了 org.dromara.soul.web.handler.SoulWebHandler.DefaultSoulPluginChain 实例对象影响了插件运行时的行为。

阅读这部分源码遇到的问题

  • 如何理解 org.dromara.soul.web.configuration.SoulConfiguration 中同时定义了 org.dromara.soul.web.handler.SoulWebHandlerorg.springframework.web.reactive.DispatcherHandler ?

org.dromara.soul.web.handler.SoulWebHandlerorg.springframework.web.reactive.DispatcherHandler 都实现了 org.springframework.web.server.WebHandler 接口, DispatcherHandler
Spring WebFlux 为了兼容 Spring Web 抽象实现的 ReactWebServer 而定义的一个 WebHandler, 请求进入 Web 容器后, 会由 DispatcherHandler 转发, 然后再由 SoulWebHandler 处理。

文章作者: David Liu
文章链接: https://davidliu.now.sh/2021/01/27/soul_plugin_chain/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 David Liu's Blog