Java Web开发人员可以使用Apache文件上传组件来接收浏览器上传的文件,该组件由多个类共同组成,但是,对于使用该组件来编写文件上传功能的Java Web开发人员来说,只需要了解和使用其中的三个类:DiskFileUpload、FileItem和FileUploadException。这三个 类全部位于org.apache.commons.fileupload包中。
1. DiskFileUpload类
DiskFileUpload类是Apache文件上传组件的核心类,应用程序开发人员通过这个类来与Apache文件上传组件进行交互。但现在Apache建议使用ServletFileUpload类 ,两个类的方法类似。下面介绍DiskFileUpload类中的几个常用的重要方法。
1.1.setSizeMax方法
setSizeMax方法用于设置请求消息实体内容的最大允许大小,以防止客户端故意通过上传特大的文件来塞满服务器端的存储空间,单位为字节。其完整语法定义如下:
public void setSizeMax(long sizeMax)
如果请求消息中的实体内容的大小超过了setSizeMax方法的设置值,该方法将会抛出FileUploadException异常。
1.2.setSizeThreshold方法
Apache文件上传组件在解析和处理上传数据中的每个字段内容时,需要临时保存解析出的数据。因为Java虚拟机默认可以使用的内存空间是有限的 (笔者测试不大于100M),超出限制时将会发生“java.lang.OutOfMemoryError”错误,如果上传的文件很大,例如上传800M 的文件,在内存中将无法保存该文件内容,Apache文件上传组件将用临时文件来保存这些数据;但如果上传的文件很小,例如上传600个字节的文件,显然 将其直接保存在内存中更加有效。setSizeThreshold方法用于设置是否使用临时文件保存解析出的数据的那个临界值,该方法传入的参数的单位是 字节。其完整语法定义如下:
public void setSizeThreshold(int sizeThreshold)
1.3. setRepositoryPath方法
setRepositoryPath方法用于设置setSizeThreshold方法中提到的临时文件的存放目录,这里要求使用绝对路径。其完整语法定义如下:
public void setRepositoryPath(String repositoryPath)
如果不设置存放路径,那么临时文件将被储存在"java.io.tmpdir"这个JVM环境属性所指定的目录中,tomcat 5.5.9将这个属性设置为了“<tomcat安装目录>/temp/”目录。
1.4. parseRequest方法
parseRequest 方法是DiskFileUpload类的重要方法,它是对HTTP请求消息进行解析的入口方法,如果请求消息中的实体内容的类型不是“multipart/form-data”,该方法将抛出FileUploadException异常。parseRequest 方法解析出FORM表单中的每个字段的数据,并将它们分别包装成独立的FileItem对象,然后将这些FileItem对象加入进一个List类型的集合对象中返回。 parseRequest 方法的完整语法定义如下:
public List parseRequest(HttpServletRequest req)
parseRequest 方法还有一个重载方法,该方法集中处理上述所有方法的功能,其完整语法定义如下:
parseRequest(HttpServletRequest req,int sizeThreshold,long sizeMax,
String path)
这两个parseRequest方法都会抛出FileUploadException异常。
1.5. isMultipartContent方法
isMultipartContent方法方法用于判断请求消息中的内容是否是“multipart/form-data”类型,是则返回 true,否则返回false。isMultipartContent方法是一个静态方法,不用创建DiskFileUpload类的实例对象即可被调 用,其完整语法定义如下:
public static final boolean isMultipartContent(HttpServletRequest req)
1.6. setHeaderEncoding方法
由于浏览器在提交FORM表单时,会将普通表单中填写的文本内容传递给服务器,对于文件上传字段,除了传递原始的文件内容外,还要传递其文件路径名 等信息,如后面的图1.3所示。不管FORM表单采用的是“application/x-www-form-urlencoded”编码,还是 “multipart/form-data”编码,它们仅仅是将各个FORM表单字段元素内容组织到一起的一种格式,而这些内容又是由某种字符集编码来表 示的。关于浏览器采用何种字符集来编码FORM表单字段中的内容,请参看笔者编著的《深入体验java Web开发内幕——核心基础》一书中的第6.9.2的讲解,“multipart/form-data”类型的表单为表单字段内容选择字符集编码的原理和 方式与“application/x-www-form-urlencoded”类型的表单是相同的。FORM表单中填写的文本内容和文件上传字段中的文 件路径名在内存中就是它们的某种字符集编码的字节数组形式,Apache文件上传组件在读取这些内容时,必须知道它们所采用的字符集编码,才能将它们转换 成正确的字符文本返回。
对于浏览器上传给WEB服务器的各个表单字段的描述头内容,Apache文件上传组件都需要将它们转换成字符串形式返 回,setHeaderEncoding 方法用于设置转换时所使用的字符集编码,其原理与笔者编著的《深入体验java Web开发内幕——核心基础》一书中的第6.9.4节讲解的ServletRequest.setCharacterEncoding方法相同。 setHeaderEncoding 方法的完整语法定义如下:
public void setHeaderEncoding(String encoding)
其中,encoding参数用于指定将各个表单字段的描述头内容转换成字符串时所使用的字符集编码。
注意:如果读者在使用Apache文件上传组件时遇到了中文字符的乱码问题,一般都是没有正确调用setHeaderEncoding方法的原因。
2. FileItem类
FileItem类用来封装单个表单字段元素的数据,一个表单字段元素对应一个FileItem对象,通过调用FileItem对象的方法可以获得相关表单字段元素的数据。 FileItem 是一个接口,在应用程序中使用的实际上是该接口一个实现类,该实现类的名称并不重要,程序可以采用FileItem接口类型来对它进行引用和访问,为了便 于讲解,这里将FileItem实现类称之为FileItem类。FileItem类还实现了Serializable接口,以支持序列化操作。
对于“multipart/form-data”类型的FORM表单,浏览器上传的实体内容中的每个表单字段元素的数据之间用字段分隔界线进行分 割,两个分隔界线间的内容称为一个分区,每个分区中的内容可以被看作两部分,一部分是对表单字段元素进行描述的描述头,另外一部是表单字段元素的主体内 容,如图1.3所示。
图 1.3
主体部分有两种可能性,要么是用户填写的表单内容,要么是文件内容。FileItem类对象实际上就是对图1.3中的一个分区的数据进行封装的对 象,它内部用了两个成员变量来分别存储描述头和主体内容,其中保存主体内容的变量是一个输出流类型的对象。当主体内容的大小小于 DiskFileUpload.setSizeThreshold方法设置的临界值大小时,这个流对象关联到一片内存,主体内容将会被保存在内存中。当主 体内容的数据超过DiskFileUpload.setSizeThreshold方法设置的临界值大小时,这个流对象关联到硬盘上的一个临时文件,主体 内容将被保存到该临时文件中。临时文件的存储目录由DiskFileUpload.setRepositoryPath方法设置,临时文件名的格式为 “upload_00000005(八位或八位以上的数字).tmp”这种形式,FileItem类内部提供了维护临时文件名中的数值不重复的机制,以保 证了临时文件名的唯一性。当应用程序将主体内容保存到一个指定的文件中时,或者在FileItem对象被垃圾回收器回收时,或者Java虚拟机结束 时,Apache文件上传组件都会尝试删除临时文件,以尽量保证临时文件能被及时清除。
下面介绍FileItem类中的几个常用的方法:
2.1. isFormField方法
isFormField方法用于判断FileItem类对象封装的数据是否属于一个普通表单字段,还是属于一个文件表单字段,如果是普通表单字段则返回true,否则返回false。该方法的完整语法定义如下:
public boolean isFormField()
2.2. getName方法
getName方法用于获得文件上传字段中的文件名,对于图1.3中的第三个分区所示的描述头,getName方法返回的结果为字符串 “C:/bg.gif”。如果FileItem类对象对应的是普通表单字段,getName方法将返回null。即使用户没有通过网页表单中的文件字段传 递任何文件,但只要设置了文件表单字段的name属性,浏览器也会将文件字段的信息传递给服务器,只是文件名和文件内容部分都为空,但这个表单字段仍然对 应一个FileItem对象,此时,getName方法返回结果为空字符串"",读者在调用Apache文件上传组件时要注意考虑这个情况。 getName方法的完整语法定义如下:
public String getName()
注意:如果用户使用Windows系统上传文件,浏览器将传递该文件的完整路径,如果用户使用Linux或者Unix系统上传文件,浏览器将只传递该文件的名称部分。
2.3.getFieldName方法
getFieldName方法用于返回表单字段元素的name属性值,也就是返回图1.3中的各个描述头部分中的name属性值,例如“name=p1”中的“p1”。getFieldName方法的完整语法定义如下:
public String getFieldName()
2.4. write方法
write方法用于将FileItem对象中保存的主体内容保存到某个指定的文件中。 如果FileItem对象中的主体内容是保存在某个临时文件中,该方法顺利完成后,临时文件有可能会被清除。该方法也可将普通表单字段内容写入到一个文件中,但它主要用途是将上传的文件内容保存在本地文件系统中。其完整语法定义如下:
public void write(File file)
2.5.getString方法
getString方法用于将FileItem对象中保存的主体内容作为一个字符串返回,它有两个重载的定义形式:
public java.lang.String getString()
public java.lang.String getString(java.lang.String encoding)
throws java.io.UnsupportedEncodingException
前者使用缺省的字符集编码将主体内容转换成字符串,后者使用参数指定的字符集编码将主体内容转换成字符串。如果在读取普通表单字段元素的内容时出现了中文乱码现象,请调用第二个getString方法,并为之传递正确的字符集编码名称。
2.6. getContentType方法
getContentType 方法用于获得上传文件的类型,对于图1.3中的第三个分区所示的描述头,getContentType方法返回的结果为字符串“image/gif”,即 “Content-Type”字段的值部分。如果FileItem类对象对应的是普通表单字段,该方法将返回null。getContentType 方法的完整语法定义如下:
public String getContentType()
2.7. isInMemory方法
isInMemory方法用来判断FileItem类对象封装的主体内容是存储在内存中,还是存储在临时文件中,如果存储在内存中则返回true,否则返回false。其完整语法定义如下:
public boolean isInMemory()
2.8. delete方法
delete方法用来清空FileItem类对象中存放的主体内容,如果主体内容被保存在临时文件中,delete方法将删除该临时文件。尽管 Apache组件使用了多种方式来尽量及时清理临时文件,但系统出现异常时,仍有可能造成有的临时文件被永久保存在了硬盘中。在有些情况下,可以调用这个 方法来及时删除临时文件。其完整语法定义如下:
public void delete()
3. FileUploadException类
在文件上传过程中,可能发生各种各样的异常,例如网络中断、数据丢失等等。为了对不同异常进行合适的处理,Apache文件上传组件还开发了四个异 常类,其中FileUploadException是其他异常类的父类,其他几个类只是被间接调用的底层类,对于Apache组件调用人员来说,只需对FileUploadException异常类进行捕获和处理即可 。
4. ServletRequestContext
ServletRequestContext类提供访问request的方法。实现RequestContext接口。
核心API-DiskFileItemFactory:
DiskFileItemFactory 是创建FileItem对象的工厂,这个工厂常用方法:
1. public DiskFileItemFactory(int sizeThreshold, java.io.File repository) ,常用的构造函数。
2. public void setSizeThreshold(int sizeThreshold) ,设置内存缓冲区的大小,默认值为10K。当上传文件大于缓冲区大小时, fileupload组件将使用临时文件缓存上传文件。
3. public void setRepository(java.io.File repository) ,指定临时文件目录,默认值为System.getProperty("java.io.tmpdir")。
核心API-ServletFileupLoad:
ServletFileUpload 负责处理上传的文件数据,并将表单中每个输入项封装到一个FileItem对象中。常用方法有:
1. boolean isMultipartContent(HttpServletRequest request) ,判断上传表单是否为上传表单类型。
2. List parseRequest(HttpServletRequest request) ,解析request对象,并把表单中的每一个输入项包装到一个fileItem 对象中,并返回一个保存了所有FileItem的list集合。
3. setFileSizeMax(long fileSizeMax) ,设置上传文件的最大尺寸值。
4. setSizeMax(long sizeMax) ,设置上传文件总量的最大值。
5. setHeaderEncoding(java.lang.String encoding) ,设置编码格式。如果文件路径中存在中文可能会造成文件路径乱码,用此方法处理可以解决。
6. setProgressListener(ProgressListener pListener) ,设置进程监听器,与AWT和Swing的事件处理机制一样。文件上传一点就会触发ProgressListener,这样我们就可以获取文件上传的进度。
上传文件案例:
public class FileuploadServlet extends HttpServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 创建文件处理工厂,它用于生成 FileItem 对象。 DiskFileItemFactory difactory = new DiskFileItemFactory(); // 设置缓存大小,如果上传文件超过缓存大小,将使用临时目录做为缓存。 difactory.setSizeThreshold(1024 * 1024); // 设置处理工厂缓存的临时目录,此目录下的文件需要手动删除。 String dir = this .getServletContext().getRealPath( "/" ); File filedir = new File(dir + "filetemp" ); if (!filedir.exists()) filedir.mkdir(); difactory.setRepository(filedir); // 设置文件实际保存的目录 String userdir = dir + "files" ; File fudir = new File(userdir); if (!fudir.exists()) fudir.mkdir(); // 创建 request 的解析器,它会将数据封装到 FileItem 对象中。 ServletFileUpload sfu = new ServletFileUpload(difactory); // 解析保存在 request 中的数据并返回 list 集合 List list = null ; try { list = sfu.parseRequest(request); } catch (FileUploadException e) { e.printStackTrace(); } // 遍历 list 集合,取出每一个输入项的 FileItem 对象,并分别获取数据 for (Iterator it = list.iterator(); it.hasNext();) { FileItem fi = (FileItem) it.next(); if (fi.isFormField()) { System. out .println(fi.getFieldName()); System. out .println(fi.getString()); } else { // 由于客户端向服务器发送的文件是客户端的全路径,在这我们只需要文件名即可 String filename = fi.getName(); int index = filename.lastIndexOf( """" ); if (index != -1) filename = filename.substring(index+1); // 向服务器写出文件 InputStream in = fi.getInputStream(); FileOutputStream fos = new FileOutputStream(fudir + "/"+filename); byte [] buf = new byte [1024]; int len = -1; while ((len = in.read(buf)) != -1){ fos.write(buf, 0, len); } // 关闭流 if (in != null ){ try { in.close(); } finally { if (fos!= null ) fos.close(); } } } } } public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doGet(request, response); } } |
上面的代码只是功能的练习,实际开发中的文件上传需要考虑诸多因素,我们接下来继续学习。
JS 动态添加文件上传框和按钮的JavaScript代码:
function add(){ var file = document.createElement( "input" ); file.type = "file" ; file.name = "file" ;
var butt = document.createElement( "input" ); butt.type = "button" ; butt.value = " 删除 " ; butt.onclick = function rem(){ // 必须使用按钮的父节点 DIV 的父节点来删除自己和自己的父节点 DIV 。 this .parentNode.parentNode.removeChild( this.parentNode); };
var div = document.createElement( "div" ); div.appendChild(file); div.appendChild(butt);
var parent = document.getElementById( "files" ); parent.appendChild(div); } |
上传文件的处理细节(1):
1. 中 文文件乱码的问题,可以调用两个方法来设置字符编码:servletUpLoader.setHeaderEncoding()或 request.setCharacterEncoding()。我们可以在源文件的创建ServletFileUpload对象后边添加如下代码:
sfu.setHeaderEncoding( "UTF-8" ); |
2. 临 时文件的删除,如果临时文件大于setSizeThreshold设置的缓存大小,Commons-fileupload组件将使用 setRepository设置的临时目录来保存上传的文件,上传完成后我们需要手动调用FileItem.delete来删除临时文件。建议不要修改缓 存区大小,如果设置缓存为1MB,1000个用户上传文件就需要1000MB内存,服务器会受不了的。我们删除掉setSizeThreshold的代 码,并在每次完成一个文件后添加下而的代码:
// 删除临时目录中的文件 fi.delete(); |
上传文件的处理细节(2):
1. 在上面的代码中,我们将文件的实际保存目录设置在WEB-INF目录之外。这样外部可以直接访问被上传的文件,这会造成安全问题。比如用户上传了一个带有恶意脚本功能的JSP文件,然后从外部访问执行了JSP文件…后果不堪设想。所以我们将源代码中对应位置处修改如下:
// 之所以放在 "WEB-INF" 目录下是为了防止上传的文件被直接被访问的安全问题 String userdir = dir + "WEB-INF/files" ; |
2. 一个WEB应用会许多不同的用户访问,不同的用户可能会上传相同名称的文件,如果这样可能会造成文件覆盖的情况发生,所以我们必须保证文件名称的唯一性,我编写一个方法来生成唯一性名称的文件名:
/** * 生成具有唯一性的 UUID 文件名称 * @param fileName * @return */ private String uuidName(String fileName){ UUID uuid = UUID.randomUUID (); return uuid.toString() + "_" + fileName; } |
我们将代码“ filename = filename.substring(index + 1); ”修改为: filename = uuidName(filename.substring(index + 1));
3. 如果一个目录下的文件过多,会极大减慢文件的访问速度。比如一个目录下的文件如果超过1000个,达到1万个呢?恐怖!我们必须编写一个目录结构生成算法,来分散存上传的文件。我们一个方法:
/** * 使用哈希算法生成的文件路径 * @param dir * @param fileName * @return */ private String hashPath(String dir, String fileName) { int hashCode = fileName.hashCode();
int dir1 = (hashCode >> 4) & 0xf; int dir2 = hashCode & 0xf;
String newpath = dir + "/" + dir1 + "/" + dir2 + "/" ; File file = new File(newpath); if (!file.exists()){ file.mkdirs(); }
return newpath + uuidName(fileName); } |
上传文件的处理细节(3)
1. 使用ProgressListener显示上传文件进度,在创建ServletFileUpload之后添加如下代码:
// 设置文件上传进度监听器 sfu.setProgressListener( new ProgressListener() { public void update( long pBytesRead, long pContentLength, intpItems) { System. out .println( " 已上传: " + pBytesRead + " 总大小: " + pContentLength); } }); |
2. 上面的代码会造成频繁的打印,为了使它在上传一定数量后再打印,比如上传10KB后再打印,我们修改上面的代码如下:
// 设置文件上传进度监听器 sfu.setProgressListener( new ProgressListener() { long temp = -1; public void update( long pBytesRead, long pContentLength, intpItems) { long size = pBytesRead / 1024 * 1024 * 10; if ( temp == size) return ; temp = size; if (pBytesRead != -1) System. out .println( " 已上传: " + pBytesRead + " 总大小: " + pContentLength); else System. out .println( " 上传完成! " ); } }); |
上面的代码比较经典,好好回味一下。
文件下载:
WEB 应用中实现文件下载的两种方式:
1. 超链接直接指向下载资源
2. 程序实现下载需设置两个响应头:
(1). 设置Content-Type 的值为:application/x-msdownload。Web 服务器需要告诉浏览器其所输出的内容的类型不是普通的文本文件或 HTML 文件,而是一个要保存到本地的下载文件。
(2). Web 服 务器希望浏览器不直接处理相应的实体内容,而是由用户选择将相应的实体内容保存到一个文件中,这需要设置 Content-Disposition 报头。该报头指定了接收程序处理数据内容的方式,在 HTTP 应用中只有 attachment 是标准方式,attachment 表示要求用户干预。在 attachment 后面还可以指定 filename 参数,该参数是服务器建议浏览器将实体内容保存到文件中的文件名称。在设置 Content-Dispostion 之前一定要指定 Content-Type。
为实现文件下载,首先我们遍历目录下所有文件,Servlet:
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ListFileServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取目录
String dir = this .getServletContext().getRealPath( "/WEB-INF/files" );
HashMap map = new HashMap();
listFile( new File(dir), map);
// 将文件列表设置到 request 的属性中,然后由 JSP 页面打印列表。
request.setAttribute( "filemap" , map);
request.getRequestDispatcher( "/list.jsp" ).forward(request, response);
}
/**
* 使用递归算法,将所有子目录中的文件添加到列表中
*
* @param f
* @param l
*/
private void listFile(File f, HashMap map) {
if (f.isFile()) {
String path = f.getAbsolutePath().substring(
this .getServletContext().getRealPath( "/" ).length());
String name = f.getName();
name = name.substring(name.indexOf( "_" )+1);
//BASE64Encoder encoder = new BASE64Encoder();
map.put(path, name);
} else {
File[] files = f.listFiles();
for ( int i = 0; i < files. length ; i++) {
listFile(files[i], map);
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}