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

微信掃一掃加我為好友
QQ號(hào)聯(lián)系: 360901061
您的支持是博主寫(xiě)作最大的動(dòng)力,如果您喜歡我的文章,感覺(jué)我的文章對(duì)您有幫助,請(qǐng)用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點(diǎn)擊下面給點(diǎn)支持吧,站長(zhǎng)非常感激您!手機(jī)微信長(zhǎng)按不能支付解決辦法:請(qǐng)將微信支付二維碼保存到相冊(cè),切換到微信,然后點(diǎn)擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對(duì)您有幫助就好】元
