soul框架简单介绍与设计模式分析
1. 初识
1.1 查看官网文档了解
针对API的网关 – 可以为每个接口配置负载均衡、限流等功能
有三个角色:
- soul-admin 配置规则
- soul-gateway 网关 – 用户请求会打到这
- server 用户真实的服务 – 用户请求通过网关转发到这
流程:请求打到网关,依据用户的配置,转发到server。当然除了转发还有限流、负载均衡等~
1.2. 目标:
- 了解soul网关的实现原理
- 插件化的设计有没有什么玄机(现在看来就是不同的协议用的不同的module)
- 了解框架中使用的设计模式
- 分析框架的分层
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_MAP
、SELECTOR_MAP
、RULE_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:什么时候订阅的,什么时候会回调onSubscribe
、onSelectorSubscribe
等方法?
可以配置不同的数据同步方案,例如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. 亮点
- 代码module层次清晰。
- 类职责单一:例如数据同步例如soul-web中有很多过滤器,每个过滤器只做一件事(CrossFilter、ExcludeFilter、FileSizeFilter等)。
- 类的结构清晰:例如插件有接口、抽象,不同的协议不同的实现类。
- 数据接收等代码的处理有发布订阅的思想。
- 责任链模式:soul-web作为入口,通过WebHandler接收请求,请求处理放入chain中,插件实现请求处理。多个处理实现按序执行,请求接收无需关注链中的处理过程。
- 模版方法模式:抽象插件
AbstractSoulPlugin
有模版方法execute,定义execute需要做的事(比如:判断插件开关、选择器配置等),具体的插件实现,只需要关注正在的请求处理。 - 使用Optional代替非空判断
- WebFlux的使用(以前不认识它…所以对我来说也是亮点)
- 日志打得很规范
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/