通過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
以上是AbstractResource的實現類,有各種各樣的實現類,從名稱上來說應該比較有用的應該是:InputStreamResourceFileSystemResource,還有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上傳文件時,需要重寫該類的兩個方法,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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM