From c2e61ca3ca0df58d4ecde500ad66ddb2468dd129 Mon Sep 17 00:00:00 2001 From: fuhao Date: Wed, 14 Aug 2024 15:45:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(httpclient=20=E6=95=B4=E5=90=88):?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/ruoyi/common/utils/http/HttpConf.java | 33 ++ .../ruoyi/common/utils/http/HttpUtils.java | 525 +++++++++++++++++- .../http/IdleConnectionMonitorThread.java | 72 +++ .../common/utils/http/HttpUtilsTest.java | 22 + 4 files changed, 649 insertions(+), 3 deletions(-) create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpConf.java create mode 100644 ruoyi-common/src/main/java/com/ruoyi/common/utils/http/IdleConnectionMonitorThread.java create mode 100644 ruoyi-common/src/test/java/com/ruoyi/common/utils/http/HttpUtilsTest.java diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpConf.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpConf.java new file mode 100644 index 00000000..61c14a33 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpConf.java @@ -0,0 +1,33 @@ +package com.ruoyi.common.utils.http; + +/** + * http 配置信息 + * + * @author ruoyi + */ +public class HttpConf +{ + // 获取连接的最大等待时间 + public static int WAIT_TIMEOUT = 10000; + + // 连接超时时间 + public static int CONNECT_TIMEOUT = 10000; + + // 读取超时时间 + public static int SO_TIMEOUT = 60000; + + // 最大连接数 + public static int MAX_TOTAL_CONN = 200; + + // 每个路由最大连接数 + public static int MAX_ROUTE_CONN = 150; + + // 重试次数 + public static int RETRY_COUNT = 3; + + // EPTWebServes地址 + public static String EPTWEBSERVES_URL; + + // tomcat默认keepAliveTimeout为20s + public static int KEEP_ALIVE_TIMEOUT; +} \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java index d3b61cad..0a4b34f0 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/HttpUtils.java @@ -10,13 +10,44 @@ import java.net.SocketTimeoutException; import java.net.URL; import java.net.URLConnection; import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; + +import org.apache.commons.collections4.MapUtils; +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.config.RequestConfig.Builder; +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.conn.ConnectTimeoutException; +import org.apache.http.conn.ConnectionKeepAliveStrategy; +import org.apache.http.conn.HttpClientConnectionManager; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustStrategy; +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.ssl.SSLContextBuilder; +import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.ruoyi.common.constant.Constants; @@ -24,13 +55,26 @@ import com.ruoyi.common.utils.StringUtils; /** * 通用http发送方法 - * + * * @author ruoyi */ public class HttpUtils { private static final Logger log = LoggerFactory.getLogger(HttpUtils.class); + public static RequestConfig requestConfig; + + private static CloseableHttpClient httpClient; + + private static PoolingHttpClientConnectionManager connMgr; + + private static IdleConnectionMonitorThread idleThread; + + static + { + HttpUtils.initClient(); + } + /** * 向指定 URL 发送GET方法的请求 * @@ -59,7 +103,7 @@ public class HttpUtils * * @param url 发送请求的 URL * @param param 请求参数,请求参数应该是 name1=value1&name2=value2 的形式。 - * @param contentType 编码类型 + * @param contentType 内容类型 编码类型 * @return 所代表远程资源的响应结果 */ public static String sendGet(String url, String param, String contentType) @@ -216,7 +260,7 @@ public class HttpUtils String ret = ""; while ((ret = br.readLine()) != null) { - if (ret != null && !"".equals(ret.trim())) + if (ret != null && !ret.trim().equals("")) { result.append(new String(ret.getBytes(StandardCharsets.ISO_8859_1), StandardCharsets.UTF_8)); } @@ -271,4 +315,479 @@ public class HttpUtils return true; } } + + /** + * 获取httpClient + * + * @return + */ + public static CloseableHttpClient getHttpClient() + { + if (httpClient != null) + { + return httpClient; + } + else + { + return HttpClients.createDefault(); + } + } + + /** + * 创建连接池管理器 + * + * @return + */ + private static PoolingHttpClientConnectionManager createConnectionManager() + { + + PoolingHttpClientConnectionManager connMgr = new PoolingHttpClientConnectionManager(); + // 将最大连接数增加到 + connMgr.setMaxTotal(HttpConf.MAX_TOTAL_CONN); + // 将每个路由基础的连接增加到 + connMgr.setDefaultMaxPerRoute(HttpConf.MAX_ROUTE_CONN); + + return connMgr; + } + + /** + * 根据当前配置创建HTTP请求配置参数。 + * + * @return 返回HTTP请求配置。 + */ + private static RequestConfig createRequestConfig() + { + Builder builder = RequestConfig.custom(); + builder.setConnectionRequestTimeout(StringUtils.nvl(HttpConf.WAIT_TIMEOUT, 10000)); + builder.setConnectTimeout(StringUtils.nvl(HttpConf.CONNECT_TIMEOUT, 10000)); + builder.setSocketTimeout(StringUtils.nvl(HttpConf.SO_TIMEOUT, 60000)); + return builder.build(); + } + + /** + * 创建默认的HTTPS客户端,信任所有的证书。 + * + * @return 返回HTTPS客户端,如果创建失败,返回HTTP客户端。 + */ + private static CloseableHttpClient createHttpClient(HttpClientConnectionManager connMgr) + { + try + { + final SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy() + { + @Override + public boolean isTrusted(X509Certificate[] chain, String authType) throws CertificateException + { + // 信任所有 + return true; + } + }).build(); + final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext); + + // 重试机制 + HttpRequestRetryHandler retryHandler = new DefaultHttpRequestRetryHandler(HttpConf.RETRY_COUNT, true); + ConnectionKeepAliveStrategy connectionKeepAliveStrategy = new ConnectionKeepAliveStrategy() + { + @Override + public long getKeepAliveDuration(HttpResponse httpResponse, HttpContext httpContext) + { + return HttpConf.KEEP_ALIVE_TIMEOUT; // tomcat默认keepAliveTimeout为20s + } + }; + httpClient = HttpClients.custom().setSSLSocketFactory(sslsf).setConnectionManager(connMgr) + .setDefaultRequestConfig(requestConfig).setRetryHandler(retryHandler) + .setKeepAliveStrategy(connectionKeepAliveStrategy).build(); + } + catch (Exception e) + { + log.error("Create http client failed", e); + httpClient = HttpClients.createDefault(); + } + + return httpClient; + } + + /** + * 初始化 只需调用一次 + */ + public synchronized static CloseableHttpClient initClient() + { + if (httpClient == null) + { + connMgr = createConnectionManager(); + requestConfig = createRequestConfig(); + // 初始化httpClient连接池 + httpClient = createHttpClient(connMgr); + // 清理连接池 + idleThread = new IdleConnectionMonitorThread(connMgr); + idleThread.start(); + } + + return httpClient; + } + + /** + * 关闭HTTP客户端。 + * + */ + public synchronized static void shutdown() + { + try + { + if (idleThread != null) + { + idleThread.shutdown(); + idleThread = null; + } + } + catch (Exception e) + { + log.error("httpclient connection manager close", e); + } + + try + { + if (httpClient != null) + { + httpClient.close(); + httpClient = null; + } + } + catch (IOException e) + { + log.error("httpclient close", e); + } + } + + /** + * 请求上游 GET提交 + * + * @param uri URL + * @throws IOException IO异常 + */ + public static String getCall(final String uri) throws Exception + { + + return getCall(uri, null, null, Constants.UTF8); + } + + /** + * 请求上游 GET提交 + * + * @param uri URL + * @throws IOException IO异常 + */ + public static String getCall(final String uri, Map header) throws Exception + { + + return getCall(uri, null, header, Constants.UTF8); + } + + /** + * 请求上游 GET提交 + * + * @param uri URL + * @param contentType 内容类型 + * @throws IOException IO异常 + */ + public static String getCall(final String uri, String contentType) throws Exception + { + + return getCall(uri, contentType, null, Constants.UTF8); + } + + /** + * 请求上游 GET提交 + * + * @param uri URL + * @param contentType 内容类型 + * @param charsetName 编码格式 + * @throws IOException IO异常 + */ + public static String getCall(final String uri, String contentType, Map header, String charsetName) throws Exception + { + final String url = uri; + final HttpGet httpGet = new HttpGet(url); + httpGet.setConfig(requestConfig); + if (!StringUtils.isEmpty(contentType)) { + httpGet.addHeader("Content-Type", contentType); + } + if (!MapUtils.isEmpty(header)) { + header.forEach(httpGet::addHeader); + } + final CloseableHttpResponse httpRsp = getHttpClient().execute(httpGet); + try { + if (httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_OK + || httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_FORBIDDEN) { + final HttpEntity entity = httpRsp.getEntity(); + final String rspText = EntityUtils.toString(entity, charsetName); + EntityUtils.consume(entity); + return rspText; + } else { + throw new IOException("HTTP StatusCode=" + httpRsp.getStatusLine().getStatusCode()); + } + } finally { + try { + httpRsp.close(); + } catch (Exception e) { + log.error("关闭httpRsp异常", e); + } + } + } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param paramsMap 参数 + * @throws IOException IO异常 + */ + public static String postCall(final String uri, Map paramsMap) throws Exception + { + return postCall(uri, null, paramsMap, Constants.UTF8, null); + } + + /** + * 请求上游 POST提交 + * + * @param uri URL url + * @param paramsMap 参数 参数 + * @param header 请求头 + * @throws IOException IO异常 异常 + */ + public static String postCall(final String uri, Map paramsMap, Map header) throws Exception + { + return postCall(uri, null, paramsMap, Constants.UTF8, header); + } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param contentType 内容类型 + * @param paramsMap 参数 + * @throws IOException IO异常 + */ +// public static String postCall(final String uri, String contentType, Map paramsMap) throws Exception { +// +// return postCall(uri, contentType, paramsMap, Constants.UTF8, null); +// } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param contentType 内容类型 + * @param paramsMap 参数 + * @throws IOException IO异常 + */ + public static String postCall(final String uri, String contentType, Map paramsMap, Map header) throws Exception + { + + return postCall(uri, contentType, paramsMap, Constants.UTF8, header); + } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param contentType 内容类型 + * @param paramsMap 参数 + * @param charsetName 编码格式 + * @throws IOException IO异常 + */ + public static String postCall(final String uri, String contentType, Map paramsMap, + String charsetName, Map header) throws Exception + { + + final String url = uri; + final HttpPost httpPost = new HttpPost(url); + httpPost.setConfig(requestConfig); + if (!StringUtils.isEmpty(contentType)) + { + httpPost.addHeader("Content-Type", contentType); + } + if (!MapUtils.isEmpty(header)) { + header.forEach(httpPost::addHeader); + } + // 添加参数 + List list = new ArrayList<>(); + if (paramsMap != null) + { + for (Map.Entry entry : paramsMap.entrySet()) + { + list.add(new BasicNameValuePair(entry.getKey(), (String) entry.getValue())); + } + } + httpPost.setEntity(new UrlEncodedFormEntity(list, charsetName)); + + final CloseableHttpResponse httpRsp = getHttpClient().execute(httpPost); + + try + { + if (httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) + { + final HttpEntity entity = httpRsp.getEntity(); + final String rspText = EntityUtils.toString(entity, charsetName); + EntityUtils.consume(entity); + return rspText; + } + else + { + throw new IOException("HTTP StatusCode=" + httpRsp.getStatusLine().getStatusCode()); + } + } + finally + { + try + { + httpRsp.close(); + } + catch (Exception e) + { + log.error("关闭httpRsp异常", e); + } + } + } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param param + * @throws IOException IO异常 + */ + public static String postCall(final String uri, String param) throws Exception + { + + return postCall(uri, null, param, Constants.UTF8, null); + } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param param + * @throws IOException IO异常 + */ + public static String postCall(final String uri, String param, Map header) throws Exception + { + + return postCall(uri, null, param, Constants.UTF8, header); + } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param contentType 内容类型 + * @param param + * @throws IOException IO异常 + */ + public static String postCall(final String uri, String contentType, String param) throws Exception + { + + return postCall(uri, contentType, param, Constants.UTF8, null); + } + + /** + * 请求上游 POST提交 + * + * @param uri URL + * @param contentType 内容类型 + * @param param + * @param charsetName 编码格式 + * @throws IOException IO异常 + */ + public static String postCall(final String uri, String contentType, String param, String charsetName, Map header) + throws Exception + { + + final String url = uri; + final HttpPost httpPost = new HttpPost(url); + httpPost.setConfig(requestConfig); + if (!StringUtils.isEmpty(contentType)) + { + httpPost.addHeader("Content-Type", contentType); + } + else + { + httpPost.addHeader("Content-Type", "application/json"); + } + + if (!MapUtils.isEmpty(header)) { + header.forEach(httpPost::addHeader); + } + + // 添加参数 + StringEntity paramEntity = new StringEntity(param, charsetName); + httpPost.setEntity(paramEntity); + + final CloseableHttpResponse httpRsp = getHttpClient().execute(httpPost); + + try + { + if (httpRsp.getStatusLine().getStatusCode() == HttpStatus.SC_OK) + { + final HttpEntity entity = httpRsp.getEntity(); + final String rspText = EntityUtils.toString(entity, charsetName); + EntityUtils.consume(entity); + return rspText; + } + else + { + throw new IOException("HTTP StatusCode=" + httpRsp.getStatusLine().getStatusCode()); + } + } + finally + { + try + { + httpRsp.close(); + } + catch (Exception e) + { + log.error("关闭httpRsp异常", e); + } + } + } + + /** + * 判断HTTP异常是否为读取超时。 + * + * @param e 异常对象。 + * @return 如果是读取引起的异常(而非连接),则返回true;否则返回false。 + */ + public static boolean isReadTimeout(final Throwable e) + { + return (!isCausedBy(e, ConnectTimeoutException.class) && isCausedBy(e, SocketTimeoutException.class)); + } + + /** + * 检测异常e被触发的原因是不是因为异常cause。检测被封装的异常。 + * + * @param e 捕获的异常。 + * @param cause 异常触发原因。 + * @return 如果异常e是由cause类异常触发,则返回true;否则返回false。 + */ + public static boolean isCausedBy(final Throwable e, final Class cause) + { + if (cause.isAssignableFrom(e.getClass())) + { + return true; + } + else + { + Throwable t = e.getCause(); + while (t != null && t != e) + { + if (cause.isAssignableFrom(t.getClass())) + { + return true; + } + t = t.getCause(); + } + return false; + } + } } \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/IdleConnectionMonitorThread.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/IdleConnectionMonitorThread.java new file mode 100644 index 00000000..75649c35 --- /dev/null +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/http/IdleConnectionMonitorThread.java @@ -0,0 +1,72 @@ +package com.ruoyi.common.utils.http; + +import java.util.concurrent.TimeUnit; +import org.apache.http.conn.HttpClientConnectionManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 连接池清理 + * + * @author ruoyi + */ +public class IdleConnectionMonitorThread extends Thread +{ + private static final Logger log = LoggerFactory.getLogger(IdleConnectionMonitorThread.class); + + private final HttpClientConnectionManager connMgr; + + private volatile boolean shutdown; + + public IdleConnectionMonitorThread(HttpClientConnectionManager connMgr) + { + super(); + this.shutdown = false; + this.connMgr = connMgr; + } + + @Override + public void run() + { + while (!shutdown) + { + try + { + synchronized (this) + { + // 每5秒检查一次关闭连接 + wait(HttpConf.KEEP_ALIVE_TIMEOUT / 4); + // 关闭失效的连接 + connMgr.closeExpiredConnections(); + // 可选的, 关闭20秒内不活动的连接 + connMgr.closeIdleConnections(HttpConf.KEEP_ALIVE_TIMEOUT, TimeUnit.MILLISECONDS); + // log.debug("关闭失效的连接"); + } + } + catch (Exception e) + { + log.error("关闭失效连接异常", e); + } + } + } + + public void shutdown() + { + shutdown = true; + if (connMgr != null) + { + try + { + connMgr.shutdown(); + } + catch (Exception e) + { + log.error("连接池异常", e); + } + } + synchronized (this) + { + notifyAll(); + } + } +} \ No newline at end of file diff --git a/ruoyi-common/src/test/java/com/ruoyi/common/utils/http/HttpUtilsTest.java b/ruoyi-common/src/test/java/com/ruoyi/common/utils/http/HttpUtilsTest.java new file mode 100644 index 00000000..b9cadc8e --- /dev/null +++ b/ruoyi-common/src/test/java/com/ruoyi/common/utils/http/HttpUtilsTest.java @@ -0,0 +1,22 @@ +package com.ruoyi.common.utils.http; + +import io.netty.handler.codec.http.HttpUtil; + +import java.util.HashMap; +import java.util.Map; + + +class HttpUtilsTest { + + void getCall() throws Exception { + Map paramsMap = new HashMap<>(); + paramsMap.put("id", "1"); + paramsMap.put("name", "ruoyi"); + + String json = "{\"id\": 1, \"name\": \"ry\"}"; + String uri = "http://localhost:8080/sensor/data/judge"; + + HttpUtils.postCall(uri, paramsMap); + HttpUtils.postCall(uri, json); + } +} \ No newline at end of file