1. 配置MultipartResolver
用以解析上传文件数据
开发人员或者框架通过某种方式定义bean MultipartResolver
,Spring MVC
需要使用它解析上传文件数据。比如对于一个Spring Boot + Spring MVC
引用,Spring Boot
的自动配置类MultipartAutoConfiguration
会定义该组件:
@Configuration
@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();
}
// MultipartResolver bean 的定义在这里,并且注意 bean 名称使用了
// DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}
2. DispatcherServlet
初始化MultipartResolver
应用启动后,在第一个需要Spring MVC
处理的请求到达Servlet
容器时,Servlet
会初始化DispatcherServlet
用于处理该请求。DispatcherServlet
初始化过程中,会初始化其策略组件MultipartResolver multipartResolver
用于解析上传的文件数据。
/**
* Initialize the MultipartResolver used by this class.
* <p>If no bean is defined with the given name in the BeanFactory for this namespace,
* no multipart handling is provided.
*/
private void initMultipartResolver(ApplicationContext context) {
try {
// 因为开发人员或者框架其他部分已经定义了组件 MultipartResolver,
// 所以这里应该会能加载到一个MultipartResolver bean组件
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.multipartResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// Default is no multipart resolver.
// 如果没有加载到 MultipartResolver bean,怎么办 ?
// 1. 并不报异常,只是日志提醒
// 2. 当前启动的应用不支持文件上传
this.multipartResolver = null;
if (logger.isTraceEnabled()) {
logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
}
}
}
3. DispatcherServlet
请求处理主流程先检测文件上传
// DispatcherServlet 请求处理主流程代码
/**
* Process the actual dispatching to the handler.
* <p>The handler will be obtained by applying the servlet's HandlerMappings in order.
* The HandlerAdapter will be obtained by querying the servlet's installed HandlerAdapters
* to find the first that supports the handler class.
* <p>All HTTP methods are handled by this method. It's up to HandlerAdapters or handlers
* themselves to decide which methods are acceptable.
* @param request current HTTP request
* @param response current HTTP response
* @throws Exception in case of any kind of processing failure
*/
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
以上代码DispatcherServlet#doDispatch
是其请求处理主流程。在其中,请求处理开始部分以下行是文件上传有关的检测 :
// request 是从容器传递过来的请求对象,尚未进行文件上传请求有关的处理和包装
processedRequest = checkMultipart(request);
// 1. 如果当前请求不是文件上传请求,则 processedRequest 和 request 应该是同一个对象,
// 否则,processedRequest 是不同于 request 的另外一个针对文件上传进行了处理和包装的
// MultipartResolver
// 2. 如果当前请求是文件上传请求,上面的检查过程会处理和包装 request 形成另外一个对象 processedRequest
// multipartRequestParsed 是一个 boolean 变量, 记录当前请求是否是一个文件上传请求
multipartRequestParsed = (processedRequest != request);
方法#checkMultipart
的实现如下 :
/**
* Convert the request into a multipart request, and make multipart resolver available.
* <p>If no multipart resolver is set, simply use the existing request.
* @param request current HTTP request
* @return the processed request (multipart wrapper if necessary)
* @see MultipartResolver#resolveMultipart
*/
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
// 如果 this.multipartResolver 被设置,检测当前请求是否是一个文件上传请求
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
// 请求是文件上传请求,但是已经被MultipartFilter等处理为MultipartHttpServletRequest的情形
if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
}
else if (hasMultipartException(request) ) {
// 请求异常属性 javax.servlet.error.exception 为 MultipartException 的情况
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
// 这是一个文件上传请求,尚未被解析处理,现在
// 尝试使用 this.multipartResolver 解析请求
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;
}
4. MultipartResolver
解析文件上传请求数据
这里我们以StandardServletMultipartResolver
为例,看看其文件上传请求数据的解析过程#resolveMultipart
是怎样的:
- 检测请求是否为文件上传请求
// StandardServletMultipartResolver 代码片段
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}
从此可见,检测当前请求是否为文件上传请求的的方法就是检测请求头部Content-Type
,看它是否以multipart/
为前缀,大小写不区分。
StandardServletMultipartResolver
解析文件上传请求数据
// StandardServletMultipartResolver 代码片段
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
这里StandardServletMultipartResolver#resolveMultipart
其实只是将文件上传请求数据的解析工作交给了一个新对象StandardMultipartHttpServletRequest
,并且缺省情况下指示器马上解析:this.resolveLazily=false
。
3. StandardMultipartHttpServletRequest
解析文件上传请求数据
// StandardMultipartHttpServletRequest 代码片段
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
// 解析文件上传请求数据
parseRequest(request);
// 这里,不出异常的话,当前对象的属性 this.multipartFiles就是解析好的上传文件数据了,
// 如果Servlet 容器是 Tomcat,这里其实是一组 MultipartFile, 每个 MultipartFile 对应
// 文件上传数据中的一个 part,并且目前是存在于服务端文件系统中的一个临时文件
}
}
private void parseRequest(HttpServletRequest request) {
try {
// 这里 request 其实是由 Servlet 容器包装的当前请求对象,比如
// 对于 Tomcat, 这里是一个 RequestFacade , 在调用其 #getParameter/#getParts
// 方法时, 如果请求是一个文件上传请求,都会先触发对文件上传数据的分析,
// 比如对于 Tomcat , 其实就是使用 ServletFileUpload 解析上传文件请求数据,
// 然后将每个part写入临时文件 ,使用 DiskFileItem 表示,再进一步包装成
// ApplicationPart 集合,这里返回的 parts 就是已经分析过的 ApplicationPart 集合
Collection<Part> parts = request.getParts();
this.multipartParameterNames = new LinkedHashSet<>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
// 将 parts 包装成 files 并设置到 this.multipartFiles ,就相当于当前方法的
// 解析逻辑完成了, 注意这里 files 是一个多值 Map
for (Part part : parts) {
// 获取part头部信息 : Content-Disposition
// 例子 : form-data; name="file"; filename="students.xlsx"
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
ContentDisposition disposition = ContentDisposition.parse(headerValue);
// 获取该 part 对应的原始文件名,比如 : students.xlsx
String filename = disposition.getFilename();
if (filename != null) {
if (filename.startsWith("=?") && filename.endsWith("?=")) {
filename = MimeDelegate.decode(filename);
}
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
else {
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files);
}
catch (Throwable ex) {
handleParseFailure(ex);
}
}
5. 控制器方法接收文件上传数据
上传文件数据最终必须由某个控制器方法来接收,该逻辑需要由开发人员提供,比如 :
@Controller
public class FileUploadController {
@PostMapping("/")
public String handleFileUpload(@RequestParam("file") MultipartFile file) {
// ...
}
}
这里FileUploadController
是一个开发人员自己提供的控制器类,handleFileUpload
是一个处理POST
文件上传的控制器方法,注意其方法的参数我们使用了类型MultipartFile
,并且指定注解@RequestParam("file")
,这样它就能相应地接收到上传文件数据multi-part
中名称为file
的part
。
这里,你可能会问,使用了注解@RequestParam("file")
怎么就能导致参数MultipartFile file
能正确地接收到文件上传数据了呢 ? 这里关键点在于Spring MVC
用于解析使用注解@RequestParam
的参数的参数解析器RequestParamMethodArgumentResolver
。我们来看其参数解析过程:
@Override
@Nullable
protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request)
throws Exception {
HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null) {
// 对于能获得 HttpServletRequest 格式 servletRequest 的情形
// 这里使用了一个代理解析器MultipartResolutionDelegate,它其实就是从
// StandardMultipartHttpServletRequest 实例的多值Map属性中获取key为name的
// 第一个 StandardMultipartFile (实现了接口 MultipartFile)
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter,
servletRequest);
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
return mpArg;
}
}
// 对于不能获得 HttpServletRequest 格式 servletRequest , 但是能获得 MultipartRequest 格式
// multipartRequest 的情形
Object arg = null;
MultipartRequest multipartRequest = request.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
// 跟上面能获得 HttpServletRequest 格式 servletRequest 的情形类似地,取出名称为 name 的 MultipartFile
List<MultipartFile> files = multipartRequest.getFiles(name);
if (!files.isEmpty()) {
arg = (files.size() == 1 ? files.get(0) : files);
}
}
// 以上手段都不行,尝试使用 getParameterValues
if (arg == null) {
String[] paramValues = request.getParameterValues(name);
if (paramValues != null) {
arg = (paramValues.length == 1 ? paramValues[0] : paramValues);
}
}
return arg;
}
总结
综上所述,Spring MVC
对文件上传的处理有以下要点 :
- 配置
MultipartResolver
用以解析上传文件数据 –StandardServletMultipartResolver bean
的定义 DispatcherServlet
初始化MultipartResolver
–DispatcherServlet#initMultipartResolver
DispatcherServlet
请求处理主流程先检测文件上传 –DispatcherServlet#checkMultipart
MultipartResolver
解析文件上传请求数据 – 缺省使用StandardMultipartHttpServletRequest#parseRequest
- 控制器方法接收文件上传数据 – 由
RequestParamMethodArgumentResolver
解析参数