依賴項
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.9</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpmime</artifactId>
<version>4.5</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<version>4.1.5</version>
</dependency>
<!-- 將Java Util Logging重定向到slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.10</version>
</dependency>
<!-- 將log4j重定向到slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.10</version>
</dependency>
異步工具類實現:
package idslilang.com.just.go.async;
import org.apache.commons.codec.Charsets;
import org.apache.http.*;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.concurrent.FutureCallback;
import org.apache.http.config.Registry;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.nio.client.CloseableHttpAsyncClient;
import org.apache.http.impl.nio.client.HttpAsyncClients;
import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;
import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;
import org.apache.http.impl.nio.reactor.IOReactorConfig;
import org.apache.http.message.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.nio.conn.NoopIOSessionStrategy;
import org.apache.http.nio.conn.SchemeIOSessionStrategy;
import org.apache.http.nio.conn.ssl.SSLIOSessionStrategy;
import org.apache.http.nio.reactor.ConnectingIOReactor;
import org.apache.http.nio.reactor.IOReactorException;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.ssl.SSLInitializationException;
import org.apache.http.ssl.TrustStrategy;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
public class AsyncHttpClientUtil implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(AsyncHttpClientUtil.class);
private final CloseableHttpAsyncClient httpClient;
private final PoolingNHttpClientConnectionManager connMgr;
private final ThreadLocal<HttpClientContext> httpContext = ThreadLocal.withInitial(HttpClientContext::create);
public AsyncHttpClientUtil() {
// 自定義keep-alive策略,keep-alive使得tcp連接可以被複用。
// 但默認的keep-alive時長爲無窮大,不符合現實,所以需要改寫,定義一個更短的時間
// 如果服務器http響應頭包含 "Keep-Alive:timeout=" ,則使用timeout=後面指定的值作爲keep-alive的時長,否則默認60秒
ConnectionKeepAliveStrategy strategy = (httpResponse, httpContext) -> {
HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return 60 * 1000;
};
//設置協議http和https對應的處理socket鏈接工廠的對象 ,ssl
SSLContext sslContext = null;
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
TrustStrategy noopTrustStrategy = new TrustStrategy() {
@Override
public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException {
return true;
}
};
sslContext = SSLContexts.custom().setProtocol(SSLConnectionSocketFactory.TLS)
.loadTrustMaterial(trustStore, noopTrustStrategy).build();
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException ex) {
throw new SSLInitializationException(ex.getMessage(), ex);
}
Registry<SchemeIOSessionStrategy> sessionStrategy = RegistryBuilder.<SchemeIOSessionStrategy>create()
.register("http", NoopIOSessionStrategy.INSTANCE)
.register("https", new SSLIOSessionStrategy(sslContext, NoopHostnameVerifier.INSTANCE)).build();
// 設置reactor參數
IOReactorConfig ioReactorConfig = IOReactorConfig.custom()
.setIoThreadCount(Runtime.getRuntime().availableProcessors()).setSoKeepAlive(true).build();
ConnectingIOReactor ioReactor;
try {
ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);
} catch (IOReactorException e) {
throw new RuntimeException(e.getMessage(), e);
}
// 自定義連接池,連接池可以連接可以使連接可以被複用。
// 連接的複用需要滿足:Keep-alive + 連接池。keep-alive使得連接能夠保持存活,不被系統銷燬;連接池使得連接可以被程序重複引用
// 默認的連接池,DefaultMaxPerRoute只有5個,MaxTotal只有10個,不滿足實際的生產需求
connMgr = new PoolingNHttpClientConnectionManager(ioReactor);
// 最大連接數500
connMgr.setMaxTotal(500);
// 同一個ip:port的請求,最大連接數200
connMgr.setDefaultMaxPerRoute(200);
// 定義請求超時配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(60) // 從連接池裏獲取連接的超時時間
.setConnectTimeout(60) // 建立TCP的超時時間
.setSocketTimeout(60) // 讀取數據包的超時時間
.build();
httpClient = HttpAsyncClients.custom()
.setConnectionManager(connMgr)
//開啓後臺線程清除閒置30秒以上的連接
.setKeepAliveStrategy(strategy)
.setDefaultRequestConfig(requestConfig)
.build();
httpClient.start();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
close();
} catch (IOException e) {
logger.info("peace close fail");
}
}));
}
/**
* 因爲HttpClient是線程安全的,可以提供給多個線程複用,同時連接池的存證的目的就是爲了可以複用連接,所以提供單例模式
*/
private static class HttpClientUtilHolder {
private static final AsyncHttpClientUtil INSTANCE = new AsyncHttpClientUtil();
}
public static AsyncHttpClientUtil getInstance() {
return HttpClientUtilHolder.INSTANCE;
}
public PoolingNHttpClientConnectionManager getConnMgr() {
return connMgr;
}
public HttpContext getHttpContext() {
return httpContext.get();
}
public CloseableHttpAsyncClient getHttpClient() {
return httpClient;
}
/**
* http get操作
*
* @param url 請求地址
* @param headers 請求頭
* @return 返回響應內容
*/
public CompletableFuture<String> get(String url, Map<String, String> headers) {
CompletableFuture<String> completableFuture = new CompletableFuture<>();
HttpGet httpGet = new HttpGet(url);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
httpGet.setHeader(header.getKey(), header.getValue());
}
}
httpClient.execute(httpGet, callback(completableFuture));
return completableFuture;
}
/**
* form表單post
*
* @param url 請求地址
* @param params 參數內容
* @param headers http頭信息
* @return http文本格式響應內容
*/
public CompletableFuture<String> post(String url, Map<String, String> params, Map<String, String> headers) {
CompletableFuture<String> completableFuture = new CompletableFuture<>();
HttpPost httpPost = new HttpPost(url);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
httpPost.setHeader(header.getKey(), header.getValue());
}
}
List<NameValuePair> formParams = new ArrayList<>();
if (params != null) {
for (Map.Entry<String, String> param : params.entrySet()) {
formParams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(formParams, Charsets.UTF_8);
httpPost.setEntity(formEntity);
httpClient.execute(httpPost, callback(completableFuture));
return completableFuture;
}
/**
* json內容post
*
* @param url 請求地址
* @param data json報文
* @param headers http頭
* @return http文本格式響應內容
*/
public CompletableFuture<String> post(String url, String data, Map<String, String> headers) {
CompletableFuture<String> completableFuture = new CompletableFuture<>();
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-type", ContentType.APPLICATION_JSON.getMimeType());
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
httpPost.setHeader(header.getKey(), header.getValue());
}
}
if (!StringUtils.isEmpty(data)) {
StringEntity stringEntity = new StringEntity(data, ContentType.create(HTTP.PLAIN_TEXT_TYPE, Charsets.UTF_8));
httpPost.setEntity(stringEntity);
}
httpClient.execute(httpPost, callback(completableFuture));
return completableFuture;
}
/**
* HttpClientUtil類回收時,通過httpClient.close(),可以間接的關閉連接池,從而關閉連接池持有的所有tcp連接
* 與response.close()的區別:response.close()只是把某個請求持有的tcp連接放回連接池,而httpClient().close()是銷燬整個連接池
*
* @throws IOException
*/
@Override
public void close() throws IOException {
httpClient.close();
}
public FutureCallback<HttpResponse> callback(CompletableFuture<String> completableFuture) {
return new FutureCallback<HttpResponse>() {
@Override
public void completed(HttpResponse httpResponse) {
try {
HttpEntity entity = httpResponse.getEntity();
if (entity == null) {
completableFuture.complete(null);
return;
}
String data = EntityUtils.toString(entity, Charsets.UTF_8);
completableFuture.complete(data);
} catch (Exception ex) {
completableFuture.complete(null);
logger.error("excute get error ", ex);
}
}
@Override
public void failed(Exception ex) {
completableFuture.obtrudeException(ex);
logger.error("excute get error ", ex);
}
@Override
public void cancelled() {
completableFuture.cancel(false);
logger.error("excute cancelled");
}
};
}
}
同步工具類
package idslilang.com.just.go.async;
import org.apache.commons.codec.Charsets;
import org.apache.http.HeaderElement;
import org.apache.http.HeaderElementIterator;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
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.protocol.HttpClientContext;
import org.apache.http.conn.ConnectionKeepAliveStrategy;
import org.apache.http.entity.ContentType;
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.BasicHeaderElementIterator;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
public class HttpClientUtil implements AutoCloseable {
private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);
private final CloseableHttpClient httpClient;
private final PoolingHttpClientConnectionManager connMgr;
private final ThreadLocal<HttpClientContext> httpContext = ThreadLocal.withInitial(HttpClientContext::create);
public HttpClientUtil() {
// 自定義keep-alive策略,keep-alive使得tcp連接可以被複用。
// 但默認的keep-alive時長爲無窮大,不符合現實,所以需要改寫,定義一個更短的時間
// 如果服務器http響應頭包含 "Keep-Alive:timeout=" ,則使用timeout=後面指定的值作爲keep-alive的時長,否則默認60秒
ConnectionKeepAliveStrategy strategy = (httpResponse, httpContext) -> {
HeaderElementIterator it = new BasicHeaderElementIterator(httpResponse.headerIterator(HTTP.CONN_KEEP_ALIVE));
while (it.hasNext()) {
HeaderElement he = it.nextElement();
String param = he.getName();
String value = he.getValue();
if (value != null && param.equalsIgnoreCase("timeout")) {
return Long.parseLong(value) * 1000;
}
}
return 60 * 1000;
};
// 自定義連接池,連接池可以連接可以使連接可以被複用。
// 連接的複用需要滿足:Keep-alive + 連接池。keep-alive使得連接能夠保持存活,不被系統銷燬;連接池使得連接可以被程序重複引用
// 默認的連接池,DefaultMaxPerRoute只有5個,MaxTotal只有10個,不滿足實際的生產需求
connMgr = new PoolingHttpClientConnectionManager();
// 最大連接數500
connMgr.setMaxTotal(500);
// 同一個ip:port的請求,最大連接數200
connMgr.setDefaultMaxPerRoute(200);
// 定義請求超時配置
RequestConfig requestConfig = RequestConfig.custom()
.setConnectionRequestTimeout(60) // 從連接池裏獲取連接的超時時間
.setConnectTimeout(60) // 建立TCP的超時時間
.setSocketTimeout(60) // 讀取數據包的超時時間
.build();
httpClient = HttpClients.custom()
.setConnectionManager(connMgr)
.evictExpiredConnections()
//開啓後臺線程清除閒置30秒以上的連接
.evictIdleConnections(30, TimeUnit.SECONDS)
.setKeepAliveStrategy(strategy)
.setDefaultRequestConfig(requestConfig)
.build();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
close();
} catch (IOException e) {
logger.info("graceful shutdown fail");
}
}));
}
/**
* 因爲HttpClient是線程安全的,可以提供給多個線程複用,同時連接池的存證的目的就是爲了可以複用連接,所以提供單例模式
*/
private static class HttpClientUtilHolder {
private static final HttpClientUtil INSTANCE = new HttpClientUtil();
}
public static HttpClientUtil getInstance() {
return HttpClientUtilHolder.INSTANCE;
}
public PoolingHttpClientConnectionManager getConnMgr() {
return connMgr;
}
public HttpContext getHttpContext() {
return httpContext.get();
}
public CloseableHttpClient getHttpClient() {
return httpClient;
}
/**
* http get操作
*
* @param url 請求地址
* @param headers 請求頭
* @return 返回響應內容
*/
public String get(String url, Map<String, String> headers) {
HttpGet httpGet = new HttpGet(url);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
httpGet.setHeader(header.getKey(), header.getValue());
}
}
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpGet, httpContext.get());
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, Charsets.UTF_8);
} catch (Exception ex) {
logger.error("excute get error ", ex);
} finally {
if (response != null) {
try {
//上面的EntityUtils.toString會調用inputStream.close(),進而也會觸發連接釋放回連接池,但因爲httpClient.execute可能拋異常,所以得在finally顯示調一次,確保連接一定被釋放
response.close();
} catch (IOException ex) {
logger.error("excute get error ", ex);
}
}
}
return null;
}
/**
* form表單post
*
* @param url 請求地址
* @param params 參數內容
* @param headers http頭信息
* @return http文本格式響應內容
*/
public String post(String url, Map<String, String> params, Map<String, String> headers) {
HttpPost httpPost = new HttpPost(url);
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
httpPost.setHeader(header.getKey(), header.getValue());
}
}
List<NameValuePair> formParams = new ArrayList<>();
if (params != null) {
for (Map.Entry<String, String> param : params.entrySet()) {
formParams.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
}
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(formParams, Charsets.UTF_8);
httpPost.setEntity(formEntity);
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpPost, httpContext.get());
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, Charsets.UTF_8);
} catch (Exception ex) {
logger.error("excute postWithForm error ", ex);
} finally {
if (response != null) {
try {
response.close();
} catch (IOException ex) {
logger.error("excute postWithForm error ", ex);
}
}
}
return null;
}
/**
* json內容post
*
* @param url 請求地址
* @param data json報文
* @param headers http頭
* @return http文本格式響應內容
*/
public String post(String url, String data, Map<String, String> headers) {
HttpPost httpPost = new HttpPost(url);
httpPost.setHeader("Content-type", ContentType.APPLICATION_JSON.getMimeType());
if (headers != null) {
for (Map.Entry<String, String> header : headers.entrySet()) {
httpPost.setHeader(header.getKey(), header.getValue());
}
}
if (!StringUtils.isEmpty(data)) {
StringEntity stringEntity = new StringEntity(data, ContentType.create(HTTP.PLAIN_TEXT_TYPE, Charsets.UTF_8));
httpPost.setEntity(stringEntity);
}
CloseableHttpResponse response = null;
try {
response = httpClient.execute(httpPost, httpContext.get());
HttpEntity entity = response.getEntity();
return EntityUtils.toString(entity, Charsets.UTF_8);
} catch (Exception ex) {
logger.error("excute postWithBody error ", ex);
} finally {
if (response != null) {
try {
response.close();
} catch (IOException ex) {
logger.error("excute postWithBody error ", ex);
}
}
}
return null;
}
/**
* HttpClientUtil類回收時,通過httpClient.close(),可以間接的關閉連接池,從而關閉連接池持有的所有tcp連接
* 與response.close()的區別:response.close()只是把某個請求持有的tcp連接放回連接池,而httpClient().close()是銷燬整個連接池
*
* @throws IOException
*/
@Override
public void close() throws IOException {
httpClient.close();
}
}
Logback 配置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE xml>
<configuration>
<property name="LOG_PATH" value="./logs"/>
<property name="LOG_PATTEN" value="%date %-5level %logger{36} %line : %msg%n"/>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/idslilang.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/idslilang.log.%d{yyyyMMdd}.%i.gz</fileNamePattern>
<maxFileSize>1GB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>120GB</totalSizeCap>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>${LOG_PATTEN}</pattern>
</encoder>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/err.idslilang.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/err.idslilang.log.%d{yyyyMMdd}.%i.gz</fileNamePattern>
<maxFileSize>1GB</maxFileSize>
<maxHistory>7</maxHistory>
<totalSizeCap>10GB</totalSizeCap>
</rollingPolicy>
<encoder charset="UTF-8">
<pattern>${LOG_PATTEN}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<OnMismatch>DENY</OnMismatch>
<OnMatch>ACCEPT</OnMatch>
</filter>
</appender>
<root level="INFO">
<appender-ref ref="FILE"/>
<appender-ref ref="ERROR_FILE"/>
</root>
</configuration>