Looly 5 年 前
コミット
d0fe78ae66

+ 2 - 0
CHANGELOG.md

@@ -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)
 
 -------------------------------------------------------------------------------------------------------------
 

+ 1 - 1
hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java

@@ -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本身,防止循环引用
 			}
 

+ 3 - 1
hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java

@@ -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);

+ 1 - 1
hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java

@@ -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 {

+ 16 - 0
hutool-core/src/main/java/cn/hutool/core/io/resource/Resource.java

@@ -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

+ 7 - 21
hutool-core/src/main/java/cn/hutool/core/util/ObjectUtil.java

@@ -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));
 	}
 
 	/**

+ 4 - 0
hutool-http/src/main/java/cn/hutool/http/ContentType.java

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

+ 69 - 117
hutool-http/src/main/java/cn/hutool/http/HttpRequest.java

@@ -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,108 +1141,67 @@ 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 输出流
+	 * 设置表单类型为Multipart(文件上传)
 	 */
-	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);
-		}
+	private void setMultipart() {
+		this.httpConnection.header(Header.CONTENT_TYPE, MultipartBody.getContentType(), true);
 	}
 
 	/**
-	 * 发送文件对象表单
+	 * 是否忽略读取响应body部分<br>
+	 * HEAD、CONNECT、OPTIONS、TRACE方法将不读取响应体
 	 *
-	 * @param out 输出流
+	 * @return 是否需要忽略响应body部分
+	 * @since 3.1.2
 	 */
-	private void writeFileForm(OutputStream out) {
-		for (Entry<String, Resource> entry : this.fileForm.entrySet()) {
-			appendPart(entry.getKey(), entry.getValue(), out);
-		}
+	private boolean isIgnoreResponseBody() {
+		return Method.HEAD == this.method //
+				|| Method.CONNECT == this.method //
+				|| Method.OPTIONS == this.method //
+				|| Method.TRACE == this.method;
 	}
 
 	/**
-	 * 添加Multipart表单的数据项
+	 * 判断是否为multipart/form-data表单,条件如下:
 	 *
-	 * @param formFieldName 表单名
-	 * @param resource      资源,可以是文件等
-	 * @param out           Http流
-	 * @since 4.1.0
+	 * <pre>
+	 *     1. 存在资源对象(fileForm非空)
+	 *     2. 用户自定义头为multipart/form-data开头
+	 * </pre>
+	 * @return 是否为multipart/form-data表单
+	 * @since 5.3.5
 	 */
-	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);
+	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 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);
-	}
-
-	/**
-	 * 是否忽略读取响应body部分<br>
-	 * HEAD、CONNECT、OPTIONS、TRACE方法将不读取响应体
-	 *
-	 * @return 是否需要忽略响应body部分
-	 * @since 3.1.2
+	 * @param name 表单属性名
+	 * @param value 属性值
+	 * @return this
 	 */
-	private boolean isIgnoreResponseBody() {
-		return Method.HEAD == this.method //
-				|| Method.CONNECT == this.method //
-				|| Method.OPTIONS == this.method //
-				|| Method.TRACE == this.method;
+	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
 

+ 154 - 0
hutool-http/src/main/java/cn/hutool/http/body/MultipartBody.java

@@ -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);
+	}
+}

+ 16 - 0
hutool-http/src/main/java/cn/hutool/http/body/RequestBody.java

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

+ 7 - 0
hutool-http/src/main/java/cn/hutool/http/body/package-info.java

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

+ 6 - 4
hutool-http/src/test/java/cn/hutool/http/server/SimpleServerTest.java

@@ -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();

+ 10 - 10
hutool-http/src/test/java/cn/hutool/http/test/UploadTest.java

@@ -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

+ 40 - 0
hutool-json/src/test/java/cn/hutool/json/IssueI1H2VN.java

@@ -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;
+	}
+}