Spring MVC文件请求处理-MultipartResolver
Spring Boot中的MultipartResolver是一个用于解析multipart/form-data类型请求的策略接口,通常用于文件上传。
对应后端使用MultipartFile对象接收。
@RequestMapping("/upload")
public String uploadFile(MultipartFile file) throws IOException {
String fileName = file.getOriginalFilename();
String filePath = "D:\\";
File file1 = new File(filePath+"\\"+fileName);
file.transferTo(file1);
return "ok";
}
一、MultipartResolver接口
MultipartResolver是个接口,不做任何实现。
public interface MultipartResolver {
/**
* 判断当前HttpServletRequest请求是否是文件请求
*/
boolean isMultipart(HttpServletRequest request);
/**
* 将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
*/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
/**
* 清除文件上传产生的临时资源(如服务器本地临时文件)
*/
void cleanupMultipart(MultipartHttpServletRequest request);
}
MultipartResolver在DispatcherServlet使用
我们都知道请求首先到达DispatcherServlet,它负责协调和组织不同组件完成请求处理并返回响应工作,所以DispatcherServlet中持有MultipartResolver成员变量,
在onRefresh中完成包括MultipartResolver在内的9个组件。
在doDispatch中调用checkMultipart(request); 方法将请求转换为 multipart 请求,如该请求不是multipart 请求则返回原request
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
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 if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
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;
}
}
}
}
// If not returned before: return original request.
return request;
}
DispatcherServlet处理文件请求会经过以下步骤:
1、判断当前HttpServletRequest请求是否是文件请求
- 是:将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
- 不是:不处理
2、DispatcherServlet对原始HttpServletRequest或MultipartHttpServletRequest对象进行业务处理
3、业务处理完成,清除文件上传产生的临时资源
二、MultipartResolver的实现类
Spring提供了两个MultipartResolver实现类:
- StandardServletMultipartResolver:根据Servlet 3.0+ Part Api实现(默认使用)
- CommonsMultipartResolver:根据Apache Commons FileUpload实现,需要引入相关的依赖
三、 StandardServletMultipartResolver
StandardServletMultipartResolver是根据Servlet 3.0+ Part Api实现,也是我们默认使用文件解析器。
public class StandardServletMultipartResolver implements MultipartResolver {
private boolean resolveLazily = false;
/**
* 设置是否延迟解析
* 可通过配置文件进行设置spring.servlet.multipart.resolve-lazily=true
* @since 3.2.9
*/
public void setResolveLazily(boolean resolveLazily) {
this.resolveLazily = resolveLazily;
}
/**
* 判断是否是multipart请求
*/
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "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()) {
// 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);
}
}
}
}
3.1 StandardMultipartHttpServletRequest
构造方法
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
parseRequest(request);
}
}
当resolveLazily为false时,在MultipartResolver#resolveMultipart()阶段并不会进行文件请求解析。也就是说,此时StandardMultipartHttpServletRequest对象的成员变量都是空值。那么,resolveLazily为false时文件请求解析是在什么时候完成的呢?
实际上,在调用StandardMultipartHttpServletRequest接口的getXxx()方法时,内部会判断是否已经完成文件请求解析。如果未解析,就会调用partRequest()方法进行解析,例如:
StandardMultipartHttpServletRequest#parseRequest
private void parseRequest(HttpServletRequest request) {
try {
//调用getParts()方法从请求中获取所有的部分(parts),这些部分可能包括文件和表单数据
Collection<Part> parts = request.getParts();
//初始化集合存储所有非文件的参数名
this.multipartParameterNames = new LinkedHashSet<>(parts.size());
//存储每个文件的名称及其对应的MultipartFile对象。
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
//对于每个部分,获取其Content-Disposition头,并解析出文件名。
for (Part part : parts) {
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
ContentDisposition disposition = ContentDisposition.parse(headerValue);
String filename = disposition.getFilename();
//如果文件名不为null,检测是否需要解码,添加到files中
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);
}
}
3.2 CommonsMultipartResolver
CommonsMultipartResolver根据Apache Commons FileUpload实现,可以处理大文件、文件流等。功能比较强大
首先需要引入了commons-fileupload 依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
编写配置类
@Configuration
public class MultipartResolverConfig {
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
// 文件请求解析配置:multipartResolver.setXxx()
multipartResolver.setResolveLazily(true);
// 设置编码格式
multipartResolver.setDefaultEncoding("UTF-8");
// 设置写入内存的最大值(单位Byte),超过此值则写入硬盘临时文件
multipartResolver.setMaxInMemorySize(1024 * 1024);
// 设置上传文件最大值
multipartResolver.setMaxUploadSize(1024 * 1024 * 1024);
return multipartResolver;
}
}
CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求,源码如下:
@Override
public boolean isMultipart(HttpServletRequest request) {
return ServletFileUpload.isMultipartContent(request);
}
public static final boolean isMultipartContent(HttpServletRequest request) {
return !"POST".equalsIgnoreCase(request.getMethod()) ? false : FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}
3.2.1CommonsMultipartResolver#resolveMultipart#
CommonsMultipartResolver在解析文件请求时,会将原始请求封装成DefaultMultipartHttpServletRequest对象:
@Override
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {
Assert.notNull(request, "Request must not be null");
// 判断是否是懒加载
if (this.resolveLazily) {
return new DefaultMultipartHttpServletRequest(request) {
@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());
}
}
3.2.2 CommonsMultipartResolver#parseRequest
CommonsMultipartResolver使用流式处理文件,可以避免内存溢出
问题背景: 文件上传过程中,如果没有采用合适的策略,处理大文件时容易导致内存溢出(OutOfMemoryError)。这通常发生在将整个文件内容加载到内存中再进行处理时。
解决方法: CommonsMultipartResolver 使用了 流式处理,即它不会一次性将整个文件加载到内存中,而是通过流(stream)方式逐步读取文件的内容。这种方式确保了即使上传的是大文件,系统内存也不会被大量占用。这种流式读取方式能有效避免内存溢出问题。
public List<FileItem> parseRequest(RequestContext ctx)
throws FileUploadException {
List<FileItem> items = new ArrayList<FileItem>();
boolean successful = false;
try {
// 获取文件迭代器,用于遍历 multipart/form-data 请求中的每个部分。每个部分可能是一个文件或者表单字段。
FileItemIterator iter = getItemIterator(ctx);
FileItemFactory fac = getFileItemFactory();
if (fac == null) {
throw new NullPointerException("No FileItemFactory has been set.");
}
while (iter.hasNext()) {
/**
* 使用 iter.hasNext() 遍历每个文件部分,item 代表当前处理的文件流。
* fileName 从当前文件部分中获取文件名(这里没有使用 getName() 是为了避免异常)。
* 调用 fac.createItem() 创建一个 FileItem,传入字段名、内容类型、是否是表单字段以及文件名,然后将 fileItem 添加到 items 列表中
*/
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 {
// 将 item 的输入流(文件数据)通过 Streams.copy 复制到 fileItem 的输出流中,完成文件上传。
Streams.copy(item.openStream(), fileItem.getOutputStream(), true);
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new IOFileUploadException(format("Processing of %s request failed. %s",
MULTIPART_FORM_DATA, e.getMessage()), e);
}
final FileItemHeaders fih = item.getHeaders();
fileItem.setHeaders(fih);
}
successful = true;
return items;
} catch (FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (IOException e) {
throw new FileUploadException(e.getMessage(), e);
} finally {
if (!successful) {
for (FileItem fileItem : items) {
try {
fileItem.delete();
} catch (Exception ignored) {
// ignored TODO perhaps add to tracker delete failure list somehow?
}
}
}
}
}