日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > javascript >内容正文

javascript

SpringBoot文件上传源码解析

發布時間:2025/3/15 javascript 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 SpringBoot文件上传源码解析 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

一、SpringMVC文件上傳源碼分析前言(這部分我覺得原作者寫的很好)

該如何研究SpringMVC的文件上傳的源碼呢?

研究源碼并不是僅僅知道程序是怎樣運行的,而應該從宏觀的角度、不同的立場去看待問題。以SpringMVC文件上傳的源碼為例(麻雀雖小,五臟俱全),我們應該從下面幾個方面去分析和研究:

  • 文件上傳的基本規則:以什么樣的格式來傳輸數據?
    • get or post ?
    • 與其他字段如何共存 ?
    • 多文件上傳?
  • 站在apache fileupload的角度
    • apache fileupload目的是想寫一個通用的解析文件上傳的jar包,可以供所有的java web框架來方便使用
    • 它對外應該提供哪些API來方便外界使用?它又需要外界的哪些參數?
    • 哪些內容應該是它做的?
    • 哪些內容不應該由它來做?
  • 站在SpringMVC框架的角度
    • 它不再重復造輪子,使用其他一些jar包即可
    • 定義自己的方便用戶使用的接口,如MultipartFile、MultipartResolver,來屏蔽掉底層所使用的jar包
    • 需要將底層jar包和自己的接口結合起來
  • 如果我們都以上述方式要求自己,則源碼分析才更有價值和意義,不然我們永遠就只能處在類與類跳轉的迷霧中
  • 二、apache fileupload源碼分析

  • 文件上傳格式
    先來看下含有文件上傳時的表單提交是怎樣的格式
  • <form action="/upload/request" method="POST" enctype="multipart/form-data" id="requestForm"><input type="file" name="myFile"><input type="text" name="user"><input type="text" name="password"><input type="submit" value="提交"> </form>

    form表單提交內容如下

    從上面可以看到,含有文件上傳的格式是這樣組織的。

    • 文件類型字段
    ------WebKitFormBoundaryCvop2jTxU5F6lj6G(分隔符) Content-Disposition: form-data; name="myFile"; filename="資產型號規格模板1.xlsx" Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet (換行) (文件內容)
    • 其他類型字段
    ------WebKitFormBoundaryCvop2jTxU5F6lj6G(分隔符) Content-Disposition: form-data; name="user" (換行) lg

    結束

    ------WebKitFormBoundaryCvop2jTxU5F6lj6G--(分隔符加上--)

    對于上面的文件內容,chrome瀏覽器是不顯示的,換成firefox可以看到,如下圖所示

    同時我們還可以注意到,不同的瀏覽器,分隔符是不一樣的,在請求頭

    Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryCvop2jTxU5F6lj6G

    中指明了分隔符的內容。

  • 文件上傳注意點:
    • 一定是post提交,如果換成get提交,則瀏覽器默認僅僅把文件名作為屬性值來上傳,不會上傳文件內容,如下
    • form表單中一定不要忘了添加
    enctype="multipart/form-data"

    否則的話,瀏覽器則不是按照上述的格式來來傳遞數據的。

    上述兩點才能保證瀏覽器正常的進行文件上傳。

  • apache fileupload的解析
  • 有了上述文件上傳的組織格式,我們就需要合理的設計后臺的解析方式,下面來看下apache fileupload的使用。先來看下整體的流程圖 apache fileUpload整體流程圖

  • Servlets and Portlets
  • apache fileupload分Servlets and Portlets兩種情形來處理。Servlet我們很熟悉,而Portlets我也沒用過,可自行去搜索。

  • 判斷request是否是Multipart
  • 對于HttpServletRequest來說,另一個不再說明,自行查看源碼,判斷規則如下:

    • 是否是post請求
    • contentType是否以multipart/開頭
      見源碼:
    public static final boolean isMultipartContent(HttpServletRequest request) {if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {return false;}return FileUploadBase.isMultipartContent(new ServletRequestContext(request)); } public static final boolean isMultipartContent(RequestContext ctx) {String contentType = ctx.getContentType();if (contentType == null) {return false;}if (contentType.toLowerCase(Locale.ENGLISH).startsWith(MULTIPART)) {return true;}return false; }
  • 對request封裝成RequestContext
  • servlet的輸入參數為HttpServletRequest,Portlets的輸入參數為ActionRequest,數據來源不同,為了統一方便后面的數據處理,引入了RequestContext接口,來統一一下目標數據的獲取。

    接口RequestContext的實現類:

    • ServletRequestContext
    • PortletRequestContext
      此時RequestContext就作為了數據源,不再與HttpServletRequest和ActionRequest打交道。

    上述的實現過程是由FileUpload的子類ServletFileUpload和PortletFileUpload分別完成包裝的。

    父類FileUpload的子類:

    • ServletFileUpload

    • PortletFileUpload
      源碼展示如下:

    • ServletFileUpload類

    public List<FileItem> parseRequest(HttpServletRequest request)throws FileUploadException {return parseRequest(new ServletRequestContext(request)); }``` - PortletFileUpload類```public List<FileItem> parseRequest(ActionRequest request)throws FileUploadException {return parseRequest(new PortletRequestContext(request)); }

    上述的parseRequest便完成了整個request的解析過程,內容如下:

    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}}}} }

    分以下兩個大步驟:

    • 根據RequestContext數據源得到解析后的數據集合 FileItemIterator
    • 遍歷FileItemIterator中的每個item,類型為FileItemStreamImpl,使用FileItemFactory工廠類來將每個FileItemStreamImpl轉化成最終的FileItem
  • 由RequestContext數據源得到解析后的數據集合 FileItemIterator
    • FileItemIterator內容如下:
    public interface FileItemIterator {boolean hasNext() throws FileUploadException, IOException;FileItemStream next() throws FileUploadException, IOException; }

    這就是一個輪詢器,可以假想成FileItemStream的集合,實際上不是,后面會進行介紹

    • FileItemStream則是之前上傳文件格式內容
    ------WebKitFormBoundary77tsMdWQBKrQOSsV Content-Disposition: form-data; name="user"lg

    或者

    ------WebKitFormBoundary77tsMdWQBKrQOSsV Content-Disposition: form-data; name="myFile"; filename="萌芽.jpg" Content-Type: image/jpeg (文件內容)

    的封裝,代碼如下

    public interface FileItemStream extends FileItemHeadersSupport {/*流中包含了數值或者文件的內容*/InputStream openStream() throws IOException;String getContentType();/*用來存放文件名,不是文件字段則為null*/String getName();/*對應input標簽中的name屬性*/String getFieldName();/*標識該字段是否是一般的form字段還是文件字段*/boolean isFormField(); }

    然后我們來具體看下由RequestContext如何解析成一個FileItemIterator的:

    public FileItemIterator getItemIterator(RequestContext ctx) throws FileUploadException, IOException {try {return new FileItemIteratorImpl(ctx);} catch (FileUploadIOException e) {// unwrap encapsulated SizeExceptionthrow (FileUploadException) e.getCause();} }

    new了一個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();}
    • 要點:
    • contentType進行判斷,是否以multipart開頭
    • 判斷整個請求流的數據大小是否超過sizeMax最大設置
    • 獲取重要的分隔符boundary信息
    • 封裝了request請求流的數據,包裝為MultipartStream類型
    • 也可以設置通知器,來通知流的讀取進度

    這里可以看到FileItemIteratorImpl并不是FileItemStreamImpl的集合,其實是FileItemIteratorImpl內部包含了一個FileItemStreamImpl屬性。FileItemIteratorImpl的一些重要屬性和方法如下:

    /*總的數據流*/ private final MultipartStream multi; /*通知器*/ private final MultipartStream.ProgressNotifier notifier; /*分隔符*/ private final byte[] boundary; /*當前已解析到的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()方法就是創建新的FileItemStreamImpl來替代當前的FileItemStreamImpl,并更新起始位置。
    • 每次調用FileItemIteratorImpl的hasNext()方法,會創建一個新的FileItemStreamImpl賦值給FileItemStreamImpl屬性
    • 每次調用FileItemIteratorImpl的next()方法,就會返回當前FileItemStreamImpl屬性的值
    • 創建的每個FileItemStreamImpl都會共享FileItemIteratorImpl的MultipartStream總流,僅僅更新了要讀取的起始位置
  • 遍歷FileItemIterator,通過FileItemFactory工廠將每一個item轉化成FileItem對象
  • 其他應用其實就可以遍歷FileItemIteratorImpl拿到每一項FileItemStreamImpl的解析數據了。只是這時候數據

    • 存儲在內存中的
    • 每個FileItemStreamImpl都是共享一個總的流,不能被重復讀取

    我們想把這些文件數據存在臨時文件中,就需要使用使用FileItemFactory來進行下轉化成FileItem。每個FileItem才是相互獨立的,而FileItemStreamImpl則不是,每個FileItem也是對應上傳文件格式中的每一項,如下

    InputStream getInputStream() throws IOException; String getContentType(); String getName(); String getFieldName(); boolean isFormField();

    FileItemFactory的實現類DiskFileItemFactory即將數據存儲在硬盤上,代碼如下:

    public static final int DEFAULT_SIZE_THRESHOLD = 10240; /*制定了臨時文件的目錄*/ private File repository; /*當數據小于該閾值時存儲到內存中,超過時存儲到臨時文件中*/ 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; }

    我們從上面可以看到,其實FileItemFactory的createItem方法,并沒有為FileItem的流賦值。再回顧下上文parseRequest方法的源代碼,賦值發生在這里

    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 {/*這里才是為每一個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()會根據FileItemFactory的臨時文件目錄配置repository,創建一個臨時文件,用于上傳文件。 這里又用到了commons-io包中的DeferredFileOutputStream類。

    • 當數據數量小于sizeThreshold閾值時,存儲在內存中
    • 當數據數量大于sizeThreshold閾值時,存儲到傳入的臨時文件中

    至此,FileItem都被創建出來了,整個過程就結束了。

    三、springboot文件上傳前言

  • 首先文件上傳功能,springboot都將其實現封裝在了文件上傳配置類MultipartAutoConfiguration里面
  • @Configuration(proxyBeanMethods = false ) @ConditionalOnClass({Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class}) @ConditionalOnProperty(prefix = "spring.servlet.multipart",name = {"enabled"},matchIfMissing = true ) @ConditionalOnWebApplication(type = Type.SERVLET ) //能配置的都是以spring.servlet.multipart開頭,進入MultipartProperties,可以查看能夠配置的有哪些屬性 @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 = {"multipartResolver"})@ConditionalOnMissingBean({MultipartResolver.class})public StandardServletMultipartResolver multipartResolver() {StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());return multipartResolver;} }
    • 相關的配置設置在MultipartProperties中,其中字段就是對應的屬性設置,經典字段有:
    • enabled:是否開啟文件上傳自動配置,默認開啟。
    • location:上傳文件的臨時目錄。
    • maxFileSize:最大文件大小,以字節為單位,默認為1M。
    • maxRequestSize:整個請求的最大容量,默認為10M。
    • fileSizeThreshold:文件大小達到該閾值,將寫入臨時目錄,默認為0,即所有文件都會直接寫入磁盤臨時文件中。
    • resolveLazily:是否惰性處理請求,默認為false。
  • SpringMVC文件上傳接口設計與實現
    整體的包結構 首先看下整體的包的結構,如下圖
  • 總共分成3大塊,分別如下

    • 一、org.springframework.web.multipart

    存放Spring定義的文件上傳接口以及異常,如

    • MultipartException對用戶拋出的解析異常(隱藏底層文件上傳解析包所拋出的異常)
      也就指明了,這個體系下只能拋出這種類型的異常,MaxUploadSizeExceededException是MultipartException它的子類,專門用于指定文件大小限制的異常。用戶不應該看到底層文件上傳解析包所拋出的異常,底層采用的文件上傳解析包在解析文件上傳時也會定義自己的解析異常,這時候就需要在整合這些jar包時,需要對解析包所拋出的異常進行轉換成上述已統一定義的面向用戶的異常

    源碼見證下:

    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解析時拋出的異常,在這里要進行try catch 處理,然后將這些異常轉化成SpringMVC自定義的異常MaxUploadSizeExceededException、MultipartException

    • MultipartFile 定義了文件解析的統一結果類型
    • MultipartResolver 定義了文件解析的處理器,不同的處理器不同的解析方式
    • 二、org.springframework.web.multipart.commons

    用于整合apache fileupload的解析,對上述定義的接口進行實現,如

    • CommonsMultipartFile實現上述MultipartFile接口,即采用apache fileupload解析的結果為CommonsMultipartFile
    • CommonsMultipartResolver實現上述MultipartResolver,
    • 三、org.springframework.web.multipart.support

    用于整合j2ee自帶的文件上傳的解析,對上述定義的接口進行實現,如

    • StandardMultipartFile實現上述MultipartFile接口,即采用這種方式解析的結果為StandardMultipartFile
    • StandardServletMultipartResolver實現上述MultipartResolver

    接下來詳細看看這些源碼內容

  • SpringMVC自己的接口設計
    • 一、MultipartResolver接口的內容:
    public interface MultipartResolver {//判斷當前的HttpServletRequest是否是文件上傳類型boolean isMultipart(HttpServletRequest request);//將HttpServletRequest轉化成MultipartHttpServletRequestMultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException;//清除產生的臨時文件等void cleanupMultipart(MultipartHttpServletRequest request); }
    • 二、MultipartHttpServletRequest接口內容:

    MultipartHttpServletRequest 繼承了 HttpServletRequest 和 MultipartRequest,然后就具有了下面的兩個主要功能

    獲取文件上傳的每一部分的請求頭信息

    HttpHeaders getRequestHeaders(); HttpHeaders getMultipartHeaders(String paramOrFileName);

    這里的請求頭信息就是如下內容中的 Content-Disposition: form-data; name=“myFile”; filename=“資產型號規格模板1.xlsx” Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet 等信息

    獲取文件上傳的文件內容(每個文件信息都是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);}} }

    可以看到這里主要有兩個步驟

    • 步驟一 檢查是否是文件上傳類型,如果是則進行解析,然后將HttpServletRequest request封裝成MultipartHttpServletRequest
    • 步驟二 如果是文件上傳,則進行資源清理,如刪除上傳的臨時文件等
      下面分別來說
  • 判斷并解析HttpServletRequest成MultipartHttpServletRequest:
  • protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {if (request instanceof MultipartHttpServletRequest) {logger.debug("Request is already a MultipartHttpServletRequest - if not in a forward, " +"this typically results from an additional MultipartFilter in web.xml");}else if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) instanceof MultipartException) {logger.debug("Multipart resolution failed for current request before - " +"skipping re-resolution for undisturbed error rendering");}else {return this.multipartResolver.resolveMultipart(request);}}// If not returned before: return original request.return request; }
    • 首先看看DispatcherServlet的multipartResolver屬性是否有值,這個在初始化注入九大組件的時候就已經初始化好了:
    protected void onRefresh(ApplicationContext context) {this.initStrategies(context); } protected void initStrategies(ApplicationContext context) {this.initMultipartResolver(context);this.initLocaleResolver(context);this.initThemeResolver(context);this.initHandlerMappings(context);this.initHandlerAdapters(context);this.initHandlerExceptionResolvers(context);this.initRequestToViewNameTranslator(context);this.initViewResolvers(context);this.initFlashMapManager(context); }

    當multipartResolver屬性有值的時候,先調用它的boolean isMultipart(HttpServletRequest request)方法,判斷當前的request是否是符合文件上傳類型,如果符合則調用它的MultipartHttpServletRequest resolveMultipart(HttpServletRequest request)方法將當前的request進行解析并且封裝成MultipartHttpServletRequest類型。有了MultipartHttpServletRequest,我們就能獲取上傳的文件信息了。

    然后我們就可以通過2種途徑來獲取上傳的文件。

    • 途徑1 直接使用MultipartHttpServletRequest request作為參數,如下
    @RequestMapping(value="/test/file",method=RequestMethod.POST) @ResponseBody public String fileUpload(MultipartHttpServletRequest request){Map<String, MultipartFile> files=request.getFileMap();//使用filesreturn "success"; }
    • 途徑2 使用@RequestParam(“myFile”) 來獲取文件(RequestParam里面的"myFile"是input標簽的name的值而不是文件名),如下
    @RequestMapping(value="/test/file",method=RequestMethod.POST) @ResponseBody public String fileUpload(@RequestParam("myFile") MultipartFile file){//使用filereturn "success"; }

    對于途徑1很好理解,對于途徑2,為什么呢?

    這里簡單提下,對于@RequestParam注解是由RequestParamMethodArgumentResolver來進行處理的,是它進行了特殊處理,當@RequestParam修飾的類型為MultipartFile或者javax.servlet.http.Part(后面再詳細說此Part)時進行特殊處理,如下

    @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; }

    我們這里可以看到,其實也是通過MultipartHttpServletRequest的getFile等方法來獲取的,同時支持數組、集合形式的參數

  • 清理占用的資源,如臨時文件
  • protected void cleanupMultipart(HttpServletRequest servletRequest) {MultipartHttpServletRequest req = WebUtils.getNativeRequest(servletRequest, MultipartHttpServletRequest.class);if (req != null) {this.multipartResolver.cleanupMultipart(req);} }

    這里其實就是調用MultipartResolver接口的void cleanupMultipart(MultipartHttpServletRequest request)方法

    至此SpringMVC已經完成了自己的文件上傳框架體系,即底層不管采用何種文件解析包都是走這樣的一個流程。這樣的一個流程其實就是對實際業務的抽象過程。我們在寫代碼的時候,經常就缺少抽象的能力,即很少抽象出各種業務邏輯的共同點。

    五、整合apache fileupload對文件上傳的解析

    剛才說了整個文件上傳的處理流程,然后我們就來看下apache fileupload是如何整合進來的。即CommonsMultipartResolver是如何實現的

  • 判斷一個request是否是multipart形式的
  • @Override public boolean isMultipart(HttpServletRequest request) {return (request != null && ServletFileUpload.isMultipartContent(request)); }

    這里就是使用apache fileupload自己的ServletFileUpload.isMultipartContent判斷方法。

    這里我們可以再多想一下,功能的職責劃分問題(雖然問題很簡單,主要是想引導大家在寫代碼的時候多去思考)。

    因為目前判斷一個request是否是multipart形式,都是一樣的,不管你是哪種解析包,為什么SpringMVC不統一進行判斷,而是采用解析包的判斷?

    如果SpringMVC自己進行統一的判斷,似乎也沒什么問題。站在apache fileupload的角度來說,判斷request是否是multipart形式 的確應該是它的一個功能,而不是等待外界來判斷。

    SpringMVC既然采用第三方的解析包,就要遵守人家解析包的判斷邏輯,而不是自行判斷,雖然他們目前的判斷邏輯是一樣的。萬一后來又出來一個解析包,判斷邏輯不一樣呢?如果流程體系還是采用SpringMVC自己的判斷,可能就沒法正常解析了

  • 將HttpServletRequest解析成DefaultMultipartHttpServletRequest
  • 一旦上述判斷通過了,則就需要執行解析過程(可以立即解析,也可以延遲解析),看下具體的解析過程

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

    這里大致說下過程,詳細的內容去看源代碼。

    使用apache fileupload的ServletFileUpload對request進行解析,解析結果為List,代碼如下:

    List<FileItem> fileItems = ((ServletFileUpload) fileUpload).parseRequest(request); FileItem為apache fileupload自己的解析結果,需要轉化為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結果轉化為了MultipartFile結果。至此就將HttpServletRequest解析成了DefaultMultipartHttpServletRequest,所以我們在使用request時,它的類型其實就是DefaultMultipartHttpServletRequest類型,我們可以通過它來獲取各種上傳的文件信息。

  • 清理臨時文件
  • 其實就是對所有的CommonsMultipartFile中的FileItem進行刪除臨時文件的操作,這個刪除操作是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的整合完成了,其他的整合也是類似的操作。

    六、流程簡單總結

  • 文件上傳自動配置類-MultipartAutoConfiguration-MultipartProperties
    ? 自動配置好了 StandardServletMultipartResolver 【文件上傳解析器】
    ? 原理步驟
    • 1、請求進來使用文件上傳解析器判斷(isMultipart)并封裝(resolveMultipart,返回MultipartHttpServletRequest)文件上傳請求
    • 2、參數解析器來解析請求中的文件內容封裝成MultipartFile
    • 3、將request中文件信息封裝為一個Map;MultiValueMap<String, MultipartFile>
      FileCopyUtils。實現文件流的拷貝

    文章轉自

    總結

    以上是生活随笔為你收集整理的SpringBoot文件上传源码解析的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。