SpringMVC 组件-MultipartResolver

SpringMVC 组件-MultipartResolverMultipartResolver 是用于处理文件上传操作的组件。 MultipartResolver 接口有两个实现类,一个是 CommonsMultipartResolver, 另一个是 StandardServletMultipartResolver。类结构图可以如下图所…

MultipartResolver 是用于处理文件上传操作的组件。 MultipartResolver 接口有两个实现类,一个是 CommonsMultipartResolver, 另一个是 StandardServletMultipartResolver。类结构图可以如下图所示。因为在 DispatcherServlet.properties 文件中是没有配置默认的 MultipartResolver 的实现类。因此,若要使用 MultipartResolver,必须得手动声明一个 MultipartResolver 类型的 Bean。

MultipartResolver.png

MultipartResolver 在 SpringMVC 中所做的工作

在 DispatcherServlet#doDispatcher 方法中,会调用 DispatcherServlet#checkMultipart 检查是否是上传文件的请求。如果是,则会对其进行处理。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
  HttpServletRequest processedRequest = request;
	HandlerExecutionChain mappedHandler = null;
	boolean multipartRequestParsed = false;
  // ...
	// 检查是否为上传请求
	// 如果是上传请求,则会对 request 进行封装 封装成 MultipartHttpServletRequest
	processedRequest = checkMultipart(request);
	// 是否为上传文件请求的标识符
	multipartRequestParsed = (processedRequest != request);
  // ... 
}

下面看下 DispatcherServlet#checkMultipart 方法。

protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
	// spring 容器中首先得配置 multipartResolver
	// 经过解析之后确认 request 中包含 multipart 信息
	if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
		if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
			if (request.getDispatcherType().equals(DispatcherType.REQUEST)) {
				logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
			}
		}
    // ...
		else {
			try {
				// 处理
				return this.multipartResolver.resolveMultipart(request);
			}
      // ...
		}
	}
	// If not returned before: return original request.
	return request;
}

前面也提到过,SpringMVC 中不会默认为我们初始化 MultipartResovler 类型的 Bean,因此我们需要手动配置 MultipartResovler,如果没有配置的话是无法处理的。此外,需要判断请求是否为上传文件的请求。通过 MultipartResolver#isMultipart 来进行判断,实现在具体的接口实现类中。以 apache commons 的实现为例,判断条件为:

  1. 请求方法为 post 请求
  2. contentType 必须不为空且为 multipart/ 开头。

随后会调用 MulpartResolver#resolveMultipart 方法进行 resolve。本小节的示例代码以 CommonsMultipartResolver 为例。

public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
	Assert.notNull(request, "Request must not be null");
	// 延迟解析
	if (this.resolveLazily) {
		return new DefaultMultipartHttpServletRequest(request) {
			// initializeMultipart 只有在调用 getXXX 方法的时候才会调用到
			// 因此被称为延迟解析
			@Override
			protected void initializeMultipart() {
				MultipartParsingResult parsingResult = parseRequest(request);
				setMultipartFiles(parsingResult.getMultipartFiles());
				setMultipartParameters(parsingResult.getMultipartParameters());
				setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
			}
		};
	}
	// 立马解析
	else {
		MultipartParsingResult parsingResult = parseRequest(request);
		return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
				parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
	}
}

解析的最终结果就是将 HttpServletRequest 封装成 DefaultMultipartHttpServletRequest。

protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
	// 返回编码
	String encoding = determineEncoding(request);
	// 获取 FileUpload
	FileUpload fileUpload = prepareFileUpload(encoding);
	try {
		// 解析出 FileItem
		List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
		return parseFileItems(fileItems, encoding);
	}
	catch (FileUploadBase.SizeLimitExceededException ex) {
		throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);
	}
	catch (FileUploadBase.FileSizeLimitExceededException ex) {
		throw new MaxUploadSizeExceededException(fileUpload.getFileSizeMax(), ex);
	}
	catch (FileUploadException ex) {
		throw new MultipartException("Failed to parse multipart servlet request", ex);
	}
}

将请求中的 File 全部解析到 fileItems 中保存。随后调用 CommonsFileUploadSupport#parseFileItems 对每一个 FileItem 进行处理。处理的内容就是对该请求的上传内容进行区别,最终分别保存到 DefaultMultipartHttpServletRequest 的 multipartFiles,multipartParameters 和 multipartParameterContentTypes。

回到了 DispatcherServlet#doDispatcher 方法。随后就是调用 getHandler 方法获取 handler。这里暂时先不展开。简单来讲就是根据 URL 寻找匹配的 HandlerMethod。 至于多个 HandlerMethod 如何选择等其他问题就留到 HandlerMapping 组件时再讲。

