Spring MVC更多家族成员----文件上传---06

  • 本节导读
    • 文件上传与MultipartResolver
      • 使用MultipartResolver进行文件上传的简单分析
      • StandardServletMultipartResolver概览
        • 注意
      • StandardMultipartHttpServletRequest概览
      • StandardMultipartFile概览
    • CommonsMultipartResolver
    • 文件上传实战
      • 注意
      • 优化
      • 再优化

本节导读

  • 文件上传与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 uploadOreillyCOS类库,以及现在使用最多的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解析得到MultipartHttpServletRequestelse {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;}}}}//非文件上传请求或者当前请求已经被转换为了MultipartHttpServletRequestreturn request;}

当Web请求类型为multipart的时候,MultipartResolver的resolveMultipart(request)所返回的MultipartHttpServletRequest将被作为后继处理流程所依赖的HttpServletRequest而使用。

也就是说,对应最初请求的HttpServletRequest将被“偷梁换柱”为MultipartHttpServletRequest,此后处理流程各个环节中所使用的HttpServletRequest的具体类型为MultipartHttpServletRequest。

当然,如果MultipartHttpServletRequest不能够提供比HttpServletRequest更多的能力,那么在这里“劳师动众”地使用Decerator模式进行“偷梁换柱”看起来也没太大意义了。

下面让我们看该接口的定义:

