soul源码分析(1)http插件的使用与soul插件工作流程分析

目标

  • 演示soul网关HTTP插件的使用
  • 分析soul插件的整体工作流程,

1. HTTP插件的使用

1.1 在soul-bootstrap项目中引入如下插件,然后重新启动soul网关,启动步骤参见 soul源码分析_0_阅读源码准备与soul基础

  <!--if you use http proxy start this-->
   <dependency>
       <groupId>org.dromara</groupId>
       <artifactId>soul-spring-boot-starter-plugin-divide</artifactId>
        <version>${last.version}</version>
   </dependency>

   <dependency>
       <groupId>org.dromara</groupId>
       <artifactId>soul-spring-boot-starter-plugin-httpclient</artifactId>
        <version>${last.version}</version>
   </dependency>

1.2 将soul-client接入到你的项目中,此处以spring-boot项目为例给下配置方式:

  • (1)在你的项目中添加soul-spring-boot-starter-client-springmvc依赖:
   <dependency>
         <groupId>org.dromara</groupId>
         <artifactId>soul-spring-boot-starter-client-springmvc</artifactId>
         <version>${last.version}</version>
     </dependency>
  • (2)在你的项目文件中添加如下配置(以YAML格式为例)
 soul:
     http:
       adminUrl: http://localhost:9095
       port: 你本项目的启动端口
       contextPath: /http
       appName: http
       full: false  
   # adminUrl: 为你启动的soul-admin 项目的ip + 端口,注意要加http://
   # port: 你本项目的启动端口
   # contextPath: 为你的这个mvc项目在soul网关的路由前缀,比如/order ,/product 等等,网关会根据你的这个前缀来进行路由.
   # appName:你的应用名称,不配置的话,会默认取 `spring.application.name` 的值
   # full: 设置true 代表代理你的整个服务,false表示代理你其中某几个controller

