The Apache Commons Project 中的 FileUpload
很久以前翻译着玩的东东,又被我翻出来了。=========================================================
The Apache Commons Project 中的 FileUpload (官方网址)
翻译:angryzorro
概述
Apache Commons 中的 FileUpload 包能够使你更容易地向 servlet 和 web应用 中添加健壮的高性能的文件上传功能.
FileUpload 解析符合 RFC 1867标准 "Form-based File Upload in HTML" 的 HTTP 请求.也就是说,如果使用 POST 方法提交一个HTTP 请求,且其中的内容类型是 "multipart/form-data",那么 FileUpload就会解析该请求,在某种意义上使得结果对调用者来说更易于使用.
用户向导
1 使用 FileUpload
FileUpload的使用可以有许多不同的方法,这依赖于你的应用的需求.在最简单的情况下,你只要调用一个单一的方法来解析 servlet请求,然后处理将要应用到你的应用的项目列表.另一种极端的情况下,你有可能会决定自定义 FileUpload来完全控制个别项目被存储的方式;例如,你可能会决定把文件内容作为流存入数据库中.
这里,我们将描述 FileUpload 的基本原理,并例举一些较为简单而且最普通的使用模式.FileUpload 的自定义将会在别处描述.
FileUpload 需要依赖 Commons IO,因此在继续之前你需要确定你的类路径中已经存在了"依赖关系"页面中所提及的 Commons IO 版本.
2 工作原理
一个文件上传请求由一组有序的项目列表组成,这些项目是依据 RFC 1867标准 "Form-based File Upload in HTML" 编码.FileUpload 能够解析这种请求,并提供给你的应用一组包含各个上传项目的列表.每个这样的项目都实现了 FileItem 接口,无论它的根本实现是什么.
本页描述的是 commons fileupload 库的传统API.传统API 是一条捷径.然而,为了最终的性能,你可能更喜欢速度更快的 流API.
每个文件项目都有许多的属性,它们可能对你的应用很有用.比如说,每个文件项目有一个名称属性和一个内容类型属性,并且提供一个 InputStream来访问它的数据.另一方面,你可能需要对多个文件项目进行不同的处理,这依赖于处理的项目是否是一个常规的表单域(即数据来自一个通常的文本框或类似的HTML域)或是一个被上传的文件.FileItem 接口提供一些方法来对此做出判定,并用最合适的方式访问数据.
FileUpload 通过 FileItemFactory 创建新的文件项目,正是它给了FileUpload 极大的适应性.工厂对每个项目如何创建拥有最终控制权.通常由 FileUpload运载的工厂执行会将项目数据存储在内存中或者磁盘上,这取决于项目的大小(即数据字节数).然而,这一行为可以通过自定义来更适合你的应用.
3 Servlets 和 Portlets
FileUpload 从1.1版开始支持在 servlet 和 portlet 两种环境下的文件上传请求.它在这两种环境下的用法基本上是一致的,所以本文接下来的部分将只涉及 servlet 环境.
如果你的应用是建立在 portlet 环境下的,那么在阅读本文时应按照以下2点做出区别:
·在你看到的涉及 ServletFileUpload 类的地方,用 PortletFileUpload 类替换.
·在你看到的涉及 HttpServletRequest 类的地方,用 ActionRequest 类替换
4 请求的解析
在你使用一个已经上传的项目工作之前,理所当然你需要对请求做出解析.确认请求是否确实是一个文件上传请求是一个直接了当的做法,但是 FileUpload 自身提供了一个静态方法来使这件事更简单.
// check that we have a file upload request
booleanisMultipart = ServletFileUpload.isMultipartContent(request);
现在我们已经准备好将解析请求的各个组成项.
4.1 最简单的情况
最简单的使用方案如下:
·上传的项目应当被保持在内存中,只要他们相当地小.
·大的项目应该被写入磁盘上的一个临时文件.
·非常大的文件的上传请求应该是不被允许的.
·接受内置的可以保持在内存的项目最大值,可以上传的文件的最大值和存放临时文件的路径的默认值.
在这种方案下的对请求的操作再简单不过了:
// Create a factory for disk-based file items
FileItemFactory factory = newDiskFileItemFactory();
// Create a new file upload handler
ServletFileUpload upload = newServletFileUpload(factory);
// Parse the request
List/* FileItem */ items = upload.parseRequest(request);
这就是所有要做的.真的!
解析后的结果是一个文件项目的 List,每个文件项目都实现了 FileItem 接口.对这些项目的处理将会在后面讨论.
4.2 练习更多的控制
如果你的使用方案与上面所描述的最简单的用例很接近,但是你需要更多一点的控制,那么你可以很容易地自定义上传处理器或者文件项目工厂的行为,又或者同时自定义两者的行为.接下来的例子展示了几个配置选项:
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory();
// Set factory constraints
factory.setSizeThreshold(yourMaxMemorySize);
factory.setRepository(yourTempDirectory);
// Create a new file upload handler
ServletFileUpload upload = new ServletFileUpload(factory);
// Set overall request size constraint
upload.setSizeMax(yourMaxRequestSize);
// Parse the request
List/* FileItem */ items = upload.parseRequest(request);
当然,每一个配置方法都与其他的无关,但是如果你想要一次性对工厂进行配置,可以使用另一个可供选择的构造函数,就像这样:
// Create a factory for disk-based file items
DiskFileItemFactory factory = new DiskFileItemFactory(yourMaxMemorySize, yourTempDirectory);
如果你还需要更进一步地对请求的解析进行控制,像是把项目存储到别的地方(例如:数据库),那么你应该到 FileUpload 的自定义查找.
5 处理上传的项目
一旦完成了解析,你会得到一个文件项目的 List 并将对其进行处理.多数情况下,你会想要用不同与处理常规表单域的方法来处理文件上传,所以你可能会这样处理:
// Process the uploaded items
Iterator iter = items.iterator();
while (iter.hasNext()) {
FileItem item = (FileItem) iter.next();
if (item.isFormField()) {
processFormField(item);
} else {
processUploadedFile(item);
}
}
对于常规的表单域,你多数只对项目的名称和字符串值感兴趣.如你所需,访问它们是非常简单的.
// Process a regular form field
if (item.isFormField()) {
String name = item.getFieldName();
String value = item.getString();
...
}
对于一个文件上传,在你处理其内容之前,你可能想要先知道几个不同的事项.这有一个例子,里面可能有你感兴趣的一些方法.
// Process a file upload
if (!item.isFormField()) {
String fieldName = item.getFieldName();
String fileName = item.getName();
String contentType = item.getContentType();
boolean isInMemory = item.isInMemory();
long sizeInBytes = item.getSize();
...
}
关于上传的文件,一般你是不会想要从内存来对其进行存取的,除非它们非常的小,或是你没有其他的选择.而将其内容作为流来处理,或者把整个文件写入它最终的位置,这样才更合适.FileUpload 对这两种做法都提供了简单的实现方法.
// Process a file upload
if (writeToFile) {
File uploadedFile = new File(...);
item.write(uploadedFile);
} else {
InputStream uploadedStream = item.getInputStream();
...
uploadedStream.close();
}
注意,在 FileUpload 的默认实现中,如果数据已经存在于临时文件中,write() 将试图将文件重命名为指定的目的文件.实际上,只有因为某些原因重命名失败了或者是数据存放在内存中的情况下才会去复制数据.
如果你需要在内存中存取上传的数据,你只要调用get()方法来获取数据的字节数组.
// Process a file upload in memory
byte[] data = item.get();
...
6 清理资源
本节只适用于你使用 DiskFileItem的情况.换句话说,就是在你处理上传的数据之前它们被存放在临时文件中.
这些临时文件在不再被使用的时候(如果相应的java.io.File是可回收的则更好)会自动被删除.这会被org.apache.commons.io.FileCleaningTracker的一个实例启动的一个收割线程默默执行.
在下文中,我们假定你正在编写一个web应用.在一个web应用中,资源清理是被javax.servlet.ServletContextListener的一个实例控制的.在其他环境中,类似的观念定是适用的.
6.1 FileCleanerCleanup 类
你的web应用应该使用org.apache.commons.fileupload.FileCleanerCleanup的一个实例.那很简单,你只要把它加到你的 web.xml 中:
<web-app>
...
<listener>
<listener-class>
org.apache.commons.fileupload.servlet.FileCleanerCleanup
</listener-class>
</listener>
...
</web-app>
6.2 创建一个 DiskFileItemFactory
FileCleanerCleanup 提供一个org.apache.commons.io.FileCleaningTracker 实例.此实例必须在创建一个org.apache.commons.fileupload.disk.DiskFileItemFactory时使用.这应该通过调用如下方法来实现:
public staticDiskFileItemFactory newDiskFileItemFactory(ServletContext context, File repository) {
FileCleaningTracker fileCleaningTracker = FileCleanerCleanup.getFileCleaningTracker(context);
return newDiskFileItemFactory(fileCleaningTracker,
DiskFileItemFactory.DEFAULT_SIZE_THRESHOLD,
repository);
}
6.3 禁用临时文件的清理
要禁用对临时文件的追踪,你就要设置 FileCleaningTracker 的值为null.从而,创建的文件将不再被追踪,也就不再会被自动删除.
7 与病毒扫描器交互
病毒扫描器与web容器运行在同一个系统上会引发使用 FileUpload 的应用产生一些意外的情况.本节描述了一些你可能遭遇到的情况,并提供了一些处理这些问题的建议.
FileUpload的默认实现会使得超过某一大小限制的上传项被写入到磁盘.当这样一个文件一完成,系统中的病毒扫描器就会激活并开始检查它,并有可能将其隔离,即将其转移到一个特殊的位置,在那里它不会引起任何问题.这无疑会给应用开发者一个出其不意,因为上传的文件项将无法再进行处理.另一方面,低于那个大小限制的上传项将被存放在内存中,因此不会被病毒扫描器发现.这使得病毒有可能以某种形式被保留来下(但是它一旦写入到磁盘,病毒扫描器就会锁定并检查它).
一个普遍使用的解决方案是将所有上传的文件都放在系统的一个固定的路径下,并且设置病毒扫描器忽略该路径.这样能够确保文件不会从应用中强行移出,但却将病毒扫描的责任留给了应用开发者.对上传文件进行病毒扫描可以由外部程序来执行,它可能会将干净的或是被清理过的文件移动至一个"认可的"路径,病毒扫描也可通过应用自身内部整合的病毒扫描器来执行.关于配置一个外部程序或者整合病毒扫描到应用已经超出了本文内容范围.
8 进度监视
如果你有大文件上传的需求,那么让你的用户知道已经接收到的大小将会十分体贴.正好 HTML 页面允许通过返回一个 multipart/replace 响应或者类似的东西来实现一个进度条.
监视上传进度可以通过加入一个进度监听器来做到:
//Create a progress listener
ProgressListener progressListener = new ProgressListener(){
public voidupdate(long pBytesRead, long pContentLength, int pItems) {
System.out.println("We are currently reading item "+ pItems);
if (pContentLength == -1) {
System.out.println("So far, "+ pBytesRead + " bytes have been read.");
} else {
System.out.println("So far, "+ pBytesRead + " of " + pContentLength
+ " bytes have been read.");
}
}
}
upload.setProgressListener(progressListener);
自己动手照上面的样子实现你的第一个进度监听器,你会发现一个缺陷:这个进度监听器的调用频率很高.根据 servlet引擎和其他环境工厂,它可能因任何网络数据包而调用!换句话说,你的进度监听器会成为一个性能问题!一个典型的解决方案是减少进度监听器的活动.例如,可能你只是发送一条消息,倘若兆字节数发生了变化:
//Create a progress listener
ProgressListener progressListener = new ProgressListener(){
private longmegaBytes = -1;
public voidupdate(long pBytesRead, long pContentLength, int pItems) {
long mBytes = pBytesRead / 1000000;
if (megaBytes == mBytes) {
return;
}
megaBytes = mBytes;
System.out.println("We are currently reading item "+ pItems);
if (pContentLength == -1) {
System.out.println("So far, "+ pBytesRead + " bytes have been read.");
} else {
System.out.println("So far, "+ pBytesRead + " of "+ pContentLength
+ " bytes have been read.");
}
}
}
9 接下来做什么
希望本文使你在自己的应用程序中如何使用 FileUpload 有了一个清晰的概念.对于这里介绍的和其他的可用的方法的更多细节请参考 JavaDocs.
这里描述的用法应该能够满足绝大部分的文件上传需求.然而,要是你有更复杂的需求,FileUpload 也仍然能够通过其灵活的自定义能力帮助你.
页:
[1]