通过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. dubbo源码调试

    1.从github上clone下duboo的源码并checkout tag到2.6.5可以看到如下的结构: 其中all-dubbo的pom如下: 这里会将dubbo的其他项目在package的时候打到 ...

  2. 交换机三种端口模式Access、Hybrid和Trunk

    以太网端口有 3种链路类型:access.trunk.hybird 什么是链路类型? vlan的链路类型可以分为接入链路和干道链路. 1.接入链路(access link)指的交换机到用户设备的链路, ...

  3. MySQL 当前时间,今日时间,前日时间 详解

    MySQL 获取当前日期及日期格式 获取系统日期: NOW() 格式化日期: DATE_FORMAT(date, format) 注: date:时间字段 format:日期格式 返回系统日期,输出 ...

  4. LeetCode 036 Valid Sudoku

    题目要求:Valid Sudoku Determine if a Sudoku is valid, according to: Sudoku Puzzles - The Rules. The Sudo ...

  5. 基于CefSharp开发(二)自定义浏览器窗体

    上一篇 https://www.cnblogs.com/mchao/p/13914726.html 简单了解了CefSharp引用配置但页面光秃秃的,这一篇着手开发简单浏览器窗体 一.Edge浏览器窗 ...

  6. LeetCode初级算法之数组:217 存在重复元素

    存在重复元素 题目地址:https://leetcode-cn.com/problems/contains-duplicate/ 给定一个整数数组,判断是否存在重复元素.如果任意一值在数组中出现至少两 ...

  7. 谈Vite在Electron环境下吃花卷拉馒头的现象

    缘起 在Electron的渲染进程中(也就是页面代码中), 我们常常使用process.env来携带一些环境变量, 比如HTTP服务地址的基质,本地静态资源的路径等 这样做主要有两个目的 一个是方便开 ...

  8. vue Export2Excel 导出文件

    使用需要引入这些js 在src目录下创建一个文件(vendor)进入Blob.js和Export2Excel.js npm install -S file-saver 用来生成文件的web应用程序 n ...

  9. 纯css 实现文本换行

    业务场景:dialog中嵌套的表单,实现信息展示,由于存储路径过长并且在一行显示,导致多出的文字出现在弹出框外面了,页面极丑,所以需要将存储路径实现自动换行. 技术点:<p style=&quo ...

  10. 百度网站统计和CNZZ网站统计对比

    一,前言 百度统计和cnzz统计是目前市面上比较流行的两种web统计工具,接下来将对两个统计工具做初步的体验测评 百度网站统计相关介绍:全球最大的中文网站流量分析平台,帮助企业收集网站访问数据,提供流 ...