文件上传组件的应用与编写
在许多Web站点应用中都需要为用户提供通过浏览器上传文档资料的功能,例如,上传邮件附件、个人相片、共享资料等。对文件上传功能,在
浏览器端提供了较好的支持,只要将FORM表单的enctype属性设置为“multipart/form-data”即可;但在Web服务器端如何获取浏览器上传的文
件,需要进行复杂的编程处理。为了简化和帮助Web开发人员接收浏览器上传的文件,一些公司和组织专门开发了文件上传组件。本章将详细介
绍如何使用Apache文件上传组件,以及分析该组件源程序的设计思路和实现方法。
1.1 准备实验环境
按下面的步骤为本章的例子程序建立运行环境:
(1) 在Tomcat 5.5.12的<tomcat的安装目录>/webapps目录中创建一个名为fileupload的子目录,并在fileupload目录中创建一个名为 test.html
的网页文件,在该文件中写上“这是test.html页面的原始内容!”这几个字符。
(2) 在<tomcat的安装目录>/webapps/fileupload目录中创建一个名为WEB-INF的子目录,在WEB-INF目录中创建一个名为classes的子目录和一个
web.xml 文件,web.xml文件内容如下:
<web-app>
</web-app>
(3) 要使用Apache文件上传组件,首先需要安装Apache文件上传组件包。在<tomcat的安装目录>/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文件,将它放置进<tomcat的安装目录>/webapps/fileupload/WEB-INF/lib目录中,就完成了Apache文件上传组件的安装。
(4) 在<tomcat的安装目录>/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文件存放
到<tomcat安装目录>/webapps/fileupload/WEB-INF/classes目录中,上面的CLASSPATH环境变量的设置值由于排版原因进行了换行,实际上不
应该有换行。接着在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文件,在该文件的<Host>元
素中增加如下一个<Context>子元素:
<Context path="/fileupload" docBase="fileupload" reloadable="true"/>
保存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所示。
图 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文件上传组件将用临时文件来保存这些数据;但如果上传的文件很小,例如上传600个字节的文件,显然
将其直接保存在内存中更加有效。setSizeThreshold方法用于设置是否使用临时文件保存解析出的数据的那个临界值,该方法传入的参数的单
位是字节。其完整语法定义如下:
public void setSizeThreshold(int sizeThreshold)
3. setRepositoryPath 方法
setRepositoryPath 方法用于设置setSizeThreshold方法中提到的临时文件的存放目录,这里要求使用绝对路径。其完整语法定义如下:
public void setRepositoryPath(String repositoryPath)
如果不设置存放路径,那么临时文件将被储存在"java.io.tmpdir"这个JVM环境属性所指定的目录中,tomcat 5.5.9将这个属性设置为了
“<tomcat 安装目录>/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文件上传组件都需要将它们转换成字符串形式返回,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 方法设置的临界值大小时,这个流对象关联到一片内存,主体内容将会被保存在内存中。当主体内容的数据
超过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)
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)在<tomcat安装目录>/webapps/fileupload目录中按例程1-1编写一个名为FileUpload.html的HTML页面,该页面用于提供文件上传的 FORM
表单,表单的enctype属性设置值为“multipart/form-data”,表单的action属性设置为“servlet/UploadServlet”。
例程 1-1 FileUpload.html
<html>
<head>
<title>upload experiment</title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<body>
<h3>测试文件上传组件的页面 </h3>
<form action="servlet/UploadServlet"
enctype="multipart/form-data" method="post">
作者: <input type="text" name="author"><br>
来自: <input type="text" name="company"><br>
文件1: <input type="file" name="file1"><br>
文件2: <input type="file" name="file2"><br>
<input type="submit" value="上载 ">
</form>
</body>
</html>
(2)在<tomcat的安装目录>/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.*;
import java.util.*;
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);
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())
{
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") + "<br>");
out.println("来自: " + request.getAttribute("company") + "<br>");
/*将上传的文件名组合成"file1,file2"这种形式显示出来,如果没有上传
* 任何文件,则显示为"无",如果只上传了第二个文件,显示为"file2"。 */
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) 修改<tomcat的安装目录>/webapps/fileupload/WEB-INF/classes/web.xml文件,在其中注册和映射UploadServlet的访问路径,如例程 1-3
所示。
例程 1-3 web.xml
<web-app>
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/servlet/UploadServlet</url-pattern>
</servlet-mapping>
</web-app>
(4)重新启动Tomcat,并在浏览器地址栏中输入如下地址:
http://localhost:8080/fileupload/FileUpload.html
填写返回页面中的FORM表单,如图1.4所示,单击“上载”按钮后,浏览器返回的页面信息如图1.5所示。
图 1.4
图1.5(这些图的标题栏中的it315改为fileupload)
查看<tomcat安装目录>/webapps/it315/upload目录,可以看到刚才上传的两个文件。
(4) 单击浏览器工具栏上的“后退”按钮回到表单填写页面,只在第二个文件字段中选择一个文件,单击“上载”按钮,浏览器返回的显示结果
如图1.6所示。
图 1.6
M脚下留心:
上面编写的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节
中讲解的“分析文件上传的请求消息结构”的知识。
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所示。
图 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类对象。
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.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头中指
定的字段分隔界线有一点细微的差别,前者是在后者前面增加了两个减号(-)字符而形成的,这倒不是什么编程难点。真正的编程难点在于在
字节数组缓冲区buffer中寻找分隔界线时,可能会遇到字节数组缓冲区buffer中只装入了分隔界线字符序列的部分内容的情况,如图1.10所示
。
图 1.10
要解决这个问题的方法之一就是在查找字段分隔界线时,如果发现字节数组缓冲区buffer中只装入了分隔界线字符序列的部分内容,那么就将
这一部分内容留给字节数组缓冲区buffer的下一次读取,如图1.11所示。
图 1.11
这种方式让字节数组缓冲区buffer下一次读取的内容不是紧接着上一次读取内容的后面,而是重叠上一次读取的一部分内容,即从上一次读取
内容中的分隔界线字符序列的第一个字节处开始读取。这种方式在实际的编程处理上存在着相当大的难度,程序首先必须确定字节数组缓冲区
buffer 上一次读取的数据的后一部分内容正好是分隔界线字符序列的前面一部分内容,而这一部分内容的长度是不确定的,可能只是分隔界线
字符序列的第一个字符,也可能是分隔界线字符序列的前面n-1个字符,其中n为分隔界线字符序列的整个长度。另外,即使确定字节数组缓冲
区buffer上一次读取的数据的后一部分内容正好是分隔界线字符序列的前面一部分内容,但它们在整个输入字节流中的后续内容不一定就整个
分隔界线字符序列的后一部分内容,出现这种情况的可能性是完全存在,程序必须进行全面和严谨的考虑。
Apache 文件上传组件的解决方法比较巧妙,它在查找字段分隔界线时,如果搜索到最后第n个字符时,n为分隔界线字符序列的长度,发现最后 n
个字符不能与分隔界线字符序列匹配,则将最后的n-1个字符留给字节数组缓冲区buffer的下一次读取,程序再对buffer的下一次读取的整个内
容从头开始查找字段分隔界线,如图1.12所示。
图 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类中定义了一个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中的位置指针
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;
}
}
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所示。
图 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缓冲区中的最后
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方法内部定义的各个变量的示意图。
图 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() ;
// 对描述头部分的数据内容过大进行限制处理
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 ;
}
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变量跳过主体数据,指向分隔界线的首字节
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 ;
}
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 ;
}
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’}
else if (arrayequals(marker, FIELD_SEPARATOR, 2))
{
nextChunk = true;
}
else
{
/* 如果读到的既不是回车换行,又不是两个减号,
在许多Web站点应用中都需要为用户提供通过浏览器上传文档资料的功能,例如,上传邮件附件、个人相片、共享资料等。对文件上传功能,在
浏览器端提供了较好的支持,只要将FORM表单的enctype属性设置为“multipart/form-data”即可;但在Web服务器端如何获取浏览器上传的文
件,需要进行复杂的编程处理。为了简化和帮助Web开发人员接收浏览器上传的文件,一些公司和组织专门开发了文件上传组件。本章将详细介
绍如何使用Apache文件上传组件,以及分析该组件源程序的设计思路和实现方法。
1.1 准备实验环境
按下面的步骤为本章的例子程序建立运行环境:
(1) 在Tomcat 5.5.12的<tomcat的安装目录>/webapps目录中创建一个名为fileupload的子目录,并在fileupload目录中创建一个名为 test.html
的网页文件,在该文件中写上“这是test.html页面的原始内容!”这几个字符。
(2) 在<tomcat的安装目录>/webapps/fileupload目录中创建一个名为WEB-INF的子目录,在WEB-INF目录中创建一个名为classes的子目录和一个
web.xml 文件,web.xml文件内容如下:
<web-app>
</web-app>
(3) 要使用Apache文件上传组件,首先需要安装Apache文件上传组件包。在<tomcat的安装目录>/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文件,将它放置进<tomcat的安装目录>/webapps/fileupload/WEB-INF/lib目录中,就完成了Apache文件上传组件的安装。
(4) 在<tomcat的安装目录>/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文件存放
到<tomcat安装目录>/webapps/fileupload/WEB-INF/classes目录中,上面的CLASSPATH环境变量的设置值由于排版原因进行了换行,实际上不
应该有换行。接着在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文件,在该文件的<Host>元
素中增加如下一个<Context>子元素:
<Context path="/fileupload" docBase="fileupload" reloadable="true"/>
保存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所示。
图 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文件上传组件将用临时文件来保存这些数据;但如果上传的文件很小,例如上传600个字节的文件,显然
将其直接保存在内存中更加有效。setSizeThreshold方法用于设置是否使用临时文件保存解析出的数据的那个临界值,该方法传入的参数的单
位是字节。其完整语法定义如下:
public void setSizeThreshold(int sizeThreshold)
3. setRepositoryPath 方法
setRepositoryPath 方法用于设置setSizeThreshold方法中提到的临时文件的存放目录,这里要求使用绝对路径。其完整语法定义如下:
public void setRepositoryPath(String repositoryPath)
如果不设置存放路径,那么临时文件将被储存在"java.io.tmpdir"这个JVM环境属性所指定的目录中,tomcat 5.5.9将这个属性设置为了
“<tomcat 安装目录>/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文件上传组件都需要将它们转换成字符串形式返回,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 方法设置的临界值大小时,这个流对象关联到一片内存,主体内容将会被保存在内存中。当主体内容的数据
超过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)
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)在<tomcat安装目录>/webapps/fileupload目录中按例程1-1编写一个名为FileUpload.html的HTML页面,该页面用于提供文件上传的 FORM
表单,表单的enctype属性设置值为“multipart/form-data”,表单的action属性设置为“servlet/UploadServlet”。
例程 1-1 FileUpload.html
<html>
<head>
<title>upload experiment</title>
<meta http-equiv="Content-Type" content="text/html; charset=gb2312">
</head>
<body>
<h3>测试文件上传组件的页面 </h3>
<form action="servlet/UploadServlet"
enctype="multipart/form-data" method="post">
作者: <input type="text" name="author"><br>
来自: <input type="text" name="company"><br>
文件1: <input type="file" name="file1"><br>
文件2: <input type="file" name="file2"><br>
<input type="submit" value="上载 ">
</form>
</body>
</html>
(2)在<tomcat的安装目录>/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.*;
import java.util.*;
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);
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())
{
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") + "<br>");
out.println("来自: " + request.getAttribute("company") + "<br>");
/*将上传的文件名组合成"file1,file2"这种形式显示出来,如果没有上传
* 任何文件,则显示为"无",如果只上传了第二个文件,显示为"file2"。 */
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) 修改<tomcat的安装目录>/webapps/fileupload/WEB-INF/classes/web.xml文件,在其中注册和映射UploadServlet的访问路径,如例程 1-3
所示。
例程 1-3 web.xml
<web-app>
<servlet>
<servlet-name>UploadServlet</servlet-name>
<servlet-class>UploadServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>UploadServlet</servlet-name>
<url-pattern>/servlet/UploadServlet</url-pattern>
</servlet-mapping>
</web-app>
(4)重新启动Tomcat,并在浏览器地址栏中输入如下地址:
http://localhost:8080/fileupload/FileUpload.html
填写返回页面中的FORM表单,如图1.4所示,单击“上载”按钮后,浏览器返回的页面信息如图1.5所示。
图 1.4
图1.5(这些图的标题栏中的it315改为fileupload)
查看<tomcat安装目录>/webapps/it315/upload目录,可以看到刚才上传的两个文件。
(4) 单击浏览器工具栏上的“后退”按钮回到表单填写页面,只在第二个文件字段中选择一个文件,单击“上载”按钮,浏览器返回的显示结果
如图1.6所示。
图 1.6
M脚下留心:
上面编写的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节
中讲解的“分析文件上传的请求消息结构”的知识。
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所示。
图 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类对象。
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.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头中指
定的字段分隔界线有一点细微的差别,前者是在后者前面增加了两个减号(-)字符而形成的,这倒不是什么编程难点。真正的编程难点在于在
字节数组缓冲区buffer中寻找分隔界线时,可能会遇到字节数组缓冲区buffer中只装入了分隔界线字符序列的部分内容的情况,如图1.10所示
。
图 1.10
要解决这个问题的方法之一就是在查找字段分隔界线时,如果发现字节数组缓冲区buffer中只装入了分隔界线字符序列的部分内容,那么就将
这一部分内容留给字节数组缓冲区buffer的下一次读取,如图1.11所示。
图 1.11
这种方式让字节数组缓冲区buffer下一次读取的内容不是紧接着上一次读取内容的后面,而是重叠上一次读取的一部分内容,即从上一次读取
内容中的分隔界线字符序列的第一个字节处开始读取。这种方式在实际的编程处理上存在着相当大的难度,程序首先必须确定字节数组缓冲区
buffer 上一次读取的数据的后一部分内容正好是分隔界线字符序列的前面一部分内容,而这一部分内容的长度是不确定的,可能只是分隔界线
字符序列的第一个字符,也可能是分隔界线字符序列的前面n-1个字符,其中n为分隔界线字符序列的整个长度。另外,即使确定字节数组缓冲
区buffer上一次读取的数据的后一部分内容正好是分隔界线字符序列的前面一部分内容,但它们在整个输入字节流中的后续内容不一定就整个
分隔界线字符序列的后一部分内容,出现这种情况的可能性是完全存在,程序必须进行全面和严谨的考虑。
Apache 文件上传组件的解决方法比较巧妙,它在查找字段分隔界线时,如果搜索到最后第n个字符时,n为分隔界线字符序列的长度,发现最后 n
个字符不能与分隔界线字符序列匹配,则将最后的n-1个字符留给字节数组缓冲区buffer的下一次读取,程序再对buffer的下一次读取的整个内
容从头开始查找字段分隔界线,如图1.12所示。
图 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类中定义了一个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中的位置指针
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;
}
}
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所示。
图 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缓冲区中的最后
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方法内部定义的各个变量的示意图。
图 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() ;
// 对描述头部分的数据内容过大进行限制处理
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 ;
}
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变量跳过主体数据,指向分隔界线的首字节
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 ;
}
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 ;
}
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’}
else if (arrayequals(marker, FIELD_SEPARATOR, 2))
{
nextChunk = true;
}
else
{
/* 如果读到的既不是回车换行,又不是两个减号,
说明输入流有问题,则抛异常。
文章来自sauzny博客:http://sauzny.iteye.com/blog/1974675