soul框架简单介绍与设计模式分析

1. 初识

1.1 查看官网文档了解

针对API的网关 – 可以为每个接口配置负载均衡、限流等功能

有三个角色:

  • soul-admin 配置规则
  • soul-gateway 网关 – 用户请求会打到这
  • server 用户真实的服务 – 用户请求通过网关转发到这

流程:请求打到网关,依据用户的配置,转发到server。当然除了转发还有限流、负载均衡等~

1.2. 目标:

  1. 了解soul网关的实现原理
  2. 插件化的设计有没有什么玄机(现在看来就是不同的协议用的不同的module)
  3. 了解框架中使用的设计模式
  4. 分析框架的分层

2. 分析源码

2.1. 网关的实现原理

网关接收请求入口中哪,如何处理不同协议请求。网关接收到规则,做了什么?

2.1.1. 跨域请求问题

soul-web有个 CrossFilter 跨域过滤器,将response的Access-Control-Allow-Origin等设置为*

扩展知识1:
cors请求分为简单查询(请求方法为HEAD、GET、POST且HTTP头信息不超过以下五种[Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain])和非简单查询。
针对非简单查询,浏览器在正式通信之前,会先发起一次OPTIONS预检
http://www.ruanyifeng.com/blog/2016/04/cors.html

2.1.2. 网关请求处理

请求入口是soul-web的SoulWebHandler
SoulWebHandler 中持有一个插件列表List<SoulPlugin> plugins(根据order排序), 在WebHandler触发handle的时候,将插件放到一个chain里面,按顺序执行插件的execute方法。将请求上链后,soul-web无需关注请求的处理与传递,发送和处理解耦。【责任链模式

扩展知识2:
Reactor,Java响应式编程。Spring5的WebFlux:与Spring WebMvc 同级,是一个支持反应式编程模型的新框架体系。反应式模型区别于传统的 MVC 最大的不同是异步的、事件驱动的、非阻塞的。提高应用程序的并发性能,单位时间能够处理更多的请求。
https://www.jianshu.com/p/8a2c9376bc11
https://www.jianshu.com/p/7ee89f70dfe5?from=singlemessage
https://learnku.com/articles/30263

扩展知识3:
Mono.just 和Mono.defer区别:
例如:Mono m2 = Mono.defer(()->Mono.just(new Date())); 每执行一次:m2.subscribe(System.out::println); 都会输出不同的date,因为他是在订阅的时候才去new Date的;如果执行是Mono.just(new Date()); 是声明的时候就new date, 订阅几次都是这个date

2.1.3. http网关插件

所有的网关继承一个抽象的网关类,抽象类定义模版方法(SoulWebHandler的handle方法调用的就是插件的这个Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain)方法,此方法是模板方法:先处理通用的选择器、规则等信息,最终调用了抽象方法abstract Mono<Void> doExecute(ServerWebExchange exchange, SoulPluginChain chain, SelectorData selector, RuleData rule),此方法每个插件有不同的实现。【模版方法模式

2.1.3.1 插件数据获取(插件、选择器、规则)

插件数据都缓存中内存里BaseDataCache,以map的结构存储,分为:PLUGIN_MAPSELECTOR_MAPRULE_MAP
BaseDataCache类中代码很整洁,得益于Optional类的使用,对于map对象操作的方法,基本上一行代码就解决(不需要各种非空判断)。【使用Optional替代非空判断

[其实这点在某本书(重构?码工?还是整洁?忘了哪本了)上也提到过,不过当时没重视起来。现在看来对代码整洁性有很大提升,至少看着这些代码很舒服,以后要注意使用]

2.1.3.1.1 数据的订阅

问题1:数据从缓存取,那么缓存数据哪里来

可以看到有个类CommonPluginDataSubscriber,类中方法subscribeDataHandler 处理了三类(插件、选择器、配置)数据的消息(修改、删除)。这块应该是【观察者模式的思想】。
CommonPluginDataSubscriber类整体设计不错,实现来PluginDataSubscriber接口,不同的订阅消息都是一个方法,对应三类数据的订阅操作等。收到消息后,基本上会调用一个subscribeDataHandler方法。个人认为此方法就封装等不太好, 如下代码: 就算不用策略模式[什么时候使用这个模式又不会有过度设计等嫌疑呢],至少把三个if xx instanceof xx 的分支内容都封装成一个方法。

    private <T> void subscribeDataHandler(final T classData, final DataEventTypeEnum dataType) {
        Optional.ofNullable(classData).ifPresent(data -> {
            if (data instanceof PluginData) {
                if (dataType == DataEventTypeEnum.UPDATE) {
                   ...
                } else if (dataType == DataEventTypeEnum.DELETE) {
                   ...
                }
            } else if (data instanceof SelectorData) {
                if (dataType == DataEventTypeEnum.UPDATE) {
                   ...
                } else if (dataType == DataEventTypeEnum.DELETE) {
                   ...
                }
            } else if (data instanceof RuleData) {
                if (dataType == DataEventTypeEnum.UPDATE) {
                   ...
                } else if (dataType == DataEventTypeEnum.DELETE) {
                   ...
                }
            }
        });
    }

