站内搜索: 请输入搜索关键词

当前页面: 开发资料首页JSP 专题使用JSF和MyFaces实现文件上载

使用JSF和MyFaces实现文件上载

摘要: 使用JSF和MyFaces实现文件上载


  Web浏览器为我们利用Web应用程序发送文件提供了一条简单的途径,但是当前版本的Java Web标准(servlets、JSP和JSF)却无法为我们提供任何帮助。幸运的是,有一些第三方框架组件(例如Apache Commons File Upload、 Apache MyFaces和Oracle ADF Faces)实现了这种特性,它们都暴露了简单的API和定制的标记(tag)。

  本文的前面一部分介绍文件上载操作是如何实现的,解释了MyFaces和通用文件上载(前者在内部使用了后者)。我们了解这些开放源代码框架组件的内部工作方式是非常重要的,因为只有这样我们才能高效率地使用它们,才能根据自己的需要对它们进行修改。本文的第二部分将介绍一个简单的应用程序,它让用户使用web浏览器上载文件。

  基于Web的文件上载

  我们把"上载"这个专业术语用得有点过滥了。Web管理员在自己的站点上发布文件的时候,它会说自己"上载"了一个文件。Web开发者在建立HTML窗体和脚本,让普通用户通过web浏览器发送一个文件的时候,他也会说自己实现了文件"上载"功能。

  这两种含义在某些方面是重叠的,因为web管理员可能使用基于web的界面来发布文件(页面、图像、脚本等等)。免费提供个人web站点的公司(例如Yahoo),实现基于web的文件上载功能的目的是为了让人们上载自己的页面。它允许任何一个拥有web浏览器和互联网访问能力的个人发布一个小型的web站点。但是,目前有一些更好的、可用于发布web内容的方式,例如FTP或安全FTP。在这种情况下,你可以利用专用的应用程序(例如FTP客户端)代替web浏览器把自己的内容上载到web服务器上。

  本文从web开发者的角度来讨论文件上载的问题。例如,一个基于web的邮件应用程序(例如Yahoo邮件),就实现了文件上载,因为只有这样,用户才能够发送那些带有附件的消息。另一个比较好的例子是找工作的web站点,它允许用户把自己的简历发送给招聘人员。本文的示例应用程序计算上载文件的散列值。你可以在自己的应用程序中对上载的文件进行任意的处理,例如把它们的内容存储在数据库中或把它们作为邮件附件发送。现在,让我们看看如何在web应用程序中实现文件上载吧。

  一个HTML窗体中可以包含一个或多个<input type="file">元素,浏览器把它们作为输入字段来显示(呈现),在这些字段中允许用户输入文件路径。在每个文件输入字段的后面,web浏览器会添加一个按钮,它会打开一个文件对话框,让用户选择一个文件(代替输入路径):


图1:包含文件输入字段的web窗体
  当用户点击窗体的"提交(Submit)"按钮的时候,web浏览器对窗体的数据进行编码,其中包括被选中的文件、文件名(或路径)和窗体的其它一些参数。接着,浏览器把编码后的数据发送给web服务器,web服务器把这些数据传递给<form>标记的action属性所指定的脚本来处理。如果你开发一个Java web应用程序,那么操作脚本可能是一个servlet(小服务程序)或JSP页面。

  由于窗体数据默认的编码方式和默认的GET方法都不适合文件上载,所以包含文件输入字段的窗体必须在<form>标记中指定multipart/form-data编码方式和POST方法:

