Spring Boot文件上传会使用到MultipartFile,下面深入了解一下它的运行机制、配置要点以及实际应用中的问题与解决方案。

一、MultipartFile解析流程

当客户端发起文件上传请求,也就是发送multipart/form-data请求时,整个处理流程如下:

  1. Servlet容器处理:Servlet容器接收到请求后,会启动HTTP协议解析工作。它根据请求头中的Content-Type来识别这是一个multipart类型的请求,然后创建临时存储结构。这个临时存储结构有可能在内存中,也可能在磁盘上,具体取决于后续配置。处理完这些后,Servlet容器会将解析后的数据封装成Request对象。
  2. Spring MVC解析:封装好的Request对象进入Spring MVC框架后,会触发MultipartResolver进行解析。这里有个关键的点要注意,只有在调用MultipartFile.getBytes()方法时,文件数据才会真正被加载到内存中。

用序列图表示的话,就是:

二、上传配置详解

在Spring Boot项目中,可以通过配置文件对文件上传进行设置。以YAML配置文件为例:

spring: servlet: multipart: enabled: true file-size-threshold: 1MB # 1MB 以下用内存,以上用磁盘 max-file-size: 5GB # 单个文件最大 max-request-size: 10GB # 整个请求最大 location: /data/tmp # 指定专用临时目录 
  • enabled:开启文件上传功能。
  • file-size-threshold:用于设定文件存储策略的阈值。小于这个阈值(1MB)的文件,会优先存储在内存中;大于这个阈值的文件,则会存储到磁盘上。
  • max-file-size:限制单个文件的最大上传大小。
  • max-request-size:规定整个上传请求的最大大小。
  • location:指定文件上传过程中临时存储的目录。

三、Feign上传文件的问题及解决方案

在实际开发中,使用Feign上传大文件时可能会遇到问题。比如,会出现“Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: Required array length 2147483639 + 9 is too large”这样的报错信息。这是因为Feign在处理大文件上传时存在一些局限性,此时可以考虑使用RestTemplate来实现文件上传。

(一)调用方实现

/** * 使用@LoadBalanced注解不能进行流式上传 * @return */ public RestTemplate getUploadRestTemplate() { // 使用 Apache HttpClient 作为底层实现,使用 流式上传 CloseableHttpClient httpClient = HttpClients.custom() .setMaxConnTotal(100) // 最大连接数 .setMaxConnPerRoute(20) // 每个路由的最大连接数 .build(); HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient); factory.setBufferRequestBody(false); factory.setConnectTimeout(30_000); // 连接超时 30s factory.setReadTimeout(300_000); // 读取超时 5min(大文件需延长) return new RestTemplate(factory); } @PostMapping(value = "/file/upload") public String uploadStream(@RequestParam("file") MultipartFile file) throws IOException { // 包装文件流为Resource(关键:实现contentLength()返回 -1) int bufSize = 64*1024; //默认8kb Resource resource = new InputStreamResource(new BufferedInputStream(file.getInputStream(),bufSize)) { @Override public long contentLength() { return -1; // 触发分块传输 } @Override public String getFilename() { return file.getName(); // 确保服务器能获取文件名 } }; // 构建Multipart请求体 // 这里没有调用file.transferTo(),可能是原代码有误,正常上传文件需要处理文件存储逻辑 // 设置请求头 HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); ResponseEntity<String> response = //手动进行负载 getUploadRestTemplate().exchange("http://localhost:8081/upload-file4?name="+file.getOriginalFilename(), HttpMethod.POST, new HttpEntity<>(resource,headers), String.class); System.out.println(JSON.toJSONString(response)); return "success"; } 

这里需要注意,添加了@LoadBalanced注解的RestTemplate会缓存请求数据,在处理大文件上传时容易导致内存溢出,所以在处理大文件上传场景时不能使用该注解。如果是在微服务系统中使用,就需要自行实现负载均衡逻辑。

(二)服务器方实现

//这样直接从request获取文件流,就不解析MultipartFile,效率也要好点 @PostMapping(value = "/upload-file4", consumes = "application/octet-stream") public String uploadStream3(HttpServletRequest request,@RequestParam("name") String name) throws Exception { try (InputStream inputStream = request.getInputStream()) { Files.copy(inputStream, Paths.get("d:/" + System.currentTimeMillis() + "_" + name)); return "Upload success!"; } } 

这种方式直接从HttpServletRequest中获取文件流,绕过了MultipartFile的解析过程,在一定程度上提高了效率。

四、Spring对application/octet-stream的处理

Spring Boot在处理application/octet-stream请求类型时,主要使用ResourceHttpMessageConverter类。

  1. 读过程:它既支持流式读取数据,也支持将数据全部加载到字节数组中。不过,将数据全部加载到字节数组这种方式,在处理大文件时可能会导致内存溢出问题。
  2. 写过程:会先将数据写入一个字节数组中,同样存在内存溢出的风险。

在实际开发中,需要根据具体的业务场景和文件大小,合理选择处理方式,避免出现内存溢出等问题,确保系统的稳定运行。