通过`RestTemplate`上传文件(InputStreamResource详解)
通过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
*/
@Override
public 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
*/
@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方法。
4. 是由于2的原因,才需要3的存在,不过使用方式是对的:使用InputStreamResource需要覆写两个方法contentLength和getFileName。
通过`RestTemplate`上传文件(InputStreamResource详解)的更多相关文章
- Uploadify 上传文件插件详解
Uploadify 上传文件插件详解 Uploadify是JQuery的一个上传插件,实现的效果非常不错,带进度显示.不过官方提供的实例时php版本的,本文将详细介绍Uploadify在Aspnet中 ...
- Django session cookie 上传文件、详解
session 在这里先说session 配置URL from django.conf.urls import patterns, include, url from django.contrib i ...
- jquery.uploadify上传文件配置详解(asp.net mvc)
页面源码: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" c ...
- windows下命令行终端使用rz上传文件参数详解
rz命令: (X) = option applies to XMODEM only (Y) = option applies to YMODEM only (Z) = option applies t ...
- express文件上传中间件Multer详解
express文件上传中间件Multer详解 转载自:https://www.cnblogs.com/chengdabelief/p/6580874.html Express默认并不处理HTTP请 ...
- 【iOS 使用github上传代码】详解
[iOS 使用github上传代码]详解 一.github创建新工程 二.直接添加文件 三.通过https 和 SSH 操作两种方式上传工程 3.1https 和 SSH 的区别: 3.1.1.前者可 ...
- Spring Boot 上传文件 获取项目根路径 物理地址 resttemplate上传文件
springboot部署之后无法获取项目目录的问题: 之前看到网上有提问在开发一个springboot的项目时,在项目部署的时候遇到一个问题:就是我将项目导出为jar包,然后用java -jar 运行 ...
- 文件上传插件uploadify详解
官网:http://www.uploadify.com/ 基于jquery的文件上传控件,支持ajax无刷新上传,多个文件同时上传,上传进行进度显示,删除已上传文件. 要求使用jquery1.4或以上 ...
- Java Struts文件上传和下载详解
Struts2文件上传 Struts 2框架提供了内置支持处理文件上传使用基于HTML表单的文件上传.上传一个文件时,它通常会被存储在一个临时目录中,他们应该由Action类进行处理或移动到一个永久的 ...
随机推荐
- H3CNE认证(题库)
H3CNE考试的题库,均为发烧友收集的,拥有将近认证考试的百分之八十五的题,但答案不具备官方性,但是题库具有解析. https://huxiaoyao.lanzous.com/b01tr2skd 密码 ...
- 《Machine Learning in Action》—— 白话贝叶斯,“恰瓜群众”应该恰好瓜还是恰坏瓜
<Machine Learning in Action>-- 白话贝叶斯,"恰瓜群众"应该恰好瓜还是恰坏瓜 概率论,可以说是在机器学习当中扮演了一个非常重要的角色了.T ...
- Map<String,Object>接收参数,Long类型降级为Integer,报类型转换异常
前言 今天看群里小伙伴问了一个非常有意思的问题: 使用 Map<String,Object> 对象接收前端传递的参数,在后端取参时,因为接口文档中明确该字段类型为 Long ,所以对接收的 ...
- Linux之【GNU】、【GPL】、【linux系统组成】
GNU,什么是GNU GNU全称:GNU's not unix GNU的重要组件(Emacs,gcc,bash,gawk等)加上自己的内核构成了GNU自己的系统--->没用 现在linux中的一 ...
- PyQt(Python+Qt)学习随笔:QMdiArea多文档界面部件的subWindowActivated信号
专栏:Python基础教程目录 专栏:使用PyQt开发图形界面Python应用 专栏:PyQt入门学习 老猿Python博文目录 QMdiArea的subWindowActivated在一个窗口激活( ...
- 第8.25节 Python风格的__getattribute__属性访问方法语法释义及使用
一. 引言 在<第8.13节 Python类中内置方法__repr__详解>老猿介绍了在命令行方式直接输入"对象"就可以调用repr内置函数或__repr__方法查看对 ...
- 第7章 Python类型、类、协议目录
第7.1节 面向对象程序设计的相关知识 第7.2节 关于面向对象设计的一些思考 第7.3节 Python特色的面向对象设计:协议.多态及鸭子类型 第7.4节 Python中与众不同的类 第7.5节 揭 ...
- PyQt(Python+Qt)学习随笔:model/view架构中的两个标准模型QStandardItemModel和QFileSystemModel
老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.PyQt中的标准模型 PyQt和Qt提供了两个标准模型QStandardItemModel和QF ...
- java中的反射(二)
java中的反射(一):https://www.cnblogs.com/KeleLLXin/p/14060555.html 目录 一.反射 1.class类 2.访问字段 3.调用方法 4.调用构造方 ...
- WebRequest抓取网页数据出现乱码问题
今天项目里突然有个功能用不起来了,本机确实好的 ,这个很无语 不知道为啥 经过写日志发现html 变成了这样的东西,很是头疼,刚开始各种编码转换,发现这并不是编码的问题 后面观察目标网站多了一个gzi ...