解决FeignClient发送post请求异常的问题
feignclient发送post请求异常
这个问题其实很基础。但是却难倒了我。记录一下
在发送post请求的时候要指定消息格式
正确的写法是这样
@postmapping(value = "/test/post", consumes = "application/json") string test(@requestbody string name);
不生效的写法
@postmapping(value = "/test/post", produces= "application/json")
关于这个区别
produces
:它的作用是指定返回值类型,不但可以设置返回值类型还可以设定返回值的字符编码;
consumes
:指定处理请求的提交内容类型(content-type),例如application/json, text/html;
基础真的很重要啊~
feignclient调用post请求时查询参数被丢失的情况分析与处理
本文没有详细介绍 feignclient 的知识点,网上有很多优秀的文章介绍了 feigncient 的知识点,在这里本人就不重复了,只是专注在这个问题点上。
查询参数丢失场景
业务描述: 业务系统需要更新用户系统中的a资源,由于只想更新a资源的一个字段信息为b,所以没有选择通过 entity 封装b,而是直接通过查询参数来传递b信息
文字描述:使用feignclient来进行远程调用时,如果post请求中有查询参数并且没有请求实体(body为空),那么查询参数被丢失,服务提供者获取不到查询参数的值。
代码描述:b的值被丢失,服务提供者获取不到b的值
@feignclient(name = "a-service", configuration = feignconfiguration.class) public interface acall { @requestmapping(method = requestmethod.post, value = "/api/xxx/{a}", headers = {"content-type=application/json"}) void updateatob(@pathvariable("a") final string a, @requestparam("b") final string b) throws exception; }
问题分析
背景
- 使用 feignclient 客户端
- 使用 feign-httpclient 中的 apachehttpclient 来进行实际请求的调用
<dependency> <groupid>com.netflix.feign</groupid> <artifactid>feign-httpclient</artifactid> <version>8.18.0</version> </dependency>
直入源码
通过对 feignclient 的源码阅读,发现问题不是出在参数解析上,而是在使用 apachehttpclient 进行请求时,其将查询参数放进请求body中了,下面看源码具体是如何处理的
feign.httpclient.apachehttpclient 这是 feign-httpclient 进行实际请求的方法
@override public response execute(request request, request.options options) throws ioexception { httpurirequest httpurirequest; try { httpurirequest = tohttpurirequest(request, options); } catch (urisyntaxexception e) { throw new ioexception("url '" + request.url() + "' couldn't be parsed into a uri", e); } httpresponse httpresponse = client.execute(httpurirequest); return tofeignresponse(httpresponse); } httpurirequest tohttpurirequest(request request, request.options options) throws unsupportedencodingexception, malformedurlexception, urisyntaxexception { requestbuilder requestbuilder = requestbuilder.create(request.method()); //per request timeouts requestconfig requestconfig = requestconfig .custom() .setconnecttimeout(options.connecttimeoutmillis()) .setsockettimeout(options.readtimeoutmillis()) .build(); requestbuilder.setconfig(requestconfig); uri uri = new uribuilder(request.url()).build(); requestbuilder.seturi(uri.getscheme() + "://" + uri.getauthority() + uri.getrawpath()); //request query params list<namevaluepair> queryparams = urlencodedutils.parse(uri, requestbuilder.getcharset().name()); for (namevaluepair queryparam: queryparams) { requestbuilder.addparameter(queryparam); } //request headers boolean hasacceptheader = false; for (map.entry<string, collection<string>> headerentry : request.headers().entryset()) { string headername = headerentry.getkey(); if (headername.equalsignorecase(accept_header_name)) { hasacceptheader = true; } if (headername.equalsignorecase(util.content_length)) { // the 'content-length' header is always set by the apache client and it // doesn't like us to set it as well. continue; } for (string headervalue : headerentry.getvalue()) { requestbuilder.addheader(headername, headervalue); } } //some servers choke on the default accept string, so we'll set it to anything if (!hasacceptheader) { requestbuilder.addheader(accept_header_name, "*/*"); } //request body if (request.body() != null) { //body为空,则httpentity为空 httpentity entity = null; if (request.charset() != null) { contenttype contenttype = getcontenttype(request); string content = new string(request.body(), request.charset()); entity = new stringentity(content, contenttype); } else { entity = new bytearrayentity(request.body()); } requestbuilder.setentity(entity); } //调用org.apache.http.client.methods.requestbuilder#build方法 return requestbuilder.build(); }
org.apache.http.client.methods.requestbuilder 此类是 httpurirequest 的builder类,下面看build方法
public httpurirequest build() { final httprequestbase result; uri urinotnull = this.uri != null ? this.uri : uri.create("/"); httpentity entitycopy = this.entity; if (parameters != null && !parameters.isempty()) { // 这里:如果httpentity为空,并且为post请求或者为put请求时,这个方法会将查询参数取出来封装成了httpentity // 就是在这里查询参数被丢弃了,准确的说是被转换位置了 if (entitycopy == null && (httppost.method_name.equalsignorecase(method) || httpput.method_name.equalsignorecase(method))) { entitycopy = new urlencodedformentity(parameters, charset != null ? charset : http.def_content_charset); } else { try { urinotnull = new uribuilder(urinotnull) .setcharset(this.charset) .addparameters(parameters) .build(); } catch (final urisyntaxexception ex) { // should never happen } } } if (entitycopy == null) { result = new internalrequest(method); } else { final internalentityeclosingrequest request = new internalentityeclosingrequest(method); request.setentity(entitycopy); result = request; } result.setprotocolversion(this.version); result.seturi(urinotnull); if (this.headergroup != null) { result.setheaders(this.headergroup.getallheaders()); } result.setconfig(this.config); return result; }
解决方案
既然已经知道原因了,那么解决方法就有很多种了,下面就介绍常规的解决方案:
- 使用 feign-okhttp 来进行请求调用,这里就不列源码了,感兴趣大家可以去看, feign-okhttp 底层没有判断如果body为空则把查询参数放入body中。
- 使用 io.github.openfeign:feign-httpclient:9.5.1 依赖,截取部分源码说明原因如下:
httpurirequest tohttpurirequest(request request, request.options options) throws unsupportedencodingexception, malformedurlexception, urisyntaxexception { requestbuilder requestbuilder = requestbuilder.create(request.method()); //省略部分代码 //request body if (request.body() != null) { //省略部分代码 } else { // 此处,如果为null,则会塞入一个byte数组为0的对象 requestbuilder.setentity(new bytearrayentity(new byte[0])); } return requestbuilder.build(); }
推荐的依赖
<dependency> <groupid>io.github.openfeign</groupid> <artifactid>feign-httpclient</artifactid> <version>9.5.1</version> </dependency>
或者
<dependency> <groupid>io.github.openfeign</groupid> <artifactid>feign-okhttp</artifactid> <version>9.5.1</version> </dependency>
总结
目前绝大部分的介绍 feign 的文章都是推荐的 com.netflix.feign:feign-httpclient:8.18.0 和 com.netflix.feign:feign-okhttp:8.18.0 ,如果不巧你使用了 com.netflix.feign:feign-httpclient:8.18.0,那么在post请求时并且body为空时就会发生丢失查询参数的问题。
这里推荐大家使用 feign-httpclient 或者是 feign-okhttp的时候不要依赖 com.netflix.feign,而应该选择 io.github.openfeign,因为看起来 netflix 很久没有对这两个组件进行维护了,而是由 openfeign 来进行维护了。
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。
推荐阅读
-
解决axios发送post请求返回400状态码的问题
-
解决Vue axios post请求,后台获取不到数据的问题方法
-
[日常] 跨语言的POST请求问题的解决
-
解决axios会发送两次请求,有个OPTIONS请求的问题
-
记录一次排查使用HttpWebRequest发送请求的发生“基础连接已关闭:接收时发生错误”异常问题的过程
-
解决angular的post请求后SpringMVC后台接收不到参数值问题的方法
-
linux shell中curl 发送post请求json格式问题的处理方法
-
解决Python发送Http请求时,中文乱码的问题
-
SpringMVC中如何解决post和get请求的中文乱码问题
-
Web项目:解决SpringMVC中Post或者Get请求中文乱码的问题