spring openfeign同时上传文件和表单与没解决的疑问

错误示例

@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);
复制代码

服务端与客户端写法一致
这样就会报错
image.png

错误原因

检查服务端

RequestPartServletServerHttpRequest 获取请求部分失败
image.png
进入this.multipartRequest.getMultipartHeaders(requestPartName) 方法
StandardMultipartHttpServletRequest 标准多部分 Http Servlet 请求
image.png
进入HttpServletRequestImpl 中的getPart方法
image.png
可以看到我们没有接收到请求的FileAndForm 实体类
image.png
这就是报错原因,这样,我们就可以定位到这是客户端的问题,而不是服务端的问题

检查客户端

这个肯定是编码时出了问题,
SpringFormEncoder 的encode方法
image.png
进入FormEncoder 的encode方法
进入MultipartFormContentProcessor的process方法,过程中需要使用findApplicableWriter判断类型,获取对应的writer
image.png
image.png
总共有那些writer呢?如下2图
image.pngimage.png
其中defaultPerocessor如下图
image.png
在feign.codec.Encoder中有
image.png
可以看到这个defaultPerocessor只会对最外层的参数进行处理,至于处理逻辑如图所示.

SingleParameterWriter ManyParametersWriter
image.png image.png
可以看出,这个writer可以写入字符串 可以看出,这个可以接收数组,但数组里的内容必须是SingleParameterWriter可以接收的内容

PojoWriter
image.png
这个一看就知道可以识别我们的FileAndForm实体类啊,于是就执行了它的writer
image.png
看这个write方法,可以发现它使用了_toMap_(object)方法,其方法如下
image.png
可以看到,它把实体类按字段进行了分割,如果有_FormProperty_注解,就使用其定义的值,如果没有,就用字段名

在PojoWriter内的wirte这里也需要执行findApplicableWriter方法,于是又回到了上面的步骤
pojowriter是不行的,这个isUserPojo有一个重大问题,就是它识别不了arraylist
image.png
可以发现没有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导致的,那么可不可以写一个呢?我们可以看看
image.png

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
复制代码

这是为什么呢?期待大佬们回答

猜你喜欢

转载自juejin.im/post/7108554977198473246