ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Apache HttpClient在PUT/POST时的一个坑

2020-03-22 22:54:35  阅读:447  来源: 互联网

标签:服务 QueryString RequestBody PUT Apache 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

 

标签:服务,QueryString,RequestBody,PUT,Apache,POST,连接,连接池
来源: https://www.cnblogs.com/softidea/p/12549071.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有