Soul网关源码学习(8)- 代理转发入口 SoulWebHandler

前言

在前一篇文章《Soul网关源码学习(7)- 代理转发流程概览2》 中,我们理清楚了不同协议的代理转发流程,知道了每种协议处理的步骤和关键节点在哪里。那么现在就开始对这些节点逐个进入详细的分析,而 SoulWebHandler 作为 Request 的第一站自然就是我们首先要攻克的对象。

WebHandler 简介

在分析 SoulWebHandler 之前,我们需要先了解一下 WebHandler,因为 SoulWebHandler 是 WebHandler 的自定义实现。那 WebHandler 又是来自哪里呢?到底是什么呢?WebHandler 是 Spring WebFlux 的API!

根据 Spring 官方的说法:Spring WebFlux 是一套全新的 Reactive Web 栈技术,实现完全非阻塞,支持 Reactive Streams 背压等特性,并且运行环境不限于 Servlet 容器(Tomcat、Jetty、Undertow),如 Netty 等。

所以既然是全新的 Web 栈技术,也就是 Spring WebFlux 脱离了传统的 Servlet API,而 WebHandler 就是其提供类似 Servlet 功能的一个接口。如果自己手动实现过 Servlet 编程的应该对下面这个东西不陌生:

//在曾经的过去我们是这样写接口处理的
public class MyServlet implements Servlet {
	...
    @Override
    public void service(ServletRequest paramServletRequest,
            ServletResponse paramServletResponse) throws ServletException,
            IOException {
            // 处理你的请求 doGet、doPost
    }
    ...
}

WebHandler 在 Spring WebFlux 中起到的就是类似上面的作用,只不过处理请求的方法有 service 变成了 handle。

WebHandler 在 Spring WebFlux 中的加载是比较复杂的,这里不做详细介绍,有兴趣的同学可以自己去了解一下,这里只介绍一下如果我们想实现一个自己的 WebHandler 需要注意上什么。

WebHandler Bean 在 WebFlux 中是唯一的,而且 Bean Name 必须是 “webHandler”,你可以实现多个,但是只有 Bean Name 是 “webHandler” 会起作用。

//这是 Spring Web 下的类,会在Spring WebFlux 加载过程中被调用
public final class WebHttpHandlerBuilder {
	public static final String WEB_HANDLER_BEAN_NAME = "webHandler";
	//WebHander Bean 会在这里实例化,可以看到它是通过 "webHandler" 名称去依赖查找的
	public static WebHttpHandlerBuilder applicationContext(ApplicationContext context) {
		WebHttpHandlerBuilder builder = new WebHttpHandlerBuilder(
				context.getBean(WEB_HANDLER_BEAN_NAME, WebHandler.class), context);
		...
	}
}

接着我们来看一下 soul 是如何注入自定义 WebHandler的:

public class SoulConfiguration {
	// 这是 soul 注入自定义 WebHandler的方法
	// 实现类就是我们今天的主角 SoulWebHandler, 注意 BeanName
    @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);
    }
}

到这里我们就了解了 SoulWebHandler 之所以是网关请求入口的原因,还有它是如何被装载的。

SoulWebHandler 详细分析

构造方法

要分析一个类,那首先就是先来分析其构造方法:

public SoulWebHandler(final List<SoulPlugin> plugins) {
	//注入插件
    this.plugins = plugins;
    // 定义 Reactor 调度线程池
    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)));
		heduler = Schedulers.newParallel("soul-work-threads", threads);
    } else {
        scheduler = Schedulers.elastic();
    }
}

插件注入

通过构造方法的注释,我相信小伙伴也不难理解,现在可能的疑问就是插件又是哪里加载的呢?

我们在次回到上面 WebHandler 的 @Bean 方法,该方法的参数是 ObjectProvider<List< SoulPlugin>>,这里又可能会有两个疑问:

  • 为什么使用 ObjectProvider,而不是直接使用List?

    使用ObjectProvider可以避免强依赖导致的依赖对象不存在异常。由于 Soul 的插件是设计成可插拔的,因此这里的 SoulPlugin 的 Bean 是有可能一个都不存在的,这时候如果直接使用 List 作为参数注入,就有可能会在装载过程中抛出异常。而 ObjectProvider#getIfAvailable 就提供了一个安全的依赖查找方法,如果此时容器中不存在相应的 Bean 则只会返回空,而不会抛出异常。

  • SoulPlugin 都在哪里被定义了?

    这个问题容易很容易解答,我们以 Dubbo 插件为例:

    @Configuration
    @ConditionalOnClass(ApacheDubboPlugin.class)
    public class ApacheDubboPluginConfiguration {
        @Bean
        public SoulPlugin apacheDubboPlugin(final ObjectProvider<DubboParamResolveService> dubboParamResolveService) {
            return new ApacheDubboPlugin(new ApacheDubboProxyService(dubboParamResolveService.getIfAvailable()));
        }
        ...
    }
    

    通过上面代码可以看到两点:第一,插件通过 @Bean 方法加载;第二,通过条件注解 @ConditionalOnClass 来实现条件装配,只有插件被依赖了才会被满足装配的条件。

执行方法

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

handler 是 WebHandler 的接口方法,作用类似 Servlet 中的 service 方法,负责处理请求,下一步就是进入插件链的执行流程了,到这里就和我们上一篇的文章连接起来了。最后我们再对插件连的调用代码做一个简单的讲解,也就是上面代码注释了“关键”的地方。

  • subscribeOn:表示执行流程在构造函数时候定义的线程池(调度器)上执行。
  • doOnSuccess:度量记录代理转发流程的结束时间。doOnSuccess 会在 Mono成功完成时触发(结果为T或null),这意味着无论数据状态如何,处理本身都成功完成。
  • excute:
    public Mono<Void> execute(final ServerWebExchange exchange) {
    	//代码很简单,就是返回一个空的 Mono,插件链执行的如何都是返回以空的 Mono
    	//空的 Mono 意味着不管插件链执行成功或者异常都能走到上面的doOnSuccess
        return Mono.defer(() -> {
            if (this.index < plugins.size()) {
    			//...省略插件链调用代码
            }
            return Mono.empty();
        });
    }
    

总结

到这里我们就把网关入口 SoulWebHandler 的代码分析完毕了,其实 SoulWebHandler 本身的代码逻辑很简单,文章更多时候分析的是代码扩展出去的其他知识点。其实,这也是学习源码的主要目的之一,我们借助分析成熟的框架来扩展和巩固自己的知识体系。一个成熟的开源框架往往都会使用到大量当下流行的技术,这些技术有的可能是我们的知识盲点,也有的可能是我们熟悉但是缺乏实践的,都可以通过学习这些开源框架来促使我们想办法去掌握它们。

这章节介绍完入口后,那下面就是主干道了,后面我们将继续分析请求进了“村口”后该何去何从。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值