org.springframework.web.multipart.MultipartResolver是Spring-Web针对RFC1867实现的多文件上传解决策略。

1 使用场景

前端上传文件时,无论是使用比较传统的表单,还是使用FormData对象,其本质都是发送一个multipart/form-data请求。

例如,前端模拟上传代码如下:

var formdata = new FormData();
formdata.append("key1", "value1");
formdata.append("key2", "value2");
formdata.append("file1", fileInput.files[0], "/d:/Downloads/rfc1867.pdf");
formdata.append("file2", fileInput.files[0], "/d:/Downloads/rfc1314.pdf"); var requestOptions = {
method: 'POST',
body: formdata,
redirect: 'follow'
}; fetch("http://localhost:10001/file/upload", requestOptions)
.then(response => response.text())
.then(result => console.log(result))
.catch(error => console.log('error', error));

实际会发送如下HTTP请求:

POST /file/upload HTTP/1.1
Host: localhost:10001
Content-Length: 536
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW ----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key1" value1
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="key2" value2
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file1"; filename="/d:/Downloads/rfc1867.pdf"
Content-Type: application/pdf (data)
----WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file2"; filename="/d:/Downloads/rfc1314.pdf"
Content-Type: application/pdf (data)
----WebKitFormBoundary7MA4YWxkTrZu0gW

在后端可以通过MultipartHttpServletRequest接收文件:

@RestController
@RequestMapping("file")
public class FileUploadController {
@RequestMapping("/upload")
public String upload(MultipartHttpServletRequest request) {
// 获取非文件参数
String value1 = request.getParameter("key1");
System.out.println(value1); // value1
String value2 = request.getParameter("key2");
System.out.println(value2); // value2
// 获取文件
MultipartFile file1 = request.getFile("file1");
System.out.println(file1 != null ? file1.getOriginalFilename() : "null"); // rfc1867.pdf
MultipartFile file2 = request.getFile("file2");
System.out.println(file2 != null ? file2.getOriginalFilename() : "null"); // rfc1314.pdf
return "Hello MultipartResolver!";
}
}

2 MultipartResolver接口

2.1 MultipartResolver的功能

org.springframework.web.multipart.MultipartResolver是Spring-Web根据RFC1867规范实现的多文件上传的策略接口。

同时,MultipartResolver是Spring对文件上传处理流程在接口层次的抽象。

