前言
本文没有详细介绍 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
来进行维护了。
参考资料:
作者:vincent_ren
链接:https://www.jianshu.com/p/7cfa4250d5ab
來源:简书
简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。