Soul网关源码阅读番外篇(一) HTTP参数请求错误
Soul网关源码阅读番外篇(一) HTTP参数请求错误
- 共同作者:石立 萧 *
简介
在Soul网关2.2.1版本源码阅读中,遇到了HTTP请求加上参数返回404的错误,此篇文章基于此进行探索
Bug复现
相关环境配置
首先把代码拉下来,然后切换到2.2.1版本,命令大致如下:
# 加速拉取
git clone https://github.com.cnpmjs.org/lw1243925457/soul.git# 切换到2.2.1版本
git fetch origin 2.2.1:2.2.1
git checkout 2.2.1
如果之前运行过Soul网关的,需要清理下数据库,这里删除原来的soul数据库,让2.2.1版本自己重新建立一个
# 使用docker启动mysql
docker run --name mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -d mysql:latest# 重启,需要删除soul数据库,然后让程序自己重建
docker restart mysql
# 使用命令登录,删除原来的数据库
docker exec -ti mysql mysql -u root -p
> drop database soul;
Soul——Admin启动
修改Soul-admin模块下的配置文件:soul-admin --> application-local.yml
修改mysql用户和密码: root root
修改链接配置:jdbc:mysql://localhost:3306/soul?useUnicode=true&characterEncoding=utf-8&allowPublicKeyRetrieval=true&&useSSL=false
启动soul-admin --> SoulAdminBootstrap
如果出现SelectorTypeEnum相关的错误,请切换到jdk8
启动Soul-Bootstrap
启动soul-bootstrap --> SoulBootstrapApplication
启动HTTP test
首先右键soul-test根目录下的pom.xml,选择 add as maven project,导入工程
可能会出现依赖错误,将其版本替换为2.2.1,大致如下:
<dependency><groupId>org.dromara</groupId><artifactId>soul-spring-boot-starter-client-springmvc</artifactId><version>2.2.1</version></dependency>
启动soul-test --> soul-test-http --> SoulTestHttpApplication
请求复现
访问管理界面: http://localhost:9095/ ,查看插件列表 --> divide ,表现正常
访问问题链接: http://localhost:9195/http/order/findById?id=1 ,可以看到出现了404
{"timestamp": "2021-01-18T02:18:19.557+0000","path": "/","status": 404,"error": "Not Found","message": null,"requestId": "84752141"
}
直接访问: http://localhost:8187/order/findById?id=11 ,正常的
{"id": "11","name": "hello world findById"
}
OK,到这问题基本复现,下面开始debug
源码Debug
查看日志进行切入
根据老哥的提示,我们也看到了这个问题请求的相关日志,大致如下
o.d.soul.plugin.base.AbstractSoulPlugin : divide selector success match , selector name :/http
o.d.soul.plugin.base.AbstractSoulPlugin : divide rule success match ,rule name :/http/order/findById
o.d.s.plugin.httpclient.WebClientPlugin : you request,The resulting urlPath is :http://192.168.101.104:8187?id=1111
最后一句urlpath非常的诡异,完整路径不对。我们就直接看下这个类: WebClientPlugin
# WebClientPluginpublic Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);assert soulContext != null;# 在这里debug看到取出来的路径是: http://192.168.101.104:8187?id=1111String urlPath = exchange.getAttribute(Constants.HTTP_URL);if (StringUtils.isEmpty(urlPath)) {Object error = SoulResultWarp.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);return WebFluxResultUtils.result(exchange, error);}long timeout = (long) Optional.ofNullable(exchange.getAttribute(Constants.HTTP_TIME_OUT)).orElse(3000L);log.info("you request,The resulting urlPath is :{}", urlPath);HttpMethod method = HttpMethod.valueOf(exchange.getRequest().getMethodValue());WebClient.RequestBodySpec requestBodySpec = webClient.method(method).uri(urlPath);return handleRequestBody(requestBodySpec, exchange, timeout, chain);}
在上面这个类中,可以看到就是单纯取路径,我们需要跟踪这个路径的来源
Divide查看
在前面几篇分析中,我们知道divide plugin 是进行路由配置,并写入真实路径到exchange中的,我们去 DividePlugin 看看
# DividePluginprotected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {final SoulContext soulContext = exchange.getAttribute(Constants.CONTEXT);assert soulContext != null;final DivideRuleHandle ruleHandle = GsonUtils.getInstance().fromJson(rule.getHandle(), DivideRuleHandle.class);final List<DivideUpstream> upstreamList = UpstreamCacheManager.getInstance().findUpstreamListBySelectorId(selector.getId());if (CollectionUtils.isEmpty(upstreamList)) {LOGGER.error("divide upstream configuration error:{}", rule.toString());Object error = SoulResultWarp.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);return WebFluxResultUtils.result(exchange, error);}final String ip = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();DivideUpstream divideUpstream = LoadBalanceUtils.selector(upstreamList, ruleHandle.getLoadBalance(), ip);if (Objects.isNull(divideUpstream)) {LOGGER.error("divide has no upstream");Object error = SoulResultWarp.error(SoulResultEnum.CANNOT_FIND_URL.getCode(), SoulResultEnum.CANNOT_FIND_URL.getMsg(), null);return WebFluxResultUtils.result(exchange, error);}//设置一下 http url : http://192.168.101.104:8187String domain = buildDomain(divideUpstream);// 在这设置realURL,进去看看这个函数String realURL = buildRealURL(domain, soulContext, exchange);// 放入exchange中exchange.getAttributes().put(Constants.HTTP_URL, realURL);exchange.getAttributes().put(Constants.HTTP_TIME_OUT, ruleHandle.getTimeout());return chain.execute(exchange);}private String buildRealURL(final String domain, final SoulContext soulContext, final ServerWebExchange exchange) {String path = domain;// 在这取url,但通过debug发现,它确实是nullfinal String rewriteURI = (String) exchange.getAttributes().get(Constants.REWRITE_URI);if (StringUtils.isNoneBlank(rewriteURI)) {path = path + rewriteURI;} else {// 然后又进到这进行取,发现也是nullfinal String realUrl = soulContext.getRealUrl();if (StringUtils.isNoneBlank(realUrl)) {path = path + realUrl;}}String query = exchange.getRequest().getURI().getQuery();if (StringUtils.isNoneBlank(query)) {return path + "?" + query;}return path;}
在上面的分析中,发现取出来的都是null,而且没有看到url的设置之类的操作,divide竟然也是单纯的取值
URL设置探索
那我们需要继续探索url的是怎么设置进去的,通过上面的分析,目前有两者设置url的方式,如下面两段代码:
final String rewriteURI = (String) exchange.getAttributes().get(Constants.REWRITE_URI);
final String realUrl = soulContext.getRealUrl();
exchange.getAttributes().get(Constants.REWRITE_URI) 方式探索
我们类比响应的设置方式,可以得到第一种URL设置的方式大致如下:
exchange.getAttributes().put(Constants.CLIENT_RESPONSE_RESULT_TYPE, ResultEnum.SUCCESS.getName());// 可以得到放Constants.REWRITE_URI的大致代码如下:
exchange.getAttributes().put(Constants.REWRITE_URI
然后使用全局搜索:ctrl+shift+r ,exchange.getAttributes().put(Constants.REWRITE_URI
# RewritePluginprotected Mono<Void> doExecute(final ServerWebExchange exchange, final SoulPluginChain chain, final SelectorData selector, final RuleData rule) {String handle = rule.getHandle();final RewriteHandle rewriteHandle = GsonUtils.getInstance().fromJson(handle, RewriteHandle.class);if (Objects.isNull(rewriteHandle) || StringUtils.isBlank(rewriteHandle.getRewriteURI())) {log.error("uri rewrite rule can not configuration:{}", handle);return chain.execute(exchange);}exchange.getAttributes().put(Constants.REWRITE_URI, rewriteHandle.getRewriteURI());return chain.execute(exchange);}
搜索到唯一一处有这个代码的类: RewritePlugin ,然后我们打断点,然后并不能进入这个逻辑,查看控制台,它是关闭的。那就先放着,看第二种设置方式
soulContext.getRealUrl() 的设置探索
运用类比,我们可以猜测设置的代码应该是: soulContext.setRealUrl
我们进行搜索,也成功的找到了唯一的一处代码,在类 DefaultSoulContextBuilder 中,大致如下:
# DefaultSoulContextBuilderprivate void setSoulContextByHttp(final SoulContext soulContext, final String path) {String contextPath = "/";String[] splitList = StringUtils.split(path, "/");if (splitList.length != 0) {contextPath = contextPath.concat(splitList[0]);}String realUrl = path.substring(contextPath.length());soulContext.setContextPath(contextPath);soulContext.setModule(contextPath);soulContext.setMethod(realUrl);// 设置urlsoulContext.setRealUrl(realUrl);}
我们在这个函数上打上断点,然而非常不幸的是,也没有进入。瞬间头上???????,这是怎么肥事啊,都没设置
不抛弃不放弃,咋继续。看到realURL是从path来的,我们继续往上追求其来源,发现调用的是同一个类的下面这个函数 transform ,再上一层是 build
# DefaultSoulContextBuilderprivate SoulContext transform(final ServerHttpRequest request, final MetaData metaData) {final String appKey = request.getHeaders().getFirst(Constants.APP_KEY);final String sign = request.getHeaders().getFirst(Constants.SIGN);final String timestamp = request.getHeaders().getFirst(Constants.TIMESTAMP);SoulContext soulContext = new SoulContext();String path = request.getURI().getPath();soulContext.setPath(path);if (Objects.nonNull(metaData) && metaData.getEnabled()) {if (RpcTypeEnum.SPRING_CLOUD.getName().equals(metaData.getRpcType())) {setSoulContextByHttp(soulContext, path);soulContext.setRpcType(metaData.getRpcType());} else {setSoulContextByDubbo(soulContext, metaData);}} else {setSoulContextByHttp(soulContext, path);soulContext.setRpcType(RpcTypeEnum.HTTP.getName());}soulContext.setAppKey(appKey);soulContext.setSign(sign);soulContext.setTimestamp(timestamp);soulContext.setStartDateTime(LocalDateTime.now());Optional.ofNullable(request.getMethod()).ifPresent(httpMethod -> soulContext.setHttpMethod(httpMethod.name()));return soulContext;}public SoulContext build(final ServerWebExchange exchange) {final ServerHttpRequest request = exchange.getRequest();String path = request.getURI().getPath();MetaData metaData = MetaDataCache.getInstance().obtain(path);if (Objects.nonNull(metaData) && metaData.getEnabled()) {exchange.getAttributes().put(Constants.META_DATA, metaData);}return transform(request, metaData);}
在build函数上打上断点,感谢老天,成功进入,通过调用栈发现,竟然是熟悉的 GlobalPlugin 进行调用的
# GlobalPluginpublic Mono<Void> execute(final ServerWebExchange exchange, final SoulPluginChain chain) {final ServerHttpRequest request = exchange.getRequest();final HttpHeaders headers = request.getHeaders();final String upgrade = headers.getFirst("Upgrade");SoulContext soulContext;if (StringUtils.isBlank(upgrade) || !"websocket".equals(upgrade)) {soulContext = builder.build(exchange);} else {final MultiValueMap<String, String> queryParams = request.getQueryParams();soulContext = transformMap(queryParams);}exchange.getAttributes().put(Constants.CONTEXT, soulContext);return chain.execute(exchange);}
在下面的函数打上端口,逐步debug。在下面注释的地方可以看到:我们的是HTTP请求,但竟然走到Dubbo的逻辑里面去,这非常的不对劲
private SoulContext transform(final ServerHttpRequest request, final MetaData metaData) {// http://127.0.0.1:9195/http/order/findById?id=1111final String appKey = request.getHeaders().getFirst(Constants.APP_KEY);final String sign = request.getHeaders().getFirst(Constants.SIGN);final String timestamp = request.getHeaders().getFirst(Constants.TIMESTAMP);SoulContext soulContext = new SoulContext();String path = request.getURI().getPath();soulContext.setPath(path);// 下面这个就神了,判断直接进到了setSoulContextByDubboif (Objects.nonNull(metaData) && metaData.getEnabled()) {if (RpcTypeEnum.SPRING_CLOUD.getName().equals(metaData.getRpcType())) {setSoulContextByHttp(soulContext, path);soulContext.setRpcType(metaData.getRpcType());} else {// 应该是进到HTTP的,估计就这出错了setSoulContextByDubbo(soulContext, metaData);}} else {setSoulContextByHttp(soulContext, path);soulContext.setRpcType(RpcTypeEnum.HTTP.getName());}soulContext.setAppKey(appKey);soulContext.setSign(sign);soulContext.setTimestamp(timestamp);soulContext.setStartDateTime(LocalDateTime.now());Optional.ofNullable(request.getMethod()).ifPresent(httpMethod -> soulContext.setHttpMethod(httpMethod.name()));return soulContext;}
我们使用下面的diff工具,看看最新版本的代码和目前版本有什么区别:
- Diffinity – 轻量级文件对比比较工具Windows
通过上图我们可以发现,最新版本中进行了更严谨的判断,并将默认的请求类型设置为了HTTP,这样再新版本代码中,就能走HTTP的处理逻辑
我们将代码修改一下,将HTTP设置为默认处理,代码大致如下:
private SoulContext transform(final ServerHttpRequest request, final MetaData metaData) {final String appKey = request.getHeaders().getFirst(Constants.APP_KEY);final String sign = request.getHeaders().getFirst(Constants.SIGN);final String timestamp = request.getHeaders().getFirst(Constants.TIMESTAMP);SoulContext soulContext = new SoulContext();String path = request.getURI().getPath();soulContext.setPath(path);if (Objects.nonNull(metaData) && metaData.getEnabled()) {if (RpcTypeEnum.SPRING_CLOUD.getName().equals(metaData.getRpcType())) {setSoulContextByHttp(soulContext, path);soulContext.setRpcType(metaData.getRpcType());} else if (RpcTypeEnum.DUBBO.getName().equals(metaData.getRpcType())) {setSoulContextByDubbo(soulContext, metaData);} else {setSoulContextByHttp(soulContext, path);soulContext.setRpcType(RpcTypeEnum.HTTP.getName());}} else {setSoulContextByHttp(soulContext, path);soulContext.setRpcType(RpcTypeEnum.HTTP.getName());}soulContext.setAppKey(appKey);soulContext.setSign(sign);soulContext.setTimestamp(timestamp);soulContext.setStartDateTime(LocalDateTime.now());Optional.ofNullable(request.getMethod()).ifPresent(httpMethod -> soulContext.setHttpMethod(httpMethod.name()));return soulContext;}
重启,发送请求: http://127.0.0.1:9195/http/order/findById?id=1111 ,OK,非常感人的成功了
{"id": "1111","name": "hello world findById"
}
到这,我们成功的定位并修复了这个错误(虽然没有啥用,但开心啊)
总结
本篇文章中对Soul网关2.2.1版本中HTTP请求出现404的错误进行了详细的分析
通过上面的分析可以看出,在2.2.1中,不是Spring cloud的HTTP请求,都会发生错误,这个bug还是有点厉害的
还认识到了GlobalPlugin这个插件的重要作用,不仅设置了类型,还设置了真实的后端服务器路径,可以说这个插件很核心。rewrite插件也有设置路径这个功能
又有了新的认识,更新下我们请求处理图:
Soul网关源码分析文章列表
Github
- Soul 源码阅读(一) 概览
- Soul 源码阅读(二)代码初步运行
- Soul 源码阅读(三)HTTP请求处理概览
- Soul 网关源码阅读(四)Dubbo请求概览
- Soul网关源码阅读(五)请求类型探索
- Soul 网关源码阅读(六)Sofa请求处理概览
掘金
- Soul 网关源码阅读(一) 概览 #掘金文章# https://juejin.cn/post/6917864624423436296
- Soul 网关源码阅读(二)代码初步运行 #掘金文章# https://juejin.cn/post/6917865804121767944
- Soul 网关源码阅读(三)请求处理概览 #掘金文章# https://juejin.cn/post/6917866538712334343
- Soul 网关源码阅读(四)Dubbo请求概览 #掘金文章# https://juejin.cn/post/6917867369909977102
- Soul网关源码阅读(五)请求类型探索 #掘金文章# https://juejin.cn/post/6918575905962983438
- Soul 网关源码阅读(六)Sofa请求处理概览 #掘金文章# https://juejin.cn/post/6918736260467015693
Soul网关源码阅读番外篇(一) HTTP参数请求错误相关推荐
- Soul网关源码阅读(十)自定义简单插件编写
Soul网关源码阅读(十)自定义简单插件编写 简介 综合前面所分析的插件处理流程相关知识,此次我们来编写自定义的插件:统计请求在插件链中的经历时长 编写准备 首先我们先探究一下,一个P ...
- Soul网关源码阅读(九)插件配置加载初探
Soul网关源码阅读(九)插件配置加载初探 简介 今日来探索一下插件的初始化,及相关的配置的加载 源码Debug 插件初始化 首先来到我们非常熟悉的插件链调用的类: SoulWebHa ...
- Soul网关源码阅读(八)路由匹配初探
Soul网关源码阅读(八)路由匹配初探 简介 今日看看路由的匹配相关代码,查看HTTP的DividePlugin匹配 示例运行 使用HTTP的示例,运行Soul-Admin,Sou ...
- Soul网关源码阅读(七)限流插件初探
Soul网关源码阅读(七)限流插件初探 简介 前面的文章中对处理流程探索的差不多了,今天来探索下限流插件:resilience4j 示例运行 环境配置 启动下MySQL和redis d ...
- Soul 网关源码阅读(六)Sofa请求处理概览
Soul 网关源码阅读(六)Sofa请求处理概览 简介 今天来探索一下Sofa请求处理流程,看看和前面的HTTP.Dubbo有什么异同 Sofa示例运行 PS:如果请求加上参数运行不成功,请更 ...
- Soul网关源码阅读(六)请求类型探索
Soul网关源码阅读(六)请求类型探索 简介 在上几篇文章中分析了请求的处理流程,HTTP和RPC请求处理是互斥的,通过请求类型来判断,这篇文章来探索下请求类型的前世今生 源码分析 通 ...
- Soul 网关源码阅读(四)Dubbo请求概览
Soul 网关源码阅读(四)Dubbo请求概览 简介 本次启动一个dubbo服务示例,初步探索Soul网关源码的Dubbo请求处理流程 示例运行 环境配置 在Soul源码clone下来 ...
- Soul 网关源码阅读(三)请求处理概览
Soul 源码阅读(三)请求处理概览 简介 基于上篇:Soul 源码阅读(二)代码初步运行的配置,这次debug下请求处理的大致路径,验证网关模型的路径 详细流程记录 查看运行日志,寻找切入点 ...
- Soul 网关源码阅读(二)代码初步运行
Soul 源码阅读(二)代码初步运行 简介 基于上篇:Soul 源码阅读(一) 概览,这部分跑一下Soul网关的示例 过程记录 现在我们可以根据地图,稍微探索一下周边,摸一摸 ...
最新文章
- 一次被僵尸网络病毒攻击的过程
- 织梦怎么改网站主页php,无忧主机教你修改织梦DedeCms网站首页为动态显示的方法...
- 数据库分库分表(持续更新中)
- Mysql 查询blob数据很慢_blob存入数据库很慢
- 药房管理(信息学奥赛一本通-T1075)
- python Scrapy 从零开始学习笔记(一)
- 《网站性能监测与优化》一2.4 软件服务应用网站
- (17)FPGA速度和面积互换原则
- jquery读取json文件跨域_跨域方法的若干种方式
- JS 日期工具类-基于yDate
- python中的counter函数_Python的 counter内置函数,统计文本中的单词数量
- 使用cmd命令创建vue(ivieiw)项目
- java中this关键字的作用
- 十个摸鱼,哦,不对,是炫酷(可以玩一整天)的网站!!!
- 小程序投标书_程序员接私活常用哪些平台?
- Unity3D怪物基本AI
- 常用的一些LDO芯片及使用
- 云原生 | Docker:基础篇
- Scrum立会报告+燃尽图(Beta阶段第二周第三次)
- AW9523B IIC驱动