Spring?MVC文件請求處理MultipartResolver詳解
org.springframework.web.multipart.MultipartResolver是Spring-Web針對RFC1867實(shí)現(xiàn)的多文件上傳解決策略。
1 使用場景
前端上傳文件時,無論是使用比較傳統(tǒng)的表單,還是使用FormData對象,其本質(zhì)都是發(fā)送一個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));實(shí)際會發(fā)送如下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) {
// 獲取非文件參數(shù)
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根據(jù)RFC1867規(guī)范實(shí)現(xiàn)的多文件上傳的策略接口。
同時,MultipartResolver是Spring對文件上傳處理流程在接口層次的抽象。
也就是說,當(dāng)涉及到文件上傳時,Spring都會使用MultipartResolver接口進(jìn)行處理,而不涉及具體實(shí)現(xiàn)類。 MultipartResolver`接口源碼如下:
public interface MultipartResolver {
/**
* 判斷當(dāng)前HttpServletRequest請求是否是文件請求
*/
boolean isMultipart(HttpServletRequest request);
/**
* 將當(dāng)前HttpServletRequest請求的數(shù)據(jù)(文件和普通參數(shù))封裝成MultipartHttpServletRequest對象
*/
MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;
/**
* 清除文件上傳產(chǎn)生的臨時資源(如服務(wù)器本地臨時文件)
*/
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實(shí)現(xiàn)類),作為文件上傳解析器:
/**
* 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并不會額外指定默認(rèn)的文件解析器。此時,DispatcherServlet不會對文件上傳請求進(jìn)行處理。也就是說,盡管當(dāng)前請求是文件請求,也不會被處理成MultipartHttpServletRequest,如果我們在控制層進(jìn)行強(qiáng)制類型轉(zhuǎn)換,會拋異常。
DispatcherServlet在處理業(yè)務(wù)時,會按照順序分別調(diào)用這些方法進(jìn)行文件上傳處理,相關(guān)核心源碼如下:
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
boolean multipartRequestParsed = false;
try {
// 判斷&封裝文件請求
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 請求處理……
}
finally {
// 清除文件上傳產(chǎn)生的臨時資源
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}在checkMultipart()方法中,會進(jìn)行判斷、封裝文件請求:
/**
* 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處理文件請求會經(jīng)過以下步驟:
- 判斷當(dāng)前HttpServletRequest請求是否是文件請求
是:將當(dāng)前
HttpServletRequest請求的數(shù)據(jù)(文件和普通參數(shù))封裝成MultipartHttpServletRequest對象不是:不處理
DispatcherServlet對原始HttpServletRequest或MultipartHttpServletRequest對象進(jìn)行業(yè)務(wù)處理- 業(yè)務(wù)處理完成,清除文件上傳產(chǎn)生的臨時資源
2.3 MultipartResolver實(shí)現(xiàn)類&配置方式
Spring提供了兩個MultipartResolver實(shí)現(xiàn)類:
org.springframework.web.multipart.support.StandardServletMultipartResolver:根據(jù)Servlet 3.0+ Part Api實(shí)現(xiàn)org.springframework.web.multipart.commons.CommonsMultipartResolver:根據(jù)Apache Commons FileUpload實(shí)現(xiàn)
在Spring Boot 2.0+中,默認(rèn)會在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中創(chuàng)建StandardServletMultipartResolver作為默認(rèn)文件解析器:
@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;
}
}當(dāng)需要指定其他文件解析器時,只需要引入相關(guān)依賴,然后配置一個名為multipartResolver的bean對象:
@Bean
public MultipartResolver multipartResolver() {
MultipartResolver multipartResolver = ...;
return multipartResolver;
}
接下來,我們分別詳細(xì)介紹兩種實(shí)現(xiàn)類的使用和原理。
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的成員變量,默認(rèn)false,表示是否嚴(yán)格遵守Servlet 3.0規(guī)范。簡單來說就是對Content-Type校驗(yàn)的嚴(yán)格程度。如果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成員變量,表示是否延遲解析。我們可以來看對應(yīng)構(gòu)造函數(shù)源碼:
public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {
super(request);
if (!lazyParsing) {
parseRequest(request);
}
}如果需要修改resolveLazily成員變量的值,需要在初始化StandardServletMultipartResolver時指定值。
在Spring Boot 2.0+中,默認(rèn)會在org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration中創(chuàng)建StandardServletMultipartResolver作為默認(rèn)文件解析器,此時會從MultipartProperties中讀取resolveLazily值。因此,如果是使用Spring Boot 2.0+默認(rèn)配置的文件解析器,可以在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
當(dāng)resolveLazily為true時,會馬上調(diào)用parseRequest()方法會對請求進(jìn)行實(shí)際解析,該方法會完成兩件事情:
- 使用Servlet 3.0的
PartAPI,獲取Part集合 - 解析
Part對象,封裝表單參數(shù)和表單文件
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);
}
}經(jīng)過parseRequest()方法處理,我們在業(yè)務(wù)處理時,直接調(diào)用StandardMultipartHttpServletRequest接口的getXxx()方法就可以獲取表單參數(shù)或表單文件信息。
當(dāng)resolveLazily為false時,在MultipartResolver#resolveMultipart()階段并不會進(jìn)行文件請求解析。也就是說,此時StandardMultipartHttpServletRequest對象的成員變量都是空值。那么,resolveLazily為false時文件請求解析是在什么時候完成的呢?
實(shí)際上,在調(diào)用StandardMultipartHttpServletRequest接口的getXxx()方法時,內(nèi)部會判斷是否已經(jīng)完成文件請求解析。如果未解析,就會調(diào)用partRequest()方法進(jìn)行解析,例如:
@Override
public Enumeration<String> getParameterNames() {
if (this.multipartParameterNames == null) {
initializeMultipart(); // parseRequest(getRequest());
}
// 業(yè)務(wù)處理……
}3.4 HttpServletRequest#getParts
根據(jù)StandardMultipartHttpServletRequest#parseRequest源碼可以發(fā)現(xiàn),StandardServletMultipartResolver解析文件請求依靠的是HttpServletRequest#getParts方法。
這是StandardServletMultipartResolver是根據(jù)標(biāo)準(zhǔn)Servlet 3.0實(shí)現(xiàn)的核心體現(xiàn)。
在Servlet 3.0中定義了javax.servlet.http.Part,用來表示multipart/form-data請求體中的表單數(shù)據(jù)或文件:
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;
}
所有實(shí)現(xiàn)標(biāo)準(zhǔn)Servlet 3.0規(guī)范的Web服務(wù)器,都必須實(shí)現(xiàn)getPart()/getParts()方法。也就是說,這些Web服務(wù)器在解析請求時,會將multipart/form-data請求體中的表單數(shù)據(jù)或文件解析成Part對象集合。通過HttpServletRequest的getPart()/getParts()方法,可以獲取這些Part對象,進(jìn)而獲取multipart/form-data請求體中的表單數(shù)據(jù)或文件。
每個Web服務(wù)器對Servlet 3.0規(guī)范都有自己的實(shí)現(xiàn)方式。對于Spring Boot來說,通常使用的是Tomcat/Undertow/Jetty內(nèi)嵌Web服務(wù)器。通常只需要了解這三種服務(wù)器的實(shí)現(xiàn)方式即可。
3.4.1 Tomcat實(shí)現(xiàn)
Tomcat是Spring Boot默認(rèn)使用的內(nèi)嵌Web服務(wù)器,只需要引入如下依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency>
會默認(rèn)引入Tomcat依賴:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </dependency>
Tomcat解析文件請求的核心在于org.apache.catalina.connector.Request#parseParts方法,核心代碼如下:
// 1、創(chuàng)建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;
}核心步驟如下:
- 創(chuàng)建ServletFileUpload文件上傳對象
- 解析文件請求
- 封裝
Part對象org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest會進(jìn)行實(shí)際解析文件請求:
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(傳統(tǒng)IO流)將multipart請求中的表單參數(shù)和文件保存到服務(wù)器本地臨時文件,然后將本地臨時文件信息封裝成Part對象返回。
也就是說,我們在業(yè)務(wù)中獲取到的文件實(shí)際上都來自服務(wù)器本地臨時文件。
3.4.2 Undertow實(shí)現(xiàn)
為了使用Undertow服務(wù)器,需要引入如下依賴:
<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方法會進(jìn)行實(shí)際解析文件請求,核心代碼如下:
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(傳統(tǒng)IO流),結(jié)合java.nio.ByteBuffer將multipart請求中的表單參數(shù)和文件保存到服務(wù)器本地臨時文件,然后將本地臨時文件信息封裝成Part對象返回(具體細(xì)節(jié)可以繼續(xù)深入閱讀相關(guān)源碼)。
也就是說,我們在業(yè)務(wù)中獲取到的文件實(shí)際上都來自服務(wù)器本地臨時文件。
3.4.2 Jetty實(shí)現(xiàn)
為了使用Jetty服務(wù)器,需要引入如下依賴:
<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會創(chuàng)建文件解析器:
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則會進(jìn)行文件請求解析:
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(傳統(tǒng)IO流),結(jié)合java.nio.ByteBuffer將multipart請求中的表單參數(shù)和文件保存到服務(wù)器本地臨時文件,然后將本地臨時文件信息封裝成Part對象返回。
也就是說,我們在業(yè)務(wù)中獲取到的文件實(shí)際上都來自服務(wù)器本地臨時文件。
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解析器,除了基礎(chǔ)的spring-boot-starter-web,還需要額外引入如下依賴:
<dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.4</version> </dependency>
然后,配置名為multipartResolver的bean(此時Spring Boot不會添加默認(rèn)文件解析器):
@Bean
public MultipartResolver multipartResolver() {
CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
// 文件刪除配置:multipartResolver.setXxx()
multipartResolver.setResolveLazily(true);
return multipartResolver;
}4.1 CommonsMultipartResolver#isMultipart
CommonsMultipartResolver解析器會根據(jù)請求方法和請求頭來判斷文件請求,源碼如下:
public boolean isMultipart(HttpServletRequest request) {
return (this.supportedMethods != null ?
this.supportedMethods.contains(request.getMethod()) &&
FileUploadBase.isMultipartContent(new ServletRequestContext(request)) :
ServletFileUpload.isMultipartContent(request));
}supportedMethods成員變量表示支持的請求方法,默認(rèn)為null,可以在初始化時指定。
當(dāng)supportedMethods為null時,即在默認(rèn)情況下,會調(diào)用ServletFileUpload.isMultipartContent()方法進(jìn)行判斷。此時文件請求的滿足條件為:
- 請求方法為
POST - 請求頭
Content-Type為以multipart/開頭
當(dāng)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成員變量也表示是否會馬上解析文件。
當(dāng)resolveLazily為false時,即默認(rèn)情況下,不會立即解析文件,只是會將原始請求進(jìn)行簡單封裝。只有在調(diào)用DefaultMultipartHttpServletRequest#getXxx方法時,會判斷文件是否已經(jīng)解析。如果沒有解析,會調(diào)用DefaultMultipartHttpServletRequest#initializeMultipart進(jìn)行解析。
當(dāng)resolveLazily為true時,會立即調(diào)用CommonsMultipartResolver#parseRequest方法進(jìn)行文件解析。
4.3 CommonsMultipartResolver#parseRequest
CommonsMultipartResolver#parseRequest方法會進(jìn)行文件請求解析,總的來說包括兩個步驟:
- 解析文件請求
- 封裝響應(yīng)
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request); return parseFileItems(fileItems, encoding);
深入閱讀源碼可以發(fā)現(xiàn),在解析文件請求時,會采用與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);
}
}
}到此這篇關(guān)于Spring MVC文件請求處理MultipartResolver詳解的文章就介紹到這了,更多相關(guān)Spring MVC文件請求內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot啟動時如何獲取端口和項(xiàng)目名
這篇文章主要介紹了springboot啟動時如何獲取端口和項(xiàng)目名,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
SpringCloud分布式項(xiàng)目下feign的使用示例詳解
這篇文章主要介紹了SpringCloud分布式項(xiàng)目下feign的使用,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-07-07
EasyExcel實(shí)現(xiàn)導(dǎo)入+各種數(shù)據(jù)校驗(yàn)功能
這篇文章主要介紹了EasyExcel實(shí)現(xiàn)導(dǎo)入+各種數(shù)據(jù)校驗(yàn),本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2023-05-05
spring boot實(shí)現(xiàn)profiles動態(tài)切換的示例
Spring Boot支持在不同的環(huán)境下使用不同的配置文件,該技術(shù)非常有利于持續(xù)集成,在構(gòu)建項(xiàng)目的時候只需要使用不同的構(gòu)建命令就可以生成不同運(yùn)行環(huán)境下war包,而不需要手動切換配置文件。2020-10-10
Mybatis基于TypeHandler實(shí)現(xiàn)敏感數(shù)據(jù)加密
業(yè)務(wù)場景中經(jīng)常會遇到諸如用戶手機(jī)號,身份證號,銀行卡號,郵箱,地址,密碼等等信息,屬于敏感信息,本文就來介紹一下Mybatis基于TypeHandler實(shí)現(xiàn)敏感數(shù)據(jù)加密,感興趣的可以了解一下2023-10-10
java實(shí)現(xiàn)文件夾上傳功能實(shí)例代碼(SpringBoot框架)
在web項(xiàng)目中上傳文件夾現(xiàn)在已經(jīng)成為了一個主流的需求,下面這篇文章主要給大家介紹了關(guān)于java實(shí)現(xiàn)文件夾上傳功能(springBoot框架)的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-04-04
IDEA利用jclasslib 修改class文件的實(shí)現(xiàn)
這篇文章主要介紹了IDEA利用jclasslib 修改class文件的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02
詳解Spring bean的注解注入之@Autowired的原理及使用
之前講過bean注入是什么,也使用了xml的配置文件進(jìn)行bean注入,這也是Spring的最原始的注入方式(xml注入).本文主要講解的注解有以下幾個:@Autowired、 @Service、@Repository、@Controller 、@Component、@Bean、@Configuration、@Resource ,需要的朋友可以參考下2021-06-06

