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
處理檔案請求會經過以下步驟:
- 判斷當前HttpServletRequest請求是否是檔案請求
- 是:將當前
HttpServletRequest
請求的資料(檔案和普通引數)封裝成MultipartHttpServletRequest
物件 - 不是:不處理
- 是:將當前
DispatcherServlet
對原始HttpServletRequest
或MultipartHttpServletRequest
物件進行業務處理- 業務處理完成,清除檔案上傳產生的臨時資源
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;
}
}
當需要指定其他檔案解析器時,只需要引入相關依賴,然後配置一個名為multipartResolver
的bean
物件:
@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/"));
}
其中,strictServletCompliance
是StandardServletMultipartResolver
的成員變數,預設false
,表示是否嚴格遵守Servlet 3.0規範。簡單來說就是對Content-Type
校驗的嚴格程度。如果strictServletCompliance
為false
,請求頭以multipart/
開頭就滿足檔案請求條件;如果strictServletCompliance
為true
,則需要請求頭以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
當resolveLazily
為true
時,會馬上呼叫parseRequest()
方法會對請求進行實際解析,該方法會完成兩件事情:
- 使用Servlet 3.0的
Part
API,獲取Part
集合 - 解析
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()
方法就可以獲取表單引數或表單檔案資訊。
當resolveLazily
為false
時,在MultipartResolver#resolveMultipart()
階段並不會進行檔案請求解析。也就是說,此時StandardMultipartHttpServletRequest
物件的成員變數都是空值。那麼,resolveLazily
為false
時檔案請求解析是在什麼時候完成的呢?
實際上,在呼叫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
物件集合。透過HttpServletRequest
的getPart()
/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;
}
核心步驟如下:
- 建立ServletFileUpload檔案上傳物件
- 解析檔案請求
- 封裝
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.InputStream
和java.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;
核心步驟如下:
- 解析檔案請求,封裝FormData物件
- 封裝
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.InputStream
和java.io.OutputStream
(傳統IO流),結合java.nio.ByteBuffer
將multipart
請求中的表單引數和檔案儲存到伺服器本地臨時檔案,然後將本地臨時檔案資訊封裝成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#getParts
或org.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.InputStream
和java.io.OutputStream
(傳統IO流),結合java.nio.ByteBuffer
將multipart
請求中的表單引數和檔案儲存到伺服器本地臨時檔案,然後將本地臨時檔案資訊封裝成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
,可以在初始化時指定。
當supportedMethods
為null
時,即在預設情況下,會呼叫ServletFileUpload.isMultipartContent()
方法進行判斷。此時檔案請求的滿足條件為:
- 請求方法為
POST
- 請求頭
Content-Type
為以multipart/
開頭
當supportedMethods
不為null
時,檔案請求滿足條件為: - 請求方法在
supportedMethods
列表中 - 請求頭
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
相同,CommonsMultipartResolver
的resolveLazily
成員變數也表示是否會馬上解析檔案。
當resolveLazily
為false
時,即預設情況下,不會立即解析檔案,只是會將原始請求進行簡單封裝。只有在呼叫DefaultMultipartHttpServletRequest#getXxx
方法時,會判斷檔案是否已經解析。如果沒有解析,會呼叫DefaultMultipartHttpServletRequest#initializeMultipart
進行解析。
當resolveLazily
為true
時,會立即呼叫CommonsMultipartResolver#parseRequest
方法進行檔案解析。
4.3 CommonsMultipartResolver#parseRequest
CommonsMultipartResolver#parseRequest
方法會進行檔案請求解析,總的來說包括兩個步驟:
- 解析檔案請求
- 封裝響應
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);
}
}
}