MultipartFile为org.springframework.web.mutipart包下的一个类,也就是说如果想使用MultipartFile这个类就必须引入spring框架,换句话说,如果想在项目中使用MultipartFile这个类,那么项目必须要使用spring框架才可以,否则无法引入这个类。
以下基于spring-web.5.2.9.RELEASE源码了解一下MultipartFile
MultipartFile注释说明
第一句:一种可以接收使用多种请求方式来进行上传文件的代表形式。也就是说,如果你想用spring框架来实现项目中的文件上传功能,则MultipartFile可能是最合适的选择,而这里提到的多种请求方式则可以通俗理解为以表单的形式提交。
第二句:这个文件内容可以存储到内存中或者存储在磁盘的临时位置上。
第三句:无论发生哪种情况,用户都可以自由地拷贝文件内容到session存储中,或者以一种永久存储的形式进行存储,如果有需要的话。
第四句:这种临时性的存储在请求结束之后将会被清除掉。
首先MultipartFile是一个接口,并继承自InputStreamSource,且在InputStreamSource接口中封装了getInputStream方法,该方法的返回类型为InputStream类型,这也就是为什么MultipartFile文件可以转换为输入流。通过以下代码即可将MultipartFile格式的文件转换为输入流。
multipartFile.getInputStream();
方法 | 说明 |
---|---|
String getName() | 返回参数的名称,如(MultipartFile oriFile)则返回为oriFile |
String getOriginalFilename() | 返回客户端文件系统中的原始文件名。这可能包含盘符路径信息,具体取决于所使用的浏览器, 解决方法可参考 link |
String getContentType() | getContentType方法获取的是文件的类型,注意是文件的类型,不是文件的拓展名。 |
boolean isEmpty() | 判断传入的文件是否为空,如果为空则表示没有传入任何文件。 |
long getSize() | 用来获取文件的大小,单位是字节。 |
byte[] getBytes() | 用来将文件转换成一种字节数组的方式进行传输,会抛出IOException异常。 |
InputStream getInputStream() | 返回一个InputStream以从中读取文件的内容。通过此方法就可以获取到流 |
Resource getResource() | |
void transferTo(File dest) | 把接收到的文件写入到目的文件中,如果目的文件已经存在了则会先进行删除。用于将MultipartFile转换为File类型文件 |
void transferTo(Path dest) | 将接收到的文件传输到给定的目标文件。默认实现只复制文件输入流。 |
@GetMapping("/uploadFile") public ApiResult test(@RequestParam MultipartFile uploadFile) throws IOException { // 原文件名称 System.out.println("uploadFile.getOriginalFilename() = " + uploadFile.getOriginalFilename()); // 文件的接收参数 @RequestParam MultipartFile uploadFile中的 uploadFile System.out.println("uploadFile.getName() = " + uploadFile.getName()); // 文件的类型 System.out.println("uploadFile.getContentType() = " + uploadFile.getContentType()); System.out.println("uploadFile.getResource() = " + uploadFile.getResource()); System.out.println("uploadFile.getBytes() = " + uploadFile.getBytes()); // 文件大小 System.out.println("uploadFile.getSize() = " + uploadFile.getSize()); return ApiResult.ok(); }
上传的文件:a.png
执行结果:
uploadFile.getOriginalFilename() = a.png uploadFile.getName() = uploadFile uploadFile.getContentType() = image/jpeg uploadFile.getResource() = MultipartFile resource [file] uploadFile.getBytes() = [B@1fa8cd72 uploadFile.getSize() = 143
springboot默认文件上传大小为1M, 若超过1M会抛出异常,如下:
org.apache.tomcat.util.http.fileupload.FileUploadBase$FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes. at org.apache.tomcat.util.http.fileupload.FileUploadBase$FileItemIteratorImpl$FileItemStreamImpl.raiseError(FileUploadBase.java:628) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.checkLimit(LimitedInputStream.java:76) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.tomcat.util.http.fileupload.util.LimitedInputStream.read(LimitedInputStream.java:135) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at java.io.FilterInputStream.read(Unknown Source) ~[na:1.8.0_131] at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:98) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.tomcat.util.http.fileupload.util.Streams.copy(Streams.java:68) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.tomcat.util.http.fileupload.FileUploadBase.parseRequest(FileUploadBase.java:293) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.connector.Request.parseParts(Request.java:2902) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.connector.Request.parseParameters(Request.java:3242) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.connector.Request.getParameter(Request.java:1136) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.connector.RequestFacade.getParameter(RequestFacade.java:381) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:84) ~[spring-web-4.3.19.RELEASE.jar:4.3.19.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.19.RELEASE.jar:4.3.19.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197) ~[spring-web-4.3.19.RELEASE.jar:4.3.19.RELEASE] at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) ~[spring-web-4.3.19.RELEASE.jar:4.3.19.RELEASE] at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:198) ~[tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:81) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:800) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:806) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1498) [tomcat-embed-core-8.5.34.jar:8.5.34] at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.34.jar:8.5.34] at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [na:1.8.0_131] at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [na:1.8.0_131] at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.34.jar:8.5.34] at java.lang.Thread.run(Unknown Source) [na:1.8.0_131]
application.properties配置文件
# 上传的单个文件的大小最大为3MB spring.servlet.multipart.max-file-size = 3MB # 单次请求的所有文件的总大小最大为9MB spring.servlet.multipart.max-request-size = 9MB # 如果是想要不限制文件上传的大小,那么就把两个值都设置为-1
在配置类中配置MultipartConfigElement方法,并将配置类注册到容器中,代码示例:
@Configuration @SpringBootApplication @MapperScan("com.demo.explain.mapper") public class StoreApplication { public static void main(String[] args) { SpringApplication.run(StoreApplication.class, args); } @Bean public MultipartConfigElement getMultipartConfigElement() { MultipartConfigFactory factory = new MultipartConfigFactory(); // DataSize dataSize = DataSize.ofMegabytes(10); // 设置文件最大10M,DataUnit提供5中类型B,KB,MB,GB,TB factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES)); factory.setMaxRequestSize(DataSize.of(10, DataUnit.MEGABYTES)); // 设置总上传数据总大小10M return factory.createMultipartConfig(); } }
全局异常捕获
package com.example.demo.config; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.multipart.MaxUploadSizeExceededException; // 全局异常捕获类 @ControllerAdvice public class GlobalExceptionHandler { // 若上传的文件大小超过配置类中配置的指定大小后会触发下面的异常 @ExceptionHandler(MaxUploadSizeExceededException.class) public void defultExcepitonHandler(MaxUploadSizeExceededException ex) { log.info("[exceptionHandler] maxUploadSizeExceededExceptionHandler :"+e.getMessage(),e); return ResponseFactory.build(900001,"文件大小超过限制"); } }
Controller层代码如下:
@RestController @RequestMapping("/test") public class MultipartFileController { @PostMapping("/upload") public String multipartFileTest(@ApiParam(value = "multipartFile") @RequestParam MultipartFile multipartFile,@ApiParam(value = "用户名") @RequestParam String userName) throws Exception{ return "成功"; } }
postman传参如下:
请求体里, 首先要选择from-data这种方式;key中的multipartFile与后端接口的变量名保持一致即可,类型选择File。
Controller层代码如下:请求体需要使用@Valid注解而不是@RequestBody
@RestController @RequestMapping("/test") public class MultipartFileController { @PostMapping("/upload") public String multipartFileTest(@ApiParam(value = "multipartFiles") @RequestParam MultipartFile[] multipartFiles,@Valid UserDO userDO) throws Exception{ return "成功"; } }
import lombok.Data; @Data public class UserDO { private String userName; private String email; private int age;
postman传参如下:
2023-02-06 10:00:20.557 ERROR 14780 --- [http-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception java.io.FileNotFoundException: C:\Users\AppData\Local\Temp\work\demo\upload_file.tmp (系统找不到指定的文件。) at java.io.FileInputStream.open0(Native Method) ~[na:1.8.0_322] at java.io.FileInputStream.open(FileInputStream.java:195) ~[na:1.8.0_322] at java.io.FileInputStream.(FileInputStream.java:138) ~[na:1.8.0_322] at org.apache.tomcat.util.http.fileupload.disk.DiskFileItem.getInputStream(DiskFileItem.java:198) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.apache.catalina.core.ApplicationPart.getInputStream(ApplicationPart.java:100) ~[tomcat-embed-core-9.0.65.jar:9.0.65] at org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile.getInputStream(StandardMultipartHttpServletRequest.java:254) ~[spring-web-5.3.22.jar:5.3.22] at com.tanwei.spring.app.controllers.FileController.file(FileController.java:29) ~[classes/:na] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_322] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_322] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_322] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_322]
FileNotFoundException异常就是文件找不到了,也就是说tansferTo()可能在传输完成后把临时文件删除了,这是肯定的,但是答案只能说是对一半,我们将一步一步的进行源码分析
以下基于spring-web.5.2.9.RELEASE源码分析MultipartFile的tansferTo()方法
调用tansferTo()方法,Spring Boot Web默认是调用StandardMultipartHttpServletRequest.StandardMultipartFile.tansferTo()方法,如下所示:
private static class StandardMultipartFile implements MultipartFile, Serializable { //......省略其他内容 @Override public void transferTo(File dest) throws IOException, IllegalStateException { this.part.write(dest.getPath()); if (dest.isAbsolute() && !dest.exists()) { // Servlet 3.0 Part.write is not guaranteed to support absolute file paths: // may translate the given path to a relative location within a temp dir // (e.g. on Jetty whereas Tomcat and Undertow detect absolute paths). // At least we offloaded the file from memory storage; it'll get deleted // from the temp dir eventually in any case. And for our user's purposes, // we can manually copy it to the requested location as a fallback. FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath())); } } @Override public void transferTo(Path dest) throws IOException, IllegalStateException { FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest)); } }
我们主要看一下.tansferTo(File dest)这个方法里的this.part.write(dest.getPath());代码,这里的part实现是ApplicationPart,如下所示:
public class ApplicationPart implements Part { //......省略其他内容 public void write(String fileName) throws IOException { // 构建一个需要存储的文件 File file = new File(fileName); // 判断文件的地址是否是一个绝对路径地址,如C://demo/xxx.txt,返回true // 若文件路径不是绝对路径,将创建一个临时目录,所以这里也是经常会出现问题的地方,在使用multipartFile.tansferTo()的时候最好给绝对路径 if (!file.isAbsolute()) { // 如果不是一个绝对路径地址,则在this.location下创建 // this.location是一个临时文件对象,地址(C:\Users\xxxx\AppData\Local\Temp\tomcat.8090.3830877266980608489\work\Tomcat\localhost) file = new File(this.location, fileName); } try { this.fileItem.write(file); } catch (Exception var4) { throw new IOException(var4); } } }
this.fileItem.write(file);这行代码是主要的核心代码,我们继续跟进去查看一下具体做了什么,如下所示:
public class DiskFileItem implements FileItem { //......省略其他内容 public void write(File file) throws Exception { // 判断文件项是否缓存在内存中的,这里我们没设置,一般都是存在上面的临时磁盘中 if (this.isInMemory()) { FileOutputStream fout = null; try { fout = new FileOutputStream(file); fout.write(this.get()); fout.close(); } finally { IOUtils.closeQuietly(fout); } } else { // 主要看一下这个代码块 // 获取文件项的存储位置,即你上传的文件在磁盘上的临时文件 File outputFile = this.getStoreLocation(); if (outputFile == null) { throw new FileUploadException("Cannot write uploaded file to disk!"); } // 获取文件长度 this.size = outputFile.length(); if (file.exists() && !file.delete()) { throw new FileUploadException("Cannot write uploaded file to disk!"); } // 之所以不能再调用file.getInputStream()方法,原因就是在这 // fileA.renameTo(fileB)方法: // 1) 当fileA文件信息(包含文件名、文件路径)与fileB全部相同时,只是单纯的重命名 // 2) 当fileA文件信息(特别是文件路径)与fileB不一致时,则存在重命名和剪切,这里的剪切就会把临时文件删除,并将文件复制到fileB位置 // 所以,在调用file.getInputStream()时,file获取的还是原始的文件位置,调用transerTo()方法后(其实调用了renameTo()),原始文件已经不存在了 // 故而抛出FileNotFoundException异常 if (!outputFile.renameTo(file)) { BufferedInputStream in = null; BufferedOutputStream out = null; try { in = new BufferedInputStream(new FileInputStream(outputFile)); out = new BufferedOutputStream(new FileOutputStream(file)); IOUtils.copy(in, out); out.close(); } finally { IOUtils.closeQuietly(in); IOUtils.closeQuietly(out); } } } } }
错误代码
@PostMapping("/uploadFile") public String uploadImg(@RequestParam("file") MultipartFile file) { String baseDir = "./imgFile"; // 这里不能直接使用相对路径 if (!file.isEmpty()) { String name = file.getOriginalFilename(); String prefix = name.lastIndexOf(".") != -1 ? name.substring(name.lastIndexOf(".")) : ".jpg"; String path = UUID.randomUUID().toString().replace("-", "") + prefix; try { // 这里代码都是没有问题的 File filePath = new File(baseDir, path); // 第一次执行代码时,路径是不存在的 logger.info("文件保存路径:{},是否存在:{}", filePath.getParentFile().exists(), filePath.getParent()); if (!filePath.getParentFile().exists()) { // 如果存放路径的父目录不存在,就创建它。 filePath.getParentFile().mkdirs(); } // 如果路径不存在,上面的代码会创建路径,此时路径即已经创建好了 logger.info("文件保存路径:{},是否存在:{}", filePath.getParentFile().exists(), filePath.getParent()); // 此处使用相对路径,似乎是一个坑! // 相对路径:filePath // 绝对路径:filePath.getAbsoluteFile() logger.info("文件将要保存的路径:{}", filePath.getPath()); file.transferTo(filePath); logger.info("文件成功保存的路径:{}", filePath.getAbsolutePath()); return "上传成功"; } catch (Exception e) { logger.error(e.getMessage()); } } return "上传失败"; }
一旦执行到file.transferTo(filePath),就会产生一个FileNotFoundException,如下图:
2020-11-27 10:15:06.522 ERROR 5200 --- [nio-8080-exec-1] r.controller.LearnController : java.io.FileNotFoundException: C:\Users\Alfred\AppData\Local\Temp \tomcat.8080.2388870592947355119\work\Tomcat\localhost\ROOT\.\imgFile4918a520684801b658c85a02bf9ba5.jpg (系统找不到指定的路径。)
原理在上面那个问题中已经分析过,void transferTo(File dest)内部若判断文件路径不是绝对路径,则会创建一个临时目录出来
String baseDir = "./imgFile"; File saveFile= new File(baseDir, path); //修改此处传如的参数,改为文件的绝对路径 multipartFile.transferTo(saveFile.getAbsoluteFile());
若MultipartFile对象是在controller层传入,回话结束后MultipartFile文件会自动清理; 若MultipartFile对象是自己创建出来的,则使用完需要自己手动删除文件。
@PostMapping("/upload") public String multipartFileTest(@ApiParam(value = "multipartFile") @RequestParam MultipartFile multipartFile,@ApiParam(value = "用户名") @RequestParam String userName) throws Exception{ //考虑不同浏览器上传文件会带入盘符等路径信息,此处处理一下 String fileName = org.apache.commons.io.FilenameUtils.getName(multipartFile.getOriginalFilename()); File saveFile= new File("/app/home/data/",fileName ); //修改此处传如的参数,改为文件的绝对路径 multipartFile.transferTo(saveFile.getAbsoluteFile()); return "成功"; }
导入依赖包
commons-io commons-io 2.7
File file = new File(path,"demo.txt"); // 得到MultipartFile文件 MultipartFile multipartFile = getFile(); // 把流输出到文件 FileUtils.copyInputStreamToFile(multipartFile.getInputStream(),file);
(1):使用org.springframework.mock.web.MockMultipartFile 需要导入spring-test.jar
该方法需要使用spring-test.jar包,生产环境一般是跳过测试包的,因此可能会导致其他问题,所以尽量不要使用这种方式
public static void main(String[] args) throws Exception { String filePath = "F:\\test.txt"; File file = new File(filePath); FileInputStream fileInputStream = new FileInputStream(file); // MockMultipartFile(String name, @Nullable String originalFilename, @Nullable String contentType, InputStream contentStream) // 其中originalFilename,String contentType 旧名字,类型 可为空 // ContentType.APPLICATION_OCTET_STREAM.toString() 需要使用HttpClient的包 MultipartFile multipartFile = new MockMultipartFile("copy"+file.getName(),file.getName(),ContentType.APPLICATION_OCTET_STREAM.toString(),fileInputStream); System.out.println(multipartFile.getName()); // 输出copytest.txt }
导入依赖包
commons-fileupload commons-fileupload 1.3.1
import org.apache.commons.fileupload.FileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.io.IOUtils; import org.springframework.http.MediaType; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.commons.CommonsMultipartFile; import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.io.OutputStream; public class Test { public static MultipartFile getMultipartFile(File file) { //如果传输有点问题可能传输的类型有点不同,可以试下更改为MediaType.TEXT_PLAIN_VALUE FileItem item = new DiskFileItemFactory().createItem("file" , MediaType.MULTIPART_FORM_DATA_VALUE , true , file.getName()); try (InputStream input = new FileInputStream(file); OutputStream os = item.getOutputStream()) { // 流转移 IOUtils.copy(input, os); } catch (Exception e) { throw new IllegalArgumentException("Invalid file: " + e, e); } return new CommonsMultipartFile(item); } }