错误示例
@Data
public class FileAndForm {
private List<SaveBatchDto> list;
}
复制代码
客户端Client
@PostMapping(value = "/client/saveBatch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
R<List<SaveBatchDto>> saveBatchData(@RequestPart("file") MultipartFile[] fileList, @RequestPart("list") FileAndForm form);
复制代码
错误原因
检查服务端
RequestPartServletServerHttpRequest 获取请求部分失败
进入this.multipartRequest.getMultipartHeaders(requestPartName) 方法
StandardMultipartHttpServletRequest 标准多部分 Http Servlet 请求
进入HttpServletRequestImpl 中的getPart方法
可以看到我们没有接收到请求的FileAndForm 实体类
这就是报错原因,这样,我们就可以定位到这是客户端的问题,而不是服务端的问题
检查客户端
这个肯定是编码时出了问题,
SpringFormEncoder 的encode方法
进入FormEncoder 的encode方法
进入MultipartFormContentProcessor的process方法,过程中需要使用findApplicableWriter判断类型,获取对应的writer
总共有那些writer呢?如下2图
其中defaultPerocessor如下图
在feign.codec.Encoder中有
可以看到这个defaultPerocessor只会对最外层的参数进行处理,至于处理逻辑如图所示.
SingleParameterWriter | ManyParametersWriter |
---|---|
可以看出,这个writer可以写入字符串 | 可以看出,这个可以接收数组,但数组里的内容必须是SingleParameterWriter可以接收的内容 |
PojoWriter
这个一看就知道可以识别我们的FileAndForm实体类啊,于是就执行了它的writer
看这个write方法,可以发现它使用了_toMap_(object)方法,其方法如下
可以看到,它把实体类按字段进行了分割,如果有_FormProperty_注解,就使用其定义的值,如果没有,就用字段名
在PojoWriter内的wirte这里也需要执行findApplicableWriter方法,于是又回到了上面的步骤
pojowriter是不行的,这个isUserPojo有一个重大问题,就是它识别不了arraylist
可以发现没有writer适用于ArrayList
显而易见了,feign在进行 多部分表单内容处理 编码的时候,如果要用列表,数组等,都需要将内容转为String等,不能使用实体类的传参方式
结论
所以,我们的FileAndForm实体类内的列表根本没有被编码进去,服务端肯定也接受不了
解决办法
原
@Data
public class FileAndForm {
private List<SaveDocBatchDto> list;
}
复制代码
@PostMapping(value = "/client/saveBatch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
R<List<SaveBatchDto>> saveBatchData(@RequestPart("file") MultipartFile[] fileList, @RequestPart("list") FileAndForm form);
复制代码
通过上面的错误分析,我们可以看出,对于上述客户端代码,有很多办法
法1:转为string
很多情况下我们会把Client的服务端直接继承客户端的Client接口,这时使用这个很方便
把复杂的表单直接转为json字符串,到了服务端手动解析
@PostMapping(value = "/client/saveBatch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
R<List<SaveBatchDto>> saveBatchData(@RequestPart("file") MultipartFile[] fileList, @RequestPart("list") String formStr);
复制代码
法2:使用FormProperty
定义FileAndForm实体类,使用FormProperty注解
@Data
public class FileAndForm {
@FormProperty("list")
private String strInfo;
@FormProperty("file")
@JsonIgnore
@JSONField(serialize = false, deserialize = false)
private List<MultipartFile> file;
}
复制代码
在客户端
@PostMapping(value = "/client/saveBatch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
R<List<SaveBatchDto>> saveBatchData(FileAndForm fileAndForm);
复制代码
服务端
@PostMapping(value = "/client/saveBatch", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public R<List<SaveBatchDto>> saveBatchData(@RequestPart("file") MultipartFile[] fileList, @RequestPart("list") String saveBatchDtoListJsonStr) {}
复制代码
其他方法(失败)
上面的错误是没有解析的Writer导致的,那么可不可以写一个呢?我们可以看看
protected void write (Output output, String key, Object value) throws EncodeException {
val string = new StringBuilder()
.append("Content-Disposition: form-data; name=\"").append(key).append('"').append(CRLF)
.append("Content-Type: text/plain; charset=").append(output.getCharset().name()).append(CRLF)
.append(CRLF)
.append(value.toString())
.toString();
output.write(string);
}
复制代码
Content-Disposition: form-data; name=" 这一段是要干什么呢?
百度了一下,参考以下文章:
zhuanlan.zhihu.com/p/122912935
blog.csdn.net/wyn126/arti…
Content-Type: multipart/form-data; boundary=AaB03x
--AaB03x
Content-Disposition: form-data; name="submit-name"
Larry
--AaB03x
Content-Disposition: form-data; name="files"
Content-Type: multipart/mixed; boundary=BbC04y
--BbC04y
Content-Disposition: file; filename="file1.txt"
Content-Type: text/plain
... contents of file1.txt ...
--BbC04y
Content-Disposition: file; filename="file2.gif"
Content-Type: image/gif
Content-Transfer-Encoding: binary
...contents of file2.gif...
--BbC04y--
--AaB03x--
复制代码
package org.springblade.ws.config;
import com.alibaba.fastjson.JSONObject;
import feign.codec.EncodeException;
import feign.form.multipart.AbstractWriter;
import feign.form.multipart.Output;
import lombok.val;
import org.springframework.web.multipart.MultipartFile;
import static feign.form.ContentProcessor.CRLF;
public class MyFormWriter extends AbstractWriter {
/**
* 如果是MultipartFile或 MultipartFile[]不接收
* 其他任何类都被转为json
* 可以自由设置判断,如果判断少,就用不了其他writer
* 不良反应:不再像其他Writer一样可以接收其他类型的文件,只能接收
*/
@Override
public boolean isApplicable(Object value) {
if (value instanceof MultipartFile
|| value instanceof MultipartFile[]
// || value instanceof byte[]
// || value instanceof FormData
// || value instanceof File
// || value instanceof File[]
// || value instanceof Number
|| value instanceof CharSequence
// || value instanceof Boolean
) {
return false;
}
return true;
}
@Override
public void write (Output output, String boundary, String key, Object value) throws EncodeException {
String json = JSONObject.toJSONString(value);
val string = new StringBuilder()
// .append("Content-Disposition: form-data; name=\"").append(key).append('"').append(CRLF)
.append("Content-Type: application/json; charset=").append(output.getCharset().name()).append(CRLF)
.append(CRLF)
.append(json)
.toString();
output.write(string);
}
}
复制代码
package org.springblade.ws.config;
import feign.codec.Encoder;
import feign.form.MultipartFormContentProcessor;
import feign.form.spring.SpringFormEncoder;
import lombok.val;
import static feign.form.ContentType.MULTIPART;
public class FileAndFormEncoder extends SpringFormEncoder {
public FileAndFormEncoder() {
this(new Encoder.Default());
}
public FileAndFormEncoder(Encoder delegate) {
super(delegate);
val processor = (MultipartFormContentProcessor) getContentProcessor(MULTIPART);
processor.addFirstWriter(new MyFormWriter());
}
}
复制代码
package org.springblade.ws.config;
import org.springframework.context.annotation.Bean;
/**
* 单独配置文件表单对应Client
*/
public class FileAndFormClientConfiguration {
@Bean
public FileAndFormEncoder fileAndFormEncoder(){
return new FileAndFormEncoder();
}
}
复制代码
写完后会出现如下异常
java.io.IOException: UT000036: Connection terminated parsing multipart data
复制代码
这是为什么呢?期待大佬们回答