public interface MultipartHttpServletRequest extends HttpServletRequest, MultipartRequest {@NullableHttpMethod getRequestMethod();HttpHeaders getRequestHeaders();@NullableHttpHeaders getMultipartHeaders(String paramOrFileName);
}
public interface MultipartRequest {Iterator<String> getFileNames();@NullableMultipartFile getFile(String name);List<MultipartFile> getFiles(String name);Map<String, MultipartFile> getFileMap();MultiValueMap<String, MultipartFile> getMultiFileMap();@NullableString 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=multipartResolverthis.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方法@Overridepublic boolean isMultipart(HttpServletRequest request) {//判断请求类型是否是multipart/form-datareturn StringUtils.startsWithIgnoreCase(request.getContentType(),(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));}//直接生成一个StandardMultipartHttpServletRequest,然后返回@Overridepublic MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {return new StandardMultipartHttpServletRequest(request, this.resolveLazily);}@Overridepublic 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) {//解析获得上传的文件的fileNameString headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);ContentDisposition disposition = ContentDisposition.parse(headerValue);String filename = disposition.getFilename();if (filename != null) {if (filename.startsWith("=?") && filename.endsWith("?=")) {filename = MimeDelegate.decode(filename);}//加入files集合,key就是文件名,value就是封装part对象的StandardMultipartFilefiles.add(part.getName(), new StandardMultipartFile(part, filename));}else {this.multipartParameterNames.add(part.getName());}}//设置到当前类内部的private MultiValueMap<String, MultipartFile> multipartFiles;//属性中去setMultipartFiles(files);}catch (Throwable ex) {//处理解析错误handleParseFailure(ex);}}

如果底层使用tomcat作为服务器,那么getParts实际调用的是Request的parseParts方法:

parseParts方法源码如下:

private void parseParts(boolean explicit) {...Context context = getContext();//拿到当前servlet关联的MultipartConfigElement MultipartConfigElement mce = getWrapper().getMultipartConfigElement();//如果我们没有在web.xml中配置MultipartConfig或者使用注解配置的话//这里就无法获取到if (mce == null) {//默认是不开启的if(context.getAllowCasualMultipartParsing()) {//使用最大值作为相关属性的参数mce = new MultipartConfigElement(null, connector.getMaxPostSize(),connector.getMaxPostSize(), connector.getMaxPostSize());} else {//getParts方法调用的时候传入的是true,因此当我们不进行配置时if (explicit) {//就会抛出下面这个异常,该异常对应的中文翻译为: 由于没有提供multi-part配置,无法处理partspartsParseException = new IllegalStateException(//这里使用的是tomcat内部的错误翻译机制StringManagersm.getString("coyoteRequest.noMultipartConfig"));return;} else {parts = Collections.emptyList();return;}}}Parameters parameters = coyoteRequest.getParameters();parameters.setLimit(getConnector().getMaxParameterCount());boolean success = false;try {File location;//拿到上传文件要推送到那个路径上String locationStr = mce.getLocation();if (locationStr == null || locationStr.length() == 0) {//我们一般不会设置,那么就推送到默认的临时文件位置处location = ((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR));} else {//如果我们指定了路径,还要判断是不是相对路径//如果是相对路径,那么该相对路径对应的父路径为默认临时文件位置开始// If relative, it is relative to TEMPDIRlocation = new File(locationStr);if (!location.isAbsolute()) {location = new File((File) context.getServletContext().getAttribute(ServletContext.TEMPDIR),locationStr).getAbsoluteFile();}}//如果相关路径文件不存在,会去创建对应目录---CreateUploadTargets默认为falseif (!location.exists() && context.getCreateUploadTargets()) {log.warn(sm.getString("coyoteRequest.uploadCreate",location.getAbsolutePath(), getMappingData().wrapper.getName()));if (!location.mkdirs()) {log.warn(sm.getString("coyoteRequest.uploadCreateFail",location.getAbsolutePath()));}}//如果目标路径是指向目录的,那么抛出异常if (!location.isDirectory()) {parameters.setParseFailedReason(FailReason.MULTIPART_CONFIG_INVALID);partsParseException = new IOException(sm.getString("coyoteRequest.uploadLocationInvalid",location));return;}// Create a new file upload handler//tomcat是先把上传的文件读入到临时文件中//因此这里工厂生产出来的DiskFileItem就是封装了读取到磁盘上的文件的相关信息DiskFileItemFactory factory = new DiskFileItemFactory();try {//设置文件路径factory.setRepository(location.getCanonicalFile());} catch (IOException ioe) {parameters.setParseFailedReason(FailReason.IO_ERROR);partsParseException = ioe;return;}//设置文件写入磁盘的大小阈值factory.setSizeThreshold(mce.getFileSizeThreshold());//进一步封装关于获取上传的文件ServletFileUpload upload = new ServletFileUpload();//设置上传文件对应的FileItem关联的工厂upload.setFileItemFactory(factory);//max-file-size: 指定上传文件允许的最大大小。 //max-request-size: 指定multipart/form-data请求允许的最大大小。//PS: 其中max-file-size指定的大小是单文件上传的大小限制,而max-request-size是一次请求的多个文件大小限制。upload.setFileSizeMax(mce.getMaxFileSize());upload.setSizeMax(mce.getMaxRequestSize());parts = new ArrayList<>();try {//解析拿到当前请求关联的FileItemsList<FileItem> items =upload.parseRequest(new ServletRequestContext(this));int maxPostSize = getConnector().getMaxPostSize();int postSize = 0;Charset charset = getCharset();//挨个遍历处理for (FileItem item : items) {//封装为一个partApplicationPart part = new ApplicationPart(item, location);//加入内部维护的parts集合中去parts.add(part);//multipart/form-data方式进行表单提交,可以在表单中提交文件的同时,提交其他文本属性if (part.getSubmittedFileName() == null) {//如果当前处理的是文本属性,那么判断是否超过了默认提交的最大限制String name = part.getName();//计算是否超过了默认最大提交限制if (maxPostSize >= 0) {// Have to calculate equivalent size. Not completely// accurate but close enough.postSize += name.getBytes(charset).length;// Equals signpostSize++;// Value length//文件大小postSize += part.getSize();// Value separatorpostSize++;if (postSize > maxPostSize) {parameters.setParseFailedReason(FailReason.POST_TOO_LARGE);throw new IllegalStateException(sm.getString("coyoteRequest.maxPostSizeExceeded"));}}String value = null;try {value = part.getString(charset.name());} catch (UnsupportedEncodingException uee) {// Not possible}//将当前文本属性加paramters集合,这样开发人员可以通过request.getParam直接取得对应的属性parameters.addParameter(name, value);}}success = true;}//。。。catch一堆} finally {// This might look odd but is correct. setParseFailedReason() only// sets the failure reason if none is currently set. This code could// be more efficient but it is written this way to be robust with// respect to changes in the remainder of the method.if (partsParseException != null || !success) {parameters.setParseFailedReason(FailReason.UNKNOWN);}}}

还有常用的就是我们平时获取上传的文件,使用的getFile方法:

 @Overridepublic MultipartFile getFile(String name) {return getMultipartFiles().getFirst(name);}@Overridepublic List<MultipartFile> getFiles(String name) {List<MultipartFile> multipartFiles = getMultipartFiles().get(name);if (multipartFiles != null) {return multipartFiles;}else {return Collections.emptyList();}}

就是从上面解析好的multipartFiles集合中查找即可,本质还是查找map集合。


StandardMultipartFile概览

StandardMultipartFile是StandardMultipartHttpServletRequest的内部类,主要是对Part使用的包装:

 private static class StandardMultipartFile implements MultipartFile, Serializable {private final Part part;private final String filename;public StandardMultipartFile(Part part, String filename) {this.part = part;this.filename = filename;}@Overridepublic String getName() {return this.part.getName();}@Overridepublic String getOriginalFilename() {return this.filename;}@Overridepublic String getContentType() {return this.part.getContentType();}@Overridepublic boolean isEmpty() {return (this.part.getSize() == 0);}@Overridepublic long getSize() {return this.part.getSize();}@Overridepublic byte[] getBytes() throws IOException {return FileCopyUtils.copyToByteArray(this.part.getInputStream());}@Overridepublic InputStream getInputStream() throws IOException {return this.part.getInputStream();}@Overridepublic void transferTo(File dest) throws IOException, IllegalStateException {this.part.write(dest.getPath());if (dest.isAbsolute() && !dest.exists()) {// Servlet 3.0 Part.write is not guaranteed to support absolute file paths:// may translate the given path to a relative location within a temp dir// (e.g. on Jetty whereas Tomcat and Undertow detect absolute paths).// At least we offloaded the file from memory storage; it'll get deleted// from the temp dir eventually in any case. And for our user's purposes,// we can manually copy it to the requested location as a fallback.FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest.toPath()));}}@Overridepublic void transferTo(Path dest) throws IOException, IllegalStateException {FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));}}

所以要想真正搞清楚文件上传的解析过程,需要去查阅相关http文件上传协议,包括servlet对文件上传的规范制定,还有对应服务器的实现,例如: tomcat,jetty还有netty等。

Tomcat文件上传处理


CommonsMultipartResolver

使用该解析器时,需要引入相关依赖实现:

        <!--文件上传的依赖--><dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.3.1</version></dependency><dependency><groupId>commons-io</groupId><artifactId>commons-io</artifactId><version>2.5</version></dependency>

使用时,直接在DispathcerServlet对应的WebApplicationContext中声明即可:

    <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maxUploadSize" value="1000000"/></bean>

不需要在web.xml中声明,是因为CommonsMultipartResolver自己完成了对相关文件上传解析的工作,不需要使用tomcat提供的支持。


内部自己实现了servlet相关文件上传规范,具体细节就不展开了。


文件上传实战

要实现文件上传,首先按照刚才所阐述的内容,添加一个MultipartResolver的实例到DispatcherServlet的WebApplicationContext中,然后再着手实际的工作。

    <bean id="multipartResolver" class="org.springframework.web.multipart.support.CommonsMultipartResolver"/>

实际上,有了MultipartResolver帮我们返回的MultipartHttpServletRequest实例,表单中
<INPUT TYPE="file"...>元素完全可以与普通的文本域那样享受同等对待。

假设我们使用扩展AbstractControllerl的方式来处理multipart/form-data类型的如下表单提交:

<form method="post" action="/FILE/upload" enctype="multipart/form-data"><input name="file" type="file"><input type="submit" value="upload">
</form>

我们的controller实现看起来与使用servlet处理通常的Web请求没有太大差别,如下所示:

public class FileUpLoadController extends AbstractController {@Overrideprotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;MultipartFile multipartFile = multipartRequest.getFile("file");assert multipartFile != null;multipartFile.transferTo(new File(Objects.requireNonNull(multipartFile.getOriginalFilename())));return new ModelAndView("index");}
}

对应的配置文件为:

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">//默认是去webapp目录下寻找资源,如果不是以/开头的,那么转发请求的时候会拼接上当前servlet请求的路径 <property name="prefix" value="/"/><property name="suffix" value=".jsp"/></bean><bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"><property name="maxUploadSize" value="1000000"/></bean><bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping"><property name="mappings"><props><prop key="/FILE/**">uploadController</prop></props></property></bean><bean id="uploadController" class="com.example.controller.FileUpLoadController"/>

如果是tomcat部署的项目,注意使用相对路径时,对应的项目根路径是在tomcat对应的bin目录下


注意

如果抛出了下面的异常,说明没有配置相关文件上传的参数:


具体细节上面也提到了,可以回看。


优化

如果我们在表单里面多增加几个字段进行提交:

<form method="post" action="/FILE/upload" enctype="multipart/form-data"><input name="headImg" type="file"><input name="name" type="text"><input name="age" type="text"><input type="submit" value="upload">
</form>

那么controller在接收数据的时候就可以使用对象来接收了:

@Data
public class Peo {private MultipartFile headImg;private String name;private Integer age;
}

controller如下:

@Data
public class FileUpLoadController extends AbstractController {private Class<?> fileClass;@Overrideprotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {Object obj = BeanUtils.instantiateClass(fileClass);ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(obj,"peo");dataBinder.bind(request);return doHandle(obj,request);}private ModelAndView doHandle(Object obj,HttpServletRequest request) throws IOException {Peo peo = (Peo) obj;System.out.println("姓名: "+peo.getName()+"  年龄: "+peo.getAge()+"  头像大小: "+peo.getHeadImg().getSize());return new ModelAndView("index");}
}


再优化

为了进一步降低我们程序与Spring mvc API的耦合度,我们需要将Peo对象中的headImg属性对应的MultipartFile类型替换为byte[],我们可以手动替换,也可以增加一个类型转换器,这样数据绑定过程中就帮助我们自动完成了,这里采用后者做个演示:

@Data
public class Peo {private byte[] headImg;private String name;private Integer age;
}
@Data
public class FileUpLoadController extends AbstractController {private Class<?> fileClass;@Overrideprotected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {Object obj = BeanUtils.instantiateClass(fileClass);ServletRequestDataBinder dataBinder = new ServletRequestDataBinder(obj,"peo");dataBinder.registerCustomEditor(byte[].class,new ByteArrayMultipartFileEditor());dataBinder.bind(request);return doHandle(obj,request);}private ModelAndView doHandle(Object obj,HttpServletRequest request) throws IOException {Peo peo = (Peo) obj;System.out.println("姓名: "+peo.getName()+"  年龄: "+peo.getAge()+"  头像大小: "+peo.getHeadImg().length);return new ModelAndView("index");}
}

Spring MVC默认提供了两个自定义PropertyEditor实现类,一个就是我们刚刚使用的org,springframework.web.multipart.support.ByteArrayMultipartFileEditor,它将负责MultipartFile:类型到byte[]类型的转换;

另一个则是org.springframework,web,multipart.support.StringMultipartFileEditor,它负贵MultipartFile类型到String类型的转换。

如果使用StringMultipartFileEditor的话,Command对象中对应文件的属性需要声明为String类型,这通
常对应文本文件上传的情形。

有了相应的处理文件上传的Controller.之后,只需要将它们添加到DispatcherServlet的WebApplicationContext启用即可。

对此,你应该已经很熟悉了。总地来说,如果不去关心细节的话,在Spring MVC中实现文件上传还是比较惬意的事情。

Spring MVC更多家族成员----文件上传---06相关推荐

  1. Spring MVC 高级技术之文件上传(multipart)

    目录 1.所需 jar 包 2.配置文件上传解析器 3.前端 Form 4.后台接收 Handler 文件上传,即处理 multipart 形式的数据. 原生 servlet 处理上传的文件数据的,s ...

  2. Spring Boot 2.X 实现文件上传(三)

    使用 SpringBoot 项目完成单个.多个文件的上传处理,并将上传的文件保存到指定目录下. 代码演示案例 所有的 HTML 页面文件 index.html <!DOCTYPE html> ...

  3. ASP.NET MVC实现简单的文件上传与下载

    使用ASP.NET MVC实现简单的文件上传与下载. 1.上传文件HTML <form action='@Url.Action("Upload", "File&qu ...

  4. Spring MVC环境中的文件上传功能实现

    在实际开发过程中,尤其是web项目开发,文件上传和下载的需求的功能非常场景,比如说用户头像.商品图片.邮件附件等等.其实文件上传下载的本质都是通过流的形式进行读写操作,而在开发中不同的框架都会对文件上 ...

  5. Spring Boot 利用WebUploader进行文件上传

    Web Uploader WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件.在现代的浏览器里面能充分发挥HTML5的优势 ...

  6. 关于jQuery在Asp.Net Mvc 框架下Ajax文件上传的实现

    1. 实现传统的网络文件上传解决方案 首先,我先实现一个传统的网络文件上传方案,建立一个web页面,我还需要一个<form>和两个<input>元素就能解决问题,如在Index ...

  7. spring boot基础教程之文件上传下载

    一文件上传 文件上传主要分以下几个步骤: (1)新建maven java project: (2)在pom.xml加入相应依赖: (3)新建一个文件上传表单页面; (4)编写controller; ( ...

  8. uploadify 2.1.0 java spring mvc 2003版excel 附件上传

    上传页面uploadExcel.jsp代码: <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" & ...

  9. mvc获取ajax上传base64文件,Spring MVC+ajax+base64+amazeui框架上传头像带裁剪功能

    后台关键代码String filePath = null; String uuid = UUID.randomUUID().toString().replace("-", &quo ...

最新文章

  1. 自行控制loadrunner的socket协议性能测试 (转)
  2. python里none什么意思_python中stream=None什么意思?
  3. python是不是特别垃圾-11道Python最基本的面试题,不会好好反思吧!
  4. 腾讯爬虫python_Python爬虫,爬取腾讯漫画实战
  5. HTTP一个 TCP 连接可以发多少个 HTTP 请求等面试题
  6. 数学狂想曲(五)——概率分布(2), 自相关互相关卷积
  7. python编码格式有哪些_Python JSON编解码的方式有哪些
  8. 可能是全网最好的MySQL重要知识点/面试题总结
  9. linux服务器静态,为Linux服务器设置静态IP的方法
  10. Javascript button onclick和input type button在form中失效解决方案
  11. kitti数据集简介、百度网盘分享 kitti-object、kitti-tracking 全套数据集 自动驾驶
  12. idea 设置内存_IntelliJ IDEA修改内存使得变得流畅
  13. 人工智能十种“新”数学
  14. TeamTalk Base静态库说明
  15. 反向传播算法带动了业界使用两层神经网络研究的热潮
  16. tensorflowpython32_Python tensorflow.python.framework.dtypes 模块,float32() 实例源码 - 编程字典...
  17. 我是大海里的一页扁舟
  18. 实习期间工作、学习、成长、收获总结
  19. 删除在计算机的没用东西,怎么删除电脑中没用的东西?
  20. Web前端设计与开发课程设计:简易淘宝网页设计

热门文章

  1. Matlab——filter函数和butter函数
  2. 关于AQS中的enq方法的理解
  3. python更改文件后缀名
  4. Vue全家桶系列之Vue-router(五)
  5. eclipse与tomcat7配置
  6. 怎么用Folx下载制作短视频所用的素材
  7. java微信开发者模式开发_微信开发之启用开发者模式(三)
  8. Matlab进行gnss用户坐标计算,论文推荐 | 嵇昆浦,沈云中:含缺值GNSS基准站坐标序列的非插值小波分析与信号提取...
  9. 个人空间html源码,HTML常用代码段 - lynn_xiao的个人空间 - OSCHINA - 中文开源技术交流社区...
  10. 我热爱计算机作文450字,我的兴趣爱好作文450字精选5篇