问题2:什么时候订阅的,什么时候会回调onSubscribeonSelectorSubscribe等方法?

可以配置不同的数据同步方案,例如http、websocke、zookeeper等,这里结构清晰,易于扩展,不同等实现是一个module。例如:soul-sync-data-http、soul-sync-data-nacos、soul-sync-data-websocket等等。

  • http:项目启的时候(spring bean注解new 一个HttpSyncDataService 构造方法里面执行网络请求),做一次fetch。并有一个线程开启轮询。
  • websocket:同样用spring 的bean注解作为入口,建立websocket连接WebsocketSyncDataService

问题3:数据的可用性,如何保证数据高可用
以http拉取方式为例,soul做了异常(超时)重查。除了重查,看起来没有做进一步的操作保证数据拉取,但有日志,可以接入监控报警。基本上够了~
这里把代码贴出来的目的是:日志打的很细节,先是warn,后是error。(打日志要注意规范,什么时候是warn, 什么时候是error)

 catch (Exception e) {
    // print warnning log.
    if (time < retryTimes) {
        log.warn("Long polling failed, tried {} times, {} times left, will be suspended for a while! {}",
                time, retryTimes - time, e.getMessage());
        ThreadUtils.sleep(TimeUnit.SECONDS, 5);
        continue;
    }
    // print error, then suspended for a while.
    log.error("Long polling failed, try again after 5 minutes!", e);
    ThreadUtils.sleep(TimeUnit.MINUTES, 5);
}
2.1.3.2 插件的执行(以http插件为例)
  • 获取负载均衡器,这里是个spi,方便自定义复杂的负载均衡算法
  • 根据规则生成新的url
  • 转发请求
    其实这个转发有点没看明白, 就写了个exchange.getAttributes().put(Constants.HTTP_URL, realURL);

2.2. 亮点

  1. 代码module层次清晰。
  2. 类职责单一:例如数据同步例如soul-web中有很多过滤器,每个过滤器只做一件事(CrossFilter、ExcludeFilter、FileSizeFilter等)。
  3. 类的结构清晰:例如插件有接口、抽象,不同的协议不同的实现类。
  4. 数据接收等代码的处理有发布订阅的思想。
  5. 责任链模式:soul-web作为入口,通过WebHandler接收请求,请求处理放入chain中,插件实现请求处理。多个处理实现按序执行,请求接收无需关注链中的处理过程。
  6. 模版方法模式:抽象插件AbstractSoulPlugin有模版方法execute,定义execute需要做的事(比如:判断插件开关、选择器配置等),具体的插件实现,只需要关注正在的请求处理。
  7. 使用Optional代替非空判断
  8. WebFlux的使用(以前不认识它…所以对我来说也是亮点)
  9. 日志打得很规范

3. 疑问

1、直接中方法里面用断言是否合适?
例如非测试代码里有这种assert soulContext != null;

答:经了解,Java断言默认是关闭的(通过 -ea 开启),所以个人认为业务代码不应该使用断言做判断。

断言默认不执行测试:

    public static void main(String[] args) {
        boolean isOpen = false;
        // 如果开启了断言,会将isOpen的值改为true
        assert isOpen = true;
        // 打印是否开启了断言,如果为false,则没有启用断言
        System.out.println(isOpen);
    }

输出:

false

2、什么时候用策略模式不会显得过度设计?
策略模式可以消除if else,但是也带来来代码的复杂性。if else封装的好,似乎也没那么大问题?

答:基本上,现在代码里没必要用原生的策略模式,可以用Spring的注解(同一个接口不同的bean实现类,注入的时候选择不同的beanName,也可以配合工厂)解决问题。代码设计应该考虑如何使代码简洁轻量,而不是一定想要使用哪种设计模式。

其他

对于责任链但选型需要考虑场景,过多的节点、层次结构深也会给整个链带来复杂性,发展成树形就变得难以维护。另外,引入责任链对于问题的排查也会带来难度。这个是今后自己在做选型要注意的点。


[1] soul官网 https://dromara.org/zh/

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值