获取找 HandlerMethod 之后,会被封装成一个 HandlerExecutionChain。随后根据刚刚找到的 Handler(HandlerMethod) 去寻找 HandlerAdapter。HandlerAdapter 找到后调用拦截器的 preHandler。随后 HandlerMethod 进行处理。处理通过反射调用到定义的 HandlerMethod,也就是在 Controller 中定义的处理方法。

因此,MultipartResolver 在一个 SpringMVC 中的主要作用就是将 HttpServletRequest 封装成 DefaultMultipartHttpServletRequest, 随后通过 HandlerMethodArgumentResolver DefaultMultipartHttpServletRequest 中的文件解析到处理方法的入参 MultipartFile 上,解析后就可以在 handler 方法中对上传的文件进行处理了。 多文件上传可以通过 MultipartFile 数组来接收。并且前端需要将上传的文件名设置相同。

MultipartResolver 接口及其实现

MultipartResolver 是处理文件上传的解析器。 SpringMVC 中有两个文件上传的实现类,分别是基于 Servlet3.0 标准的 StandardServletMultipartResolver 以及基于 apache commons-fileupload 的 CommonsMultipartResolver。

MultipartResolver 接口

public interface MultipartResolver {
	boolean isMultipart(HttpServletRequest request);
	MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
	void cleanupMultipart(MultipartHttpServletRequest request);
}

MultipartResolver 接口中仅定义了三个方法,isMultipart 用于返回请求是否为上传文件的请求。resolveMultipart 将 HttpServletRequest 封装成 MultipartHttpServletRequest , MultipartHttpServletRequest 中包含了文件的信息。 cleanupMultipart 在请求执行完之后清理临时资源。临时资源一般指的是上传文件的临时文件,清理则是将其删除。

CommonsMultipartResolver 实现

CommonsMultipartResolver.png

上图中可以看到, CommonsMultipartResolver 实现了 MultipartResolver 和 ServletContextAware 接口,以及继承了 CommonsFileUploadSupport。 实现了 ServletContextAware 使得 CommonsMultipartResolver 具有获取 ServletContext 的能力。 CommonsFileUploadSupport 组合了 FileUpload, 并且 FileUpload 封装了对上传文件进行操作的高层次的 API。因此,可以认为 CommonsFileUploadSupport 充当了对上传文件进行操作的工具类。

isMultipart 方法

首先看下判断是否为上传文件请求时如何处理的。

	// MultipartResolver#isMultipart
	public boolean isMultipart(HttpServletRequest request) {
		return ServletFileUpload.isMultipartContent(request);
	}
	// ServletFileUpload#isMultipartContent
    public static final boolean isMultipartContent( HttpServletRequest request) {
        if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {
            return false;
        }
        return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
    }
	// FileUploadBase#isMultipartContent
    public static final boolean isMultipartContent(RequestContext ctx) {
        String contentType = ctx.getContentType();
        if (contentType == null) {
            return false;
        }
        if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {
            return true;
        }
        return false;
    }

前面也简单介绍过了,将 request 交由 ServletFileUpload 的静态方法处理,判断是否是 POST 请求。再交由 FileUploadBase 的静态方法处理,判断请求头中的 Content-type。ServletFileUpload 继承了 FileUpload, FileUpload 继承了 FileUploadBase。

resolveMultipart 方法

resolveMultipart 方法解析出请求中上传的文件,并将 HttpServletRequest 封装成 MultipartHttpServletRequest(具体类型为 DefaultMultipartHttpServletRequest)。

DefaultMultipartHttpServletRequest 中有三个比较重要的 map。

private Map<String, String[]> multipartParameters;
private Map<String, String> multipartParameterContentTypes;
// AbstractMultipartHttpServletRequest
private MultiValueMap<String, MultipartFile> multipartFiles;
  • multipartFiles: 保存上传的文件名与文件的关系,是 MultiValueMap 类型。
  • multipartParameterContentTypes: 参数名作为 key, content-type 类型作为 value。
  • multipartParameters: 参数名作为 key, 字符串数组作为 value

这里面最重要的就是 multipartFiles,在该 map 中保存了文件。

