【问题标题】:Spring Boot multipart/form-data request file streaming to downstream serviceSpring Boot multipart/form-data 请求文件流式传输到下游服务
【发布时间】:2022-01-26 09:16:23
【问题描述】:

我有一个微服务架构,其中一个服务充当代理,并且必须只使用 restTemplate 将上传的表单数据负载转发到下游服务,最好不要从请求中加载任何内容到磁盘或内存中。

【问题讨论】:

    标签: java spring resttemplate


    【解决方案1】:

    我设法通过以下步骤解决了这个问题。 在这里,我将描述这些方法和使用的限制:

    我有以下其余模板配置:

    @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;
    }        
    

    在我的控制器中,我正在使用带有一个星号的 Apache Commons FileUpload Streaming Api 直接处理 HttpServletRequest: 特别注意多部分表单数据,因此首先在 while 循环中处理表单字段,然后我只能处理一个文件,因为:

     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,如下所示(在示例中简化,在请求中包含文件名的完整灵感:here):

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

    在此之后,我确实调用了我的休息模板:

        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 中调用的,所以迟早我们需要将 inputStream 中的字节写入 outputStream 中。

    一个障碍:

    如果我配置restTemplate如下:

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

    有一个拦截器/方面用 RibbonClientHttpRequestFactory(使用 spring netflix 堆栈)覆盖了 RestTemplate HttpComponentsClientHttpRequestFactory,它不支持 setBufferRequestBody(false)。

    这就是我设法解决文件流问题的方法,希望对其他人也有帮助: 限制/约束:

    1. 您不能在控制器中使用 MultipartFile,因为 spring 默认情况下会将数据保存到 fileSystem 上的临时文件中(也不能使用 resolve-lazily:because),我只能通过 Apache Commons FileUpload 克服这个问题李>
    2. 使用 Apache Commons FileUpload 我设法只处理了一个文件,并且需要在处理文件数据之前处理表单数据
    3. spring.application.servlet.multipart.enabled: false -> 也会影响其他端点
    4. 使用正确的 Content-Disposition 组合下游表单数据:form-data;名称="文件"; filename="my.txt" 需要一些奇怪的嵌入式 HttpEntity 结构
    5. @LoadBalanced 覆盖整个 restTemplate requestFactory

    祝大家好运,欢迎任何反馈。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2022-01-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-27
      • 2014-02-20
      • 1970-01-01
      相关资源
      最近更新 更多