一、使用
基本配置步骤:
- 构建文件上传表单,表单必须是
post
提交,编码的方式是multipart/form-data
- Controller方法参数类型是
MultipartFile
,注解是@RequestPart
1、构建文件上传表单
<form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">名字</label>
<input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="headerImg" id="exampleInputFile">
<p class="help-block">Example block-level help text here.</p>
</div>
<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photo" multiple>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
2、文件上传代码
//表单提交必须用post请求
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg, // MultipartFile:用于上传文件功能 @RequestPart:获取表单里的文件项
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){ //isEmpty,getOriginalFilename,transferTo:都是MultipartFile接口里的方法
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("D:\\cache\\"+originalFilename));//transferTo:文件保存(传输)到哪
}
if(photos.length>0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("D:\\cache\\"+originalFilename));
}
}
}
return "main";
}
}
如果上传出现了超出限制的大小异常
org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field profile-photo exceeds its maximum permitted size of 1048576 bytes.
可以修改底层默认限制文件的大小:
spring.servlet.multipart.max-file-size=10 #单个文件最大的大小
#多文件上传时,总的一次提交的最大大小,默认是10MB,改成100
spring.servlet.multipart.max-request-size=100
二、原理
文件上传自动配置类是MultipartAutoConfiguration,所有有关文件上传的配置都封装在MultipartProperties里
springboot已经自动配置好了StandardServletMultipartResolver【文件上传解析器】它只能解析标准的以servlet的方式,相当于以servlet协议上传过来的文件。如果是自定义方式,直接往上传流的方式应该写自定义的文件上传解析器
@Configuration( proxyBeanMethods = false ) @ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class}) @ConditionalOnProperty( prefix = "spring.servlet.multipart", name = {"enabled"}, matchIfMissing = true ) @ConditionalOnWebApplication( type = Type.SERVLET ) @EnableConfigurationProperties({MultipartProperties.class}) public class MultipartAutoConfiguration { private final MultipartProperties multipartProperties; public MultipartAutoConfiguration(MultipartProperties multipartProperties) { this.multipartProperties = multipartProperties; } @Bean @ConditionalOnMissingBean({MultipartConfigElement.class, CommonsMultipartResolver.class}) public MultipartConfigElement multipartConfigElement() { return this.multipartProperties.createMultipartConfig(); } @Bean( name = {"multipartResolver"} ) @ConditionalOnMissingBean({MultipartResolver.class}) public StandardServletMultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; } }
原理步骤
一、请求进来使用文件上传解析器判断(用isMultipart方法判断)并封装(用resolveMultipart方法封装,返回MultipartHttpServletRequest类型)文件上传请求
- 1、请求被DispatcherServlet的doDispatch()拦截
- 在选择使用哪个解析器去处理请求之前,会先调用checkMultipart()检查当前的请求是否是一个文件上传的请求
- 2、checkMultipart()逻辑:
判断multipartResolver文件上传解析器是否在容器中存在并且判断当前请求是否是文件上传请求
文件上传解析器是否在容器中就看MultipartAutoConfiguration有没有给我们把文件上传解析器放到容器中
- 可以看到MultipartAutoConfiguration类里的multipartResolver()方法用了@ConditionalOnMissingBean注解,所以如果我们没有写自定义的文件上传解析器的话,SpringBoot会自动往容器中注入StandardServletMultipartResolver文件上传解析器。
判断当前请求是否是文件上传请求:
public boolean isMultipart(HttpServletRequest request) { //判断请求的ContentType是否是multipart/开头,由于我们的表单设置enctype="multipart/form-data",所以这是一个文件上传的请求 return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/"); }
3、是一个文件上传请求的话,就用容器中的multipartResolver文件上传解析器解析请求
return this.multipartResolver.resolveMultipart(request);
并把原生的request封装成StandardMultipartHttpServletRequest类,然后所有的文件上传请求最终会被返回一个叫MultipartHttpServletRequest对象
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { return new StandardMultipartHttpServletRequest(request, this.resolveLazily); }
因为前面的checkMultipart()会返回一个包装后的request请求,所以包装后的request请求和原生的request不相等,那么它就是一个文件上传请求,那么multipartRequestParsed的属性值也就变成了true
二、判断文件上传请求是用哪个参数解析器并最终把参数值确定的
- 1、找到这个参数解析器
- 来到DispatcherServlet的handler方法并进入
- 再进入这个方法:
再进入这个方法:
再进入这个方法:
再进入这个方法:
再进入这个方法:
再进入:- 判断当前解析器支不支持解析这个参数的逻辑:遍历所有的参数解析器
- 进入resolveArgument方法
再进入getArgumentResolver获取参数解析器方法: - 最终找到了这个参数解析器(如果参数前面标注了@RequestPart注解,就会用这个参数解析器解析参数)
- 2、解析请求中的文件内容并封装成MultipartFile
拿到参数解析器后,就看他如何解析(RequestPartMethodArgumentResolver)
- 进入参数解析流程:
进入resolveMultipartArgument方法:
进入getFiles方法
再进入getMultipartFiles方法
这个方法将request中的文件信息封装为一个Map并返回;MultiValueMap<String, MultipartFile>
因为参数的位置是这么写的,所以相当于将headerImg,photos全部封装到map中,想要获取headerImg里的数据可以从map里拿出来,photo也是。
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos)
最终是通过文件上传解析器把所有的文件信息获取到。
总结:
1、请求被DispatcherServlet的doDispatch()拦截,
2、先调用checkMultipart()检查当前的请求是否是一个文件上传的请求,文件上传解析器是否在容器中存在
用isMultipart方法判断是否是一个文件上传的请求(请求的ContentType是否是multipart/ 开头)
判断multipartResolver文件上传解析器是否在容器中存在(如果没有写自定义的文件上传解析器的话,就会给我们把文件上传解析器放到容器中)
3、是一个文件上传请求的话,就用容器中的multipartResolver.resolveMultipart()文件上传解析器解析请求,最后把原生的request封装(包装)成MultipartHttpServletRequest类返回
4、遍历所有的参数解析器,最终找到RequestPartMethodArgumentResolver参数解析器,调用这个参数的解析参数方法解析文件内容并把文件信息封装为一个Map并返回
MultipartFile接口中还有另外非常强大的方法
public interface MultipartFile extends InputStreamSource {
//获取表单中的属性名
String getName();
//获取原始的文件名
@Nullable
String getOriginalFilename();
//获取内容的类型(multipart/form-data)
@Nullable
String getContentType();
//判断当前文件是否为空
boolean isEmpty();
//当前大小
long getSize();
//获取字节流
byte[] getBytes() throws IOException;
//获取输入流
InputStream getInputStream() throws IOException;
//获取文件资源的路径信息
default Resource getResource() {
return new MultipartFileResource(this);
}
//把文件保存到哪里
void transferTo(File var1) throws IOException, IllegalStateException;
default void transferTo(Path dest) throws IOException, IllegalStateException {
//FileCopyUtils文件复制工具类
FileCopyUtils.copy(this.getInputStream(), Files.newOutputStream(dest));
}
}
----------------FileCopyUtils------------------------------
//把文件复制到哪里:实现文件流的拷贝
public static int copy(File in, File out) throws IOException {
Assert.notNull(in, "No input File specified");
Assert.notNull(out, "No output File specified");
return copy(Files.newInputStream(in.toPath()), Files.newOutputStream(out.toPath()));
}
分析原理从两处着手,一是这个功能springboot有没有为他做自动配置、自动配置了哪些,二是调试源码看这个功能是怎么实现的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。