Files
hutool/hutool-http/src/main/java/cn/hutool/http/HttpUtil.java
2020-05-29 14:26:53 +08:00

775 lines
24 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package cn.hutool.http;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.StreamProgress;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.UrlQuery;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.server.SimpleServer;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
/**
* Http请求工具类
*
* @author xiaoleilu
*/
public class HttpUtil {
/**
* 正则Content-Type中的编码信息
*/
public static final Pattern CHARSET_PATTERN = Pattern.compile("charset\\s*=\\s*([a-z0-9-]*)", Pattern.CASE_INSENSITIVE);
/**
* 正则匹配meta标签的编码信息
*/
public static final Pattern META_CHARSET_PATTERN = Pattern.compile("<meta[^>]*?charset\\s*=\\s*['\"]?([a-z0-9-]*)", Pattern.CASE_INSENSITIVE);
/**
* 检测是否https
*
* @param url URL
* @return 是否https
*/
public static boolean isHttps(String url) {
return url.toLowerCase().startsWith("https");
}
/**
* 创建Http请求对象
*
* @param method 方法枚举{@link Method}
* @param url 请求的URL可以使HTTP或者HTTPS
* @return {@link HttpRequest}
* @since 3.0.9
*/
public static HttpRequest createRequest(Method method, String url) {
return new HttpRequest(url).method(method);
}
/**
* 创建Http GET请求对象
*
* @param url 请求的URL可以使HTTP或者HTTPS
* @return {@link HttpRequest}
* @since 3.2.0
*/
public static HttpRequest createGet(String url) {
return HttpRequest.get(url);
}
/**
* 创建Http POST请求对象
*
* @param url 请求的URL可以使HTTP或者HTTPS
* @return {@link HttpRequest}
* @since 3.2.0
*/
public static HttpRequest createPost(String url) {
return HttpRequest.post(url);
}
/**
* 发送get请求
*
* @param urlString 网址
* @param customCharset 自定义请求字符集,如果字符集获取不到,使用此字符集
* @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null
*/
public static String get(String urlString, Charset customCharset) {
return HttpRequest.get(urlString).charset(customCharset).execute().body();
}
/**
* 发送get请求
*
* @param urlString 网址
* @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null
*/
public static String get(String urlString) {
return get(urlString, HttpGlobalConfig.timeout);
}
/**
* 发送get请求
*
* @param urlString 网址
* @param timeout 超时时长,-1表示默认超时单位毫秒
* @return 返回内容,如果只检查状态码,正常只返回 "",不正常返回 null
* @since 3.2.0
*/
public static String get(String urlString, int timeout) {
return HttpRequest.get(urlString).timeout(timeout).execute().body();
}
/**
* 发送get请求
*
* @param urlString 网址
* @param paramMap post表单数据
* @return 返回数据
*/
public static String get(String urlString, Map<String, Object> paramMap) {
return HttpRequest.get(urlString).form(paramMap).execute().body();
}
/**
* 发送get请求
*
* @param urlString 网址
* @param paramMap post表单数据
* @param timeout 超时时长,-1表示默认超时单位毫秒
* @return 返回数据
* @since 3.3.0
*/
public static String get(String urlString, Map<String, Object> paramMap, int timeout) {
return HttpRequest.get(urlString).form(paramMap).timeout(timeout).execute().body();
}
/**
* 发送post请求
*
* @param urlString 网址
* @param paramMap post表单数据
* @return 返回数据
*/
public static String post(String urlString, Map<String, Object> paramMap) {
return post(urlString, paramMap, HttpGlobalConfig.timeout);
}
/**
* 发送post请求
*
* @param urlString 网址
* @param paramMap post表单数据
* @param timeout 超时时长,-1表示默认超时单位毫秒
* @return 返回数据
* @since 3.2.0
*/
public static String post(String urlString, Map<String, Object> paramMap, int timeout) {
return HttpRequest.post(urlString).form(paramMap).timeout(timeout).execute().body();
}
/**
* 发送post请求<br>
* 请求体body参数支持两种类型
*
* <pre>
* 1. 标准参数,例如 a=1&amp;b=2 这种格式
* 2. Rest模式此时body需要传入一个JSON或者XML字符串Hutool会自动绑定其对应的Content-Type
* </pre>
*
* @param urlString 网址
* @param body post表单数据
* @return 返回数据
*/
public static String post(String urlString, String body) {
return post(urlString, body, HttpGlobalConfig.timeout);
}
/**
* 发送post请求<br>
* 请求体body参数支持两种类型
*
* <pre>
* 1. 标准参数,例如 a=1&amp;b=2 这种格式
* 2. Rest模式此时body需要传入一个JSON或者XML字符串Hutool会自动绑定其对应的Content-Type
* </pre>
*
* @param urlString 网址
* @param body post表单数据
* @param timeout 超时时长,-1表示默认超时单位毫秒
* @return 返回数据
* @since 3.2.0
*/
public static String post(String urlString, String body, int timeout) {
return HttpRequest.post(urlString).timeout(timeout).body(body).execute().body();
}
// ---------------------------------------------------------------------------------------- download
/**
* 下载远程文本
*
* @param url 请求的url
* @param customCharsetName 自定义的字符集
* @return 文本
*/
public static String downloadString(String url, String customCharsetName) {
return downloadString(url, CharsetUtil.charset(customCharsetName), null);
}
/**
* 下载远程文本
*
* @param url 请求的url
* @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换
* @return 文本
*/
public static String downloadString(String url, Charset customCharset) {
return downloadString(url, customCharset, null);
}
/**
* 下载远程文本
*
* @param url 请求的url
* @param customCharset 自定义的字符集,可以使用{@link CharsetUtil#charset} 方法转换
* @param streamPress 进度条 {@link StreamProgress}
* @return 文本
*/
public static String downloadString(String url, Charset customCharset, StreamProgress streamPress) {
if (StrUtil.isBlank(url)) {
throw new NullPointerException("[url] is null!");
}
FastByteArrayOutputStream out = new FastByteArrayOutputStream();
download(url, out, true, streamPress);
return null == customCharset ? out.toString() : out.toString(customCharset);
}
/**
* 下载远程文件
*
* @param url 请求的url
* @param dest 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* @return 文件大小
*/
public static long downloadFile(String url, String dest) {
return downloadFile(url, FileUtil.file(dest));
}
/**
* 下载远程文件
*
* @param url 请求的url
* @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* @return 文件大小
*/
public static long downloadFile(String url, File destFile) {
return downloadFile(url, destFile, null);
}
/**
* 下载远程文件
*
* @param url 请求的url
* @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* @param timeout 超时,单位毫秒,-1表示默认超时
* @return 文件大小
* @since 4.0.4
*/
public static long downloadFile(String url, File destFile, int timeout) {
return downloadFile(url, destFile, timeout, null);
}
/**
* 下载远程文件
*
* @param url 请求的url
* @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* @param streamProgress 进度条
* @return 文件大小
*/
public static long downloadFile(String url, File destFile, StreamProgress streamProgress) {
return downloadFile(url, destFile, -1, streamProgress);
}
/**
* 下载远程文件
*
* @param url 请求的url
* @param destFile 目标文件或目录当为目录时取URL中的文件名取不到使用编码后的URL做为文件名
* @param timeout 超时,单位毫秒,-1表示默认超时
* @param streamProgress 进度条
* @return 文件大小
* @since 4.0.4
*/
public static long downloadFile(String url, File destFile, int timeout, StreamProgress streamProgress) {
if (StrUtil.isBlank(url)) {
throw new NullPointerException("[url] is null!");
}
if (null == destFile) {
throw new NullPointerException("[destFile] is null!");
}
final HttpResponse response = HttpRequest.get(url).timeout(timeout).executeAsync();
if (false == response.isOk()) {
throw new HttpException("Server response error with status code: [{}]", response.getStatus());
}
return response.writeBody(destFile, streamProgress);
}
/**
* 下载远程文件
*
* @param url 请求的url
* @param out 将下载内容写到输出流中 {@link OutputStream}
* @param isCloseOut 是否关闭输出流
* @return 文件大小
*/
public static long download(String url, OutputStream out, boolean isCloseOut) {
return download(url, out, isCloseOut, null);
}
/**
* 下载远程文件
*
* @param url 请求的url
* @param out 将下载内容写到输出流中 {@link OutputStream}
* @param isCloseOut 是否关闭输出流
* @param streamProgress 进度条
* @return 文件大小
*/
public static long download(String url, OutputStream out, boolean isCloseOut, StreamProgress streamProgress) {
if (StrUtil.isBlank(url)) {
throw new NullPointerException("[url] is null!");
}
if (null == out) {
throw new NullPointerException("[out] is null!");
}
final HttpResponse response = HttpRequest.get(url).executeAsync();
if (false == response.isOk()) {
throw new HttpException("Server response error with status code: [{}]", response.getStatus());
}
return response.writeBody(out, isCloseOut, streamProgress);
}
/**
* 下载远程文件数据
*
* @param url 请求的url
* @return 文件数据
*/
public static byte[] downloadBytes(String url) {
if (StrUtil.isBlank(url)) {
throw new NullPointerException("[url] is null!");
}
HttpRequest request = new HttpRequest(url);
request.setFollowRedirects(true);
final HttpResponse response = request.executeAsync();
if (false == response.isOk()) {
throw new HttpException("Server response error with status code: [{}]", response.getStatus());
}
return response.bodyBytes();
}
/**
* 将Map形式的Form表单数据转换为Url参数形式会自动url编码键和值
*
* @param paramMap 表单数据
* @return url参数
*/
public static String toParams(Map<String, ?> paramMap) {
return toParams(paramMap, CharsetUtil.CHARSET_UTF_8);
}
/**
* 将Map形式的Form表单数据转换为Url参数形式<br>
* 编码键和值对
*
* @param paramMap 表单数据
* @param charsetName 编码
* @return url参数
*/
public static String toParams(Map<String, Object> paramMap, String charsetName) {
return toParams(paramMap, CharsetUtil.charset(charsetName));
}
/**
* 将Map形式的Form表单数据转换为Url参数形式<br>
* paramMap中如果key为空null和""会被忽略如果value为null会被做为空白符""<br>
* 会自动url编码键和值
*
* <pre>
* key1=v1&amp;key2=&amp;key3=v3
* </pre>
*
* @param paramMap 表单数据
* @param charset 编码null表示不encode键值对
* @return url参数
*/
public static String toParams(Map<String, ?> paramMap, Charset charset) {
return URLUtil.buildQuery(paramMap, charset);
}
/**
* 对URL参数做编码只编码键和值<br>
* 提供的值可以是url附带参数但是不能只是url
*
* <p>注意此方法只能标准化整个URL并不适合于单独编码参数值</p>
*
* @param urlWithParams url和参数可以包含url本身也可以单独参数
* @param charset 编码
* @return 编码后的url和参数
* @since 4.0.1
*/
public static String encodeParams(String urlWithParams, Charset charset) {
if (StrUtil.isBlank(urlWithParams)) {
return StrUtil.EMPTY;
}
String urlPart = null; // url部分不包括问号
String paramPart; // 参数部分
final int pathEndPos = urlWithParams.indexOf('?');
if (pathEndPos > -1) {
// url + 参数
urlPart = StrUtil.subPre(urlWithParams, pathEndPos);
paramPart = StrUtil.subSuf(urlWithParams, pathEndPos + 1);
if (StrUtil.isBlank(paramPart)) {
// 无参数返回url
return urlPart;
}
} else if (false == StrUtil.contains(urlWithParams, '=')) {
// 无参数的URL
return urlWithParams;
} else {
// 无URL的参数
paramPart = urlWithParams;
}
paramPart = normalizeParams(paramPart, charset);
return StrUtil.isBlank(urlPart) ? paramPart : urlPart + "?" + paramPart;
}
/**
* 标准化参数字符串即URL中后的部分
*
* <p>注意此方法只能标准化整个URL并不适合于单独编码参数值</p>
*
* @param paramPart 参数字符串
* @param charset 编码
* @return 标准化的参数字符串
* @since 4.5.2
*/
public static String normalizeParams(String paramPart, Charset charset) {
final StrBuilder builder = StrBuilder.create(paramPart.length() + 16);
final int len = paramPart.length();
String name = null;
int pos = 0; // 未处理字符开始位置
char c; // 当前字符
int i; // 当前字符位置
for (i = 0; i < len; i++) {
c = paramPart.charAt(i);
if (c == '=') { // 键值对的分界点
if (null == name) {
// 只有=前未定义name时被当作键值分界符否则做为普通字符
name = (pos == i) ? StrUtil.EMPTY : paramPart.substring(pos, i);
pos = i + 1;
}
} else if (c == '&') { // 参数对的分界点
if (pos != i) {
if (null == name) {
// 对于像&a&这类无参数值的字符串我们将name为a的值设为""
name = paramPart.substring(pos, i);
builder.append(URLUtil.encodeQuery(name, charset)).append('=');
} else {
builder.append(URLUtil.encodeQuery(name, charset)).append('=').append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset)).append('&');
}
name = null;
}
pos = i + 1;
}
}
// 结尾处理
if (null != name) {
builder.append(URLUtil.encodeQuery(name, charset)).append('=');
}
if (pos != i) {
if (null == name && pos > 0) {
builder.append('=');
}
builder.append(URLUtil.encodeQuery(paramPart.substring(pos, i), charset));
}
// 以&结尾则去除之
int lastIndex = builder.length() - 1;
if ('&' == builder.charAt(lastIndex)) {
builder.delTo(lastIndex);
}
return builder.toString();
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
* @since 4.0.2
* @deprecated 请使用 {@link #decodeParamMap(String, Charset)}
*/
@Deprecated
public static Map<String, String> decodeParamMap(String paramsStr, String charset) {
return decodeParamMap(paramsStr, CharsetUtil.charset(charset));
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
* @since 5.2.6
*/
public static Map<String, String> decodeParamMap(String paramsStr, Charset charset) {
final Map<CharSequence, CharSequence> queryMap = UrlQuery.of(paramsStr, charset).getQueryMap();
if (MapUtil.isEmpty(queryMap)) {
return MapUtil.empty();
}
return Convert.toMap(String.class, String.class, queryMap);
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
*/
public static Map<String, List<String>> decodeParams(String paramsStr, String charset) {
return decodeParams(paramsStr, CharsetUtil.charset(charset));
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
* @since 5.2.6
*/
public static Map<String, List<String>> decodeParams(String paramsStr, Charset charset) {
final Map<CharSequence, CharSequence> queryMap = UrlQuery.of(paramsStr, charset).getQueryMap();
if (MapUtil.isEmpty(queryMap)) {
return MapUtil.empty();
}
final Map<String, List<String>> params = new LinkedHashMap<>();
queryMap.forEach((key, value) -> {
final List<String> values = params.computeIfAbsent(StrUtil.str(key), k -> new ArrayList<>(1));
// 一般是一个参数
values.add(StrUtil.str(value));
});
return params;
}
/**
* 将表单数据加到URL中用于GET表单提交<br>
* 表单的键值对会被url编码但是url中原参数不会被编码
*
* @param url URL
* @param form 表单数据
* @param charset 编码
* @param isEncodeParams 是否对键和值做转义处理
* @return 合成后的URL
*/
public static String urlWithForm(String url, Map<String, Object> form, Charset charset, boolean isEncodeParams) {
if (isEncodeParams && StrUtil.contains(url, '?')) {
// 在需要编码的情况下如果url中已经有部分参数则编码之
url = encodeParams(url, charset);
}
// url和参数是分别编码的
return urlWithForm(url, toParams(form, charset), charset, false);
}
/**
* 将表单数据字符串加到URL中用于GET表单提交
*
* @param url URL
* @param queryString 表单数据字符串
* @param charset 编码
* @param isEncode 是否对键和值做转义处理
* @return 拼接后的字符串
*/
public static String urlWithForm(String url, String queryString, Charset charset, boolean isEncode) {
if (StrUtil.isBlank(queryString)) {
// 无额外参数
if (StrUtil.contains(url, '?')) {
// url中包含参数
return isEncode ? encodeParams(url, charset) : url;
}
return url;
}
// 始终有参数
final StrBuilder urlBuilder = StrBuilder.create(url.length() + queryString.length() + 16);
int qmIndex = url.indexOf('?');
if (qmIndex > 0) {
// 原URL带参数则对这部分参数单独编码如果选项为进行编码
urlBuilder.append(isEncode ? encodeParams(url, charset) : url);
if (false == StrUtil.endWith(url, '&')) {
// 已经带参数的情况下追加参数
urlBuilder.append('&');
}
} else {
// 原url无参数则不做编码
urlBuilder.append(url);
if (qmIndex < 0) {
// 无 '?' 追加之
urlBuilder.append('?');
}
}
urlBuilder.append(isEncode ? encodeParams(queryString, charset) : queryString);
return urlBuilder.toString();
}
/**
* 从Http连接的头信息中获得字符集<br>
* 从ContentType中获取
*
* @param conn HTTP连接对象
* @return 字符集
*/
public static String getCharset(HttpURLConnection conn) {
if (conn == null) {
return null;
}
return getCharset(conn.getContentType());
}
/**
* 从Http连接的头信息中获得字符集<br>
* 从ContentType中获取
*
* @param contentType Content-Type
* @return 字符集
* @since 5.2.6
*/
public static String getCharset(String contentType) {
if (StrUtil.isBlank(contentType)) {
return null;
}
return ReUtil.get(CHARSET_PATTERN, contentType, 1);
}
/**
* 从流中读取内容<br>
* 首先尝试使用charset编码读取内容如果为空默认UTF-8如果isGetCharsetFromContent为true则通过正则在正文中获取编码信息转换为指定编码
*
* @param in 输入流
* @param charset 字符集
* @param isGetCharsetFromContent 是否从返回内容中获得编码信息
* @return 内容
*/
public static String getString(InputStream in, Charset charset, boolean isGetCharsetFromContent) {
final byte[] contentBytes = IoUtil.readBytes(in);
return getString(contentBytes, charset, isGetCharsetFromContent);
}
/**
* 从流中读取内容<br>
* 首先尝试使用charset编码读取内容如果为空默认UTF-8如果isGetCharsetFromContent为true则通过正则在正文中获取编码信息转换为指定编码
*
* @param contentBytes 内容byte数组
* @param charset 字符集
* @param isGetCharsetFromContent 是否从返回内容中获得编码信息
* @return 内容
*/
public static String getString(byte[] contentBytes, Charset charset, boolean isGetCharsetFromContent) {
if (null == contentBytes) {
return null;
}
if (null == charset) {
charset = CharsetUtil.CHARSET_UTF_8;
}
String content = new String(contentBytes, charset);
if (isGetCharsetFromContent) {
final String charsetInContentStr = ReUtil.get(META_CHARSET_PATTERN, content, 1);
if (StrUtil.isNotBlank(charsetInContentStr)) {
Charset charsetInContent = null;
try {
charsetInContent = Charset.forName(charsetInContentStr);
} catch (Exception e) {
if (StrUtil.containsIgnoreCase(charsetInContentStr, "utf-8") || StrUtil.containsIgnoreCase(charsetInContentStr, "utf8")) {
charsetInContent = CharsetUtil.CHARSET_UTF_8;
} else if (StrUtil.containsIgnoreCase(charsetInContentStr, "gbk")) {
charsetInContent = CharsetUtil.CHARSET_GBK;
}
// ignore
}
if (null != charsetInContent && false == charset.equals(charsetInContent)) {
content = new String(contentBytes, charsetInContent);
}
}
}
return content;
}
/**
* 根据文件扩展名获得MimeType
*
* @param filePath 文件路径或文件名
* @param defaultValue 当获取MimeType为null时的默认值
* @return MimeType
* @see FileUtil#getMimeType(String)
* @since 4.6.5
*/
public static String getMimeType(String filePath, String defaultValue) {
return ObjectUtil.defaultIfNull(getMimeType(filePath), defaultValue);
}
/**
* 根据文件扩展名获得MimeType
*
* @param filePath 文件路径或文件名
* @return MimeType
* @see FileUtil#getMimeType(String)
*/
public static String getMimeType(String filePath) {
return FileUtil.getMimeType(filePath);
}
/**
* 从请求参数的body中判断请求的Content-Type类型支持的类型有
*
* <pre>
* 1. application/json
* 1. application/xml
* </pre>
*
* @param body 请求参数体
* @return Content-Type类型如果无法判断返回null
* @see ContentType#get(String)
* @since 3.2.0
*/
public static String getContentTypeByRequestBody(String body) {
final ContentType contentType = ContentType.get(body);
return (null == contentType) ? null : contentType.toString();
}
/**
* 创建简易的Http服务器
*
* @param port 端口
* @return {@link SimpleServer}
* @since 5.2.6
*/
public static SimpleServer createServer(int port) {
return new SimpleServer(port);
}
}