解決FeignClient發(fā)送post請求異常的問題
FeignClient發(fā)送post請求異常
這個(gè)問題其實(shí)很基礎(chǔ)。但是卻難倒了我。記錄一下
在發(fā)送post請求的時(shí)候要指定消息格式
正確的寫法是這樣
@PostMapping(value = "/test/post", consumes = "application/json") String test(@RequestBody String name);
不生效的寫法
@PostMapping(value = "/test/post", produces= "application/json")
關(guān)于這個(gè)區(qū)別
produces:它的作用是指定返回值類型,不但可以設(shè)置返回值類型還可以設(shè)定返回值的字符編碼;
consumes:指定處理請求的提交內(nèi)容類型(Content-Type),例如application/json, text/html;
基礎(chǔ)真的很重要啊~
FeignClient調(diào)用POST請求時(shí)查詢參數(shù)被丟失的情況分析與處理
本文沒有詳細(xì)介紹 FeignClient 的知識點(diǎn),網(wǎng)上有很多優(yōu)秀的文章介紹了 FeignCient 的知識點(diǎn),在這里本人就不重復(fù)了,只是專注在這個(gè)問題點(diǎn)上。
查詢參數(shù)丟失場景
業(yè)務(wù)描述: 業(yè)務(wù)系統(tǒng)需要更新用戶系統(tǒng)中的A資源,由于只想更新A資源的一個(gè)字段信息為B,所以沒有選擇通過 entity 封裝B,而是直接通過查詢參數(shù)來傳遞B信息
文字描述:使用FeignClient來進(jìn)行遠(yuǎn)程調(diào)用時(shí),如果POST請求中有查詢參數(shù)并且沒有請求實(shí)體(body為空),那么查詢參數(shù)被丟失,服務(wù)提供者獲取不到查詢參數(shù)的值。
代碼描述:B的值被丟失,服務(wù)提供者獲取不到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 來進(jìn)行實(shí)際請求的調(diào)用
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>8.18.0</version>
</dependency>
直入源碼
通過對 FeignClient 的源碼閱讀,發(fā)現(xiàn)問題不是出在參數(shù)解析上,而是在使用 ApacheHttpClient 進(jìn)行請求時(shí),其將查詢參數(shù)放進(jìn)請求body中了,下面看源碼具體是如何處理的
feign.httpclient.ApacheHttpClient 這是 feign-httpclient 進(jìn)行實(shí)際請求的方法
@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);
}
//調(diào)用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請求時(shí),這個(gè)方法會將查詢參數(shù)取出來封裝成了HttpEntity
// 就是在這里查詢參數(shù)被丟棄了,準(zhǔn)確的說是被轉(zhuǎn)換位置了
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;
}
解決方案
既然已經(jīng)知道原因了,那么解決方法就有很多種了,下面就介紹常規(guī)的解決方案:
- 使用 feign-okhttp 來進(jìn)行請求調(diào)用,這里就不列源碼了,感興趣大家可以去看, feign-okhttp 底層沒有判斷如果body為空則把查詢參數(shù)放入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,則會塞入一個(gè)byte數(shù)組為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>
總結(jié)
目前絕大部分的介紹 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請求時(shí)并且body為空時(shí)就會發(fā)生丟失查詢參數(shù)的問題。
這里推薦大家使用 feign-httpclient 或者是 feign-okhttp的時(shí)候不要依賴 com.netflix.feign,而應(yīng)該選擇 io.github.openfeign,因?yàn)榭雌饋?Netflix 很久沒有對這兩個(gè)組件進(jìn)行維護(hù)了,而是由 OpenFeign 來進(jìn)行維護(hù)了。
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
SpringBoot?Knife4j框架&Knife4j的顯示內(nèi)容的配置方式
Knife4j框架是基于Swagger2開發(fā)的在線API文檔生成工具,主要功能包括自動生成API文檔、接口文檔展示、接口測試工具、接口權(quán)限控制和在線調(diào)試,該框架支持通過注解自動生成詳細(xì)的接口文檔,開發(fā)者可以直接在文檔界面進(jìn)行接口測試和調(diào)試2024-09-09
Java中JDBC連接池的基本原理及實(shí)現(xiàn)方式
本文詳細(xì)講解了Java中JDBC連接池的基本原理及實(shí)現(xiàn)方式,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12
SpringMVC 上傳文件 MultipartFile 轉(zhuǎn)為 File的方法
這篇文章主要介紹了SpringMVC 上傳文件 MultipartFile 轉(zhuǎn)為 File的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-02-02
springboot Quartz動態(tài)修改cron表達(dá)式的方法
這篇文章主要介紹了springboot Quartz動態(tài)修改cron表達(dá)式的方法,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-09-09
SpringBoot Nacos實(shí)現(xiàn)自動刷新
這篇文章主要介紹了SpringBoot Nacos實(shí)現(xiàn)自動刷新,Nacos(Dynamic Naming and Configuration Service)是阿里巴巴開源的一個(gè)動態(tài)服務(wù)發(fā)現(xiàn)、配置管理和服務(wù)管理平臺2023-01-01
解決SpringBoot application.yaml文件配置schema 無法執(zhí)行sql問題
這篇文章主要介紹了解決SpringBoot application.yaml文件配置schema 無法執(zhí)行sql問題,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
springboot 在xml里讀取yml的配置信息的示例代碼
這篇文章主要介紹了springboot 在xml里讀取yml的配置信息的示例代碼,代碼簡單易懂,對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-09-09
JAVA實(shí)現(xiàn)按時(shí)間段查詢數(shù)據(jù)操作
這篇文章主要介紹了JAVA實(shí)現(xiàn)按時(shí)間段查詢數(shù)據(jù)操作,具有很好的參考價(jià)值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-08-08

