package com.fanruan.api.net.http; import com.fanruan.api.macro.EncodeConstants; import com.fanruan.api.log.LogKit; import com.fanruan.api.net.http.rs.BaseHttpResponseHandle; import com.fanruan.api.net.http.rs.HttpRequest; import com.fanruan.api.net.http.rs.HttpRequestType; import com.fanruan.api.net.http.rs.HttpResponseType; import com.fanruan.api.net.http.rs.StreamResponseHandle; import com.fanruan.api.net.http.rs.TextResponseHandle; import com.fanruan.api.net.http.rs.UploadResponseHandle; import com.fr.third.guava.collect.Maps; import com.fr.third.org.apache.http.HttpEntity; import com.fr.third.org.apache.http.HttpEntityEnclosingRequest; import com.fr.third.org.apache.http.HttpHost; import com.fr.third.org.apache.http.NameValuePair; import com.fr.third.org.apache.http.NoHttpResponseException; import com.fr.third.org.apache.http.client.HttpRequestRetryHandler; import com.fr.third.org.apache.http.client.config.RequestConfig; import com.fr.third.org.apache.http.client.entity.UrlEncodedFormEntity; import com.fr.third.org.apache.http.client.methods.CloseableHttpResponse; import com.fr.third.org.apache.http.client.methods.HttpEntityEnclosingRequestBase; import com.fr.third.org.apache.http.client.methods.HttpRequestBase; import com.fr.third.org.apache.http.client.protocol.HttpClientContext; import com.fr.third.org.apache.http.client.utils.URIBuilder; import com.fr.third.org.apache.http.config.Registry; import com.fr.third.org.apache.http.config.RegistryBuilder; import com.fr.third.org.apache.http.conn.routing.HttpRoute; import com.fr.third.org.apache.http.conn.socket.ConnectionSocketFactory; import com.fr.third.org.apache.http.conn.socket.LayeredConnectionSocketFactory; import com.fr.third.org.apache.http.conn.socket.PlainConnectionSocketFactory; import com.fr.third.org.apache.http.conn.ssl.SSLConnectionSocketFactory; import com.fr.third.org.apache.http.entity.FileEntity; import com.fr.third.org.apache.http.entity.mime.HttpMultipartMode; import com.fr.third.org.apache.http.entity.mime.MultipartEntityBuilder; import com.fr.third.org.apache.http.impl.client.CloseableHttpClient; import com.fr.third.org.apache.http.impl.client.HttpClients; import com.fr.third.org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import com.fr.third.org.apache.http.message.BasicNameValuePair; import com.fr.third.org.apache.http.protocol.HttpContext; import com.fr.third.org.apache.http.ssl.SSLContexts; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.SSLHandshakeException; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InterruptedIOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.net.URLEncoder; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import static com.fanruan.api.net.http.rs.HttpRequestType.POST; /** * @author richie * @version 10.0 * Created by richie on 2019-08-29 *

* http请求工具类,封装了用于http请求的各种方法 *

*/ public class HttpKit { private static final int RETRY_TIMES = 5; private static CloseableHttpClient httpClient = null; private final static Object SYNC_LOCK = new Object(); /** * 根据请求地址创建HttpClient对象 * * @param url 请求地址 * @return HttpClient对象 */ public static CloseableHttpClient getHttpClient(String url) { String hostname = url.split("/")[2]; int port = 80; if (hostname.contains(":")) { String[] arr = hostname.split(":"); hostname = arr[0]; port = Integer.parseInt(arr[1]); } if (httpClient == null) { synchronized (SYNC_LOCK) { if (httpClient == null) { httpClient = createHttpClient(hostname, port, SSLContexts.createDefault()); } } } return httpClient; } public static CloseableHttpClient createHttpClient(String hostname, int port, SSLContext sslContext) { return createHttpClient(200, 40, 100, hostname, port, sslContext); } private static CloseableHttpClient createHttpClient(int maxTotal, int maxPerRoute, int maxRoute, String hostname, int port, SSLContext sslContext) { ConnectionSocketFactory socketFactory = PlainConnectionSocketFactory.getSocketFactory(); LayeredConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext); Registry registry = RegistryBuilder .create() .register("http", socketFactory) .register("https", sslConnectionSocketFactory) .build(); PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(registry); // 将最大连接数增加 cm.setMaxTotal(maxTotal); // 将每个路由基础的连接增加 cm.setDefaultMaxPerRoute(maxPerRoute); HttpHost httpHost = new HttpHost(hostname, port); // 将目标主机的最大连接数增加 cm.setMaxPerRoute(new HttpRoute(httpHost), maxRoute); // 请求重试处理 HttpRequestRetryHandler httpRequestRetryHandler = new HttpRequestRetryHandler() { @Override public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { if (executionCount >= RETRY_TIMES) {// 如果已经重试了5次,就放弃 return false; } if (exception instanceof NoHttpResponseException) {// 如果服务器丢掉了连接,那么就重试 return true; } if (exception instanceof SSLHandshakeException) {// 不要重试SSL握手异常 return false; } if (exception instanceof InterruptedIOException) {// 超时 return false; } if (exception instanceof UnknownHostException) {// 目标服务器不可达 return false; } if (exception instanceof SSLException) {// SSL握手异常 return false; } HttpClientContext clientContext = HttpClientContext.adapt(context); com.fr.third.org.apache.http.HttpRequest request = clientContext.getRequest(); // 如果请求是幂等的,就再次尝试 return !(request instanceof HttpEntityEnclosingRequest); } }; return HttpClients.custom() .setConnectionManager(cm) .setRetryHandler(httpRequestRetryHandler) .build(); } /** * 设置 httpEntity * * @param requestBase 请求体 * @param httpRequest 请求 */ private static void setHttpEntity(@NotNull HttpEntityEnclosingRequestBase requestBase, @NotNull HttpRequest httpRequest) { HttpEntity httpEntity = httpRequest.getHttpEntity(); if (httpEntity != null) { // 如果存在 httpEntity 直接设置 requestBase.setEntity(httpEntity); return; } Map params = httpRequest.getParams(); if (params == null || params.isEmpty()) { return; } List pairs = new ArrayList(); for (Map.Entry entry : params.entrySet()) { pairs.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } try { requestBase.setEntity(new UrlEncodedFormEntity(pairs, httpRequest.getEncoding())); } catch (UnsupportedEncodingException e) { LogKit.error(e.getMessage(), e); } } private static Map transformMap(Map oldMap) { if (oldMap == null) { return null; } return Maps.transformEntries(oldMap, new Maps.EntryTransformer() { @Override public String transformEntry(@Nullable String key, @Nullable V value) { return value == null ? null : value.toString(); } }); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @return 服务器返回的文本内容 */ public static String post(String url, Map params) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .post(transformMap(params)) .build()); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @param responseType 返回类型 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#execute(HttpRequest) */ @Deprecated public static T post(String url, Map params, HttpResponseType responseType) throws IOException { CloseableHttpResponse response = execute(HttpRequest .custom() .url(url) .post(transformMap(params)) .build()); return responseType.result(response, null); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @param headers 请求头 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#executeAndParse(HttpRequest) */ @Deprecated public static String post(String url, Map params, Map headers) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .post(transformMap(params)) .headers(headers) .build()); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @param headers 请求头 * @param responseType 返回类型 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#execute(HttpRequest) */ @Deprecated public static T post(String url, Map params, Map headers, HttpResponseType responseType) throws IOException { CloseableHttpResponse response = execute(HttpRequest .custom() .url(url) .post(transformMap(params)) .headers(headers) .build()); return responseType.result(response, null); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @param responseEncoding 响应的文本的编码 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#executeAndParse(HttpRequest, BaseHttpResponseHandle) */ @Deprecated public static String post(String url, Map params, String responseEncoding) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .post(transformMap(params)) .build(), new TextResponseHandle(responseEncoding)); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @param responseEncoding 响应的文本的编码 * @param headers 请求头 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#executeAndParse(HttpRequest, BaseHttpResponseHandle) */ @Deprecated public static String post(String url, Map params, String responseEncoding, Map headers) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .post(transformMap(params)) .headers(headers) .build(), new TextResponseHandle(responseEncoding)); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @param responseEncoding 响应的文本的编码 * @param paramsEncoding 参数编码 * @param headers 请求头 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#executeAndParse(HttpRequest, BaseHttpResponseHandle) */ public static String post(String url, Map params, String responseEncoding, String paramsEncoding, Map headers) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .post(transformMap(params)) .encoding(paramsEncoding) .headers(headers) .build(), new TextResponseHandle(responseEncoding)); } /** * 发起POST请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params POST请求的参数 * @param responseEncoding 响应的文本的编码 * @param paramsEncoding 参数编码 * @param headers 请求头 * @param responseType 返回值类型 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#execute(HttpRequest) */ public static T post(String url, Map params, String responseEncoding, String paramsEncoding, Map headers, HttpResponseType responseType) throws IOException { CloseableHttpResponse response = execute(HttpRequest .custom() .url(url) .post(transformMap(params)) .encoding(paramsEncoding) .headers(headers) .build()); return responseType.result(response, responseEncoding); } /** * 发起GET请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @return 服务器返回的文本内容 */ public static String get(String url) throws IOException { return executeAndParse(HttpRequest.custom().url(url).build()); } /** * 发起GET请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params 参数 * @return 服务器返回的文本内容 */ public static String get(String url, Map params) throws IOException { return executeAndParse(HttpRequest.custom().url(url).params(params).build()); } /** * 发起GET请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params 参数 * @param headers 请求头 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#executeAndParse(HttpRequest, BaseHttpResponseHandle) */ public static String get(String url, Map params, Map headers) throws IOException { return executeAndParse(HttpRequest.custom().url(url).params(params).headers(headers).build()); } /** * 发起GET请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params 参数 * @param responseEncoding 返回的文本的编码 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#executeAndParse(HttpRequest, BaseHttpResponseHandle) */ public static String get(String url, Map params, String responseEncoding) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .params(params) .build(), new TextResponseHandle(responseEncoding)); } /** * 发起GET请求并获取返回的文本 * * @param url 响应请求的的服务器地址 * @param params 参数 * @param responseEncoding 返回的文本的编码 * @return 服务器返回的文本内容 * @see com.fanruan.api.net.http.HttpKit#executeAndParse(HttpRequest, BaseHttpResponseHandle) */ public static String get(String url, Map params, String responseEncoding, Map headers) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .params(params) .headers(headers) .build(), new TextResponseHandle(responseEncoding)); } /** * 从指定的地址下载文件 * @param url 文件下载地址 * @return 文件的字节流 * @throws IOException 下载过程中出现错误则抛出此异常 */ public static ByteArrayInputStream download(String url) throws IOException { return executeAndParse(HttpRequest.custom().url(url).build(), StreamResponseHandle.DEFAULT); } /** * 从指定的地址下载文件 * @param url 文件下载地址 * @param params 参数对 * @param responseEncoding 响应的文件编码 * @param headers 请求头 * @return 文件的字节流 * @throws IOException 下载过程中出现错误则抛出此异常 */ public static ByteArrayInputStream download(String url, Map params, String responseEncoding, Map headers) throws IOException { return executeAndParse(HttpRequest .custom() .url(url) .params(params) .headers(headers) .build(), new StreamResponseHandle(responseEncoding)); } /** * 上传文件到指定的服务器 * * @param url 接收文件的服务器地址 * @param file 要上传的文件,默认的文件编码为utf-8 * @throws IOException 上传中出现错误则抛出此异常 */ public static void upload(String url, File file) throws IOException { upload(url, file, Charset.forName("utf-8")); } /** * 上传文件到指定的服务器 * * @param url 接收文件的服务器地址 * @param file 要上传的文件 * @param charset 文件的编码 * @throws IOException 上传中出现错误则抛出此异常 */ public static void upload(String url, File file, Charset charset) throws IOException { upload(url, new FileEntity(file), charset); } /** * 上传文件到指定的服务器 * * @param url 接收文件的服务器地址 * @param builder 附件构造器 * @param charset 文件的编码 * @throws IOException 上传中出现错误则抛出此异常 */ public static void upload(String url, MultipartEntityBuilder builder, Charset charset) throws IOException { upload(url, builder, charset, Collections.emptyMap(), POST); } /** * 上传文件到指定的服务器 * * @param url 接收文件的服务器地址 * @param fileEntity 文件实体 * @param charset 文件的编码 * @throws IOException 上传中出现错误则抛出此异常 */ public static void upload(String url, FileEntity fileEntity, Charset charset) throws IOException { upload(url, fileEntity, charset, Collections.emptyMap(), POST); } /** * 上传多文件到指定的服务器 * * @param url 接收文件的服务器地址 * @param builder 附件构造器 * @param charset 文件的编码 * @param headers 请求头 * @param httpRequestType 请求类型 * @throws IOException 上传中出现错误则抛出此异常 */ public static void upload(String url, MultipartEntityBuilder builder, Charset charset, Map headers, HttpRequestType httpRequestType) throws IOException { // richie:采用浏览器模式,防止出现乱码 builder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE); HttpEntity reqEntity = builder.setCharset(charset).build(); upload(url, reqEntity, charset, headers, httpRequestType); } /** * 上传文件到指定的服务器 * * @param url 接收文件的服务器地址 * @param reqEntity 请求实体 * @param charset 文件的编码 * @param headers 请求头 * @param httpRequestType 请求类型 * @throws IOException 上传中出现错误则抛出此异常 */ public static void upload(String url, HttpEntity reqEntity, Charset charset, Map headers, HttpRequestType httpRequestType) throws IOException { executeAndParse(HttpRequest .custom() .url(url) .headers(headers) .method(httpRequestType) .httpEntity(reqEntity) .encoding(charset.toString()) .build(), UploadResponseHandle.DEFAULT); } /** * 请求资源或服务,使用默认文本http解析器,UTF-8编码 * * @param httpRequest httpRequest * @return 返回处理结果 */ public static String executeAndParse(HttpRequest httpRequest) throws IOException { return executeAndParse(httpRequest, TextResponseHandle.DEFAULT); } /** * 请求资源或服务,自请求参数,并指定 http 响应处理器 * 例: *
     *      String res = HttpToolbox.executeAndParse(HttpRequest
     *              .custom()
     *              .url("")
     *              .build(),
     *          TextResponseHandle.DEFAULT);
     * 
* * @param httpRequest httpRequest * @param handle http 解析器 * @return 返回处理结果 */ public static T executeAndParse(HttpRequest httpRequest, BaseHttpResponseHandle handle) throws IOException { return handle.parse(execute(httpRequest)); } /** * 请求资源或服务,传入请求参数 * * @param httpRequest httpRequest * @return 返回处理结果 */ public static CloseableHttpResponse execute(HttpRequest httpRequest) throws IOException { return execute(getHttpClient(httpRequest.getUrl()), httpRequest); } /** * 请求资源或服务,自定义client对象,传入请求参数 * * @param httpClient http客户端 * @param httpRequest httpRequest * @return 返回处理结果 */ public static CloseableHttpResponse execute(CloseableHttpClient httpClient, HttpRequest httpRequest) throws IOException { String url = httpRequest.getUrl(); // 创建请求对象 HttpRequestBase httpRequestBase = httpRequest.getMethod().createHttpRequest(url); // 设置header信息 httpRequestBase.setHeader("User-Agent", "Mozilla/5.0"); Map headers = httpRequest.getHeaders(); if (headers != null && !headers.isEmpty()) { for (Map.Entry entry : headers.entrySet()) { httpRequestBase.setHeader(entry.getKey(), entry.getValue()); } } // 配置请求的设置 RequestConfig requestConfig = httpRequest.getConfig(); if (requestConfig != null) { httpRequestBase.setConfig(requestConfig); } // 判断是否支持设置entity(仅HttpPost、HttpPut、HttpPatch支持) if (HttpEntityEnclosingRequestBase.class.isAssignableFrom(httpRequestBase.getClass())) { setHttpEntity((HttpEntityEnclosingRequestBase) httpRequestBase, httpRequest); } else { Map params = httpRequest.getParams(); if (params != null && !params.isEmpty()) { // 注意get等不支持设置entity需要更新拼接之后的URL,但是url变量没有更新 httpRequestBase.setURI(URI.create(buildUrl(url, params, httpRequest.getEncoding()))); } } return httpClient.execute(httpRequestBase); } /** * 构建 Url * * @param url 请求地址 * @param params 参数 * @return 拼接之后的地址 */ public static String buildUrl(String url, Map params) { try { return buildUrl(url, params, EncodeConstants.ENCODING_UTF_8); } catch (UnsupportedEncodingException ignore) { } return url; } /** * 构建 Url * * @param url 请求地址 * @param params 参数 * @return 拼接之后的地址 * @throws UnsupportedEncodingException 不支持的编码 */ private static String buildUrl(String url, Map params, String paramsEncoding) throws UnsupportedEncodingException { if (params == null || params.isEmpty()) { return url; } URIBuilder builder; try { builder = new URIBuilder(url); for (Map.Entry entry : params.entrySet()) { String key = URLEncoder.encode(entry.getKey(), paramsEncoding); String value = URLEncoder.encode(entry.getValue(), paramsEncoding); builder.setParameter(key, value); } return builder.build().toString(); } catch (URISyntaxException e) { LogKit.debug("Error to build url, please check the arguments."); } return url; } }