commons-fileupload 文件上传组件


深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 第 1 章 文件上传组件的应用与编写 在许多 Web 站点应用中都需要为用户提供通过浏览器上传文档资料的功能,例如,上传邮件 附件、个人相片、共享资料等。对文件上传功能,在浏览器端提供了较好的支持,只要将 FORM 表单的 enctype 属性设置为“multipart/form-data”即可;但在 Web 服务器端如何获取浏览器上传的 文件,需要进行复杂的编程处理。为了简化和帮助 Web 开发人员接收浏览器上传的文件,一些公 司和组织专门开发了文件上传组件。本章将详细介绍如何使用 Apache 文件上传组件,以及分析该 组件源程序的设计思路和实现方法。 1.1 准备实验环境 按下面的步骤为本章的例子程序建立运行环境: (1)在 Tomcat 5.5.12 的\webapps 目录中创建一个名为 fileupload 的子目录,并在 fileupload 目录中创建一个名为 test.html 的网页文件,在该文件中写上“这是 test.html 页面的原始内容!”这几个字符。 (2)在\webapps\fileupload 目录中创建一个名为 WEB-INF 的子目录, 在 WEB-INF 目录中创建一个名为 classes 的子目录和一个 web.xml 文件,web.xml 文件内容如下: (3)要使用 Apache 文件上传组件,首先需要安装 Apache 文件上传组件包。在\webapps\fileupload\WEB-INF 目录中创建一个名为 lib 的子目录,然后从网址 http://jakarta.apache.org/commons/fileupload 下载到 Apache 组件的二进制发行包,在本书 的附带带光盘中也提供了该组件的二进制发行包,文件名为 commons-fileupload-1.0.zip。从 commons-fileupload-1.0.zip 压缩包中解压出 commons-fileupload-1.0.jar 文件,将它放置进 \webapps\fileupload\WEB-INF\lib 目录中,就完成了 Apache 文件上传组件 的安装。 (4)在\webapps\fileupload 目录中创建一个名为 src 的子目录,src 目录用于放置本章编写的 Java 源程序。为了便于对 Servlet 源文件进行编译,在 src 目录中编写 一个 compile.bat 批处理文件,如例程 1-1 所示。 例程 1-1 compile.bat set PATH=C:\jdk1.5.0_01\bin;%path% set CLASSPATH=C:\tomcat-5.5.12\common\lib\servlet-api.jar;C:\tomcat-5.5.12\\webapps\ fileupload\WEB-INF\lib\commons-fileupload-1.0.jar;%CLASSPATH% javac -d ..\WEB-INF\classes %1 pause 在 compile.bat 批处理文件中要注意将 commons-fileupload-1.0.jar 文件的路径加入到 CLASSPATH 环境变量中和确 保 编 译 后 生 成 的 class 文 件 存 放 到 \webapps\fileupload\WEB-INF\classes 目录中,上面的 CLASSPATH 环境变量的设置值由于排 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 版原因进行了换行,实际上不应该有换行。接着在 src 目录中为 compile.bat 文件创建一个快捷方 式,以后只要在 Windows 资源管理器窗口中将 Java 源文件拖动到 compile.bat 文件的快捷方式上, 就可以完成 Java 源程序的编译了。之所以要创建 compile.bat 文件的快捷方式,是因为直接将 Java 源程序拖动到 compile.bat 批处理文件时,compile.bat 批处理文件内编写的相对路径不被支持。创 建完的 fileupload 目录中的文件结构如图 1.1 所示。 图 1.1 (4)启动 Tomcat,在本地计算机的浏览器地址栏中输入如下地址: http://localhost:8080/fileupload/test.html 验证浏览器能够成功到该网页文档。如果浏览器无法访问到该网页文档,请检查前面的操作 步骤和改正问题,直到浏览器能够成功到该网页文档为止。 (5)为了让/fileupload 这个 WEB 应用程序能自动重新装载发生了修改的 Servlet 程序,需 要修改 Tomcat 的 server.xml 文件,在该文件的元素中增加如下一个子元素: 保存 server.xml 文件后,重新启动 Tomcat。 1.2 Apache 文件上传组件的应用 Java Web 开发人员可以使用 Apache 文件上传组件来接收浏览器上传的文件,该组件由多个 类共同组成,但是,对于使用该组件来编写文件上传功能的 Java Web 开发人员来说,只需要了解 和使用其中的三个类:DiskFileUpload、FileItem 和 FileUploadException。这三个类全部位于 org.apache.commons.fileupload 包中。 1.2.1 查看 API 文档 在准备实验环境时获得的 commons-fileupload-1.0.zip 文件的解压缩目录中可以看到一个 docs 的子目录,其中包含了 Apache 文件上传组件中的各个 API 类的帮助文档,从这个文档中可 以了解到各个 API 类的使用帮助信息。打开文件上传组件 API 帮助文档中的 index.html 页面,在 左侧分栏窗口页面中列出了文件上传组件中的各个 API 类的名称,在右侧分栏窗口页面的底部列 出了一段示例代码,如图 1.2 所示。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 图 1.2 读者不需要逐个去阅读图 1.2 中列出的各个 API 类的帮助文档,而应该以图 1.2 中的示例代 码为线索,以其中所使用到的类为入口点,按图索骥地进行阅读,对于示例代码中调用到的各个 API 类的方法则应重点掌握。 1.2.2 DiskFileUpload 类 DiskFileUpload 类是 Apache 文件上传组件的核心类,应用程序开发人员通过这个类来与 Apache 文件上传组件进行交互。下面介绍 DiskFileUpload 类中的几个常用的重要方法。 1.setSizeMax 方法 setSizeMax 方法用于设置请求消息实体内容的最大允许大小,以防止客户端故意通过上传特 大的文件来塞满服务器端的存储空间,单位为字节。其完整语法定义如下: public void setSizeMax(long sizeMax) 如果请求消息中的实体内容的大小超过了 setSizeMax 方法的设置值,该方法将会抛出 FileUploadException 异常。 2.setSizeThreshold 方法 Apache 文件上传组件在解析和处理上传数据中的每个字段内容时,需要临时保存解析出的数 据。因为 Java 虚拟机默认可以使用的内存空间是有限的(笔者测试不大于 100M),超出限制时将 会发生“java.lang.OutOfMemoryError”错误,如果上传的文件很大,例如上传 800M 的文件,在 内存中将无法保存该文件内容,Apache 文件上传组件将用临时文件来保存这些数据;但如果上传 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 的 文 件 很 小 , 例 如 上 传 600 个字节的文件,显然将其直接保存在内存中更加有效。 setSizeThreshold 方法用于设置是否使用临时文件保存解析出的数据的那个临界值,该方法传入 的参数的单位是字节。其完整语法定义如下: public void setSizeThreshold(int sizeThreshold) 3. setRepositoryPath 方法 setRepositoryPath 方法用于设置 setSizeThreshold 方法中提到的临时文件的存放目录,这 里要求使用绝对路径。其完整语法定义如下: public void setRepositoryPath(String repositoryPath) 如果不设置存放路径,那么临时文件将被储存在"java.io.tmpdir"这个 JVM 环境属性所指定 的目录中,tomcat 5.5.9 将这个属性设置为了“/temp/”目录。 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 异常。 5. isMultipartContent 方法 isMultipartContent 方法方法用于判断请求消息中的内容是否是“multipart/form-data” 类型,是则返回 true,否则返回 false。isMultipartContent 方法是一个静态方法,不用创建 DiskFileUpload 类的实例对象即可被调用,其完整语法定义如下: public static final boolean isMultipartContent(HttpServletRequest req) 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 文件上传组件都需要将 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 它们转换成字符串形式返回,setHeaderEncoding 方法用于设置转换时所使用的字符集编码,其 原理与笔者编著的《深入体验 java Web 开发内幕——核心基础》一书中的第 6.9.4 节讲解的 ServletRequest.setCharacterEncoding 方法相同。setHeaderEncoding 方法的完整语法定义如下: public void setHeaderEncoding(String encoding) 其中,encoding 参数用于指定将各个表单字段的描述头内容转换成字符串时所使用的字符集 编码。 注意:如果读者在使用 Apache 文件上传组件时遇到了中文字符的乱码问题,一般都是没有 正确调用 setHeaderEncoding 方法的原因。 1.2.3 FileItem 类 FileItem 类用来封装单个表单字段元素的数据,一个表单字段元素对应一个 FileItem 对象, 通过调用 FileItem 对象的方法可以获得相关表单字段元素的数据。FileItem 是一个接口,在应 用程序中使用的实际上是该接口一个实现类,该实现类的名称并不重要,程序可以采用 FileItem 接口类型来对它进行引用和访问,为了便于讲解,这里将 FileItem 实现类称之为 FileItem 类。 FileItem 类还实现了 Serializable 接口,以支持序列化操作。 对于“multipart/form-data”类型的 FORM 表单,浏览器上传的实体内容中的每个表单字段 元素的数据之间用字段分隔界线进行分割,两个分隔界线间的内容称为一个分区,每个分区中的 内容可以被看作两部分,一部分是对表单字段元素进行描述的描述头,另外一部是表单字段元素 的主体内容,如图 1.3 所示。 图 1.3 主体部分有两种可能性,要么是用户填写的表单内容,要么是文件内容。FileItem 类对象实 际上就是对图 1.3 中的一个分区的数据进行封装的对象,它内部用了两个成员变量来分别存储描 述头和主体内容,其中保存主体内容的变量是一个输出流类型的对象。当主体内容的大小小于 DiskFileUpload.setSizeThreshold 方法设置的临界值大小时,这个流对象关联到一片内存,主 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 体内容将会被保存在内存中。当主体内容的数据超过 DiskFileUpload.setSizeThreshold 方法设 置的临界值大小时,这个流对象关联到硬盘上的一个临时文件,主体内容将被保存到该临时文件 中。临时文件的存储目录由 DiskFileUpload.setRepositoryPath 方法设置,临时文件名的格式为 “upload_00000005(八位或八位以上的数字).tmp”这种形式,FileItem 类内部提供了维护临 时文件名中的数值不重复的机制,以保证了临时文件名的唯一性。当应用程序将主体内容保存到 一个指定的文件中时,或者在FileItem对象被垃圾回收器回收时,或者Java虚拟机结束时,Apache 文件上传组件都会尝试删除临时文件,以尽量保证临时文件能被及时清除。 下面介绍 FileItem 类中的几个常用的方法: 1. isFormField 方法 isFormField 方法用于判断 FileItem 类对象封装的数据是否属于一个普通表单字段,还是属 于一个文件表单字段,如果是普通表单字段则返回 true,否则返回 false。该方法的完整语法定 义如下: public boolean isFormField() 2. getName 方法 getName 方法用于获得文件上传字段中的文件名,对于图 1.3 中的第三个分区所示的描述头, getName 方法返回的结果为字符串“C:\bg.gif”。如果 FileItem 类对象对应的是普通表单字段, getName 方法将返回 null。即使用户没有通过网页表单中的文件字段传递任何文件,但只要设置 了文件表单字段的 name 属性,浏览器也会将文件字段的信息传递给服务器,只是文件名和文件 内容部分都为空,但这个表单字段仍然对应一个 FileItem 对象,此时,getName 方法返回结果为 空字符串"",读者在调用 Apache 文件上传组件时要注意考虑这个情况。getName 方法的完整语法 定义如下: public String getName() 注意:如果用户使用 Windows 系统上传文件,浏览器将传递该文件的完整路径,如果用户使 用 Linux 或者 Unix 系统上传文件,浏览器将只传递该文件的名称部分。 3.getFieldName 方法 getFieldName 方法用于返回表单字段元素的 name 属性值,也就是返回图 1.3 中的各个描述 头部分中的 name 属性值,例如“name=p1”中的“p1”。 getFieldName 方法的完整语法定义如下: public String getFieldName() 4. write 方法 write 方法用于将 FileItem 对象中保存的主体内容保存到某个指定的文件中。如果 FileItem 对象中的主体内容是保存在某个临时文件中,该方法顺利完成后,临时文件有可能会被清除。该 方法也可将普通表单字段内容写入到一个文件中,但它主要用途是将上传的文件内容保存在本地 文件系统中。其完整语法定义如下: public void write(File file) 5.getString 方法 getString 方法用于将 FileItem 对象中保存的主体内容作为一个字符串返回,它有两个重载 的定义形式: public java.lang.String getString() public java.lang.String getString(java.lang.String encoding) 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org throws java.io.UnsupportedEncodingException 前者使用缺省的字符集编码将主体内容转换成字符串,后者使用参数指定的字符集编码将主 体内容转换成字符串。如果在读取普通表单字段元素的内容时出现了中文乱码现象,请调用第二 个 getString 方法,并为之传递正确的字符集编码名称。 6. getContentType 方法 getContentType 方法用于获得上传文件的类型,对于图 1.3 中的第三个分区所示的描述头, getContentType 方法返回的结果为字符串“image/gif”,即“Content-Type”字段的值部分。如 果 FileItem 类对象对应的是普通表单字段,该方法将返回 null。getContentType 方法的完整语 法定义如下: public String getContentType() 7. isInMemory 方法 isInMemory 方法用来判断 FileItem 类对象封装的主体内容是存储在内存中,还是存储在临 时文件中,如果存储在内存中则返回 true,否则返回 false。其完整语法定义如下: public boolean isInMemory() 8. delete 方法 delete 方法用来清空 FileItem 类对象中存放的主体内容,如果主体内容被保存在临时文件 中,delete 方法将删除该临时文件。尽管 Apache 组件使用了多种方式来尽量及时清理临时文件, 但系统出现异常时,仍有可能造成有的临时文件被永久保存在了硬盘中。在有些情况下,可以调 用这个方法来及时删除临时文件。其完整语法定义如下: public void delete() 1.2.4 FileUploadException 类 在文件上传过程中,可能发生各种各样的异常,例如网络中断、数据丢失等等。为了对不同 异常进行合适的处理,Apache 文件上传组件还开发了四个异常类,其中 FileUploadException 是其 他异常类的父类,其他几个类只是被间接调用的底层类,对于 Apache 组件调用人员来说,只需 对 FileUploadException 异常类进行捕获和处理即可。 1.2.5 文件上传编程实例 下面参考图 1.2 中看到的示例代码编写一个使用 Apache 文件上传组件来上传文件的例子程 序。 动手体验:使用 Apache 文件上传组件 (1)在\webapps\fileupload 目录中按例程 1-1 编写一个名为 FileUpload.html 的 HTML 页面,该页面用于提供文件上传的 FORM 表单,表单的 enctype 属性设置值为 “multipart/form-data”,表单的 action 属性设置为“servlet/UploadServlet”。 例程 1-1 FileUpload.html 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org upload experiment

