通过`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的实现类,有各种各样的实现类,从名称上来说应该比较有用的应该是: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上传文件时,需要重写该类的两个方法,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
     */
    @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方法。

  1. 是由于2的原因,才需要3的存在,不过使用方式是对的:使用InputStreamResource需要覆写两个方法contentLengthgetFileName

原文地址:https://www.cnblogs.com/paxing/p/11485049.html

时间: 2024-08-25 16:04:49

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

Django session cookie 上传文件、详解

session 在这里先说session 配置URL from django.conf.urls import patterns, include, url from django.contrib import admin admin.autodiscover() urlpatterns = patterns('', # Examples: # url(r'^$', 'csvt11.views.home', name='home'), # url(r'^blog/', include('blog

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

页面源码: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>jquery upload上传文件(asp.net mvc)配置</title> <script src="Resources/jquery.js"

Uploadify 上传文件插件详解

Uploadify是JQuery的一个上传插件,实现的效果非常不错,带进度显示.不过官方提供的实例时php版本的,本文将详细介绍Uploadify在Aspnet中的使用,您也可以点击下面的链接进行演示或下载. •官方下载 •官方文档 •官方演示 <html xmlns="http://www.w3.org/1999/xhtml"> <head runat="server"> <title>Uploadify</title&g

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

rz命令: (X) = option applies to XMODEM only (Y) = option applies to YMODEM only (Z) = option applies to ZMODEM only -+, --append                append to existing files -a, --ascii                 ASCII transfer (change CR/LF to LF) -b, --binary       

Django文件上传机制用法详解(转)

Django文件上传机制用法详解 http://www.jbxue.com/article/24283.html 分享下Django文件上传机制的用法,包括基本上传文件的原理,以及如何处理上传文件的方法,需要的朋友参考下. 当Django处理上传一个文件时,文件数据被放在request.FILES中. 这个文档解释文件怎么样被存储在磁盘上或者内存中,怎样定制默认的行为. 一,基本文件上传考虑一个包含FileField的简单的表单: 复制代码 代码示例: from  django  import 

jquery的uploadify插件多文件上传配置参数详解

最近做了个多文件上传,需要限制上传文件类型的例子.以前没做过找了一些资料,下次有用.同时也给大家做参考. uploader: uploadify.swf 文件的相对路径,该swf文件是一个带有文字BROWSE的按钮,点击后淡出打开文件对话框,默认值:uploadify.swf. script: 后台处理程序的相对路径 .默认值:uploadify.php checkScript:用来判断上传选择的文 件在服务器是否存在的后台处理程序的相对路径 fileDataName:设置一个名字,在服务器处理

文件上传插件uploadify详解

官网:http://www.uploadify.com/ 基于jquery的文件上传控件,支持ajax无刷新上传,多个文件同时上传,上传进行进度显示,删除已上传文件. 要求使用jquery1.4或以上版本,flash player 9.0.24以上. 有两个版本,一个用flash,一个是html5.html5的需要付费~所以这里只说flash版本的用法.

express文件上传中间件Multer详解

Express默认并不处理HTTP请求体中的数据,对于普通请求体(JSON.二进制.字符串)数据,可以使用body-parser中间件.而文件上传(multipart/form-data请求),可以基于请求流处理,也可以使用formidable模块或Multer中间件. 1. multer中间件 Multer是Express官方推出的,用于Node.jsmultipart/form-data请求数据处理的中间件. 它基于busboy构建,可以高效的处理文件上传,但并不处理multipart/fo

文件上传和下载--详解实现

 1.文件上传和下载分析 文件上传: 就是将客户端的数据发送到服务器上 文件上传要求:            浏览器端要求:                                    1.表单提交方式 post                                    2.提供文件上传框(组件) input type="file"                                    3.表单entype属性必须为 multipart/form-