javascript
SpringBoot文件上传源码解析
一、SpringMVC文件上傳源碼分析前言(這部分我覺得原作者寫的很好)
該如何研究SpringMVC的文件上傳的源碼呢?
研究源碼并不是僅僅知道程序是怎樣運(yùn)行的,而應(yīng)該從宏觀的角度、不同的立場去看待問題。以SpringMVC文件上傳的源碼為例(麻雀雖小,五臟俱全),我們應(yīng)該從下面幾個(gè)方面去分析和研究:
- get or post ?
- 與其他字段如何共存 ?
- 多文件上傳?
- apache fileupload目的是想寫一個(gè)通用的解析文件上傳的jar包,可以供所有的java web框架來方便使用
- 它對外應(yīng)該提供哪些API來方便外界使用?它又需要外界的哪些參數(shù)?
- 哪些內(nèi)容應(yīng)該是它做的?
- 哪些內(nèi)容不應(yīng)該由它來做?
- 它不再重復(fù)造輪子,使用其他一些jar包即可
- 定義自己的方便用戶使用的接口,如MultipartFile、MultipartResolver,來屏蔽掉底層所使用的jar包
- 需要將底層jar包和自己的接口結(jié)合起來
二、apache fileupload源碼分析
先來看下含有文件上傳時(shí)的表單提交是怎樣的格式
form表單提交內(nèi)容如下
從上面可以看到,含有文件上傳的格式是這樣組織的。
- 文件類型字段
- 其他類型字段
結(jié)束
------WebKitFormBoundaryCvop2jTxU5F6lj6G--(分隔符加上--)對于上面的文件內(nèi)容,chrome瀏覽器是不顯示的,換成firefox可以看到,如下圖所示
同時(shí)我們還可以注意到,不同的瀏覽器,分隔符是不一樣的,在請求頭
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryCvop2jTxU5F6lj6G中指明了分隔符的內(nèi)容。
- 一定是post提交,如果換成get提交,則瀏覽器默認(rèn)僅僅把文件名作為屬性值來上傳,不會上傳文件內(nèi)容,如下
- form表單中一定不要忘了添加
否則的話,瀏覽器則不是按照上述的格式來來傳遞數(shù)據(jù)的。
上述兩點(diǎn)才能保證瀏覽器正常的進(jìn)行文件上傳。
有了上述文件上傳的組織格式,我們就需要合理的設(shè)計(jì)后臺的解析方式,下面來看下apache fileupload的使用。先來看下整體的流程圖 apache fileUpload整體流程圖
apache fileupload分Servlets and Portlets兩種情形來處理。Servlet我們很熟悉,而Portlets我也沒用過,可自行去搜索。
對于HttpServletRequest來說,另一個(gè)不再說明,自行查看源碼,判斷規(guī)則如下:
- 是否是post請求
- contentType是否以multipart/開頭
見源碼:
servlet的輸入?yún)?shù)為HttpServletRequest,Portlets的輸入?yún)?shù)為ActionRequest,數(shù)據(jù)來源不同,為了統(tǒng)一方便后面的數(shù)據(jù)處理,引入了RequestContext接口,來統(tǒng)一一下目標(biāo)數(shù)據(jù)的獲取。
接口RequestContext的實(shí)現(xiàn)類:
- ServletRequestContext
- PortletRequestContext
此時(shí)RequestContext就作為了數(shù)據(jù)源,不再與HttpServletRequest和ActionRequest打交道。
上述的實(shí)現(xiàn)過程是由FileUpload的子類ServletFileUpload和PortletFileUpload分別完成包裝的。
父類FileUpload的子類:
-
ServletFileUpload
-
PortletFileUpload
源碼展示如下: -
ServletFileUpload類
上述的parseRequest便完成了整個(gè)request的解析過程,內(nèi)容如下:
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 (Throwable e) {// ignore it}}}} }分以下兩個(gè)大步驟:
- 根據(jù)RequestContext數(shù)據(jù)源得到解析后的數(shù)據(jù)集合 FileItemIterator
- 遍歷FileItemIterator中的每個(gè)item,類型為FileItemStreamImpl,使用FileItemFactory工廠類來將每個(gè)FileItemStreamImpl轉(zhuǎn)化成最終的FileItem
- FileItemIterator內(nèi)容如下:
這就是一個(gè)輪詢器,可以假想成FileItemStream的集合,實(shí)際上不是,后面會進(jìn)行介紹
- FileItemStream則是之前上傳文件格式內(nèi)容
或者
------WebKitFormBoundary77tsMdWQBKrQOSsV Content-Disposition: form-data; name="myFile"; filename="萌芽.jpg" Content-Type: image/jpeg (文件內(nèi)容)的封裝,代碼如下
public interface FileItemStream extends FileItemHeadersSupport {/*流中包含了數(shù)值或者文件的內(nèi)容*/InputStream openStream() throws IOException;String getContentType();/*用來存放文件名,不是文件字段則為null*/String getName();/*對應(yīng)input標(biāo)簽中的name屬性*/String getFieldName();/*標(biāo)識該字段是否是一般的form字段還是文件字段*/boolean isFormField(); }然后我們來具體看下由RequestContext如何解析成一個(gè)FileItemIterator的:
public FileItemIterator getItemIterator(RequestContext ctx) throws FileUploadException, IOException {try {return new FileItemIteratorImpl(ctx);} catch (FileUploadIOException e) {// unwrap encapsulated SizeExceptionthrow (FileUploadException) e.getCause();} }new了一個(gè)FileItemIteratorImpl,來看下具體的過程:
FileItemIteratorImpl(RequestContext ctx)throws FileUploadException, IOException {if (ctx == null) {throw new NullPointerException("ctx parameter");}String contentType = ctx.getContentType();if ((null == contentType)|| (!contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART))) {throw new InvalidContentTypeException(format("the request doesn't contain a %s or %s stream, content type header is %s",MULTIPART_FORM_DATA, MULTIPART_MIXED, contentType));}InputStream input = ctx.getInputStream();@SuppressWarnings("deprecation") // still has to be backward compatiblefinal int contentLengthInt = ctx.getContentLength();final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass())// Inline conditional is OK here CHECKSTYLE:OFF? ((UploadContext) ctx).contentLength(): contentLengthInt;// CHECKSTYLE:ONif (sizeMax >= 0) {if (requestSize != -1 && requestSize > sizeMax) {throw new SizeLimitExceededException(format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",Long.valueOf(requestSize), Long.valueOf(sizeMax)),requestSize, sizeMax);}input = new LimitedInputStream(input, sizeMax) {@Overrideprotected void raiseError(long pSizeMax, long pCount)throws IOException {FileUploadException ex = new SizeLimitExceededException(format("the request was rejected because its size (%s) exceeds the configured maximum (%s)",Long.valueOf(pCount), Long.valueOf(pSizeMax)),pCount, pSizeMax);throw new FileUploadIOException(ex);}};}String charEncoding = headerEncoding;if (charEncoding == null) {charEncoding = ctx.getCharacterEncoding();}boundary = getBoundary(contentType);if (boundary == null) {throw new FileUploadException("the request was rejected because no multipart boundary was found");}notifier = new MultipartStream.ProgressNotifier(listener, requestSize);try {multi = new MultipartStream(input, boundary, notifier);} catch (IllegalArgumentException iae) {throw new InvalidContentTypeException(format("The boundary specified in the %s header is too long", CONTENT_TYPE), iae);}multi.setHeaderEncoding(charEncoding);skipPreamble = true;findNextItem();}- 要點(diǎn):
- contentType進(jìn)行判斷,是否以multipart開頭
- 判斷整個(gè)請求流的數(shù)據(jù)大小是否超過sizeMax最大設(shè)置
- 獲取重要的分隔符boundary信息
- 封裝了request請求流的數(shù)據(jù),包裝為MultipartStream類型
- 也可以設(shè)置通知器,來通知流的讀取進(jìn)度
這里可以看到FileItemIteratorImpl并不是FileItemStreamImpl的集合,其實(shí)是FileItemIteratorImpl內(nèi)部包含了一個(gè)FileItemStreamImpl屬性。FileItemIteratorImpl的一些重要屬性和方法如下:
/*總的數(shù)據(jù)流*/ private final MultipartStream multi; /*通知器*/ private final MultipartStream.ProgressNotifier notifier; /*分隔符*/ private final byte[] boundary; /*當(dāng)前已解析到的FileItemStreamImpl對象*/ private FileItemStreamImpl currentItem;public boolean hasNext() throws FileUploadException, IOException {if (eof) {return false;}if (itemValid) {return true;}try {return findNextItem();} catch (FileUploadIOException e) {// unwrap encapsulated SizeExceptionthrow (FileUploadException) e.getCause();} }public FileItemStream next() throws FileUploadException, IOException {if (eof || (!itemValid && !hasNext())) {throw new NoSuchElementException();}itemValid = false;return currentItem; }- findNextItem()方法就是創(chuàng)建新的FileItemStreamImpl來替代當(dāng)前的FileItemStreamImpl,并更新起始位置。
- 每次調(diào)用FileItemIteratorImpl的hasNext()方法,會創(chuàng)建一個(gè)新的FileItemStreamImpl賦值給FileItemStreamImpl屬性
- 每次調(diào)用FileItemIteratorImpl的next()方法,就會返回當(dāng)前FileItemStreamImpl屬性的值
- 創(chuàng)建的每個(gè)FileItemStreamImpl都會共享FileItemIteratorImpl的MultipartStream總流,僅僅更新了要讀取的起始位置
其他應(yīng)用其實(shí)就可以遍歷FileItemIteratorImpl拿到每一項(xiàng)FileItemStreamImpl的解析數(shù)據(jù)了。只是這時(shí)候數(shù)據(jù)
- 存儲在內(nèi)存中的
- 每個(gè)FileItemStreamImpl都是共享一個(gè)總的流,不能被重復(fù)讀取
我們想把這些文件數(shù)據(jù)存在臨時(shí)文件中,就需要使用使用FileItemFactory來進(jìn)行下轉(zhuǎn)化成FileItem。每個(gè)FileItem才是相互獨(dú)立的,而FileItemStreamImpl則不是,每個(gè)FileItem也是對應(yīng)上傳文件格式中的每一項(xiàng),如下
InputStream getInputStream() throws IOException; String getContentType(); String getName(); String getFieldName(); boolean isFormField();FileItemFactory的實(shí)現(xiàn)類DiskFileItemFactory即將數(shù)據(jù)存儲在硬盤上,代碼如下:
public static final int DEFAULT_SIZE_THRESHOLD = 10240; /*制定了臨時(shí)文件的目錄*/ private File repository; /*當(dāng)數(shù)據(jù)小于該閾值時(shí)存儲到內(nèi)存中,超過時(shí)存儲到臨時(shí)文件中*/ private int sizeThreshold = DEFAULT_SIZE_THRESHOLD; public FileItem createItem(String fieldName, String contentType,boolean isFormField, String fileName) {DiskFileItem result = new DiskFileItem(fieldName, contentType,isFormField, fileName, sizeThreshold, repository);FileCleaningTracker tracker = getFileCleaningTracker();if (tracker != null) {tracker.track(result.getTempFile(), result);}return result; }我們從上面可以看到,其實(shí)FileItemFactory的createItem方法,并沒有為FileItem的流賦值。再回顧下上文parseRequest方法的源代碼,賦值發(fā)生在這里
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 {/*這里才是為每一個(gè)FileItem的流賦值*/Streams.copy(item.openStream(), fileItem.getOutputStream(), true);} catch (FileUploadIOException e) {throw (FileUploadException) e.getCause();} catch (IOException e) {throw new IOFileUploadException(format("Processing of %s request failed. %s",MULTIPART_FORM_DATA, e.getMessage()), e);}final FileItemHeaders fih = item.getHeaders();fileItem.setHeaders(fih); }上述FileItem的openStream()方法如下:
public OutputStream getOutputStream()throws IOException {if (dfos == null) {File outputFile = getTempFile();dfos = new DeferredFileOutputStream(sizeThreshold, outputFile);}return dfos; }protected File getTempFile() {if (tempFile == null) {File tempDir = repository;if (tempDir == null) {tempDir = new File(System.getProperty("java.io.tmpdir"));}String tempFileName = format("upload_%s_%s.tmp", UID, getUniqueId());tempFile = new File(tempDir, tempFileName);}return tempFile; }getTempFile()會根據(jù)FileItemFactory的臨時(shí)文件目錄配置repository,創(chuàng)建一個(gè)臨時(shí)文件,用于上傳文件。 這里又用到了commons-io包中的DeferredFileOutputStream類。
- 當(dāng)數(shù)據(jù)數(shù)量小于sizeThreshold閾值時(shí),存儲在內(nèi)存中
- 當(dāng)數(shù)據(jù)數(shù)量大于sizeThreshold閾值時(shí),存儲到傳入的臨時(shí)文件中
至此,FileItem都被創(chuàng)建出來了,整個(gè)過程就結(jié)束了。
三、springboot文件上傳前言
- 相關(guān)的配置設(shè)置在MultipartProperties中,其中字段就是對應(yīng)的屬性設(shè)置,經(jīng)典字段有:
- enabled:是否開啟文件上傳自動配置,默認(rèn)開啟。
- location:上傳文件的臨時(shí)目錄。
- maxFileSize:最大文件大小,以字節(jié)為單位,默認(rèn)為1M。
- maxRequestSize:整個(gè)請求的最大容量,默認(rèn)為10M。
- fileSizeThreshold:文件大小達(dá)到該閾值,將寫入臨時(shí)目錄,默認(rèn)為0,即所有文件都會直接寫入磁盤臨時(shí)文件中。
- resolveLazily:是否惰性處理請求,默認(rèn)為false。
整體的包結(jié)構(gòu) 首先看下整體的包的結(jié)構(gòu),如下圖
總共分成3大塊,分別如下
- 一、org.springframework.web.multipart
存放Spring定義的文件上傳接口以及異常,如
- MultipartException對用戶拋出的解析異常(隱藏底層文件上傳解析包所拋出的異常)
也就指明了,這個(gè)體系下只能拋出這種類型的異常,MaxUploadSizeExceededException是MultipartException它的子類,專門用于指定文件大小限制的異常。用戶不應(yīng)該看到底層文件上傳解析包所拋出的異常,底層采用的文件上傳解析包在解析文件上傳時(shí)也會定義自己的解析異常,這時(shí)候就需要在整合這些jar包時(shí),需要對解析包所拋出的異常進(jìn)行轉(zhuǎn)換成上述已統(tǒng)一定義的面向用戶的異常
源碼見證下:
protected MultipartParsingResult parseRequest(HttpServletRequest request) throws MultipartException {String encoding = determineEncoding(request);FileUpload fileUpload = prepareFileUpload(encoding);try {List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request);return parseFileItems(fileItems, encoding);}catch (FileUploadBase.SizeLimitExceededException ex) {throw new MaxUploadSizeExceededException(fileUpload.getSizeMax(), ex);}catch (FileUploadException ex) {throw new MultipartException("Could not parse multipart servlet request", ex);} }FileUploadBase.SizeLimitExceededException、FileUploadException 都是底層解析包apache fileupload解析時(shí)拋出的異常,在這里要進(jìn)行try catch 處理,然后將這些異常轉(zhuǎn)化成SpringMVC自定義的異常MaxUploadSizeExceededException、MultipartException
- MultipartFile 定義了文件解析的統(tǒng)一結(jié)果類型
- MultipartResolver 定義了文件解析的處理器,不同的處理器不同的解析方式
- 二、org.springframework.web.multipart.commons
用于整合apache fileupload的解析,對上述定義的接口進(jìn)行實(shí)現(xiàn),如
- CommonsMultipartFile實(shí)現(xiàn)上述MultipartFile接口,即采用apache fileupload解析的結(jié)果為CommonsMultipartFile
- CommonsMultipartResolver實(shí)現(xiàn)上述MultipartResolver,
- 三、org.springframework.web.multipart.support
用于整合j2ee自帶的文件上傳的解析,對上述定義的接口進(jìn)行實(shí)現(xiàn),如
- StandardMultipartFile實(shí)現(xiàn)上述MultipartFile接口,即采用這種方式解析的結(jié)果為StandardMultipartFile
- StandardServletMultipartResolver實(shí)現(xiàn)上述MultipartResolver
接下來詳細(xì)看看這些源碼內(nèi)容
- 一、MultipartResolver接口的內(nèi)容:
- 二、MultipartHttpServletRequest接口內(nèi)容:
MultipartHttpServletRequest 繼承了 HttpServletRequest 和 MultipartRequest,然后就具有了下面的兩個(gè)主要功能
獲取文件上傳的每一部分的請求頭信息
HttpHeaders getRequestHeaders(); HttpHeaders getMultipartHeaders(String paramOrFileName);這里的請求頭信息就是如下內(nèi)容中的 Content-Disposition: form-data; name=“myFile”; filename=“資產(chǎn)型號規(guī)格模板1.xlsx” Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 等信息
獲取文件上傳的文件內(nèi)容(每個(gè)文件信息都是MultipartFile類型)
Iterator<String> getFileNames(); MultipartFile getFile(String name); List<MultipartFile> getFiles(String name); Map<String, MultipartFile> getFileMap();四、文件上傳的核心流程
在SpringMVC的入口類DispatcherServlet中的doDispatch方法中,可以看到是如下的處理流程
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;boolean multipartRequestParsed = false;try {//略//步驟一processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);//略}catch (Exception ex) {triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}finally {//略// Clean up any resources used by a multipart request.//步驟二if (multipartRequestParsed) {cleanupMultipart(processedRequest);}} }可以看到這里主要有兩個(gè)步驟
- 步驟一 檢查是否是文件上傳類型,如果是則進(jìn)行解析,然后將HttpServletRequest request封裝成MultipartHttpServletRequest
- 步驟二 如果是文件上傳,則進(jìn)行資源清理,如刪除上傳的臨時(shí)文件等
下面分別來說
- 首先看看DispatcherServlet的multipartResolver屬性是否有值,這個(gè)在初始化注入九大組件的時(shí)候就已經(jīng)初始化好了:
當(dāng)multipartResolver屬性有值的時(shí)候,先調(diào)用它的boolean isMultipart(HttpServletRequest request)方法,判斷當(dāng)前的request是否是符合文件上傳類型,如果符合則調(diào)用它的MultipartHttpServletRequest resolveMultipart(HttpServletRequest request)方法將當(dāng)前的request進(jìn)行解析并且封裝成MultipartHttpServletRequest類型。有了MultipartHttpServletRequest,我們就能獲取上傳的文件信息了。
然后我們就可以通過2種途徑來獲取上傳的文件。
- 途徑1 直接使用MultipartHttpServletRequest request作為參數(shù),如下
- 途徑2 使用@RequestParam(“myFile”) 來獲取文件(RequestParam里面的"myFile"是input標(biāo)簽的name的值而不是文件名),如下
對于途徑1很好理解,對于途徑2,為什么呢?
這里簡單提下,對于@RequestParam注解是由RequestParamMethodArgumentResolver來進(jìn)行處理的,是它進(jìn)行了特殊處理,當(dāng)@RequestParam修飾的類型為MultipartFile或者javax.servlet.http.Part(后面再詳細(xì)說此Part)時(shí)進(jìn)行特殊處理,如下
@Override protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest webRequest) throws Exception {Object arg;HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);MultipartHttpServletRequest multipartRequest =WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);if (MultipartFile.class.equals(parameter.getParameterType())) {assertIsMultipartRequest(servletRequest);Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");arg = multipartRequest.getFile(name);}else if (isMultipartFileCollection(parameter)) {assertIsMultipartRequest(servletRequest);Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");arg = multipartRequest.getFiles(name);}else if(isMultipartFileArray(parameter)) {assertIsMultipartRequest(servletRequest);Assert.notNull(multipartRequest, "Expected MultipartHttpServletRequest: is a MultipartResolver configured?");arg = multipartRequest.getFiles(name).toArray(new MultipartFile[0]);}else if ("javax.servlet.http.Part".equals(parameter.getParameterType().getName())) {assertIsMultipartRequest(servletRequest);arg = servletRequest.getPart(name);}else if (isPartCollection(parameter)) {assertIsMultipartRequest(servletRequest);arg = new ArrayList<Object>(servletRequest.getParts());}else if (isPartArray(parameter)) {assertIsMultipartRequest(servletRequest);arg = RequestPartResolver.resolvePart(servletRequest);}else {arg = null;if (multipartRequest != null) {List<MultipartFile> files = multipartRequest.getFiles(name);if (!files.isEmpty()) {arg = (files.size() == 1 ? files.get(0) : files);}}if (arg == null) {String[] paramValues = webRequest.getParameterValues(name);if (paramValues != null) {arg = paramValues.length == 1 ? paramValues[0] : paramValues;}}}return arg; }我們這里可以看到,其實(shí)也是通過MultipartHttpServletRequest的getFile等方法來獲取的,同時(shí)支持?jǐn)?shù)組、集合形式的參數(shù)
這里其實(shí)就是調(diào)用MultipartResolver接口的void cleanupMultipart(MultipartHttpServletRequest request)方法
至此SpringMVC已經(jīng)完成了自己的文件上傳框架體系,即底層不管采用何種文件解析包都是走這樣的一個(gè)流程。這樣的一個(gè)流程其實(shí)就是對實(shí)際業(yè)務(wù)的抽象過程。我們在寫代碼的時(shí)候,經(jīng)常就缺少抽象的能力,即很少抽象出各種業(yè)務(wù)邏輯的共同點(diǎn)。
五、整合apache fileupload對文件上傳的解析
剛才說了整個(gè)文件上傳的處理流程,然后我們就來看下apache fileupload是如何整合進(jìn)來的。即CommonsMultipartResolver是如何實(shí)現(xiàn)的
這里就是使用apache fileupload自己的ServletFileUpload.isMultipartContent判斷方法。
這里我們可以再多想一下,功能的職責(zé)劃分問題(雖然問題很簡單,主要是想引導(dǎo)大家在寫代碼的時(shí)候多去思考)。
因?yàn)槟壳芭袛嘁粋€(gè)request是否是multipart形式,都是一樣的,不管你是哪種解析包,為什么SpringMVC不統(tǒng)一進(jìn)行判斷,而是采用解析包的判斷?
如果SpringMVC自己進(jìn)行統(tǒng)一的判斷,似乎也沒什么問題。站在apache fileupload的角度來說,判斷request是否是multipart形式 的確應(yīng)該是它的一個(gè)功能,而不是等待外界來判斷。
SpringMVC既然采用第三方的解析包,就要遵守人家解析包的判斷邏輯,而不是自行判斷,雖然他們目前的判斷邏輯是一樣的。萬一后來又出來一個(gè)解析包,判斷邏輯不一樣呢?如果流程體系還是采用SpringMVC自己的判斷,可能就沒法正常解析了
一旦上述判斷通過了,則就需要執(zhí)行解析過程(可以立即解析,也可以延遲解析),看下具體的解析過程
public MultipartHttpServletRequest resolveMultipart(final HttpServletRequest request) throws MultipartException {Assert.notNull(request, "Request must not be null");if (this.resolveLazily) {return new DefaultMultipartHttpServletRequest(request) {@Overrideprotected 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());} }這里大致說下過程,詳細(xì)的內(nèi)容去看源代碼。
使用apache fileupload的ServletFileUpload對request進(jìn)行解析,解析結(jié)果為List,代碼如下:
List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request); FileItem為apache fileupload自己的解析結(jié)果,需要轉(zhuǎn)化為SpringMVC自己定義的MultipartFileprotected MultipartParsingResult parseFileItems(List<FileItem> fileItems, String encoding) {MultiValueMap<String, MultipartFile> multipartFiles = new LinkedMultiValueMap<String,MultipartFile>();Map<String, String[]> multipartParameters = new HashMap<String, String[]>();Map<String, String> multipartParameterContentTypes = new HashMap<String, String>();// Extract multipart files and multipart parameters.for (FileItem fileItem : fileItems) {if (fileItem.isFormField()) {String value;String partEncoding = determineEncoding(fileItem.getContentType(), encoding);if (partEncoding != null) {try {value = fileItem.getString(partEncoding);}catch (UnsupportedEncodingException ex) {value = fileItem.getString();}}else {value = fileItem.getString();}String[] curParam = multipartParameters.get(fileItem.getFieldName());if (curParam == null) {// simple form fieldmultipartParameters.put(fileItem.getFieldName(), new String[] {value});}else {// array of simple form fieldsString[] newParam = StringUtils.addStringToArray(curParam, value);multipartParameters.put(fileItem.getFieldName(), newParam);}multipartParameterContentTypes.put(fileItem.getFieldName(), fileItem.getContentType());}else {// multipart file fieldCommonsMultipartFile file = new CommonsMultipartFile(fileItem);multipartFiles.add(file.getName(), file);}}return new MultipartParsingResult(multipartFiles, multipartParameters, multipartParameterContentTypes); }這里有普通字段的處理和文件字段的處理。還記得上文講的org.springframework.web.multipart.commons包的CommonsMultipartFile嗎?可以看到通過new CommonsMultipartFile(fileItem),就將FileItem結(jié)果轉(zhuǎn)化為了MultipartFile結(jié)果。至此就將HttpServletRequest解析成了DefaultMultipartHttpServletRequest,所以我們在使用request時(shí),它的類型其實(shí)就是DefaultMultipartHttpServletRequest類型,我們可以通過它來獲取各種上傳的文件信息。
其實(shí)就是對所有的CommonsMultipartFile中的FileItem進(jìn)行刪除臨時(shí)文件的操作,這個(gè)刪除操作是apache fileupload自己定義的,如下
protected void cleanupFileItems(MultiValueMap<String, MultipartFile> multipartFiles) {for (List<MultipartFile> files : multipartFiles.values()) {for (MultipartFile file : files) {if (file instanceof CommonsMultipartFile) {CommonsMultipartFile cmf = (CommonsMultipartFile) file;cmf.getFileItem().delete();}}} }至此,SpringMVC與apache fileupload的整合完成了,其他的整合也是類似的操作。
六、流程簡單總結(jié)
? 自動配置好了 StandardServletMultipartResolver 【文件上傳解析器】
? 原理步驟
- 1、請求進(jìn)來使用文件上傳解析器判斷(isMultipart)并封裝(resolveMultipart,返回MultipartHttpServletRequest)文件上傳請求
- 2、參數(shù)解析器來解析請求中的文件內(nèi)容封裝成MultipartFile
- 3、將request中文件信息封裝為一個(gè)Map;MultiValueMap<String, MultipartFile>
FileCopyUtils。實(shí)現(xiàn)文件流的拷貝
文章轉(zhuǎn)自
總結(jié)
以上是生活随笔為你收集整理的SpringBoot文件上传源码解析的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 2021抖音私域经营白皮书
- 下一篇: 投递简历得不到回复,并不是你的简历不好,