【Soul源码探秘】插件链实现
引言
插件是 Soul 的灵魂。
Soul 使用了插件化设计思想,实现了插件的热插拔,且极易扩展。内置丰富的插件支持,鉴权,限流,熔断,防火墙等等。
Soul 是如何实现插件化设计的呢?
一切还得从插件链说起,本篇我们来探密 Soul 中插件链的实现。
从插件说起
Soul 中所有插件最终均继承自 SoulPlugin,其完整继承关系如下所示:
可以看到,Soul 的插件生态极其丰富,正是如此丰富的插件支撑起了 Soul 网关强大的扩展能力。
我们以常用的 DividePlugin 为例,分析插件内部所做工作。
DividePlugin 继承结构:
1、SoulPlugin 插件接口:
- execute 方法:处理方法,需要传入 exchange交换区 和 SoulPluginChain插件链
- getOrder 方法:取得序号,用作插件排序
- named 方法:获得插件名
- skip 方法:判断是否跳过本次处理
每次处理时,将先进行 skip 判断,不跳过则执行 excute 处理方法。
2、AbstractSoulPlugin 抽象插件:
重点关注 execute 方法,其核心代码如下:
if (pluginData.getEnable()){
// 获取插件数据
final PluginData pluginData = BaseDataCache.getInstance().obtainPluginData(pluginName);
// 获取选择器数据
final Collection<SelectorData> selectors = BaseDataCache.getInstance().obtainSelectorData(pluginName);
final SelectorData selectorData = matchSelector(exchange, selectors);
// 获取规则
final List<RuleData> rules = BaseDataCache.getInstance().obtainRuleData(selectorData.getId());
RuleData rule;
if (selectorData.getType() == SelectorTypeEnum.FULL_FLOW.getCode()) {
//get last
rule = rules.get(rules.size() - 1);
} else {
rule = matchRule(exchange, rules);
}
// 执行具体处理
return doExecute(exchange, chain, selectorData, rule);
}
// 继续执行后续插件处理
return chain.execute(exchange);
获取选择器数据和规则,然后传入 doExecute 方法进行具体处理,doExecute 方法为抽象方法,交由子类具体实现。
3、DividePlugin 插件:
重点关注 doExecute 方法,以下是核心代码:
// 获取网关上下文和规则处理器
final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);
// 获取上游列表
final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());
// 选择待分发的目标上游
final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);
// 设置 http url
String domain = buildDomain(divideUpstream);
String realURL = buildRealURL(domain, soulContext, exchange);
exchange.getAttributes().put(Constants.HTTP_URL, realURL);
// 设置 http timeout
exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());
exchange.getAttributes().put(Constants.HTTP_RETRY, ruleHandle.getRetry());
return chain.execute(exchange);
很明显,divide 插件只是完成目标上游服务的待分发,即根据选择器和规则找到对应服务,再通过负载均衡策略分配上游服务实例。
而调用上游服务的工作是由其他相应的 client 类插件完成。
插件链实现
借由插件链,Soul 将众多插件整合到一起进行统一调度处理。
插件链继承结构:
可以看到,Soul 中插件链 SoulPluginChain 仅有一个默认实现类 DefaultSoulPluginChain。
1、DefaultSoulPluginChain:
其持有通过构造方法传入的插件链,看看 execute 方法:
public Mono<Void> execute(final ServerWebExchange exchange) {
// 反应式编程语法:Mono.defer
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();
});
}
DefaultSoulPluginChain 是 SoulWebHandler 的内部类,看下 SoulWebHandler 的实现。
2、SoulWebHandler:
SoulWebHandler 是 web 请求处理的起点,在此创建并开始插件链的处理。
同 DefaultSoulPluginChain一样,SoulWebHandler 也是持有通过构造方法传入的插件链。
看看 handle 方法:
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)));
}
handle 方法负责插件链执行指标度量的采集,通过在 DefaultSoulPluginChain 执行时加订阅实现,DefaultSoulPluginChain 在此处完成初始化。
全局查找 SoulWebHandler 构造方法可以定位到 SoulConfiguration 的 soulWebHandler 方法,看下 SoulConfiguration 的实现。
3、SoulConfiguration:
SoulConfiguration 是 Soul 的核心配置类,负责自动装配网关所需的核心 bean 对象。
如装配 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);
}
注意此处的插件列表经过了一次重排,重排顺序参见 PluginEnum。
4、初始化 SoulWebHandler
soul-bootstrap启动的过程中,所有插件是怎么形成final ObjectProvider<List> plugins,然后初始化SoulWebHandler的呢?
SoulWebHandler 所在的配置类通过配置 @ComponentScan("org.dromara.soul"),通知 spring 扫描 org.dromara.soul 包。
同属 org.dromara.soul 包下所有插件的 starter项目中,在各自的spring.factories 文件里都指定了自动配置类。
因此,List会去收集在soul-bootstrap的 pom 文件里引入了相关依赖的插件,并以此形成插件链。
总结
本篇从单独的插件开始,到探究插件链的实现,基本理清了 Soul 中插件式设计的实现。
- 由 SoulConfiguration 自动装配 SoulWebHandler,但此时 SoulWebHandler 持有插件列表但未初始化插件链。
- 待调用 handle 方法处理请求时,才初始化插件链进入插件处理。
个人知识库
【Soul源码探秘】插件链实现的更多相关文章
- MyBatis 源码分析 - 插件机制
1.简介 一般情况下,开源框架都会提供插件或其他形式的拓展点,供开发者自行拓展.这样的好处是显而易见的,一是增加了框架的灵活性.二是开发者可以结合实际需求,对框架进行拓展,使其能够更好的工作.以 My ...
- 可视化工具gephi源码探秘(二)---导入netbeans
在上篇<可视化工具gephi源码探秘(一)>中主要介绍了如何将gephi的源码导入myeclipse中遇到的一些问题,此篇接着上篇而来,主要讲解当下通过myeclipse导入gephi源码 ...
- WeMall微商城源码报名插件Apply的主要源码
WeMall微信商城源码报名插件Apply,用于商城的签到系统,分享了部分比较重要的代码,供技术员学习参考 AdminController.class.php <?php namespace A ...
- WeMall微商城源码投票插件Vote的主要源码
WeMall微信商城源码投票插件Vote,用于商城的签到系统,分享了部分比较重要的代码,供技术员学习参考 AdminController.class.php <?php namespace Ad ...
- SpringMvc请求处理流程与源码探秘
流程梳理 dispatcherServlet作为前端控制器的主要作用就是接受请求与处理响应. 不过它不是传统意义上的servlet,它在接受到请求后采用转发的方式,将具体工作交给专业人士去做. 参与角 ...
- 可视化工具gephi源码探秘(二)
在上篇<可视化工具gephi源码探秘(一)>中主要介绍了如何将gephi的源码导入myeclipse中遇到的一些问题,此篇接着上篇而来,主要讲解当下通过myeclipse导入gephi源码 ...
- 34、[源码]-AOP原理-链式调用通知方法
34.[源码]-AOP原理-链式调用通知方法
- MyBatis 源码篇-插件模块
本章主要描述 MyBatis 插件模块的原理,从以下两点出发: MyBatis 是如何加载插件配置的? MyBatis 是如何实现用户使用自定义拦截器对 SQL 语句执行过程中的某一点进行拦截的? 示 ...
- Maven 依赖调解源码解析(二):如何调试 Maven 源码和插件源码
本文是系列文章<Maven 源码解析:依赖调解是如何实现的?>第二篇,主要介绍如何调试 Maven 源码和插件源码.系列文章总目录参见:https://www.cnblogs.com/xi ...
随机推荐
- Java基础进阶:学生管理系统数组方式分包源码实现,教师管理系统集合和数组两种方式源码实现,图书馆管理系统源码实现,现附重难点,代码实现源码,课堂笔记,课后扩展及答案
1.案例驱动模式 1.1案例驱动模式概述 (理解) 通过我们已掌握的知识点,先实现一个案例,然后找出这个案例中,存在的一些问题,在通过新知识点解决问题 1.2案例驱动模式的好处 (理解) 解决重复代码 ...
- Centos7路由设置
再添加路由时,很多时候都是采用命令行用route添加的.但是在机器重启后.就失效了.这里也是参考了几位博主的经验 作出以下记载 一:路由表常用设置 1.route命令路由表常用设置: //添加到主机的 ...
- 每天学习一点ES6(二)let 和 const
let 命令 let 和 var 差不多,只是限制了有效范围. 先定义后使用 不管是什么编程语言,不管语法是否允许,都要秉承先定义,然后再使用的习惯,这样不会出幺蛾子.以前JavaScript比较随意 ...
- 【Objective-C】1.oc点语法
在Java中,我们可以通过"对象名.成员变量名"来访问对象的公共成员变量,这个就称为"点语法".比如: 1.在Student类的第2行定义了一个公共的成员变量a ...
- IQueryable的简单封装
IQueryable的简单封装 前言 前两天在园子上看到一个问题 半年前我也考虑过这些问题,但由于这样那样的问题,没有尝试去解决. 后来公司用上了 abp vnext ,然后有一部分代码可以这样写 p ...
- [LeetCode]313. Super Ugly Number超级丑数,丑数系列看这一道就行了
丑数系列的题看这一道就可以了 /* 和ugly number2差不多,不过这次的质因子多了,所以用数组来表示质因子的target坐标 target坐标指的是这个质因子此次要乘的前任丑数是谁 */ pu ...
- JS 获取(期号、当前日期、本周第一天、最后一天及当前月第一、最后天函数)
JS 获取(期号.当前日期.本周第一天.最后一天及当前月第一.最后天函数 /** 2 * 获取当前月期号 3 * 返回格式: YYYY-mm 4 * / 5 function getCurrentMo ...
- javaweb登陆实例
1 <%@ page language="java" contentType="text/html; charset=UTF-8" 2 pageEncod ...
- stm32之can总线过滤器研究
stm32的can总线的配置如下: CAN_InitStructure.CAN_TTCM=DISABLE;//禁止时间触发通信模式 CAN_InitStructure.CAN_A ...
- 伯俊BOS2.0店铺收入对账功能设计
一.客户需求 通过导入银行POS机流水,将流水与ERP系统的零售付款数据进行对比,统计差异! 二.功能设计 1.新增"POS机号对应表单",用于维护POS机与erp店仓对应 2.新 ...