SpringBoot如何转发Multipart请求至内网微服务实现文件不落地存储
完全可以实现,核心原则是全链路以流的方式处理上传请求,禁止任何节点触发Multipart请求的自动落地逻辑,两个服务都不需要把上传文件写到本地磁盘。
第一步:基础配置(两个服务必须同步修改)
SpringBoot默认会自动解析multipart/form-data请求,把上传的文件写入本地临时目录,这是文件落地的核心原因,首先要关掉这个自动配置:
spring: servlet: multipart: enabled: false
避坑提醒:配置完后不要在接口里注入
MultipartHttpServletRequest、MultipartFile类型的参数,也不要提前调用request.getParameter()系列方法,这些操作都会触发Servlet容器自动解析Multipart请求,导致文件写本地。
第二步:microservice-1(入口服务)请求透传实现
入口层不要解析请求内容,直接把原始请求的输入流原样转发给内网的microservice-2即可,转发时必须保留原请求的Content-Type头(里面带Multipart分隔需要的boundary参数,不能改),可以用RestTemplate、OkHttp、WebClient任意HTTP客户端实现,核心逻辑参考:
@RestController public class UploadForwardController { private final RestTemplate restTemplate = new RestTemplate(); private final URI targetUri = URI.create("http://microservice-2/internal/upload"); @PostMapping(value = "/api/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity<byte[]> forwardUpload(HttpServletRequest request) throws IOException { // 直接复用原请求的输入流,不做全量读取 InputStreamResource requestBody = new InputStreamResource(request.getInputStream()) { @Override public long contentLength() throws IOException { // 直接取原请求的content-length,避免提前读流计算长度导致全量加载到内存 return Long.parseLong(request.getHeader(HttpHeaders.CONTENT_LENGTH)); } }; RequestEntity<InputStreamResource> forwardReq = RequestEntity.post(targetUri) .header(HttpHeaders.CONTENT_TYPE, request.getContentType()) .body(requestBody); // 转发请求,把后端服务的响应原样返回给用户 return restTemplate.exchange(forwardReq, byte[].class); } }
如果microservice-1本身是Spring Cloud Gateway网关,甚至不用写上面的转发代码,只要关掉Multipart自动解析,网关默认就是流式透传,不会落地文件。
第三步:microservice-2(内网服务)流式处理实现
内网服务同样不要用默认的Multipart解析,直接从请求里拿输入流做流式处理即可。如果需要解析Multipart里的普通表单字段、区分多个上传文件,可以用Apache Commons FileUpload的流式解析API,这个API不会把文件写本地,是逐段读取解析的,核心逻辑参考:
首先引入依赖:
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.5</version> </dependency>
处理逻辑:
@RestController public class InternalUploadController { @PostMapping("/internal/upload") public void handleUpload(HttpServletRequest request) throws Exception { ServletFileUpload upload = new ServletFileUpload(); FileItemIterator itemIterator = upload.getItemIterator(request); while (itemIterator.hasNext()) { FileItemStream item = itemIterator.next(); try (InputStream stream = item.openStream()) { if (item.isFormField()) { // 处理普通表单字段,直接读流拿值 String fieldValue = Streams.asString(stream, StandardCharsets.UTF_8); // 业务逻辑处理字段 } else { // 拿到文件输入流,直接边读边处理,比如直接推送到对象存储、做实时内容校验等 // 注意不要把流全量转成byte[]存在内存,保持边读边写的流式处理逻辑 } } } } }
额外优化点
- 两个服务都可以适当调整Servlet容器的连接超时、请求大小限制,适配大文件上传场景
- 如果需要支持断点续传,可以在流透传的时候透传对应的Range头即可,逻辑和普通HTTP请求透传没有区别
- 大文件场景下建议用异步HTTP客户端(比如WebClient、OkHttp异步模式)做转发,避免阻塞Servlet线程
内容的提问来源于stack exchange,提问作者Popeye

