前言
本文沒有詳細介紹 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
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。