Apache HttpClient在PUT/POST時的一個坑


結論:
Feign如果使用Apache HttpClient,PUT/POST時,傳參時盡量使用RequestBody。
如果沒有RequestBody,QueryString會被Apache HttpClient轉換成表單中key value進行提交,這樣數據接口方就會取不到

報錯了
像往常一樣把服務B的接口定義 copy 到服務A的FeignClient中,然后在Postman中自測期間一個接口報錯了【服務A 調 服務B時出錯了】。

報錯信息:

 

 

提示信息不是很優雅,勿怪,因為正常情況下根本不可能出現這種情況。就是攻擊者看到這個提示也會止步【參數校驗很嚴格】。

數據的生產、消費情況
服務B提供的服務【生產】:

 

 

服務A提供給前端的服務:

 

 

服務A調用服務B【消費】:

 

 

當時這樣寫是想偷個懶:直接使用QueryString,就不用再新定義傳數據用的DTO。 報錯就因為不走尋常路,這是后話,下面有分析。

BugShooting:分析日志


按請求的數據流,日志依次為:服務A的日志【與期望一致】:

 

 

服務B的日志【與期望不一致:少了QueryString】:

 

 

問題已經定位:服務A調用服務B時,把QueryString參數 弄丟了
論打印日志的重要性!打印有用的日志是一門學問

 

又check了代碼,沒有毛病呀,QueryString專用的標@RequestParam也已經打上了!奇怪

BugShooting:站到巨人的肩膀上


想看看是不是有人趟過這個坑,baidu、google、bing下沒找到相關的信息。只是看到有通過@Headers("Content-Type: application/json")@PutMapping(value = "/provide/sync_strategies/{syncStrategyId}", headers = {"Content-Type:application/json"})來顯式聲明 Request Header的做法,試了下沒用。

 

BugShooting:Debug【必殺技

會不會大家都沒有這樣用過其實,直接將參數全部通過@RequestBody也可以解決,但之前給自己立了一個Flag:要把Feign的源碼重新梳理一遍。
Debug時,發現Apache HttpClient在PUT或POST方法時會有這樣一個邏輯:
有QueryString但沒有RequestBody時,QueryString不會放到URL中,而是將QueryString轉化成表單的key value結構的數據,然后按表單的方式提交:

org.apache.http.client.methods.RequestBuilder#build
指定使用application/x-www-form-urlencoded,並將QueryString寫到RequestBody中:

 

 

org.apache.http.client.entity.UrlEncodedFormEntity#UrlEncodedFormEntity(java.lang.Iterable<? extends org.apache.http.NameValuePair>, java.nio.charset.Charset)

 

 

 

org.apache.http.client.utils.URLEncodedUtils#format(java.lang.Iterable<? extends org.apache.http.NameValuePair>, char, java.nio.charset.Charset)其實將QueryString進行轉換,並以表單的形式提交,也是符合Htpp協議的,但需要接收方也按這種方式來接收就可以。看上面的截圖,服務B 使用了@RequestParam,即從QueryString取值,那當然就取不到了。簡單地講,就像取快遞一樣。平時都在南門取,但是如果快遞員跑到北門后,又沒告訴你這個變動。如果你還到南門,肯定是取不到的。


兩種不同的數據傳輸方式

報錯時Apache HttpClient發起的請求:

 

 

 

期望的方式:

 

 

 

問題找到了,解決辦法就一目了然了:增加一個RequestBody不就可以了
我傳一個冗余的RequestBody進去:

 

 

 

 

可以看到ReqestBody已經值了

 

繼續看QueryString的處理邏輯是否發生變化,可以看到與期望的一樣了:

 

 

但這種處理方式,增加了業務不需要參數,會增加代碼的維護成本,其它同學看代碼時,將這個當做無效參數去掉的話,服務就不可用了。
於是,就將請求的參數封裝到一個DTO中,然后在Body中傳數據即可:

 

 

 

 

補充


1、Apache Http Client初始化entity【RequestBody】的代碼入口:

feign.httpclient.ApacheHttpClient#toHttpUriRequest

 

