Feign踩坑源码分析 -- 请求参数分号变逗号

源码仓库:Gitee·GitHub

一.案例

1.1.Post请求:

http://localhost:8250/xx/task/test json格式参数: { "string": "a;b;c;d" }

1.2.controller代码:

@Autowired DataSourceClientService dataSourceClientService; @RequestMapping("/test") @ResponseBody public void test(@RequestBody String string) { System.out.println(string); String result = dataSourceClientService.test(string); System.out.println(result); }

1.3.feign代码:

@FeignClient( value = "zz") public interface DataSourceClientService { @RequestMapping(value = "/dataSource/test",method = RequestMethod.POST,produces = "text/plain;charset=UTF-8") String test(@RequestParam("str") String str); }

1.4服务提供方代码:

@RequestMapping("/test") @ResponseBody public String test(@RequestParam String str) { System.out.println(str); String result = "success;"; return result; }

1.5发起请求后控制台打印结果:

请求方控制台: 用户[null]开始调用web接口:http://localhost:8250/xx/task/test { "string": "a;b;c;d" } 服务提供方控制台: 用户[null]开始调用web接口:http://localhost:8247/zz/dataSource/test { "string": "a,b,c,d" }

二.解决办法

2.1.在请求方对参数进行编码:

string = UriUtils.encode(string, StandardCharsets.UTF_8); String test = dataSourceClientService.test(string);

2.2.服务提供方@RequestParam改成@RequestBody;

三.分析

3.1.需求

服务提供方需要的字符串是包含“;”而不是“,”,因为实际传递的是一个JSONObject的字符串,导致JSONUtil.parse转换成对象失败,导致业务失败;

3.2请求方法参数@RequestParam而传参不加密

3.2.1 请求方源码探究

通过一步步debug代码调试,发现调用 dataSourceClientService.test(string) 是jdk动态代理,调用链接到

feign.ReflectiveFeign.FeignInvocationHandler#invoke

-->feign.SynchronousMethodHandler#invoke

-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create

-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#resolve

-->feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>)

-->feign.template.QueryTemplate#expand

-->feign.template.QueryTemplate#queryString

static final String COLLECTION_DELIMITER = ";"; private String queryString(String name, String values) { if (this.pure) { return name; } /* covert the comma separated values into a value query string */ List<String> resolved = Arrays.stream(values.split(COLLECTION_DELIMITER)) .filter(Objects::nonNull) .filter(s -> !UNDEF.equalsIgnoreCase(s)) .collect(Collectors.toList()); if (!resolved.isEmpty()) { return this.collectionFormat.join(name, resolved, this.getCharset()).toString(); } /* nothing to return, all values are unresolved */ return null; }

从上面可以看到,一个参数字符串“a;b;c;d”被分割处理成了一个数组,然后重新组合成字符串(Collection)传递给服务提供方,服务提供方接收参数是一个string字符串,会用逗号给拼接成字符串。

也就是说,feign预设了使用者如果是用传参带了分号过来的,会认为你传的是Collection,而不是String。

3.2.1 服务提供方源码探究

下面是服务提供方接收的参数详情:

org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument

从上面可以看出,请求解析出来是一个字符串数组,在

org.springframework.core.convert.support.CollectionToStringConverter#convert 中用逗号拼接起来:

3.2.1 调用服务提供方方法前参数编码:

当没有编码前,org.springframework.core.convert.support.GenericConversionService#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor) 调用链进入的是org.springframework.core.convert.support.CollectionToStringConverter#convert 中用逗号拼接起来,因为参数类型是字符串数组

当编码后,进入的是org.springframework.core.convert.support.GenericConversionService.NoOpConverter#convert,直接返回字符串:

参数值的获取在org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName 中

参数的解码方法在org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset)中进入

在org.apache.tomcat.util.http.Parameters#urlDecode进行解码

前面的是参数进行编码后解码,因为编码成一个字符串,所以解码的也是字符串,当没有进行编码,传递的会认为是一个collection,所以会遍历解码:

然后加入到数组中:

3.3请求方法参数改成@RequestBody

3.3.1服务提供方

当请求方法是@RequestBody时,获取参数在org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor#resolveArgument:

直接从httprequest请求中获取body参数值:org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(org.springframework.http.HttpInputMessage, org.springframework.core.MethodParameter, java.lang.reflect.Type)

3.3.2请求方

参数直接封装到body中:

feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create

--> feign.ReflectiveFeign.BuildEncodedTemplateFromArgs#resolve

对比3.2.1流程会发现,body不会进行字符串的切割,当然拉,也跟参数的请求类型不一致有关,一个是query,一个是body,下面是3.2.1流程

-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#create

-->feign.ReflectiveFeign.BuildTemplateByResolvingArgs#resolve

-->feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>)

-->feign.template.QueryTemplate#expand

-->feign.template.QueryTemplate#queryString

四.扩展

4.1.get请求

get请求的参数类型是query,所以走的是3.2.1流程,所以string的传参中包含“;”也会被转换成“,”;

@FeignClient( value = "zz") public interface DataSourceClientService { @RequestMapping(value = "/dataSource/testGet",method = RequestMethod.GET,produces = "text/plain;charset=UTF-8") String testGet(@RequestParam("str") String str); } @RequestMapping(value = "/testGet") @ResponseBody public String testGet(String str) { System.out.println(str); String result = "success"; return result; } 服务提供方控制台输出: 用户[null]开始调用web接口:http://localhost:8247/zz/dataSource/testGet a,b,c,d

ps:当请求参数字符串存在特殊符号比如“+”时,会被转义为空格

http://localhost:8250/xx/task/test2?string=a+b 请求方控制台输出: 用户[null]开始调用web接口:用户[null]开始调用web接口:http://localhost:8250/xx/task/test2 a b

通过debug调试发现当发起请求时在org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest中获取参数:

继续调用链:

-->org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument

-->org.springframework.web.method.annotation.AbstractNamedValueMethodArgumentResolver#resolveArgument

-->org.springframework.web.method.annotation.RequestParamMethodArgumentResolver#resolveName

-->org.springframework.web.context.request.ServletWebRequest#getParameterValues

-->org.apache.catalina.connector.RequestFacade#getParameterValues

-->org.apache.catalina.connector.Request#getParameterValues

-->org.apache.coyote.Request#getParameters

从上面可以得知参数在parameters中,并且特殊符号已经被处理过了,那接下来找下什么时候放进来和怎么被处理的;在org.apache.catalina.connector.Request中看见了parseParameters方法,可以看出是对参数的解析方法,在里面打下断点重新跑下:

从下面可以看出这里是真正进行参数解析处理的:

org.apache.tomcat.util.http.Parameters#handleQueryParameters:

-->org.apache.tomcat.util.http.Parameters#processParameters(org.apache.tomcat.util.buf.MessageBytes, java.nio.charset.Charset)

-->org.apache.tomcat.util.http.Parameters#processParameters(byte[], int, int, java.nio.charset.Charset)

-->org.apache.tomcat.util.http.Parameters#urlDecode

从上面可以看到,是在进行解码时将“+”去除掉的;

继续debug发现在下面的代码中,可以清晰的看到对“+”替换成空格;

-->org.apache.tomcat.util.buf.UDecoder#convert(org.apache.tomcat.util.buf.ByteChunk, boolean)

解决办法为:

1)对参数“a+b”进行编码;

2)改成post请求;

4.2.header参数

在网上看见一篇文章,在这里收藏下《FeignClient传入的header中带逗号引发的401问题》

五.总结

1.@RequestParam参数的";"会被QueryTemplate#queryString进行分割组成Collection字符串;
然后在服务提供方CollectionToStringConverter#convert 中用逗号拼接起来;
2.当没有编码前进入的是CollectionToStringConverter#convert,当编码后进入的是GenericConversionService.NoOpConverter#convert;
3.参数值的获取在RequestParamMethodArgumentResolver#resolveName 中;
4.@RequestBody获取参数在RequestResponseBodyMethodProcessor#resolveArgument中,直接从body中读取然后返回;
5.请求参数字符串存在特殊符号比如“+”时,在参数编码过程中UDecoder#convert会被转义为空格;

相关新闻

MIPI D-PHY一致性测试失败排查:从CTS规范到Layout与调参的工程实践

MIPI D-PHY一致性测试失败排查:从CTS规范到Layout与调参的工程实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 1:09:54 阅读更多 →
UE5零基础3D平台游戏蓝图实战:从零到可玩Demo完整流程

UE5零基础3D平台游戏蓝图实战:从零到可玩Demo完整流程

Unreal Engine 5 零基础 3D 平台游戏蓝图实战&#xff1a;从零到可玩 Demo 的完整流程很多刚接触 Unreal Engine 5 的朋友&#xff0c;第一反应是打开引擎看到满屏面板就开始发懵&#xff1a;世界大纲是什么&#xff1f;关卡蓝图和角色蓝图有什么区别&#xff1f;为什么照着视频…

2026/10/21 1:09:54 阅读更多 →
数显差压变送器校验全流程:从原理到实操的工程指南

数显差压变送器校验全流程:从原理到实操的工程指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/21 1:09:54 阅读更多 →

最新新闻

不登录、不注册,如何免费完整破解所有国际版 Unity?UniHacker 快速上手指南

不登录、不注册,如何免费完整破解所有国际版 Unity?UniHacker 快速上手指南

