文件上傳的使用
1. 前端構(gòu)造一個(gè)有文件上傳的表單
<form role="form" th:action="@{/form-layout-submit}" enctype="multipart/form-data" method="post">
<div class="form-group">
<label for="exampleInputEmail1">Email address</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1"
placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">Username</label>
<input type="text" name="username" class="form-control" id="exampleInputPassword1"
placeholder="Username">
</div>
<!--單文件上傳-->
<div class="form-group">
<label for="exampleInputFile">Profile Photo</label>
<input type="file" name="profile-photo" id="exampleInputFile">
<p class="help-block">Upload your profile photo here</p>
</div>
<!--多文件上傳-->
<div class="form-group">
<label for="exampleInputFile">Daily Photos</label>
<input type="file" name="daily-photos" multiple>
<p class="help-block">Upload your daily photos here</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
2. 處理表單請(qǐng)求的controller
/**
* 處理 form-layout 頁(yè)面的 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. 提交表單后遇到的錯(cuò)誤
- 提交表單發(fā)現(xiàn)了報(bào)錯(cuò)了
org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field profile-photo exceeds its maximum permitted size of 1048576 bytes.
這是因?yàn)樯蟼鞯膬?nèi)容超出了SpringBoot
默認(rèn)配置的上傳文件的大小1MB
- 由于所有的文件上傳請(qǐng)求都是經(jīng)過(guò)
MultipartAutoConfiguration
先自動(dòng)配置了牡属,然后由相應(yīng)的解析器去處理的,這里點(diǎn)進(jìn)該自動(dòng)配置類(lèi)的源碼查看一下相應(yīng)的修改上傳文件大小限制的配置項(xiàng)怀薛,并去修改即可
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
通過(guò)該注解可以指導(dǎo)莫杈,在配置類(lèi)中修改spring.servlet.multipart
下的配置項(xiàng)即可
可以看到有一個(gè)max-file-size
的配置項(xiàng),默認(rèn)是"1MB"钓辆,說(shuō)明修改該配置項(xiàng)為想要限制的大小即可弃衍,這里我改成10MB盲厌,而max-request-size
是多文件上傳時(shí),總的一次提交的最大大小臭脓,默認(rèn)是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);
// 保存上傳的文件到本地
// 設(shè)置保存文件的目錄 -- 不存在則創(chuàng)建保存目錄
File mediaDir = new File("media");
if (!mediaDir.exists()) {
mediaDir.mkdir();
log.warn("創(chuàng)建上傳文件的保存目錄" + 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()開(kāi)始看起
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
...
}
可以看到,在選擇使用哪個(gè)解析器去處理請(qǐng)求(也就是根據(jù)映射關(guān)系来累,找到請(qǐng)求的url
對(duì)應(yīng)的用@RequestMapping
注解過(guò)的方法)之前砚作,會(huì)先調(diào)用checkMultipart()
檢查一下當(dāng)前的請(qǐng)求是否是一個(gè)文件上傳的請(qǐng)求
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;
}
首先會(huì)判斷
this.multipartResolver
是否存在,那么這個(gè)是怎么來(lái)的呢嘹锁?可以看看文件上傳解析器的自動(dòng)配置類(lèi)-
文件上傳解析器自動(dòng)配置類(lèi)
MultipartAutoConfiguration
中的一個(gè)方法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
注解葫录,所以如果我們沒(méi)有寫(xiě)自定義的文件上傳解析器的話,SpringBoot會(huì)自動(dòng)往容器中注入StandardServletMultipartResolver
這樣一個(gè)標(biāo)準(zhǔn)的文件上傳解析器 -
然后判斷
this.multipartResolver.isMultipart(request)
這個(gè)就是確認(rèn)當(dāng)前的請(qǐng)求是否是文件上傳請(qǐng)求
isMultipart
@Override public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); }
判斷邏輯很簡(jiǎn)單领猾,就是判斷請(qǐng)求的
ContentType
開(kāi)頭是否是multipart/
米同,由于我們的表單設(shè)置的enctype
是multipart/form-data
,所以是一個(gè)文件上傳的請(qǐng)求 -
用
multipartResolver
去解析文件上傳的請(qǐng)求this.multipartResolver.resolveMultipart(request);
resolveMultipart()
@Override public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { return new StandardMultipartHttpServletRequest(request, this.resolveLazily); }
該方法實(shí)際上就是將
HttpServletRequest
的request
封裝成了StandardMultipartHttpServletRequest
的request
摔竿,這樣就得到了processedRequest
了面粮,這個(gè)request
中有
3. multipartRequestParsed是如何變化的
multipartRequestParsed = (processedRequest != request);
- 由于前面的
checkMultipart()
會(huì)返回一個(gè)封裝過(guò)后的request
,所以當(dāng)返回的request
和原本的request
不相等是继低,就說(shuō)明已經(jīng)被multipartResolver()
解析過(guò)了熬苍,現(xiàn)在的request
就是一個(gè)multipartRequest
了,后面的代碼就可以通過(guò)multipartRequestParsed
這個(gè)布爾值來(lái)判斷是否要對(duì)文件上傳請(qǐng)求作出額外的處理
4. 文件字段如何被解析的
使用
@RequestPart
注解的字段是文件上傳時(shí)的字段名袁翁,這個(gè)字段是如何被解析的呢柴底?-
查看參數(shù)解析器中有什么解析器
參數(shù)解析器中的解析器RequestParamMethodArgumentResolver
解析被@RequestParam
注解了的參數(shù)RequestPartMethodArgumentResolver
解析被@RequestPart
注解了的參數(shù),所以在controller中用了該注解后就能夠被識(shí)別成是文件上傳字段
5. 如何根據(jù)注解獲取到相應(yīng)的文件上傳字段
前面已經(jīng)分析了粱胜,文件上傳字段的注解是@RequestPart
似枕,而相應(yīng)的解析器是RequestPartMethodArgumentResolver
,所以我們需要先找到該解析器的執(zhí)行流程先
-
找到該解析器的執(zhí)行方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
-
一路step into后年柠,可以看到一個(gè)這樣的方法 --
getMethodArgumentValues()
,該方法會(huì)遍歷我們controller中的參數(shù)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中的參數(shù)這里就是之前在controller中傳入的4個(gè)參數(shù)褪迟,由于文件上傳字段是數(shù)組中的第2和第3個(gè)冗恨,這里就先讓for循環(huán)遍歷到第2個(gè)元素
遍歷到文件上傳參數(shù)然后就是找到相應(yīng)的解析器,并調(diào)用解析器的解析方法開(kāi)始解析
解析方法
@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); }
該方法中會(huì)獲取注解
RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
然后根據(jù)注解獲取注解中的參數(shù)名
String name = getPartName(parameter, requestPart);
然后就根據(jù)這些信息開(kāi)始解析參數(shù)了
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<MultipartFile> 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<MultipartFile> 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<Part> parts = resolvePartList(request, name); return (!parts.isEmpty() ? parts : null); } else if (isPartArray(parameter)) { if (!isMultipart) { return null; } List<Part> parts = resolvePartList(request, name); return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null); } else { return UNRESOLVABLE; } }
接下來(lái)就會(huì)根據(jù)參數(shù)類(lèi)型去調(diào)用
getFile(name)
或者getFiles(name)
味赃,本質(zhì)上getFile()
就是等價(jià)于getFiles().getFirst()
掀抹,所以只用研究getFiles()
即可getFiles()
@Override public List<MultipartFile> getFiles(String name) { List<MultipartFile> multipartFiles = getMultipartFiles().get(name); if (multipartFiles != null) { return multipartFiles; } else { return Collections.emptyList(); } }
執(zhí)行完畢后,files中的就是上傳的文件了
files中的內(nèi)容
6. 總結(jié)
總體原理就是根據(jù)注解的類(lèi)型以及注解中的參數(shù)心俗,構(gòu)造出一個(gè)映射傲武,這個(gè)映射是以注解@RequestPart
中的name
為key蓉驹,而上傳的文件為value,根據(jù)這個(gè)映射就可以給相應(yīng)的參數(shù)賦值揪利,這樣我們就可以從MultipartFile
對(duì)象中調(diào)用相應(yīng)方法對(duì)上傳的文件做想要的操作了