<form enctype="multipart/form-data" method="POST" action="...">
...
<input type="file" name="...">
...
</form>


  但是事情并非这么简单,因为实现servlet和JSP规范的应用程序服务器不一定能够处理multipart/form-data编码方式。因此,你必须分析请求的输入流,例如Apache通用文件上载就是一个小型的Java程序包,它让你能够从编码的数据中获取被上载文件的内容。这个程序包的API是很灵活的,它允许你把小文件存储在内存中,把大文件存储在磁盘的临时目录中。你可以指定一个文件大小的阀值,大于这个值的文件都会被写到磁盘上,而不会保留在内存中,而且你还可以规定允许的被上载文件的最大大小。

  前面提到的org.apache.commons.fileupload程序包包含一个叫作DiskFileUpload的类,它的parseRequest()方法获取HttpServletRequest参数,并返回org.apache.commons.fileupload.FileItem实例列表。编码后的窗体数据从servlet请求的getInputStream()方法所返回的数据流流中读取。FileItem这个名字容易使人误解,因为这个接口的实例同时表现了被上载的文件和正常的请求参数。

  通用文件上载程序包所提供的API赋予了你访问分解后的窗体数据的权利,但是servlet请求的getParameter()和getParameterValues()方法却无法工作。这是一个难题,因为在输入字段、检查框、单选框和列表框后台运行的JSF组件需要调用这两个方法。我们可以利用Servlets API提供的两个特性(过滤器和请求包装器)来解决这个问题。下一部分描述了Apache MyFaces如何实现过滤器,添加了大量必要的对文件上载的支持,而不会中断已有的JSF组件。此外,MyFaces为JavaBeans提供了API,同时还提供了一个定制的JSF组件,它表现为<input type="file">元素。

  配置JSF和MyFaces扩展

  目前,JSF规范的主要实现是JSF参考实现(RI),与此同时,Apache提供了另外一个实现,就是MyFaces。当然可能存在其它一些JSF实现,但是JSF RI和MyFaces是其中最流行的两个。很多开发者更喜欢前者,因为Sun把它称为"官方的"实现,但是,MyFaces拥有一些有趣的扩展(例如对上载文件操作的支持)。如果你愿意,可以把MyFaces扩展与Sun的JSF RI一起使用。你只需要把myfaces-extensions.jar文件、JSF RI的JAR文件和commons-fileupload-1.0.jar文件一起放到自己的web应用程序的WEB-INF/lib目录中就可以了。下面是需要的JAR文件:

JSF 1.1 RI jsf-api.jarjsf-impl.jar
JSTL 1.1 RI jstl.jarstandard.jar
MyFaces扩展 myfaces-extensions.jar
Apache Commons(供JSF和MyFaces 扩展使用) commons-collections.jarcommons-digester.jarcommons-beanutils.jarcommons-logging.jarcommons-fileupload-1.0.jar


  org.apache.myfaces.component.html.util程序包中的MultipartRequestWrapper类充当MyFaces和通用文件上载之间的桥梁。这个类扩展了HttpServletRequestWrapper,重载了getParameterMap()、getParameterNames()、getParameter()和getParameterValues()方法,这样它们才能适应multipart/form-data编码方式。此外,MultipartRequestWrapper提供了两个新的方法,分别是getFileItem()和getFileItems(),它允许你通过org.apache.commons.fileupload.FileItem接口来访问被上载的文件。

  当MyFaces检测到编码方式的时候,org.apache.myfaces.component.html.util程序包中的ExtensionsFilter就建立MultipartRequestWrapper实例。因此,你不需要关心窗体数据的分解,但是如果你希望改变被上载文件的处理方式,那么了解它是如何实现的就很有用处了。在典型的应用程序中,你必须在自己的web应用程序的web.xml描述信息中配置ExtensionsFilter,这样它才能在JSF的FacesServlet前面截取HTTP请求:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">

<web-app>

<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>client</param-value>
</context-param>

<servlet>
<servlet-name>FacesServlet</servlet-name>
<servlet-class>
javax.faces.webapp.FacesServlet
</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>FacesServlet</servlet-name>
<url-pattern>/faces/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>FacesServlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>

<filter>
<filter-name>ExtensionsFilter</filter-name>
<filter-class>
org.apache.myfaces.component.html.util.ExtensionsFilter
</filter-class>
<init-param>
<param-name>uploadMaxFileSize</param-name>
<param-value>10m</param-value>
</init-param>
<init-param>
<param-name>uploadThresholdSize</param-name>
<param-value>100k</param-value>
</init-param>
</filter>

<filter-mapping>
<filter-name>ExtensionsFilter</filter-name>
<servlet-name>FacesServlet</servlet-name>
</filter-mapping>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

