1.setSizeMax方法
setSizeMax方法用于设置请求消息实体内容的最大允许大小,以防止客户端故意通过上传特大的文件来塞满服务器端的存储空间,单位为字节。 2.setSizeThreshold方法 Apache文件上传组件在解析和处理上传数据中的每个字段内容时,需要临时保存解析出的数据。因为Java虚拟机默认可以使用的内存空间是有 限的(笔者测试不大于100M),超出限制时将会发生“java.lang.OutOfMemoryError”错误,如果上传的文件很大,例如上传800M的文件,在 内存中将无法保存该文件内容,Apache文件上传组件将用临时文件来保存这些数据;但如果上传的文件很小,例如上传600个字节的文件,显然 将其直接保存在内存中更加有效。setSizeThreshold方法用于设置是否使用临时文件保存解析出的数据的那个临界值,该方法传入的参数的单。 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(); 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>"); 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())); } 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下一次读取的内容不是紧接着上一次读取内容的后面,而是重叠上一次读取的一部分内容,即从上一次读取 |