經過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
的實現類,有各類各樣的實現類,從名稱上來講應該比較有用的應該是:InputStreamResource
和FileSystemResource
,還有ByteArrayResource
和 UrlResource
等。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的大小。
而InputStreamResource
的contentLength
方法是繼承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(); } }
註釋中的兩個標記處,分別會調用contentLength
和getInputStream
方法,可是第一個方法會直接返回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
方法,這裏會走到各個Resource
的getFileName
方法。
真相即將獲得: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須要覆寫兩個方法contentLength
和getFileName
。