Spring Boot中MultipartFile实现原理机制详解
Spring Boot文件上传会使用到MultipartFile,下面深入了解一下它的运行机制、配置要点以及实际应用中的问题与解决方案。
一、MultipartFile解析流程
当客户端发起文件上传请求,也就是发送multipart/form-data
请求时,整个处理流程如下:
- Servlet容器处理:Servlet容器接收到请求后,会启动HTTP协议解析工作。它根据请求头中的
Content-Type
来识别这是一个multipart
类型的请求,然后创建临时存储结构。这个临时存储结构有可能在内存中,也可能在磁盘上,具体取决于后续配置。处理完这些后,Servlet容器会将解析后的数据封装成Request
对象。 - 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
类。
- 读过程:它既支持流式读取数据,也支持将数据全部加载到字节数组中。不过,将数据全部加载到字节数组这种方式,在处理大文件时可能会导致内存溢出问题。
- 写过程:会先将数据写入一个字节数组中,同样存在内存溢出的风险。
在实际开发中,需要根据具体的业务场景和文件大小,合理选择处理方式,避免出现内存溢出等问题,确保系统的稳定运行。