解決HTTP GET方法調用帶有body問題

1.背景描述

         上游服務提供的方法非常比較奇特,查詢接口,定義的GET方法,參數通過request body傳遞的,在使用Feign Client封裝GET方法調用時,會遇到一個報錯,“405 Method Not Allowed”。通過查詢,知道這個錯誤原因是HTTP調用方法錯誤,比如:定義的API是GET方法,通過POST方法(非GET方法)調用,就會返回這個錯誤。

          @RequestLine("GET /api/user/get/")

          Object getUser(@HeaderMap Map headers, UserRequest request);

2.原因分析

        奇怪代碼明明寫得是使用GET方法啊,進一步查資料,得知原因是Feign client框架本身有一個坑:Feign client框架,默認情況下使用的是HttpURLConnection完成實際的http請求調用,但是HttpURLConnection本身不支持GET方法調用時帶有body,帶有body的調用方法,只能是POST方法。

// sun.net.www.protocol.http.HttpURLConnection

private synchronized OutputStream getOutputStream0() throws IOException {

try {

if(!this.doOutput) {

throw new ProtocolException("cannot write to a URLConnection if doOutput=false - call setDoOutput(true)");

} else {

if(this.method.equals("GET")) {

this.method = "POST";

}

// ........

}

}

}

HTTP GET方法調用,到底支不支持帶有body呢,HTTP協議是支持的,沒有禁止,但是呢,不建議這麼做,不是一個良好的習慣,因爲有些瀏覽器

啥的可能不支持,這個時候,你寫的方法就尷尬了。

Stackoverflow解釋如下:

     In other words, any HTTP request message is allowed to contain a message body, and thus must parse messages with that in mind. Server semantics for GET, however,

are restricted such that a body, if any, has no semantic meaning to the request. The requirements on parsing are separate from the requirements on method semantics.

So, yes, you can send a body with GET, and no, it is never useful to do so.

This is part of the layered design of HTTP/1.1 that will become clear again once the spec is partitioned (work in progress).

3.解決方法

方案一:

         網上可以很容易搜索到這個解決方法,相關博客非常多,直接copy的情況,太嚴重了。但是實際驗證,沒有生效,具體原因待排查。

         1.yml配置文件中,加入feign的配置項:feign.httpclient.enabled: true

         2.增加如下maven依賴。

            <dependency>

                <groupId>org.apache.httpcomponents</groupId>                   

                <artifactId>httpclient</artifactId>                   

                <version>4.5.3</version>             

            </dependency>

            <dependency>

                <groupId>com.netflix.feign</groupId>

                <artifactId>feign-httpclient</artifactId>

                <version>8.17.0</version>

            </dependency>

方案原理:HttpURLConnection不支持GET方法帶有body的調用,ApacheHttpClient支持GET方法帶有body的調用。這個配置,就是將feign client默認使用的HTTP調用方式,從HttpURLConnection切換到ApacheHttpClient方式。

方法不生效原因:

1.可能HTTP調用方式沒有切換成功,也就是配置沒有生效。(確定是這個原因,因爲我使用的Feign方式:Feign.builder()默認生成的就是HttpURLConnection方式的http請求調用。相關源碼如下:

  // feign.Feign.Builder

private Client client = new Client.Default(null, null);

// feign.Client.Default

final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection();

因此相關配置修改是不生效的,需要重新生成一個client才行,比如:ApacheHttpClient

  2.可能ApacheHttpClient本身也不支持GET方法帶有body的請求。(因爲直接使用ApacheHttpClient,發現沒有支持GET方法帶有body的調用方式)

補充:(待驗證,證明)

         feign分別嘗試了Java原生URLConnection,OkHttp,ApacheHttpClient三種方式:

             1.URLConnection 報405錯誤,說明http方法不對,但是feign配置是GET方法,查feign的日誌也是用的GET方法。後來發現原因是URLConnection在的

原因:對於有request body的GET方法,自動改爲POST方法了。

             2.OkHttp 直接報錯:method GET must not have a request body.

             3.ApacheHttpClient完美支持。

方案二:

        使用AsyncHttpClient,因爲AsyncHttpClient支持GET方法帶有Body的調用。

        網上也可以很容易搜索到這個解決方法,感覺都是複製粘貼的,沒有經過驗證和實證,內容完全一樣,但是都缺少最關鍵的信息,沒有給出需要引用的jar包,怎麼使用測試呢?而需要引用的jar包還不好找到,實在是大坑。

         1.引入maven依賴

              <dependency>

                    <groupId>org.asynchttpclient</groupId>

                    <artifactId>async-http-client</artifactId>

                    <version>2.2.0</version>

             </dependency>

         2.解決方法demo

         public static String get(String url, String bodyData, Map<String, String> headers) throws Exception {

                // 構建請求

                BoundRequestBuilder requestBuilder = asyncHttpClient.prepareGet(url).setBody(bodyData);

                headers.forEach(requestBuilder::addHeader);

                List<Response> list = new ArrayList<>();

                requestBuilder.execute()

                        .toCompletableFuture()

                        .thenAccept(list::add)

                        .join();

                if (list.isEmpty()) {

                    return null;

                }

                Response response = list.get(0);

                if (response.getStatusCode() != 200) {

                    return null;

                }

                return response.getResponseBody();

         }

備註1:

         1.方法可以返回map,增加:new ObjectMapper().readValue(response.getResponseBody(), Map.class);

         2.方法本身必須返回json 對象的string才行,不能是非json對象的string,否則解析異常。

備註2:

          1.沒有body的get方法,去掉.setBody(bodyData)即可。

          2.沒有header的get方法調用,去掉headers.forEach(requestBuilder::addHeader);即可。

4.總結

         網上資源很多、很豐富,各種問題解決方案很多,但是也存在很多缺陷,不去驗證、實踐,根本不知道里面有問題,因此不要隨便copy別人的博客,往往copy的博客本身就存在潛在的問題,copy之前,請試驗一下,證明方法是正確的,減少給需要同學的誤導。

5.參考資料

1.https://yanbin.blog/why-http-get-cannot-sent-data-with-reuqest-body/http://www.programmersought.com/article/752346261/

2.https://blog.csdn.net/f641385712/article/details/82431502

3.https://segmentfault.com/q/1010000011958034

4.https://stackoverflow.com/questions/978061/http-get-with-request-body
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章