This commit is contained in:
Looly
2020-05-14 01:56:03 +08:00
parent 3da83e0227
commit d0fe78ae66
14 changed files with 337 additions and 156 deletions

View File

@@ -43,6 +43,10 @@ public enum ContentType {
private final String value;
/**
* 构造
* @param value ContentType值
*/
ContentType(String value) {
this.value = value;
}

View File

@@ -1,14 +1,13 @@
package cn.hutool.http;
import cn.hutool.core.codec.Base64;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.BytesResource;
import cn.hutool.core.io.resource.FileResource;
import cn.hutool.core.io.resource.MultiFileResource;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
@@ -16,8 +15,8 @@ import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.body.MultipartBody;
import cn.hutool.http.cookie.GlobalCookieManager;
import cn.hutool.http.ssl.SSLSocketFactoryBuilder;
@@ -25,18 +24,15 @@ import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLSocketFactory;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.CookieManager;
import java.net.HttpCookie;
import java.net.HttpURLConnection;
import java.net.Proxy;
import java.net.URLStreamHandler;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
/**
* http请求类<br>
@@ -46,12 +42,7 @@ import java.util.Map.Entry;
*/
public class HttpRequest extends HttpBase<HttpRequest> {
private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16);
private static final byte[] BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY).getBytes();
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
private static final String CONTENT_TYPE_MULTIPART_PREFIX = "multipart/form-data; boundary=";
private static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary=";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";
/**
@@ -113,9 +104,9 @@ public class HttpRequest extends HttpBase<HttpRequest> {
*/
private Map<String, Object> form;
/**
* 文件表单对象,用于文件上传
* 是否为Multipart表单
*/
private Map<String, Resource> fileForm;
private boolean isMultiPart;
/**
* Cookie
*/
@@ -492,17 +483,17 @@ public class HttpRequest extends HttpBase<HttpRequest> {
if (value instanceof File) {
// 文件上传
return this.form(name, (File) value);
} else if (value instanceof Resource) {
// 自定义流上传
return this.form(name, (Resource) value);
} else if (this.form == null) {
this.form = new LinkedHashMap<>();
}
if(value instanceof Resource){
return form(name, (Resource)value);
}
// 普通值
String strValue;
if (value instanceof List) {
// 列表对象
strValue = CollectionUtil.join((List<?>) value, ",");
strValue = CollUtil.join((List<?>) value, ",");
} else if (ArrayUtil.isArray(value)) {
if (File.class == ArrayUtil.getComponentType(value)) {
// 多文件
@@ -515,8 +506,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
strValue = Convert.toStr(value, null);
}
form.put(name, strValue);
return this;
return putToForm(name, strValue);
}
/**
@@ -531,8 +521,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
form(name, value);
for (int i = 0; i < parameters.length; i += 2) {
name = parameters[i].toString();
form(name, parameters[i + 1]);
form(parameters[i].toString(), parameters[i + 1]);
}
return this;
}
@@ -545,9 +534,7 @@ public class HttpRequest extends HttpBase<HttpRequest> {
*/
public HttpRequest form(Map<String, Object> formMap) {
if (MapUtil.isNotEmpty(formMap)) {
for (Map.Entry<String, Object> entry : formMap.entrySet()) {
form(entry.getKey(), entry.getValue());
}
formMap.forEach(this::form);
}
return this;
}
@@ -557,10 +544,13 @@ public class HttpRequest extends HttpBase<HttpRequest> {
* 一旦有文件加入表单变为multipart/form-data
*
* @param name 名
* @param files 需要上传的文件
* @param files 需要上传的文件,为空跳过
* @return this
*/
public HttpRequest form(String name, File... files) {
if(ArrayUtil.isEmpty(files)){
return this;
}
if (1 == files.length) {
final File file = files[0];
return form(name, file, file.getName());
@@ -628,11 +618,8 @@ public class HttpRequest extends HttpBase<HttpRequest> {
keepAlive(true);
}
if (null == this.fileForm) {
fileForm = new HashMap<>();
}
// 文件对象
this.fileForm.put(name, resource);
this.isMultiPart = true;
return putToForm(name, resource);
}
return this;
}
@@ -653,7 +640,13 @@ public class HttpRequest extends HttpBase<HttpRequest> {
* @since 3.3.0
*/
public Map<String, Resource> fileForm() {
return this.fileForm;
final Map<String, Resource> result = MapUtil.newHashMap();
this.form.forEach((key, value)->{
if(value instanceof Resource){
result.put(key, (Resource)value);
}
});
return result;
}
// ---------------------------------------------------------------- Form end
@@ -1091,10 +1084,10 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|| Method.PUT.equals(this.method) //
|| Method.DELETE.equals(this.method) //
|| this.isRest) {
if (CollectionUtil.isEmpty(this.fileForm)) {
sendFormUrlEncoded();// 普通表单
} else {
if (isMultipart()) {
sendMultipart(); // 文件上传表单
} else {
sendFormUrlEncoded();// 普通表单
}
} else {
this.httpConnection.connect();
@@ -1148,94 +1141,15 @@ public class HttpRequest extends HttpBase<HttpRequest> {
setMultipart();// 设置表单类型为Multipart
try (OutputStream out = this.httpConnection.getOutputStream()) {
writeFileForm(out);
writeForm(out);
formEnd(out);
MultipartBody.create(this.form, this.charset).write(out);
}
}
// 普通字符串数据
/**
* 发送普通表单内容
*
* @param out 输出流
*/
private void writeForm(OutputStream out) {
if (CollectionUtil.isNotEmpty(this.form)) {
StringBuilder builder = StrUtil.builder();
for (Entry<String, Object> entry : this.form.entrySet()) {
builder.append("--").append(BOUNDARY).append(StrUtil.CRLF);
builder.append(StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, entry.getKey()));
builder.append(entry.getValue()).append(StrUtil.CRLF);
}
IoUtil.write(out, this.charset, false, builder);
}
}
/**
* 发送文件对象表单
*
* @param out 输出流
*/
private void writeFileForm(OutputStream out) {
for (Entry<String, Resource> entry : this.fileForm.entrySet()) {
appendPart(entry.getKey(), entry.getValue(), out);
}
}
/**
* 添加Multipart表单的数据项
*
* @param formFieldName 表单名
* @param resource 资源,可以是文件等
* @param out Http流
* @since 4.1.0
*/
private void appendPart(String formFieldName, Resource resource, OutputStream out) {
if (resource instanceof MultiResource) {
// 多资源
for (Resource subResource : (MultiResource) resource) {
appendPart(formFieldName, subResource, out);
}
} else {
// 普通资源
final StringBuilder builder = StrUtil.builder().append("--").append(BOUNDARY).append(StrUtil.CRLF);
final String fileName = resource.getName();
builder.append(StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName)));
// 根据name的扩展名指定互联网媒体类型默认二进制流数据
builder.append(StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream")));
IoUtil.write(out, this.charset, false, builder);
InputStream in = null;
try {
in = resource.getStream();
IoUtil.copy(in, out);
} finally {
IoUtil.close(in);
}
IoUtil.write(out, this.charset, false, StrUtil.CRLF);
}
}
// 添加结尾数据
/**
* 上传表单结束
*
* @param out 输出流
* @throws IOException IO异常
*/
private void formEnd(OutputStream out) throws IOException {
out.write(BOUNDARY_END);
out.flush();
}
/**
* 设置表单类型为Multipart文件上传
*/
private void setMultipart() {
this.httpConnection.header(Header.CONTENT_TYPE, CONTENT_TYPE_MULTIPART_PREFIX + BOUNDARY, true);
this.httpConnection.header(Header.CONTENT_TYPE, MultipartBody.getContentType(), true);
}
/**
@@ -1251,6 +1165,44 @@ public class HttpRequest extends HttpBase<HttpRequest> {
|| Method.OPTIONS == this.method //
|| Method.TRACE == this.method;
}
/**
* 判断是否为multipart/form-data表单条件如下
*
* <pre>
* 1. 存在资源对象fileForm非空
* 2. 用户自定义头为multipart/form-data开头
* </pre>
* @return 是否为multipart/form-data表单
* @since 5.3.5
*/
private boolean isMultipart(){
if(this.isMultiPart){
return true;
}
final String contentType = header(Header.CONTENT_TYPE);
return StrUtil.isNotEmpty(contentType) &&
contentType.startsWith(ContentType.MULTIPART.getValue());
}
/**
* 将参数加入到form中如果form为空新建之。
*
* @param name 表单属性名
* @param value 属性值
* @return this
*/
private HttpRequest putToForm(String name, Object value){
if(null == name || null == value){
return this;
}
if(null == this.form){
this.form = new LinkedHashMap<>();
}
this.form.put(name, value);
return this;
}
// ---------------------------------------------------------------- Private method end
}

View File

@@ -0,0 +1,154 @@
package cn.hutool.http.body;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.io.resource.MultiResource;
import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.HttpUtil;
import java.io.OutputStream;
import java.nio.charset.Charset;
import java.util.Map;
/**
* Multipart/form-data数据的请求体封装
*
* @author looly
* @since 5.3.5
*/
public class MultipartBody implements RequestBody{
private static final String BOUNDARY = "--------------------Hutool_" + RandomUtil.randomString(16);
private static final String BOUNDARY_END = StrUtil.format("--{}--\r\n", BOUNDARY);
private static final String CONTENT_DISPOSITION_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"\r\n\r\n";
private static final String CONTENT_DISPOSITION_FILE_TEMPLATE = "Content-Disposition: form-data; name=\"{}\"; filename=\"{}\"\r\n";
private static final String CONTENT_TYPE_MULTIPART_PREFIX = ContentType.MULTIPART.getValue() + "; boundary=";
private static final String CONTENT_TYPE_FILE_TEMPLATE = "Content-Type: {}\r\n\r\n";
/**
* 存储表单数据
*/
private final Map<String, Object> form;
/**
* 编码
*/
private final Charset charset;
/**
* 根据已有表单内容构建MultipartBody
* @param form 表单
* @param charset 编码
* @return MultipartBody
*/
public static MultipartBody create(Map<String, Object> form, Charset charset){
return new MultipartBody(form, charset);
}
/**
* 获取Multipart的Content-Type类型
*
* @return Multipart的Content-Type类型
*/
public static String getContentType(){
return CONTENT_TYPE_MULTIPART_PREFIX + BOUNDARY;
}
/**
* 构造
*
* @param form 表单
* @param charset 编码
*/
public MultipartBody(Map<String, Object> form, Charset charset) {
this.form = form;
this.charset = charset;
}
/**
* 写出Multiparty数据不关闭流
*
* @param out out流
*/
@Override
public void write(OutputStream out) {
writeForm(out);
formEnd(out);
}
// 普通字符串数据
/**
* 发送文件对象表单
*
* @param out 输出流
*/
private void writeForm(OutputStream out) {
if (MapUtil.isNotEmpty(this.form)) {
for (Map.Entry<String, Object> entry : this.form.entrySet()) {
appendPart(entry.getKey(), entry.getValue(), out);
}
}
}
/**
* 添加Multipart表单的数据项
*
* @param formFieldName 表单名
* @param value 值,可以是普通值、资源(如文件等)
* @param out Http流
* @throws IORuntimeException IO异常
*/
private void appendPart(String formFieldName, Object value, OutputStream out) throws IORuntimeException {
// 多资源
if (value instanceof MultiResource) {
for (Resource subResource : (MultiResource) value) {
appendPart(formFieldName, subResource, out);
}
return;
}
write(out, "--", BOUNDARY, StrUtil.CRLF);
if(value instanceof Resource){
// 文件资源(二进制资源)
final Resource resource = (Resource)value;
final String fileName = resource.getName();
write(out, StrUtil.format(CONTENT_DISPOSITION_FILE_TEMPLATE, formFieldName, ObjectUtil.defaultIfNull(fileName, formFieldName)));
// 根据name的扩展名指定互联网媒体类型默认二进制流数据
write(out, StrUtil.format(CONTENT_TYPE_FILE_TEMPLATE, HttpUtil.getMimeType(fileName, "application/octet-stream")));
resource.writeTo(out);
} else{
// 普通数据
write(out, StrUtil.format(CONTENT_DISPOSITION_TEMPLATE, formFieldName));
write(out, value);
}
write(out, StrUtil.CRLF);
}
/**
* 上传表单结束
*
* @param out 输出流
* @throws IORuntimeException IO异常
*/
private void formEnd(OutputStream out) throws IORuntimeException {
write(out, BOUNDARY_END);
}
/**
* 写出对象
*
* @param out 输出流
* @param objs 写出的对象(转换为字符串)
*/
private void write(OutputStream out, Object... objs) {
IoUtil.write(out, this.charset, false, objs);
}
}

View File

@@ -0,0 +1,16 @@
package cn.hutool.http.body;
import java.io.OutputStream;
/**
* 定义请求体接口
*/
public interface RequestBody {
/**
* 写出数据,不关闭流
*
* @param out out流
*/
void write(OutputStream out);
}

View File

@@ -0,0 +1,7 @@
/**
* 请求体封装实现
*
* @author looly
*
*/
package cn.hutool.http.body;