经过`RestTemplate`上传文件(InputStreamResource详解)

经过RestTemplate上传文件

1.上传文件File

碰到一个需求,在代码中经过HTTP方式作一个验证的请求,请求的参数包含了文件类型。想一想其实很简单,直接使用定义好的MultiValueMap,把文件参数传入便可。html

咱们知道,restTemplate 默认定义了几个通用的消息转换器,见org.springframework.web.client.RestTemplate#RestTemplate(),那么文件应该对应哪一种资源呢?java

看了上面这个方法以后,能够很快联想到是ResourceHttpMessageConverter,从类签名也能够看出来:web

Implementation of {@link HttpMessageConverter} that can read/write {@link Resource Resources}
and supports byte range requests.

这个转换器主要是用来读写各类类型的字节请求的。spring

既然是Resource,那么咱们来看一下它的实现类有哪些: AbstractResource 以上是AbstractResource的实现类,有各类各样的实现类,从名称上来讲应该比较有用的应该是:InputStreamResourceFileSystemResource,还有ByteArrayResourceUrlResource等。ide

1.1 使用FileSystemResource上传文件

这种方式使用起来比较简单,直接把文件转换成对应的形式便可。函数

MultiValueMap<String, Object> resultMap = new LinkedMultiValueMap<>();
	Resource resource = new FileSystemResource(file);
	param.put("file", resource);

网上使用RestTemplate上传文件大多数是这种方式,简单,方便,不用作过多的转换,直接传递参数便可。ui

可是为何会写这篇博客来记录呢?由于,有一个不喜欢的地方就是,它须要传递一个文件。而我获得是文件源是一个流,我须要在本地建立一个临时文件,而后把InputStream写入到文件中去。使用完以后,还须要把文件删除。this

那么既然这么麻烦,有没有更好的方式呢?url

1.2 使用InputStreamResource上传文件

这个类的构造函数能够直接传入流文件。那么就直接试试吧!spa

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. 结论

一、使用RestTemplate上传文件使用FileSystemResource在直接是文件的状况下很简单。 二、若是不想在本地新建临时文件可使用:InputStreamResource,可是须要覆写FileName方法。 三、因为2的缘由,2.2.1 中的contentLength方法,不会对InputStreamResource作特殊处理,而是直接去读取流,致使流被读取屡次;按照类签名,会报错。因此也须要覆写contentLength方法。 4. 是因为2的缘由,才须要3的存在,不过使用方式是对的:使用InputStreamResource须要覆写两个方法contentLengthgetFileName

相关文章
相关标签/搜索