2、踩坑的一個條件:指定Feign使用Client為Apache Http Client

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
            <version>10.8</version>
        </dependency>

feign-httpclient的較低版本還需要添加下面這個依賴:

        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
        </dependency>

 

說明:Apache HttpClient是老牌http客戶端了,可以設置連接池、超時時間等對服務之間的調用調優。Spring Cloud從Brixtion.SR5版本開始就支持這種替換。
一個經典的HttpClient配置:

//httpclient 4.5.2使用連接池的經典配置
    private CloseableHttpClient getHttpClient() {
        //注冊訪問協議相關的Socket工廠
        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.INSTANCE)
                .register("https", SSLConnectionSocketFactory.getSocketFactory())
                .build();

        //HttpConnectionFactory:配置寫請求/解析響應處理器
        HttpConnectionFactory<HttpRoute, ManagedHttpClientConnection> connectionFactory = new ManagedHttpClientConnectionFactory(
                DefaultHttpRequestWriterFactory.INSTANCE,
                DefaultHttpResponseParserFactory.INSTANCE
        );

        //DNS解析器
        DnsResolver dnsResolver = SystemDefaultDnsResolver.INSTANCE;
        //創建連接池管理器
        PoolingHttpClientConnectionManager manager = new PoolingHttpClientConnectionManager(socketFactoryRegistry, connectionFactory, dnsResolver);
        //設置默認的socket參數
        manager.setDefaultSocketConfig(SocketConfig.custom().setTcpNoDelay(true).build());
        manager.setMaxTotal(300);//設置最大連接數。高於這個值時,新連接請求,需要阻塞,排隊等待
        //路由是對MaxTotal的細分。
        // 每個路由實際最大連接數默認值是由DefaultMaxPerRoute控制。
        // MaxPerRoute設置的過小,無法支持大並發:ConnectionPoolTimeoutException:Timeout waiting for connection from pool
        manager.setDefaultMaxPerRoute(200);//每個路由的最大連接
        manager.setValidateAfterInactivity(5 * 1000);//在從連接池獲取連接時,連接不活躍多長時間后需要進行一次驗證,默認為2s

        //配置默認的請求參數
        RequestConfig defaultRequestConfig = RequestConfig.custom()
                .setConnectTimeout(2 * 1000)//連接超時設置為2s
                .setSocketTimeout(5 * 1000)//等待數據超時設置為5s
                .setConnectionRequestTimeout(2 * 1000)//從連接池獲取連接的等待超時時間設置為2s
//                .setProxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("192.168.0.2", 1234))) //設置代理
                .build();

        CloseableHttpClient closeableHttpClient = HttpClients.custom()
                .setConnectionManager(manager)
                .setConnectionManagerShared(false)//連接池不是共享模式,這個共享是指與其它httpClient是否共享
                .evictIdleConnections(60, TimeUnit.SECONDS)//定期回收空閑連接
                .evictExpiredConnections()//回收過期連接
                .setConnectionTimeToLive(60, TimeUnit.SECONDS)//連接存活時間,如果不設置,則根據長連接信息決定
                .setDefaultRequestConfig(defaultRequestConfig)//設置默認的請求參數
                .setConnectionReuseStrategy(DefaultConnectionReuseStrategy.INSTANCE)//連接重用策略,即是否能keepAlive
                .setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE)//長連接配置,即獲取長連接生產多長時間
                .setRetryHandler(new DefaultHttpRequestRetryHandler(0, false))//設置重試次數,默認為3次;當前是禁用掉
                .build();

        /**
         *JVM停止或重啟時,關閉連接池釋放掉連接
         */
        Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                try {
                    closeableHttpClient.close();
                    log.info("http client closed");
                } catch (IOException e) {
                    log.error(e.getMessage(), e);
                }
            }
        });
        return closeableHttpClient;
    }

 

https://github.com/helloworldtang/spring-boot-cookbook/blob/master/learning-demo/src/main/java/com/tangcheng/learning/web/config/RestTemplateConfig.java

 

https://mp.weixin.qq.com/s/N4zqSfMAgB6b5jnUsa1z2w

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM