通过RestTemplate上传文件
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
通过RestTemplate上传⽂件
1.上传⽂件File
碰到⼀个需求,在代码中通过HTTP⽅式做⼀个验证的请求,请求的参数包含了⽂件类型。
想想其实很简单,直接使⽤定义好的MultiValueMap,把⽂件参数传⼊即可。
我们知道,restTemplate 默认定义了⼏个通⽤的消息转换器,见org.springframework.web.client.RestTemplate#RestTemplate(),那么⽂件应该对应哪种资源呢?
看了上⾯这个⽅法之后,可以很快联想到是ResourceHttpMessageConverter,从类签名也可以看出来:
Implementation of {@link HttpMessageConverter} that can read/write {@link Resource Resources}
and supports byte range requests.
这个转换器主要是⽤来读写各种类型的字节请求的。
既然是Resource,那么我们来看⼀下它的实现类有哪些:
以上是AbstractResource的实现类,有各种各样的实现类,从名称上来说应该⽐较有⽤的应该是:InputStreamResource和FileSystemResource,还
有ByteArrayResource和UrlResource等。
1.1 使⽤FileSystemResource上传⽂件
这种⽅式使⽤起来⽐较简单,直接把⽂件转换成对应的形式即可。
MultiValueMap<String, Object> resultMap = new LinkedMultiValueMap<>();
Resource resource = new FileSystemResource(file);
param.put("file", resource);
⽹上使⽤RestTemplate上传⽂件⼤多数是这种⽅式,简单,⽅便,不⽤做过多的转换,直接传递参数即可。
但是为什么会写这篇博客来记录呢?因为,有⼀个不喜欢的地⽅就是,它需要传递⼀个⽂件。
⽽我得到是⽂件源是⼀个流,我需要在本地创建⼀个临时⽂件,然后把InputStream写⼊到⽂件中去。
使⽤完之后,还需要把⽂件删除。
那么既然这么⿇烦,有没有更好的⽅式呢?
1.2 使⽤InputStreamResource上传⽂件
这个类的构造函数可以直接传⼊流⽂件。
那么就直接试试吧!
MultiValueMap<String, Object> resultMap = new LinkedMultiValueMap<>();
Resource resource = new InputStreamResource(inputStream);
param.put("file", resource);
没有想到,服务端报错了,返回的是:没有传递⽂件。
这可就纳闷了,明明已经有了啊。
⽹上使⽤这种⽅式上传的⽅式不多,只找到这么⼀个⽂件,但已经够了:.
博主的疑问和我⼀样,不想去创建本地⽂件,然后就使⽤了这个流的⽅式。
但是也碰到了问题。
⽂章写得很清晰:使⽤InputStreamResource上传⽂件时,需要重写该类的两个⽅法,contentLength和getFilename。
果然按照这个⽂章的思路尝试之后,就成功了。
代码如下:
public class CommonInputStreamResource extends InputStreamResource {
private int length;
public CommonInputStreamResource(InputStream inputStream) {
super(inputStream);
}
public CommonInputStreamResource(InputStream inputStream, int length) {
super(inputStream);
this.length = length;
}
/**
* 覆写⽗类⽅法
* 如果不重写这个⽅法,并且⽂件有⼀定⼤⼩,那么服务端会出现异常
* {@code The multi-part request contained parameter data (excluding uploaded files) that exceeded}
*
* @return
*/
@Override
public String getFilename() {
return "temp";
}
/**
* 覆写⽗类 contentLength ⽅法
* 因为 {@link org.springframework.core.io.AbstractResource#contentLength()}⽅法会重新读取⼀遍⽂件,
* ⽽上传⽂件时,restTemplate 会通过这个⽅法获取⼤⼩。
然后当真正需要读取内容的时候,发现已经读完,会报如下错误。
* <code>
* ng.IllegalStateException: InputStream has already been read - do not use InputStreamResource if a stream needs to be read multiple times
* at org.springframework.core.io.InputStreamResource.getInputStream(InputStreamResource.java:96)
* </code>
* <p>
* ref:com.amazonaws.services.s3.model.S3ObjectInputStream#available()
*
* @return
*/
@Override
public long contentLength() {
int estimate = length;
return estimate == 0 ? 1 : estimate;
}
}
关于contentLength⽂章⾥说的很清楚:上传⽂件时resttemplate会通过这个⽅法得到inputstream的⼤⼩。
⽽InputStreamResource的contentLength⽅法是继承AbstractResource,它的实现如下:
InputStream is = getInputStream();
Assert.state(is != null, "Resource InputStream must not be null");
try {
long size = 0;
byte[] buf = new byte[255];
int read;
while ((read = is.read(buf)) != -1) {
size += read;
}
return size;
}
finally {
try {
is.close();
}
catch (IOException ex) {
}
}
已经读完了流,导致会报错,其实InputStreamResource的类签名是已经注明了:如果需要把流读多次,不要使⽤它。
Do not use an {@code InputStreamResource} if you need to
keep the resource descriptor somewhere, or if you need to read from a stream
multiple times.
所以需要像我上⾯⼀样改写⼀下,然后就可以完成了。
那么原理到底是不是这样呢?继续看。
2. RestTemplate上传⽂件时的处理
上⾯我们说到RestTemplate初始化时,需要注册⼏个消息转换器,那么其中有⼀个就是ResourceHTTPMessageConverter,那么我们看看它完成了哪些功能呢:
⽅法很少,⼀下⼦就可以看完:关于⽂件⼤⼩(contentLength),⽂件类型(ContentType),读(readInternal),写
(org.springframework.http.converter.ResourceHttpMessageConverter#writeInternal)等⽅法。
上⾯的第⼆点,我们说InputStreamResource不做任何处理时,会导致⽂件多次读取,那么是怎么做的呢,我们看看源码:
2.1 第⼀次读取
InputStreamResouce中有两个读取流的⽅法,上⾯讲过,⼀个是contentLength,第⼆个是getInputStream
我们从读取到了⼀下代码:
public final void write(final T t, MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType); //1
if (outputMessage instanceof StreamingHttpOutputMessage) {
StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(final OutputStream outputStream) throws IOException {
writeInternal(t, new HttpOutputMessage() {
@Override
public OutputStream getBody() throws IOException {
return outputStream;
}
@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}
});
}
else {
writeInternal(t, outputMessage);//2
outputMessage.getBody().flush();
}
}
注释中的两个标记处,分别会调⽤contentLength和getInputStream⽅法,但是第⼀个⽅法会直接返回null,不会调⽤。
但是第⼆个⽅法会调⽤⼀次。
这⾥说明上传时,流会被读第⼀次。
3. 服务端上传⽂件时的处理
⽂件源
AbstractMultipartHttpServletRequest # multipartFiles
赋值
StandardMultipartHttpServletRequest # parseRequest
需要 disposition ("content-disposition")⾥有“filename=” 字段或者“filename*=”,从⾥⾯获取 fileName
io.undertow.servlet.spec.HttpServletRequestImpl#loadParts ⾥对 getParts 赋值
MultiPartParserDefinition #io.undertow.servlet.spec.HttpServletRequestImpl#loadParts 解析表单数据
- 其中获取流 ServletInputStreamImpl
按照上⾯的流程排查下来,没有发现有什么问题,唯⼀出问题的地⽅是请求中的“diposition”字段设置有问题,没有把filename=放⼊,导致解析不到⽂件。
3.1 重新回到请求体写⼊FormHttpMessageConverter#writePart
从这个⽅法中,我们可以看到各个转换器的遍历调⽤。
看看下⾯的代码:
private void writePart(String name, HttpEntity<?> partEntity, OutputStream os) throws IOException {
Object partBody = partEntity.getBody();
Class<?> partType = partBody.getClass();
HttpHeaders partHeaders = partEntity.getHeaders();
MediaType partContentType = partHeaders.getContentType();
for (HttpMessageConverter<?> messageConverter : this.partConverters) {
if (messageConverter.canWrite(partType, partContentType)) {
HttpOutputMessage multipartMessage = new MultipartHttpOutputMessage(os);
multipartMessage.getHeaders().setContentDispositionFormData(name, getFilename(partBody)); // 1
if (!partHeaders.isEmpty()) {
multipartMessage.getHeaders().putAll(partHeaders);
}
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
return;
}
}
throw new HttpMessageNotWritableException("Could not write request: no suitable HttpMessageConverter " +
"found for request type [" + partType.getName() + "]");
}
从中我们可以看setContentDispositionFormData这⼀⾏:getFileName⽅法,这⾥会⾛到各个Resource的getFileName⽅法。
真相即将得到:InputStreamResource的这个⽅法是继承⾃org.springframework.core.io.AbstractResource#getFilename,这个⽅法直接返回null。
之后的就很简单了:当fileName为null时,不会在setContentDispositionFormData中把filename=拼⼊。
所以服务端不会解析到⽂件,导致报错。
4. 结论
1、使⽤RestTemplate上传⽂件使⽤FileSystemResource在直接是⽂件的情况下很简单。
2、如果不想在本地新建临时⽂件可以使⽤:InputStreamResource,但是需要覆写FileName⽅法。
3、由于2的原因,2.2.1 中的contentLength⽅法,不会对InputStreamResource做特殊处理,⽽是直接去读取流,导致流被读取多次;按照类签名,会报错。
所以也需要覆写contentLength⽅法。
1. 是由于2的原因,才需要3的存在,不过使⽤⽅式是对的:使⽤InputStreamResource需要覆写两个⽅
法contentLength和getFileName。