最近项目中开发有很多外部http调用,但是我方的接口相应有时效性要求,所以就需要针对项目使用到的http调用进行连接池改造,原先没做也是时效性要求不是很严格,但是现在需要了,就需要整体调整,这也是对原先开发不负责的后果吧。

废话不多说,项目中使用到的http调用方式,这个方式有三种,为啥有三种也不多说了,谁让建项初期没做严格要求,导致大家都是使用自己习惯的方式去完成的设计。

目前有三种:httpclient、resttemplate、feign,接下来针对一样一样的记录了。

1、httpclient

pom依赖的引用:

<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents/httpclient -->
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.5.12</version>
        </dependency>

代码工具类:

package com.ifeng.datacenter.assist.utils;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.NameValuePair;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.config.SocketConfig;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.DefaultHttpRequestRetryHandler;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * @author : 
 * @Date: 2020/6/1 16:00
 * @Description: V-
 */
@SuppressWarnings("all")
@Slf4j
public class HttpClientUtil {
    private static CloseableHttpClient httpClient = null;

    static {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        // 总连接池数量
        connectionManager.setMaxTotal(1500);
        // 可为每个域名设置单独的连接池数量
        // connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost("127.0.0.1")), 500);
        connectionManager.setDefaultMaxPerRoute(150);  // 这个必须设置,默认2,也就是单个路由最大并发数是2

        // setTcpNoDelay 是否立即发送数据,设置为true会关闭Socket缓冲,默认为false
        // setSoReuseAddress 是否可以在一个进程关闭Socket后,即使它还没有释放端口,其它进程还可以立即重用端口
        // setSoLinger 关闭Socket时,要么发送完所有数据,要么等待60s后,就关闭连接,此时socket.close()是阻塞的
        // setSoTimeout 接收数据的等待超时时间,单位ms
        // setSoKeepAlive 开启监视TCP连接是否有效
        SocketConfig socketConfig = SocketConfig.custom()
                .setTcpNoDelay(true)
                .setSoReuseAddress(true)
                .setSoLinger(60)
                .setSoTimeout(500)
                .setSoKeepAlive(true)
                .build();
        connectionManager.setDefaultSocketConfig(socketConfig);

        // setConnectTimeout表示设置建立连接的超时时间
        // setConnectionRequestTimeout表示从连接池中拿连接的等待超时时间
        // setSocketTimeout表示发出请求后等待对端应答的超时时间
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(10000)
                .setConnectionRequestTimeout(10000)
                .setSocketTimeout(10000)
                .build();

        // 重试处理器,StandardHttpRequestRetryHandler这个是官方提供的,看了下感觉比较挫,很多错误不能重试,可自己实现HttpRequestRetryHandler接口去做
//        HttpRequestRetryHandler retryHandler = new StandardHttpRequestRetryHandler();
        // 关闭重试策略
        HttpRequestRetryHandler requestRetryHandler = new DefaultHttpRequestRetryHandler(0, false);

        // 自定义请求存活策略
        ConnectionKeepAliveStrategy connectionKeepAliveStrategy = new ConnectionKeepAliveStrategy() {
            /**
             * 返回时间单位是毫秒
             */
            @Override
            public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) {
                return 60 * 1000;
            }
        };

