Spring MVC : 文件上传处理原理

1. 配置MultipartResolver用以解析上传文件数据

开发人员或者框架通过某种方式定义bean MultipartResolverSpring 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是怎样的:

  1. 检测请求是否为文件上传请求
    // StandardServletMultipartResolver 代码片段
	@Override
	public boolean isMultipart(HttpServletRequest request) {
		return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
	}

从此可见,检测当前请求是否为文件上传请求的的方法就是检测请求头部Content-Type,看它是否以multipart/为前缀,大小写不区分。

  1. 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中名称为filepart

这里,你可能会问,使用了注解@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对文件上传的处理有以下要点 :

  1. 配置MultipartResolver用以解析上传文件数据 – StandardServletMultipartResolver bean的定义
  2. DispatcherServlet初始化MultipartResolverDispatcherServlet#initMultipartResolver
  3. DispatcherServlet请求处理主流程先检测文件上传 – DispatcherServlet#checkMultipart
  4. MultipartResolver解析文件上传请求数据 – 缺省使用StandardMultipartHttpServletRequest#parseRequest
  5. 控制器方法接收文件上传数据 – 由RequestParamMethodArgumentResolver解析参数

参考文章

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值