Spring?MVC文件請求處理MultipartResolver詳解
org.springframework.web.multipart.MultipartResolver
是Spring-Web針對RFC1867實現(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));
實際會發(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ī)范實現(xiàn)的多文件上傳的策略接口。
同時,MultipartResolver
是Spring對文件上傳處理流程在接口層次的抽象。
也就是說,當(dāng)涉及到文件上傳時,Spring都會使用MultipartResolver接口進行處理,而不涉及具體實現(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
實現(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
不會對文件上傳請求進行處理。也就是說,盡管當(dāng)前請求是文件請求,也不會被處理成MultipartHttpServletRequest
,如果我們在控制層進行強制類型轉(zhuǎn)換,會拋異常。
DispatcherServlet
在處理業(yè)務(wù)時,會按照順序分別調(diào)用這些方法進行文件上傳處理,相關(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()
方法中,會進行判斷、封裝文件請求:
/** * 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
對象進行業(yè)務(wù)處理- 業(yè)務(wù)處理完成,清除文件上傳產(chǎn)生的臨時資源
2.3 MultipartResolver實現(xiàn)類&配置方式
Spring提供了兩個MultipartResolver
實現(xiàn)類:
org.springframework.web.multipart.support.StandardServletMultipartResolver
:根據(jù)Servlet 3.0+ Part Api實現(xiàn)org.springframework.web.multipart.commons.CommonsMultipartResolver
:根據(jù)Apache Commons FileUpload實現(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ì)介紹兩種實現(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)格程度。如果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()
方法會對請求進行實際解析,該方法會完成兩件事情:
- 使用Servlet 3.0的
Part
API,獲取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()
階段并不會進行文件請求解析。也就是說,此時StandardMultipartHttpServletRequest
對象的成員變量都是空值。那么,resolveLazily
為false
時文件請求解析是在什么時候完成的呢?
實際上,在調(diào)用StandardMultipartHttpServletRequest
接口的getXxx()
方法時,內(nèi)部會判斷是否已經(jīng)完成文件請求解析。如果未解析,就會調(diào)用partRequest()
方法進行解析,例如:
@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實現(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; }
所有實現(xiàn)標(biāo)準(zhǔn)Servlet 3.0規(guī)范的Web服務(wù)器,都必須實現(xiàn)getPart()
/getParts()
方法。也就是說,這些Web服務(wù)器在解析請求時,會將multipart/form-data
請求體中的表單數(shù)據(jù)或文件解析成Part
對象集合。通過HttpServletRequest
的getPart()
/getParts()
方法,可以獲取這些Part
對象,進而獲取multipart/form-data
請求體中的表單數(shù)據(jù)或文件。
每個Web服務(wù)器對Servlet 3.0規(guī)范都有自己的實現(xiàn)方式。對于Spring Boot來說,通常使用的是Tomcat/Undertow/Jetty內(nèi)嵌Web服務(wù)器。通常只需要了解這三種服務(wù)器的實現(xiàn)方式即可。
3.4.1 Tomcat實現(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
會進行實際解析文件請求:
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ù)中獲取到的文件實際上都來自服務(wù)器本地臨時文件。
3.4.2 Undertow實現(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
方法會進行實際解析文件請求,核心代碼如下:
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ù)中獲取到的文件實際上都來自服務(wù)器本地臨時文件。
3.4.2 Jetty實現(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
則會進行文件請求解析:
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ù)中獲取到的文件實際上都來自服務(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()
方法進行判斷。此時文件請求的滿足條件為:
- 請求方法為
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)情況下,不會立即解析文件,只是會將原始請求進行簡單封裝。只有在調(diào)用DefaultMultipartHttpServletRequest#getXxx
方法時,會判斷文件是否已經(jīng)解析。如果沒有解析,會調(diào)用DefaultMultipartHttpServletRequest#initializeMultipart
進行解析。
當(dāng)resolveLazily
為true
時,會立即調(diào)用CommonsMultipartResolver#parseRequest
方法進行文件解析。
4.3 CommonsMultipartResolver#parseRequest
CommonsMultipartResolver#parseRequest
方法會進行文件請求解析,總的來說包括兩個步驟:
- 解析文件請求
- 封裝響應(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)文章
Java中的BlockingQueue阻塞隊列原理以及實現(xiàn)詳解
這篇文章主要介紹了Java中的BlockingQueue阻塞隊列原理以及實現(xiàn)詳解,在最常見的使用到這個阻塞隊列的地方,就是我們耳熟能詳?shù)木€程池里面了,作為我們線程池的一大最大參與者,也是AQS的一個具體實現(xiàn),需要的朋友可以參考下2023-12-12Spring Cloud實現(xiàn)提供API給客戶端的方法詳解
這篇文章主要給大家介紹了關(guān)于Spring Cloud實現(xiàn)提供API給客戶端的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01Java獲取兩個集合List的交集、補集、并集(相加)和差集(相減)的不同方式
這篇文章主要給大家介紹了關(guān)于Java獲取兩個集合List的交集、補集、并集(相加)和差集(相減)的不同方式,在一般操作中對于list集合取交集、差集、并集,比較簡單,需要的朋友可以參考下2023-08-08