</web-app>


  前面示例中的两个过滤器(filter)参数告诉MyFaces把小于100K的文件放入内存中,并且忽略(即不处理)占用10MB以上磁盘空间的文件。大小介于uploadThresholdSize和uploadMaxFileSize之间的文件将作为临时文件存储在磁盘上。如果你试图载入一个太大的文件,MyFaces当前版本将忽略所有的窗体数据,就像用户提交了一个空表单一样。如果你希望给上载文件失败的用户一些提示信息,就需要更改MyFaces的MultipartRequestWrapper类,找到捕捉SizeLimitExceededException异常的位置,并使用FacesContext.getCurrentInstance().addMessage()来警告用户。

  前面提到,MyFaces扩展包含了文件上载组件,我们可以在JSF页面中使用它。下一部分将介绍如何实现这样的操作。 使用MyFaces的文件上载组件。

  为了在web页面中使用JSF和MyFaces,你必须在<%@taglib%>指令中声明它们的标记库:

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://myfaces.apache.org/extensions" prefix="x"%>


  JSF的<h:form>标记没有method属性,这是因为它只支持POST方法,但它拥有enctype属性,如果你希望上载文件,就必须使用该属性,把窗体数据设置为多部分(multipart)编码类型:

<f:view>
<h:form id="MyForm" enctype="multipart/form-data" >
...
<x:inputFileUpload id="myFileId"
value="#{myBean.myFile}"
storage="file"
required="true"/>
...
</h:form>
</f:view>


  MyFaces的<x:inputFileUpload>标记使你能够定义UI组件的属性,它的呈现部件(renderer)生成<input type="file">元素。org.apache.myfaces.custom.fileupload程序包包含了UI组件类(HtmlInputFileUpload)、呈现部件(HtmlFileUploadRenderer)、定制的标记处理程序(HtmlInputFileUploadTag)、UploadedFile接口和其它一些辅助类。HtmlInputFileUpload类扩展了JSF标准的HtmlInputText组件,重载了它的一些方法。HtmlFileUploadRenderer负责生成HTML标记,从MultipartRequestWrapper中获取FileItem。

  MyFaces并没有让你直接访问通用文件上载所建立的FileItem实例,它提供了自己的UploadedFile接口来获取被上载的文件的内容、内容类型、文件名和文件大小。JSF窗体的后台bean必须拥有一个UploadedFile类型的属性。前面的例子用JSF表达式(#{myBean.myFile})把UI组件的值绑定到了这样一个bean上。JSF框架组件将获取HtmlInputFileUpload组件的值,它是一个UploadedFile实例,并且会设置后台bean的myFile属性:

import org.apache.myfaces.custom.fileupload.UploadedFile;
...
public class MyBean {
private UploadedFile myFile;

public UploadedFile getMyFile() {
return myFile;
}

public void setMyFile(UploadedFile myFile) {
this.myFile = myFile;
}
...
}


  MyFaces拥有UploadedFile接口的两种实现方式:UploadedFileDefaultMemoryImpl和UploadedFileDefaultFileImpl。当<x:inputFileUpload>标记没有带storage属性,或者这个属性的值是memory的时候,就使用前者。当storage属性的值是file的时候使用后者。

  UploadedFileDefaultMemoryImpl类从FileItem实例中获取被上载文件的内容、文件名、文件大小和内容类型,并把所有这些信息存储在私有字段中。因此,即使通用文件上载把这个文件保存在磁盘上,UploadedFile的这种实现也会把被上载文件的内容保存在内存中,浪费了资源。

  UploadedFileDefaultFileImpl类使用一个过渡字段来保存FileItem实例的指针,仅仅在调用getInputStream()方法的时候,才使用它来获取被上载文件的内容。这种实现节约了内存空间,但是,如果它是序列化的(serialize),那么在还原序列化(deserialization)之后,你就无法获取文件内容了。因此,文件上载窗体的后台bean不能保存在session作用域(scope)中,因为当应用程序重新启动或服务器关闭的时候,应用程序服务器会序列化对话(session)bean。

  如果你希望得到了一个工作正常的、效率很高的解决方案,那么就把后台bean保存在request作用域中,并在<x:inputFileUpload>中指定storage="file"以节约内存资源。你也可以通过添加writeObject()方法(它用于序列化被上载文件的内容)来解决UploadedFileDefaultFileImpl类的序列化问题。为了保证UploadedFile的这种实现的高效性,相应的readObject()方法应该重新建立一个临时文件,而不是从内存中读取内容。

  使用MyFaces的时候还需要注意一个问题:UploadedFile接口并没有定义一个用于删除通用文件上载在磁盘上所建立的临时文件的方法。当FileItem实例作为垃圾被清理的时候,就应该删掉这些文件。通用文件上载的DefaultFileItem类拥有一个finalize()方法,当管理临时文件的对象被从内存中删除的时候,它可以删除它们所管理的临时文件。如果你的应用程序正在上载大型文件的时候,你可能希望在它们被处理后立即删除,而不用等待垃圾清除过程。为了实现这样的功能,你必须添加一个getFileItem()方法(在UploadedFileDefaultFileImpl中),它应该返回FileItem实例,而该实例拥有delete()方法。

  示例应用程序

  前面的部分介绍了是MyFaces如何在通用文件上载的帮助下支持文件上载的。现在我们来看一个实际的利用了这种特性的应用程序。JSF窗体(MyForm.jsp)让用户选择一个文件和一个报文摘要算法,后台bean( MyBean.java)使用这个算法计算出一个散列值,显示在另外一个web页面(MyResult.jsp)中。这些页面和后台bean通过一个JSF配置文件(faces-config.xml)结合在一起。

  MyForm.jsp页面

  这个JSF窗体使用了MyFaces的<x:inputFileUpload>标记,同时还使用了其它一些标准的JSF标记来呈现标签、消息、包含报文摘要算法的下拉列表、使用JSF表达式(#{myBean.processMyFile})指定了处理被上载文件的操作方法的命令按钮:

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>
<%@ taglib uri="http://myfaces.apache.org/extensions" prefix="x"%>

<f:view>
<h:form id="MyForm" enctype="multipart/form-data" >
 <h:messages globalOnly="true" styleClass="message"/>
 <h:panelGrid columns="3" border="0" cellspacing="5">
 <h:outputLabel for="myFileId" value="File: "/>
 <x:inputFileUpload id="myFileId" value="#{myBean.myFile}" storage="file" required="true"/>
 <h:message for="myFileId"/>
 <h:outputLabel for="myParamId" value="Param: "/>
 <h:selectOneMenu id="myParamId" value="#{myBean.myParam}" required="true">
 <f:selectItem itemLabel="" itemValue=""/>
 <f:selectItem itemLabel="MD5" itemValue="MD5"/>
 <f:selectItem itemLabel="SHA-1" itemValue="SHA-1"/>
 <f:selectItem itemLabel="SHA-256" itemValue="SHA-256"/>
 <f:selectItem itemLabel="SHA-384" itemValue="SHA-384"/>
 <f:selectItem itemLabel="SHA-512" itemValue="SHA-512"/>
</h:selectOneMenu>
<h:message for="myParamId"/>

<h:outputText value=" "/>
<h:commandButton value="Submit"
action="#{myBean.processMyFile}"/>
<h:outputText value=" "/>

</h:panelGrid>
</h:form>
</f:view>


  MyBean类

  后台bean拥有三个属性:myFile、myParam和myResult。前面已经解释了myFile属性的角色。它让你获取被上载文件的内容和文件名、文件大小、内容类型。myParam属性的值是报文摘要算法。myResult属性将保存processMyFile()方法执行之后的散列值。MyBean类为自己的所有属性提供了get和set方法:

package com.devsphere.articles.jsfupload;

import org.apache.myfaces.custom.fileupload.UploadedFile;
...
public class MyBean {
 private UploadedFile myFile;
 private String myParam;
 private String myResult;

 public UploadedFile getMyFile() {
  return myFile;
 }

 public void setMyFile(UploadedFile myFile) {
  this.myFile = myFile;
 }

 public String getMyParam() {
  return myParam;
 }

 public void setMyParam(String myParam) {
  this.myParam = myParam;
 }

 public String getMyResult() {
  return myResult;
 }

 public void setMyResult(String myResult) {
  this.myResult = myResult;
 }
 ...
}


  processMyFile()通过一个输入流获取被上载文件的内容,这个输入流是通过myFile.getInputStream()获得的。散列值是在java.security.MessageDigest的帮助下计算出来的,然后把它转换为字符串,这样就可以被myResult属性访问了:

package com.devsphere.articles.jsfupload;
...
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import java.io.*;

public class MyBean {
 ...
 public String processMyFile() {
  try {
   MessageDigest md= MessageDigest.getInstance(myParam);
   InputStream in = new BufferedInputStream(
   myFile.getInputStream());
   try {
    byte[] buffer = new byte[64 * 1024];
    int count;
    while ((count = in.read(buffer)) > 0)
     md.update(buffer, 0, count);
  } finally {
   in.close();
  }
  byte hash[] = md.digest();
  StringBuffer buf = new StringBuffer();
  for (int i = 0; i < hash.length; i++) {
   int b = hash[i] & 0xFF;
   int c = (b >> 4) & 0xF;
   c = c < 10 ? ’0’ + c : ’A’ + c - 10;
   buf.append((char) c);
   c = b & 0xF;
   c = c < 10 ? ’0’ + c : ’A’ + c - 10;
   buf.append((char) c);
  }
  myResult = buf.toString();
  return "OK";
 } catch (Exception x) {
  FacesMessage message = new FacesMessage(
  FacesMessage.SEVERITY_FATAL,
  x.getClass().getName(), x.getMessage());
  FacesContext.getCurrentInstance().addMessage(null, message);
  return null;
 }
}

}


  faces-config.xml文件

  JSF配置文件把后台bean定义在request作用域中,并且指定了一个导航规则:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE faces-config PUBLIC
"-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.1//EN"
"http://java.sun.com/dtd/web-facesconfig_1_1.dtd">

<faces-config>

<managed-bean>
<managed-bean-name>myBean</managed-bean-name>
<managed-bean-class>
com.devsphere.articles.jsfupload.MyBean
</managed-bean-class>
<managed-bean-scope>request</managed-bean-scope>
</managed-bean>

<navigation-rule>
<from-view-id>/MyForm.jsp</from-view-id>
<navigation-case>
<from-outcome>OK</from-outcome>
<to-view-id>/MyResult.jsp</to-view-id>
</navigation-case>
</navigation-rule>

</faces-config>


  MyResult.jsp页面

  这个web页面显示了被上载文件的一些信息:

<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%>
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%>

<f:view>

<h:panelGrid columns="2" border="0" cellspacing="5">

<h:outputText value="FileName:"/>
<h:outputText value="#{myBean.myFile.name}"/>

<h:outputText value="FileSize:"/>
<h:outputText value="#{myBean.myFile.size}"/>

<h:outputText value="Param:"/>
<h:outputText value="#{myBean.myParam}"/>

<h:outputText value="Result:"/>
<h:outputText value="#{myBean.myResult}"/>

</h:panelGrid>

</f:view>


  被显示的文件名可能带有客户端文件系统的完整路径,如下所示:


图2:结果页面产生的输出信息
  总结

  在很多情况下,用户需要通过浏览器上载文件,但是在服务器端却没有处理这些文件的理想方法。把文件的内容保存在内存中对于小型文件来说是可以接受的,但是把被上载的文件存储在临时文件中使这种情况变复杂了。MyFaces让你能够选择一个适合应用程序需求的解决方案,但是这个框架组件也有一些小问题。当你不再需要临时文件的时候,它无法让你删除它们;文件名有时候带有文件路径;当用户上载过大的文件的时候也没有警告信息。这些缺陷是可以修补的,因为我们可以使用源代码,而且本文就介绍了MyFaces代码可以改进的一些的地方。无论如何,对于多数应用程序来说,MyFaces不做修改就合乎需求。本文的例子是使用JSF 1.1.01、MyFaces 1.0.9和通用文件上载1.0环境测试的。

</td> </tr> <tr> <td vAlign=top align=left height="100%">
↑返回目录
前一篇: 用JSP从数据库中读取图片并显示在网页上
后一篇: JSPSmart实现文件上传时file和text表单同时提交的问题