• 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>
    
    • 1
    • 2
    • 3
    • 4

    客户端浏览器将按照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);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    当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);
    	           ...
    }			
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    	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;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    当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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    MultipartHttpServletRequest的附加能力来自于它的父接口MultipartRequest。

    简单地说,我们现在可以在某个Controller中,通过MultipartHttpServletRequest直接获取MultipartFile类所封装的上传后的文件,如下所示:

            MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;
            MultipartFile file = multipartHttpServletRequest.getFile("file");
    
    • 1
    • 2

    至于要将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"/>
    
    • 1

    为什么在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;
                 ....    
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    现在,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);
    				}
    		}
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这样,整个文件上传的生命周期即告结束。


    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);
    			}
    		}
    	}
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    注意

    为了使用基于 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>controllerservlet-name>
           <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
           <load-on-startup>2load-on-startup>
           <multipart-config>
               <max-file-size>20848820max-file-size>
               <max-request-size>418018841max-request-size>
               <file-size-threshold>1048576file-size-threshold>
           multipart-config>
       servlet>
    
    
        <servlet-mapping>
            <servlet-name>controllerservlet-name>
    
            <url-pattern>/url-pattern>
        servlet-mapping>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 通过在对应的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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 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方法
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45

    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("?=")) {
    						filename = MimeDelegate.decode(filename);
    					}
    					//加入files集合,key就是文件名,value就是封装part对象的StandardMultipartFile
    					files.add(part.getName(), new StandardMultipartFile(part, filename));
    				}
    				else {
    					this.multipartParameterNames.add(part.getName());
    				}
    			}
    			//设置到当前类内部的private MultiValueMap multipartFiles;
                 //属性中去
    			setMultipartFiles(files);
    		}
    		catch (Throwable ex) {
    			//处理解析错误
    			handleParseFailure(ex);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    如果底层使用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配置,无法处理parts
                        partsParseException = new IllegalStateException(
                                 //这里使用的是tomcat内部的错误翻译机制StringManager
                                sm.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 TEMPDIR
                    location = new File(locationStr);
                    if (!location.isAbsolute()) {
                        location = new File(
                                (File) context.getServletContext().getAttribute(ServletContext.TEMPDIR),
                                locationStr).getAbsoluteFile();
                    }
                }
                //如果相关路径文件不存在,会去创建对应目录---CreateUploadTargets默认为false
                if (!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 {
                //解析拿到当前请求关联的FileItems
                    List<FileItem> items =
                            upload.parseRequest(new ServletRequestContext(this));
                    int maxPostSize = getConnector().getMaxPostSize();
                    int postSize = 0;
                    Charset charset = getCharset();
                    //挨个遍历处理
                    for (FileItem item : items) {
                        //封装为一个part
                        ApplicationPart 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 sign
                                postSize++;
                                // Value length
                                //文件大小
                                postSize += part.getSize();
                                // Value separator
                                postSize++;
                                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);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154

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

    	@Override
    	public MultipartFile getFile(String name) {
    		return getMultipartFiles().getFirst(name);
    	}
    
    	@Override
    	public List<MultipartFile> getFiles(String name) {
    		List<MultipartFile> multipartFiles = getMultipartFiles().get(name);
    		if (multipartFiles != null) {
    			return multipartFiles;
    		}
    		else {
    			return Collections.emptyList();
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    就是从上面解析好的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;
    		}
    
    		@Override
    		public String getName() {
    			return this.part.getName();
    		}
    
    		@Override
    		public String getOriginalFilename() {
    			return this.filename;
    		}
    
    		@Override
    		public String getContentType() {
    			return this.part.getContentType();
    		}
    
    		@Override
    		public boolean isEmpty() {
    			return (this.part.getSize() == 0);
    		}
    
    		@Override
    		public long getSize() {
    			return this.part.getSize();
    		}
    
    		@Override
    		public byte[] getBytes() throws IOException {
    			return FileCopyUtils.copyToByteArray(this.part.getInputStream());
    		}
    
    		@Override
    		public InputStream getInputStream() throws IOException {
    			return this.part.getInputStream();
    		}
    
    		@Override
    		public 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()));
    			}
    		}
    
    		@Override
    		public void transferTo(Path dest) throws IOException, IllegalStateException {
    			FileCopyUtils.copy(this.part.getInputStream(), Files.newOutputStream(dest));
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65

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

    Tomcat文件上传处理


    CommonsMultipartResolver

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

            
            <dependency>
                <groupId>commons-fileuploadgroupId>
                <artifactId>commons-fileuploadartifactId>
                <version>1.3.1version>
            dependency>
            <dependency>
                <groupId>commons-iogroupId>
                <artifactId>commons-ioartifactId>
                <version>2.5version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

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

    在这里插入图片描述
    内部自己实现了servlet相关文件上传规范,具体细节就不展开了。


    文件上传实战

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

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

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

    假设我们使用扩展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>
    
    • 1
    • 2
    • 3
    • 4

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

    public class FileUpLoadController extends AbstractController {
        @Override
        protected 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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    对应的配置文件为:

    <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/**">uploadControllerprop>
                props>
            property>
        bean>
    
        <bean id="uploadController" class="com.example.controller.FileUpLoadController"/>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    如果是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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    @Data
    public class Peo {
        private MultipartFile headImg;
        private String name;
        private Integer age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    controller如下:

    @Data
    public class FileUpLoadController extends AbstractController {
        private Class<?> fileClass;
    
        @Override
        protected 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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述


    再优化

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

    @Data
    public class Peo {
        private byte[] headImg;
        private String name;
        private Integer age;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    @Data
    public class FileUpLoadController extends AbstractController {
        private Class<?> fileClass;
    
        @Override
        protected 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");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    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中实现文件上传还是比较惬意的事情。

  • 相关阅读:
    嵌套路由写法
    maven 项目导入本地jar包
    关系型数据库存储多维指标数据
    Java项目防止SQL注入的四种方案
    kubernetes部署jenkins
    NNDL 实验三 线性回归
    微软秋招内推八月截止,快来找我内推!
    Docker 必知必会4----容器之间的通信
    学长告诉我,大厂MySQL都是通过SSH连接的
    【微服务】服务发现和管理技术框架选型调研
  • 原文地址:https://blog.csdn.net/m0_53157173/article/details/126097078