Soul网关源码阅读(十二)- Soul 网关 soul bootstrap 运行时插件拦截

本文解析了SoulBootstrap如何基于Spring-webflux实现热插拔,介绍其如何通过插件链处理请求,并重点讲解了SoulConfiguration中的关键组件及其作用。

学习目标:

  1. soul bootstrap 的插件链

学习内容:

上篇文章介绍了soul实现热插拔的原理,今天学习一下源码,看看新增插件/数据程序是如何运行的

学习时间:

2021年1月31号

学习产出:

soul 网关运行时接收流量的组件是 soul bootstrap, 这个工程是基于 spring-webflux 开发的响应式的 Java Web 应用, soul 网关的插件通过组成插件链的形式, 让所有进入 web 应用的请求都经过一次插件链。
soul bootstrap 的工程依赖中加入了所有 starter 形式的可用的插件信息,通过运行时数据同步的方式从 soul admin 信息中同步影响插件行为的数据,这些数据存放在了 JVM 缓存当中。
soul bootstrap 运行时相关的配置在 soul-web 模块中加载,这个模块定义了 soul bootstrap 运行时的相关功能,比如统一异常处理,统一处理过滤器,部分 RPC 类型参数转化等,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 {
    
    /**
     * 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);
    }
    
    /**
     * init dispatch handler.
     *
     * @return {@link DispatcherHandler}.
     */
    @Bean("dispatcherHandler")
    public DispatcherHandler dispatcherHandler() {
        return new DispatcherHandler();
    }
    
    /**
     * Plugin data subscriber plugin data subscriber.
     *
     * @param pluginDataHandlerList the plugin data handler list
     * @return the plugin data subscriber
     */
    @Bean
    public PluginDataSubscriber pluginDataSubscriber(final ObjectProvider<List<PluginDataHandler>> pluginDataHandlerList) {
        return new CommonPluginDataSubscriber(pluginDataHandlerList.getIfAvailable(Collections::emptyList));
    }
    
    /**
     * Generic param resolve service generic param resolve service.
     *
     * @return the generic param resolve service
     */
    @Bean
    @ConditionalOnProperty(name = "soul.dubbo.parameter", havingValue = "multi")
    public DubboParamResolveService dubboMultiParameterResolveServiceImpl() {
        return new DubboMultiParameterResolveServiceImpl();
    }
    
    /**
     * Generic param resolve service dubbo param resolve service.
     *
     * @return the dubbo param resolve service
     */
    @Bean
    @ConditionalOnMissingBean(value = DubboParamResolveService.class, search = SearchStrategy.ALL)
    public DubboParamResolveService defaultDubboParamResolveService() {
        return new DefaultDubboParamResolveService();
    }
    
    /**
     * Remote address resolver remote address resolver.
     *
     * @return the remote address resolver
     */
    @Bean
    @ConditionalOnMissingBean(RemoteAddressResolver.class)
    public RemoteAddressResolver remoteAddressResolver() {
        return new ForwardedRemoteAddressResolver(1);
    }
    
    /**
     * Cross filter web filter.
     * if you application has cross-domain.
     * this is demo.
     * 1. Customize webflux's cross-domain requests.
     * 2. Spring bean Sort is greater than -1.
     *
     * @return the web filter
     */
    @Bean
    @Order(-100)
    @ConditionalOnProperty(name = "soul.cross.enabled", havingValue = "true")
    public WebFilter crossFilter() {
        return new CrossFilter();
    }
    
    /**
     * Body web filter web filter.
     *
     * @param soulConfig the soul config
     * @return the web filter
     */
    @Bean
    @Order(-10)
    @ConditionalOnProperty(name = "soul.file.enabled", havingValue = "true")
    public WebFilter fileSizeFilter(final SoulConfig soulConfig) {
        return new FileSizeFilter(soulConfig.getFileMaxSize());
    }
    
    /**
     * Soul config soul config.
     *
     * @return the soul config
     */
    @Bean
    @ConfigurationProperties(prefix = "soul")
    public SoulConfig soulConfig() {
        return new SoulConfig();
    }
    
    /**
     * Init time web filter.
     *
     * @param soulConfig the soul config
     * @return {@linkplain TimeWebFilter}
     */
    @Bean
    @Order(30)
    @ConditionalOnProperty(name = "soul.filterTimeEnable")
    public WebFilter timeWebFilter(final SoulConfig soulConfig) {
        return new TimeWebFilter(soulConfig);
    }
    
    /**
     * Web socket web filter web filter.
     *
     * @return the web filter
     */
    @Bean
    @Order(4)
    public WebFilter webSocketWebFilter() {
        return new WebSocketParamFilter();
    }
}

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;

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

    /**
     * Handle the web server exchange.
     *
     * @param exchange the current server exchange
     * @return {@code Mono<Void>} to indicate when request handling is complete
     */
    @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;

        /**
         * Instantiates a new Default soul plugin chain.
         *
         * @param plugins the plugins
         */
        DefaultSoulPluginChain(final List<SoulPlugin> plugins) {
            this.plugins = plugins;
        }

        /**
         * Delegate to the next {@code WebFilter} in the chain.
         *
         * @param exchange the current server exchange
         * @return {@code Mono<Void>} to indicate when request handling is complete
         */
        @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 实例对象影响了插件运行时的行为。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值