zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    SQL 取日期
    myeclipse 8.5 安装jbpm3.2开发插件
    持续感悟
    程序员应该读的书与经常上的网站
    java连接ms sql server各类问题解析
    怎么实现Redis的高可用?(主从、哨兵、集群)
    Web系统突然爆”Asp.net ajax客户端框架未能加载“的一种可能原因(误改服务器系统时间)
    【转】Skyline软件介绍
    ArcSDE启动遇到ORA12560: TNS: 协议适配器错误解决办法
    开放源代码GIS资源集锦
  • 原文地址:https://www.cnblogs.com/softidea/p/12549071.html
Copyright © 2011-2022 走看看