一、背景
业务开发中,经常会遇到通过http/https向下游服务发送请求。每次都要重复造轮子写HttpClient的逻辑,而且性能、功能参差不齐。这里分享一个高性能的、带连接池的通用Http客户端工具。
请尊重作者劳动成果,转载请标明原文链接:https://www.cnblogs.com/waterystone/p/11551280.html
二、特点
- 基于apache的高性能Http客户端org.apache.http.client.HttpClient;
- 连接池的最大连接数默认是20,可通过系统变量-Dadu.common.http.max.total=200指定;
- 连接池的每个路由的最大连接数默认是2,可通过系统变量-Dadu.common.http.max.per.route=10指定;
- 可设置超时,通过HttpOptions进行设置;
- 可重试,通过HttpOptions进行设置。
三、源码
参考:https://github.com/waterystone/adu-test/blob/master/src/main/java/com/adu/utils/HttpClientUtil.java
1 package com.adu.utils; 2 3 import com.adu.Constants; 4 import com.adu.handler.HttpRequestRetryHandler; 5 import com.adu.model.HttpOptions; 6 import com.adu.model.HttpRequest; 7 import com.adu.model.HttpResponse; 8 import org.apache.http.Header; 9 import org.apache.http.HttpEntity; 10 import org.apache.http.NameValuePair; 11 import org.apache.http.client.config.RequestConfig; 12 import org.apache.http.client.entity.UrlEncodedFormEntity; 13 import org.apache.http.client.methods.CloseableHttpResponse; 14 import org.apache.http.client.methods.HttpGet; 15 import org.apache.http.client.methods.HttpPost; 16 import org.apache.http.client.methods.HttpRequestBase; 17 import org.apache.http.client.utils.URIBuilder; 18 import org.apache.http.client.utils.URLEncodedUtils; 19 import org.apache.http.config.Registry; 20 import org.apache.http.config.RegistryBuilder; 21 import org.apache.http.conn.socket.ConnectionSocketFactory; 22 import org.apache.http.conn.socket.PlainConnectionSocketFactory; 23 import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 24 import org.apache.http.entity.StringEntity; 25 import org.apache.http.entity.mime.MultipartEntityBuilder; 26 import org.apache.http.entity.mime.content.ContentBody; 27 import org.apache.http.impl.client.CloseableHttpClient; 28 import org.apache.http.impl.client.HttpClients; 29 import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 30 import org.apache.http.message.BasicNameValuePair; 31 import org.apache.http.util.EntityUtils; 32 import org.slf4j.Logger; 33 import org.slf4j.LoggerFactory; 34 35 import javax.net.ssl.SSLContext; 36 import java.nio.charset.StandardCharsets; 37 import java.util.ArrayList; 38 import java.util.HashMap; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.stream.Collectors; 43 44 /** 45 * 带有连接池的Http客户端工具类。具有如下特点: 46 * <ol> 47 * <li>基于apache的高性能Http客户端{@link org.apache.http.client.HttpClient};</li> 48 * <li>连接池的最大连接数默认是20,可通过{@link #init(int, int)}、或者系统变量-Dzzarch.common.http.max.total=200指定;</li> 49 * <li>连接池的每个路由的最大连接数默认是2,可通过{@link #init(int, int)}、或者系统变量-Dzzarch.common.http.max.per.route=10指定;</li> 50 * <li>可设置超时,通过{@link HttpOptions}进行设置;</li> 51 * <li>可重试,通过{@link HttpOptions}进行设置;</li> 52 * </ol> 53 * 54 * @author duyunjie 55 * @date 2019-09-18 16:33 56 */ 57 public class HttpClientUtil { 58 private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class); 59 60 /** 61 * HttpClient 连接池 62 */ 63 private static PoolingHttpClientConnectionManager CONNECTION_MANAGER = buildPoolingHttpClientConnectionManager(null, null); 64 65 /** 66 * @param maxTotal 连接池的最大连接数,默认为20。 67 * @param maxPerRoute 连接池的每个路由的最大连接数,默认为2。 68 */ 69 public static void init(int maxTotal, int maxPerRoute) { 70 CONNECTION_MANAGER = buildPoolingHttpClientConnectionManager(maxTotal, maxPerRoute); 71 } 72 73 74 public static HttpResponse httpGet(HttpRequest httpRequest) throws Exception { 75 return httpGet(httpRequest, null); 76 } 77 78 /** 79 * 发送 HTTP GET请求 80 * 81 * @param httpRequest 请求参数,如url,header等。 82 * @param httpOptions 配置参数,如重试次数、超时时间等。 83 * @return 84 * @throws Exception 85 */ 86 public static HttpResponse httpGet(HttpRequest httpRequest, HttpOptions httpOptions) throws Exception { 87 // 装载请求地址和参数 88 URIBuilder ub = new URIBuilder(httpRequest.getUrl()); 89 90 // 转换请求参数 91 List<NameValuePair> pairs = convertParams2NVPS(httpRequest.getParams()); 92 if (!pairs.isEmpty()) { 93 ub.setParameters(pairs); 94 } 95 HttpGet httpGet = new HttpGet(ub.build()); 96 97 // 设置请求头 98 if (Objects.nonNull(httpRequest.getHeaders())) { 99 for (Map.Entry<String, String> header : httpRequest.getHeaders().entrySet()) { 100 httpGet.addHeader(header.getKey(), String.valueOf(header.getValue())); 101 } 102 } 103 104 return doHttp(httpGet, httpOptions); 105 } 106 107 108 public static HttpResponse httpPost(HttpRequest httpRequest) throws Exception { 109 return httpPost(httpRequest, null); 110 } 111 112 113 /** 114 * 发送 HTTP POST请求 115 * 116 * @param httpRequest 请求参数 117 * @param httpOptions 配置参数 118 * @return 119 * @throws Exception 120 */ 121 public static HttpResponse httpPost(HttpRequest httpRequest, HttpOptions httpOptions) throws Exception { 122 HttpPost httpPost = new HttpPost(httpRequest.getUrl()); 123 124 // 转换请求参数 125 List<NameValuePair> pairs = convertParams2NVPS(httpRequest.getParams()); 126 if (!pairs.isEmpty()) { 127 httpPost.setEntity(new UrlEncodedFormEntity(pairs, StandardCharsets.UTF_8.name())); 128 } 129 130 // 设置请求头 131 if (Objects.nonNull(httpRequest.getHeaders())) { 132 for (Map.Entry<String, String> header : httpRequest.getHeaders().entrySet()) { 133 httpPost.addHeader(header.getKey(), String.valueOf(header.getValue())); 134 } 135 } 136 137 return doHttp(httpPost, httpOptions); 138 } 139 140 141 /** 142 * 发送 HTTP POST请求,参数格式JSON 143 * <p>请求参数是JSON格式,数据编码是UTF-8</p> 144 * 145 * @param url 146 * @param param 147 * @return 148 * @throws Exception 149 */ 150 public static HttpResponse httpPostJson(String url, String param, HttpOptions httpOptions) throws Exception { 151 HttpPost httpPost = new HttpPost(url); 152 153 // 设置请求头 154 httpPost.addHeader("Content-Type", "application/json; charset=UTF-8"); 155 156 // 设置请求参数 157 httpPost.setEntity(new StringEntity(param, StandardCharsets.UTF_8.name())); 158 159 return doHttp(httpPost, httpOptions); 160 } 161 162 /** 163 * 发送 HTTP POST请求,参数格式XML 164 * <p>请求参数是XML格式,数据编码是UTF-8</p> 165 * 166 * @param url 167 * @param param 168 * @return 169 * @throws Exception 170 */ 171 public static HttpResponse httpPostXml(String url, String param, HttpOptions httpOptions) throws Exception { 172 HttpPost httpPost = new HttpPost(url); 173 174 // 设置请求头 175 httpPost.addHeader("Content-Type", "application/xml; charset=UTF-8"); 176 177 // 设置请求参数 178 httpPost.setEntity(new StringEntity(param, StandardCharsets.UTF_8.name())); 179 180 return doHttp(httpPost, httpOptions); 181 } 182 183 /** 184 * 通过post发送multipart信息。 185 * 186 * @param url 187 * @param multiparts 188 * @param httpOptions 189 * @return 190 * @throws Exception 191 */ 192 public static HttpResponse httpPostMultipart(String url, Map<String, ContentBody> multiparts, HttpOptions httpOptions) throws Exception { 193 HttpPost httpPost = new HttpPost(url); 194 195 // 设置Multipart 196 if (Objects.nonNull(multiparts)) { 197 MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create(); 198 for (Map.Entry<String, ContentBody> multipartEntry : multiparts.entrySet()) { 199 multipartEntityBuilder.addPart(multipartEntry.getKey(), multipartEntry.getValue()); 200 } 201 202 httpPost.setEntity(multipartEntityBuilder.build()); 203 } 204 205 return doHttp(httpPost, httpOptions); 206 } 207 208 209 /** 210 * 转换请求参数,将Map键值对拼接成QueryString字符串 211 * 212 * @param params 213 * @return 214 */ 215 public static String convertParams2QueryStr(Map<String, ?> params) { 216 List<NameValuePair> pairs = convertParams2NVPS(params); 217 218 return URLEncodedUtils.format(pairs, StandardCharsets.UTF_8.name()); 219 } 220 221 /** 222 * 转换请求参数 223 * 224 * @param params 225 * @return 226 */ 227 public static List<NameValuePair> convertParams2NVPS(Map<String, ?> params) { 228 if (Objects.isNull(params)) { 229 return new ArrayList<>(); 230 } 231 232 return params.entrySet().stream().map(param -> new BasicNameValuePair(param.getKey(), String.valueOf(param.getValue()))).collect(Collectors.toList()); 233 } 234 235 /** 236 * 发送 HTTP 请求 237 * 238 * @param request 239 * @return 240 * @throws Exception 241 */ 242 private static HttpResponse doHttp(HttpRequestBase request, HttpOptions httpOptions) throws Exception { 243 if (Objects.isNull(httpOptions)) {//如果为空,则用默认的。 244 httpOptions = HttpOptions.DEFAULT_HTTP_OPTION; 245 } 246 // 设置超时时间 247 if (Objects.nonNull(httpOptions.getTimeoutMs())) { 248 request.setConfig(RequestConfig.custom().setSocketTimeout(httpOptions.getTimeoutMs()).build()); 249 } 250 251 //设置重试策略 252 HttpRequestRetryHandler httpRequestRetryHandler = null; 253 if (Objects.nonNull(httpOptions.getRetryCount())) { 254 httpRequestRetryHandler = new HttpRequestRetryHandler(httpOptions.getRetryCount()); 255 } 256 257 // 通过连接池获取连接对象 258 CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(CONNECTION_MANAGER).setRetryHandler(httpRequestRetryHandler).build(); 259 return doRequest(httpClient, request); 260 261 } 262 263 /** 264 * 处理Http/Https请求,并返回请求结果 265 * <p>注:默认请求编码方式 UTF-8</p> 266 * 267 * @param httpClient 268 * @param request 269 * @return 270 * @throws Exception 271 */ 272 private static HttpResponse doRequest(CloseableHttpClient httpClient, HttpRequestBase request) throws Exception { 273 HttpResponse res = new HttpResponse(); 274 CloseableHttpResponse response = null; 275 long start = System.currentTimeMillis(); 276 277 try { 278 // 获取请求结果 279 response = httpClient.execute(request); 280 281 // 解析请求结果 282 HttpEntity entity = response.getEntity(); 283 String result = EntityUtils.toString(entity, StandardCharsets.UTF_8.name()); // 转换结果 284 EntityUtils.consume(entity); // 关闭IO流 285 286 //解析返回header 287 Map<String, String> headers = new HashMap<>(response.getAllHeaders().length); 288 for (Header header : response.getAllHeaders()) { 289 headers.put(header.getName(), header.getValue()); 290 } 291 292 res.setStatusCode(response.getStatusLine().getStatusCode()).setResult(result).setHeaders(headers); 293 } finally { 294 if (Objects.nonNull(response)) { 295 response.close(); 296 } 297 } 298 299 long elapsed = System.currentTimeMillis() - start; 300 logger.debug("op=end_doRequest,request={},res={},elapsed={}", request, res, elapsed); 301 return res; 302 } 303 304 305 /** 306 * 初始化连接池 307 * 308 * @return 309 */ 310 private static PoolingHttpClientConnectionManager buildPoolingHttpClientConnectionManager(Integer maxTotal, Integer maxPerRoute) { 311 // 初始化连接池,可用于请求HTTP/HTTPS(信任所有证书) 312 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(getRegistry()); 313 314 // 整个连接池的最大连接数 315 String maxTotalProperty = null; 316 if (Objects.nonNull(maxTotal)) { //首先看有没有在参数中显式指定 317 connectionManager.setMaxTotal(maxTotal); 318 } else { //如果没有在参数中显式指定,则再看有没有在系统变量中指定 319 maxTotalProperty = System.getProperty(Constants.SYSTEM_PROPERTY_KEY_HTTP_MAX_TOTAL); 320 if (Objects.nonNull(maxTotalProperty)) { 321 connectionManager.setMaxTotal(Integer.valueOf(maxTotalProperty)); 322 } 323 } 324 325 // 每个路由的最大连接数 326 String maxPerRouteProperty = null; 327 if (Objects.nonNull(maxPerRoute)) { //首先看有没有在参数中显式指定 328 connectionManager.setDefaultMaxPerRoute(maxPerRoute); 329 } else { //如果没有在参数中显式指定,则再看有没有在系统变量中指定 330 maxPerRouteProperty = System.getProperty(Constants.SYSTEM_PROPERTY_KEY_HTTP_MAX_PER_ROUTE); 331 if (Objects.nonNull(maxPerRouteProperty)) { 332 connectionManager.setDefaultMaxPerRoute(Integer.valueOf(maxPerRouteProperty)); 333 } 334 } 335 336 logger.info("[ZZARCH_COMMON_SUCCESS_buildPoolingHttpClientConnectionManager]maxTotal={},maxPerRoute={},maxTotalProperty={},maxPerRouteProperty={}", maxTotal, maxPerRoute, maxTotalProperty, maxPerRouteProperty); 337 return connectionManager; 338 } 339 340 341 /** 342 * 获取 HTTPClient注册器 343 * 344 * @return 345 * @throws Exception 346 */ 347 private static Registry<ConnectionSocketFactory> getRegistry() { 348 try { 349 return RegistryBuilder.<ConnectionSocketFactory>create().register("http", new PlainConnectionSocketFactory()).register("https", new SSLConnectionSocketFactory(SSLContext.getDefault())).build(); 350 } catch (Exception e) { 351 logger.error("[ERROR_getRegistry]", e); 352 } 353 354 return null; 355 } 356 }
end