Struts Spring Hibernate實現上傳下載
2007-09-06 13:31
下載本文源代碼
引言 文件的上傳和下載在J2EE編程已經是一個非常古老的話題了,也許您馬上就能掰著指頭數出好幾個著名的大件:如SmartUpload、Apache的FileUpload。但如果您的項目是構建在Struts Spring Hibernate(以下稱SSH)框架上的,這些大件就顯得笨重而滄桑了,SSH提供了一個簡捷方便的文件上傳下載的方案,我們只需要通過一些配置并輔以少量的代碼就可以完好解決這個問題了。 本文將圍繞SSH文件上傳下載的主題,向您詳細講述如何開發基于SSH的Web程序。SSH各框架的均為當前最新版本: ?Struts 1.2 ?Spring 1.2.5 ?Hibernate 3.0 本文選用的數據庫為Oracle 9i,當然你可以在不改動代碼的情況下,通過配置文件的調整將其移植到任何具有Blob字段類型的數據庫上,如MySQL,SQLServer等。 總體實現 上傳文件保存到T_FILE表中,T_FILE表結構如下:
其中: ?FILE_ID:文件ID,32個字符,用Hibernate的uuid.hex算法生成。 ?FILE_NAME:文件名。 ?FILE_CONTENT:文件內容,對應Oracle的Blob類型。 ?REMARK:文件備注。 文件數據存儲在Blob類型的FILE_CONTENT表字段上,在Spring中采用OracleLobHandler來處理Lob字段(包括Clob和Blob),由于在程序中不需要引用到oracle數據驅動程序的具體類且屏蔽了不同數據庫處理Lob字段方法上的差別,從而撤除程序在多數據庫移植上的樊籬。 1.首先數據表中的Blob字段在Java領域對象中聲明為byte[]類型,而非java.sql.Blob類型。 2.數據表Blob字段在Hibernate持久化映射文件中的type為org.springframework.orm.hibernate3.support.BlobByteArrayType,即Spring所提供的用戶自定義的類型,而非java.sql.Blob。 3.在Spring中使用org.springframework.jdbc.support.lob.OracleLobHandler處理Oracle數據庫的Blob類型字段。 通過這樣的設置和配置,我們就可以象持久化表的一般字段類型一樣處理Blob字段了。 以上是Spring+Hibernate將文件二進制數據持久化到數據庫的解決方案,而Struts通過將表單中file類型的組件映射為ActionForm中類型為org.apache.struts.upload. FormFile的屬性來獲取表單提交的文件數據。 綜上所述,我們可以通過圖 2,描繪出SSH處理文件上傳的方案:
文件上傳的頁面如圖 3所示:
文件下載的頁面如圖 4所示:
該工程的資源結構如圖 5所示:
工程的類按SSH的層次結構劃分為數據持久層、業務層和Web層;WEB-INF下的applicationContext.xml為Spring的配置文件,struts-config.xml為Struts的配置文件,file-upload.jsp為文件上傳頁面,file-list.jsp為文件列表頁面。 本文后面的章節將從數據持久層->業務層->Web層的開發順序,逐層講解文件上傳下載的開發過程。 數據持久層 1、領域對象及映射文件 您可以使用Hibernate Middlegen、HIbernate Tools、Hibernate Syhchronizer等工具或手工的方式,編寫Hibernate的領域對象和映射文件。其中對應T_FILE表的領域對象Tfile.java為: 代碼 1 領域對象Tfile
特別需要注意的是:數據庫表為Blob類型的字段在Tfile中的fileContent類型為byte[]。Tfile的Hibernate映射文件Tfile.hbm.xml放在Tfile .java類文件的相同目錄下: 代碼 2 領域對象映射文件
fileContent字段映射為Spring所提供的BlobByteArrayType類型,BlobByteArrayType是用戶自定義的數據類型,它實現了Hibernate 的org.hibernate.usertype.UserType接口。BlobByteArrayType使用從sessionFactory獲取的Lob操作句柄lobHandler將byte[]的數據保存到Blob數據庫字段中。這樣,我們就再沒有必要通過硬編碼的方式,先insert然后再update來完成Blob類型數據的持久化,這個原來難伺候的老爺終于被平民化了。關于lobHandler的配置請見本文后面的內容。 此外lazy="true"說明地返回整個Tfile對象時,并不返回fileContent這個字段的數據,只有在顯式調用tfile.getFileContent()方法時才真正從數據庫中獲取fileContent的數據。這是Hibernate3引入的新特性,對于包含重量級大數據的表字段,這種抽取方式提高了對大字段操作的靈活性,否則加載Tfile對象的結果集時如果總是返回fileContent,這種批量的數據抽取將可以引起數據庫的"洪泛效應"。 2、DAO編寫和配置 Spring強調面向接口編程,所以我們將所有對Tfile的數據操作的方法定義在TfileDAO接口中,這些接口方法分別是: ?findByFildId(String fileId) ?save(Tfile tfile) ?List findAll() TfileDAOHibernate提供了對TfileDAO接口基于Hibernate的實現,如代碼 3所示: 代碼 3 基于Hibernate 的fileDAO實現類
TfileDAOHibernate通過擴展Spring提供的Hibernate支持類HibernateDaoSupport而建立,HibernateDaoSupport封裝了HibernateTemplate,而HibernateTemplate封裝了Hibernate所提供幾乎所有的的數據操作方法,如execute(HibernateCallback action),load(Class entityClass, Serializable id),save(final Object entity)等等。 所以我們的DAO只需要簡單地調用父類的HibernateTemplate就可以完成幾乎所有的數據庫操作了。 由于Spring通過代理Hibernate完成數據層的操作,所以原Hibernate的配置文件hibernate.cfg.xml的信息也轉移到Spring的配置文件中: 代碼 4 Spring中有關Hibernate的配置信息
第3~9行定義了一個數據源,其實現類是apache的BasicDataSource,第11~25行定義了Hibernate的會話工廠,會話工廠類用Spring提供的LocalSessionFactoryBean維護,它注入了數據源和資源映射文件,此外還通過一些鍵值對設置了Hibernate所需的屬性。 其中第16行通過類路徑的映射方式,將sshfile.model類包目錄下的所有領域對象的映射文件裝載進來,在本文的例子里,它將裝載進Tfile.hbm.xml映射文件。如果有多個映射文件需要聲明,使用類路徑映射方式顯然比直接單獨指定映射文件名的方式要簡便。 第27~30行定義了Spring代理Hibernate數據操作的HibernateTemplate模板,而第32~34行將該模板注入到tfileDAO中。 需要指定的是Spring 1.2.5提供了兩套Hibernate的支持包,其中Hibernate 2相關的封裝類位于org.springframework.orm.hibernate2.*包中,而Hibernate 3.0的封裝類位于org.springframework.orm.hibernate3.*包中,需要根據您所選用Hibernate版本進行正確選擇。 3、Lob字段處理的配置 我們前面已經指出Oracle的Lob字段和一般類型的字段在操作上有一個明顯的區別--那就是你必須首先通過Oracle的empty_blob()/empty_clob()初始化Lob字段,然后獲取該字段的引用,通過這個引用更改其值。所以要完成對Lob字段的操作,Hibernate必須執行兩步數據庫訪問操作,先Insert再Update。 使用BlobByteArrayType字段類型后,為什么我們就可以象一般的字段類型一樣操作Blob字段呢?可以確定的一點是:BlobByteArrayType不可能逾越Blob天生的操作方式,原來是BlobByteArrayType數據類型本身具體數據訪問的功能,它通過LobHandler將兩次數據訪問的動作隱藏起來,使Blob字段的操作在表現上和其他一般字段業類型無異,所以LobHandler即是那個"苦了我一個,幸福十億人"的那位幕后英雄。 LobHandler必須注入到Hibernate會話工廠sessionFactory中,因為sessionFactory負責產生與數據庫交互的Session。LobHandler的配置如代碼 5所示: 代碼 5 Lob字段的處理句柄配置
首先,必須定義一個能夠從連接池中抽取出本地數據庫JDBC對象(如OracleConnection,OracleResultSet等)的抽取器:nativeJdbcExtractor,這樣才可以執行一些特定數據庫的操作。對于那些僅封裝了Connection而未包括Statement的簡單數據連接池,SimpleNativeJdbcExtractor是效率最高的抽取器實現類,但具體到apache的BasicDataSource連接池,它封裝了所有JDBC的對象,這時就需要使用CommonsDbcpNativeJdbcExtractor了。Spring針對幾個著名的Web服務器的數據源提供了相應的JDBC抽取器: ?WebLogic:WebLogicNativeJdbcExtractor ?WebSphere:WebSphereNativeJdbcExtractor ?JBoss:JBossNativeJdbcExtractor 在定義了JDBC抽取器后,再定義lobHandler。Spring 1.2.5提供了兩個lobHandler: ?DefaultLobHandler:適用于大部分的數據庫,如SqlServer,MySQL,對Oracle 10g也適用,但不適用于Oracle 9i(看來Oracle 9i確實是個怪胎,誰叫Oracle 公司自己都說Oracle 9i是一個過渡性的產品呢)。 ?OracleLobHandler:適用于Oracle 9i和Oracle 10g。 由于我們的數據庫是Oracle9i,所以使用OracleLobHandler。 在配置完LobHandler后, 還需要將其注入到sessionFactory的Bean中,下面是調用后的sessionFactory Bean的配置: 代碼 6 將lobHandler注入到sessionFactory中的配置
業務層
其中save(FileActionForm fileForm)方法,將封裝在fileForm中的上傳文件保存到數據庫中,這里我們使用FileActionForm作為方法入參,FileActionForm是Web層的表單數據對象,它封裝了提交表單的數據。將FileActionForm直接作為業務層的接口入參,相當于將Web層傳播到業務層中去,即將業務層綁定在特定的Web層實現技術中,按照分層模型學院派的觀點,這是一種反模塊化的設計,但在"一般"的業務系統并無需提供多種UI界面,系統Web層將來切換到另一種實現技術的可能性也微乎其微,所以筆者覺得沒有必要為了這個業務層完全獨立于調用層的過高目標而去搞一個額外的隔離層,浪費了原材料不說,還將系統搞得過于復雜,相比于其它原則,"簡單"始終是最大的一條原則。 getAllFile()負責獲取T_FILE表所有記錄,以便在網頁上顯示出來。 而getFileName(String fileId)和write(OutputStream os,String fileId)則用于下載某個特定的文件。具體的調用是將Web層將response.getOutputStream()傳給write(OutputStream os,String fileId)接口,業務層直接將文件數據輸出到這個響應流中。具體實現請參見錯誤!未找到引用源。節下載文件部分。 2、業務層接口實現類 FileService的實現類為FileServiceImpl,其中save(FileActionForm fileForm)的實現如下所示: 代碼 8 業務接口實現類之save()
在save(FileActionForm fileForm)方法里,完成兩個步驟: 其一,象在水桶間倒水一樣,將FileActionForm對象中的數據倒入到Tfile對象中; 其二,調用TfileDAO保存數據。 需要特別注意的是代碼的第11行,FileActionForm的fileContent屬性為org.apache.struts.upload.FormFile類型,FormFile提供了一個方便的方法getFileData(),即可獲取文件的二進制數據。通過解讀FormFile接口實現類DiskFile的原碼,我們可能知道FormFile本身并不緩存文件的數據,只有實際調用getFileData()時,才從磁盤文件輸入流中獲取數據。由于FormFile使用流讀取方式獲取數據,本身沒有緩存文件的所有數據,所以對于上傳超大體積的文件,也是沒有問題的;但是,由于數據持久層的Tfile使用byte[]來緩存文件的數據,所以并不適合處理超大體積的文件(如100M),對于超大體積的文件,依然需要使用java.sql.Blob類型以常規流操作的方式來處理。 此外,通過FileForm的getFileName()方法就可以獲得上傳文件的文件名,如第21行代碼所示。 write(OutputStream os,String fileId)方法的實現,如代碼 9所示: 代碼 9 業務接口實現類之write()
write(OutputStream os,String fileId)也簡單地分為兩個操作步驟,首先,根據fileId加載表記錄,然后將fileContent寫入到輸出流中。 3、Spring事務配置 下面,我們來看如何在Spring配置文件中為FileService配置聲明性的事務
Spring的事務配置包括兩個部分: 其一,定義事務管理器transactionManager,使用HibernateTransactionManager實現事務管理; 其二,對各個業務接口進行定義,其實txProxyTemplate和fileService是父子節點的關系,本來可以將txProxyTemplate定義的內容合并到fileService中一起定義,由于我們的系統僅有一個業務接口需要定義,所以將其定義的一部分抽象到父節點txProxyTemplate中意義確實不大,但是對于真實的系統,往往擁有為數眾多的業務接口需要定義,將這些業務接口定義內容的共同部分抽取到一個父節點中,然后在子節點中通過parent進行關聯,就可以大大簡化業務接口的配置了。 父節點txProxyTemplate注入了事務管理器,此外還定義了業務接口事務管理的方法(允許通過通配符的方式進行匹配聲明,如前兩個接口方法),有些接口方法僅對數據進行讀操作,而另一些接口方法需要涉及到數據的更改。對于前者,可以通過readOnly標識出來,這樣有利于操作性能的提高,需要注意的是由于父類節點定義的Bean僅是子節點配置信息的抽象,并不能具體實現化一個Bean對象,所以需要特別標注為abstract="true",如第8行所示。 fileService作為一個目標類被注入到事務代理器中,而fileService實現類所需要的tfileDAO實例,通過引用3.2節中定義的tfileDAO Bean注入。 Web層實現 1、Web層的構件和交互流程 Web層包括主要3個功能: ?上傳文件。 ?列出所有已經上傳的文件列表,以供點擊下載。 ?下載文件。 Web層實現構件包括與2個JSP頁面,1個ActionForm及一個Action: ?file-upload.jsp:上傳文件的頁面。 ?file-list.jsp:已經上傳文件的列表頁面。 ?FileActionForm:file-upload.jsp頁面表單對應的ActionForm。 ?FileAction:繼承org.apache.struts.actions.DispatchAction的Action,這樣這個Action就可以通過一個URL參數區分中響應不同的請求。 Web層的這些構件的交互流程如圖 6所示:
其中,在執行文件上傳的請求時,FileAction在執行文件上傳后,forward到loadAllFile出口中,loadAllFile加載數據庫中所有已經上傳的記錄,然后forward到名為fileListPage的出口中,調用file-list.jsp頁面顯示已經上傳的記錄。 2、FileAction功能 Struts 1.0的Action有一個弱項:一個Action只能處理一種請求,Struts 1.1中引入了一個DispatchAction,允許通過URL參數指定調用Action中的某個方法,如http://yourwebsite/fileAction.do?method=upload即調用FileAction中的upload方法。通過這種方式,我們就可以將一些相關的請求集中到一個Action當中編寫,而沒有必要為某個請求操作編寫一個Action類。但是參數名是要在struts-config.xml中配置的:
第6行的parameter="method"指定了承載方法名的參數,第9行中,我們還配置了一個調用FileAction不同方法的Action出口。 FileAction共有3個請求響應的方法,它們分別是: ?upload(…):處理上傳文件的請求。 ?listAllFile(…):處理加載數據庫表中所有記錄的請求。 ?download(…):處理下載文件的請求。 下面我們分別對這3個請求處理方法進行講解。 2.1 上傳文件 上傳文件的請求處理方法非常簡單,簡之言之,就是從Spring容器中獲取業務層處理類FileService,調用其save(FileActionForm form)方法上傳文件,如下所示:
由于FileAction其它兩個請求處理方法也需要從Spring容器中獲取FileService實例,所以我們特別提供了一個getFileService()方法(第15~21行)。重構的一條原則就是:"發現代碼中有重復的表達式,將其提取為一個變量;發現類中有重復的代碼段,將其提取為一個方法;發現不同類中有相同的方法,將其提取為一個類"。在真實的系統中,往往擁有多個Action和多個Service類,這時一個比較好的設置思路是,提供一個獲取所有Service實現對象的工具類,這樣就可以將Spring 的Service配置信息屏蔽在一個類中,否則Service的配置名字散落在程序各處,維護性是很差的。 2.2 列出所有已經上傳的文件 listAllFile方法調用Servie層方法加載T_FILE表中所有記錄,并將其保存在Request域中,然后forward到列表頁面中:
file-list.jsp頁面使用Struts標簽展示出保存在Request域中的記錄:
展現頁面的每條記錄掛接著一個鏈接地址,形如:fileAction.do?method=download&fileId=xxx,method參數指定了這個請求由FileAction的download方法來響應,fileId指定了記錄的主鍵。 由于在FileActionForm中,我們定義了fileId的屬性,所以在download響應方法中,我們將可以從FileActionForm中取得fileId的值。這里涉及到一個處理多個請求Action所對應的ActionForm的設計問題,由于原來的Action只能對應一個請求,那么原來的ActionForm非常簡單,它僅需要將這個請求的參數項作為其屬性就可以了,但現在一個Action對應多個請求,每個請求所對應的參數項是不一樣的,此時的ActionForm的屬性就必須是多請求參數項的并集了。所以,除了文件上傳請求所對應的fileContent和remark屬性外還包括文件下載的fileId屬性:
當然這樣會造成屬性的冗余,比如在文件上傳的請求中,只會用到fileContent和remark屬性,而在文件下載的請求時,只會使用到fileId屬性。但這種冗余是會帶來好處的--它使得一個Action可以處理多個請求。 2.3 下載文件 在列表頁面中點擊一個文件下載,其請求由FileAction的download方法來響應,download方法調用業務層的FileService方法,獲取文件數據并寫出到response的響應流中。通過合理設置HTTP響應頭參數,將響應流在客戶端表現為一個下載文件對話框,其代碼如下所示: 代碼 10 業務接口實現類之download
第15~18行,設置HTTP響應頭,將響應類型設置為application/x-msdownload MIME類型,則響應流在IE中將彈出一個文件下載的對話框,如圖 4所示。IE所支持的MIME類型多達26種,您可以通過這個網址查看其他的MIME類型: http://msdn.microsoft.com/workshop/networking/moniker/overview/appendix_a.asp。 如果下載文件的文件名含有中文字符,如果不對其進行硬編碼,如第18行所示,客戶文件下載對話框中出現的文件名將會發生亂碼。 第19行代碼獲得response的輸出流,作為FileServie write(OutputStream os,String fileId)的入參,這樣文件的內容將寫到response的輸出流中。 3、web.xml文件的配置 Spring容器在何時啟動呢?我可以在Web容器初始化來執行啟動Spring容器的操作,Spring提供了兩種方式啟動的方法: ?通過org.springframework.web.context .ContextLoaderListener容器監聽器,在Web容器初始化時觸發初始化Spring容器,在web.xml中通過<listener></listener>對其進行配置。 ?通過Servlet org.springframework.web.context.ContextLoaderServlet,將其配置為自動啟動的Servlet,在Web容器初始化時,通過這個Servlet啟動Spring容器。 在初始化Spring容器之前,必須先初始化log4J的引擎,Spring也提供了容器監聽器和自動啟動Servlet兩種方式對log4J引擎進行初始化: ?org.springframework.web.util .Log4jConfigListener ?org.springframework.web.util.Log4jConfigServlet 下面我們來說明如何配置web.xml啟動Spring容器: 代碼 11 web.xml中對應Spring的配置內容
啟動Spring容器時,需要得到兩個信息:Spring配置文件的地址和Log4J屬性文件,這兩上信息分別通過contextConfigLocationWeb和log4jConfigLocation容器參數指定,如果有多個Spring配置文件,則用逗號隔開,如: /WEB-INF/applicationContext_1.xml, /WEB-INF/applicationContext_1.xm2 由于在啟動ContextLoaderServlet之前,必須事先初始化Log4J的引擎,所以Log4jConfigServlet必須在ContextLoaderServlet之前啟動,這通過<load-on-startup>來指定它們啟動的先后順序。 亂碼是開發Web應用程序一個比較老套又常見問題,由于不同Web應用服務器的默認編碼是不一樣的,為了方便Web應用在不同的Web應用服務器上移植,最好的做法是Web程序自身來處理編碼轉換的工作。經典的作法是在web.xml中配置一個編碼轉換過濾器,Spring就提供了一個編碼過濾器類CharacterEncodingFilter,下面,我們為應用配置上這個過濾器:
Spring的過濾器類是org.springframework.web.filter.CharacterEncodingFilter,通過encoding參數指定編碼轉換類型為GBK,<filter-mapping>的配置使該過濾器截獲所有的請示。 Struts的框架也需要在web.xml中配置,想必讀者朋友對Struts的配置都很熟悉,故在此不再提及,請參見本文所提供的源碼。 總結 本文通過一個文件上傳下載的Web應用,講解了如何構建基于SSH的Web應用,通過Struts和FormFile,Spring的LobHandler以及Spring為HibernateBlob處理所提供的用戶類BlobByteArrayType ,實現上傳和下載文件的功能僅需要廖廖數行的代碼即告完成。讀者只需對程序作稍許的調整,即可處理Clob字段: ?領域對象對應Clob字段的屬性聲明為String類型; ?映射文件對應Clob字段的屬性聲明為org.springframework.orm.hibernate3.support.ClobStringType類型。 本文通過SSH對文件上傳下載簡捷完美的實現得以管中窺豹了解SSH強強聯合構建Web應用的強大優勢。在行文中,還穿插了一些分層的設計經驗,配置技巧和Spring所提供的方便類,相信這些知識對您的開發都有所裨益。 |