SpringMVC的接入方式参见官方文档:https://dromara.org/zh-cn/docs/soul/user-http.html

  • (3)在你的 controller 的接口上加上 @SoulSpringMvcClient 注解。

    • 可以把注解加到 Controller 类上面,里面的path属性则为前缀,如果含有 /** 代表你的整个接口需要被网关代理。

    此处可以参考官方示例soul-examples-http中的代码。

    简单起见本文也将以soul-examples-http作为后端应用。

运行soul-examples-http

soul-examples-http作为示例。在上述工作准备完毕后,检查下soul-admin管理端页面中的System Manage->Plugindivide插件状态、保证为开启状态,

在这里插入图片描述

然后启动soul-examples-http,会发现后端程序启动时将接口注册到了soul中,参考下面日志:

.......
2021-01-22 17:41:44.422  INFO 27980 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'org.dromara.soul.springboot.starter.client.springmvc.SoulSpringMvcClientConfiguration' of type [org.dromara.soul.springboot.starter.client.springmvc.SoulSpringMvcClientConfiguration$$EnhancerBySpringCGLIB$$a469355d] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-22 17:41:44.446  INFO 27980 --- [           main] trationDelegate$BeanPostProcessorChecker : Bean 'soulHttpConfig' of type [org.dromara.soul.client.springmvc.config.SoulSpringMvcConfig] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2021-01-22 17:41:45.118  INFO 27980 --- [pool-1-thread-1] o.d.s.client.common.utils.RegisterUtils  : http client register success: {"appName":"http","context":"/http","path":"/http/test/**","pathDesc":"","rpcType":"http","host":"172.0.1.58","port":8188,"ruleName":"/http/test/**","enabled":true,"registerMetaData":false} 
2021-01-22 17:41:45.157  INFO 27980 --- [pool-1-thread-1] o.d.s.client.common.utils.RegisterUtils  : http client register success: {"appName":"http","context":"/http","path":"/http/order/save","pathDesc":"Save order","rpcType":"http","host":"172.0.1.58","port":8188,"ruleName":"/http/order/save","enabled":true,"registerMetaData":false} 
.......

此时可以在soul管理端看到如下信息:

在这里插入图片描述

此时就可以尝试被网关代理后的接口,务必注意端口地址哈:

  • 比如我运行的soul-examples-http默认配置是8188端口
  • soul管理端管理端soul-admin默认配置的端口是9095
  • soul网关程序默认端口是9195

所以!我们想访问网关的接口就应该是:127.0.0.1:9195/xxxx,如order/findById?id=1,使用postman先直接访问后端:

在这里插入图片描述

测试网关:

在这里插入图片描述

soul-bootstrap打印如下日志:

2021-01-22 18:12:42.467  INFO 25552 --- [-work-threads-4] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http
2021-01-22 18:12:42.468  INFO 25552 --- [-work-threads-4] o.d.soul.plugin.base.AbstractSoulPlugin  : divide rule success match , rule name :/http/order/findById
2021-01-22 18:12:42.468  INFO 25552 --- [-work-threads-4] o.d.s.plugin.httpclient.WebClientPlugin  : The request urlPath is http://172.0.1.58:8188/order/findById?id=2, retryTimes is 0

说明网关正常运行。

2. soul插件工作流程分析

接下来我们先初步分析一个HTTP请求是如何工作的。

一般而言,我们进行源码分析,主要依据就是运行日志,再次看下接口被代理、转发时的日志:

2021-01-22 18:12:42.467  INFO 25552 --- [-work-threads-4] o.d.soul.plugin.base.AbstractSoulPlugin  : divide selector success match , selector name :/http
2021-01-22 18:12:42.468  INFO 25552 --- [-work-threads-4] o.d.soul.plugin.base.AbstractSoulPlugin  : divide rule success match , rule name :/http/order/findById
2021-01-22 18:12:42.468  INFO 25552 --- [-work-threads-4] o.d.s.plugin.httpclient.WebClientPlugin  : The request urlPath is http://172.0.1.58:8188/order/findById?id=2, retryTimes is 0

可以看到HTTP请求被转发时,主要是AbstractSoulPluginWebClientPlugin在起作用。我们进入到AbstractSoulPlugin,依据日志内容divide selector success match , selector name :/http,可以找到核心逻辑是在execute方法。

@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);
            if (CollectionUtils.isEmpty(selectors)) {
                return handleSelectorIsNull(pluginName, exchange, chain);
            }
            final SelectorData selectorData = matchSelector(exchange, selectors);
            if (Objects.isNull(selectorData)) {
                return handleSelectorIsNull(pluginName, exchange, chain);
            }
            selectorLog(selectorData, pluginName);
            final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
            if (CollectionUtils.isEmpty(rules)) {
                return handleRuleIsNull(pluginName, exchange, chain);
            }
            RuleData rule;
            if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) {
                //get last
                rule = rules.get(rules.size() - 1);
            } else {
                rule = matchRule(exchange, rules);
            }
            if (Objects.isNull(rule)) {
                return handleRuleIsNull(pluginName, exchange, chain);
            }
            ruleLog(rule, pluginName);
            return doExecute(exchange, chain, selectorData, rule);
        }
        return chain.execute(exchange);
    }

在该方法中可以看到’Mono’这种返回值,这是因为soul中使用了反应式编程,引入了reactor-spring,有关反应式编程又是一大块内容,请读者自行搜索。

execute方法中,从整体来看该方法采用了模板方法模式、责任链设计模式,这块后续分析。

模板方法模式的使用

在框架开发开发中,经常使用模板方法模式,在父类中规定好主要流程,或是可以复用的代码逻辑,然后个性化的逻辑定义为抽象方法,由子类完成,最大限度的复用代码。

此处execute方法的逻辑并不复杂:

1. 根据当前插件名称在缓存中找到插件数据pluginData
2. if pluginData存在 且 可用 
	根据插件名称从缓存中找到对应selector
	根据selector拿到所有规则rules,找到当前请求所匹配的那个规则rule
	执行该规则(`doExecute`方法,具体逻辑取决于是哪个插件,比如HTTP的需要看DividePlugin的doExecute)
3. 执行后续的插件处理逻辑(责任链)

这个是父类定义好的,具体处理HTTP的逻辑在AbstractSoulPlugin子类DividePlugin中的doExecute方法中,大体可以看到该方法主要逻辑如下:

1. 获取divide规则、配置
2. 拿出请求中的IP地址,根据负责均衡规则,从divide配置中找出一个作为目标地址
3. 重写请求(改写URL、超时、重试等)
4. 继续执行后续逻辑(责任链)

这里又涉及到n多细节逻辑,比如:

  • divide规则、配置是如何保存的?又是如何加载soul网关中的?
  • 负载均衡策略如何实现?
  • 请求是如何转发给目标服务器的?返回数据又是如何传会给请求方的?

这些等后续再分析,此处先略过。

责任链模式的使用

我个人理解用责任链主要是为了方便扩展,毕竟说是插件化,那插件放到链上、或是从链上移除,就可以实现插件化、可插拔。

你问我为啥能看出是责任链?这个真的靠经验啦,看传入参数、参数命名、return语句又调用了chain的execute方法,基本可以确定是责任链。

嗯,源码看多了自然能知道这些,所以啊,要多看优秀框架的源码,比如soul,插入广告一下 😄

总结

  • 如何使用soul代理HTTP请求
  • 如何分析开源代码:通过日志,一点点debug,追根溯源
  • divide的基本处理逻辑
  • soul插件处理整体分析

TODO

  • soul中插件是如何初始化的?如何通过责任链关联起来、实现功能可插拔?
  • divide规则、配置是如何保存的?又是如何加载soul网关中的?
  • 负载均衡策略如何实现?
  • 请求是如何转发给目标服务器的?返回数据又是如何传会给请求方的?

版权声明:本文为evasnowind原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。