在实际项目中,经常会遇到调用外部(第三方)的接口,如果调用量较大的话,可能需要考虑连接池、失败重试、SSL证书等问题,以提升性能和稳定性。
以下代码是封装的小组件,供大家参考。
- maven依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.6</version>
</dependency>
- HttpUtil.java代码:
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.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.springframework.util.CollectionUtils;
import javax.exceptions.ServiceException;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* http/https工具类。
* 支持线程池管理、failRetry特性。
*/
public class HttpUtil {
private static final String DEFAULT_CHARSET = "UTF-8";
private static CloseableHttpClient httpClient;
public static String get(final String url) throws Exception {
return getExecute(url, null, getHttpClient(null), null);
}
public static String get(final String url, final Map<String, Object> paramMap) throws Exception {
return getExecute(url, paramMap, getHttpClient(null), null);
}
public static String get(final String url, final Map<String, Object> paramMap, final Map<String, String> headers) throws Exception {
return getExecute(url, paramMap, getHttpClient(null), headers);
}
/**
* POST请求,支持失败重试。
*/
public static String post(final String url, final Map<String, Object> paramMap) throws Exception {
return postExecute(url, paramMap, new HashMap<>(), getHttpClient(null));
}
/**
* POST请求,支持失败重试。
*/
public static String postRetry(final String url, final Map<String, Object> paramMap, final Map<String, String> headers) throws Exception {
return postExecute(url, paramMap, headers, getHttpClient(null));
}
/**
* POST请求,支持失败重试。
*/
public static String postRetry(final String url, final String paramStr,
final CertInfo certInfo, final Map<String, String> headers) throws Exception {
return postExecute(url, paramStr, headers, getHttpClient(certInfo));
}
/**
* POST请求,支持失败重试。
*/
public static String postRetry(final String url, final Map<String, Object> paramMap,
final CertInfo certInfo, final Map<String, String> headers) throws Exception {
return postExecute(url, paramMap, headers, getHttpClient(certInfo));
}
private static synchronized CloseableHttpClient getHttpClient(final CertInfo certInfo) {
if (null == certInfo && null != httpClient) {
return httpClient;
} else {
//https请求暂不走httpClient缓存,因为每次请求可能使用不同的证书。
RequestConfig config = RequestConfig.custom()
.setSocketTimeout(Constants.HTTP_READ_TIMEOUT_MS)
.setConnectTimeout(Constants.HTTP_CONNECTION_TIMEOUT_MS)
.build();
HttpClientConnectionManager connManager = getPoolConnManager(certInfo);
HttpRequestRetryHandler retryHandler = RetryHandler.getHandler(Constants.DEFAULT_HTTP_RETRY_MAX_TIMES, Constants.DEFAULT_HTTP_RETRY_INTERVAL_MILLIS);
httpClient = HttpClients.custom()
.setConnectionManager(connManager)
.setDefaultRequestConfig(config)
.setRetryHandler(retryHandler)
.build();
//jvm关闭时,释放连接
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
httpClient.close();
} catch (IOException e) {
//catched
}
}));
return httpClient;
}
}
/**
* 获取 连接管理器实例 。
* 支持http/https和连接池。
*/
private static HttpClientConnectionManager getPoolConnManager(final CertInfo certInfo) {
Registry<ConnectionSocketFactory> registry;
if (null != certInfo) {
certInfo.validate();
try {
// 证书
char[] password = certInfo.getPassword().toCharArray();
InputStream certStream = certInfo.getCertStream();
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(certStream, password);
// 实例化密钥库 & 初始化密钥工厂
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmf.init(ks, password);
// 创建 SSLContext
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
sslContext,
new String[]{"TLSv1"},
null,
new DefaultHostnameVerifier());
registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("https", sslConnectionSocketFactory)
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.build();
} catch (Exception e) {
throw new ServiceException(ErrorCodeEnums.InternalServerError, "创建 SSLContext 失败,证书初始化失败");
}
} else {
registry = RegistryBuilder.<ConnectionSocketFactory>create()
.register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build();
}
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(registry);
//http连接池: 最大连接数,默认20
connectionManager.setMaxTotal(Constants.HTTP_POOL_MAX_TOTAL);
//http连接池: 每个路由的并发连接,默认2
connectionManager.setDefaultMaxPerRoute(Constants.HTTP_POOL_MAX_PER_ROUTE);
return connectionManager;
}
/**
* 执行POST请求。
*/
private static String postExecute(String url, String data, Map<String, String> headers,
CloseableHttpClient closeableHttpClient) throws IOException {
HttpPost httpPost = new HttpPost(url);
if (!CollectionUtils.isEmpty(headers)) {
headers.forEach(httpPost::addHeader);
}
StringEntity postEntity = new StringEntity(data, DEFAULT_CHARSET);
httpPost.setEntity(postEntity);
HttpResponse httpResponse = closeableHttpClient.execute(httpPost);
int httpStatus = httpResponse.getStatusLine().getStatusCode();
if (HttpStatus.SC_OK != httpStatus) {
System.err.println(url + "接口调用失败,返回状态码:" + httpStatus);
}
HttpEntity httpEntity = httpResponse.getEntity();
return EntityUtils.toString(httpEntity, DEFAULT_CHARSET);
}
private static String postExecute(String url, Map<String, Object> params, Map<String, String> headers,
CloseableHttpClient closeableHttpClient) throws IOException {
HttpPost httpPost = new HttpPost(url);
if (headers != null && !headers.isEmpty()) {
headers.forEach(httpPost::addHeader);
}
List<NameValuePair> pairList = new ArrayList<>(params.size());
for (Map.Entry<String, Object> entry : params.entrySet()) {
NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue().toString());
pairList.add(pair);
}
httpPost.setEntity(new UrlEncodedFormEntity(pairList, DEFAULT_CHARSET));
HttpResponse httpResponse = closeableHttpClient.execute(httpPost);
int httpStatus = httpResponse.getStatusLine().getStatusCode();
if (HttpStatus.SC_OK != httpStatus) {
System.err.println(url + "接口调用失败,返回状态码:" + httpStatus);
}
HttpEntity httpEntity = httpResponse.getEntity();
return EntityUtils.toString(httpEntity, DEFAULT_CHARSET);
}
private static String getExecute(String url, Map<String, Object> params, CloseableHttpClient closeableHttpClient, Map<String, String> headers) throws IOException {
CloseableHttpResponse response;
if (!CollectionUtils.isEmpty(params)) {
StringBuilder realUrl = new StringBuilder().append(url).append("?");
for (String key : params.keySet()) {
realUrl.append(key).append("=").append(params.get(key)).append("&");
}
url = realUrl.substring(0, realUrl.length() - 1);
}
HttpGet httpGet = new HttpGet(url);
if (!CollectionUtils.isEmpty(headers)) {
headers.forEach(httpGet::addHeader);
}
response = closeableHttpClient.execute(httpGet);
int httpStatus = response.getStatusLine().getStatusCode();
if (HttpStatus.SC_OK != httpStatus) {
System.err.println(url + "接口调用失败,返回状态码:" + httpStatus);
}
return EntityUtils.toString(response.getEntity(), DEFAULT_CHARSET);
}
}
- Constants 常量类:
public class Constants {
// --------------------- http配置相关 start ------------------
/**
* HTTP连接超时时间。
*/
public static final int HTTP_CONNECTION_TIMEOUT_MS = 3000;
/**
* http read timeout
*/
public static final int HTTP_READ_TIMEOUT_MS = 25000;
/**
* http连接池: 最大连接数,默认20
**/
public static final int HTTP_POOL_MAX_TOTAL = 32;
/**
* http连接池: 每个路由的并发连接,默认2
**/
public static final int HTTP_POOL_MAX_PER_ROUTE = 16;
/**
* http失败重试: 默认最大重试次数。
*/
public static final int DEFAULT_HTTP_RETRY_MAX_TIMES = 2;
/**
* http失败重试: 默认每次的间隔时间。默认为1秒。
*/
public static final Integer DEFAULT_HTTP_RETRY_INTERVAL_MILLIS = 1000;
// --------------------- http配置相关 end ------------------
}
- ErrorCodeEnums.java类:
public enum ErrorCodeEnums {
InternalServerError(500, "服务器内部错误"),
}
- SSL证书类CertInfo.java:
import org.springframework.util.Assert;
import java.io.InputStream;
/**
* 发起HTTPS/SSL使用的证书。
*
*
*/
public class CertInfo {
//证书密码(口令)
private String password;
//证书文件
private InputStream certStream;
public String getPassword() {
return password;
}
public CertInfo setPassword(String password) {
this.password = password;
return this;
}
public InputStream getCertStream() {
return certStream;
}
public CertInfo setCertStream(InputStream certStream) {
this.certStream = certStream;
return this;
}
public void validate() {
Assert.hasLength(password, "password不能为空");
Assert.notNull(certStream, "certStream不能为空");
}
}
- HTTP重试策略类:
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpRequest;
import org.apache.http.NoHttpResponseException;
import org.apache.http.client.HttpRequestRetryHandler;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ConnectTimeoutException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.UnknownHostException;
/**
* HTTP重试策略。
*
* @author machunlin
* @date 2018/4/26
*/
public class RetryHandler {
private static final Logger LOGGER = LogManager.getLogger(RetryHandler.class);
/**
* 自定义HTTP重试策略。
*
* @param maxRetryTimes 最大重试次数
* @param intervalMillis 重试间隔时间,毫秒
* @return
*/
public static HttpRequestRetryHandler getHandler(final Integer maxRetryTimes, final Integer intervalMillis) {
if (maxRetryTimes == null || maxRetryTimes <= 0) {
throw new IllegalArgumentException("maxRetryTimes参数不合法:" + maxRetryTimes);
}
return (exception, executionCount, context) -> {
if (executionCount > maxRetryTimes) {
LOGGER.error(" 已超过最大重试次数。 当前为第{}次重试,最大重试次数为{}", executionCount, maxRetryTimes, exception);
return false;
}
try {
long interval = (null != intervalMillis) ? intervalMillis : Constants.DEFAULT_HTTP_RETRY_INTERVAL_MILLIS;
long nextInterval = interval * executionCount;
Thread.sleep(nextInterval);
} catch (InterruptedException e) {
//声明线程状态为"可中断的"。
Thread.currentThread().interrupt();
}
if (exception instanceof UnknownHostException) {
LOGGER.error("目标服务器不可达, Unknown host", exception);
return false;
}
if (exception instanceof ConnectTimeoutException) {
LOGGER.error("连接被拒绝,ConnectTimeout ", exception);
return true;
}
if (exception instanceof InterruptedIOException) {
LOGGER.error("连接超时, Timeout", exception);
return true;
}
if (exception instanceof IOException) {
LOGGER.error("网络异常, IOException", exception);
return true;
}
if (exception instanceof SSLHandshakeException) {
LOGGER.error("SSL握手异常,SSLHandshakeException ", exception);
return false;
}
if (exception instanceof SSLException) {
LOGGER.error("SSLException exception", exception);
return false;
}
if (exception instanceof NoHttpResponseException) {
// 如果服务器丢掉了连接,那么就重试
return true;
}
if (exception instanceof Exception) {
return true;
}
HttpClientContext clientContext = HttpClientContext.adapt(context);
HttpRequest request = clientContext.getRequest();
if (!(request instanceof HttpEntityEnclosingRequest)) {
// 如果请求是幂等的,就再次尝试
return true;
}
LOGGER.error("HttpRequestRetryHandler, return false");
return false;
};
}
}