繁体   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