Spring Boot多部分/表单数据请求文件流式传输到下游服务



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

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

我有以下休息模板配置:

@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直接处理HttpServlet请求,其中有一个星号:特别注意多部分表单数据,因此首先在while循环中处理表单字段,然后我只能处理一个文件,因为:

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

必须在不调用itemIterator.hasNext((的情况下返回,因为这将导致FileItemStream.ItemSkippedException它工作得很好,没有数据保存在磁盘上

c:UsersmyuserAppDataLocalTemptomcat.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其中条件

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:因为(,我只能通过Apache Commons FileUpload解决这个问题
  2. 使用ApacheCommonsFileUpload,我只处理了一个文件,表单数据需要在文件数据之前处理
  3. spring.application.servlet.multipart.enabled:false->也会影响其他端点
  4. 组成具有正确内容处置的下游表单数据:表单数据;name=";文件";;filename=";my.txt";需要一些奇怪的嵌入式HttpEntity构造
  5. @LoadBalanced覆盖整个restTemplate requestFactory

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

最新更新