什么!我上传的文件不见了?
前言:
最近在实现一个文件上传功能时使用了异步处理,但是在异步处理文件时,却提示NoSuchFileException
错误。简化代码如下:
@PostMapping("/upload")
void testFileUpload(@RequestParam("file") MultipartFile file) {
new Thread(() -> {
try {
Thread.sleep(1000);
InputStream in = file.getInputStream();
} catch (Exception e) {
throw new RuntimeException(e);
}
}).start();
}
这个问题其实是因为MultipartFile
并不持有文件,它只是映射了文件对象,文件暂时存在于Tomcat的临时目录下,在Controller层的方法执行完后,MultipartFile关联的文件就会被清除。
问题分析
要说明这个问题首先要了解MultipartFile
对象的生命周期。
MultipartFile对象的生命周期
-
创建与初始化
当客户端通过HTTP 请求上传文件时,Spring MVC的DispatcherServlet会拦截这个请求。根据请求中的Content-Type(通常是multipart/form-data),Spring MVC会识别出这是一个包含文件上传的请求。Spring MVC使用其内置的
MultipartResolver
(如CommonsMultipartResolver
或StandardServletMultipartResolver
)来解析请求体,将文件保存到Tomcat的临时目录,并将文件数据封装成MultipartFile对象。MultipartFile
对象在此时被创建并初始化,包含了文件的绝对路径、名称、类型、大小等信息。我们跟踪源码来看一下:
-
首先
DispatcherServlet
拦截到请求并执行doDispatch
方法-
在
checkMultipart
方法中,如果请求类型中包含multipart/,则会将请求内容request中的文件进行处理,把文件内容通过输出流保存到临时文件,并封装到request中的parts属性中这个方法就会将文件保存到临时目录下
-
最终,处理完成后,
DispatcherServlet
会将request中的文件封装为StandardMultipartFile
对象,里面的part属性就来自于上面处理后的request中的part对象,值得注意的是,该Part
对象只是文件的映射,并没有对文件的引用。使用文件需要通过getInputStream()
方法获取输入流。
-
-
-
传递给Controller
就是上面说的,将文件处理后,封裝为
MultipartFile
对象。 -
处理与存储
在Controller中,开发者可以对
MultipartFile
对象进行进一步处理,如验证文件类型、大小、存储等。在这个过程中,MultipartFile
对象作为处理文件的媒介,其生命周期与请求处理流程同步。 -
请求处理完成
当Controller层的方法处理完文件上传请求后,会返回一个响应给客户端。此时,与请求相关的资源(包括
MultipartFile
对象)可能会被Spring MVC的底层框架(如Servlet容器)清理,以释放存储资源。也就是说在请求完成后,MultipartFile
映射的临时文件可能会被清除,再通过内存中的MultipartFile
去获取当前文件就会出现找不到文件的异常。
解决办法
-
避免异步处理文件,对文件的操作与
MultipartFile
的生命周期同步。 -
在异步处理文件时,先将文件存储到其他地方,比如对象存储或者本地存储,之后的操作都对重新存储后新文件进行。
-
在进行异步前,提前获取文件的引用。比如在异步线程开始前,通过
getInputStream()
方法获取文件的输入流,再将输入流对象的引用传递到异步线程中。因为此时文件被引用,所以即使MultipartFile
被销毁了,文件暂时也不会被清除。@PostMapping("/upload") void testFileUpload(@RequestParam("file") MultipartFile file) throws IOException { InputStream in = file.getInputStream(); new Thread(() -> { while (true) { try { // todo 操作文件 in.read(); } catch (Exception e) { throw new RuntimeException(e); } } }).start(); }