文件的上傳和下載在J2EE編程已經(jīng)是一個非常古老的話題了,也許您馬上就能掰著指頭數(shù)出好幾個著名的大件:如SmartUpload、Apache的FileUpload。但如果您的項目是構(gòu)建在Struts+
Spring
+
Hibernate
(以下稱SSH)框架上的,這些大件就顯得笨重而滄桑了,SSH提供了一個簡捷方便的
文件上傳
下載的方案,我們只需要通過一些配置并輔以少量的代碼就可以完好解決這個問題了。
本文將圍繞SSH 文件上傳 下載的主題,向您詳細講述如何開發(fā)基于SSH的Web程序。SSH各框架的均為當前最新版本:
·Struts 1.2
· Spring 1.2.5
· Hibernate 3.0
本文選用的數(shù)據(jù)庫為Oracle 9i,當然你可以在不改動代碼的情況下,通過配置文件的調(diào)整將其移植到任何具有Blob字段類型的數(shù)據(jù)庫上,如MySQL,SQLServer等。
總體實現(xiàn)
上傳文件保存到T_FILE表中,T_FILE表結(jié)構(gòu)如下:
其中:
·FILE_ID:文件ID,32個字符,用 Hibernate 的uuid.hex算法生成。
·FILE_NAME:文件名。
·FILE_CONTENT:文件內(nèi)容,對應Oracle的Blob類型。
·REMARK:文件備注。
文件數(shù)據(jù)存儲在Blob類型的FILE_CONTENT表字段上,在 Spring 中采用OracleLobHandler來處理Lob字段(包括Clob和Blob),由于在程序中不需要引用到oracle數(shù)據(jù)驅(qū)動程序的具體類且屏蔽了不同數(shù)據(jù)庫處理Lob字段方法上的差別,從而撤除程序在多數(shù)據(jù)庫移植上的樊籬。
1.首先數(shù)據(jù)表中的Blob字段在Java領域?qū)ο笾新暶鳛閎yte[]類型,而非java.sql.Blob類型。
2.數(shù)據(jù)表Blob字段在 Hibernate 持久化映射文件中的type為org.springframework.orm.hibernate3.support.BlobByteArrayType,即 Spring 所提供的用戶自定義的類型,而非java.sql.Blob。
3.在 Spring 中使用org.springframework.jdbc.support.lob.OracleLobHandler處理Oracle數(shù)據(jù)庫的Blob類型字段。
通過這樣的設置和配置,我們就可以象持久化表的一般字段類型一樣處理Blob字段了。
以上是 Spring + Hibernate 將文件二進制數(shù)據(jù)持久化到數(shù)據(jù)庫的解決方案,而Struts通過將表單中file類型的組件映射為ActionForm中類型為org.apache.struts.upload. FormFile的屬性來獲取表單提交的文件數(shù)據(jù)。
綜上所述,我們可以通過圖 2,描繪出SSH處理 文件上傳 的方案:
文件上傳 的頁面如圖 3所示:
文件下載的頁面如圖 4所示:
該工程的資源結(jié)構(gòu)如圖 5所示:
工程的類按SSH的層次結(jié)構(gòu)劃分為數(shù)據(jù)持久層、業(yè)務層和Web層;WEB-INF下的applicationContext.xml為 Spring 的配置文件,struts-config.xml為Struts的配置文件,file-upload.jsp為 文件上傳 頁面,file-list.jsp為文件列表頁面。
本文后面的章節(jié)將從數(shù)據(jù)持久層->業(yè)務層->W(wǎng)eb層的開發(fā)順序,逐層講解 文件上傳 下載的開發(fā)過程。
數(shù)據(jù)持久層
1、領域?qū)ο蠹坝成湮募?
您可以使用 Hibernate Middlegen、HIbernate Tools、 Hibernate Syhchronizer等工具或手工的方式,編寫 Hibernate 的領域?qū)ο蠛陀成湮募F渲袑猅_FILE表的領域?qū)ο骉file.java為:
代碼 1 領域?qū)ο骉file
特別需要注意的是:數(shù)據(jù)庫表為Blob類型的字段在Tfile中的fileContent類型為byte[]。Tfile的 Hibernate 映射文件Tfile.hbm.xml放在Tfile .java類文件的相同目錄下:
代碼 2 領域?qū)ο笥成湮募?
fileContent字段映射為 Spring 所提供的BlobByteArrayType類型,BlobByteArrayType是用戶自定義的數(shù)據(jù)類型,它實現(xiàn)了 Hibernate 的org.hibernate.usertype.UserType接口。BlobByteArrayType使用從sessionFactory獲取的Lob操作句柄lobHandler將byte[]的數(shù)據(jù)保存到Blob數(shù)據(jù)庫字段中。這樣,我們就再沒有必要通過硬編碼的方式,先insert然后再update來完成Blob類型數(shù)據(jù)的持久化,這個原來難伺候的老爺終于被平民化了。關于lobHandler的配置請見本文后面的內(nèi)容。
此外lazy="true"說明地返回整個Tfile對象時,并不返回fileContent這個字段的數(shù)據(jù),只有在顯式調(diào)用tfile.getFileContent()方法時才真正從數(shù)據(jù)庫中獲取fileContent的數(shù)據(jù)。這是 Hibernate 3引入的新特性,對于包含重量級大數(shù)據(jù)的表字段,這種抽取方式提高了對大字段操作的靈活性,否則加載Tfile對象的結(jié)果集時如果總是返回fileContent,這種批量的數(shù)據(jù)抽取將可以引起數(shù)據(jù)庫的"洪泛效應"。
2、DAO編寫和配置
Spring 強調(diào)面向接口編程,所以我們將所有對Tfile的數(shù)據(jù)操作的方法定義在TfileDAO接口中,這些接口方法分別是:
·findByFildId(String fileId)
·save(Tfile tfile)
·List findAll()
TfileDAO Hibernate 提供了對TfileDAO接口基于 Hibernate 的實現(xiàn),如代碼 3所示:
代碼 3 基于 Hibernate 的fileDAO實現(xiàn)類
TfileDAO Hibernate 通過擴展 Spring 提供的 Hibernate 支持類 Hibernate DaoSupport而建立, Hibernate DaoSupport封裝了 Hibernate Template,而 Hibernate Template封裝了 Hibernate 所提供幾乎所有的的數(shù)據(jù)操作方法,如execute( Hibernate Callback action),load(Class entityClass, Serializable id),save(final Object entity)等等。
所以我們的DAO只需要簡單地調(diào)用父類的 Hibernate Template就可以完成幾乎所有的數(shù)據(jù)庫操作了。
由于 Spring 通過代理 Hibernate 完成數(shù)據(jù)層的操作,所以原 Hibernate 的配置文件hibernate.cfg.xml的信息也轉(zhuǎn)移到 Spring 的配置文件中:
代碼 4 Spring 中有關 Hibernate 的配置信息
第3~9行定義了一個數(shù)據(jù)源,其實現(xiàn)類是apache的BasicDataSource,第11~25行定義了 Hibernate 的會話工廠,會話工廠類用 Spring 提供的LocalSessionFactoryBean維護,它注入了數(shù)據(jù)源和資源映射文件,此外還通過一些鍵值對設置了 Hibernate 所需的屬性。
其中第16行通過類路徑的映射方式,將sshfile.model類包目錄下的所有領域?qū)ο蟮挠成湮募b載進來,在本文的例子里,它將裝載進Tfile.hbm.xml映射文件。如果有多個映射文件需要聲明,使用類路徑映射方式顯然比直接單獨指定映射文件名的方式要簡便。
第27~30行定義了 Spring 代理 Hibernate 數(shù)據(jù)操作的 Hibernate Template模板,而第32~34行將該模板注入到tfileDAO中。
需要指定的是 Spring 1.2.5提供了兩套 Hibernate 的支持包,其中 Hibernate 2相關的封裝類位于org.springframework.orm.hibernate2.*包中,而 Hibernate 3.0的封裝類位于org.springframework.orm.hibernate3.*包中,需要根據(jù)您所選用 Hibernate 版本進行正確選擇。
3、Lob字段處理的配置
我們前面已經(jīng)指出Oracle的Lob字段和一般類型的字段在操作上有一個明顯的區(qū)別--那就是你必須首先通過Oracle的empty_blob()/empty_clob()初始化Lob字段,然后獲取該字段的引用,通過這個引用更改其值。所以要完成對Lob字段的操作, Hibernate 必須執(zhí)行兩步數(shù)據(jù)庫訪問操作,先Insert再Update。
使用BlobByteArrayType字段類型后,為什么我們就可以象一般的字段類型一樣操作Blob字段呢?可以確定的一點是:BlobByteArrayType不可能逾越Blob天生的操作方式,原來是BlobByteArrayType數(shù)據(jù)類型本身具體數(shù)據(jù)訪問的功能,它通過LobHandler將兩次數(shù)據(jù)訪問的動作隱藏起來,使Blob字段的操作在表現(xiàn)上和其他一般字段業(yè)類型無異,所以LobHandler即是那個"苦了我一個,幸福十億人"的那位幕后英雄。
LobHandler必須注入到 Hibernate 會話工廠sessionFactory中,因為sessionFactory負責產(chǎn)生與數(shù)據(jù)庫交互的Session。LobHandler的配置如代碼 5所示:
代碼 5 Lob字段的處理句柄配置
首先,必須定義一個能夠從連接池中抽取出本地數(shù)據(jù)庫JDBC對象(如OracleConnection,OracleResultSet等)的抽取器:nativeJdbcExtractor,這樣才可以執(zhí)行一些特定數(shù)據(jù)庫的操作。對于那些僅封裝了Connection而未包括Statement的簡單數(shù)據(jù)連接池,SimpleNativeJdbcExtractor是效率最高的抽取器實現(xiàn)類,但具體到apache的BasicDataSource連接池,它封裝了所有JDBC的對象,這時就需要使用CommonsDbcpNativeJdbcExtractor了。 Spring 針對幾個著名的Web服務器的數(shù)據(jù)源提供了相應的JDBC抽取器:
·WebLogic:WebLogicNativeJdbcExtractor
·WebSphere:WebSphereNativeJdbcExtractor
·JBoss:JBossNativeJdbcExtractor
在定義了JDBC抽取器后,再定義lobHandler。 Spring 1.2.5提供了兩個lobHandler:
·DefaultLobHandler:適用于大部分的數(shù)據(jù)庫,如SqlServer,MySQL,對Oracle 10g也適用,但不適用于Oracle 9i(看來Oracle 9i確實是個怪胎,誰叫Oracle 公司自己都說Oracle 9i是一個過渡性的產(chǎn)品呢)。
·OracleLobHandler:適用于Oracle 9i和Oracle 10g。
由于我們的數(shù)據(jù)庫是Oracle9i,所以使用OracleLobHandler。
在配置完LobHandler后, 還需要將其注入到sessionFactory的Bean中,下面是調(diào)用后的sessionFactory Bean的配置:
代碼 6 將lobHandler注入到sessionFactory中的配置
如第7所示,通過sessionFactory的lobHandler屬性進行注入。
其中save(FileActionForm fileForm)方法,將封裝在fileForm中的上傳文件保存到數(shù)據(jù)庫中,這里我們使用FileActionForm作為方法入?yún)ⅲ現(xiàn)ileActionForm是Web層的表單數(shù)據(jù)對象,它封裝了提交表單的數(shù)據(jù)。將FileActionForm直接作為業(yè)務層的接口入?yún)ⅲ喈斢趯eb層傳播到業(yè)務層中去,即將業(yè)務層綁定在特定的Web層實現(xiàn)技術(shù)中,按照分層模型學院派的觀點,這是一種反模塊化的設計,但在"一般"的業(yè)務系統(tǒng)并無需提供多種UI界面,系統(tǒng)Web層將來切換到另一種實現(xiàn)技術(shù)的可能性也微乎其微,所以筆者覺得沒有必要為了這個業(yè)務層完全獨立于調(diào)用層的過高目標而去搞一個額外的隔離層,浪費了原材料不說,還將系統(tǒng)搞得過于復雜,相比于其它原則,"簡單"始終是最大的一條原則。
getAllFile()負責獲取T_FILE表所有記錄,以便在網(wǎng)頁上顯示出來。
而getFileName(String fileId)和write(OutputStream os,String fileId)則用于下載某個特定的文件。具體的調(diào)用是將Web層將response.getOutputStream()傳給write(OutputStream os,String fileId)接口,業(yè)務層直接將文件數(shù)據(jù)輸出到這個響應流中。具體實現(xiàn)請參見錯誤!未找到引用源。節(jié)下載文件部分。
2、業(yè)務層接口實現(xiàn)類
FileService的實現(xiàn)類為FileServiceImpl,其中save(FileActionForm fileForm)的實現(xiàn)如下所示:
代碼 8 業(yè)務接口實現(xiàn)類之save()
在save(FileActionForm fileForm)方法里,完成兩個步驟:
其一,象在水桶間倒水一樣,將FileActionForm對象中的數(shù)據(jù)倒入到Tfile對象中;
其二,調(diào)用TfileDAO保存數(shù)據(jù)。
需要特別注意的是代碼的第11行,F(xiàn)ileActionForm的fileContent屬性為org.apache.struts.upload.FormFile類型,F(xiàn)ormFile提供了一個方便的方法getFileData(),即可獲取文件的二進制數(shù)據(jù)。通過解讀FormFile接口實現(xiàn)類DiskFile的原碼,我們可能知道FormFile本身并不緩存文件的數(shù)據(jù),只有實際調(diào)用getFileData()時,才從磁盤文件輸入流中獲取數(shù)據(jù)。由于FormFile使用流讀取方式獲取數(shù)據(jù),本身沒有緩存文件的所有數(shù)據(jù),所以對于上傳超大體積的文件,也是沒有問題的;但是,由于數(shù)據(jù)持久層的Tfile使用byte[]來緩存文件的數(shù)據(jù),所以并不適合處理超大體積的文件(如100M),對于超大體積的文件,依然需要使用java.sql.Blob類型以常規(guī)流操作的方式來處理。
此外,通過FileForm的getFileName()方法就可以獲得上傳文件的文件名,如第21行代碼所示。
write(OutputStream os,String fileId)方法的實現(xiàn),如代碼 9所示:
代碼 9 業(yè)務接口實現(xiàn)類之write()
write(OutputStream os,String fileId)也簡單地分為兩個操作步驟,首先,根據(jù)fileId加載表記錄,然后將fileContent寫入到輸出流中。
3、 Spring 事務配置
下面,我們來看如何在 Spring 配置文件中為FileService配置聲明性的事務
Spring 的事務配置包括兩個部分:
其一,定義事務管理器transactionManager,使用 Hibernate TransactionManager實現(xiàn)事務管理;
其二,對各個業(yè)務接口進行定義,其實txProxyTemplate和fileService是父子節(jié)點的關系,本來可以將txProxyTemplate定義的內(nèi)容合并到fileService中一起定義,由于我們的系統(tǒng)僅有一個業(yè)務接口需要定義,所以將其定義的一部分抽象到父節(jié)點txProxyTemplate中意義確實不大,但是對于真實的系統(tǒng),往往擁有為數(shù)眾多的業(yè)務接口需要定義,將這些業(yè)務接口定義內(nèi)容的共同部分抽取到一個父節(jié)點中,然后在子節(jié)點中通過parent進行關聯(lián),就可以大大簡化業(yè)務接口的配置了。
父節(jié)點txProxyTemplate注入了事務管理器,此外還定義了業(yè)務接口事務管理的方法(允許通過通配符的方式進行匹配聲明,如前兩個接口方法),有些接口方法僅對數(shù)據(jù)進行讀操作,而另一些接口方法需要涉及到數(shù)據(jù)的更改。對于前者,可以通過readOnly標識出來,這樣有利于操作性能的提高,需要注意的是由于父類節(jié)點定義的Bean僅是子節(jié)點配置信息的抽象,并不能具體實現(xiàn)化一個Bean對象,所以需要特別標注為abstract="true",如第8行所示。
fileService作為一個目標類被注入到事務代理器中,而fileService實現(xiàn)類所需要的tfileDAO實例,通過引用3.2節(jié)中定義的tfileDAO Bean注入。
Web層實現(xiàn)
1、Web層的構(gòu)件和交互流程
Web層包括主要3個功能:
·上傳文件。
·列出所有已經(jīng)上傳的文件列表,以供點擊下載。
·下載文件。
Web層實現(xiàn)構(gòu)件包括與2個JSP頁面,1個ActionForm及一個Action:
·file-upload.jsp:上傳文件的頁面。
·file-list.jsp:已經(jīng)上傳文件的列表頁面。
·FileActionForm:file-upload.jsp頁面表單對應的ActionForm。
·FileAction:繼承org.apache.struts.actions.DispatchAction的Action,這樣這個Action就可以通過一個URL參數(shù)區(qū)分中響應不同的請求。
Web層的這些構(gòu)件的交互流程如圖 6所示:
其中,在執(zhí)行 文件上傳 的請求時,F(xiàn)ileAction在執(zhí)行 文件上傳 后,forward到loadAllFile出口中,loadAllFile加載數(shù)據(jù)庫中所有已經(jīng)上傳的記錄,然后forward到名為fileListPage的出口中,調(diào)用file-list.jsp頁面顯示已經(jīng)上傳的記錄。
2、FileAction功能
Struts 1.0的Action有一個弱項:一個Action只能處理一種請求,Struts 1.1中引入了一個DispatchAction,允許通過URL參數(shù)指定調(diào)用Action中的某個方法,如http://yourwebsite/fileAction.do?method=upload即調(diào)用FileAction中的upload方法。通過這種方式,我們就可以將一些相關的請求集中到一個Action當中編寫,而沒有必要為某個請求操作編寫一個Action類。但是參數(shù)名是要在struts-config.xml中配置的:
第6行的parameter="method"指定了承載方法名的參數(shù),第9行中,我們還配置了一個調(diào)用FileAction不同方法的Action出口。
FileAction共有3個請求響應的方法,它們分別是:
·upload(…):處理上傳文件的請求。
·listAllFile(…):處理加載數(shù)據(jù)庫表中所有記錄的請求。
·download(…):處理下載文件的請求。
下面我們分別對這3個請求處理方法進行講解。
2.1 上傳文件
上傳文件的請求處理方法非常簡單,簡之言之,就是從 Spring 容器中獲取業(yè)務層處理類FileService,調(diào)用其save(FileActionForm form)方法上傳文件,如下所示:
由于FileAction其它兩個請求處理方法也需要從 Spring 容器中獲取FileService實例,所以我們特別提供了一個getFileService()方法(第15~21行)。重構(gòu)的一條原則就是:"發(fā)現(xiàn)代碼中有重復的表達式,將其提取為一個變量;發(fā)現(xiàn)類中有重復的代碼段,將其提取為一個方法;發(fā)現(xiàn)不同類中有相同的方法,將其提取為一個類"。在真實的系統(tǒng)中,往往擁有多個Action和多個Service類,這時一個比較好的設置思路是,提供一個獲取所有Service實現(xiàn)對象的工具類,這樣就可以將 Spring 的Service配置信息屏蔽在一個類中,否則Service的配置名字散落在程序各處,維護性是很差的。
2.2 列出所有已經(jīng)上傳的文件
listAllFile方法調(diào)用Servie層方法加載T_FILE表中所有記錄,并將其保存在Request域中,然后forward到列表頁面中:
file-list.jsp頁面使用Struts標簽展示出保存在Request域中的記錄:
展現(xiàn)頁面的每條記錄掛接著一個鏈接地址,形如:fileAction.do?method=download&fileId=xxx,method參數(shù)指定了這個請求由FileAction的download方法來響應,fileId指定了記錄的主鍵。
由于在FileActionForm中,我們定義了fileId的屬性,所以在download響應方法中,我們將可以從FileActionForm中取得fileId的值。這里涉及到一個處理多個請求Action所對應的ActionForm的設計問題,由于原來的Action只能對應一個請求,那么原來的ActionForm非常簡單,它僅需要將這個請求的參數(shù)項作為其屬性就可以了,但現(xiàn)在一個Action對應多個請求,每個請求所對應的參數(shù)項是不一樣的,此時的ActionForm的屬性就必須是多請求參數(shù)項的并集了。所以,除了 文件上傳 請求所對應的fileContent和remark屬性外還包括文件下載的fileId屬性:
當然這樣會造成屬性的冗余,比如在 文件上傳 的請求中,只會用到fileContent和remark屬性,而在文件下載的請求時,只會使用到fileId屬性。但這種冗余是會帶來好處的--它使得一個Action可以處理多個請求。
2.3 下載文件
在列表頁面中點擊一個文件下載,其請求由FileAction的download方法來響應,download方法調(diào)用業(yè)務層的FileService方法,獲取文件數(shù)據(jù)并寫出到response的響應流中。通過合理設置HTTP響應頭參數(shù),將響應流在客戶端表現(xiàn)為一個下載文件對話框,其代碼如下所示:
代碼 10 業(yè)務接口實現(xiàn)類之download
第15~18行,設置HTTP響應頭,將響應類型設置為application/x-msdownload MIME類型,則響應流在IE中將彈出一個文件下載的對話框,如圖 4所示。IE所支持的MIME類型多達26種,您可以通過這個網(wǎng)址查看其他的MIME類型:
http://msdn.microsoft.com/workshop/networking/moniker/overview/appendix_a.asp。
如果下載文件的文件名含有中文字符,如果不對其進行硬編碼,如第18行所示,客戶文件下載對話框中出現(xiàn)的文件名將會發(fā)生亂碼。
第19行代碼獲得response的輸出流,作為FileServie write(OutputStream os,String fileId)的入?yún)ⅲ@樣文件的內(nèi)容將寫到response的輸出流中。
3、web.xml文件的配置
Spring 容器在何時啟動呢?我可以在Web容器初始化來執(zhí)行啟動 Spring 容器的操作, Spring 提供了兩種方式啟動的方法:
·通過org.springframework.web.context .ContextLoaderListener容器監(jiān)聽器,在Web容器初始化時觸發(fā)初始化 Spring 容器,在web.xml中通過<listener></listener>對其進行配置。
·通過Servlet org.springframework.web.context.ContextLoaderServlet,將其配置為自動啟動的Servlet,在Web容器初始化時,通過這個Servlet啟動 Spring 容器。
在初始化 Spring 容器之前,必須先初始化log4J的引擎, Spring 也提供了容器監(jiān)聽器和自動啟動Servlet兩種方式對log4J引擎進行初始化:
·org.springframework.web.util .Log4jConfigListener
·org.springframework.web.util.Log4jConfigServlet
下面我們來說明如何配置web.xml啟動 Spring 容器:
代碼 11 web.xml中對應 Spring 的配置內(nèi)容
啟動 Spring 容器時,需要得到兩個信息: Spring 配置文件的地址和Log4J屬性文件,這兩上信息分別通過contextConfigLocationWeb和log4jConfigLocation容器參數(shù)指定,如果有多個 Spring 配置文件,則用逗號隔開,如:
/WEB-INF/applicationContext_1.xml, /WEB-INF/applicationContext_1.xm2
由于在啟動ContextLoaderServlet之前,必須事先初始化Log4J的引擎,所以Log4jConfigServlet必須在ContextLoaderServlet之前啟動,這通過<load-on-startup>來指定它們啟動的先后順序。
亂碼是開發(fā)Web應用程序一個比較老套又常見問題,由于不同Web應用服務器的默認編碼是不一樣的,為了方便Web應用在不同的Web應用服務器上移植,最好的做法是Web程序自身來處理編碼轉(zhuǎn)換的工作。經(jīng)典的作法是在web.xml中配置一個編碼轉(zhuǎn)換過濾器, Spring 就提供了一個編碼過濾器類CharacterEncodingFilter,下面,我們?yōu)閼门渲蒙线@個過濾器:
Spring 的過濾器類是org.springframework.web.filter.CharacterEncodingFilter,通過encoding參數(shù)指定編碼轉(zhuǎn)換類型為GBK,<filter-mapping>的配置使該過濾器截獲所有的請示。
Struts的框架也需要在web.xml中配置,想必讀者朋友對Struts的配置都很熟悉,故在此不再提及,請參見本文所提供的源碼。
總結(jié)
本文通過一個 文件上傳 下載的Web應用,講解了如何構(gòu)建基于SSH的Web應用,通過Struts和FormFile, Spring 的LobHandler以及 Spring 為 Hibernate Blob處理所提供的用戶類BlobByteArrayType ,實現(xiàn)上傳和下載文件的功能僅需要廖廖數(shù)行的代碼即告完成。讀者只需對程序作稍許的調(diào)整,即可處理Clob字段:
·領域?qū)ο髮狢lob字段的屬性聲明為String類型;
·映射文件對應Clob字段的屬性聲明為org.springframework.orm.hibernate3.support.ClobStringType類型。
本文通過SSH對 文件上傳 下載簡捷完美的實現(xiàn)得以管中窺豹了解SSH強強聯(lián)合構(gòu)建Web應用的強大優(yōu)勢。在行文中,還穿插了一些分層的設計經(jīng)驗,配置技巧和 Spring 所提供的方便類,相信這些知識對您的開發(fā)都有所裨益。?
本文將圍繞SSH 文件上傳 下載的主題,向您詳細講述如何開發(fā)基于SSH的Web程序。SSH各框架的均為當前最新版本:
·Struts 1.2
· Spring 1.2.5
· Hibernate 3.0
本文選用的數(shù)據(jù)庫為Oracle 9i,當然你可以在不改動代碼的情況下,通過配置文件的調(diào)整將其移植到任何具有Blob字段類型的數(shù)據(jù)庫上,如MySQL,SQLServer等。
總體實現(xiàn)
上傳文件保存到T_FILE表中,T_FILE表結(jié)構(gòu)如下:
|
圖 1 T_FILE表結(jié)構(gòu) |
其中:
·FILE_ID:文件ID,32個字符,用 Hibernate 的uuid.hex算法生成。
·FILE_NAME:文件名。
·FILE_CONTENT:文件內(nèi)容,對應Oracle的Blob類型。
·REMARK:文件備注。
文件數(shù)據(jù)存儲在Blob類型的FILE_CONTENT表字段上,在 Spring 中采用OracleLobHandler來處理Lob字段(包括Clob和Blob),由于在程序中不需要引用到oracle數(shù)據(jù)驅(qū)動程序的具體類且屏蔽了不同數(shù)據(jù)庫處理Lob字段方法上的差別,從而撤除程序在多數(shù)據(jù)庫移植上的樊籬。
1.首先數(shù)據(jù)表中的Blob字段在Java領域?qū)ο笾新暶鳛閎yte[]類型,而非java.sql.Blob類型。
2.數(shù)據(jù)表Blob字段在 Hibernate 持久化映射文件中的type為org.springframework.orm.hibernate3.support.BlobByteArrayType,即 Spring 所提供的用戶自定義的類型,而非java.sql.Blob。
3.在 Spring 中使用org.springframework.jdbc.support.lob.OracleLobHandler處理Oracle數(shù)據(jù)庫的Blob類型字段。
通過這樣的設置和配置,我們就可以象持久化表的一般字段類型一樣處理Blob字段了。
以上是 Spring + Hibernate 將文件二進制數(shù)據(jù)持久化到數(shù)據(jù)庫的解決方案,而Struts通過將表單中file類型的組件映射為ActionForm中類型為org.apache.struts.upload. FormFile的屬性來獲取表單提交的文件數(shù)據(jù)。
綜上所述,我們可以通過圖 2,描繪出SSH處理 文件上傳 的方案:
|
圖 2 SSH處理 文件上傳 技術(shù)方案 |
文件上傳 的頁面如圖 3所示:
|
圖 3 文件上傳 頁面 |
文件下載的頁面如圖 4所示:
|
圖 4 文件下載頁面 |
該工程的資源結(jié)構(gòu)如圖 5所示:
|
圖 5 工程資源結(jié)構(gòu) |
工程的類按SSH的層次結(jié)構(gòu)劃分為數(shù)據(jù)持久層、業(yè)務層和Web層;WEB-INF下的applicationContext.xml為 Spring 的配置文件,struts-config.xml為Struts的配置文件,file-upload.jsp為 文件上傳 頁面,file-list.jsp為文件列表頁面。
本文后面的章節(jié)將從數(shù)據(jù)持久層->業(yè)務層->W(wǎng)eb層的開發(fā)順序,逐層講解 文件上傳 下載的開發(fā)過程。
數(shù)據(jù)持久層
1、領域?qū)ο蠹坝成湮募?
您可以使用 Hibernate Middlegen、HIbernate Tools、 Hibernate Syhchronizer等工具或手工的方式,編寫 Hibernate 的領域?qū)ο蠛陀成湮募F渲袑猅_FILE表的領域?qū)ο骉file.java為:
代碼 1 領域?qū)ο骉file
|
1. package sshfile.model;
2. public class Tfile 3.{ 4. private String fileId; 5. private String fileName; 6. private byte[] fileContent; 7. private String remark; 8. …//getter and setter 9. } |
特別需要注意的是:數(shù)據(jù)庫表為Blob類型的字段在Tfile中的fileContent類型為byte[]。Tfile的 Hibernate 映射文件Tfile.hbm.xml放在Tfile .java類文件的相同目錄下:
代碼 2 領域?qū)ο笥成湮募?
|
1. <?xml version="1.0"?>
2. <!DOCTYPE hibernate-mapping PUBLIC 3. "-// Hibernate / Hibernate Mapping DTD 3.0//EN" 4. "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" > 5. <hibernate-mapping> 6. <class name="sshfile.model.Tfile" table="T_FILE"> 7. <id name="fileId" type="java.lang.String" column="FILE_ID"> 8. <generator class="uuid.hex"/> 9. </id> 10. <property name="fileContent" 11. type="org.springframework.orm.hibernate3.support.BlobByteArrayType" 12. column="FILE_CONTENT" lazy="true"/> 13. …//其它一般字段的映射 14. </class> 15. </hibernate-mapping> |
fileContent字段映射為 Spring 所提供的BlobByteArrayType類型,BlobByteArrayType是用戶自定義的數(shù)據(jù)類型,它實現(xiàn)了 Hibernate 的org.hibernate.usertype.UserType接口。BlobByteArrayType使用從sessionFactory獲取的Lob操作句柄lobHandler將byte[]的數(shù)據(jù)保存到Blob數(shù)據(jù)庫字段中。這樣,我們就再沒有必要通過硬編碼的方式,先insert然后再update來完成Blob類型數(shù)據(jù)的持久化,這個原來難伺候的老爺終于被平民化了。關于lobHandler的配置請見本文后面的內(nèi)容。
此外lazy="true"說明地返回整個Tfile對象時,并不返回fileContent這個字段的數(shù)據(jù),只有在顯式調(diào)用tfile.getFileContent()方法時才真正從數(shù)據(jù)庫中獲取fileContent的數(shù)據(jù)。這是 Hibernate 3引入的新特性,對于包含重量級大數(shù)據(jù)的表字段,這種抽取方式提高了對大字段操作的靈活性,否則加載Tfile對象的結(jié)果集時如果總是返回fileContent,這種批量的數(shù)據(jù)抽取將可以引起數(shù)據(jù)庫的"洪泛效應"。
2、DAO編寫和配置
Spring 強調(diào)面向接口編程,所以我們將所有對Tfile的數(shù)據(jù)操作的方法定義在TfileDAO接口中,這些接口方法分別是:
·findByFildId(String fileId)
·save(Tfile tfile)
·List findAll()
TfileDAO Hibernate 提供了對TfileDAO接口基于 Hibernate 的實現(xiàn),如代碼 3所示:
代碼 3 基于 Hibernate 的fileDAO實現(xiàn)類
|
1. package sshfile.dao;
2. 3. import sshfile.model.*; 4. import org.springframework.orm.hibernate3.support. Hibernate DaoSupport; 5. import java.util.List; 6. 7. public class TfileDAO Hibernate 8. extends Hibernate DaoSupport implements TfileDAO 9. { 10. public Tfile findByFildId(String fileId) 11. { 12. return (Tfile) get Hibernate Template().get(Tfile.class, fileId); 13. } 14. public void save(Tfile tfile) 15. { 16. get Hibernate Template().save(tfile); 17. get Hibernate Template().flush(); 18. } 19. public List findAll() 20. { 21. return get Hibernate Template().loadAll(Tfile.class); 22. } 23. } |
TfileDAO Hibernate 通過擴展 Spring 提供的 Hibernate 支持類 Hibernate DaoSupport而建立, Hibernate DaoSupport封裝了 Hibernate Template,而 Hibernate Template封裝了 Hibernate 所提供幾乎所有的的數(shù)據(jù)操作方法,如execute( Hibernate Callback action),load(Class entityClass, Serializable id),save(final Object entity)等等。
所以我們的DAO只需要簡單地調(diào)用父類的 Hibernate Template就可以完成幾乎所有的數(shù)據(jù)庫操作了。
由于 Spring 通過代理 Hibernate 完成數(shù)據(jù)層的操作,所以原 Hibernate 的配置文件hibernate.cfg.xml的信息也轉(zhuǎn)移到 Spring 的配置文件中:
代碼 4 Spring 中有關 Hibernate 的配置信息
|
1. <beans>
2. <!-- 數(shù)據(jù)源的配置 //--> 3. <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 4. destroy-method="close"> 5. <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> 6. <property name="url" value="jdbc:oracle:thin:@localhost:1521:ora9i"/> 7. <property name="username" value="test"/> 8. <property name="password" value="test"/> 9. </bean> 10. <!-- Hibernate 會話工廠配置 //--> 11. <bean id="sessionFactory" 12. class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> 13. <property name="dataSource" ref="dataSource"/> 14. <property name="mappingDirectoryLocations"> 15. <list> 16. <value>classpath:/sshfile/model</value> 17. </list> 18. </property> 19. <property name="hibernateProperties"> 20. <props> 21. <prop key="hibernate.dialect">org.hibernate.dialect.OracleDialect</prop> 22. <prop key="hibernate.cglib.use_reflection_optimizer">true</prop> 23. </props> 24. </property> 25. </bean> 26. <!-- Hibernate 模板//--> 27. <bean id="hibernateTemplate" 28. class="org.springframework.orm.hibernate3. Hibernate Template"> 29. <property name="sessionFactory" ref="sessionFactory"/> 30. </bean> 31. <!--DAO配置 //--> 32. <bean id="tfileDAO" class="sshfile.dao.TfileDAO Hibernate "> 33. <property name="hibernateTemplate" ref="hibernateTemplate" /> 34. </bean> 35. … 36. </beans> |
第3~9行定義了一個數(shù)據(jù)源,其實現(xiàn)類是apache的BasicDataSource,第11~25行定義了 Hibernate 的會話工廠,會話工廠類用 Spring 提供的LocalSessionFactoryBean維護,它注入了數(shù)據(jù)源和資源映射文件,此外還通過一些鍵值對設置了 Hibernate 所需的屬性。
其中第16行通過類路徑的映射方式,將sshfile.model類包目錄下的所有領域?qū)ο蟮挠成湮募b載進來,在本文的例子里,它將裝載進Tfile.hbm.xml映射文件。如果有多個映射文件需要聲明,使用類路徑映射方式顯然比直接單獨指定映射文件名的方式要簡便。
第27~30行定義了 Spring 代理 Hibernate 數(shù)據(jù)操作的 Hibernate Template模板,而第32~34行將該模板注入到tfileDAO中。
需要指定的是 Spring 1.2.5提供了兩套 Hibernate 的支持包,其中 Hibernate 2相關的封裝類位于org.springframework.orm.hibernate2.*包中,而 Hibernate 3.0的封裝類位于org.springframework.orm.hibernate3.*包中,需要根據(jù)您所選用 Hibernate 版本進行正確選擇。
3、Lob字段處理的配置
我們前面已經(jīng)指出Oracle的Lob字段和一般類型的字段在操作上有一個明顯的區(qū)別--那就是你必須首先通過Oracle的empty_blob()/empty_clob()初始化Lob字段,然后獲取該字段的引用,通過這個引用更改其值。所以要完成對Lob字段的操作, Hibernate 必須執(zhí)行兩步數(shù)據(jù)庫訪問操作,先Insert再Update。
使用BlobByteArrayType字段類型后,為什么我們就可以象一般的字段類型一樣操作Blob字段呢?可以確定的一點是:BlobByteArrayType不可能逾越Blob天生的操作方式,原來是BlobByteArrayType數(shù)據(jù)類型本身具體數(shù)據(jù)訪問的功能,它通過LobHandler將兩次數(shù)據(jù)訪問的動作隱藏起來,使Blob字段的操作在表現(xiàn)上和其他一般字段業(yè)類型無異,所以LobHandler即是那個"苦了我一個,幸福十億人"的那位幕后英雄。
LobHandler必須注入到 Hibernate 會話工廠sessionFactory中,因為sessionFactory負責產(chǎn)生與數(shù)據(jù)庫交互的Session。LobHandler的配置如代碼 5所示:
代碼 5 Lob字段的處理句柄配置
|
1. <beans>
2. … 3. <bean id="nativeJdbcExtractor" 4. class="org.springframework.jdbc.support.nativejdbc.CommonsDbcpNativeJdbcExtractor" 5. lazy-init="true"/> 6. <bean id="lobHandler" 7. class="org.springframework.jdbc.support.lob.OracleLobHandler" lazy-init="true"> 8. <property name="nativeJdbcExtractor"> 9. <ref local="nativeJdbcExtractor"/> 10. </property> 11. </bean> 12. … 13. </beans> |
首先,必須定義一個能夠從連接池中抽取出本地數(shù)據(jù)庫JDBC對象(如OracleConnection,OracleResultSet等)的抽取器:nativeJdbcExtractor,這樣才可以執(zhí)行一些特定數(shù)據(jù)庫的操作。對于那些僅封裝了Connection而未包括Statement的簡單數(shù)據(jù)連接池,SimpleNativeJdbcExtractor是效率最高的抽取器實現(xiàn)類,但具體到apache的BasicDataSource連接池,它封裝了所有JDBC的對象,這時就需要使用CommonsDbcpNativeJdbcExtractor了。 Spring 針對幾個著名的Web服務器的數(shù)據(jù)源提供了相應的JDBC抽取器:
·WebLogic:WebLogicNativeJdbcExtractor
·WebSphere:WebSphereNativeJdbcExtractor
·JBoss:JBossNativeJdbcExtractor
在定義了JDBC抽取器后,再定義lobHandler。 Spring 1.2.5提供了兩個lobHandler:
·DefaultLobHandler:適用于大部分的數(shù)據(jù)庫,如SqlServer,MySQL,對Oracle 10g也適用,但不適用于Oracle 9i(看來Oracle 9i確實是個怪胎,誰叫Oracle 公司自己都說Oracle 9i是一個過渡性的產(chǎn)品呢)。
·OracleLobHandler:適用于Oracle 9i和Oracle 10g。
由于我們的數(shù)據(jù)庫是Oracle9i,所以使用OracleLobHandler。
在配置完LobHandler后, 還需要將其注入到sessionFactory的Bean中,下面是調(diào)用后的sessionFactory Bean的配置:
代碼 6 將lobHandler注入到sessionFactory中的配置
|
1. <beans>
2. … 3. <bean id="sessionFactory" 4. class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> 5. <property name="dataSource" ref="dataSource"/> 6. <!-- 為處理Blob類型字段的句柄聲明 //--> 7. <property name="lobHandler" ref="lobHandler"/> 8. … 9. </bean> 10. … 11. </beans> |
如第7所示,通過sessionFactory的lobHandler屬性進行注入。
業(yè)務層
1、業(yè)務層接口
"面向接口而非面向類編程"是 Spring 不遺余力所推薦的編程原則,這條原則也已經(jīng)為大部開發(fā)者所接受;此外,JDK的動態(tài)代理只對接口有效,否則必須使用CGLIB生成目標類的子類。我們依從于 Spring 的倡導為業(yè)務類定義一個接口:
代碼 7 業(yè)務層操作接口
?
1、業(yè)務層接口
"面向接口而非面向類編程"是 Spring 不遺余力所推薦的編程原則,這條原則也已經(jīng)為大部開發(fā)者所接受;此外,JDK的動態(tài)代理只對接口有效,否則必須使用CGLIB生成目標類的子類。我們依從于 Spring 的倡導為業(yè)務類定義一個接口:
代碼 7 業(yè)務層操作接口
?
|
1. public interface FileService
2. { 3. void save(FileActionForm fileForm);//將提交的上傳文件保存到數(shù)據(jù)表中 4. List getAllFile();//得到T_FILE所示記錄 5. void write(OutputStream os,String fileId);//將某個文件的文件數(shù)據(jù)寫出到輸出流中 6. String getFileName(String fileId);//獲取文件名 7. } |
其中save(FileActionForm fileForm)方法,將封裝在fileForm中的上傳文件保存到數(shù)據(jù)庫中,這里我們使用FileActionForm作為方法入?yún)ⅲ現(xiàn)ileActionForm是Web層的表單數(shù)據(jù)對象,它封裝了提交表單的數(shù)據(jù)。將FileActionForm直接作為業(yè)務層的接口入?yún)ⅲ喈斢趯eb層傳播到業(yè)務層中去,即將業(yè)務層綁定在特定的Web層實現(xiàn)技術(shù)中,按照分層模型學院派的觀點,這是一種反模塊化的設計,但在"一般"的業(yè)務系統(tǒng)并無需提供多種UI界面,系統(tǒng)Web層將來切換到另一種實現(xiàn)技術(shù)的可能性也微乎其微,所以筆者覺得沒有必要為了這個業(yè)務層完全獨立于調(diào)用層的過高目標而去搞一個額外的隔離層,浪費了原材料不說,還將系統(tǒng)搞得過于復雜,相比于其它原則,"簡單"始終是最大的一條原則。
getAllFile()負責獲取T_FILE表所有記錄,以便在網(wǎng)頁上顯示出來。
而getFileName(String fileId)和write(OutputStream os,String fileId)則用于下載某個特定的文件。具體的調(diào)用是將Web層將response.getOutputStream()傳給write(OutputStream os,String fileId)接口,業(yè)務層直接將文件數(shù)據(jù)輸出到這個響應流中。具體實現(xiàn)請參見錯誤!未找到引用源。節(jié)下載文件部分。
2、業(yè)務層接口實現(xiàn)類
FileService的實現(xiàn)類為FileServiceImpl,其中save(FileActionForm fileForm)的實現(xiàn)如下所示:
代碼 8 業(yè)務接口實現(xiàn)類之save()
|
1. …
2. public class FileServiceImpl 3. implements FileService 4. { 5. private TfileDAO tfileDAO; 6. public void save(FileActionForm fileForm) 7. { 8. Tfile tfile = new Tfile(); 9. try 10. { 11. tfile.setFileContent(fileForm.getFileContent().getFileData()); 12. } 13. catch (FileNotFoundException ex) 14. { 15. throw new RuntimeException(ex); 16. } 17. catch (IOException ex) 18. { 19. throw new RuntimeException(ex); 20. } 21. tfile.setFileName(fileForm.getFileContent().getFileName()); 22. tfile.setRemark(fileForm.getRemark()); 23. tfileDAO.save(tfile); 24. } 25. … 26. } |
在save(FileActionForm fileForm)方法里,完成兩個步驟:
其一,象在水桶間倒水一樣,將FileActionForm對象中的數(shù)據(jù)倒入到Tfile對象中;
其二,調(diào)用TfileDAO保存數(shù)據(jù)。
需要特別注意的是代碼的第11行,F(xiàn)ileActionForm的fileContent屬性為org.apache.struts.upload.FormFile類型,F(xiàn)ormFile提供了一個方便的方法getFileData(),即可獲取文件的二進制數(shù)據(jù)。通過解讀FormFile接口實現(xiàn)類DiskFile的原碼,我們可能知道FormFile本身并不緩存文件的數(shù)據(jù),只有實際調(diào)用getFileData()時,才從磁盤文件輸入流中獲取數(shù)據(jù)。由于FormFile使用流讀取方式獲取數(shù)據(jù),本身沒有緩存文件的所有數(shù)據(jù),所以對于上傳超大體積的文件,也是沒有問題的;但是,由于數(shù)據(jù)持久層的Tfile使用byte[]來緩存文件的數(shù)據(jù),所以并不適合處理超大體積的文件(如100M),對于超大體積的文件,依然需要使用java.sql.Blob類型以常規(guī)流操作的方式來處理。
此外,通過FileForm的getFileName()方法就可以獲得上傳文件的文件名,如第21行代碼所示。
write(OutputStream os,String fileId)方法的實現(xiàn),如代碼 9所示:
代碼 9 業(yè)務接口實現(xiàn)類之write()
|
1. …
2. public class FileServiceImpl 3. implements FileService 4. { 5. 6. public void write(OutputStream os, String fileId) 7. { 8. Tfile tfile = tfileDAO.findByFildId(fileId); 9. try 10. { 11. os.write(tfile.getFileContent()); 12. os.flush(); 13. } 14. catch (IOException ex) 15. { 16. throw new RuntimeException(ex); 17. } 18. } 19. … 20. } |
write(OutputStream os,String fileId)也簡單地分為兩個操作步驟,首先,根據(jù)fileId加載表記錄,然后將fileContent寫入到輸出流中。
3、 Spring 事務配置
下面,我們來看如何在 Spring 配置文件中為FileService配置聲明性的事務
|
1. <beans>
2. … 3. <bean id="transactionManager" 4. class="org.springframework.orm.hibernate3. Hibernate TransactionManager"> 5. <property name="sessionFactory" ref="sessionFactory"/> 6. </bean> 7. <!-- 事務處理的AOP配置 //--> 8. <bean id="txProxyTemplate" abstract="true" 9. class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean"> 10. <property name="transactionManager" ref="transactionManager"/> 11. <property name="transactionAttributes"> 12. <props> 13. <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop> 14. <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop> 15. <prop key="save">PROPAGATION_REQUIRED</prop> 16. <prop key="write">PROPAGATION_REQUIRED,readOnly</prop> 17. </props> 18. </property> 19. </bean> 20. <bean id="fileService" parent="txProxyTemplate"> 21. <property name="target"> 22. <bean class="sshfile.service.FileServiceImpl"> 23. <property name="tfileDAO" ref="tfileDAO"/> 24. </bean> 25. </property> 26. </bean> 27. </beans> |
Spring 的事務配置包括兩個部分:
其一,定義事務管理器transactionManager,使用 Hibernate TransactionManager實現(xiàn)事務管理;
其二,對各個業(yè)務接口進行定義,其實txProxyTemplate和fileService是父子節(jié)點的關系,本來可以將txProxyTemplate定義的內(nèi)容合并到fileService中一起定義,由于我們的系統(tǒng)僅有一個業(yè)務接口需要定義,所以將其定義的一部分抽象到父節(jié)點txProxyTemplate中意義確實不大,但是對于真實的系統(tǒng),往往擁有為數(shù)眾多的業(yè)務接口需要定義,將這些業(yè)務接口定義內(nèi)容的共同部分抽取到一個父節(jié)點中,然后在子節(jié)點中通過parent進行關聯(lián),就可以大大簡化業(yè)務接口的配置了。
父節(jié)點txProxyTemplate注入了事務管理器,此外還定義了業(yè)務接口事務管理的方法(允許通過通配符的方式進行匹配聲明,如前兩個接口方法),有些接口方法僅對數(shù)據(jù)進行讀操作,而另一些接口方法需要涉及到數(shù)據(jù)的更改。對于前者,可以通過readOnly標識出來,這樣有利于操作性能的提高,需要注意的是由于父類節(jié)點定義的Bean僅是子節(jié)點配置信息的抽象,并不能具體實現(xiàn)化一個Bean對象,所以需要特別標注為abstract="true",如第8行所示。
fileService作為一個目標類被注入到事務代理器中,而fileService實現(xiàn)類所需要的tfileDAO實例,通過引用3.2節(jié)中定義的tfileDAO Bean注入。
Web層實現(xiàn)
1、Web層的構(gòu)件和交互流程
Web層包括主要3個功能:
·上傳文件。
·列出所有已經(jīng)上傳的文件列表,以供點擊下載。
·下載文件。
Web層實現(xiàn)構(gòu)件包括與2個JSP頁面,1個ActionForm及一個Action:
·file-upload.jsp:上傳文件的頁面。
·file-list.jsp:已經(jīng)上傳文件的列表頁面。
·FileActionForm:file-upload.jsp頁面表單對應的ActionForm。
·FileAction:繼承org.apache.struts.actions.DispatchAction的Action,這樣這個Action就可以通過一個URL參數(shù)區(qū)分中響應不同的請求。
Web層的這些構(gòu)件的交互流程如圖 6所示:
|
圖 6 Web層Struts流程圖 |
其中,在執(zhí)行 文件上傳 的請求時,F(xiàn)ileAction在執(zhí)行 文件上傳 后,forward到loadAllFile出口中,loadAllFile加載數(shù)據(jù)庫中所有已經(jīng)上傳的記錄,然后forward到名為fileListPage的出口中,調(diào)用file-list.jsp頁面顯示已經(jīng)上傳的記錄。
2、FileAction功能
Struts 1.0的Action有一個弱項:一個Action只能處理一種請求,Struts 1.1中引入了一個DispatchAction,允許通過URL參數(shù)指定調(diào)用Action中的某個方法,如http://yourwebsite/fileAction.do?method=upload即調(diào)用FileAction中的upload方法。通過這種方式,我們就可以將一些相關的請求集中到一個Action當中編寫,而沒有必要為某個請求操作編寫一個Action類。但是參數(shù)名是要在struts-config.xml中配置的:
|
1. <struts-config>
2. <form-beans> 3. <form-bean name="fileActionForm" type="sshfile.web.FileActionForm" /> 4. </form-beans> 5. <action-mappings> 6. <action name="fileActionForm" parameter="method" path="/fileAction" 7. type="sshfile.web.FileAction"> 8. <forward name="fileListPage" path="/file-list.jsp" /> 9. <forward name="loadAllFile" path="/fileAction.do?method=listAllFile" /> 10. </action> 11. </action-mappings> 12. </struts-config> |
第6行的parameter="method"指定了承載方法名的參數(shù),第9行中,我們還配置了一個調(diào)用FileAction不同方法的Action出口。
FileAction共有3個請求響應的方法,它們分別是:
·upload(…):處理上傳文件的請求。
·listAllFile(…):處理加載數(shù)據(jù)庫表中所有記錄的請求。
·download(…):處理下載文件的請求。
下面我們分別對這3個請求處理方法進行講解。
2.1 上傳文件
上傳文件的請求處理方法非常簡單,簡之言之,就是從 Spring 容器中獲取業(yè)務層處理類FileService,調(diào)用其save(FileActionForm form)方法上傳文件,如下所示:
|
1. public class FileAction
2. extends DispatchAction 3. { 4. //將上傳文件保存到數(shù)據(jù)庫中 5. public ActionForward upload(ActionMapping mapping, ActionForm form, 6. HttpServletRequest request, 7. HttpServletResponse response) 8. { 9. FileActionForm fileForm = (FileActionForm) form; 10. FileService fileService = getFileService(); 11. fileService.save(fileForm); 12. return mapping.findForward("loadAllFile"); 13. } 14. //從 Spring 容器中獲取FileService對象 15. private FileService getFileService() 16. { 17. ApplicationContext appContext = WebApplicationContextUtils. 18. getWebApplicationContext(this.getServlet().getServletContext()); 19. return (FileService) appContext.getBean("fileService"); 20. } 21. … 22. } |
由于FileAction其它兩個請求處理方法也需要從 Spring 容器中獲取FileService實例,所以我們特別提供了一個getFileService()方法(第15~21行)。重構(gòu)的一條原則就是:"發(fā)現(xiàn)代碼中有重復的表達式,將其提取為一個變量;發(fā)現(xiàn)類中有重復的代碼段,將其提取為一個方法;發(fā)現(xiàn)不同類中有相同的方法,將其提取為一個類"。在真實的系統(tǒng)中,往往擁有多個Action和多個Service類,這時一個比較好的設置思路是,提供一個獲取所有Service實現(xiàn)對象的工具類,這樣就可以將 Spring 的Service配置信息屏蔽在一個類中,否則Service的配置名字散落在程序各處,維護性是很差的。
2.2 列出所有已經(jīng)上傳的文件
listAllFile方法調(diào)用Servie層方法加載T_FILE表中所有記錄,并將其保存在Request域中,然后forward到列表頁面中:
|
1. public class FileAction
2. extends DispatchAction 3. { 4. … 5. public ActionForward listAllFile(ActionMapping mapping, ActionForm form, 6. HttpServletRequest request, 7. HttpServletResponse response) 8. throws ModuleException 9. { 10. FileService fileService = getFileService(); 11. List fileList = fileService.getAllFile(); 12. request.setAttribute("fileList",fileList); 13. return mapping.findForward("fileListPage"); 14. } 15. } |
file-list.jsp頁面使用Struts標簽展示出保存在Request域中的記錄:
|
1. <%@page contentType="text/html; charset=GBK"%>
2. <%@taglib uri="/WEB-INF/struts-logic.tld" prefix="logic"%> 3. <%@taglib uri="/WEB-INF/struts-bean.tld" prefix="bean"%> 4. <html> 5. <head> 6. <title>file-download</title> 7. </head> 8. <body bgcolor="#ffffff"> 9. <ol> 10. <logic:iterate id="item" name="fileList" scope="request"> 11. <li> 12. <a href='fileAction.do?method=download&fileId= 13. <bean:write name="item"property="fileId"/>'> 14. <bean:write name="item" property="fileName"/> 15. </a> 16. </li> 17. </logic:iterate> 18. </ol> 19. </body> 20. </html> |
展現(xiàn)頁面的每條記錄掛接著一個鏈接地址,形如:fileAction.do?method=download&fileId=xxx,method參數(shù)指定了這個請求由FileAction的download方法來響應,fileId指定了記錄的主鍵。
由于在FileActionForm中,我們定義了fileId的屬性,所以在download響應方法中,我們將可以從FileActionForm中取得fileId的值。這里涉及到一個處理多個請求Action所對應的ActionForm的設計問題,由于原來的Action只能對應一個請求,那么原來的ActionForm非常簡單,它僅需要將這個請求的參數(shù)項作為其屬性就可以了,但現(xiàn)在一個Action對應多個請求,每個請求所對應的參數(shù)項是不一樣的,此時的ActionForm的屬性就必須是多請求參數(shù)項的并集了。所以,除了 文件上傳 請求所對應的fileContent和remark屬性外還包括文件下載的fileId屬性:
|
圖 7 FileActionForm |
當然這樣會造成屬性的冗余,比如在 文件上傳 的請求中,只會用到fileContent和remark屬性,而在文件下載的請求時,只會使用到fileId屬性。但這種冗余是會帶來好處的--它使得一個Action可以處理多個請求。
2.3 下載文件
在列表頁面中點擊一個文件下載,其請求由FileAction的download方法來響應,download方法調(diào)用業(yè)務層的FileService方法,獲取文件數(shù)據(jù)并寫出到response的響應流中。通過合理設置HTTP響應頭參數(shù),將響應流在客戶端表現(xiàn)為一個下載文件對話框,其代碼如下所示:
代碼 10 業(yè)務接口實現(xiàn)類之download
|
1. public class FileAction
2. extends DispatchAction 3. { 4. … 5. public ActionForward download(ActionMapping mapping, ActionForm form, 6. HttpServletRequest request, 7. HttpServletResponse response) 8. throws ModuleException 9. { 10. FileActionForm fileForm = (FileActionForm) form; 11. FileService fileService = getFileService(); 12. String fileName = fileService.getFileName(fileForm.getFileId()); 13. try 14. { 15. response.setContentType("application/x-msdownload"); 16. response.setHeader("Content-Disposition", 17. "attachment;" + " filename="+ 18. new String(fileName.getBytes(), "ISO-8859-1")); 19. fileService.write(response.getOutputStream(), fileForm.getFileId()); 20. } 21. catch (Exception e) 22. { 23. throw new ModuleException(e.getMessage()); 24. } 25. return null; 26. } 27. } |
第15~18行,設置HTTP響應頭,將響應類型設置為application/x-msdownload MIME類型,則響應流在IE中將彈出一個文件下載的對話框,如圖 4所示。IE所支持的MIME類型多達26種,您可以通過這個網(wǎng)址查看其他的MIME類型:
http://msdn.microsoft.com/workshop/networking/moniker/overview/appendix_a.asp。
如果下載文件的文件名含有中文字符,如果不對其進行硬編碼,如第18行所示,客戶文件下載對話框中出現(xiàn)的文件名將會發(fā)生亂碼。
第19行代碼獲得response的輸出流,作為FileServie write(OutputStream os,String fileId)的入?yún)ⅲ@樣文件的內(nèi)容將寫到response的輸出流中。
3、web.xml文件的配置
Spring 容器在何時啟動呢?我可以在Web容器初始化來執(zhí)行啟動 Spring 容器的操作, Spring 提供了兩種方式啟動的方法:
·通過org.springframework.web.context .ContextLoaderListener容器監(jiān)聽器,在Web容器初始化時觸發(fā)初始化 Spring 容器,在web.xml中通過<listener></listener>對其進行配置。
·通過Servlet org.springframework.web.context.ContextLoaderServlet,將其配置為自動啟動的Servlet,在Web容器初始化時,通過這個Servlet啟動 Spring 容器。
在初始化 Spring 容器之前,必須先初始化log4J的引擎, Spring 也提供了容器監(jiān)聽器和自動啟動Servlet兩種方式對log4J引擎進行初始化:
·org.springframework.web.util .Log4jConfigListener
·org.springframework.web.util.Log4jConfigServlet
下面我們來說明如何配置web.xml啟動 Spring 容器:
代碼 11 web.xml中對應 Spring 的配置內(nèi)容
|
1. <web-app>
2. <context-param> 3. <param-name>contextConfigLocation</param-name> 4. <param-value>/WEB-INF/applicationContext.xml</param-value> 5. </context-param> 6. <context-param> 7. <param-name>log4jConfigLocation</param-name> 8. <param-value>/WEB-INF/log4j.properties</param-value> 9. </context-param> 10. <servlet> 11. <servlet-name>log4jInitServlet</servlet-name> 12. <servlet-class>org.springframework.web.util.Log4jConfigServlet</servlet-class> 13. <load-on-startup>1</load-on-startup> 14. </servlet> 15. <servlet> 16. <servlet-name>springInitServlet</servlet-name> 17. <servlet-class>org.springframework.web.context.ContextLoaderServlet</servlet-class> 18. <load-on-startup>2</load-on-startup> 19. </servlet> 20. … 21. </web-app> |
啟動 Spring 容器時,需要得到兩個信息: Spring 配置文件的地址和Log4J屬性文件,這兩上信息分別通過contextConfigLocationWeb和log4jConfigLocation容器參數(shù)指定,如果有多個 Spring 配置文件,則用逗號隔開,如:
/WEB-INF/applicationContext_1.xml, /WEB-INF/applicationContext_1.xm2
由于在啟動ContextLoaderServlet之前,必須事先初始化Log4J的引擎,所以Log4jConfigServlet必須在ContextLoaderServlet之前啟動,這通過<load-on-startup>來指定它們啟動的先后順序。
亂碼是開發(fā)Web應用程序一個比較老套又常見問題,由于不同Web應用服務器的默認編碼是不一樣的,為了方便Web應用在不同的Web應用服務器上移植,最好的做法是Web程序自身來處理編碼轉(zhuǎn)換的工作。經(jīng)典的作法是在web.xml中配置一個編碼轉(zhuǎn)換過濾器, Spring 就提供了一個編碼過濾器類CharacterEncodingFilter,下面,我們?yōu)閼门渲蒙线@個過濾器:
|
1. <web-app>
2. … 3. <filter> 4. <filter-name>encodingFilter</filter-name> 5. <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> 6. <init-param> 7. <param-name>encoding</param-name> 8. <param-value>GBK</param-value> 9. </init-param> 10. </filter> 11. <filter-mapping> 12. <filter-name>encodingFilter</filter-name> 13. <url-pattern>/*</url-pattern> 14. </filter-mapping> 15. … 16. </web-app> |
Spring 的過濾器類是org.springframework.web.filter.CharacterEncodingFilter,通過encoding參數(shù)指定編碼轉(zhuǎn)換類型為GBK,<filter-mapping>的配置使該過濾器截獲所有的請示。
Struts的框架也需要在web.xml中配置,想必讀者朋友對Struts的配置都很熟悉,故在此不再提及,請參見本文所提供的源碼。
總結(jié)
本文通過一個 文件上傳 下載的Web應用,講解了如何構(gòu)建基于SSH的Web應用,通過Struts和FormFile, Spring 的LobHandler以及 Spring 為 Hibernate Blob處理所提供的用戶類BlobByteArrayType ,實現(xiàn)上傳和下載文件的功能僅需要廖廖數(shù)行的代碼即告完成。讀者只需對程序作稍許的調(diào)整,即可處理Clob字段:
·領域?qū)ο髮狢lob字段的屬性聲明為String類型;
·映射文件對應Clob字段的屬性聲明為org.springframework.orm.hibernate3.support.ClobStringType類型。
本文通過SSH對 文件上傳 下載簡捷完美的實現(xiàn)得以管中窺豹了解SSH強強聯(lián)合構(gòu)建Web應用的強大優(yōu)勢。在行文中,還穿插了一些分層的設計經(jīng)驗,配置技巧和 Spring 所提供的方便類,相信這些知識對您的開發(fā)都有所裨益。?
更多文章、技術(shù)交流、商務合作、聯(lián)系博主
微信掃碼或搜索:z360901061
微信掃一掃加我為好友
QQ號聯(lián)系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元

