#文件上传格式 先来看下含有文件上传时的表单提交是怎样的格式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本身如何将上述功能加入到本身的框架中来。