文件上传的使用
1. 前端构造一个有文件上传的表单
2. 处理表单请求的controller
/**
* 处理 form-layout 页面的 BASIC FORMS 表单提交
*
* @param email email
* @param userName username
* @param profilePhoto 单文件
* @param dailyPhotos 多文件
*/
@PostMapping("/form-layout-submit")
public String formLayoutUpload(@RequestParam("email") String email,
@RequestParam("username") String userName,
@RequestPart("profile-photo") MultipartFile profilePhoto,
@RequestPart("daily-photos") MultipartFile[] dailyPhotos) {
log.info("email: {} | username: {} | size of profile-photo: {} | count of daily-photos: {}",
email, userName, profilePhoto.getSize(), dailyPhotos.length);
return "form/form_layouts";
}
3. 提交表单后遇到的错误
- 提交表单发现了报错了
org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field profile-photo exceeds its maximum permitted size of 1048576 bytes.
这是因为上传的内容超出了SpringBoot
默认配置的上传文件的大小1MB
- 由于所有的文件上传请求都是经过
MultipartAutoConfiguration
先自动配置了,然后由相应的解析器去处理的,这里点进该自动配置类的源码查看一下相应的修改上传文件大小限制的配置项,并去修改即可
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
通过该注解可以指导,在配置类中修改spring.servlet.multipart
下的配置项即可
可以看到有一个max-file-size
的配置项,默认是"1MB",说明修改该配置项为想要限制的大小即可,这里我改成10MB,而max-request-size
是多文件上传时,总的一次提交的最大大小,默认是10MB,我改成100MB
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
4. 将上传的文件保存到本地
@PostMapping("/form-layout-submit")
public String formLayoutUpload(@RequestParam("email") String email,
@RequestParam("username") String userName,
@RequestPart("profile-photo") MultipartFile profilePhoto,
@RequestPart("daily-photos") MultipartFile[] dailyPhotos) throws IOException {
log.info("email: {} | username: {} | size of profile-photo: {} | count of daily-photos: {}",
email, userName, profilePhoto.getSize(), dailyPhotos.length);
// 保存上传的文件到本地
// 设置保存文件的目录 -- 不存在则创建保存目录
File mediaDir = new File("media");
if (!mediaDir.exists()) {
mediaDir.mkdir();
log.warn("创建上传文件的保存目录" + mediaDir.getName());
}
String mediaPath = mediaDir.getAbsolutePath();
if (!profilePhoto.isEmpty()) {
String originalFilename = profilePhoto.getOriginalFilename();
profilePhoto.transferTo(new File(mediaPath + "/" + originalFilename));
log.info("文件 " + originalFilename + " 已保存");
}
if (dailyPhotos.length > 0) {
for (MultipartFile dailyPhoto : dailyPhotos) {
if (!dailyPhoto.isEmpty()) {
String originalFilename = dailyPhoto.getOriginalFilename();
dailyPhoto.transferTo(new File(mediaPath + "/" + originalFilename));
log.info("文件 " + originalFilename + " 已保存");
}
}
}
return "form/form_layouts";
}
文件上传原理分析
1. 先从doDispatch()开始看起
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
...
}
可以看到,在选择使用哪个解析器去处理请求(也就是根据映射关系,找到请求的url
对应的用@RequestMapping
注解过的方法)之前,会先调用checkMultipart()
检查一下当前的请求是否是一个文件上传的请求
2. 分析checkMultipart()
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}
首先会判断
this.multipartResolver
是否存在,那么这个是怎么来的呢?可以看看文件上传解析器的自动配置类-
文件上传解析器自动配置类
MultipartAutoConfiguration
中的一个方法StandardServletMultipartResolver()
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) @ConditionalOnMissingBean(MultipartResolver.class) public StandardServletMultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; }
该方法用了
@ConditionalOnMissingBean
注解,所以如果我们没有写自定义的文件上传解析器的话,SpringBoot会自动往容器中注入StandardServletMultipartResolver
这样一个标准的文件上传解析器 -
然后判断
this.multipartResolver.isMultipart(request)
这个就是确认当前的请求是否是文件上传请求
isMultipart
@Override public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); }
判断逻辑很简单,就是判断请求的
ContentType
开头是否是multipart/
,由于我们的表单设置的enctype
是multipart/form-data
,所以是一个文件上传的请求 -
用
multipartResolver
去解析文件上传的请求this.multipartResolver.resolveMultipart(request);
resolveMultipart()
@Override public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { return new StandardMultipartHttpServletRequest(request, this.resolveLazily); }
该方法实际上就是将
HttpServletRequest
的request
封装成了StandardMultipartHttpServletRequest
的request
,这样就得到了processedRequest
了,这个request
中有
3. multipartRequestParsed是如何变化的
multipartRequestParsed = (processedRequest != request);
- 由于前面的
checkMultipart()
会返回一个封装过后的request
,所以当返回的request
和原本的request
不相等是,就说明已经被multipartResolver()
解析过了,现在的request
就是一个multipartRequest
了,后面的代码就可以通过multipartRequestParsed
这个布尔值来判断是否要对文件上传请求作出额外的处理
4. 文件字段如何被解析的
使用
@RequestPart
注解的字段是文件上传时的字段名,这个字段是如何被解析的呢?-
查看参数解析器中有什么解析器
RequestParamMethodArgumentResolver
解析被@RequestParam
注解了的参数RequestPartMethodArgumentResolver
解析被@RequestPart
注解了的参数,所以在controller中用了该注解后就能够被识别成是文件上传字段
5. 如何根据注解获取到相应的文件上传字段
前面已经分析了,文件上传字段的注解是@RequestPart
,而相应的解析器是RequestPartMethodArgumentResolver
,所以我们需要先找到该解析器的执行流程先
-
找到该解析器的执行方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
-
一路step into后,可以看到一个这样的方法 --
getMethodArgumentValues()
,该方法会遍历我们controller中的参数for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); args[i] = findProvidedArgument(parameter, providedArgs); if (args[i] != null) { continue; } if (!this.resolvers.supportsParameter(parameter)) { throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); } try { args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); } catch (Exception ex) { // Leave stack trace for later, exception may actually be resolved and handled... if (logger.isDebugEnabled()) { String exMsg = ex.getMessage(); if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) { logger.debug(formatArgumentError(parameter, exMsg)); } } throw ex; } }
这里就是之前在controller中传入的4个参数,由于文件上传字段是数组中的第2和第3个,这里就先让for循环遍历到第2个元素
然后就是找到相应的解析器,并调用解析器的解析方法开始解析
解析方法
@Override @Nullable public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception { HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); Assert.state(servletRequest != null, "No HttpServletRequest"); RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class); boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional()); String name = getPartName(parameter, requestPart); parameter = parameter.nestedIfOptional(); Object arg = null; Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { arg = mpArg; } else { try { HttpInputMessage inputMessage = new RequestPartServletServerHttpRequest(servletRequest, name); arg = readWithMessageConverters(inputMessage, parameter, parameter.getNestedGenericParameterType()); if (binderFactory != null) { WebDataBinder binder = binderFactory.createBinder(request, arg, name); if (arg != null) { validateIfApplicable(binder, parameter); if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) { throw new MethodArgumentNotValidException(parameter, binder.getBindingResult()); } } if (mavContainer != null) { mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); } } } catch (MissingServletRequestPartException | MultipartException ex) { if (isRequired) { throw ex; } } } if (arg == null && isRequired) { if (!MultipartResolutionDelegate.isMultipartRequest(servletRequest)) { throw new MultipartException("Current request is not a multipart request"); } else { throw new MissingServletRequestPartException(name); } } return adaptArgumentIfNecessary(arg, parameter); }
该方法中会获取注解
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
然后根据注解获取注解中的参数名
String name = getPartName(parameter, requestPart);
然后就根据这些信息开始解析参数了
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
resolveMultipartArgument
@Nullable public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) throws Exception { MultipartHttpServletRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); if (MultipartFile.class == parameter.getNestedParameterType()) { if (!isMultipart) { return null; } if (multipartRequest == null) { multipartRequest = new StandardMultipartHttpServletRequest(request); } return multipartRequest.getFile(name); } else if (isMultipartFileCollection(parameter)) { if (!isMultipart) { return null; } if (multipartRequest == null) { multipartRequest = new StandardMultipartHttpServletRequest(request); } List
files = multipartRequest.getFiles(name); return (!files.isEmpty() ? files : null); } else if (isMultipartFileArray(parameter)) { if (!isMultipart) { return null; } if (multipartRequest == null) { multipartRequest = new StandardMultipartHttpServletRequest(request); } List files = multipartRequest.getFiles(name); return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null); } else if (Part.class == parameter.getNestedParameterType()) { if (!isMultipart) { return null; } return request.getPart(name); } else if (isPartCollection(parameter)) { if (!isMultipart) { return null; } List parts = resolvePartList(request, name); return (!parts.isEmpty() ? parts : null); } else if (isPartArray(parameter)) { if (!isMultipart) { return null; } List parts = resolvePartList(request, name); return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null); } else { return UNRESOLVABLE; } } 接下来就会根据参数类型去调用
getFile(name)
或者getFiles(name)
,本质上getFile()
就是等价于getFiles().getFirst()
,所以只用研究getFiles()
即可getFiles()
@Override public List
getFiles(String name) { List multipartFiles = getMultipartFiles().get(name); if (multipartFiles != null) { return multipartFiles; } else { return Collections.emptyList(); } } 执行完毕后,files中的就是上传的文件了
6. 总结
总体原理就是根据注解的类型以及注解中的参数,构造出一个映射,这个映射是以注解@RequestPart
中的name
为key,而上传的文件为value,根据这个映射就可以给相应的参数赋值,这样我们就可以从MultipartFile
对象中调用相应方法对上传的文件做想要的操作了