簡體   English   中英

Spring 引導多部分/表單數據請求文件流式傳輸到下游服務

[英]Spring Boot multipart/form-data request file streaming to downstream service

我有一個微服務架構,其中一個服務充當代理,並且必須只使用 restTemplate 將上傳的表單數據有效負載轉發到下游服務,最好不要從磁盤上的請求或 memory 中加載任何內容。

我設法通過以下步驟解決了這個問題。 在這里,我將描述這些方法和使用的限制:

我有以下 rest 模板配置:

@Bean
public RestTemplate myRestTemplate() {
    HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory();
    requestFactory.setBufferRequestBody(false);
    RestTemplate restTemplate = new RestTemplate(requestFactory);
    restTemplate.setInterceptors(new ArrayList<>()); // to avoid interceptors loading data into memory
    return restTemplate;
}        

in my controller I am processing the HttpServletRequest directly using Apache Commons FileUpload Streaming Api with one asterix: Special care on the multipart form data, so first the form fields are processed in the while loop, and then only one file was I able to process,自從:

 FileItemStream fileItemStream = uploadItemIterator.next();
 return fileItemStream.openStream();               

必須在不調用itemIterator.hasNext()的情況下返回,因為這將導致FileItemStream.ItemSkippedException工作得很好,沒有數據保存在磁盤上

c:\Users\myuser\AppData\Local\Temp\tomcat.11416588345568217859.8077\

注意:我已按照文檔中的說明設置了以下屬性。

spring.application.servlet.multipart.enabled: false

從這里開始,使用流式 api 我有一個 inputStream,我將進一步向下傳遞以創建我的 HttpEntity,如下所示(在示例中進行了簡化,在請求中包含文件名的完整靈感: 這里):

    MultiValueMap<String, Object> multiPartBody = new LinkedMultiValueMap<>();
    multiPartBody.add(FILE, inputStream);
    HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(multiPartBody, myHeaders);

在此之后,我確實撥打了我的 rest 模板:

    myRestTemplate.postForEntity(url, requestEntity, MyResponse.class);       

這通過以下順序一直進行:

RestTemplate.doExecute()
  HttpAccessor.createRequest()
    HttpComponentsClientHttpRequestFactory.createRequest() -> which will return a **HttpComponentsStreamingClientHttpRequest** <- this one is important
      RestTemplate.doWithRequest(ClientHttpRequest httpRequest) -> calls: ((HttpMessageConverter<Object>) messageConverter).write(
                                requestBody, requestContentType, httpRequest);
        FormHttpMessageConverter.write()
          FormHttpMessageConverter.writeMultipart() -> where outputMessage instanceof StreamingHttpOutputMessage is true
            HttpComponentsStreamingClientHttpRequest.executeInternal -> creates a new StreamingHttpEntity(...)
              after which this goes down on InternalCLientExecution, and in execChain

遲早會進入鏈條:

HttpComponentsStreamingClientHttpRequest.StreamingHttpEntity.writeTo(OutputStream outputStream) throws IOException {
    this.body.writeTo(outputStream);
}

其中 body 是來自上方的 FormHttpMessageConverter.lambda:

if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
    streamingOutputMessage.setBody(outputStream -> {
        writeParts(outputStream, parts, boundary);
        writeEnd(outputStream, boundary);
    });
}

所以我們再往下走,最終得到:

FormHttpMessageConverter.writeParts()
    FormHttpMessageConverter.writePart()

這里一個 multipartMessage 被組合並進一步向下傳遞(或調用超類 AbstractHttpMessageConverter 方法)

multipartMessage = new MultipartHttpOutputMessage(os, charset);
...
((HttpMessageConverter<Object>) messageConverter).write(partBody, partContentType, multipartMessage);
                

從這里我們進入 AbstractHttpMessageConverter.write where 條件

if (outputMessage instanceof StreamingHttpOutputMessage)

計算結果為 false,因為 MultipartHttpOutputMessage 不是 StreamingHttpOutputMessage 的實例

但這似乎不影響任何事情,因為整個事情是在上面提到的lambda中調用的,遲早我們需要將輸入流中的字節寫入輸出流。

障礙之一:

如果我將 restTemplate 配置如下:

@Bean
@org.springframework.cloud.client.loadbalancer.LoadBalanced
public RestTemplate myRestTemplate() {
...
}

有一個攔截器/方面用 RibbonClientHttpRequestFactory 覆蓋 RestTemplate HttpComponentsClientHttpRequestFactory(使用 spring netflix 堆棧),它不支持 setBufferRequestBody(false)。

這就是我設法解決文件流問題的方法,希望它也對其他人有所幫助:限制/約束:

  1. 您不能在控制器中使用 MultipartFile,因為 spring 默認情況下會將數據保存到文件系統上的臨時文件中(也不能懶惰地使用解析: 因為),我只能使用 Apache Commons FileUpload 來克服這個問題
  2. 使用 Apache Commons FileUpload 我設法只處理了一個文件,並且需要在文件數據之前處理表單數據
  3. spring.application.servlet.multipart.enabled: false -> 也會影響其他端點
  4. 用正確的 Content-Disposition 組合下游表單數據:表單數據; 名稱=“文件”; filename="my.txt" 需要一些奇怪的嵌入式 HttpEntity 結構
  5. @LoadBalanced 覆蓋整個 restTemplate requestFactory

祝大家好運,歡迎任何反饋。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM