RestTemplate传递文件流
通过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);
没有想到,服务端报错了,返回的是:没有传递文件。这可就纳闷了,明明已经有了啊。
网上使用这种方式上传的方式不多,只找到这么一个文件,但已经够了:RestTemplate通过InputStreamResource上传文件.
博主的疑问和我一样,不想去创建本地文件,然后就使用了这个流的方式。但是也碰到了问题。
文章写得很清晰:使用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*/@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的大小。
而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 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();}}
注释中的两个标记处,分别会调用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)); // 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
方法,这里会走到各个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
方法。
4. 是由于2的原因,才需要3的存在,不过使用方式是对的:使用InputStreamResource需要覆写两个方法contentLength
和getFileName.
ByteArray
5.推荐使用Resource进行文件流的上传
RestTemplate传递文件流相关推荐
- restTemplate接受文件流
后台导出积木报表PDF 目的 积木报表版本 代码 功能特色 心得记录 目的 为了将pdf留存下来进行存档,使用的积木报表pdf接口返回的文件流,进行保存到本地. 积木报表版本 <dependen ...
- java流式上传下载_精讲RestTemplate第6篇-文件上传下载与大文件流式下载
C++Templates(第2版英文版) 123.24元 (需用券) 去购买 > 本文是精讲RestTemplate第6篇,前篇的blog访问地址如下: 精讲RestTemplate第1篇-在S ...
- vue-axios下载文件流blob,ie下载报传递给系统调用的数据区域太小.ie文件流下载报错;文件下载失败将blob的错误信息转换成json格式
本次下载是后台文件流传输,前端下载,前端将拿到的下载id和名称downloadName传递给下载方法:如果是多个下载,可以采用数组for循环 情景描述: 1.如果符合导出条件, 后端直接返回数据流,如 ...
- c++语言文件流,C++ IO类、文件输入输出、string流详细讲解
新的C++标准中有三分之二的内容都是描述标准库.接下来重点学习其中几种核心库设施,这些是应该熟练掌握的. 标准库的核心是很多容器类(顺序容器和关联容器等)和一簇泛型算法(该类算法通常在顺序容器一定范围 ...
- .Net 文件流 System.IO之Stream
转自 :http://www.cnblogs.com/yukaizhao/archive/2011/07/28/stream.html Stream在msdn的定义:提供字节序列的一般性视图(prov ...
- php接收流文件,PHP传输文件流及文件流的保存
什么是文件流 在HTTP数据传送过程中,传输一方直接以二进制流方式传送文件内容,这样就形成了一个文件流: 文件流的接收通常涉及到预定义变量函数 $HTTP_RAW_POST_DATA 和 file_g ...
- java 文件流的处理 文件打包成zip
1.下载文件到本地 public void download(HttpServletResponse response){String filePath ="";//文件路径Str ...
- java流与文件——流
[0]README 0.1) 本文描述转自 core java volume 2, 旨在理解 java流与文件--流 的相关知识: 0.2) 输入流和输出流(InputStream 和 OutputS ...
- C++ 流的操作 | 初识IO类、文件流、string流的使用
文章目录 前言 IO头文件 iostream fstream sstream 流的使用 不能拷贝或对 IO对象 赋值 条件状态与 iostate 类型 输出缓冲区 文件流 fstream类型 文件模式 ...
最新文章
- 占据翻译机市场大半壁江山,科大讯飞现AI新物种
- [Android Pro] linux下查看一个文件的属性(ls,lsattr,file,stat)
- debian下安装LNMP环境(二)
- Zend Studio实现移动程序开发一体化的秘密武器——CCM
- centos yum install redis
- 数学--数论--HDU 12151七夕节 Plus (因子和线性筛)
- 成功人士都有的好习惯
- bootstrap兼容ie8以下版本
- USACO 2016 January Contest, Gold解题报告
- angularjs 笔记(1) -- 引导
- 《python黑帽子 黑客与渗透测试编程之道》第二章-网络基础 tcp、udp客户端、服务端
- 向量空间 内积空间 欧氏空间 希尔伯特空间
- mac_excel_条件格式
- 数据科学数据清理和可视化,适合使用python的初学者
- 人艰不拆~找实习之路(二)。
- DELPHI 读取TXT文件unicode乱码
- 实验一 |彩色空间rgb和yuv的相互转换
- 乓乓响再次冲刺香港上市,黄建义、张卫平夫妇套现约2130万元
- 618蓝牙耳机选哪一款比较好?推荐口碑最好的无线蓝牙耳机品牌
- TearDrops(泪滴攻击)教程
热门文章
- 微信 weui框架代码汇总
- 苹果手表|apple watch series 6健康功能提升
- 记一次某应用虚拟化系统远程代码执行
- 51单片机二进制转bcd码c语言,16位二进制数转换成BCD码的的快速算法-51单片机...
- 我的CSS学习笔记(五)
- 【洛谷】P1957 口算练习题【C++】
- android+ble室内定位,基于BLE的室内定位系统的设计与实现
- dto java_Java学习笔记(十八)——Java DTO
- 什么是硬分叉和软分叉?Tokenview
- [Guhao总结]wap个人建站[学习资料]