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

@ -11,10 +11,12 @@
* 【system 】 OshiUtil增加getNetworkIFs方法
* 【core 】 CollUtil增加unionDistinct、unionAll方法pr#122@Gitee
* 【core 】 增加IoUtil.readObj重载通过ValidateObjectInputStream由用户自定义安全检查。
* 【http 】 改造HttpRequest中文件上传部分增加MultipartBody类
### Bug修复
* 【core 】 修复IoUtil.readObj中反序列化安全检查导致的一些问题去掉安全检查。
* 【http 】 修复SimpleServer文件访问404问题issue#I1GZI3@Gitee
* 【core 】 修复BeanCopier中循环引用逻辑问题issue#I1H2VN@Gitee
-------------------------------------------------------------------------------------------------------------

View File

@ -267,7 +267,7 @@ public class BeanCopier<T> implements Copier<T>, Serializable {
if (null == value && copyOptions.ignoreNullValue) {
continue;// 当允许跳过空时跳过
}
if (bean.equals(value)) {
if (bean == value) {
continue;// 值不能为bean本身防止循环引用
}

View File

@ -65,7 +65,9 @@ public class BeanConverter<T> extends AbstractConverter<T> {
@Override
protected T convertInternal(Object value) {
if(value instanceof Map || value instanceof ValueProvider || BeanUtil.isBean(value.getClass())) {
if(value instanceof Map ||
value instanceof ValueProvider ||
BeanUtil.isBean(value.getClass())) {
if(value instanceof Map && this.beanClass.isInterface()) {
// 将Map动态代理为Bean
return MapProxy.create((Map<?, ?>)value).toProxyBean(this.beanClass);

View File

@ -1001,9 +1001,9 @@ public class IoUtil {
for (Object content : contents) {
if (content != null) {
osw.write(Convert.toStr(content, StrUtil.EMPTY));
osw.flush();
}
}
osw.flush();
} catch (IOException e) {
throw new IORuntimeException(e);
} finally {

View File

@ -1,11 +1,13 @@
package cn.hutool.core.io.resource;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.nio.charset.Charset;
@ -36,6 +38,20 @@ public interface Resource {
* @return {@link InputStream}
*/
InputStream getStream();
/**
* 将资源内容写出到流不关闭输出流但是关闭资源流
* @param out 输出流
* @throws IORuntimeException IO异常
* @since 5.3.5
*/
default void writeTo(OutputStream out) throws IORuntimeException{
try (InputStream in = getStream()) {
IoUtil.copy(in, out);
} catch (IOException e) {
throw new IORuntimeException(e);
}
}
/**
* 获得Reader

View File

@ -397,18 +397,8 @@ public class ObjectUtil {
if (false == (obj instanceof Serializable)) {
return null;
}
FastByteArrayOutputStream byteOut = new FastByteArrayOutputStream();
ObjectOutputStream oos = null;
try {
oos = new ObjectOutputStream(byteOut);
oos.writeObject(obj);
oos.flush();
} catch (Exception e) {
throw new UtilException(e);
} finally {
IoUtil.close(oos);
}
final FastByteArrayOutputStream byteOut = new FastByteArrayOutputStream();
IoUtil.writeObjects(byteOut, false, (Serializable) obj);
return byteOut.toByteArray();
}
@ -416,20 +406,16 @@ public class ObjectUtil {
* 反序列化<br>
* 对象必须实现Serializable接口
*
* <p>
* 注意 此方法不会检查反序列化安全可能存在反序列化漏洞风险
* </p>
*
* @param <T> 对象类型
* @param bytes 反序列化的字节码
* @return 反序列化后的对象
*/
@SuppressWarnings("unchecked")
public static <T> T deserialize(byte[] bytes) {
ObjectInputStream ois;
try {
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ois = new ObjectInputStream(bais);
return (T) ois.readObject();
} catch (Exception e) {
throw new UtilException(e);
}
return IoUtil.readObj(new ByteArrayInputStream(bytes));
}
/**

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;

View File

@ -27,11 +27,13 @@ public class SimpleServerTest {
// 文件上传测试
// http://localhost:8888/formTest?a=1&a=2&b=3
.addAction("/file", (request, response) -> {
final UploadFile file = request.getMultipart().getFile("file");
final UploadFile[] files = request.getMultipart().getFiles("file");
// 传入目录默认读取HTTP头中的文件名然后创建文件
file.write("d:/test/");
Console.log("Write file to: d:/test/");
response.write(request.getParams().toString(), ContentType.TEXT_PLAIN.toString());
for (UploadFile file : files) {
file.write("d:/test/");
Console.log("Write file: d:/test/" + file.getFileName());
}
response.write(request.getMultipart().getParamMap().toString(), ContentType.TEXT_PLAIN.toString());
}
)
.start();

View File

@ -1,15 +1,15 @@
package cn.hutool.http.test;
import java.io.File;
import java.util.HashMap;
import org.junit.Ignore;
import org.junit.Test;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import org.junit.Ignore;
import org.junit.Test;
import java.io.File;
import java.util.HashMap;
/**
* 文件上传单元测试
@ -24,16 +24,16 @@ public class UploadTest {
@Test
@Ignore
public void uploadFilesTest() {
File file = FileUtil.file("e:\\face.jpg");
File file2 = FileUtil.file("e:\\face2.jpg");
File file = FileUtil.file("d:\\图片1.JPG");
File file2 = FileUtil.file("d:\\图片3.png");
// 方法一自定义构建表单
HttpRequest request = HttpRequest//
.post("http://localhost:8090/file/upload")//
.post("http://localhost:8888/file")//
.form("file", file2, file)//
.form("fileType", "图片");
HttpResponse response = request.execute();
System.out.println(response.body());
Console.log(response.body());
}
@Test

View File

@ -0,0 +1,40 @@
package cn.hutool.json;
import lombok.Data;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
/**
* 测试同一对象作为对象的字段是否会有null的问题
* 此问题原来出在BeanCopier中判断循环引用使用了equals并不严谨
* 修复后使用==判断循环引用
*/
public class IssueI1H2VN {
@Test
public void toBeanTest() {
String jsonStr = "{'conditionsVo':[{'column':'StockNo','value':'abc','type':'='},{'column':'CheckIncoming','value':'1','type':'='}]," +
"'queryVo':{'conditionsVo':[{'column':'StockNo','value':'abc','type':'='},{'column':'CheckIncoming','value':'1','type':'='}],'queryVo':null}}";
QueryVo vo = JSONUtil.toBean(jsonStr, QueryVo.class);
Assert.assertEquals(2, vo.getConditionsVo().size());
final QueryVo subVo = vo.getQueryVo();
Assert.assertNotNull(subVo);
Assert.assertEquals(2, subVo.getConditionsVo().size());
Assert.assertNull(subVo.getQueryVo());
}
@Data
public static class ConditionVo {
private String column;
private String value;
private String type;
}
@Data
public static class QueryVo {
private List<ConditionVo> conditionsVo;
private QueryVo queryVo;
}
}