Soul API 网关源码解析 03
目标
使用 soul 代理 dubbo 服务
dubbo 服务如何注册到网关的?
dubbo 插件是如何工作的?
理清 http --> 网关--> dubbo provider 整条链路经历了什么。
总结
一、使用 soul 代理 dubbo 服务
1、dubbo 服务接入网关
1.1 springboot 项目接入方式
1.1.1 依赖引入
alibaba dubbo 用户
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-client-alibaba-dubbo</artifactId>
<version>${last.version}</version>
</dependency>
apache dubbo 用户
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-spring-boot-starter-client-apache-dubbo</artifactId>
<version>${last.version}</version>
</dependency>
1.1.2 yml 配置
soul:
dubbo:
adminUrl: http://localhost:9095
contextPath: /dubbo
appName: dubbo
1.2 spring 项目接入方式
1.2.1 依赖引入
alibaba dubbo 用户
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-client-alibaba-dubbo</artifactId>
<version>${last.version}</version>
</dependency>
apache dubbo 用户
<dependency>
<groupId>org.dromara</groupId>
<artifactId>soul-client-apache-dubbo</artifactId>
<version>${last.version}</version>
</dependency>
1.2.2 xml 配置
alibaba dubbo 用户
<bean id ="alibabaDubboServiceBeanPostProcessor" class ="org.dromara.soul.client.alibaba.dubbo.AlibabaDubboServiceBeanPostProcessor">
<constructor-arg ref="dubboConfig"/>
</bean>
<bean id="dubboConfig" class="org.dromara.soul.client.dubbo.common.config.DubboConfig">
<property name="adminUrl" value="http://localhost:9095"/>
<property name="contextPath" value="/你的contextPath"/>
<property name="appName" value="你的名字"/>
</bean>
apache dubbo 用户
<bean id ="apacheDubboServiceBeanPostProcessor" class ="org.dromara.soul.client.apache.dubbo.ApacheDubboServiceBeanPostProcessor">
<constructor-arg ref="dubboConfig"/>
</bean>
<bean id="dubboConfig" class="org.dromara.soul.client.dubbo.common.config.DubboConfig">
<property name="adminUrl" value="http://localhost:9095"/>
<property name="contextPath" value="/你的contextPath"/>
<property name="appName" value="你的名字"/>
</bean>
2、配置 dubbo 插件
启动 soul-admin
和 soul-bootstrap
,前往后台插件管理页面
启用 dubbo 插件,并配置注册中心 ip + 端口
3、注册 dubbo 服务到网关
dubbo 服务实现类的方法上加上 @SoulDubboClient
注解
@Override
@SoulDubboClient(path = "/findById", desc = "Query by Id")
public DubboTest findById(final String id) {
DubboTest dubboTest = new DubboTest();
dubboTest.setId(id);
dubboTest.setName("hello world Soul Alibaba Dubbo, findById");
return dubboTest;
}
启动 dubbo 提供者项目,日志输出 dubbo client register success
注册成功
4、http 方式请求 dubbo 服务
通过 http post 方式请求网关,通过 body 传递 json 格式参数(contextPath=/dubbo)
dubbo 服务访问成功,同时 soul-bootstrap
输出匹配日志
dubbo selector success match , selector name :/dubbo
dubbo selector success match , selector name :/dubbo/findById
二、dubbo 服务如何注册到网关?
alibaba dubbo 与 apache dubbo 差异较小,alibaba dubbo 的注册流程弄明白后,apache dubbo 也自然清楚了。
从控制台日志 dubbo client register success
定位到 RegisterUtils 类,在打印注册日志的地方加上断点
重启项目,可以看到成功进入断点,在此处调用 OkHttpTools.post 进行服务注册。
注意此处是注册到 soul-admin
,再由网关和 soul-admin
的数据同步机制同步到网关内存。
注册的元数据怎么拿到的呢?
从堆栈定位到 doRegister 方法的调用方 AlibabaDubboServiceBeanPostProcessor.handler
private void handler(final ServiceBean<?> serviceBean) {
Class<?> clazz = serviceBean.getRef().getClass();
// 如果是 cglib 代理,则取带有泛型的父类
if (ClassUtils.isCglibProxyClass(clazz)) {
String superClassName = clazz.getGenericSuperclass().getTypeName();
try {
clazz = Class.forName(superClassName);
} catch (ClassNotFoundException e) {
log.error(String.format("class not found: %s", superClassName));
return;
}
}
final Method[] methods = ReflectionUtils.getUniqueDeclaredMethods(clazz);
for (Method method : methods) {
// 如果方法上有 SoulDubboClient 注解,则调用上面的 doRegister 方法注册元数据信息
SoulDubboClient soulDubboClient = method.getAnnotation(SoulDubboClient.class);
if (Objects.nonNull(soulDubboClient)) {
RegisterUtils.doRegister(buildJsonParams(serviceBean, soulDubboClient, method), url, RpcTypeEnum.DUBBO);
}
}
}
handler 方法通过识别 serviceBean 上的 SoulDubboClient
来判断需要注册哪些服务的元数据。
注意到这里使用 buildJsonParams 来构造元数据,其内容如下:
private String buildJsonParams(final ServiceBean<?> serviceBean, final SoulDubboClient soulDubboClient, final Method method) {
// 从 dubboConfig 取 appName,若未配置则使用 applicationName
String appName = dubboConfig.getAppName();
if (StringUtils.isEmpty(appName)) {
appName = serviceBean.getApplication().getName();
}
String path = dubboConfig.getContextPath() + soulDubboClient.path();
String desc = soulDubboClient.desc();
String serviceName = serviceBean.getInterface();
String configRuleName = soulDubboClient.ruleName();
String ruleName = ("".equals(configRuleName)) ? path : configRuleName;
String methodName = method.getName();
Class<?>[] parameterTypesClazz = method.getParameterTypes();
// 拼装方法参数类型名
String parameterTypes = Arrays.stream(parameterTypesClazz).map(Class::getName)
.collect(Collectors.joining(","));
// 构造元数据并序列化为 json 串
MetaDataDTO metaDataDTO = MetaDataDTO.builder()
.appName(appName)
.serviceName(serviceName)
.methodName(methodName)
.contextPath(dubboConfig.getContextPath())
.path(path)
.ruleName(ruleName)
.pathDesc(desc)
.parameterTypes(parameterTypes)
.rpcExt(buildRpcExt(serviceBean))
.rpcType("dubbo")
.enabled(soulDubboClient.enabled())
.build();
return OkHttpTools.getInstance().getGson().toJson(metaDataDTO);
}
继续回到上面的handler,通过堆栈找到其调用方 AlibabaDubboServiceBeanPostProcessor.handler
@Override
public void onApplicationEvent(final ContextRefreshedEvent contextRefreshedEvent) {
if (Objects.nonNull(contextRefreshedEvent.getApplicationContext().getParent())) {
return;
}
// Fix bug(https://github.com/dromara/soul/issues/415), upload dubbo metadata on ContextRefreshedEvent
Map<String, ServiceBean> serviceBean = contextRefreshedEvent.getApplicationContext().getBeansOfType(ServiceBean.class);
for (Map.Entry<String, ServiceBean> entry : serviceBean.entrySet()) {
executorService.execute(() -> handler(entry.getValue()));
}
}
通过实现 ApplicationListener<ContextRefreshedEvent>
接口监听容器刷新事件,容器刷新后取到 ServiceBean 调用 handler 进行处理。
至此,alibaba dubbo 服务注册到网关的流程梳理清楚:
1)AlibabaDubboServiceBeanPostProcessor 监听到容器刷新后,从 spring 上下文取出 ServiceBean 调用 handler 进行处理
2)handler 拿到 ServiceBean 的接口或带泛型父类带 @SoulDubboClient
的方法,调用 RegisterUtils.doRegister
进行注册
3)RegisterUtils.doRegister
使用 OkHttpTools 将元数据 post 到 soul-admin
落库
4)soul-admin
再将元数据同步到网关内存
三、dubbo 插件如何工作的?
在此就不展开插件的统一工作流程了,只聚焦于 dubbo 插件本身的工作内容。
打开 soul-plugin-alibaba-dubbo
插件工程,定位到 AlibabaDubboPlugin.doExecute
@Override
protected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {
String body = exchange.getAttribute(Constants.DUBBO_PARAMS);
SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);
assert soulContext != null;
MetaData metaData = exchange.getAttribute(Constants.META_DATA);
// 元数据检查未通过处理
if (!checkMetaData(metaData)) {
assert metaData != null;
log.error(" path is :{}, meta data have error.... {}", soulContext.getPath(), metaData.toString());
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
Object error = SoulResultWrap.error(SoulResultEnum.META_DATA_ERROR.getCode(), SoulResultEnum.META_DATA_ERROR.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
// 请求体缺失处理
if (StringUtils.isNoneBlank(metaData.getParameterTypes()) && StringUtils.isBlank(body)) {
exchange.getResponse().setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
Object error = SoulResultWrap.error(SoulResultEnum.DUBBO_HAVE_BODY_PARAM.getCode(), SoulResultEnum.DUBBO_HAVE_BODY_PARAM.getMsg(), null);
return WebFluxResultUtils.result(exchange, error);
}
// dubbo 泛化调用
Object result = alibabaDubboProxyService.genericInvoker(body, metaData);
if (Objects.nonNull(result)) {
exchange.getAttributes().put(Constants.DUBBO_RPC_RESULT, result);
} else {
exchange.getAttributes().put(Constants.DUBBO_RPC_RESULT, Constants.DUBBO_RPC_RESULT_EMPTY);
}
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName());
return chain.execute(exchange);
}
可以看到,进入 dubbo 插件处理以后,最核心的是从 exchange 拿到元数据和请求体后进行 dubbo 泛化调用。
进入 AlibabaDubboProxyService.genericInvoker
public Object genericInvoker(final String body, final MetaData metaData) throws SoulException {
// 拿到 ReferenceConfig
ReferenceConfig<GenericService> reference = ApplicationConfigCache.getInstance().get(metaData.getPath());
if (Objects.isNull(reference) || StringUtils.isEmpty(reference.getInterface())) {
ApplicationConfigCache.getInstance().invalidate(metaData.getPath());
reference = ApplicationConfigCache.getInstance().initRef(metaData);
}
// 组织方法名、参数类型和参数,再交由 genericService 进行泛化调用
GenericService genericService = reference.get();
try {
Pair<String[], Object[]> pair;
if (ParamCheckUtils.dubboBodyIsEmpty(body)) {
pair = new ImmutablePair<>(new String[]{}, new Object[]{});
} else {
pair = dubboParamResolveService.buildParameter(body, metaData.getParameterTypes());
}
return genericService.$invoke(metaData.getMethodName(), pair.getLeft(), pair.getRight());
} catch (GenericException e) {
log.error("dubbo invoker have exception", e);
throw new SoulException(e.getExceptionMessage());
}
}
可以看到,AlibabaDubboProxyService 只是封装了 alibaba 的 GenericService,先组织方法名、参数类型和参数,再交由 genericService 进行泛化调用。
四、总结
本篇从使用侧先梳理 dubbo 服务接入流程,再从 dubbo 服务注册到网关以及 dubbo 插件的工作内容进行分析,最终将 soul 代理 dubbo 服务的整个脉络串起来。
1)客户端发布 dubbo 服务,同时注册 dubbo 服务元数据到 soul-admin
2) soul-admin
同步元数据到 网关 jvm 内存
3)客户向网关发起 http 请求,访问目标为后端 dubbo 服务
4)网关拿到 requestBody 和内存中的元数据,解析成方法名、参数类型和参数值后发起 dubbo 泛化调用
5)客户端响应 dubbo 泛化调用并回吐响应报文
下一篇,将分析 soul 如何代理 sofa-rpc 服务。
Soul API 网关源码解析 03的更多相关文章
- Soul API 网关源码解析 02
如何读开源项目:对着文档跑demo,对着demo看代码,懂一点就开始试,有问题了问社区. 今日目标: 1.运行examples下面的 http服务 2.学习文档,结合divde插件,发起http请求s ...
- zuul网关源码解析
zuul网关源码解析 zuul请求的生命周期 ZuulServlet ZuulServlet定义了对zuul整个过程的处理,如下: public void service(javax.servlet. ...
- Spring中AOP相关的API及源码解析
Spring中AOP相关的API及源码解析 本系列文章: 读源码,我们可以从第一行读起 你知道Spring是怎么解析配置类的吗? 配置类为什么要添加@Configuration注解? 谈谈Spring ...
- Soul 网关 Nacos 数据同步源码解析
学习目标: 学习Soul 网关 Nacos 数据同步源码解析 学习内容: 环境配置 Soul 网关 Nacos 数据同步基本概念 源码分析 学习时间:2020年1月28号 早7点 学习产出: 环境配置 ...
- Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例
概要 上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具体实现类进行讲解:首先,讲解List,而List中ArrayList又最为常用.因此,本章我们讲解Arra ...
- Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例
java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java ...
- 【转】Java 集合系列03之 ArrayList详细介绍(源码解析)和使用示例
原文网址:http://www.cnblogs.com/skywang12345/p/3308556.html 上一章,我们学习了Collection的架构.这一章开始,我们对Collection的具 ...
- sqler sql 转rest api 源码解析(一)应用的启动入口
sqler sql 转rest api 的源码还是比较简单的,没有比较复杂的设计,大部分都是基于开源 模块实现的. 说明: 当前的版本为2.0,代码使用go mod 进行包管理,如果本地运行注意gol ...
- 机器学习实战(Machine Learning in Action)学习笔记————03.决策树原理、源码解析及测试
机器学习实战(Machine Learning in Action)学习笔记————03.决策树原理.源码解析及测试 关键字:决策树.python.源码解析.测试作者:米仓山下时间:2018-10-2 ...
随机推荐
- [实用指南]如何使您的旧代码库(遗留代码)符合MISRA C 2012编码规范?
重用旧代码是现实,但是在安全关键型软件项目中重用旧代码并实现MISRA C 2012的完全合规性是艰巨的任务. 最初的MISRA原则是为了在开发代码时应用而创建的,即使文档本身也有警告: " ...
- 微信小说分销系统设计之使用百度Echarts地图统计功能统计微信粉丝地域分布情况
/** 转载请保留博客园原地址以及版权声明,请勿恶意修改,本博客中的内容均属于技术交流,请勿用于商业用途,谢谢配合 * 作者:杨浩瑞 QQ:1420213383 独立博客:http://www. ...
- 基于frp的内网穿透实例2-通过自定义域名访问部署于内网的 web 服务
原文地址:https://wuter.cn/1837.html/ 一.想要实现的功能 1.将部署在自己电脑上的网站用于公网访问. 2.将未备案域名解析至国内服务器(即我宿舍的老母鸡上). 二.服务端配 ...
- 前置机器学习(五):30分钟掌握常用Matplitlib用法
Matplotlib 是建立在NumPy基础之上的Python绘图库,是在机器学习中用于数据可视化的工具. 我们在前面的文章讲过NumPy的用法,这里我们就不展开讨论NumPy的相关知识了. Matp ...
- 配置简单的拦截器java中
springMVC.xml文件中==== <!-- 拦截器 --> <mvc:interceptors> <mvc:interceptor> <mvc:map ...
- win10 设置文件夹别名、修改文件夹图标、修改文件夹别名、英文目录和中文目录、设置文件夹中文名称、快捷访问显示设置中文
最近在设置文件夹的时候发现个有趣的事情: 系统路径 C:\Users\Administrator 内的文件夹不仅有图标还显示中文名称,但是打开路径的时候显示的却是英文,这就激发了我的探索欲,究竟是为 ...
- Ubuntu 20.04 安装和编译poco 1.10.1
1.首先安装其openssl其它依赖库,打开终端,使用root账户(sudo su),完成以下库的安装 //安装odbc相关库 apt-get install unixodbc apt-get ins ...
- Android多线程消息处理机制
(1)主线程和ANR 主线程:UI线程,界面的修改只能在主线程中,其它线程对界面进行修改会造成异常.这样就解决了多线程竞争UI资源的问题. 一旦主线程的代码阻塞,界面将无法响应,这种行为就是Appli ...
- securefx 系统中不到指定文件 (转中文)
如何处理上传工具SecureFX中的中文乱码 工具/原料 SecureFX centos7 方法/步骤 转百度知道 https://jingyan.baidu.com/article/eae07 ...
- python virtualenv 基本使用
下载 pip install virtualenv 校验是否成功 virtualenv --version 使用 创建env环境 要写一个新项目,使用env先创建环境 cd xx\xx\xx\ # 进 ...