也就是说,当涉及到文件上传时,Spring都会使用MultipartResolver接口进行处理,而不涉及具体实现类。 MultipartResolver`接口源码如下:

public interface MultipartResolver {
/**
* 判断当前HttpServletRequest请求是否是文件请求
*/
boolean isMultipart(HttpServletRequest request);
/**
* 将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
*/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
/**
* 清除文件上传产生的临时资源(如服务器本地临时文件)
*/
void cleanupMultipart(MultipartHttpServletRequest request);
}

2.2 在DispatcherServlet中的使用

DispatcherServlet中持有MultipartResolver成员变量:

public class DispatcherServlet extends FrameworkServlet {
/** Well-known name for the MultipartResolver object in the bean factory for this namespace. */
public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver";
/** MultipartResolver used by this servlet. */
@Nullable
private MultipartResolver multipartResolver;
}

DispatcherServlet在初始化时,会从Spring容器中获取名为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 {
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.
this.multipartResolver = null;
if (logger.isTraceEnabled()) {
logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
}
}
}

需要注意的是,如果Spring容器中不存在名为multipartResolver的对象,DispatcherServlet并不会额外指定默认的文件解析器。此时,DispatcherServlet不会对文件上传请求进行处理。也就是说,尽管当前请求是文件请求,也不会被处理成MultipartHttpServletRequest,如果我们在控制层进行强制类型转换,会抛异常。

DispatcherServlet在处理业务时,会按照顺序分别调用这些方法进行文件上传处理,相关核心源码如下:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
boolean multipartRequestParsed = false;
try {
// 判断&封装文件请求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 请求处理……
}
finally {
// 清除文件上传产生的临时资源
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}

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 {
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
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");
}
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请求是否是文件请求

    1. 是:将当前HttpServletRequest请求的数据(文件和普通参数)封装成MultipartHttpServletRequest对象
    2. 不是:不处理
  2. DispatcherServlet对原始HttpServletRequestMultipartHttpServletRequest对象进行业务处理
  3. 业务处理完成,清除文件上传产生的临时资源

2.3 MultipartResolver实现类&配置方式

Spring提供了两个MultipartResolver实现类:

  • org.springframework.web.multipart.support.StandardServletMultipartResolver:根据Servlet 3.0+ Part Api实现
  • org.springframework.web.multipart.commons.CommonsMultipartResolver:根据Apache Commons FileUpload实现

在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器:

@AutoConfiguration
@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();
} @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
}

当需要指定其他文件解析器时,只需要引入相关依赖,然后配置一个名为multipartResolverbean对象:

@Bean
public MultipartResolver multipartResolver() {
MultipartResolver multipartResolver = ...;
return multipartResolver;
}

接下来,我们分别详细介绍两种实现类的使用和原理。

3 StandardServletMultipartResolver解析器

3.1 StandardServletMultipartResolver#isMultipart

StandardServletMultipartResolver解析器的通过判断请求的Content-Type来判断是否是文件请求:

public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(),
(this.strictServletCompliance ? "multipart/form-data" : "multipart/"));
}

其中,strictServletComplianceStandardServletMultipartResolver的成员变量,默认false,表示是否严格遵守Servlet 3.0规范。简单来说就是对Content-Type校验的严格程度。如果strictServletCompliancefalse,请求头以multipart/开头就满足文件请求条件;如果strictServletCompliancetrue,则需要请求头以multipart/form-data开头。

3.2 StandardServletMultipartResolver#resolveMultipart

StandardServletMultipartResolver在解析文件请求时,会将原始请求封装成StandardMultipartHttpServletRequest对象:

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

需要注意的是,这里传入this.resolveLazily成员变量,表示是否延迟解析。我们可以来看对应构造函数源码:

public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException { super(request);
if (!lazyParsing) {
parseRequest(request);
}
}

如果需要修改resolveLazily成员变量的值,需要在初始化StandardServletMultipartResolver时指定值。

在Spring Boot 2.0+中,默认会在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中创建StandardServletMultipartResolver作为默认文件解析器,此时会从MultipartProperties中读取resolveLazily值。因此,如果是使用Spring Boot 2.0+默认配置的文件解析器,可以在properties.yml文件中指定resolveLazily值:

spring.servlet.multipart.resolve-lazily=true

如果是使用自定义配置的方式配置StandardServletMultipartResolver,则可以在初始化的手动赋值:

@Bean
public MultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(true);
return multipartResolver;
}

3.3 StandardMultipartHttpServletRequest#parseRequest

resolveLazilytrue时,会马上调用parseRequest()方法会对请求进行实际解析,该方法会完成两件事情:

  1. 使用Servlet 3.0的Part API,获取Part集合
  2. 解析Part对象,封装表单参数和表单文件
private void parseRequest(HttpServletRequest request) {
try {
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 {
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files);
}
catch (Throwable ex) {
handleParseFailure(ex);
}
}

经过parseRequest()方法处理,我们在业务处理时,直接调用StandardMultipartHttpServletRequest接口的getXxx()方法就可以获取表单参数或表单文件信息。

resolveLazilyfalse时,在MultipartResolver#resolveMultipart()阶段并不会进行文件请求解析。也就是说,此时StandardMultipartHttpServletRequest对象的成员变量都是空值。那么,resolveLazilyfalse时文件请求解析是在什么时候完成的呢?

实际上,在调用StandardMultipartHttpServletRequest接口的getXxx()方法时,内部会判断是否已经完成文件请求解析。如果未解析,就会调用partRequest()方法进行解析,例如:

@Override
public Enumeration<String> getParameterNames() {
if (this.multipartParameterNames == null) {
initializeMultipart(); // parseRequest(getRequest());
}
// 业务处理……
}

3.4 HttpServletRequest#getParts

根据StandardMultipartHttpServletRequest#parseRequest源码可以发现,StandardServletMultipartResolver解析文件请求依靠的是HttpServletRequest#getParts方法。

这是StandardServletMultipartResolver是根据标准Servlet 3.0实现的核心体现。

在Servlet 3.0中定义了javax.servlet.http.Part,用来表示multipart/form-data请求体中的表单数据或文件:

public interface Part {
public InputStream getInputStream() throws IOException;
public String getContentType();
public String getName();
public String getSubmittedFileName();
public long getSize();
public void write(String fileName) throws IOException;
public void delete() throws IOException;
public String getHeader(String name);
public Collection<String> getHeaders(String name);
public Collection<String> getHeaderNames();
}

javax.servlet.http.HttpServletRequest,提供了获取multipart/form-data请求体各个part的方法:

public interface HttpServletRequest extends ServletRequest {
/**
* Return a collection of all uploaded Parts.
*
* @return A collection of all uploaded Parts.
* @throws IOException
* if an I/O error occurs
* @throws IllegalStateException
* if size limits are exceeded or no multipart configuration is
* provided
* @throws ServletException
* if the request is not multipart/form-data
* @since Servlet 3.0
*/
public Collection<Part> getParts() throws IOException, ServletException; /**
* Gets the named Part or null if the Part does not exist. Triggers upload
* of all Parts.
*
* @param name The name of the Part to obtain
*
* @return The named Part or null if the Part does not exist
* @throws IOException
* if an I/O error occurs
* @throws IllegalStateException
* if size limits are exceeded
* @throws ServletException
* if the request is not multipart/form-data
* @since Servlet 3.0
*/
public Part getPart(String name) throws IOException, ServletException;
}

所有实现标准Servlet 3.0规范的Web服务器,都必须实现getPart()/getParts()方法。也就是说,这些Web服务器在解析请求时,会将multipart/form-data请求体中的表单数据或文件解析成Part对象集合。通过HttpServletRequestgetPart()/getParts()方法,可以获取这些Part对象,进而获取multipart/form-data请求体中的表单数据或文件。

每个Web服务器对Servlet 3.0规范都有自己的实现方式。对于Spring Boot来说,通常使用的是Tomcat/Undertow/Jetty内嵌Web服务器。通常只需要了解这三种服务器的实现方式即可。

3.4.1 Tomcat实现

Tomcat是Spring Boot默认使用的内嵌Web服务器,只需要引入如下依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

会默认引入Tomcat依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</dependency>

Tomcat解析文件请求的核心在于org.apache.catalina.connector.Request#parseParts方法,核心代码如下:

// 1、创建ServletFileUpload文件上传对象
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();
upload.setFileItemFactory(factory);
upload.setFileSizeMax(mce.getMaxFileSize());
upload.setSizeMax(mce.getMaxRequestSize());
this.parts = new ArrayList<>();
try {
// 2、解析文件请求
List<FileItem> items =
upload.parseRequest(new ServletRequestContext(this));
// 3、封装Part对象
for (FileItem item : items) {
ApplicationPart part = new ApplicationPart(item, location);
this.parts.add(part);
}
}
success = true;
}

核心步骤如下:

  1. 创建ServletFileUpload文件上传对象
  2. 解析文件请求
  3. 封装Part对象

    org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest会进行实际解析文件请求:
public List<FileItem> parseRequest(final RequestContext ctx) throws FileUploadException {
final List<FileItem> items = new ArrayList<>();
boolean successful = false;
try {
final FileItemIterator iter = getItemIterator(ctx);
final FileItemFactory fileItemFactory = Objects.requireNonNull(getFileItemFactory(),
"No FileItemFactory has been set.");
final byte[] buffer = new byte[Streams.DEFAULT_BUFFER_SIZE];
while (iter.hasNext()) {
final FileItemStream item = iter.next();
// Don't use getName() here to prevent an InvalidFileNameException.
final String fileName = item.getName();
final FileItem fileItem = fileItemFactory.createItem(item.getFieldName(), item.getContentType(),
item.isFormField(), fileName);
items.add(fileItem);
try {
Streams.copy(item.openStream(), fileItem.getOutputStream(), true, buffer);
} catch (final FileUploadIOException e) {
throw (FileUploadException) e.getCause();
} catch (final IOException e) {
throw new IOFileUploadException(String.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;
}
}

简单来说,Tomcat会使用java.io.InputStreamjava.io.OutputStream(传统IO流)将multipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回。

也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

3.4.2 Undertow实现

为了使用Undertow服务器,需要引入如下依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>

Undertow解析文件请求的核心在于io.undertow.servlet.spec.HttpServletRequestImpl#loadParts方法,核心代码如下

final List<Part> parts = new ArrayList<>();
String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE);
if (mimeType != null && mimeType.startsWith(MultiPartParserDefinition.MULTIPART_FORM_DATA)) {
// 1、解析文件请求,封装FormData对象
FormData formData = parseFormData();
// 2、封装Part对象
if(formData != null) {
for (final String namedPart : formData) {
for (FormData.FormValue part : formData.get(namedPart)) {
parts.add(new PartImpl(namedPart,
part,
requestContext.getOriginalServletPathMatch().getServletChain().getManagedServlet().getMultipartConfig(),
servletContext, this));
}
}
}
} else {
throw UndertowServletMessages.MESSAGES.notAMultiPartRequest();
}
this.parts = parts;

核心步骤如下:

  1. 解析文件请求,封装FormData对象
  2. 封装Part对象

    io.undertow.servlet.spec.HttpServletRequestImpl#parseFormData方法会进行实际解析文件请求,核心代码如下:
final FormDataParser parser = originalServlet.getFormParserFactory().createParser(exchange)
try {
return parsedFormData = parser.parseBlocking();
}

io.undertow.server.handlers.form.MultiPartParserDefinition.MultiPartUploadHandler#parseBlocking核心代码如下:

InputStream inputStream = exchange.getInputStream();
try (PooledByteBuffer pooled = exchange.getConnection().getByteBufferPool().getArrayBackedPool().allocate()){
ByteBuffer buf = pooled.getBuffer();
while (true) {
buf.clear();
int c = inputStream.read(buf.array(), buf.arrayOffset(), buf.remaining());
if (c == -1) {
if (parser.isComplete()) {
break;
} else {
throw UndertowMessages.MESSAGES.connectionTerminatedReadingMultiPartData();
}
} else if (c != 0) {
buf.limit(c);
parser.parse(buf);
}
}
exchange.putAttachment(FORM_DATA, data);
}
return exchange.getAttachment(FORM_DATA);

在这个过程中,Undertow会使用java.io.InputStreamjava.io.OutputStream(传统IO流),结合java.nio.ByteBuffermultipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回(具体细节可以继续深入阅读相关源码)。

也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

3.4.2 Jetty实现

为了使用Jetty服务器,需要引入如下依赖:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

Jetty解析文件请求的核心在于org.eclipse.jetty.server.Request#getParts方法,核心代码如下

MultipartConfigElement config = (MultipartConfigElement)this.getAttribute("org.eclipse.jetty.multipartConfig");
this._multiParts = this.newMultiParts(config);
// 省略……
return this._multiParts.getParts();

org.eclipse.jetty.server.Request#newMultiParts会创建文件解析器:

private MultiParts newMultiParts(MultipartConfigElement config) throws IOException {
MultiPartFormDataCompliance compliance = this.getHttpChannel().getHttpConfiguration().getMultipartFormDataCompliance(); switch(compliance) {
case RFC7578:
return new MultiPartsHttpParser(this.getInputStream(), this.getContentType(), config, this._context != null ? (File)this._context.getAttribute("javax.servlet.context.tempdir") : null, this);
case LEGACY:
default:
return new MultiPartsUtilParser(this.getInputStream(), this.getContentType(), config, this._context != null ? (File)this._context.getAttribute("javax.servlet.context.tempdir") : null, this);
}
}

org.eclipse.jetty.server.MultiParts.MultiPartsHttpParser#getPartsorg.eclipse.jetty.server.MultiParts.MultiPartsUtilParser#getParts则会进行文件请求解析:

public Collection<Part> getParts() throws IOException {
Collection<Part> parts = this._httpParser.getParts();
this.setNonComplianceViolationsOnRequest();
return parts;
} public Collection<Part> getParts() throws IOException {
Collection<Part> parts = this._utilParser.getParts();
this.setNonComplianceViolationsOnRequest();
return parts;
}

在这个过程中,Jetty会使用java.io.InputStreamjava.io.OutputStream(传统IO流),结合java.nio.ByteBuffermultipart请求中的表单参数和文件保存到服务器本地临时文件,然后将本地临时文件信息封装成Part对象返回。

也就是说,我们在业务中获取到的文件实际上都来自服务器本地临时文件。

3.5 StandardServletMultipartResolver#cleanupMultipart

StandardServletMultipartResolver#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);
}
}
}

4 CommonsMultipartResolver解析器

为了使用CommonsMultipartResolver解析器,除了基础的spring-boot-starter-web,还需要额外引入如下依赖:

<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>

然后,配置名为multipartResolver的bean(此时Spring Boot不会添加默认文件解析器):

@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
// 文件删除配置:multipartResolver.setXxx()
multipartResolver.setResolveLazily(true);
return multipartResolver;
}

4.1 CommonsMultipartResolver#isMultipart

CommonsMultipartResolver解析器会根据请求方法和请求头来判断文件请求,源码如下:

public boolean isMultipart(HttpServletRequest request) {
return (this.supportedMethods != null ?
this.supportedMethods.contains(request.getMethod()) &&
FileUploadBase.isMultipartContent(new ServletRequestContext(request)) :
ServletFileUpload.isMultipartContent(request));
}

supportedMethods成员变量表示支持的请求方法,默认为null,可以在初始化时指定。

supportedMethodsnull时,即在默认情况下,会调用ServletFileUpload.isMultipartContent()方法进行判断。此时文件请求的满足条件为:

  1. 请求方法为POST
  2. 请求头Content-Type为以multipart/开头

    supportedMethods不为null时,文件请求满足条件为:
  3. 请求方法在supportedMethods列表中
  4. 请求头Content-Type为以multipart/开头

4.2 CommonsMultipartResolver#resolveMultipart

CommonsMultipartResolver在解析文件请求时,会将原始请求封装成DefaultMultipartHttpServletRequest对象:

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());
}
}

StandardServletMultipartResolver相同,CommonsMultipartResolverresolveLazily成员变量也表示是否会马上解析文件。

resolveLazilyfalse时,即默认情况下,不会立即解析文件,只是会将原始请求进行简单封装。只有在调用DefaultMultipartHttpServletRequest#getXxx方法时,会判断文件是否已经解析。如果没有解析,会调用DefaultMultipartHttpServletRequest#initializeMultipart进行解析。

resolveLazilytrue时,会立即调用CommonsMultipartResolver#parseRequest方法进行文件解析。

4.3 CommonsMultipartResolver#parseRequest

CommonsMultipartResolver#parseRequest方法会进行文件请求解析,总的来说包括两个步骤:

  1. 解析文件请求
  2. 封装响应
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);
return parseFileItems(fileItems, encoding);

深入阅读源码可以发现,在解析文件请求时,会采用与StandardServletMultipartResolver+Tomcat相同的方式保存临时文件:

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);
} 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?
}
}
}
}
}

4.4 CommonsMultipartResolver#cleanupMultipart

CommonsMultipartResolver#cleanupMultipart方法会将临时文件删除:

public void cleanupMultipart(MultipartHttpServletRequest request) {
if (!(request instanceof AbstractMultipartHttpServletRequest) ||
((AbstractMultipartHttpServletRequest) request).isResolved()) {
try {
cleanupFileItems(request.getMultiFileMap());
}
catch (Throwable ex) {
logger.warn("Failed to perform multipart cleanup for servlet request", ex);
}
}
}

Spring MVC文件请求处理详解:MultipartResolver的更多相关文章

  1. Spring MVC测试框架详解——服务端测试

    随着RESTful Web Service的流行,测试对外的Service是否满足期望也变的必要的.从Spring 3.2开始Spring了Spring Web测试框架,如果版本低于3.2,请使用sp ...

  2. spring mvc+myBatis配置详解

    一.spring mvc Spring框架(框架即:编程注解+xml配置的方式)MVC是Spring框架的一大特征,Spring框架有三大特征(IOC(依赖注入),AOP(面向切面),MVC(建模M- ...

  3. Spring MVC @RequestMapping注解详解

    @RequestMapping 参数说明 value:定义处理方法的请求的 URL 地址.(重点) method:定义处理方法的 http method 类型,如 GET.POST 等.(重点) pa ...

  4. spring集成spring mvc 和hibernate详解

    1.配置IOC容器 <!-- 配置IOC容器 --> <context-param> <param-name>contextConfigLocation</p ...

  5. Spring MVC @RequestMapping注解详解(2)

    @RequestMapping 参数说明 value:定义处理方法的请求的 URL 地址.(重点) method:定义处理方法的 http method 类型,如 GET.POST 等.(重点) pa ...

  6. Spring mvc中DispatcherServlet详解

    简介 DispatcherServlet是前端控制器设计模式的实现,提供SpringWebMVC的集中访问点,而且负责职责的分派,而且与spring IOC容器无缝集成,从而可以获得Spring的优势 ...

  7. Spring mvc整合freemarker详解

    1.什么是FreeMarker FreeMarker是一个模板引擎,一个基于模板生成文本输出的通用工具,使用纯Java编写 FreeMarker被设计用来生成HTML Web页面,特别是基于MVC模式 ...

  8. Spring MVC之LocaleResolver详解

    2019独角兽企业重金招聘Python工程师标准>>> 对于LocaleResolver,其主要作用在于根据不同的用户区域展示不同的视图,而用户的区域也称为Locale,该信息是可以 ...

  9. Spring MVC之@ControllerAdvice详解

    本文链接:https://blog.csdn.net/zxfryp909012366/article/details/82955259   对于@ControllerAdvice,我们比较熟知的用法是 ...

  10. Spring MVC 配置Controller详解

    在SpringMVC中,对于Controller的配置方式有很多种,如下做简单总结 第一种 URL对应Bean如果要使用此类配置方式,需要在XML中做如下样式配置: <!-- 表示将请求的URL ...

随机推荐

  1. mocha、chai和supertest单元测试

    mocha单元测试 1. 因为有时候在代码中加了新的东西需要反复测试接口 或者 别人要求 重新跑接口非常的繁琐 2. 所有我们需要一个帮我们重复测试的东西  那就是mocha 3. 先下载 一定不要全 ...

  2. Kibana:在Kibana中对数据进行深入分析 (drilldown)

    文章转载自:https://blog.csdn.net/UbuntuTouch/article/details/105193907 在上面,我们需要把之前地址栏中拷贝的内容粘贴过来,并做相应的修改.针 ...

  3. Elasticsearch中的一些重要概念:cluster, node, index, document, shards及replica

    首先,我们来看下一下如下的这个图: Cluster Cluster也就是集群的意思.Elasticsearch集群由一个或多个节点组成,可通过其集群名称进行标识.通常这个Cluster 的名字是可以在 ...

  4. Codeforces Round #442 (Div. 2) B. Nikita and string

    题意:求最长可以分a b a为三部分子串,a b a可以为空 思路在代码里 1 #include<cstdio> 2 #include<iostream> 3 #include ...

  5. Selenium+Python系列(二) - 元素定位那些事

    一.写在前面 今天一实习生小孩问我,说哥你自动化学了多久才会的,咋学的? 自学三个月吧,真的是硬磕呀,当时没人给讲! 其实,学什么都一样,真的就是你想改变的决心有多强罢了. 二.元素定位 这部分内容可 ...

  6. servlet过滤器--使用过滤器统计网站访问人数的计数(注解形式)

    文章目录 1.什么是过滤器? 2.过滤器核心对象 3.过滤器创建和配置 4.举例子 1.什么是过滤器? 主要用于对客户端的请求进行过滤处理,再将经过过滤后的请求转交给下一个资源. 2.过滤器核心对象 ...

  7. 【SSM】学习笔记(二)——SpringMVC入门

    原视频链接:https://www.bilibili.com/video/BV1Fi4y1S7ix/?p=43&spm_id_from=pageDriver&vd_source=8ae ...

  8. Istio(二):在Kubernetes(k8s)集群上安装部署istio1.14

    目录 一.模块概览 二.系统环境 三.安装istio 3.1 使用 Istioctl 安装 3.2 使用 Istio Operator 安装 3.3 生产部署情况如何? 3.4 平台安装指南 四.Ge ...

  9. k8s健康检查报错

    编辑yaml去掉健康检查 kubectl edit deployment deploymentname 服务恢复

  10. 所有selenium相关的库

    通过爬虫 获取 官方文档库 如果想获取 相应的库 修改对应配置即可 代码如下 from urllib.parse import urljoin import requests from lxml im ...