不登录、不注册&#xff0c;如何免费完整破解所有国际版 Unity&#xff1f;UniHacker 快速上手指南 【免费下载链接】UniHacker 为Windows、MacOS、Linux和Docker修补所有版本的Unity3D和UnityHub 项目地址: https://gitcode.com/GitHub_Trending/un/UniHacker UniHacke…

2026/10/21 1:44:29 阅读更多 →
戴森球计划燃料棒蓝图选型:FactoryBluePrints 的 3 条生产链 + 导入排障指南

戴森球计划燃料棒蓝图选型:FactoryBluePrints 的 3 条生产链 + 导入排障指南

戴森球计划燃料棒蓝图选型&#xff1a;FactoryBluePrints 的 3 条生产链 导入排障指南 【免费下载链接】FactoryBluePrints 游戏戴森球计划的**工厂**蓝图仓库 项目地址: https://gitcode.com/GitHub_Trending/fa/FactoryBluePrints FactoryBluePrints 是戴森球计划的工…

2026/10/21 1:44:29 阅读更多 →
Switch 游戏如何装进电脑:yuzu 模拟器完整指南

Switch 游戏如何装进电脑:yuzu 模拟器完整指南

Switch 游戏如何装进电脑&#xff1a;yuzu 模拟器完整指南 【免费下载链接】yuzu 任天堂 Switch 模拟器 项目地址: https://gitcode.com/GitHub_Trending/yu/yuzu yuzu 是一款开源的任天堂 Switch 模拟器&#xff0c;能把 NSP、XCI 等 Switch 游戏文件装进电脑真正跑起来…

2026/10/21 1:44:29 阅读更多 →
pytorch-metric-learning Testers 完整指南:从选型到训练闭环,一次讲透度量学习模型评估

pytorch-metric-learning Testers 完整指南:从选型到训练闭环,一次讲透度量学习模型评估

pytorch-metric-learning Testers 完整指南&#xff1a;从选型到训练闭环&#xff0c;一次讲透度量学习模型评估 【免费下载链接】ocp FAIR Chemistrys library of machine learning methods for chemistry 项目地址: https://gitcode.com/GitHub_Trending/oc/ocp pyto…

2026/10/21 1:44:29 阅读更多 →
gamdl 教程:如何用 3 条命令下载 Apple Music 的 ALAC 无损音乐

gamdl 教程:如何用 3 条命令下载 Apple Music 的 ALAC 无损音乐

gamdl 教程&#xff1a;如何用 3 条命令下载 Apple Music 的 ALAC 无损音乐 【免费下载链接】gamdl A command-line app for downloading Apple Music songs, music videos and post videos. 项目地址: https://gitcode.com/GitHub_Trending/ga/gamdl Apple Music 的普通…

2026/10/21 1:44:29 阅读更多 →
douyin-downloader 教程:从扫码登录到抖音主页作品批量去水印下载

douyin-downloader 教程:从扫码登录到抖音主页作品批量去水印下载

douyin-downloader 教程&#xff1a;从扫码登录到抖音主页作品批量去水印下载 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fal…

2026/10/21 1:43:29 阅读更多 →

日新闻

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

统一内存+量化:锐龙AI Max PRO 400如何本地跑320B大模型

这段时间硬件圈和AI圈都在传一个事&#xff1a;AMD锐龙AI Max PRO 400系列把最高192GB统一内存搬进了PC形态的产品里&#xff0c;按宣传口径&#xff0c;本地跑320B参数的大家伙不再是纸面概念。我第一反应倒不是“内存真大”&#xff0c;而是这话背后的含金量——它把过去只能…

2026/10/21 0:00:11 阅读更多 →
AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

AI助手深度使用体验:对话质量、长文本处理与指令遵循如何留住用户

1. 从“封号重开”这个行为说起&#xff1a;用户到底在追求什么“封号重开”这四个字&#xff0c;放在任何产品语境里都带着一股决绝的味道。一个账号从零开始积累使用记录、调教偏好、沉淀工作流&#xff0c;背后是大量的时间和精力成本。正常情况下&#xff0c;没有人愿意主动…

2026/10/21 0:00:11 阅读更多 →
ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

ROS中YOLOv3工程落地:通信同步、坐标变换与性能调优

1. 为什么在ROS里硬刚YOLOv3不是“炫技”&#xff0c;而是工程落地的必经之路我第一次在某高校机器人实验室看到那台搭载Jetson TX2的移动底盘原地打转、反复识别错垃圾桶时&#xff0c;就意识到&#xff1a;单纯跑通darknet_ros demo和真正让YOLOv3在ROS里“干活”&#xff0c…

2026/10/21 0:00:11 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/20 20:43:59 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →