apache fileupload源碼分析

#文件上傳格式 先來看下含有文件上傳時的表單提交是怎樣的格式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文件上傳

從上面能夠看到,含有文件上傳的格式是這樣組織的。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能夠看到,以下圖所示框架

chrome文件上傳

同時咱們還能夠注意到,不一樣的瀏覽器,分隔符是不同的,在請求頭ide

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

中指明瞭分隔符的內容。post

更加詳細的上傳格式仍是參看 rfc文檔firefox

#文件上傳注意點:

  • 必定是post提交,若是換成get提交,則瀏覽器默認僅僅把文件名做爲屬性值來上傳,不會上傳文件內容,以下

    文件上傳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 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();
    }

要點:

  • 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 SizeException
        throw (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都被建立出來了,整個過程就結束了。

#結束語

這篇文章完成了上一篇文章的前兩個部分,接下來就是SpringMVC本身如何將上述功能加入到本身的框架中來。

相關文章
相關標籤/搜索