        httpClient = HttpClients.custom()
                .setConnectionManager(connectionManager)
                .setDefaultRequestConfig(requestConfig)
                .setRetryHandler(requestRetryHandler)
                .setKeepAliveStrategy(connectionKeepAliveStrategy)
                .build();
    }

    /**
     * httpclient get
     *
     * @param uri       请求地址
     * @param getParams 请求参数
     * @return
     */
    public static JSONObject doHttpGet(String uri, Map<String, String> getParams) {
        HttpGet httpGet = null;
        CloseableHttpResponse response = null;
        try {
            URIBuilder uriBuilder = new URIBuilder(uri);
            if (null != getParams && !getParams.isEmpty()) {
                List<NameValuePair> list = new ArrayList<>();
                for (Map.Entry<String, String> param : getParams.entrySet()) {
                    list.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                uriBuilder.setParameters(list);
            }
            httpGet = new HttpGet(uriBuilder.build());
            response = httpClient.execute(httpGet);
            int statusCode = response.getStatusLine().getStatusCode();
            if (HttpStatus.SC_OK == statusCode) {
                HttpEntity entity = response.getEntity();
                if (null != entity) {
                    String resStr = EntityUtils.toString(entity, "utf-8");
                    return JSON.parseObject(resStr);
                }
            }
        } catch (Exception e) {
            log.error("CloseableHttpClient-get-请求异常", e);
        } finally {
            try {
                if (null != response)
                    response.close();
            } catch (IOException e) {
                log.error("CloseableHttpClient-post-请求异常,释放连接异常", e);
            }
            try {
                if (null != httpGet)
                    httpGet.releaseConnection();
            } catch (Exception e) {
                log.error("CloseableHttpClient-post-请求异常,释放连接异常", e);
            }
        }
        return new JSONObject();
    }

    /**
     * httpclient post
     *
     * @param uri       请求地址
     * @param getParams map化的请求体对象
     * @return
     */
    public static JSONObject doHttpPost(String uri, Map<String, String> getParams) {
        HttpPost httpPost = null;
        CloseableHttpResponse response = null;
        try {
            httpPost = new HttpPost(uri);
            if (null != getParams && !getParams.isEmpty()) {
                List<NameValuePair> list = new ArrayList<>();
                for (Map.Entry<String, String> param : getParams.entrySet()) {
                    list.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                HttpEntity httpEntity = new UrlEncodedFormEntity(list, "utf-8");
                httpPost.setEntity(httpEntity);
            }
            response = httpClient.execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            if (HttpStatus.SC_OK == statusCode) {
                HttpEntity entity = response.getEntity();
                if (null != entity) {
                    String resStr = EntityUtils.toString(entity, "utf-8");
                    return JSON.parseObject(resStr);
                }
            }
        } catch (Exception e) {
            log.error("CloseableHttpClient-post-请求异常", e);
        } finally {
            try {
                if (null != response)
                    response.close();
            } catch (IOException e) {
                log.error("CloseableHttpClient-post-请求异常,释放连接异常", e);
            }
            try {
                if (null != httpPost)
                    httpPost.releaseConnection();
            } catch (Exception e) {
                log.error("CloseableHttpClient-post-请求异常,释放连接异常", e);
            }
        }
        return new JSONObject();
    }

    /**
     * httpclient post
     *
     * @param uri       请求地址
     * @param reqParams json串
     * @return
     */
    public static JSONObject doHttpPost(String uri, String reqParams) {
        HttpPost httpPost = null;
        CloseableHttpResponse response = null;
        try {
            httpPost = new HttpPost(uri);
            httpPost.addHeader("Content-Type", "application/json;charset=utf-8");
            if (StringUtils.isNotBlank(reqParams)) {
                StringEntity postingString = new StringEntity(reqParams, "utf-8");
                httpPost.setEntity(postingString);
            }
            response = httpClient.execute(httpPost);
            int statusCode = response.getStatusLine().getStatusCode();
            if (HttpStatus.SC_OK == statusCode) {
                HttpEntity entity = response.getEntity();
                if (null != entity) {
                    String resStr = EntityUtils.toString(entity, "utf-8");
                    return JSON.parseObject(resStr);
                }
            }
        } catch (Exception e) {
            log.error("CloseableHttpClient-post-请求异常", e);
        } finally {
            try {
                if (null != response)
                    response.close();
            } catch (IOException e) {
                log.error("CloseableHttpClient-post-请求异常,释放连接异常", e);
            }
            try {
                if (null != httpPost)
                    httpPost.releaseConnection();
            } catch (Exception e) {
                log.error("CloseableHttpClient-post-请求异常,释放连接异常", e);
            }
        }
        return new JSONObject();
    }
}

效果是很显著的,由原先的几百ms到几ms的优化效果。这个效率高实际上就是三次握手四次挥手拜拜的优化,有兴趣的可以找相关资料仔细了解下流程就会明白了。

2、RestTemplate

这个是springboot的web包自带的,所以需要引入web包

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

代码样列:

package com.zh.boot.config;

import org.apache.http.client.HttpClient;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.StandardHttpRequestRetryHandler;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;
import java.util.List;

/**
 * @author : Lu Ma Ren
 * @Date: 2020/6/1 16:00
 * @Description: V-自定义restTemplate模板
 */
@SuppressWarnings("all")
@Configuration
public class RestTemplateConfig {

    @Bean
    public RestTemplate initRestTemplate() {
        RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
        // 设置编码格式为UTF-8
        List<HttpMessageConverter<?>> converterList = restTemplate.getMessageConverters();
        HttpMessageConverter<?> converterTarget = null;
        for (HttpMessageConverter<?> item : converterList) {
            if (item.getClass() == StringHttpMessageConverter.class) {
                converterTarget = item;
                break;
            }
        }
        if (converterTarget != null) {
            converterList.remove(converterTarget);
        }
        HttpMessageConverter<?> converter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
        converterList.add(1, converter);
        return restTemplate;
    }

    public ClientHttpRequestFactory httpRequestFactory() {
        return new HttpComponentsClientHttpRequestFactory(httpClient());
    }

    public HttpClient httpClient() {
        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
        //设置整个连接池最大连接数 根据自己的场景决定
        connectionManager.setMaxTotal(200);
        // 可为每个域名设置单独的连接池数量,特殊配置,针对域名或者指定ip配置池大小
//        connectionManager.setMaxPerRoute(new HttpRoute(new HttpHost("10.66.224.52")), 50);
        //路由是对maxTotal的细分
        connectionManager.setDefaultMaxPerRoute(100);
        //SocketTimeout:服务器返回数据(response)的时间,超过该时间抛出read timeout
        //连接上服务器(握手成功)的时间,超出该时间抛出connect timeout
        //从连接池中获取连接的超时时间,超过该时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool
        RequestConfig requestConfig = RequestConfig.custom()
                .setSocketTimeout(3000)
                .setConnectTimeout(1000)
                .setConnectionRequestTimeout(1000)
                .build();
        // 重试处理器,StandardHttpRequestRetryHandler这个是官方提供的,看了下感觉比较挫,很多错误不能重试,可自己实现HttpRequestRetryHandler接口去做
        HttpRequestRetryHandler retryHandler = new StandardHttpRequestRetryHandler();
        return HttpClientBuilder.create()
                .setDefaultRequestConfig(requestConfig)
                .setConnectionManager(connectionManager)
                .setRetryHandler(retryHandler)
                .build();
    }
}

这个也是采用了http连接池去做的,有些码子也是参考了网上一些大佬的资料,就不做一一说明了,看那些资料没留存记录,如果有冒犯了,见谅了,有找到地址了会重新修正文章。

3、feign

这个是cloud组件中的,但是未了避免cloud版本的考虑,直接用的带版本号的pom地址

<!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-openfeign -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-netflix-hystrix -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
            <version>2.2.2.RELEASE</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.github.openfeign/feign-httpclient -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
            <version>11.0</version>
        </dependency>

这块的配置只需要配置文件添加

feign:
  httpclient:
    enabled: true

这个的配置有点迷糊,因为没有前两个那么明确的去设置连接池的参数,都是默认的,所以这块就不做评价和说明了,等后续搞清楚了在更新了。

在项目中,由于很多时候配置的连接池或者静态代码的使用,导致项目启动后第一次访问很慢,而在有时效性的接口中,有一定的错误率的要求,所以就需要针对接口进行预加载的处理,springboot也提供这种方便,下边就简要说明下预加载的使用。

在springboot中,只需要实现接口ApplicationRunner,实现run方法即可。

package com.zh.boot.controller.preload;

import com.zh.boot.controller.goods.GoodsController;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @author : Lu Ma Ren
 * @Date: 2020/6/1 16:00
 * @Description: V-
 */
@SuppressWarnings("all")
@Slf4j
@Component
@Order(99)
public class GoodsControllerPre implements ApplicationRunner {

    @Autowired
    private GoodsController goodsController;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        goodsController.getGoodsId(100);
    }
}

该类会在boot项目启动完成后就进行调用,在这说下几点坑,有时候接口中需要使用到加载到内存的数据,所以这个加载顺序以及延时时间都是需要处理考虑到的,不然会在预加载的时候报错,尤其是空指针等这种常见错误。

以上代码都是参考用例,针对自己实际使用需要考虑下具体参数的配置。采用连接池这个思想其实可以使用到很多场景,最大的好处就是重复利用和避免重复创建销毁,就拿feign的连接池配置,在不配置的时候,微服务之间的调用会很慢,但是做了连接池配置,提升的会很明显的。

一点一点积累,一点一点进步