public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
	Assert.notNull(request, "Request must not be null");
	// 延迟解析
	if (this.resolveLazily) {
		return new DefaultMultipartHttpServletRequest(request) {
			// initializeMultipart 只有在调用 getXXX 方法的时候才会调用到
			// 因此被称为延迟解析
			@Override
			protected void initializeMultipart() {
				MultipartParsingResult parsingResult = parseRequest(request);
				setMultipartFiles(parsingResult.getMultipartFiles());
				setMultipartParameters(parsingResult.getMultipartParameters());
				setMultipartParameterContentTypes(parsingResult.getMultipartParameterContentTypes());
			}
		};
	}
	// 立马解析
	else {
		MultipartParsingResult parsingResult = parseRequest(request);
		return new DefaultMultipartHttpServletRequest(request, parsingResult.getMultipartFiles(),
				parsingResult.getMultipartParameters(), parsingResult.getMultipartParameterContentTypes());
	}
}

首先调用了 CommonsMultipartResolver#parseRequest 方法。

protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {
	// 返回编码
	String encoding = determineEncoding(request);
	// 获取 FileUpload 用于操作文件
	FileUpload fileUpload = prepareFileUpload(encoding);
	try {
		// 解析出 FileItem,一个文件对应一个 FileItem
		List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
		return parseFileItems(fileItems, encoding);
	}
	// ...
}

获取到了请求的编码类型,并且获取到了后续对文件进行处理的工具 FileUpload。随后调用了 ServletFileUpload#parseRequest 方法,最终调到 FileUploadBase#parseRequest 方法。

 public List<FileItem> parseRequest(RequestContext ctx)
         throws FileUploadException {
     List<FileItem> items = new ArrayList<FileItem>();
     boolean successful = false;
     try {
         FileItemIterator iter = getItemIterator(ctx);
         FileItemFactory fac = getFileItemFactory();
         if (fac == null) {
             throw new NullPointerException("No FileItemFactory has been set.");
         }
         while (iter.hasNext()) {
             final FileItemStream item = iter.next();
             // Don't use getName() here to prevent an InvalidFileNameException.
             final String fileName = ((FileItemIteratorImpl.FileItemStreamImpl) item).name;
             FileItem fileItem = fac.createItem(item.getFieldName(), item.getContentType(),
                                                item.isFormField(), fileName);
             items.add(fileItem);
             try {
                 Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
             } 
			 // ...
             final FileItemHeaders fih = item.getHeaders();
             fileItem.setHeaders(fih);
         }
         successful = true;
         return items;
     } // ...
	  finally {
         if (!successful) {
             for (FileItem fileItem : items) {
                 try {
                     fileItem.delete();
                 } catch (Exception ignored) {
                     // ignored TODO perhaps add to tracker delete failure list somehow?
                 }
             }
         }
     }
 }

就不展开细节了,将一个文件解析到了一个 FileItem 对象上,并且设置了 FileItem 的 Header。该方法返回一个存放了 FileItem 的 List。 得到List 后调用了父类 CommonsFileUploadSupport#parseFileItems。

protected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {
	// 上传的文件<文件名,文件>
	MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<>();
	// 保存参数的 contentType
	Map<String, String[]> multipartParameters = new HashMap<>();
	// 参数名 ContentTypes
	Map<String, String> multipartParameterContentTypes = new HashMap<>();
	// Extract multipart files and multipart parameters.
	// 遍历所有的 FileItem,对应一个文件
	for (FileItem fileItem : fileItems) {
		// 如果是 文本表单字段
		if (fileItem.isFormField()) {
			// ...
		}
		// 如果是 文件类型
		else {
			// multipart file field
			// 把 FileItem 封装成 CommonsMultipartFile
			CommonsMultipartFile file = createMultipartFile(fileItem);
			// 添加文件以及文件名
			multipartFiles.add(file.getName(), file);
			LogFormatUtils.traceDebug(logger, traceOn ->
					"Part '" + file.getName() + "', size " + file.getSize() +
							" bytes, filename='" + file.getOriginalFilename() + "'" +
							(traceOn ? ", storage=" + file.getStorageDescription() : "")
			);
		}
	}
	return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes);
}

这里只看是文件类型的情况。遍历每个 FileItem,并把 Fileitem 封装成 CommonsMultipartFile,放到 multipartFiles 中。 最终把三个 map 丢进 MultipartParsingResult 中并返回。 从这里可以看出,基于 apache commons-fileupload 处理文件上传操作,每个文件会被存储成 FileItem, 并封装成 CommonsMultipartFile,最后全放到 MultipartParsingResult 中返回处理。得到 MultipartParsingResult 后,还要将 MultipartParsingResult 中的属性取出,设置到 DefaultMultipartHttpServletRequest 中,最终返回处理后的 HttpServletRequest 具体类型为 DefaultMultipartHttpServletRequest。

cleanupMultipart 方法

protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) {
	for (List<MultipartFile> files : multipartFiles.values()) {
		for (MultipartFile file : files) {
			if (file instanceof CommonsMultipartFile) {
				CommonsMultipartFile cmf = (CommonsMultipartFile) file;
				// 删除所有的 file
				cmf.getFileItem().delete();
				LogFormatUtils.traceDebug(logger, traceOn ->
						"Cleaning up part '" + cmf.getName() +
								"', filename '" + cmf.getOriginalFilename() + "'" +
								(traceOn ? ", stored " + cmf.getStorageDescription() : ""));
			}
		}
	}
}

cleanupFileItems 方法通过获取 FileItem,并且清除其在临时目录下的文件。当 FileItem 实例被 GC 的时候,临时文件资源会被真正的回收。

StandardServletMultipartResolver 实现

StandardServletMultipartResolver.png

StandardServletMultipartResolver 是基于 Servlet 3.0 实现的文件上传解析器。同样来看看其实现的 MultipartResolver 的三个接口方法。

isMultipart 方法

public boolean isMultipart(HttpServletRequest request) {
	return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}

看到 StandardServletMultipartResolver 与 CommonsMultipartResolver 是不同的, StandardServletMultipartResolver 仅仅判断 content-type 是否以 multipart/ 开头。

resolveMultipart 方法

public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
	return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}

相较于 CommonsMultipartResolver 返回一个 DefaultMultipartHttpServletRequest, StandardServletMultipartResolver#resolveMultipart 处理之后返回一个 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) {
			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.add(part.getName(), new StandardMultipartFile(part, filename));
			}
			else {
				// 保存 Part 的名字
				this.multipartParameterNames.add(part.getName());
			}
		}
		setMultipartFiles(files);
	}
	catch (Throwable ex) {
		handleParseFailure(ex);
	}
}

重点就是 Request#parseParts 方法,解析 request 中的 file,并解析成 Part。

private void parseParts(boolean explicit) {
	// ...
    Context context = getContext();
    MultipartConfigElement mce = getWrapper().getMultipartConfigElement();
	// ...
    Parameters parameters = coyoteRequest.getParameters();
	// ...
    try {
		// 获取临时目录
        File location;
        String locationStr = mce.getLocation();
		// ...
        // Create a new file upload handler
        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
        ServletFileUpload upload = new ServletFileUpload();
        upload.setFileItemFactory(factory);
        upload.setFileSizeMax(mce.getMaxFileSize());
        upload.setSizeMax(mce.getMaxRequestSize());
		// 保存 parts, 一个 part 对应 一个 File
        parts = new ArrayList<>();
        try {
            List<FileItem> items =
                    upload.parseRequest(new ServletRequestContext(this));
            int maxPostSize = getConnector().getMaxPostSize();
            int postSize = 0;
            Charset charset = getCharset();
            for (FileItem item : items) {
                ApplicationPart part = new ApplicationPart(item, location);
                parts.add(part);
				// ...
            }
            success = true;
        } // ...
    } 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. 首先看第一步,获取 MultipartConfigElement,即获取为 DispatcherServlet 的 multipart-config 配置的属性值。
  2. 获取 ServletFileUpload,并根据配置,对其进行设置。
  3. 从 request 中解析出所有的 FileItem,以前面的 CommonsMultipartResolver 处理是一致的。
  4. 将 FileItem 封装成 ApplicationPart,并丢到 Collection 中。

回到上面的 StandardMultipartHttpServletRequest#parseRequest 方法。将 part 封装成 StandardMultipartFile, 然后设置到 AbstractMultipartHttpServletRequest#multipartFiles 中。至此,resolveMultipart 也处理完了。 同样,先从 request 中将文件处理成 Fileitem,随后封装成 ApplicationPart, 再封装成 StandardMultipartFile

cleanupMultipart 方法

public void cleanupMultipart(MultipartHttpServletRequest request) {
	if (!(request instanceof AbstractMultipartHttpServletRequest) ||
			((AbstractMultipartHttpServletRequest) request).isResolved()) {
		// To be on the safe side: explicitly delete the parts,
		// but only actual file parts (for Resin compatibility)
		try {
			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);
		}
	}
}

遍历所有的 Part,并执行删除相关的资源以及临时文件。与 CommonsMultipartResolver 是类似的。

小插曲

在写 StandardServletMultipartResolver 的测试 demo 的时候,报了一个 由于没有提供multi-part配置,无法处理parts 的异常。看了一下 StandardServletMultipartResolver 中也就只有一个 resolveLazily 可以设置。查了资料之后,因为 StandardServletMultipartResolver 是基于 Servlet 3.0 的,因此需要在 web.xml 的 dispatcherServlet 中配置 multipart-config 属性。注解也是类似的。

推荐阅读

今天的文章SpringMVC 组件-MultipartResolver分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15505.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注