通过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,还有ByteArrayResourceUrlResource等。

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的大小。

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 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();
}
}

注释中的两个标记处,分别会调用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)); // 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方法,这里会走到各个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

通过`RestTemplate`上传文件(InputStreamResource详解)的更多相关文章

  1. Uploadify 上传文件插件详解

    Uploadify 上传文件插件详解 Uploadify是JQuery的一个上传插件,实现的效果非常不错,带进度显示.不过官方提供的实例时php版本的,本文将详细介绍Uploadify在Aspnet中 ...

  2. Django session cookie 上传文件、详解

    session 在这里先说session 配置URL from django.conf.urls import patterns, include, url from django.contrib i ...

  3. jquery.uploadify上传文件配置详解(asp.net mvc)

    页面源码: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" c ...

  4. windows下命令行终端使用rz上传文件参数详解

    rz命令: (X) = option applies to XMODEM only (Y) = option applies to YMODEM only (Z) = option applies t ...

  5. express文件上传中间件Multer详解

    express文件上传中间件Multer详解 转载自:https://www.cnblogs.com/chengdabelief/p/6580874.html   Express默认并不处理HTTP请 ...

  6. 【iOS 使用github上传代码】详解

    [iOS 使用github上传代码]详解 一.github创建新工程 二.直接添加文件 三.通过https 和 SSH 操作两种方式上传工程 3.1https 和 SSH 的区别: 3.1.1.前者可 ...

  7. Spring Boot 上传文件 获取项目根路径 物理地址 resttemplate上传文件

    springboot部署之后无法获取项目目录的问题: 之前看到网上有提问在开发一个springboot的项目时,在项目部署的时候遇到一个问题:就是我将项目导出为jar包,然后用java -jar 运行 ...

  8. 文件上传插件uploadify详解

    官网:http://www.uploadify.com/ 基于jquery的文件上传控件,支持ajax无刷新上传,多个文件同时上传,上传进行进度显示,删除已上传文件. 要求使用jquery1.4或以上 ...

  9. Java Struts文件上传和下载详解

    Struts2文件上传 Struts 2框架提供了内置支持处理文件上传使用基于HTML表单的文件上传.上传一个文件时,它通常会被存储在一个临时目录中,他们应该由Action类进行处理或移动到一个永久的 ...

随机推荐

  1. 冰河教你一次性成功安装K8S集群(基于一主两从模式)

    写在前面 研究K8S有一段时间了,最开始学习K8S时,根据网上的教程安装K8S环境总是报错.所以,我就改变了学习策略,先不搞环境搭建了.先通过官网学习了K8S的整体架构,底层原理,又硬啃了一遍K8S源 ...

  2. Java进阶专题(十八) 系统缓存架构设计 (下)

    前言 上章节介绍了Redis相关知识,了解了Redis的高可用,高性能的原因.很多人认为提到缓存,就局限于Redis,其实缓存的应用不仅仅在于Redis的使用,比如还有Nginx缓存,缓存队列等等.这 ...

  3. spring + quartz 分布式自定义注解

    相关技术 本文采用spring + quartz的方案.使用mysql作为任务的持久化,支持分布式. 自定义注解 1.启用定时任务 @Target(ElementType.TYPE) @Retenti ...

  4. 第4.2节 神秘而强大的Python生成器精讲

    一. 生成器(generator)概念 生成器是一个特殊的迭代器,它保存的是算法,每次调用next()或send()就计算出下一个元素的值,直到计算出最后一个元素,没有更多的元素时,抛出StopIte ...

  5. 第二十二章、 Model/View便利类树型部件QTreeWidget

    老猿Python博文目录 专栏:使用PyQt开发图形界面Python应用 老猿Python博客地址 一.引言 树部件(Tree Widget)是Qt Designer中 Item Widgets(It ...

  6. Monkey 部署环境

    Monkey的介绍 它是Android系统自带一个命令行工具,可以运行在模拟器里或者真是设备中运行. Monkey是发送伪随机用户事件的工具. Monkey向系统发送伪随机的用户事件流,实现对正在开发 ...

  7. PHP 的$server变量

    PHP中$_SERVER["QUERY_STRING"]函数   详解PHP中$_SERVER函数的QUERY_STRING和 $_SERVER存储当前服务器信息,其中有几个值 如 ...

  8. day105:Mofang:设置页面初始化&更新头像/上传头像&设置页面显示用户基本信息

    目录 1.设置页面初始化 2.更新头像 1.点击头像进入更新头像界面 2.更新头像页面初始化 3.更新头像页面CSS样式 4.头像上传来源选择:相册/相机 5.调用api提供的本地接口从相册/相机提取 ...

  9. Codeforces Round #682 Div2 简要题解

    Contest link A.Specific Tastes of Andre Problem link 题意 构造一个长度为 \(n\) 的序列,使得每个非空子序列的和都被其长度整除. 思路 直接每 ...

  10. 【NOI2019】弹跳(KDT优化建图)

    Description 平面上有 \(n\) 个点,分布在 \(w \times h\) 的网格上.有 \(m\) 个弹跳装置,由一个六元组描述.第 \(i\) 个装置有参数:\((p_i, t_i, ...