解决FeignClient发送post请求异常的问题

网友投稿 2297 2022-12-29

解决FeignClient发送post请求异常的问题

解决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 来进行实际请求的调用

com-flix.feign

feign-httpclient

8.18.0

直入源码

通过对 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 queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name());

for (NameValuePair queryParam: queryParams) {

requestBuilder.addParameter(queryParam);

}

//request headers

boolean hasAcceptHeader = false;

for (Map.Entry> 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();

}

推荐的依赖

io.github.openfeign

feign-httpclient</artifactId>

9.5.1

或者

io.github.openfeign

feign-okhttp

9.5.1

总结

目前绝大部分的介绍 feign 的文章都是推荐的 com-flix.feign:feign-httpclient:8.18.0 和 com-flix.feign:feign-okhttp:8.18.0 ,如果不巧你使用了 com-flix.feign:feign-httpclient:8.18.0,那么在POST请求时并且body为空时就会发生丢失查询参数的问题。

这里推荐大家使用 feign-httpclient 或者是 feign-okhttp的时候不要依赖 com-flix.feign,而应该选择 io.github.openfeign,因为看起来 Netflix 很久没有对这两个组件进行维护了,而是由 OpenFeign 来进行维护了。

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:极客时间(编程是什么去极客时间)
下一篇:小程序组件 跳转 页面(小程序组件 跳转 页面怎么设置)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~