测试文件上传组件的页面

作者:
来自:
文件 1:
文件 2:
(2)在\webapps\fileupload\src 目录中按例程 1-2 创建一个名为 UploadServlet.java 的 Servlet 程序,UploadServlet.java 调用 Apache 文件上传组件来处理 FORM 表 单提交的文件内容和普通字段数据。 例程 1-2 UploadServlet.java import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import org.apache.commons.fileupload.*; public class UploadServlet extends HttpServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException { response.setContentType("text/html;charset=gb2312"); PrintWriter out = response.getWriter(); //设置保存上传文件的目录 String uploadDir = getServletContext().getRealPath("/upload"); if (uploadDir == null) { out.println("无法访问存储目录!"); return; } File fUploadDir = new File(uploadDir); 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org if(!fUploadDir.exists()) { if(!fUploadDir.mkdir()) { out.println("无法创建存储目录!"); return; } } if (!DiskFileUpload.isMultipartContent(request)) { out.println("只能处理 multipart/form-data 类型的数据!"); return ; } DiskFileUpload fu = new DiskFileUpload(); //最多上传 200M 数据 fu.setSizeMax(1024 * 1024 * 200); //超过 1M 的字段数据采用临时文件缓存 fu.setSizeThreshold(1024 * 1024); //采用默认的临时文件存储位置 //fu.setRepositoryPath(...); //设置上传的普通字段的名称和文件字段的文件名所采用的字符集编码 fu.setHeaderEncoding("gb2312"); //得到所有表单字段对象的集合 List fileItems = null; try { fileItems = fu.parseRequest(request); } catch (FileUploadException e) { out.println("解析数据时出现如下问题:"); e.printStackTrace(out); return; } //处理每个表单字段 Iterator i = fileItems.iterator(); while (i.hasNext()) { FileItem fi = (FileItem) i.next(); if (fi.isFormField()) 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org { String content = fi.getString("GB2312"); String fieldName = fi.getFieldName(); request.setAttribute(fieldName,content); } else { try { String pathSrc = fi.getName(); /*如果用户没有在 FORM 表单的文件字段中选择任何文件, 那么忽略对该字段项的处理*/ if(pathSrc.trim().equals("")) { continue; } int start = pathSrc.lastIndexOf('\\'); String fileName = pathSrc.substring(start + 1); File pathDest = new File(uploadDir, fileName); fi.write(pathDest); String fieldName = fi.getFieldName(); request.setAttribute(fieldName, fileName); } catch (Exception e) { out.println("存储文件时出现如下问题:"); e.printStackTrace(out); return; } finally //总是立即删除保存表单字段内容的临时文件 { fi.delete(); } } } //显示处理结果 out.println("用户:" + request.getAttribute("author") + "
"); out.println("来自:" + request.getAttribute("company") + "
"); /*将上传的文件名组合成"file1,file2"这种形式显示出来,如果没有上传 *任何文件,则显示为"无",如果只上传了第二个文件,显示为"file2"。*/ 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org StringBuffer filelist = new StringBuffer(); String file1 = (String)request.getAttribute("file1"); makeUpList(filelist,file1); String file2 = (String)request.getAttribute("file2"); makeUpList(filelist,file2); out.println("成功上传的文件:" + (filelist.length()==0 ? "无" : filelist.toString())); } /** *将一段字符串追加到一个结果字符串中。如果结果字符串的初始内容不为空, *在追加当前这段字符串之前先最加一个逗号(,)。在组合 sql 语句的查询条件时, *经常要用到类似的方法,第一条件前没有"and",而后面的条件前都需要用"and" *作连词,如果没有选择第一个条件,第二个条件就变成第一个,依此类推。 * *@param result 要将当前字符串追加进去的结果字符串 *@param fragment 当前要追加的字符串 */ private void makeUpList(StringBuffer result,String fragment) { if(fragment != null) { if(result.length() != 0) { result.append(","); } result.append(fragment); } } } 在 Windows 资源管理器窗口中将 UploadServlet.java 源文件拖动到 compile.bat 文件的快捷方 式上进行编译,修改 Javac 编译程序报告的错误,直到编译成功通过为止。 (3)修改\webapps\fileupload\WEB-INF\classes\web.xml 文件,在其中 注册和映射 UploadServlet 的访问路径,如例程 1-3 所示。 例程 1-3 web.xml UploadServlet UploadServlet 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org UploadServlet /servlet/UploadServlet (4)重新启动 Tomcat,并在浏览器地址栏中输入如下地址: http://localhost:8080/fileupload/FileUpload.html 填写返回页面中的 FORM 表单,如图 1.4 所示,单击“上载”按钮后,浏览器返回的页面信 息如图 1.5 所示。 图 1.4 图 1.5(这些图的标题栏中的 it315 改为 fileupload) 查看\webapps\it315\upload 目录,可以看到刚才上传的两个文件。 (4)单击浏览器工具栏上的“后退”按钮回到表单填写页面,只在第二个文件字段中选择一 个文件,单击“上载”按钮,浏览器返回的显示结果如图 1.6 所示。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 图 1.6 脚下留心: 上面编写的 Servlet 程序将上传的文件保存在了当前 WEB 应用程序下面的 upload 目录中,这 个目录是客户端浏览器可以访问到的目录。如果用户通过浏览器上传了一个名称为 test.jsp 的文 件,那么用户接着就可以在浏览器中访问这个 test.jsp 文件了,对于本地浏览器来说,这个 jsp 文 件的访问 URL 地址如下所示: http://localhost:8080/fileupload/upload/test.jsp 对于远程客户端浏览器而言,只需要将上面的 url 地址中的 localhost 改写为 Tomcat 服务器的 主机名或 IP 地址即可。用户可以通过上面的 Servlet 程序来上传自己编写的 jsp 文件,然后又可以 通过浏览器来访问这个 jsp 文件,如果用户在 jsp 文件中编写一些有害的程序代码,例如,查看服 务器上的所有目录结构,调用服务器上的操作系统进程等等,这将是一个非常致命的安全漏洞和 隐患,这台服务器对外就没有任何安全性可言了。 1.3 Apache 文件上传组件的源码赏析 经常阅读一些知名的开源项目的源代码,可以帮助我们开阔眼界和快速提高编程能力。Apache 文 件 上 传组件是 Apache 组织开发的一个开源项目,从网址 http://jakarta.apache.org/commons/fileupload 可以下载到 Apache 组件的源程序包,在本书 的附带带光盘中也提供了该组件的源程序包,文件名为 commons-fileupload-1.0-src.zip。该组 件的设计思想和程序编码细节包含有许多值得借鉴的技巧,为了便于有兴趣的读者学习和研究该 组件的源码,本节将分析 Apache 文件上传组件的源代码实现。对于只想了解如何使用 Apache 文 件上传组件来上传文件的读者来说,不必学习本节的内容。在学习本节内容之前,读者需要仔细 学习了笔者编著的《深入体验 java Web 开发内幕——核心基础》一书中的第 6.7.2 节中讲解的“分 析文件上传的请求消息结构”的知识。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 1.3.1 Apache 文件上传组的类工作关系 Apache 文件上传组件总共由两个接口,十二个类组成。在 Apache 文件上传组件的十二个类 中,有两个抽象类,四个的异常类,六个主要类,其中 FileUpLoad 类用暂时没有应用,是为了以 后扩展而保留的。Apache 文件上传组件中的各个类的关系如图 1.7 所示,图中省略了异常类。 图 1.7 DiskFileUpload 类是文件上传组件的核心类,它是一个总的控制类,首先由 Apache 文件上 传组件的使用者直接调用 DiskFileUpload 类的方法,DiskFileUpload 类再调用和协调更底层的 类来完成具体的功能。解析类 MultipartStream 和工厂类 DefaultFileItemFactory 就是 DiskFileUpload 类调用的两个的底层类。MultipartStream 类用于对请求消息中的实体数据进行 具体解析,DefaultFileItemFactory 类对 MultipartStream 类解析出来的数据进行封装,它将每 个表单字段数据封装成一个个的 FileItem 类对象,用户通过 FileItem 类对象来获得相关表单字 段的数据。 DefaultFileItem 是 FileItem 接口的实现类,实现了 FileItem 接口中定义的功能,用户只 需 关心 FileItem 接口, 通过 FileItem 接 口 来 使 用 DefaultFileItem 类 实 现 的 功 能 。 DefaultFileItem 类使用了两个成员变量来分别存储表单字段数据的描述头和主体内容,其中保 存主体内容的变量类型为 DeferredFileOutputStream 类。DeferredFileOutputStream 类是一个 输出流类型,在开始时,DeferredFileOutputStream 类内部使用一个 ByteArrayOutputStream 类 对象来存储数据,当写入它里面的主体内容的大小大于 DiskFileUpload.setSizeThreshold 方法 设置的临界值时,DeferredFileOutputStream 类内部创建一个文件输出流对象来存储数据,并将 前面写入到 ByteArrayOutputStream 类对象中的数据转移到文件输出流对象中。这个文件输出流 对象关联的文件是一个临时文件,它的保存路径由 DiskFileUpload.setRepositoryPath 方法指 定。 Apache 文件上传组件的处理流程如图 1.8 所示。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org Web容器 请求对象 ① DiskFileUpload MultipartStream创建并调用 MultipartStream 请求对象 解析 数据1 数据2 数据3 请求体 ③ 解析出 ② DiskFileUpload 创建并调用 DefaultFileItemFactory ④ DefaultFileItemFactory 数据1 数据2 数据3 请求体 ⑤ 封装 DefaultFileItem1 DefaultFileItem2 DefaultFileItem3 List 封装成 创建 图 1.8 图 1.8 中的每一步骤的详细解释如下: (1)Web 容器接收用户的 HTTP 请求消息,创建 request 请求对象。 (2)调用 DiskFileUpload 类对象的 parseRequest 方法对 request 请求对象进行解析。该方 法首先检查 request 请求对象中的数据内容是否是“multipart/form-data”类型,如果是,该方 法则创建 MultipartStream 类对象对 request 请求对象中的请求体 进行解析。 (3)MultipartStream 类对象对 request 请求体进行解析,并返回解析出的各个表单字段元 素对应的内容。 (4)DiskFileUpload 类对象的 parseRequest 方法接着创建 DefaultFileItemFactory 类对 象,用来将 MultipartStream 类对象解析出的每个表单字段元素的数据封装成 FileItem 类对象。 (5)DefaultFileItemFactory 工厂类对象把 MultipartStream 类对象解析出的各个表单字 段元素的数据封装成若干 DefaultFileItem 类对象,然后加入到一个 List 类型的集合对象中, parseRequest 方法返回该 List 集合对象。 实际上,步骤(3)和步骤(5)是交替同步进行的,即在 MultipartStream 类对象解析每个 表单字段元素时,都会调用 DefaultFileItemFactory 工厂类把该表单字段元素封装成对应的 FileItem 类对象。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 1.3.2 Apache 文件上传组件的核心编程问题 WEB 服务器端程序接收到“multipart/form-data”类型的 HTTP 请求消息后,其核心和基本 的编程工作就是读取请求消息中的实体内容,然后解析出每个分区的数据,接着再从每个分区中 解析出描述头和主体内容部分。 在读取 HTTP 请求消息中的实体内容时,只能调用 HttpServletRequest.getInputStream 方法返 回的字节输入流,而不能调用 HttpServletRequest.getReader 方法返回的字符输入流,因为不管上 传的文件类型是文本的、还是其他各种格式的二进制内容,WEB 服务器程序要做的工作就是将属 于文件内容的那部分数据原封不动地提取出来,然后原封不动地存储到本地文件系统中。如果使 用 HttpServletRequest.getReader 方法返回的字符输入流对象来读取 HTTP 请求消息中的实体内容, 它将 HTTP 请求消息中的字节数据转换成字符后再返回,这主要是为了方便要以文本方式来处理 本来就全是文本内容的请求消息的应用,但本程序要求的是“原封不动”,显然不能使用 HttpServletRequest.getReader 方法返回的字符输入流对象来进行读取。 另外,不能期望用一个很大的字节数组就可以装进 HTTP 请求消息中的所有实体内容,因为 程序中定义的字节数组大小总是有限制的,但应该允许客户端上传超过这个字节数组大小的实体 内容。所以,只能创建一个一般大小的字节数组缓冲区来逐段读取请求消息中的实体内容,读取 一段就处理一段,处理完上一段以后,再读取下一段,如此循环,直到处理完所有的实体内容, 如图 1.9 所示。 „„ 输入字节流 第1次填充缓冲区buffer 第2次填充缓冲区buffer 第3次填充缓冲区buffer „„ 图 1.9 在图 1.9 中,buffer 即为用来逐段读取请求消息中的实体内容的字节数组缓冲区。因为读取 到缓冲区中的数据处理完后就会被抛弃,确切地说,是被下一段数据覆盖,所以,解析和封装过 程必须同步进行,程序一旦识别出图 1.3 中的一个分区的开始后,就要开始将它封装到一个 FileItem 对象中。 程序要识别出图 1.3 中的每一个分区,需要在图 1.9 所示的字节数组缓冲区 buffer 中寻找分 区的字段分隔界线,当找到一个字段分隔界线后,就等于找到了一个分区的开始。笔者在《深入 体验 java Web 开发内幕——核心基础》一书中的第 6.7.2 节中已经讲过,上传文件的请求消息的 Content-Type 头字段中包含有用作字段分隔界线的字符序列,如下所示: content-type : multipart/form-data; boundary=---------------------------7d51383203e8 显然,我们可以通过调用 HttpServletRequest.getHeader 方法读取 Content-Type 头字段的 内容,从中分离出分隔界线的字符序列,然后在字节数组缓冲区 buffer 中寻找分区的字段分隔界 线。content-type 头字段的 boundary 参数中指定的字段分隔界线是浏览器随机产生的,浏览器 保证它不会与用户上传的所有数据中的任何部分出现相同。在这里有一点需要注意,图 1.3 中的 实体内容内部的字段分隔界线与 content-type 头中指定的字段分隔界线有一点细微的差别,前者是 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 在后者前面增加了两个减号(-)字符而形成的,这倒不是什么编程难点。真正的编程难点在于在 字节数组缓冲区 buffer 中寻找分隔界线时,可能会遇到字节数组缓冲区 buffer 中只装入了分隔 界线字符序列的部分内容的情况,如图 1.10 所示。 输入字节流 缓冲区buffer上一次装载的数据 分隔界线 字节序列 „„ 缓冲区buffer下一次装载的数据 图 1.10 要解决这个问题的方法之一就是在查找字段分隔界线时,如果发现字节数组缓冲区 buffer 中只装入了分隔界线字符序列的部分内容,那么就将这一部分内容留给字节数组缓冲区 buffer 的下一次读取,如图 1.11 所示。 输入字节流 缓冲区buffer上一次装载的数据 分隔界线 字节序列 „„ 缓冲区buffer下一次装载的数据 图 1.11 这种方式让字节数组缓冲区 buffer 下一次读取的内容不是紧接着上一次读取内容的后面,而 是重叠上一次读取的一部分内容,即从上一次读取内容中的分隔界线字符序列的第一个字节处开 始读取。这种方式在实际的编程处理上存在着相当大的难度,程序首先必须确定字节数组缓冲区 buffer 上一次读取的数据的后一部分内容正好是分隔界线字符序列的前面一部分内容,而这一部 分内容的长度是不确定的,可能只是分隔界线字符序列的第一个字符,也可能是分隔界线字符序 列的前面 n-1 个字符,其中 n 为分隔界线字符序列的整个长度。另外,即使确定字节数组缓冲区 buffer 上一次读取的数据的后一部分内容正好是分隔界线字符序列的前面一部分内容,但它们在 整个输入字节流中的后续内容不一定就整个分隔界线字符序列的后一部分内容,出现这种情况的 可能性是完全存在,程序必须进行全面和严谨的考虑。 Apache 文件上传组件的解决方法比较巧妙,它在查找字段分隔界线时,如果搜索到最后第 n 个字符时,n 为分隔界线字符序列的长度,发现最后 n 个字符不能与分隔界线字符序列匹配,则 将最后的 n-1 个字符留给字节数组缓冲区 buffer 的下一次读取,程序再对 buffer 的下一次读取 的整个内容从头开始查找字段分隔界线,如图 1.12 所示。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 输入字节流 缓冲区buffer上一次装载的数据 保留n-1 个字符 „ „ 缓冲区buffer下一次装载的数据 图 1.12 Apache 文件上传组件查找字段分隔界线的具体方法,读者可以请参见 MultipartStream 类的 findSeparator()方法中的源代码。 当找到一个分区的开始位置后,程序还需要分辨出分区中的描述头和主体内容,并对这两部 分内容分开存储。如何分辨出一个分区的描述头和主体部分呢?从图 1.3 中可以看到,每个分区 中的描述头和主体内容之间有一空行,再加上描述头后面的换行,这就说明描述头和主体部分之 间是使用“\n”、“\r”、“\n”、“ \r”这四个连续的字节内容进行分隔。因此,程序需要把“\n”、 “\r”、“\n”、“ \r”这四个连续的字节内容作为描述头和主体部分之间的分隔界线,并在字节数 组缓冲区 buffer 中寻找这个特殊的分隔界线来识别描述头和主体部分。 当识别出一个分区中的描述头和主体部分后,程序需要解决的下一个问题就是如何将描述头 和主体部分的数据保存到 FileItem 对象中,以便用户以后可以调用 FileItem 类的方法来获得这 些数据。主体部分的数据需要能够根据用户上传的文件大小有伸缩性地进行存储,因此,程序要 求编写一个特殊的类来封装主体部分的数据,对于这个问题的具体实现细节,读者可参见 1.2.4 小节中讲解的 DeferredFileOutputStream 类来了解。 1.3.3 MultipartStream 类 MultipartStream 类用来对上传的请求输入流进行解析,它 是整个 Apache 上传组件中最复杂 的类。 1.设计思想 MultipartStream 类中定义了一个 byte[]类型的 boundary 成员变量,这个成员变量用于保存图 1.3 中的各个数据分区之间的分隔界线,每个分区分别代表一个表单字段的信息。图 1.3 中的每 个分区又可以分为描述头部分和主体部分,MultipartStream 类中定义了一个 readHeaders()方法 来读取描述头部分的内容 , MultipartStream 类 中定义了一个 readBodyData(OutputStream output)方法来读取主体部分的内容,并将这些内容写入到一个作 为参数传入进来的输出流对象中。readBodyData 方法接收的参数 output 对象在应用中的实际类 型是 DeferredFileOutputStream,这个对象又是保存在 DefaultFileItem 类对象中的一个成员变 量,这样,readBodyData 方法就可以将一个分区的主体部分的数据写入到 DefaultFileItem 类对 象中。 因为图 1.3 中的实体内容内部的字段分隔界线是在 content-type 头中指定的字段分隔界线前面 增加了两个减号(-)字符而形成的,而每个字段分隔界线与它前面内容之间还进行了换行,这个 换行并不属于表单字段元素的内容。所以,MultipartStream 类中的成员变量 boundary 中存储的字 节数组并不是直接从 content-type 头的 boundary 参数中获得的字符序列,而是在 boundary 参数 中指定的字符序列前面增加了四个字节,依次是‘\n’、‘\r’、‘-’和‘-’。MultipartStream 类中 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 定义了一个 readBoundary()方法来读取和识别各个字段之间分隔界线,有一点特殊的是,图 1.3 中的第一个分隔界线前面没有回车换行符,它是无法与成员变量 boundary 中的数据相匹配的,所 以无法调用 readBoundary()方法进行读取,而是需要进行特殊处理,其后的每个分隔界线都与 boundary 中的数据相匹配,可以直接调用 readBoundary()方法进行读取。在本章的后面部分,如 果没有特别说明,所说的分隔界线都是指成员变量 boundary 中的数据内容。 RFC 1867 格式规范规定了描述头和主体部分必须用一个空行进行分隔,如图 1.3 所示,也就 是描述头和主体部分使用“\n”、“\r”、“\n”、“\r”这四个连续的字节内容进行分隔。MultipartStream 类的设计者为了简化编程,在 readHeaders()方法中将“\n”、“\r”、“\n”、“\r”这四个连续的 字节内容连同描述头一起进行读取。readHeaders()方法在读取数据的过程中,当它发现第一个 ‘\n’、‘\r’、‘\n’、‘\r’ 连续的字节序列时就会返回,即使主体部分正好也包含了“\n”、“\r”、 “\n”、“\r”这四个连续的字节内容,但是,它们只会被随后调用的 readBodyData 方法作为主体 内容读取,永远不会被 readHeaders()方法读取到,所以,它们不会与作为描述头和主体部分的 分隔字符序列发生冲突。 由于 readHeaders()方法读取了一个分区中的主体部分前面的所有内容(包括它前面的换 行),而它与下一个分区之间的分隔界线前面的换行又包含在了成员变量 boundary 中,这个换行 将被 readBoundary()方法读取,所以,夹在 readheaders()方法读取的内容和 readBoundary()方 法读取的内容之间的数据全部都属于表单字段元素的内容了,因此,读取分区中的主体部分的 readBodyData(OutputStream output)方法不需要进行特别的处理,它直接将读取的数据写入到 DefaultFileItem 类对象中封装的 DeferredFileOutputStream 属性对象中即可。 2. 构造方法 MultipartStream 类中的一个主要的构造方法的语法定义如下: public (InputStream input, byte[] boundary, int bufSize) 其中,参数 input 是指从 HttpServetRequest 请求对象中获得的字节输入流对象,参数 boundary 是从请求消息头中获得的未经处理的分隔界线,bufSize 指定图 1.10 中的 buffer 缓冲 区字节数组的长度,默认值是 4096 个字节。这个构造方法的源代码如下: public MultipartStream(InputStream input, byte[] boundary, int bufSize) { // 初始化成员变量 this.input = input; this.bufSize = bufSize; this.buffer = new byte[bufSize]; this.boundary = new byte[boundary.length + 4]; this.boundaryLength = boundary.length + 4; //buffer 缓冲区中保留给下次读取的最大字节个数 this.keepRegion = boundary.length + 3; this.boundary[0] = 0x0D; //‘\n’的 16 进制形式 this.boundary[1] = 0x0A; //‘\r’的 16 进制形式 this.boundary[2] = 0x2D; //‘-’的 16 进制形式 this.boundary[3] = 0x2D; //在成员变量 boundary 中生成最终的分隔界线 System.arraycopy (boundary, 0, this.boundary, 4, boundary.length); head = 0; // 成员变量,表示正在处理的这个字节在 buffer 中的位置指针 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org tail = 0; // 成员变量,表示实际读入到 buffer 中的字节个数 } 3. readByte 方法 MultipartStream 类中的 readByte()方法从字节数组缓冲区 buffer 中读一个字节,当 buffer 缓冲区中没有更多的数据可读时,该方法会自动从输入流中读取一批新的字节数据来重新填充 buffer 缓冲区。readByte()方法的源代码如下: public byte readByte () throws IOException { // 判断是否已经读完了 buffer 缓冲区中的所有数据 if (head == tail) { head = 0; //读入新的数据内容来填充 buffer 缓冲区 tail = input.read(buffer, head, bufSize); if (tail == -1) { throw new IOException("No more data is available "); } } return buffer[head++];// 返回当前字节,head++ } 其中,head 变量是 MultipartStream 类中定义的一个 int 类型的成员变量,它用于表示正在 读取的字节在 buffer 数组缓冲区中的位置;tail 变量也是 MultipartStream 类中定义的一个 int 类型的成员变量,它用于表示当前 buffer 数组缓冲区装入的实际字节内容的长度。在 MultipartStream 类中主要是通过控制成员变量 head 的值来控制对 buffer 缓冲区中的数据的读 取和直接跳过某段数据,通过比较 head 与 tail 变量的值了解是否需要向 buffer 缓冲区中装入新 的数据内容。当每次向 buffer 缓冲区中装入新的数据内容后,都应该调整成员变量 head 和 tail 的值。 4. arrayequals 静态方法 MultipartStream 类中定义了一个的 arrayequals 静态方法,用于比较两个字节数组中的前 面一部分内容是否相等,相等返回 true,否则返回 false。arrayequals 方法的源代码如下,参 数 count 指定了对字节数组中的前面几个字节内容进行比较: public static boolean arrayequals(byte[] a, byte[] b,int count) { for (int i = 0; i < count; i++) { if (a[i] != b[i]) { return false; } 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org } return true; } 5. findByte 方法 MultipartStream类中的findByte()方法从字节数组缓冲区buffer中的某个位置开始搜索一 个特定的字节数据,如果找到了,则返回该字节在 buffer 缓冲区中的位置,不再继续搜索,如果 没有找到,则返回-1。findByte 方法的源代码如下,参数 pos 制定了不搜索的起始位置值,value 是要搜索的字节数据: protected int findByte(byte value,int pos) { for (int i = pos; i < tail; i++) { if (buffer[i] == value) { return i; // 找到该值,findByte 方法返回 } } return - 1; } 如果程序需要在 buffer 缓冲区中多次搜索某个特定的字节数据,那就可以循环调用 findByte 方法,只是在每次调用 findByte 方法时,必须不断地改变参数 pos 的值,让 pos 的值等于上次调 用 findByte 的返回值,直到 findByte 方法返回-1 时为止,如图 1.13 所示。 当前缓冲buffer value值 value值 value值 pos pos pos pos 图 1.13 6. findSeparator 方法 MultipartStream 类中的 findSeparator 方法用于从字节数组缓冲区 buffer 中查找成员变量 boundary 中定义的分隔界线,并返回分隔界线的第一个字节在 buffer 缓冲区中的位置,如果在 buffer 缓冲区中没有找到分隔界线,则返回-1。 findSeparator 方法内部首先调用 findByte 方法在 buffer 缓冲区中搜索分隔界线 boundary 的第一个字节内容,如果没有找到,则说明 buffer 缓冲区中没有包含分隔界线;如果 findByte 方法在 buffer 缓冲区中找到了分隔界线 boundary 的第一个字节内容,findSeparator 方法内部 接着确定该字节及随后的字节序列是否确实是分隔界线。findSeparator 方法内部循环调用 findByte 方法,直到找到分隔界线或者 findByte 方法已经查找到了 buffer 缓冲区中的最后 boundaryLength -1 个字节。findSeparator 方法内部为什么调用 findByte 方法查找到 buffer 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 缓冲区中的最后 boundaryLength-1 个字节时就停止查找呢?这是为了解决如图 1.10 所示的 buffer 缓冲区中装入了分隔界线的部分内容的特殊情况,所以在 findSeparator()方法中不要搜 索 buffer 缓冲区中的最后的 boundaryLength -1 个字节,而是把 buffer 缓冲区中的最后这 boundaryLength -1 个字节作为保留区,在下次读取 buffer 缓冲区时将这些保留的字节数据重新 填充到 buffer 缓冲区的开始部分。findSeparator 方法的源代码如下: protected int findSeparator() { int first; int match = 0; int maxpos = tail - boundaryLength;//在 buffer 中搜索的最大位置 for (first = head;(first <= maxpos) && (match != boundaryLength); first++) { //在 buffer 缓冲区中寻找 boundary 的第一个字节 first = findByte(boundary[0], first); /*buffer 中找不到 boundary[0]或者 boundary[0]位于保留区中, 则可以判断 buffer 中不存在分隔界线*/ if (first == -1 || (first > maxpos)) { return -1; } //确定随后的字节序列是否确实是分隔界线的其他字节内容 for (match = 1; match < boundaryLength; match++) { if (buffer[first + match] != boundary[match]) { break; } } } // 当前 buffer 中找到 boundary,返回第一个字节所在位置值 if (match == boundaryLength) { return first - 1; } return -1; // 当前 buffer 中没找到 boundary,返回-1 } 图 1.14 中描述了 findSeparator 方法内部定义的各个变量的示意图。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 当前缓冲buffer boundary[0] maxpos first 取出boundaryLength-1个字节进行比较 保留区字节 图 1.14 findSeparator 方法内部的代码主要包括如下三个步骤: (1)循环调用 findByte(boundary[0], first)找到 buffer 缓冲区中的与 boundary[0]相同 的字节的位置,并将位置记录在 first 变量中。 (2)比较 buffer 缓冲区中的 first 后的 boundaryLength-1 个字节序列是否与 boundary 中 的其他字节序列相同。如果不同,说明这个 first 变量指向的字节不是分隔界线的开始字节,跳 出内循环,将 first 变量加 1 后继续外循环调用 findByte 方法;如果相同,说明在当前缓冲区 buffer 中找到了分隔界线,内循环正常结束,此时 match 变量的值为 boundaryLength,接着执行 外循环将 first 变量加 1,然后执行外循环的条件判断,由于 match != boundaryLength 条件不 成立,外循环也随之结束。 (3)判断 match 是否等于 boundaryLength,如果等于则说明找到了分隔界线,此时返回成 员变量 boundary 的第一个字节在缓冲区 buffer 中位置,由于第(2)中将 first 加 1 了,所以这 里的返回值应该是 first-1;如果不等,说明当前缓冲区 huffer 中没有分隔界线,返回-1。 7. readHeaders 方法 MultipartStream 类中的 readHeaders 方法用于读取一个分区的描述头部分,并根据 DiskFileUpload 类的 setHeaderEncoding 方法设定的字符集编码将描述头部分转换成一个字符串 返回。 在调用 readHeaders 方法之前时,程序已经调用了 findSeparator 方法找到了分隔界线和读 取了分隔界线前面的内容,此时 MultipartStream 类中的成员变量 head 指向了 buffer 缓冲区中 的分隔界线 boundary 的第一个字节,程序接着应调用 readBoundary 方法跳过分隔界线及其随后 的回车换行两个字节,以保证在调用 readHeaders 方法时,成员变量 head 已经指向了分区的描述 头的第一个字节。在 readHeaders 方法内部,直接循环调用 readByte 方法读取字节数据,并把读 到的数据存储在一个字节数组输出流中,直到读取到了连续的两次回车换行字符,就认为已经读 取完了描述头的全部内容,此时成员变量 head 将指向分区中的主体内容的第一个字节。 readHeaders()方法的源代码如下: public String readHeaders()throws MalformedStreamException { int i = 0; //从下面的代码看来,这里定义成一个 byte 即可,不用定义成 byte 数组 byte b[] = new byte[1]; //用于临时保存描述头信息的字节数组输出流 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org //对描述头部分的数据内容过大进行限制处理 int sizeMax = HEADER_PART_SIZE_MAX; int size = 0; while (i < 4) { try { b[0] = readByte(); } catch (IOException e) { throw new MalformedStreamException("Stream ended unexpectedly " ); } size++; //静态常量 HEADER_SEPARATOR 的值为:{0x0D, 0x0A, 0x0D, 0x0A} if (b[0] == HEADER_SEPARATOR[i]) { i++; } else { i = 0; } if (size <= sizeMax) { baos.write(b[0]); // 将当前字节存入缓冲流 } } String headers = null; // 找到 HEADER_SEPARATOR 后,获取描述头 if (headerEncoding != null) { try { headers = baos.toString(headerEncoding); } catch (UnsupportedEncodingException e) { headers = baos.toString(); } } else { headers = baos.toString(); } return headers; 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org } readHeaders 方法循环调用 readByte()方法逐个读取 buffer 缓冲区中的字节,并将读取的字 节与 HEADER_SEPARATOR ={‘\n’,‘\r’,‘ \n’,‘\r’}的第一个字节进行比较,如果这个字节等 于 HEADER_SEPARATOR 的首字节‘\n’,则循环控制因子 i 加 1,这样,下次调用 readByte()方法 读取的字节将与 HEADER_SEPARATOR 中的第二字节比较,如果相等,则依照这种方式比较后面的字 节内容,如果连续读取到了 HEADER_SEPARATOR 字节序列,则循环语句结束。readHeaders 方法将 读取到的每个正常字节写入到了一个字节数组输出流中,其中也包括作为描述头与主体内容之间 的分隔序列 HEADER_SEPARATOR 中的字节数据。由于 readByte()方法会自动移动 head 变量的值和 自动向缓冲区 buffer 中载入数据,所以,readHeaders 方法执行完以后,成员变量 head 指向分 区主体部分的首字节。readHeaders 方法最后将把存入字节数组输出流中的字节数据按指定字符 集编码转换成字符串并返回,该字符串就是描述头字符串。 8. readBodyData 方法 MultipartStream 类中的 readBodyData 方法用于把主体部分的数据写入到一个输出流对象 中,并返回写入到输出流中的字节总数。当调用 readBodyData 方法前,成员变量 head 已经指向 了分区的主体部分的首字节,readBodyData 方法调用完成后,成员变量 head 指向分区分隔界线 的首字节。readBodyData 方法中需要调用 findSeparator 方法找出下一个分区分隔界线的首字节 位置,才能知道这次读取的分区主体内容的结束位置。从分区主体部分的首字节开始,直到在 findSeparator 方法找到的下一个分区分隔界线前的所有数据都是这个分区的主体部分的数据, readBodyData 方法需要把这些数据都写到输出流 output 对象中。如果 findSeparator 方法在 buffer 缓冲区中没有找到分区分隔界线,readBodyData 方法还必须向 buffer 缓冲区中装入新的 数据内容后继续调用 findSeparator 方法进行处理。在向 buffer 缓冲区中装入新的数据内容时, 必须先将上次保留在 buffer 缓冲区中的内容转移进新 buffer 缓冲区的开始处。readBodyData 方 法的源代码如下,传递给 readBodyData 方法的参数实际上是一个 DeferredFileOutputStream 类 对象: public int readBodyData(OutputStream output) throws MalformedStreamException,IOException { // 用于控制循环的变量 boolean done = false; int pad; int pos; int bytesRead; // 写入到输出流中的字节个数 int total = 0; while (!done) { pos = findSeparator();// 搜索分隔界线 if (pos != -1) //缓冲区 buffer 中包含有分隔界线 { output.write(buffer, head, pos - head); total += pos - head; head = pos;//head 变量跳过主体数据,指向分隔界线的首字节 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org done = true;// 跳出循环 } else //缓冲区 buffer 中没有包含分隔界线 { /*根据缓冲区中未被 readHeaders 方法读取的数据内容是否大于图 1.4 中的 保留区的大小,来决定保留到下一次 buffer 缓冲区中的字节个数 */ if (tail - head > keepRegion) { pad = keepRegion; } else { pad = tail - head; } output.write(buffer, head, tail - head - pad); total += tail - head - pad;//统计写入到输出流中的字节个数 /*将上一次 buffer 缓冲区中的未处理的数据转移到 下一次 buffer 缓冲区的开始位置 */ System.arraycopy(buffer, tail - pad, buffer, 0, pad); head = 0; //让 head 变量指向缓冲区的开始位置 //向 buffer 缓冲区中载入新的数据 bytesRead = input.read(buffer, pad, bufSize - pad); if (bytesRead != -1) { //设置 buffer 缓冲区中的有效字节的个数 tail = pad + bytesRead; } else { /*还没有找到分隔界线,输入流就结束了,输入流中的数据格式 显然不正确,保存缓冲区 buffer 中还未处理的数据后抛出异常 */ output.write(buffer, 0, pad); output.flush(); total += pad; throw new MalformedStreamException ("Stream ended unexpectedly "); } } } output.flush(); return total; 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org } 9. discardBodyData 方法 MultipartStream 类中的 discardBodyData 方法用来跳过主体数据,它与 readBodyData 方法 非常相似,不同之处在于 readBodyData 方法把数据写入到一个输出流中,而 discardBodyData 方法是把数据丢弃掉。discardBodyData 方法返回被丢掉的字节个数,方法调用完成后成员变量 head 指向下一个分区分隔界线的首字节。MultipartStream 类中定义 discardBodyData 这个方法, 是为了忽略主体内容部分的第一个分隔界线前面的内容,按照 MIME 规范,消息头和消息体之间的 分隔界线前面可以有一些作为注释信息的内容,discardBodyData 就是为了抛弃这些注释信息而 提供的。discardBodyData 方法的源代码如下: public int discardBodyData() throws MalformedStreamException,IOException { boolean done = false; int pad; int pos; int bytesRead; int total = 0; while (!done) { pos = findSeparator(); if (pos != -1) { total += pos - head; head = pos; done = true; } else { if (tail - head > keepRegion) { pad = keepRegion; } else { pad = tail - head; } total += tail - head - pad; System.arraycopy(buffer, tail - pad, buffer, 0, pad); head = 0; bytesRead = input.read(buffer, pad, bufSize - pad); if (bytesRead != -1) { tail = pad + bytesRead; 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org } else { total += pad; throw new MalformedStreamException ("Stream ended unexpectedly "); } } } return total; } 10. readBoundary 方法 对于图 1.3 中的每一个分区的解析处理,程序首先要调用 readHeaders 方法读取描述头,接 着要调用 readBodyData(OutputStream output)读取主体数据,这样就完成了一个分区的解析。 readBodyData 方法内部调用 findSeparator 方法找到了分隔界线,然后读取分隔界线前面的内容, 此时 MultipartStream 类中的成员变量 head 指向了 buffer 缓冲区中的分隔界线 boundary 的第一 个字节。findSeparator 方法只负责寻找分隔界线 boundary 在缓冲区 buffer 中的位置,不负责 从 buffer 缓冲区中读走分隔界线的字节数据。在调用 readBodyData 方法之后,程序接着应该让 成员变量 head 跳过分隔界线,让它指向下一个分区的描述头的第一个字节,才能调用 readHeaders 方法去读取下一个分区的描述头。 MultipartStream 类中定义了一个 readBoundary 方法,用于让成员变量 head 跳过分隔界线, 让它指向下一个分区的描述头的第一个字节。对于图 1.3 中的最后的分隔界线,它比其他的分隔 界线后面多了两个“-”字符,而其他分隔界线与下一个分区的内容之间还有一个回车换行,所以, readBoundary 方法内部跳过分隔界线后,还需要再读取两个字节的数据,才能让成员变量 head 指向下一个分区的描述头的第一个字节。readBoundary 方法内部读取分隔界线后面的两个字节数 据后,根据它们是回车换行、还是两个“-”字符,来判断这个分隔界线是下一个分区的开始标记, 还是整个请求消息的实体内容的结束标记。如果 readBoundary 方法发现分隔界线是下一个分区的 开始标记,那么它返回 true,否则返回 false。readBoundary()方法的源代码如下: public boolean readBoundary()throws MalformedStreamException { byte[] marker = new byte[2]; boolean nextChunk = false; head += boundaryLength; // 跳过分隔界线符 try { marker[0] = readByte(); marker[1] = readByte(); // 静态常量 STREAM_TERMINATOR ={‘-’、‘-’} if (arrayequals(marker, STREAM_TERMINATOR, 2)) { nextChunk = false; } // 静态常量 FIELD_SEPARATOR ={‘/n’、‘/r’} 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org else if (arrayequals(marker, FIELD_SEPARATOR, 2)) { nextChunk = true; } else { /*如果读到的既不是回车换行,又不是两个减号, 说明输入流有问题,则抛出异常。*/ throw new MalformedStreamException( " Unexpected characters follow a boundary " ); } } catch (IOException e) { throw new MalformedStreamException("Stream ended unexpectedly "); } return nextChunk; } 11. skipPreamble 方法 在本节开始部分已经分析过,因为图 1.3 中的实体内容内部的字段分隔界线是在 content-type 头中指定的字段分隔界线前面增加了两个减号(-)字符而形成的,而每个字段分隔界线与它前面 内容之间还进行了换行,这个换行并不属于表单字段元素的内容,所以,MultipartStream 类中的 成员变量 boundary 中存储的字节数组并不是直接从 content-type 头的 boundary 参数中获得的字 符序列,而是在 boundary 参数中指定的字符序列前面增加了四个字节,依次是‘\n’、‘\r’、‘-’ 和‘-’。readBoundary 方法根据成员变量 boundary 中的内容来识别和跳过各个字段之间的分隔 界线,但是,图 1.3 中的第一个分隔界线前面没有回车换行符,它是无法与成员变量 boundary 中的数据相匹配的,所以无法调用 readBoundary()方法来跳过第一个分隔界线。 MultipartStream 类 中 定 义 的 skipPreamble 方 法 专 门 用 于 跳 过 第 一 个 分 隔 界 线 。 skipPreamble 方法内部的编写思路如下: (1)首先修改成员变量 boundary 中的数据内容,去掉前面的‘\n’和‘\r’这两个字节, 这样,成员变量 boundary 中的数据内容就可以与第一个分隔界线想匹配了。 (2)接着就可以调用 readBoundary 方法跳过第一个分隔界线。 (3)跳过第一个分隔界线后,skipPreamble 方法又恢复成员变量 boundary 中的原始数据, 以便以后调用的 readBoundary 方法可以正常工作。 skipPreamble()方法的源代码如下: public boolean skipPreamble()throws IOException { //修改成员变量 boundary 和 boundaryLength 的值 System.arraycopy(boundary, 2, boundary, 0, boundary.length - 2); boundaryLength = boundary.length - 2; try { //丢掉第一个分隔界线符前的注释数据 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org discardBodyData(); return readBoundary(); } catch (MalformedStreamException e) { return false; } finally { //恢复成员变量 boundary 和 boundaryLength 的值 System.arraycopy(boundary, 0, boundary, 2, boundary.length - 2); boundaryLength = boundary.length; boundary[0] = 0x0D; //‘\n’ boundary[1] = 0x0A; // ‘\r’ } } 12. MultipartStream 类的综合应用 了解了 MultipartStream 类中的各个方法的作用与工作原理后,我们就可以直接调用 MultipartStrean 类中的这些方法来获取浏览器上传的文件内容了。下面列举出一段简单的示意代 码,以帮助读者更好地了解和掌握 MultipartStream 类的各个方法。为了让代码简单易读,下面的 代码没有进行异常处理和没有考虑非文件字段的情况。 public void doPost(HttpServletRequest req, HttpServletResponse res) { //从请求消息中获得 content-type 头字段 String content_type = req.getHeader("content-type"); byte [] boundary = //从 content_type 中分离出分隔界线 //从请求消息中获得用于读取实体内容的输入流对象 InputStream input = req.getInputStream(); //构建 MultipartStream 对象 MultipartStream ms = new MultipartStream(input,boundary); //跳过第一个分隔界线 boolean nexPart = ms.skipPreamble(); //循环处理每个分区 while(nexPart) { String header = ms.readHeaders() String file = //从 header 中提取出文件名 FileOutputStream fos = new FileOutputStream(file); //将分区的主体内容直接写入到一个文件中 ms.readBodyData(fos); //跳过下一个分隔界线,并返回是否是最后一个分隔界线 nexPart = ms.readBoundary(); 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org } } 1.3.4 DeferredFileOutputStream 类 DeferredFileOutputStream 类用来封装 MultipartStream 类解析出的各个分区的主体数据, 它继承了抽象类 ThresholdingOutputStream。DeferredFileOutputStream 类的内部在开始时使用 ByteArrayOutputStream 类对象存储主体数据,当其存储的数据量超过一个限定值时,它就会创 建一个临时文件来储存所有的主体数据。 1.构造方法 DeferredFileOutputStream 类中的一个主要的构造方法的源代码如下: public DeferredFileOutputStream(int threshold, File outputFile) { //设置转换存储方式的临界值 super(threshold); //设置储存主体数据的临时文件 this.outputFile = outputFile; memoryOutputStream = new ByteArrayOutputStream(threshold); //记录当前正使用的输出流对象 currentOutputStream = memoryOutputStream; } DeferredFileOutputStream 类的父类 ThresholdingOutputStream 的构造方法的源代码如下: public ThresholdingOutputStream(int threshold) { this.threshold = threshold; } 2. getData 方法 DeferredFileOutputStream 类中的 getData 方法用于把存储在 ByteArrayOutputStream 类型 的 memoryOutputStream 对象中的主体数据以字节数组的形式返回,如果主体数据当前是存储在 FileOutputStream 类型的 diskOutputStream 对象中,这个方法返回 null。getData 方法的源代 码如下: public byte[] getData() { if (memoryOutputStream != null) { return memoryOutputStream.toByteArray(); } return null; } 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 3. getFile 方法 DeferredFileOutputStream 类中的 getFile 方法返回代表临时文件的 File 类 型 的 outputFile 成员变量的引用。getFile 方法的源代码如下: public File getFile() { return outputFile; } 4. getStream 方法 DeferredFileOutputStream 类中的 getStream 方法用于返回成员变量 currentOutputStream 的引用,即返回当前正用于存储主体数据的输出流对象。getStream 方法的源代码如下: protected OutputStream getStream() throws IOException { return currentOutputStream; } 5. isThresholdExceeded 方法 DeferredFileOutputStream 类中的 isThresholdExceeded 方 法 用 于 判 断 保 存 在 DeferredFileOutputStream 对象中的主体数据是否超过了要转换存储方式的临界值,它是从父类 继承的方法,其源代码如下: public boolean isThresholdExceeded() { return (written > threshold); } 其中,written 是一个 int 型的成员变量,表示当前已经写入到 DeferredFileOutputStream 对象中的字节数,threshold 表示要转换存储方式的那个临界值。 6. isInMemory()方法 DeferredFileOutputStream 类中的 isInMemory 方法用于检测主体数据当前是否存储在内存 (memoryOutputStream 对象)中,是则返回 true,否则返回 false。isInMemory 方法的源代码如 下: public boolean isInMemory() { return (!isThresholdExceeded()); } 7. thresholdReached()方法 DeferredFileOutputStream 类中的 thresholdReached 方法用于把内存(memoryOutputStream 对象)中的数据转入到 outputFile 文件中,并切换到文件存储模式状态(让 currentOutputStream 指向文件输出流对象,将 memoryOutputStream 对象置为 null)。thresholdReached 方法的源代码 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 如下: protected void thresholdReached() throws IOException { byte[] data = memoryOutputStream.toByteArray(); FileOutputStream fos = new FileOutputStream(outputFile); fos.write(data); diskOutputStream = fos; // 改变成员变量 currentOutputStream = fos; // 改变成员变量 memoryOutputStream = null; // 将对象置空 } diskOutputStream 是 FileOutputStream 类型的一个成员变量,表示临时文件输出流对象。 8. checkThreshold(int count)方法 DeferredFileOutputStream 类中的 checkThreshold 方法从父类继承而来,用于判断如果写 入指定数量的字节,写入到 DeferredFileOutputStream 对象中的总字节数是否会超过要转换存储 方式的那个临界值,如果是,则调用 thresholdReached()方法把内存中的数据转入到文件中。 checkThreshold 方法的源代码如下: protected void checkThreshold(int count) throws IOException { if (!thresholdExceeded && (written + count > threshold)) { thresholdReached(); thresholdExceeded = true; } } 传递给 checkThreshold 方法的 count 参数必须大于 1。thresholdExceeded 是一个 boolean 类型的成员变量,表示写入到 DeferredFileOutputStream 对象中的总字节数是否超过了要转换存 储方式的那个临界值,是则为 true,否则为 false。 9. write 方法 DeferredFileOutputStream 类中实现了 OutputStream 类中定义的多种重载形式的 write 方 法,用于向 DeferredFileOutputStream 对象中写入一个或多个字节,下面是两个重载的 write 方法的源代码: public void write(int b) throws IOException { checkThreshold(1); getStream().write(b); written++; } public void write(byte b[]) throws IOException { 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org checkThreshold(b.length); getStream().write(b); written += b.length; } 从 DeferredFileOutputStream 类中实现的 write 方法的源代码中可以看到,write 方法在写 入一个或多个字节之前,都先要调用 checkThreshold 方法检查是否要转换存储方式和完成存储方 式的转换。 上面仅讲解了一些能阐述清楚 DeferredFileOutputStream 类的工作机制的方法和将被 DefaultFileItem 类调用的主要方法,DeferredFileOutputStream 类的其他方法请读者自己参看 其源文件。 1.3.5 DefaultFileItem 类 DefaultFileItem 类实现了 FileItem 接口,它用用封装 MultipartStream 类解析出的一个分 区的数据。DefaultFileItem 类中定义了多个私有属性来分别保存分区的描述头部分的各个字段 和参数的值,它还定义了一个 DeferredFileOutputStream 类型的成员变量 dfos 来保存分区的主 体部分。 1.构造方法 DefaultFileItem 类的构造方法的源代码如下: DefaultFileItem(String fieldName, String contentType, boolean isFormField, String fileName, int sizeThreshold, File repository) { this.fieldName = fieldName; this.contentType = contentType; this.isFormField = isFormField; this.fileName = fileName; this.sizeThreshold = sizeThreshold; this.repository = repository; } DefaultFileItem 构造方法中所用到的各个成员变量的意义和作用如下: (1)成员变量 fieldName 中保存了如图 1.3 所示的 Content-Disposition 字段中的 name 参 数的值,以后可以调用 DefaultFileItem 类的 getFieldName()方法来获取这个变量值; (2)成员变量 fileName 中保存了如图 1.3 所示的 Content-Disposition 字段中的 filename 参数的值,以后可以调用 DefaultFileItem 类的 getName()方法来获取这个变量值; (3)成员变量 contentType 中保存了如图 1.3 所示的 contentType 字段的值,以后可以调用 DefaultFileItem 类的 getContentType()方法来获取这个变量值; (4)成员变量 sizeThreshold 用于设置 DefaultFileItem 中的 DeferredFileOutputStream 对象转换存储模式的临界值,如果超过该值,则 DeferredFileOutputStream 对象将把主体数据转 入一个临时文件中; (5)成员变量 isFormField 用于说明当前分区内容是普通表单字段的信息,还是文件表单字 段的信息,true 表示当前分区内容是普通表单字段的信息,false 表示当前分区内容是文件表单 字段的信息。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org (6)成员变量 repository 用于设置主体数据存入到的临时文件的目录。 2.delete 方法 DefaultFileItem 类的 delete 方法清空成员变量 cachedContent,并删除临时文件,这个方 法主要用于删除因各种原因而没有及时被删除掉的临时文件。delete 方法的源代码如下: public void delete() { cachedContent = null; File outputFile = getStoreLocation() // 该方法返回临时文件对应的 File 对象 if (outputFile != null && outputFile.exists()) { outputFile.delete(); } } 其中的 cachedContent 变量是一个 byte 数 组 类 型 的 成 员 变 量 , 它 代 表 存 储 在 DeferredFileOutputStream 对象中的 ByteArrayOutputStream 对象中的数据;outputFile 变量代 表临时文件的 File 类对象。 3.finalize 方法 DefaultFileItem 类覆盖了 Object 类的 finalize 方法,在 finalize 方法中删除临时文件, 以增强清除临时文件的保险系数。finalize 方法的源代码如下: protected void finalize() { File outputFile = dfos.getFile(); // 返回临时文件对应的 File 对象 if (outputFile != null && outputFile.exists()) { outputFile.delete(); } } 4. getSize 方法 DefaultFileItem 类的 getSize 方法返回 DeferredFileOutputStream 类型的成员变量 dfos 对象中保存的字节数,即返回分区主体部分的数据大小。getSize 方法的源代码如下: public long getSize() { if (cachedContent != null) { return cachedContent.length; } else if (dfos.isInMemory()) { return dfos.getData().length; } 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org else { return dfos.getFile().length(); } } 5. get 方法 DefaultFileItem 类的 get 方法以字节数组的形式,返回存储在成员变量 dfos 对象(存储分 区主体部分的数据)中的所有内容。如果数据是存储在临时文件中,且文件中的数据量非常大, 将会出现内存不够的错误。get 方法的源代码如下: public byte[] get() { if (dfos.isInMemory()) { if (cachedContent == null) { cachedContent = dfos.getData();// 取出内存中所有数据 } return cachedContent; } byte[] fileData = new byte[(int) getSize()];//取出临时文件中数据 FileInputStream fis = null; try { fis = new FileInputStream(dfos.getFile()); fis.read(fileData); } catch (IOException e) { fileData = null; } finally // 无论如何,关闭流 { if (fis != null) { try { fis.close(); } catch (IOException e) { // 不作任何处理 } } 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org } return fileData; } 6. getInputStream 方法 DefaultFileItem 类的 getInputStream 方法以输入流的形式返回存储在成员变量 dfos 对象 (存储分区主体部分的数据)中的数据,它的源代码如下: public InputStream getInputStream() throws IOException { if (!dfos.isInMemory()) { return new FileInputStream(dfos.getFile()); } if (cachedContent == null) { cachedContent = dfos.getData(); } return new ByteArrayInputStream(cachedContent); } 7. getUniqueId()方法 DefaultFileItem 类的 getUniqueId 方法返回一个至少包含八个字符的字符串,这个字符串 被用作临时文件名称的标识部分。getUniqueId 方法保证该字符串从 WEB 服务器启动以来是唯一 的,它的源代码如下: private static String getUniqueId() { int current; // 同步处理,防止在多线程情况下出现临时文件重名的现象 synchronized (DefaultFileItem.class) { current = counter++; } String id = Integer.toString(current); if (current < 100000000) { //将 id 变成前面补 0 的八位字符串 id = (“00000000” + id).substring(id.length()); } return id; } getUniqueId 方法中两个编程小技巧值得我们借鉴: (1)为了保证 id 在多线程情况下的唯一性,getUniqueId 方法中加入了同步代码块,其同 步锁对象选择了 DefaultFileItem.class,DefaultFileItem.class 为代表 DefaultFileItem 类 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org 的字节码的对象,它与静态变量 counter 在当前类装载器的名称空间中都是唯一的,从而可以用 作静态变量 counter 的同步锁对象。 (2)为了将变量 id 变成前面补 0 的八位字符串,getUniqueId 方法使用了如下语句,真是 有点巧妙: id = (“00000000” + id).substring(id.length()); 8. getTempFile 方法 DefaultFileItem 类的 getTempFile 方法返回一个代表临时文件的 File 类对象,这个 File 类对象将被用来保存分区主体部分的数据。getTempFile 方法的源代码如下: protected File getTempFile() { File tempDir = repository; if (tempDir == null) { tempDir = new File(System.getProperty("java.io.tmpdir")); } String fileName = "upload_" + getUniqueId() + ".tmp"; File f = new File(tempDir, fileName); f.deleteOnExit(); return f; } 9. getOutputStream 方法 DefaultFileItem 类的 getOutputStream 方法返回 DefaultFileItem 对象中的用于保存分区 主体数据的 DeferredFileOutputStream 类型的 dfos 成员变量的引用,它的源代码如下: public OutputStream getOutputStream()throws IOException { if (dfos == null) { File outputFile = getTempFile();// 产生一个唯一文件名的文件对象 dfos = new DeferredFileOutputStream(sizeThreshold, outputFile); } return dfos; } 10.getString 方法 DefaultFileItem 类的 getString 方法以字符串的形式返回 dfos 对象中的所有内容,即以字 符串形式返回分区的主体部分的数据。getString 方法有两种重载形式,它的源代码如下: public String getString(String encoding) throws UnsupportedEncodingException { return new String(get(), encoding); } 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org public String getString() { return new String(get()); } 11. write(File file)方法 DefaultFileItem 类的 write 方法用于把 dfos 对象中的所有数据写入到一个文件中。write 方法在把 dfos 对象的数据写入到指定的文件中时,采用的处理办法也很巧妙,首先是把文件重 命名成指定的文件名,如果重命名不成功,则按指定的文件名新建一个文件输出流对象,再将 dfos 对象中的所有数据循环写入到新建文件中。write 方法的源代码如下: public void write(File file) throws Exception { if (isInMemory())// 主体数据存在内存中 { FileOutputStream fout = null; try { fout = new FileOutputStream(file); fout.write(get()); } finally // 无论任何都要关闭输出流 { if (fout != null) { fout.close(); } } } else // 主体数据存储在临时文件中 { //返回临时文件对应的 File 对象 File outputFile = getStoreLocation(); if (outputFile != null) { //不能改文件名 if (!outputFile.renameTo(file)) { BufferedInputStream in = null; BufferedOutputStream out = null; try { in = new BufferedInputStream( new FileInputStream(outputFile)); out = new BufferedOutputStream( 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org new FileOutputStream(file)); byte[] bytes = new byte[2048]; int s = 0; while ((s = in.read(bytes)) != -1) { out.write(bytes, 0, s); } } finally { try { in.close(); } catch (IOException e) { // 忽略 } try { out.close(); } catch (IOException e) { // ignore } } } } else // outputFile = null,找不到指定文件 { throw new FileUploadException( "Cannot write uploaded file to disk! "); } } } 1.3.6 DefaultFileItemFactory 类 DefaultFileItemFactory 类实现了 FileItemFactory 接口,该类负责创建 DefaultFileItem 类的实例对象。 1. 构造方法 DefaultFileItemFactory 类的构造方法有如下两种重载形式: 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org public DefaultFileItemFactory(); public DefaultFileItemFactory(int sizeThreshold,File repository); 在第二个构造方法中,参数 sizeThreshold 用于设置是否使用临时文件保存解析出的数据的 那个临界值,参数 repository 用于设置临时文件的存储目录。DefaultFileItemFactory 类中也 定义了用于设置和读取 sizeThreshold 和 repository 属性的 setter 和 getter 方法,对于通过第 一个构造方法创建的 DefaultFileItemFactory 实例对象,可以通过相应的 setter 方法设置 sizeThreshold 和 repository 属性。 2. createItem 方法 DefaultFileItemFactory 类的 createItem 方法用于创建一个 FileItem 对象,其 源代码如下: public FileItem createItem(String fieldName, String contentType,boolean isFormField,String fileName) { return new DefaultFileItem(fieldName, contentType, isFormField, fileName, sizeThreshold, repository); } createItem 方法中的各个参数的详细信息请参看 1.2.5 节中的 DefaultFileItem 类的构造方 法的讲解。 1.3.7 DiskFileUpload 类 DiskFileUpload 类继承了 FileUploadBase 类,它是 Apache 文件上传组件对外提供的入口操 作类,应用程序开发人员通过这个类来与 Apache 文件上传组件进行交互。DiskFileUpload 类除 了定义了各个成员变量外,其中还定义了许多代表各个头字段名称和参数名称的静态常量。 1.构造方法 DiskFileUpload 类有两个重载的构造方法,它们的源代码如下: public DiskFileUpload() { super(); this.fileItemFactory = new DefaultFileItemFactory(); } public DiskFileUpload(DefaultFileItemFactory fileItemFactory) { super(); this.fileItemFactory = fileItemFactory; } 可见,DiskFileUpload 类的构造方法的主要作用是准备 FileItemFactory 类的实例对象,以 便以后调用这个 FileItemFactory 实例对象来创建各个 FileItem 对象。 2.isMultipartContent 方法 DiskFileUpload 类的 isMultipartContent 方法是从父类继承来的,其作用是判断当前请求 消 息 的 类 型 是 不 是 “ multipart/form-data ”,如 果 是 则 返回 true , 否则返回 false 。 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org isMultipartContent 方法的源代码如下: public static final boolean isMultipartContent(HttpServletRequest req) { //静态常量 CONTENT_TYPE ="Content-type" String contentType = req.getHeader(CONTENT_TYPE); if (contentType == null) { return false; } //静态常量 MULTIPART ="multipart/" if (contentType.startsWith(MULTIPART)) { return true; } return false; } 3. parseHeaders 方法 DiskFileUpload 类的 parseHeaders 方法是从父类继承来的,其作用是从 MulitpartStream 类解析出的一个分区的描述头串字符中提取各个头字段的信息,并将这些信息保存到一个 HashMap 对象中后返回,HashMap 中的关键字为头字段的名称,HashMap 中的值为字段的值。参看 图 1.3,可以知道通常有两个字段名:Content-Disposition 和 Content-Type,parseHeaders 方 法会将它们统一转换成小写的形式后再保存到 HashMap 对象中。parseHeaders 方法的源代码如下, 参数 headerPart 就是从 MulitpartStream 类的 readHeaders 方法返回的一个分区的描述头字符 串: protected Map parseHeaders(String headerPart) { Map headers = new HashMap(); // int MAX_HEADER_SIZE = 1024 char buffer[] = new char[MAX_HEADER_SIZE]; boolean done = false; int j = 0; int i; String header, headerName, headerValue; try { while (!done) { i = 0; //从描述头部分读取一行信息,行结束的依据是连续的回车换行符 while (i < 2 || buffer[i - 2] != '\r' || buffer[i - 1] != '\n') { buffer[i++] = headerPart.charAt(j++); } 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org // 丢掉最后的回车换行符 header = new String(buffer, 0, i - 2); //如果为空行,表示处理完了整个描述头字符串,结束循环 if (header.equals("")) { done = true; } else { if (header.indexOf(':') == -1) { //这一行格式有问题,跳过这行后继续下一行的处理 continue; } //获取头字段的名称,并将它们统一成小写形式 headerName = header.substring(0, header.indexOf(':')) .trim().toLowerCase(); headerValue = header.substring(header.indexOf(':') + 1).trim(); /*将头字段信息保存到 HashMap 对象中,如果一个头字段出现了多次, 对每次的值进行组合,并以逗号进行分隔 */ if (getHeader(headers, headerName) != null) { headers.put(headerName, getHeader(headers, headerName) + ','+ headerValue); } else { headers.put(headerName, headerValue); } } } } catch (IndexOutOfBoundsException e) { // 如果有异常,说明描述部分格式有问题,不作任何处理 } return headers; } 4. getHeader 方法 DiskFileUpload 类的 getHeader 方法是从父类继承来的,它 的作用是从保存各个头字段信息 的 HashMap 对象中检索某个头字段的值。getHeader 方法的源代码如下,其中参数 headers 代表 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org parseHeaders 方法创建的 HashMap 对象,参数 name 代表字段的名称: protected final String getHeader(Map headers, String name) { return (String) headers.get(name.toLowerCase()); } 5. getFieldName 方法 DiskFileUpload 类的 getFieldName 方法是从父类继承来的,该方法从描述头部分的 content-disposition 头字段中检索 name 参数的值,也就是获取一个分区所对应的表单字段的名 称。getFieldName 方法的源代码如下: protected String getFieldName(Map headers) { String fieldName = null; // 静态常量 CONTENT_DISPOSITION =“content-disposition” String cd = getHeader(headers, CONTENT_DISPOSITION); // 静态常量 FORM_DATA = "form-data" if (cd != null && cd.startsWith(FORM_DATA)) { int start = cd.indexOf("name=\" "); int end = cd.indexOf(‘"‘, start + 6); if (start != -1 && end != -1) { fieldName = cd.substring(start + 6, end); } } return fieldName; } 6. getFileName 方法 DiskFileUpload 类的 getFileName 方法是从父类继承来的,该方法从描述头部分的 content-disposition 头字段中检索 filename 参数的值,也就是获得文件上传字段中的文件名。 getFileName 方法的源代码如下: protected String getFileName(Map /* String, String */ headers) { String fileName = null; String cd = getHeader(headers, CONTENT_DISPOSITION); //静态常量 ATTACHMENT = "attachment" if (cd.startsWith(FORM_DATA) || cd.startsWith(ATTACHMENT)) { int start = cd.indexOf("filename=\""); int end = cd.indexOf('"', start + 10); if (start != -1 && end != -1) { 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org fileName = cd.substring(start + 10, end).trim(); } } return fileName; } 7. createItem 方法 DiskFileUpload 类的 createItem 方 法 是 从 父 类 继 承 来的,它调用 作 为成 员 变 量 的 DefaultFileItemFactory 实例对象来创建一个 DefaultFileItem 类对象。createItem 方法创建出 的 DefaultFileItem 类对象当前只包含从分区的描述头部分提取出的信息,而没有包含分区的主 体部分的内容。createItem 方法的源代码如下: protected FileItem createItem(Map headers, boolean isFormField) throws FileUploadException { return getFileItemFactory().createItem(getFieldName(headers), getHeader(headers, CONTENT_TYPE),isFormField, getFileName(headers)); } 8. parseRequest 方法 DiskFileUpload 类的 parseRequest 方法是从父类继承来的,它是 DiskFileUpload 类的核心 方法。parseRequest 方法负责创建 MultipartStream 类的实例对象,并调用该对象来解析请求消 息中实体内容,然后再调用作为成员变量的 DefaultFileItemFactory 对象将解析出来数据封装成 一个个的 DefaultFileItem 对象,最后将这些 FileItem 对象加入进一个 List 类型的集合对象中 返回。parseRequest 方法的源代码如下: public List parseRequest(HttpServletRequest req) throws FileUploadException { if (null == req) { throw new NullPointerException("req parameter"); } //定义用于存储所有 FileItem 对象的 List 集合 ArrayList items = new ArrayList(); String contentType = req.getHeader(CONTENT_TYPE); if ((null == contentType) || (!contentType.startsWith(MULTIPART))) { throw new InvalidContentTypeException("the request doesn't contain a " + MULTIPART_FORM_DATA // 静态成员常量"multipart/form-data" + " or " + MULTIPART_MIXED // 静态成员常量"multipart/mixed" + " stream, content type header is " + contentType); } 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org // 获得请求消息的实体内容的大小 int requestSize = req.getContentLength(); if (requestSize == -1) { throw new UnknownSizeException( "the request was rejected because it's size is unknown"); } // 实体内容超过了用户限定的大小 if (sizeMax >= 0 && requestSize > sizeMax) { throw new SizeLimitExceededException( "the request was rejected because " + "it's size exceeds allowed range"); } try { //从请求消息的 Content-Type 头字段中获取分隔界线 int boundaryIndex = contentType.indexOf("boundary="); if (boundaryIndex < 0) { throw new FileUploadException( "the request was rejected because " + "no multipart boundary was found"); } byte[] boundary = contentType.substring( boundaryIndex + 9).getBytes(); //获取实体内容 InputStream input = req.getInputStream(); //创建 MultipartStream 对象并对实体内容进行解析 MultipartStream multi = new MultipartStream(input, boundary); multi.setHeaderEncoding(headerEncoding); boolean nextPart = multi.skipPreamble(); while (nextPart) { Map headers = parseHeaders(multi.readHeaders()); String fieldName = getFieldName(headers) if (fieldName != null) { String subContentType = getHeader(headers, CONTENT_TYPE); /* 在文件上传中很少见到"multipart/mixed"这种类型, 读者可直接跳过下面的 if 语句块,只看对应 else 语句块*/ if (subContentType != null && subContentType .startsWith(MULTIPART_MIXED)) 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org { // multipart/mixed 的文件嵌套了几个文件,因此解析嵌套文件 byte[] subBoundary = subContentType.substring( subContentType.indexOf("boundary=") + 9).getBytes(); // 获得用于分隔嵌套文件的分隔界线 multi.setBoundary(subBoundary); boolean nextSubPart = multi.skipPreamble(); while (nextSubPart) { headers = parseHeaders(multi.readHeaders()); if (getFileName(headers) != null) { FileItem item = createItem(headers, false); OutputStream os = item.getOutputStream(); try { multi.readBodyData(os); } finally { os.close(); } items.add(item); } else { multi.discardBodyData(); } nextSubPart = multi.readBoundary(); } multi.setBoundary(boundary); } else //读者只需关心这个 else 语句块 { if (getFileName(headers) != null) { // 此时处理的是一个文件表单字段 FileItem item = createItem(headers, false); OutputStream os = item.getOutputStream(); try { multi.readBodyData(os); } finally 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org { os.close(); } items.add(item); } else { //此时处理的是一个普通表单字段 FileItem item = createItem(headers, true); OutputStream os = item.getOutputStream(); try { multi.readBodyData(os); } finally { os.close(); } items.add(item); } } } else //表单字段的名称不存在,丢弃该分区的内容 { multi.discardBodyData(); } nextPart = multi.readBoundary();//读取下一个分隔界线 } } catch (IOException e) { throw new FileUploadException( "Processing of " + MULTIPART_FORM_DATA + " request failed. " + e.getMessage()); } return items; } 在 DiskFileUpload 类中还定义了 parseRequest 方法的一个重载形式,这个重载的 parseRequest 方法内部调用了上面的 parseRequest 方法,同时设置了一些参数信息。这个重载 的 parseRequest 方法的源代码如下: public List parseRequest(HttpServletRequest req, int sizeThreshold,long sizeMax, String path) throws FileUploadException { 深入体验 JavaWeb 开发内幕—高级特性 张孝祥著 http://www.it315.org setSizeThreshold(sizeThreshold);//设置要切换存储模式的临界值 setSizeMax(sizeMax); // 设置允许上传的文件大小 setRepositoryPath(path); // 临时文件存储路径 return parseRequest(req); } 本节分析了 Apache 文件上传组件的实现思想和主要的源代码,建议读者参照本节的方式多分 析一些开源项目。
还剩48页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享pdf获得金币 ] 3 人已下载

下载pdf

pdf贡献者

vicky

贡献于2012-06-05

下载需要 10 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf