#文件上傳格式 先來看下含有文件上傳時的表單提交是怎樣的格式html
<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表單提交內容以下java
從上面能夠看到,含有文件上傳的格式是這樣組織的。chrome
文件類型字段apache
------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
結束app
------WebKitFormBoundaryCvop2jTxU5F6lj6G--(分隔符加上--)
對於上面的文件內容,chrome瀏覽器是不顯示的,換成firefox能夠看到,以下圖所示框架
同時咱們還能夠注意到,不一樣的瀏覽器,分隔符是不同的,在請求頭ide
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryCvop2jTxU5F6lj6G
中指明瞭分隔符的內容。post
更加詳細的上傳格式仍是參看 rfc文檔firefox
#文件上傳注意點:
必定是post提交,若是換成get提交,則瀏覽器默認僅僅把文件名做爲屬性值來上傳,不會上傳文件內容,以下
form表單中必定不要忘了添加
enctype="multipart/form-data"
不然的話,瀏覽器則不是按照上述的格式來來傳遞數據的。
上述兩點才能保證瀏覽器正常的進行文件上傳。
#apache fileupload的解析
參見官方文檔: 官方文檔
有了上述文件上傳的組織格式,咱們就須要合理的設計後臺的解析方式,下面來看下apache fileupload的使用。先來看下總體的流程圖
##Servlets and Portlets
apache fileupload分Servlets and Portlets兩種情形來處理。Servlet咱們很熟悉,而Portlets我也沒用過,可自行去搜索。
##判斷request是不是Multipart
對於HttpServletRequest來講,另外一個再也不說明,自行查看源碼,判斷規則以下:
見源碼:
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的實現類:
此時RequestContext就做爲了數據源,再也不與HttpServletRequest和ActionRequest打交道。
上述的實現過程是由FileUpload的子類ServletFileUpload和PortletFileUpload分別完成包裝的。
父類FileUpload的子類:
源碼展現以下:
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內容以下:
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 SizeException throw (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 compatible final int contentLengthInt = ctx.getContentLength(); final long requestSize = UploadContext.class.isAssignableFrom(ctx.getClass()) // Inline conditional is OK here CHECKSTYLE:OFF ? ((UploadContext) ctx).contentLength() : contentLengthInt; // CHECKSTYLE:ON if (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) { @Override protected 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(); }
要點:
這裏能夠看到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 SizeException throw (FileUploadException) e.getCause(); } } public FileItemStream next() throws FileUploadException, IOException { if (eof || (!itemValid && !hasNext())) { throw new NoSuchElementException(); } itemValid = false; return currentItem; }
##遍歷FileItemIterator,經過FileItemFactory工廠將每個item轉化成FileItem對象
其餘應用其實就能夠遍歷FileItemIteratorImpl拿到每一項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類。
至此,FileItem都被建立出來了,整個過程就結束了。
#結束語
這篇文章完成了上一篇文章的前兩個部分,接下來就是SpringMVC本身如何將上述功能加入到本身的框架中來。