通过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的实现类,有各种各样的实现类,从名称上来说应该比较有用的应该是:InputStreamResourceFileSystemResource,还有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);

没有想到,服务端报错了,返回的是:没有传递文件。这可就纳闷了,明明已经有了啊。

网上使用这种方式上传的方式不多,只找到这么一个文件,但已经够了:RestTemplate通过InputStreamResource上传文件.

博主的疑问和我一样,不想去创建本地文件,然后就使用了这个流的方式。但是也碰到了问题。

文章写得很清晰:使用InputStreamResource上传文件时,需要重写该类的两个方法,contentLengthgetFilename

果然按照这个文章的思路尝试之后,就成功了。代码如下:

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*/@Overridepublic String getFilename() {return "temp";}/*** 覆写父类 contentLength 方法* 因为 {@link org.springframework.core.io.AbstractResource#contentLength()}方法会重新读取一遍文件,* 而上传文件时,restTemplate 会通过这个方法获取大小。然后当真正需要读取内容的时候,发现已经读完,会报如下错误。* <code>* java.lang.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*/@Overridepublic long contentLength() {int estimate = length;return estimate == 0 ? 1 : estimate;}
}

关于contentLength文章里说的很清楚:上传文件时resttemplate会通过这个方法得到inputstream的大小。

InputStreamResourcecontentLength方法是继承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 tokeep the resource descriptor somewhere, or if you need to read from a streammultiple times.

所以需要像我上面一样改写一下,然后就可以完成了。那么原理到底是不是这样呢?继续看。

1.3 使用ByteArrayResource上传文件

ByteArrayResource resource = new ByteArrayResource(file.getBytes()) {@Overridepublic String getFilename() {return file.getOriginalFilename();}};MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();
params.add("file", resource);HttpEntity request = new HttpEntity<>(params, headers);
responseEntity = restTemplate.postForEntity(url, request, String.class);

只需要重写getFileName()即可

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); //1if (outputMessage instanceof StreamingHttpOutputMessage) {StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) outputMessage;streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {@Overridepublic void writeTo(final OutputStream outputStream) throws IOException {writeInternal(t, new HttpOutputMessage() {@Overridepublic OutputStream getBody() throws IOException {return outputStream;}@Overridepublic HttpHeaders getHeaders() {return headers;}});}});}else {writeInternal(t, outputMessage);//2outputMessage.getBody().flush();}}

注释中的两个标记处,分别会调用contentLengthgetInputStream方法,但是第一个方法会直接返回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)); // 1if (!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方法,这里会走到各个ResourcegetFileName方法。

真相即将得到: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方法。
4. 是由于2的原因,才需要3的存在,不过使用方式是对的:使用InputStreamResource需要覆写两个方法contentLengthgetFileName.
5.推荐使用
ByteArrayResource进行文件流的上传

RestTemplate传递文件流相关推荐

  1. restTemplate接受文件流

    后台导出积木报表PDF 目的 积木报表版本 代码 功能特色 心得记录 目的 为了将pdf留存下来进行存档,使用的积木报表pdf接口返回的文件流,进行保存到本地. 积木报表版本 <dependen ...

  2. java流式上传下载_精讲RestTemplate第6篇-文件上传下载与大文件流式下载

    C++Templates(第2版英文版) 123.24元 (需用券) 去购买 > 本文是精讲RestTemplate第6篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在S ...

  3. vue-axios下载文件流blob,ie下载报传递给系统调用的数据区域太小.ie文件流下载报错;文件下载失败将blob的错误信息转换成json格式

    本次下载是后台文件流传输,前端下载,前端将拿到的下载id和名称downloadName传递给下载方法:如果是多个下载,可以采用数组for循环 情景描述: 1.如果符合导出条件, 后端直接返回数据流,如 ...

  4. c++语言文件流,C++ IO类、文件输入输出、string流详细讲解

    新的C++标准中有三分之二的内容都是描述标准库.接下来重点学习其中几种核心库设施,这些是应该熟练掌握的. 标准库的核心是很多容器类(顺序容器和关联容器等)和一簇泛型算法(该类算法通常在顺序容器一定范围 ...

  5. .Net 文件流 System.IO之Stream

    转自 :http://www.cnblogs.com/yukaizhao/archive/2011/07/28/stream.html Stream在msdn的定义:提供字节序列的一般性视图(prov ...

  6. php接收流文件,PHP传输文件流及文件流的保存

    什么是文件流 在HTTP数据传送过程中,传输一方直接以二进制流方式传送文件内容,这样就形成了一个文件流: 文件流的接收通常涉及到预定义变量函数 $HTTP_RAW_POST_DATA 和 file_g ...

  7. java 文件流的处理 文件打包成zip

    1.下载文件到本地 public void download(HttpServletResponse response){String filePath ="";//文件路径Str ...

  8. java流与文件——流

    [0]README 0.1) 本文描述转自 core java volume 2, 旨在理解 java流与文件--流 的相关知识: 0.2) 输入流和输出流(InputStream 和 OutputS ...

  9. C++ 流的操作 | 初识IO类、文件流、string流的使用

    文章目录 前言 IO头文件 iostream fstream sstream 流的使用 不能拷贝或对 IO对象 赋值 条件状态与 iostate 类型 输出缓冲区 文件流 fstream类型 文件模式 ...

最新文章

  1. 占据翻译机市场大半壁江山,科大讯飞现AI新物种
  2. [Android Pro] linux下查看一个文件的属性(ls,lsattr,file,stat)
  3. debian下安装LNMP环境(二)
  4. Zend Studio实现移动程序开发一体化的秘密武器——CCM
  5. centos yum install redis
  6. 数学--数论--HDU 12151七夕节 Plus (因子和线性筛)
  7. 成功人士都有的好习惯
  8. bootstrap兼容ie8以下版本
  9. USACO 2016 January Contest, Gold解题报告
  10. angularjs 笔记(1) -- 引导
  11. 《python黑帽子 黑客与渗透测试编程之道》第二章-网络基础 tcp、udp客户端、服务端
  12. 向量空间 内积空间 欧氏空间 希尔伯特空间
  13. mac_excel_条件格式
  14. 数据科学数据清理和可视化,适合使用python的初学者
  15. 人艰不拆~找实习之路(二)。
  16. DELPHI 读取TXT文件unicode乱码
  17. 实验一 |彩色空间rgb和yuv的相互转换
  18. 乓乓响再次冲刺香港上市,黄建义、张卫平夫妇套现约2130万元
  19. 618蓝牙耳机选哪一款比较好?推荐口碑最好的无线蓝牙耳机品牌
  20. TearDrops(泪滴攻击)教程

热门文章

  1. 微信 weui框架代码汇总
  2. 苹果手表|apple watch series 6健康功能提升
  3. 记一次某应用虚拟化系统远程代码执行
  4. 51单片机二进制转bcd码c语言,16位二进制数转换成BCD码的的快速算法-51单片机...
  5. 我的CSS学习笔记(五)
  6. 【洛谷】P1957 口算练习题【C++】
  7. android+ble室内定位,基于BLE的室内定位系统的设计与实现
  8. dto java_Java学习笔记(十八)——Java DTO
  9. 什么是硬分叉和软分叉?Tokenview
  10. [Guhao总结]wap个人建站[学习资料]