Spring MVC更多家族成员----文件上传---06
Posted 大忽悠爱忽悠
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring MVC更多家族成员----文件上传---06相关的知识,希望对你有一定的参考价值。
Spring MVC更多家族成员----文件上传---06
本节导读
- 文件上传与MultipartResolver
- Handler与HandlerAdaptor
- 框架内处理流程拦截与HandlerInterceptor
- 框架内的异常处理与HandlerExceptionResolver
- 国际化视图与LocalResolver
- 主题(Theme)与ThemeResolver
在深入讲述Spring MVC框架之前,我们先暂时跳出对框架内主要角色的认知范围,再次“鸟瞰”Spring MVC框架总体上的逻辑结构。
到目前为止,我们主要认识了Spring MVC框架的五大主要角色,它们是HandlerMapping,Controller、ModelAndview、.ViewResolver和View。
在DispatcherServlet处理Web请求的过程中,它们顺序承担了相应的职贵。我想,在之前的内容基础上,我们应该能够对整个Wb请求的处理流程中各个角色所处的位置达成以下共识,如图所示。
它们就好像Spring MVC的“骨架”,有了它们,即使整个框架看起来就像是个“骷髅兵”,在某种程度上已经足俱战斗力。
为了能够让整个Spring MVC框架看起来更加饱满,我们还有一段路程要走。在此之前,我们可以先提前看一下地图,以免迷失方向。
在这幅“地图”(图25-2)中,已经走过的“地点”只有图标,没有文字说明,而我们要经过的新的“地名”则都有标注。
- MultipartResolver。我们将经过的第一站,它位于HandlerMapping之前,简单来说,如果有文件上传的请求,它将会大展身手。
- HandlerInterceptor。HandlerInterceptor将对处理流程进行拦截。拦截的位置可以有三个地方可以选择,我想在“地图”中不难找到这些位置(斜线背景的竖向方框所标志的位置)。
- HandlerAdaptor。实际上,Spring MVC并不只是支持Controller这种Handler类型。HandlerAdaptor可以帮助我们使用其他类型的Handler。
- HandlerExceptionResolver。在处理具体Web请求的过程中,相应的Handler出现异常情况怎么办?HandlerExceptionResolver将为我们提供一种框架内的标准处理方式。
- LocaleResolver。有了LocaleResolver,根据用户的Locale显示不同的视图变得很容易。
- ThemeResolver。用户可以选择不同的主题(Theme)?ThemeResolver正是为这而生的!
下面让我按照顺序,带大家逐一领略“地图”中每一地点的“风土人情”。在按照“地图”的指示完成整个旅程的时候,我们将能够在开发过程中完全驾驭整个Spring MVC框架。
文件上传与MultipartResolver
如果要在基于Spring MVC的Web应用程序中通过表单上传文件,那么MultipartResolver将是在服务器端处理文件上传的主要组件。
html页面中的表单最初所采用的application/x-www-fomm-urlencoded
编码方式,并不足以满足文件上传的需要,所以,RFC1867(htp:www.faqs.org/rfcs/fcl867.html)
在此基础上增加了新的multipart/formdata
编码方式以支持基于表单的文件上传。
通常情况下,按照如下形式声明表单以及表单中的元素:
<form action="/fileUpload" enctype="multipart/form-data">
<input name="file" type="file">
<input type="submit" value="upload">
</form>
客户端浏览器将按照RFC1867所规定的格式,对提交表单内容进行编码,服务器端只需要根据RFC1867规定的格式对请求中的信息进行解码,就可获得客户端表单提交的数据,包括上传的文件。
既然RFC1867所规定的规则是一定的,所以,我们没有必要每次都根据这一规则分析每一请求中的信息。
既然是通用的逻辑,当然也就有通用的类库,比如早期的jsp smart upload
和Oreilly
的COS
类库,以及现在使用最多的Commons FileUpload(http://commons..apache.org//fileupload)
类库。
实际开发中,我们只需要使用这些专门针对基于表单的文件上传处理类库即可。
在实现基于表单的文件上传功能的时候,Spring MVC
框架底层实际上也是使用了以上几种类库。
只不过,通过org.springframework,web.multipart.MultipartResolver
接口的抽象,Spring MVC
将具体选用哪一种类库的权利留给了我们。
使用MultipartResolver进行文件上传的简单分析
MultipartResolver的接口定义如下:
public interface MultipartResolver
boolean isMultipart(HttpServletRequest request);
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
void cleanupMultipart(MultipartHttpServletRequest request);
当Web请求到达DispatcherServlet并等待处理的时候,DispatcherServlet首先会检查能否从自己的WebApplicationContext中找到一个名称为multipartResolver(由DispatcherServlet的常量MULTIPART_RESOLVER_BEANNAME所决定)的MultipartResolver实例。
如果能够获得一个MultipartResolver的实例,DispatcherServlet将通过MultipartResolver的isMultipart(request)
方法检查当前Web请求是否为multipart类型。
如果是,DispatcherServlet将调用MultipartResolver的resolveMultipart(request)方法,并返回一个MultipartHttpServletRequest供后继处理流程使用,否则,直接返回最初的HttpServletRequest。
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);
...
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException
//如果multipartResolver存在,并且当前请求解析后发现是文件上传请求
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request))
//判断当前请求是否已经是MultipartHttpServletRequest了,如果是的话,就不需要进行包装了
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");
//利用multipartResolver解析得到MultipartHttpServletRequest
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;
//非文件上传请求或者当前请求已经被转换为了MultipartHttpServletRequest
return request;
当Web请求类型为multipart的时候,MultipartResolver的resolveMultipart(request)所返回的MultipartHttpServletRequest将被作为后继处理流程所依赖的HttpServletRequest而使用。
也就是说,对应最初请求的HttpServletRequest将被“偷梁换柱”为MultipartHttpServletRequest,此后处理流程各个环节中所使用的HttpServletRequest的具体类型为MultipartHttpServletRequest。
当然,如果MultipartHttpServletRequest不能够提供比HttpServletRequest更多的能力,那么在这里“劳师动众”地使用Decerator模式进行“偷梁换柱”看起来也没太大意义了。
下面让我们看该接口的定义:
public interface MultipartHttpServletRequest extends HttpServletRequest, MultipartRequest
@Nullable
HttpMethod getRequestMethod();
HttpHeaders getRequestHeaders();
@Nullable
HttpHeaders getMultipartHeaders(String paramOrFileName);
public interface MultipartRequest
Iterator<String> getFileNames();
@Nullable
MultipartFile getFile(String name);
List<MultipartFile> getFiles(String name);
Map<String, MultipartFile> getFileMap();
MultiValueMap<String, MultipartFile> getMultiFileMap();
@Nullable
String getMultipartContentType(String paramOrFileName);
MultipartHttpServletRequest的附加能力来自于它的父接口MultipartRequest。
简单地说,我们现在可以在某个Controller中,通过MultipartHttpServletRequest直接获取MultipartFile类所封装的上传后的文件,如下所示:
MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
MultipartFile file = multipartHttpServletRequest.getFile("file");
至于要将MultipartFile中保存的上传文件存入数据库,还是写入文件系统,那就看个人喜好或者应用场景的需要了。
当然,接口永远是接口,具体工作还得有人来做(这话是不是说过好几遍了)。Spring MVC框架内为MultipartResolver提供了两个可用的实现类,即
- org.springframework.web.multipart.commons.CommonsMultipartResolver
- org.springframework.web.multipart.support.StandardServletMultipartResolver
前者使用Commons FileUpload类库实现,后者则使用Oreilly Cos类库实现。要启用Spring MVC框架内的文件上传支持,本质上讲,就是选择这两个实现类中的哪一个,然后将最终的选择添加到DispatcherServlet的WebApplicationContext。
如果我们使用StandardServletMultipartResolver 进行文件上传,那么需要在DispatcherServletl的WebApplicationContext中添加如下bean定义:
<bean id="multipartResolver" class="org.springframework.web.multipart.support.StandardServletMultipartResolver"/>
为什么在WebApplicationContext中添加了beanName为multipartResolver的bean,spring mvc就可以识别到呢?
private void initMultipartResolver(ApplicationContext context)
try
//MULTIPART_RESOLVER_BEAN_NAME=multipartResolver
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
...
catch (NoSuchBeanDefinitionException ex)
// Default is no multipart resolver.
this.multipartResolver = null;
....
现在,CommonsMultipartResolver(或者CosMultipartResolver)将负责分析当前multipart请求,然后将分析后的结果附着到要返回的MultipartHttpServletRequest实例(DefaultMultipartHttpServletRequest或者StandardMultipartHttpServletRequest)上。
当后继处理流程的Controller处理Web请求的时候,就可以使用特定的MultipartHttpServletRequest进行上传
文件的获取和处理。
当然,每个MultipartResolver都会有附加的属性定义以限定文件上传的行为。可以参阅Javadoc文档获得详细信息。
当MultipartResolver返回MultipartHttpServletRequest给后继处理流程,并且后继处理流程中的组件(通常是相应的Controller.)也使用MultipartHttpServletRequest处理完相应的Web请求,DispatcherServlet将保证调用MultipartResolver的cleanupMultipart(…)方法,释放处理文件上传过程中所占用的系统资源。
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);
...
...
finally
...
if (multipartRequestParsed)
cleanupMultipart(processedRequest);
这样,整个文件上传的生命周期即告结束。
StandardServletMultipartResolver概览
该类源码很简单,如下所示:
public class StandardServletMultipartResolver implements MultipartResolver
private boolean resolveLazily = false;
private boolean strictServletCompliance = false;
//...setter方法
@Override
public boolean isMultipart(HttpServletRequest request)
//判断请求类型是否是multipart/form-data
return StringUtils.startsWithIgnoreCase(request.getContentType(),
(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
//直接生成一个StandardMultipartHttpServletRequest,然后返回
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
@Override
public void cleanupMultipart(MultipartHttpServletRequest request)
if (!(request instanceof AbstractMultipartHttpServletRequest) ||
((AbstractMultipartHttpServletRequest) request).isResolved())
try
//Part是servlet规范中对上传得到的文件进行封装的对象
//不同的servlet容器实现,例如tomcat,会给出具体的实现类,然后再解析到对应的文件上传请求后
//封装为一个Part对象,放入当前请求的request对象中
for (Part part : request.getParts())
if (request.getFile(part.getName()) != null)
//删除掉生成在磁盘上的临时文件
part.delete();
catch (Throwable ex)
LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
注意
为了使用基于 Servlet 3.0 的文件解析,您需要在 web.xml 中使用“multipart-config”
配置或在servlet注册中使用 javax.servlet.MultipartConfigElement
标记受影响的 servlet
,或者在对应的servlet 类上带有 javax.servlet.annotation.MultipartConfig
注解。需要在该 servlet 注册级别应用最大大小或存储位置等配置设置; Servlet 3.0 不允许在 MultipartResolver 级别设置它们。
servlet 3.0规范指出,必要要在处理文件上传请求前,设置好相关maxFileSize,maxRequestSize,fileSizeThreshold等参数,否则就会抛出对应的异常。
设置对应的参数有下面三种方式:
- 通过在web.xml中配置—针对的是单个servlet级别
<servlet>
<servlet-name>controller</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>2</load-on-startup>
<multipart-config>
<max-file-size>20848820</max-file-size>
<max-request-size>418018841</max-request-size>
<file-size-threshold>1048576</file-size-threshold>
</multipart-config>
</servlet>
<servlet-mapping>
<servlet-name>controller</servlet-name>
<!-- 拦截除*.jsp以外的所有请求 -->
<url-pattern>/</url-pattern>
</servlet-mapping>
- 通过在对应的servlet标注相关注解,在添加servlet的时候,会进行解析
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MultipartConfig
String location() default "";
long maxFileSize() default -1L;
long maxRequestSize() default -1L;
int fileSizeThreshold() default 0;
- MultipartConfigElement
每个StandardWrapper内部都有一个MultipartConfigElement,而一个StandardWrapper对应包装一个Servlet,可以认为一个Servlet对应一个MultipartConfigElement。
MultipartConfigElement 是 javax.servlet 包中的是 JavaEE Servlet 规范定义的标准包。
该类定义了Http服务上传文件存储位置、最大文件大小、最大请求的长度
public class MultipartConfigElement
private final String location;// = "";
private final long maxFileSize;// = -1;
private final long maxRequestSize;// = -1;
private final int fileSizeThreshold;// = 0;
//对应web.xml配置解析得到当前servlet的配置信息
public MultipartConfigElement(String location)
if (location != null)
this.location = location;
else
this.location = "";
this.maxFileSize = -1;
this.maxRequestSize = -1;
this.fileSizeThreshold = 0;
//对应web.xml配置解析得到当前servlet的配置信息
public MultipartConfigElement(String location, long maxFileSize,
long maxRequestSize, int fileSizeThreshold)
if (location != null)
this.location = location;
else
this.location = "";
this.maxFileSize = maxFileSize;
this.maxRequestSize = maxRequestSize;
if (fileSizeThreshold > 0)
this.fileSizeThreshold = fileSizeThreshold;
else
this.fileSizeThreshold = 0;
//对应注解解析过程中得到的配置信息
public MultipartConfigElement(MultipartConfig annotation)
location = annotation.location();
maxFileSize = annotation.maxFileSize();
maxRequestSize = annotation.maxRequestSize();
fileSizeThreshold = annotation.fileSizeThreshold();
//...getter方法
servlet上MultipartConfig注解被解析的时机,是在StandardWrapper的loadServlet方法中,而web.xml的解析是早于MultipartConfig 注解的解析,因此如果都进行了配置,那么后者覆盖前者。
以Springboot Tomcat为例,Tomcat 的 Request 请求会被传递到 SpringMVC 的 DispatcherServlet 中,在 doDispartch() 方法会 Request 的 getParts() 方法被调用, 会去获取 MultipartConfigElement 对象,从中获取存储位置将上传的文件存储在临时存储位置,这些文件以List< Part >的形式存在于 StandardMuiltpartHttpSevrletRequest 中返回给 DispatcherServlet 继续处理。在整个请求处理完成后,DispatcherServlet 会调用 StandardServletMultipartResolver 的 cleanupMultipart() 的方法,清理掉所有的缓存文件。
StandardMultipartHttpServletRequest概览
StandardMultipartHttpServletRequest最重要的方法就是parseRequest:
private void parseRequest(HttpServletRequest request)
try
//拿到当前请求内部封装好的part对象集合
Collection<Part> parts = request.getParts();
this.multipartParameterNames = new LinkedHashSet<>(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
//挨个处理
for (Part part : parts)
//解析获得上传的文件的fileName
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
ContentDisposition disposition = ContentDisposition.parse(headerValue);
String filename = disposition.getFilename();
if (filename != null)
if (filename.startsWith("=?") && filename.endsWith("?="Spring MVC更多家族成员---框架内处理流程拦截与HandlerInterceptor---08
Spring MVC更多家族成员----Handler与HandlerAdaptor---07
Spring MVC更多家族成员--国际化视图与LocalResolver---10