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瀏覽器會添加一個按鈕,它會開啟一個檔案對話方塊,讓使用者選擇一個檔案(代替輸入路徑):
當使用者點擊表單的"提交(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> |
被顯示的檔案名稱可能帶有用戶端檔案系統的完整路徑,如下所示:
總結
在很多情況下,使用者需要通過瀏覽器上傳檔案,但是在伺服器端卻沒有處理這些檔案的理想方法。把檔案的內容儲存在記憶體中對於小型檔案來說是可以接受的,但是把被上傳的檔案儲存體在臨時檔案中使這種情況變複雜了。MyFaces讓你能夠選擇一個適合應用程式需求的解決方案,但是這個架構組件也有一些小問題。當你不再需要臨時檔案的時候,它無法讓你刪除它們;檔案名稱有時候帶有檔案路徑;當使用者上傳過大的檔案的時候也沒有警告資訊。這些缺陷是可以修補的,因為我們可以使用原始碼,而且本文就介紹了MyFaces代碼可以改進的一些的地方。無論如何,對於多數應用程式來說,MyFaces不做修改就合乎需求。本文的例子是使用JSF 1.1.01、MyFaces 1.0.9和通用檔案上傳1.0環境測試的。