diff --git a/pom.xml b/pom.xml index 4737dfd..10510ae 100644 --- a/pom.xml +++ b/pom.xml @@ -121,6 +121,12 @@ 1.7.1 test + + com.squareup.okhttp3 + mockwebserver + 4.0.1 + test + ${project.artifactId}-${version}-${maven.build.timestamp} diff --git a/src/main/java/com/fanruan/api/net/http/HttpKit.java b/src/main/java/com/fanruan/api/net/http/HttpKit.java new file mode 100644 index 0000000..de7ae96 --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/HttpKit.java @@ -0,0 +1,677 @@ +package com.fanruan.api.net.http; + +import com.fanruan.api.consts.EncodeConstantsKit; +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, EncodeConstantsKit.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; + } +} diff --git a/src/main/java/com/fanruan/api/net/http/rs/BaseHttpResponseHandle.java b/src/main/java/com/fanruan/api/net/http/rs/BaseHttpResponseHandle.java new file mode 100644 index 0000000..4198916 --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/rs/BaseHttpResponseHandle.java @@ -0,0 +1,54 @@ +package com.fanruan.api.net.http.rs; + +import com.fr.stable.EncodeConstants; +import com.fr.third.org.apache.http.client.methods.CloseableHttpResponse; + +import java.io.IOException; + +/** + * http 结果解析器 + * + * @author vito + * @date 2019-07-14 + */ +public abstract class BaseHttpResponseHandle { + + /** + * 解析编码,默认为 UTF_8 + */ + private String encoding = EncodeConstants.ENCODING_UTF_8; + + public BaseHttpResponseHandle() { + } + + public BaseHttpResponseHandle(String encoding) { + this.encoding = encoding; + } + + /** + * 获取解析编码 + * + * @return 解析编码 + */ + public String getEncoding() { + return encoding; + } + + /** + * 设置解析编码 + * + * @param encoding 解析编码 + */ + public void setEncoding(String encoding) { + this.encoding = encoding; + } + + /** + * 解析响应结果 + * + * @param response 响应 + * @return 解析结果 + * @throws IOException io异常 + */ + public abstract T parse(CloseableHttpResponse response) throws IOException; +} diff --git a/src/main/java/com/fanruan/api/net/http/rs/HttpRequest.java b/src/main/java/com/fanruan/api/net/http/rs/HttpRequest.java new file mode 100644 index 0000000..d29aaec --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/rs/HttpRequest.java @@ -0,0 +1,215 @@ +package com.fanruan.api.net.http.rs; + +import com.fanruan.api.consts.EncodeConstantsKit; +import com.fr.third.org.apache.http.HttpEntity; +import com.fr.third.org.apache.http.client.config.RequestConfig; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collections; +import java.util.Map; + +/** + * @author richie + * @version 10.0 + * Created by richie on 2019-08-29 + */ +public class HttpRequest { + + private static final int TIME_OUT = 10 * 1000; + private static final RequestConfig DEFAULT = RequestConfig + .custom() + .setConnectionRequestTimeout(TIME_OUT) + .setConnectTimeout(TIME_OUT) + .setSocketTimeout(TIME_OUT) + .build(); + /** + * 请求地址 + */ + private String url; + + /** + * 请求头 + */ + private Map headers; + + /** + * 请求参数 + */ + private Map params; + + /** + * 请求参数 + * + * @see RequestConfig + */ + @Nullable + private RequestConfig config; + + /** + * 请求参数 + * + * @see HttpEntity + */ + @Nullable + private HttpEntity httpEntity; + + /** + * 请求方法 + */ + private HttpRequestType method; + + /** + * 参数字符集 + */ + private String encoding; + + private HttpRequest(Builder builder) { + this.url = builder.url; + this.headers = builder.headers; + this.params = builder.params; + this.config = builder.config; + this.encoding = builder.encoding; + this.httpEntity = builder.httpEntity; + this.method = builder.method; + } + + public String getUrl() { + return url; + } + + public Map getHeaders() { + return headers; + } + + public Map getParams() { + return params; + } + + public RequestConfig getConfig() { + return config; + } + + public String getEncoding() { + return encoding; + } + + public HttpEntity getHttpEntity() { + return httpEntity; + } + + public HttpRequestType getMethod() { + return method; + } + + public static Builder custom() { + return new Builder(); + } + + public static final class Builder { + private String url; + private Map headers = Collections.emptyMap(); + private Map params = Collections.emptyMap(); + @Nullable + private RequestConfig config = DEFAULT; + @Nullable + private HttpEntity httpEntity; + private String encoding = EncodeConstantsKit.ENCODING_UTF_8; + private HttpRequestType method = HttpRequestType.GET; + + private Builder() { + } + + public HttpRequest build() { + if (this.url == null) { + throw new IllegalStateException("url == null"); + } + return new HttpRequest(this); + } + + public Builder url(@NotNull String url) { + if (url == null) { + throw new NullPointerException("url == null"); + } + this.url = url; + return this; + } + + public Builder headers(Map headers) { + if (headers != null) { + this.headers = headers; + } + return this; + } + + public Builder params(Map params) { + if (params != null) { + this.params = params; + } + return this; + } + + public Builder config(RequestConfig config) { + this.config = config; + return this; + } + + public Builder get() { + this.method = HttpRequestType.GET; + return this; + } + + public Builder post(HttpEntity httpEntity) { + this.method = HttpRequestType.POST; + this.httpEntity(httpEntity); + return this; + } + + public Builder post(Map params) { + this.method = HttpRequestType.POST; + this.params(params); + return this; + } + + public Builder put(HttpEntity httpEntity) { + this.method = HttpRequestType.PUT; + this.httpEntity(httpEntity); + return this; + } + + public Builder put(Map params) { + this.method = HttpRequestType.PUT; + this.params(params); + return this; + } + + public Builder delete() { + this.method = HttpRequestType.DELETE; + return this; + } + + public Builder encoding(String encoding) { + if (encoding == null) { + throw new NullPointerException("httpEntity == null"); + } + this.encoding = encoding; + return this; + } + + public Builder httpEntity(HttpEntity httpEntity) { + this.httpEntity = httpEntity; + return this; + } + + public Builder method(@NotNull HttpRequestType method) { + if (method == null) { + throw new NullPointerException("method == null"); + } + this.method = method; + return this; + } + + } + + +} \ No newline at end of file diff --git a/src/main/java/com/fanruan/api/net/http/rs/HttpRequestType.java b/src/main/java/com/fanruan/api/net/http/rs/HttpRequestType.java new file mode 100644 index 0000000..1a1ffe9 --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/rs/HttpRequestType.java @@ -0,0 +1,114 @@ +package com.fanruan.api.net.http.rs; + +import com.fr.third.org.apache.http.client.methods.HttpDelete; +import com.fr.third.org.apache.http.client.methods.HttpGet; +import com.fr.third.org.apache.http.client.methods.HttpHead; +import com.fr.third.org.apache.http.client.methods.HttpOptions; +import com.fr.third.org.apache.http.client.methods.HttpPatch; +import com.fr.third.org.apache.http.client.methods.HttpPost; +import com.fr.third.org.apache.http.client.methods.HttpPut; +import com.fr.third.org.apache.http.client.methods.HttpRequestBase; +import com.fr.third.org.apache.http.client.methods.HttpTrace; + +/** + * @author richie + * @version 10.0 + * Created by richie on 2019-08-29 + */ +public enum HttpRequestType { + /** + * 求获取Request-URI所标识的资源 + */ + GET("GET") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpGet(url); + } + }, + + /** + * 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。 + * POST请求可能会导致新的资源的建立和/或已有资源的修改 + */ + POST("POST") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpPost(url); + } + }, + + /** + * 向服务器索要与GET请求相一致的响应,只不过响应体将不会被返回。 + * 这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息 + * 只获取响应信息报头 + */ + HEAD("HEAD") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpHead(url); + } + }, + + /** + * 向指定资源位置上传其最新内容(全部更新,操作幂等) + */ + PUT("PUT") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpPut(url); + } + }, + + /** + * 请求服务器删除Request-URI所标识的资源 + */ + DELETE("DELETE") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpDelete(url); + } + }, + + /** + * 请求服务器回送收到的请求信息,主要用于测试或诊断 + */ + TRACE("TRACE") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpTrace(url); + } + }, + + /** + * 向指定资源位置上传其最新内容(部分更新,非幂等) + */ + PATCH("PATCH") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpPatch(url); + } + }, + + /** + * 返回服务器针对特定资源所支持的HTTP请求方法。 + * 也可以利用向Web服务器发送'*'的请求来测试服务器的功能性 + */ + OPTIONS("OPTIONS") { + @Override + public HttpRequestBase createHttpRequest(String url) { + return new HttpOptions(url); + } + }; + + public abstract HttpRequestBase createHttpRequest(String url); + + private String name; + + HttpRequestType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/fanruan/api/net/http/rs/HttpResponseType.java b/src/main/java/com/fanruan/api/net/http/rs/HttpResponseType.java new file mode 100644 index 0000000..a787422 --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/rs/HttpResponseType.java @@ -0,0 +1,103 @@ +package com.fanruan.api.net.http.rs; + +import com.fr.third.org.apache.http.HttpEntity; +import com.fr.third.org.apache.http.client.methods.CloseableHttpResponse; +import com.fr.third.org.apache.http.client.methods.HttpUriRequest; +import com.fr.third.org.apache.http.client.protocol.HttpClientContext; +import com.fr.third.org.apache.http.impl.client.CloseableHttpClient; +import com.fr.third.org.apache.http.util.EntityUtils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * @author richie + * @version 10.0 + * Created by richie on 2019-08-29 + */ +public interface HttpResponseType { + + /** + * 处理http响应 + * + * @param client 客户端 + * @param url 地址 + * @param request 请求 + * @param charset 字符集 + * @return 处理之后的响应 + * @throws IOException 异常 + */ + @Deprecated + T result(CloseableHttpClient client, String url, HttpUriRequest request, String charset) throws IOException; + + /** + * 处理http响应 + * + * @param response 响应 + * @param charset 字符集 + * @return 处理之后的响应 + * @throws IOException 异常 + */ + T result(CloseableHttpResponse response, String charset) throws IOException; + + HttpResponseType TEXT = new HttpResponseType() { + + @Override + public String result(CloseableHttpClient client, String url, HttpUriRequest request, String charset) throws IOException { + CloseableHttpResponse response = client.execute(request, HttpClientContext.create()); + return result(response, charset); + } + + @Override + public String result(CloseableHttpResponse response, String charset) throws IOException { + try { + HttpEntity entity = response.getEntity(); + String result = EntityUtils.toString(entity, charset); + EntityUtils.consume(entity); + return result; + } finally { + if (response != null) { + response.close(); + } + } + } + }; + + HttpResponseType STREAM = new HttpResponseType() { + + @Override + public ByteArrayInputStream result(CloseableHttpClient client, String url, HttpUriRequest request, String charset) throws IOException { + CloseableHttpResponse response = client.execute(request, HttpClientContext.create()); + return result(response, charset); + } + + @Override + public ByteArrayInputStream result(CloseableHttpResponse response, String charset) throws IOException { + InputStream in = null; + try { + HttpEntity entity = response.getEntity(); + if (entity != null) { + in = entity.getContent(); + byte[] buff = new byte[8000]; + int bytesRead; + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + while ((bytesRead = in.read(buff)) != -1) { + bao.write(buff, 0, bytesRead); + } + byte[] data = bao.toByteArray(); + return new ByteArrayInputStream(data); + } + return null; + } finally { + if (response != null) { + response.close(); + } + if (in != null) { + in.close(); + } + } + } + }; +} \ No newline at end of file diff --git a/src/main/java/com/fanruan/api/net/http/rs/StreamResponseHandle.java b/src/main/java/com/fanruan/api/net/http/rs/StreamResponseHandle.java new file mode 100644 index 0000000..e10bfdd --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/rs/StreamResponseHandle.java @@ -0,0 +1,55 @@ +package com.fanruan.api.net.http.rs; + +import com.fr.third.org.apache.http.HttpEntity; +import com.fr.third.org.apache.http.client.methods.CloseableHttpResponse; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 流响应解析器 + * + * @author vito + * @date 2019-07-14 + */ +public class StreamResponseHandle extends BaseHttpResponseHandle { + + public static final StreamResponseHandle DEFAULT = new StreamResponseHandle(); + private static final int BUFFER_LENGTH = 8000; + + public StreamResponseHandle() { + } + + public StreamResponseHandle(String encoding) { + super(encoding); + } + + @Override + public ByteArrayInputStream parse(CloseableHttpResponse response) throws IOException { + InputStream in = null; + try { + HttpEntity entity = response.getEntity(); + if (entity != null) { + in = entity.getContent(); + byte[] buff = new byte[BUFFER_LENGTH]; + int bytesRead; + ByteArrayOutputStream bao = new ByteArrayOutputStream(); + while ((bytesRead = in.read(buff)) != -1) { + bao.write(buff, 0, bytesRead); + } + byte[] data = bao.toByteArray(); + return new ByteArrayInputStream(data); + } + return null; + } finally { + if (response != null) { + response.close(); + } + if (in != null) { + in.close(); + } + } + } +} diff --git a/src/main/java/com/fanruan/api/net/http/rs/TextResponseHandle.java b/src/main/java/com/fanruan/api/net/http/rs/TextResponseHandle.java new file mode 100644 index 0000000..c8c58c0 --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/rs/TextResponseHandle.java @@ -0,0 +1,39 @@ +package com.fanruan.api.net.http.rs; + +import com.fr.third.org.apache.http.HttpEntity; +import com.fr.third.org.apache.http.client.methods.CloseableHttpResponse; +import com.fr.third.org.apache.http.util.EntityUtils; + +import java.io.IOException; + +/** + * 文本响应解析器 + * + * @author vito + * @date 2019-07-14 + */ +public class TextResponseHandle extends BaseHttpResponseHandle { + + public static final TextResponseHandle DEFAULT = new TextResponseHandle(); + + public TextResponseHandle() { + } + + public TextResponseHandle(String encoding) { + super(encoding); + } + + @Override + public String parse(CloseableHttpResponse response) throws IOException { + try { + HttpEntity entity = response.getEntity(); + String result = EntityUtils.toString(entity, getEncoding()); + EntityUtils.consume(entity); + return result; + } finally { + if (response != null) { + response.close(); + } + } + } +} diff --git a/src/main/java/com/fanruan/api/net/http/rs/UploadResponseHandle.java b/src/main/java/com/fanruan/api/net/http/rs/UploadResponseHandle.java new file mode 100644 index 0000000..3b68be0 --- /dev/null +++ b/src/main/java/com/fanruan/api/net/http/rs/UploadResponseHandle.java @@ -0,0 +1,48 @@ +package com.fanruan.api.net.http.rs; + +import com.fr.third.org.apache.http.HttpEntity; +import com.fr.third.org.apache.http.HttpStatus; +import com.fr.third.org.apache.http.client.methods.CloseableHttpResponse; +import com.fr.third.org.apache.http.util.EntityUtils; + +import java.io.IOException; + +/** + * 上传响应解析器 + * + * @author vito + * @date 2019-07-14 + */ +public class UploadResponseHandle extends BaseHttpResponseHandle { + + public static final UploadResponseHandle DEFAULT = new UploadResponseHandle(); + + public UploadResponseHandle() { + } + + public UploadResponseHandle(String encoding) { + super(encoding); + } + + @Override + public Void parse(CloseableHttpResponse response) throws IOException { + try { + int statusCode = response.getStatusLine().getStatusCode(); + if (statusCode == HttpStatus.SC_OK) { + HttpEntity entity = response.getEntity(); + if (entity != null) { + EntityUtils.consume(entity); + } + } else { + HttpEntity entity = response.getEntity(); + String result = EntityUtils.toString(entity, getEncoding()); + throw new IOException("Connect error, error code:" + statusCode + "; message:" + result); + } + } finally { + if (response != null) { + response.close(); + } + } + return null; + } +} diff --git a/src/test/java/com/fanruan/api/net/http/HttpKitTest.java b/src/test/java/com/fanruan/api/net/http/HttpKitTest.java new file mode 100644 index 0000000..a034a4a --- /dev/null +++ b/src/test/java/com/fanruan/api/net/http/HttpKitTest.java @@ -0,0 +1,153 @@ +package com.fanruan.api.net.http; + +import com.fanruan.api.Prepare; +import com.fanruan.api.net.http.rs.HttpRequest; +import com.fanruan.api.net.http.rs.HttpResponseType; +import com.fanruan.api.net.http.rs.StreamResponseHandle; +import com.fanruan.api.util.IOKit; +import com.fr.json.JSONObject; +import okhttp3.HttpUrl; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.AfterClass; +import org.junit.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; + +/** + * @author richie + * @version 10.0 + * Created by richie on 2019-08-29 + */ +public class HttpKitTest extends Prepare { + + private static MockWebServer server = new MockWebServer(); + + @AfterClass + public static void tearDown() throws Exception { + server.shutdown(); + } + + @Test + public void testGet() { + String text = null; + try { + text = HttpKit.get("http://www.baidu.com"); + } catch (IOException e) { + e.printStackTrace(); + } + assertNotNull(text); + } + + @Test + public void testPost() { + Map map = new HashMap(); + map.put("key", "bbs"); + try { + String resText = HttpKit.post("https://cloud.fanruan.com/site", map); + assertEquals("http://bbs.fanruan.com/", new JSONObject(resText).get("value")); + } catch (SocketTimeoutException ignore) { + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void testStream() { + Map map = new HashMap(); + map.put("key", "bbs"); + try { + InputStream in = HttpKit.post("https://cloud.fanruan.com/site", map, HttpResponseType.STREAM); + String text = IOKit.inputStream2String(in, StandardCharsets.UTF_8); + assertEquals("{\"value\":\"http://bbs.fanruan.com/\"}", text); + } catch (SocketTimeoutException ignore) { + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void testStreamMock() { + server.enqueue(new MockResponse().setBody("{\"value\":\"http://bbs.fanruan.com/\"}")); + String url = server.url("/site").toString(); + Map map = new HashMap(); + map.put("key", "bbs"); + try { + InputStream in = HttpKit.executeAndParse(HttpRequest + .custom() + .url(url) + .post(map) + .build(), + new StreamResponseHandle()); + String text = IOKit.inputStream2String(in, StandardCharsets.UTF_8); + RecordedRequest takeRequest = server.takeRequest(); + assertEquals("{\"value\":\"http://bbs.fanruan.com/\"}", text); + assertEquals("POST", takeRequest.getMethod()); + assertEquals("key=bbs", takeRequest.getBody().readUtf8()); + } catch (SocketTimeoutException ignore) { + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + + @Test + public void testMethod() throws Exception { + server.enqueue(new MockResponse().setBody("get")); + server.enqueue(new MockResponse().setBody("post")); + server.enqueue(new MockResponse().setBody("put")); + server.enqueue(new MockResponse().setBody("delete")); + String url = server.url("/v1/chat/").toString(); + HttpKit.get(url); + assertEquals(server.takeRequest().getMethod(), "GET"); + HttpKit.post(url, Collections.emptyMap()); + assertEquals(server.takeRequest().getMethod(), "POST"); + HttpKit.executeAndParse(HttpRequest.custom().url(url).put(Collections.emptyMap()).build()); + assertEquals(server.takeRequest().getMethod(), "PUT"); + HttpKit.executeAndParse(HttpRequest.custom().url(url).delete().build()); + assertEquals(server.takeRequest().getMethod(), "DELETE"); + } + + @Test + public void testHeader() throws Exception { + server.enqueue(new MockResponse().setBody("hello, world!")); + HttpUrl baseUrl = server.url("/v1/chat/"); + + HashMap headers = new HashMap(1); + headers.put("Authorization", "abc"); + String s = HttpKit.executeAndParse(HttpRequest.custom().url(baseUrl.toString()).post(Collections.emptyMap()).headers(headers).build()); + assertEquals("hello, world!", s); + // 测试请求头 + RecordedRequest request = server.takeRequest(); + assertEquals(request.getHeader("Authorization"), "abc"); + assertEquals("POST /v1/chat/ HTTP/1.1", request.getRequestLine()); + } + + @Test + public void testParams() throws Exception { + server.enqueue(new MockResponse().setBody("hello, world!")); + HttpUrl baseUrl = server.url("/v1/chat/"); + + HashMap params = new HashMap(1); + params.put("key", "value"); + String s = HttpKit.executeAndParse(HttpRequest.custom().url(baseUrl.toString()).post(params).build()); + assertEquals("hello, world!", s); + // 测试参数 + RecordedRequest request = server.takeRequest(); + assertEquals("key=value", request.getBody().readUtf8()); + assertEquals("POST /v1/chat/ HTTP/1.1", request.getRequestLine()); + } +} \ No newline at end of file