STRUTS 2 概述(節選自《STRUTS 2權威指南》作者:李剛)

轉自:http://blog.csdn.net/zzhflyqh/article/details/1894038
STRUTS 2 概述(節選自《STRUTS 2權威指南》作者:李剛)
引 言
1、MVC思想概述
1.1 Web應用的發展
1.2  Model 1和Model 2
1.3 MVC思想
1.4 MVC模式的優勢
1.5 常用MVC框架及其特徵
2、Struts 2的起源和背景
2.1 Struts 1的基本結構及其存在的問題
2.2 WebWork的基本結構
2.3 Struts 2的起源
3、Struts 2體系介紹.
   3.1 Struts 2的框架架構
3.2 Struts 2的標籤庫
3.3 Struts 2的控制器組件
4、Struts 1和Struts 2的對比
5、WebWork和Struts 2對比
6、總 結 
 
引 言
Struts 1是全世界第一個發佈的MVC框架,它由Craig McClanahan在2001年發佈,該框架一經推出,就得到了世界上Java Web開發者的擁護,經過長達6年時間的錘鍊,Struts 1框架更加成熟、穩定,性能也有了很好的保證。因此,到目前爲止,Struts 1依然是世界上使用最廣泛的MVC框架。
目前,基於Web的MVC框架非常多,發展也很快,每隔一段時間就有一個新的MVC框架發佈,例如像JSF、Tapestry和Spring MVC等。除了這些有名的MVC框架外,還有一些邊緣團隊的MVC框架也很有借鑑意義。
對於企業實際使用MVC框架而言,框架的穩定性則應該是最值得考慮的問題。一個剛剛起步的框架,可能本身就存在一些隱藏的問題,會將自身的BUG引入自己的應用。
雖然Struts 2號稱是一個全新的框架,但這僅僅是相對Struts 1而言。Struts 2與Struts 1相比,確實有很多革命性的改進,但它並不是新發布的新框架,而是在另一個赫赫有名的框架:WebWork基礎上發展起來的。從某種程度上來講,Strut2沒有繼承Struts 1的血統,而是繼承了WebWork的血統。或者說,WebWork衍生出了Struts 2,而不是Struts 1衍生了Struts 2。因爲Struts 2是WebWork的升級,而不是一個全新的框架,因此穩定性、性能等各方面都有很好的保證;而且吸收了Struts 1和WebWork兩者的優勢,因此,是一個非常值得期待的框架。
 
1.  MVC思想概述
今天,我們見到的絕大部分應用,都是基於B/S(瀏覽器/服務器)架構的,其中的服務器就是Web服務器。可見,Web應用是目前廣泛使用的應用模式,而Struts 2是一個具有很好的實用價值的Web MVC框架。介紹Struts MVC框架之前,我們首先介紹Web應用的發展歷史和MVC思想。
1.1 Web技術的發展
隨着Internet技術的廣泛使用,Web技術已經廣泛應用於Internet上,但早期的Web應用全部是靜態的HTML頁面,用於將一些文本信息呈現給瀏覽者,但這些信息是固定寫在HTML頁面裏的,該頁面不具備與用戶交互的能力,沒有動態顯示的功能。
很自然地,人們希望Web應用裏應該包含一些能動態執行的頁面,最早的CGI(通用網關接口)技術滿足了該要求,CGI技術使得Web應用可以與客戶端瀏覽器交互,不再需要使用靜態的HTML頁面。CGI技術可以從數據庫讀取信息,將這些信息呈現給用戶;還可以獲取用戶的請求參數,並將這些參數保存到數據庫裏。
CGI技術開啓了動態Web應用的時代,給了這種技術無限的可能性。但CGI技術存在很多缺點,其中最大的缺點就是開發動態Web應用難度非常大,而且在性能等各方面也存在限制。到1997年時,隨着Java語言的廣泛使用,Servlet技術迅速成爲動態Web應用的主要開發技術。
相比傳統的CGI應用而言,Servlet具有大量的優勢:
Ø       Servlet是基於Java語言創建的,而Java語言則內建了多線程支持,這一點大大提高了動態Web應用的性能。
Ø       Servlet應用可以充分利用Java語言的優勢,例如JDBC(Java DataBase Connection)等。同時,Java語言提供了豐富的類庫,這些都簡化了Servlet的開發。
Ø       除此之外,Servlet運行在Web服務器中,由Web服務器去負責管理Servlet的實例化,並對客戶端提供多線程、網絡通信等功能,這都保證Servlet有更好的穩定性和性能。
Servlet在Web應用中被映射成一個URL(統一資源定位),該URL可以被客戶端瀏覽器請求,當用戶向指定URL對應的Servlet發送請求時,該請求被Web服務器接收到,該Web服務器負責處理多線程、網絡通信等功能,而Servlet的內容則決定了服務器對客戶端的響應內容。
 
 
圖1.1  Servlet的響應流程
圖1.1顯示了Servlet的響應流程。
正如圖1.1所顯示的,瀏覽器向Web服務器內指定的Servlet發送請求,Web服務器根據Servlet生成對客戶端的響應。
實際上,這是後來所有的動態Web編程技術所使用的模型,這種模型都需要一個動態的程序,或者一個動態頁面,當客戶端向該動態程序或動態頁面發送請求時,Web服務器根據該動態程序來生成對客戶端的響應。
到了1998年,微軟發佈了ASP 2.0。它是Windows NT 4 Option Pack的一部分,作爲IIS 4.0的外接式附件。它與ASP 1.0的主要區別在於它的外部組件是可以初始化的,這樣,在ASP程序內部的所有組件都有了獨立的內存空間,並可以進行事務處理。標誌着ASP技術開始真正作爲動態Web編程技術。
當ASP技術在世界上廣泛流行時,人們很快感受到這種簡單的技術的魅力:ASP使用VBScript作爲腳本語言,它的語法簡單、開發效率非常高。而且,世界上已經有了非常多的VB程序員,這些VB程序員可以很輕易地過渡成ASP程序員——因此,ASP技術馬上成爲應用最廣泛的動態Web開發技術。
隨後,由Sun帶領的Java陣營,立即發佈了JSP標準,從某種程度上來看,JSP是Java陣營爲了對抗ASP推出的一種動態Web編程技術。
ASP和JSP從名稱上如此相似,但它們的運行機制存在一些差別,這主要是因爲VBScript是一種腳本語言,無需編譯,而JSP使用Java作爲腳本語句——但Java從來就不是解釋型的腳本語言,因此JSP頁面並不能立即執行。因此,JSP必須編譯成Servlet,這就是說:JSP的實質還是Servlet。不過,書寫JSP比書寫Servlet簡單得多。
JSP的運行機理如圖1.2所示。 
 
 圖1.2  JSP的運行機理
對比圖1.1和圖1.2,發現不論是Servlet動態Web技術,還是JSP動態Web技術,它們的實質完全一樣。可以這樣理解:JSP是一種更簡單的Servlet技術,這也是JSP技術出現的意義——作爲一個和ASP對抗的技術,簡單就是JSP的最大優勢。
隨着實際Web應用的使用越來越廣泛,Web應用的規模也越來越大,開發人員發現動態Web應用的維護成本越來越大,即使只需要修改該頁面的一個簡單按鈕文本,或者一段靜態的文本內容,也不得不打開混雜的動態腳本的頁面源文件進行修改——這是一種很大的風險,完全有可能引入新的錯誤。
這個時候,人們意識到:使用單純的ASP,或者JSP頁面充當過多角色是相當失敗的選擇,這對於後期的維護相當不利。慢慢地開發人員開始在Web開發中使用MVC模式。
隨後就是Java陣營發佈了一套完整的企業開發規範:J2EE(現在已經更名爲Java EE),緊跟着微軟也發佈了ASP.NET技術,它們都採用一種優秀的分層思想,力圖解決Web應用維護困難的問題。
動態Web編程技術大致有如圖1.3所示的路線。
 
 圖1.3  動態Web編程技術的發展歷史
1.2 Model 1和Model 2
對於Java陣營的動態Web編程技術而言,則經歷了所謂的Model 1和Model 2時代。
所謂Model 1就是JSP大行其道的時代,在Model 1模式下,整個Web應用幾乎全部由JSP頁面組成,JSP頁面接收處理客戶端請求,對請求處理後直接做出響應。用少量的JavaBean來處理數據庫連接、數據庫訪問等操作。
圖1.4顯示了Model 1的程序流程。
 
 圖1.4  Model 1的程序流程
Model 1模式的實現比較簡單,適用於快速開發小規模項目。但從工程化的角度看,它的侷限性非常明顯:JSP頁面身兼View和Controller兩種角色,將控制邏輯和表現邏輯混雜在一起,從而導致代碼的重用性非常低,增加了應用的擴展性和維護的難度。
早期有大量ASP和JSP技術開發出來的Web應用,這些Web應用都採用了Model 1架構。
Model 2已經是基於MVC架構的設計模式。在Model 2架構中,Servlet作爲前端控制器,負責接收客戶端發送的請求,在Servlet中只包含控制邏輯和簡單的前端處理;然後,調用後端JavaBean來完成實際的邏輯處理;最後,轉發到相應的JSP頁面處理顯示邏輯。其具體的實現方式如圖1.5所示。
圖1.5顯示了Model 2的程序流程。
 
 圖1.5  Model 2的程序流程
正如圖1.5中看到的,Model 2下JSP不再承擔控制器的責任,它僅僅是表現層角色,僅僅用於將結果呈現給用戶,JSP頁面的請求與Servlet(控制器)交互,而Servlet負責與後臺的JavaBean通信。在Model 2模式下,模型(Model)由JavaBean 充當,視圖(View)由JSP頁面充當,而控制器(Controller)則由Servlet充當。
由於引入了MVC模式,使Model 2具有組件化的特點,更適用於大規模應用的開發,但也增加了應用開發的複雜程度。原本需要一個簡單的JSP頁面就能實現的應用,在Model 2中被分解成多個協同工作的部分,需花更多時間才能真正掌握其設計和實現過程。
Model 2已經是MVC設計思想下的架構,下面簡要介紹MVC設計思想的優勢。
*      注意:對於非常小型的Web站點,如果後期的更新、維護工作不是特別大,可以使用Model 1的模式來開發應用,而不是使用Model 2的模式。雖然Model 2提供了更好的可擴展性及可維護性,但增加了前期開發成本。從某種程度上講,Model 2爲了降低系統後期維護的複雜度,卻導致前期開發的更高複雜度。
1.3 MVC思想及其優勢
MVC並不是Java語言所特有的設計思想,也並不是Web應用所特有的思想,它是所有面向對象程序設計語言都應該遵守的規範。
MVC思想將一個應用分成三個基本部分:Model(模型)、View(視圖)和Controller(控制器),這三個部分以最少的耦合協同工作,從而提高應用的可擴展性及可維護性。
起初,MVC模式是針對相同的數據需要不同顯示的應用而設計的,其整體的效果如圖1.6所示。
   
    圖1.6  MVC結構
在經典的MVC模式中,事件由控制器處理,控制器根據事件的類型改變模型或視圖,反之亦然。具體地說,每個模型對應一系列的視圖列表,這種對應關係通常採用註冊來完成,即:把多個視圖註冊到同一個模型,當模型發生改變時,模型向所有註冊過的視圖發送通知,接下來,視圖從對應的模型中獲得信息,然後完成視圖顯示的更新。
從設計模式的角度來看,MVC思想非常類似於一個觀察者模式,但與觀察者模式存在少許差別:觀察者模式下觀察者和被觀察者可以是兩個互相對等的對象,但對於MVC思想而言,被觀察者往往只是單純的數據體,而觀察者則是單純的視圖頁面。
概括起來,MVC有如下特點。
Ø       多個視圖可以對應一個模型。按MVC設計模式,一個模型對應多個視圖,可以減少代碼的複製及代碼的維護量,一旦模型發生改變,也易於維護。
Ø       模型返回的數據與顯示邏輯分離。模型數據可以應用任何的顯示技術,例如,使用JSP頁面、Velocity模板或者直接產生Excel文檔等。
Ø       應用被分隔爲三層,降低了各層之間的耦合,提供了應用的可擴展性。
Ø       控制層的概念也很有效,由於它把不同的模型和不同的視圖組合在一起,完成不同的請求。因此,控制層可以說是包含了用戶請求權限的概念。
Ø       MVC更符合軟件工程化管理的精神。不同的層各司其職,每一層的組件具有相同的特徵,有利於通過工程化和工具化產生管理程序代碼。
相對於早期的MVC思想,Web模式下的MVC思想則又存在一些變化,因爲對於一個應用程序而言,我們可以將視圖註冊給模型,當模型數據發生改變時,即時通知視圖頁面發生改變;而對於Web應用而言,即使將多個JSP頁面註冊給一個模型,當模型發生變化時,模型無法主動發送消息給JSP頁面(因爲Web應用都是基於請求/響應模式的),只有當用戶請求瀏覽該頁面時,控制器才負責調用模型數據來更新JSP頁面。
*      注意:MVC思想與觀察者模式有一定的相似之處,但並不完全相同。經典的MVC思想與Web應用的MVC思想也存在一定的差別,引起差別的主要原因是因爲Web應用是一種請求/響應模式下應用,對於請求/響應應用,如果用戶不對應用發出請求,視圖無法主動更新自己。
1.4 常用的MVC框架
目前常用的MVC框架,除了Struts 2的兩個前身外,還有一些非常流行的MVC框架,這些框架都提供了較好的層次分隔能力。在實現良好的MVC 分隔的基礎上,還提供一些輔助類庫,幫助應用的開發。
目前常用的MVC框架還有如下一些。
1)   JSF
準確地說,JSF是一個標準,而不是一個產品。目前,JSF已經有兩個實現產品可供選擇,包含Sun的參考實現和Apache的MyFaces。大部分的時候,我們所說的JSF都是指Sun的參考實現。目前,JSF是作爲JEE 5.0的一個組成部分,與JEE 5.0一起發佈。
JSF的行爲方法在POJO中實現,JSF的Managed Bean無需繼承任何特別的類。因此,無需在表單和模型對象之間實現多餘的控制器層。JSF中沒有控制器對象,控制器行爲通過模型對象實現。
當然,JSF也允許生成獨立的控制器對象。在Struts 1中,Form Bean包含數據,Action Bean包含業務邏輯,二者無法融合在一起。在JSF中,既可以將二者分開,也可以合併在一個對象中,提供更多靈活的選擇。
JSF的事件框架可以細化到表單中每個字段。JSF依然是基於JSP/Servlet的,仍然是JSP/Servlet架構,因而學習曲線相對簡單。在實際使用過程中,JSF也會存在一些不足
Ø       作爲新興的MVC框架,用戶相對較少,相關資源也不是非常豐富。
Ø       JSF並不是一個完全組件化的框架,它依然是基於JSP/Servlet架構的。
Ø       JSF的成熟度還有待進一步提高。
2)   Tapestry
Tapestry並不是一種單純的MVC框架,它更像MVC框架和模板技術的結合,它不僅包含了前端的MVC框架,還包含了一種視圖層的模板技術,使用Tapestry完全可以與Servlet/JSP API分離,是一種非常優秀的設計。
通過使用Tapestry,開發者完全不需要使用JSP技術,用戶只需要使用Tapestry提供的模板技術即可,Tapestry實現了視圖邏輯和業務邏輯的徹底分離。
Tapestry使用組件庫替代了標籤庫,沒有標籤庫概念,從而避免了標籤庫和組件結合的問題。Tapsetry是完全組件化的框架。Tapestr只有組件或頁面兩個概念,因此,鏈接跳轉目標要麼是組件,要麼是頁面,沒有多餘的path概念。組件名,也就是對象名稱,組件名稱和path名稱合二爲一。
Tapestry具有很高的代碼複用性,在Tapestry中,任何對象都可看作可複用的組件。JSP開發者是真正面向對象,而不是URL解析。對於對頁面要求靈活度相當高的系統,Tapestry是第一選擇。精確地錯誤報告,可以將錯誤定位到源程序中的行,取代了JSP中那種編譯後的提示。
因此,筆者一直對Tapestry情有獨鍾:如果技術允許,使用Tapestry會帶給整個應用更加優雅的架構,更好的開發效率。
但是,在實際開發過程中,採用Tapestry也面臨着一些問題必須考慮:
Ø       Tapestry的學習曲線相對陡峭,國內開發羣體不是非常活躍,文檔不是十分豐富。官方的文檔太過學院派,缺乏實際的示例程序。
Ø       Tapestry的組件邏輯比較複雜,再加上OGNL表達式和屬性指定機制,因而難以添加註釋。
3)   Spring MVC
Spring提供了一個細緻完整的MVC框架。該框架爲模型、視圖、控制器之間提供了一個非常清晰的劃分,各部分耦合極低。Spring的MVC是非常靈活的,它完全基於接口編程,真正實現了視圖無關。視圖不再強制要求使用JSP,可以使用Velocity、XSLT或其他視圖技術。甚至可以使用自定義的視圖機制——只需要簡單地實現View接口,並且把對應視圖技術集成進來。Spring的Controllers由IoC容器管理。因此,單元測試更加方便。
Spring MVC框架以DispatcherServlet爲核心控制器,該控制器負責攔截用戶的所有請求,將請求分發到對應的業務控制器。
Spring MVC還包括處理器映射、視圖解析、信息國際化、主題解析、文件上傳等。所有控制器都必須實現Controller接口,該接口僅定義ModelAndView handleRequest(request,response)方法。通過實現該接口來實現用戶的業務邏輯控制器。
Spring MVC框架有一個極好的優勢,就是它的視圖解析策略:它的控制器返回一個ModelAndView對象,該對象包含視圖名字和Model,Model提供了Bean的名字及其對象的對應關係。視圖名解析的配置非常靈活,抽象的Model完全獨立於表現層技術,不會與任何表現層耦合:JSP、Velocity或者其他的技術——都可以和Spring整合。
但相對於Tapestry框架而言,Spring MVC依然是基於JSP/Servlet API的。
總體上來看,Spring MVC框架致力於一種完美的解決方案,並與Web應用緊緊耦合在一起。這都導致了Spring MVC框架的一些缺點:
Ø       Spring的MVC與Servlet API耦合,難以脫離Servlet容器獨立運行,降低了Spring MVC框架的可擴展性。
Ø       太過細化的角色劃分,太過煩瑣,降低了應用的開發效率。
Ø       過分追求架構的完美,有過度設計的危險。
2.Struts 2的起源和背景
Struts 2以WebWork優秀的設計思想爲核心,吸收了Struts 1的部分優點,建立了一個兼容WebWork和Struts 1的MVC框架,Struts 2的目標是希望可以讓原來使用Struts 1、WebWork的開發人員,都可以平穩過渡到使用Struts 2框架。
2.1 Struts 1簡介及存在的問題
從過去的歲月來看,Struts 1是所有MVC框架中不容辯駁的勝利者,不管是市場佔有率,還是所擁有的開發人羣,Struts 1都擁有其他MVC框架不可比擬的優勢。Struts 1的成功得益於它豐富的文檔、活躍的開發羣體。當然,Struts 1是世界上第一個發佈的MVC框架:Struts 1.0在2001年6月發佈,這一點可能是使它得到如此廣泛擁戴的主要原因。
爲了使讀者可以明白Struts 1的運行機制,下面將簡要介紹Struts 1的基本框架。
Struts 1框架以ActionServlet作爲核心控制器,整個應用由客戶端請求驅動。當客戶端向Web應用發送請求時,請求將被Struts 1的核心控制器ActionServlet攔截,ActionServlet根據請求決定是否需要調用業務邏輯控制器處理用戶請求(實際上,業務邏輯控制器還是控制器,它只是負責調用模型來處理用戶請求),當用戶請求處理完成後,其處理結果通過JSP呈現給用戶。
對於整個Struts 1框架而言,控制器就是它的核心,Struts 1的控制器由兩個部分組成:核心控制器和業務邏輯控制器。其中核心控制器就是ActionServlet,由Struts 1框架提供;業務邏輯控制就是用戶自定義的Action,由應用開發者提供。
對於大部分用戶請求而言,都需要得到服務器的處理。當用戶發送一個需要得到服務器處理的請求時,該請求被ActionServlet攔截到,ActionServlet將該請求轉發給對應的業務邏輯控制器,業務邏輯控制器調用模型來處理用戶請求;如果用戶請求只是希望得到某個URL資源,則由ActionServlet將被請求的資源轉發給用戶。
Struts 1的程序運行流程如圖1.7所示。
 圖1.7  Struts 1的程序運行流程
下面就Struts 1程序流程具體分析MVC中的三個角色。
1)   Model部分
Struts 1的Model部分主要由底層的業務邏輯組件充當,這些業務邏輯組件封裝了底層數據庫訪問、業務邏輯方法實現。實際上,對於一個成熟的企業應用而言,Model部分也不是一個簡單的JavaBean所能完成的,它可能是一個或多個EJB組件,可能是一個WebService服務。總之,Model部分封裝了整個應用的所有業務邏輯,但整個部分並不是由Struts 1提供的,Struts 1也沒有爲實現Model組件提供任何支持。
2)   View部分
Struts 1的View部分採用JSP實現。Struts 1提供了豐富的標籤庫,通過這些標籤庫可以最大限度地減少腳本的使用。這些自定義的標籤庫可以輸出控制器的處理結果。
雖然Struts 1提供了與Ties框架的整合,但Struts 1所支持的表現層技術非常單一:既不支持FreeMarker、Velocity等模板技術,也不支持JasperReports等報表技術。
3)   Controller部分
Struts 1的Controller由兩個部分組成。
Ø       系統核心控制器:由Struts 1框架提供,就是系統中的ActionServlet。
Ø       業務邏輯控制器:由Struts 1框架提供,就是用戶自己實現的Action實例。
Struts 1的核心控制器對應圖1.7中的核心控制器(ActionServlet)。該控制器由Struts 1框架提供,繼承HttpServlet類,因此可以配置成一個標準的Servlet,該控制器負責攔截所有HTTP請求,然後根據用戶請求決定是否需要調用業務邏輯控制器,如果需要調用業務邏輯控制器,則將請求轉發給Action處理,否則直接轉向請求的JSP頁面。
業務邏輯控制器負責處理用戶請求,但業務邏輯控制器本身並不具有處理能力,而是調用Model來完成處理。
Struts 1提供了系統所需要的核心控制器,也爲實現業務邏輯控制器提供了許多支持。因此,控制器部分就是Struts 1框架的核心。有時候,我們直接將MVC層稱爲控制器層。
*      提示:對於任何的MVC框架而言,其實只實現了C(控制器)部分,但它負責用控制器調用業務邏輯組件,並負責控制器與視圖技術(JSP、FreeMarker和Velocity等)的整合。
對於Struts 1框架而言,因爲它與JSP/Servlet耦合非常緊密,因而導致了許多不可避免的缺陷,隨着Web應用的逐漸擴大,這些缺陷逐漸變成制約Struts 1發展的重要因素——這也是Struts 2出現的原因。下面具體分析Struts 1中存在的種種缺陷
1)   支持的表現層技術單一
Struts 1只支持JSP作爲表現層技術,不提供與其他表現層技術,例如Velocity、FreeMarker等技術的整合。這一點嚴重製約了Struts 1框架的使用,對於目前的很多Java EE應用而言,並不一定使用JSP作爲表現層技術。
雖然Struts 1處理完用戶請求後,並沒有直接轉到特定的視圖資源,而是返回一個ActionForward對象(可以理解ActionForward是一個邏輯視圖名),在struts-config.xml文件中定義了邏輯視圖名和視圖資源之間的對應關係,當ActionServlet得到處理器返回的ActionForword對象後,可以根據邏輯視圖名和視圖資源之間的對應關係,將視圖資源呈現給用戶。
從上面的設計來看,不得不佩服Struts 1的設計者高度解耦的設計:控制器並沒有直接執行轉發請求,而僅僅返回一個邏輯視圖名——實際的轉發放在配置文件中進行管理。但因爲Struts 1框架出現的年代太早了,那時候還沒有FreeMarker、Velocity等技術,因而沒有考慮與這些FreeMarker、Velocity等視圖技術的整合。
*      提示:Struts 1已經通過配置文件管理邏輯視圖名和實際視圖之間的對應關係,只是沒有做到讓邏輯視圖名可以支持更多的視圖技術。
雖然Struts 1有非常優秀的設計,但由於歷史原因,它沒有提供與更多視圖技術的整合,這嚴重限制了Struts 1的使用。
1)   與Servlet API嚴重耦合,難於測試
因爲Struts 1框架是在Model 2的基礎上發展起來的,因此它完全是基於Servlet API的,所以在Struts 1的業務邏輯控制器內,充滿了大量的Servlet API。
看下面的Action代碼片段:
//業務邏輯控制器必須繼承Struts 1提供的Action類
public class LoginAction extends Action
{
//處理用戶請求的execute方法
public ActionForward execute(
ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws     AuctionException  {
//獲取封裝用戶請求參數的ActionForm對象
//將其強制類型轉換爲登錄用的ActionForm
LoginForm loginForm = (LoginForm)form;
//當用戶名爲scott,密碼爲tiger時返回成功
if (”scott”.equals(loginForm.getUsername()
&& “tiger”.equals(loginForm.getPassword())
{
//處理成功,返回一個ActionForward對象
return mapping.findForward(”success”);                                    }
else  
{
//處理失敗,返回一個ActionForward對象
return mapping.findForward(”success”);                                    }
    }
}
當我們需要測試上面Action類的execute方法時,該方法有4個參數:ActionMapping、ActionForm、HttpServletRequest和HttpServletResponse,初始化這4個參數比較困難,尤其是HttpServletRequest和HttpServletResponse兩個參數,通常由Web容器負責實例化。
因爲HttpServletRequest和HttpServletResponse兩個參數是Servlet API,嚴重依賴於Web服務器。因此,一旦脫離了Web服務器,Action的測試非常困難。
2)   代碼嚴重依賴於Struts 1 API,屬於侵入式設計
正如從上面代碼片段中所看到的,Struts 1的Action類必須繼承Struts 1的Action基類,實現處理方法時,又包含了大量Struts 1 API:如ActionMapping、ActionForm和ActionForward類。這種侵入式設計的最大弱點在於,一旦系統需要重構時,這些Action類將完全沒有利用價值,成爲一堆廢品。
可見,Struts 1的Action類這種侵入式設計導致了較低的代碼複用。
2.2 WebWork簡介
WebWork雖然沒有Struts 1那樣赫赫有名,但也是出身名門,WebWork來自另外一個優秀的開源組織:opensymphony,這個優秀的開源組織同樣開發了大量優秀的開源項目,如Qutarz、OSWorkFlow等。實際上,WebWork的創始人則是另一個Java領域的名人:Rickard Oberg(他就是JBoss和XDoclet的作者)。
相對於Struts 1存在的那些先天性不足而言,WebWork則更加優秀,它採用了一種更加鬆耦合的設計,讓系統的Action不再與Servlet API耦合。使單元測試更加方便,允許系統從B/S結構向C/S結構轉換。
相對於Struts 1僅支持JSP表現層技術的缺陷而言,WebWork支持更多的表現層技術,如Velocity、FreeMarker和XSLT等。
WebWork可以脫離Web應用使用,這一點似乎並沒有太多優勢,因爲,一個應用通常開始已經確定在怎樣的環境下使用。WebWork有自己的控制反轉(Inversion of Control)容器,通過控制反轉,可以讓測試變得更簡單,測試中設置實現服務接口的Mock對象完成測試,而不需要設置服務註冊。
WebWork 2使用OGNL這個強大的表達式語言,可以訪問值棧。OGNL對集合和索引屬性的支持非常強大。
WebWork建立在XWork之上,使用ServletDispatcher作爲該框架的核心控制器,處理HTTP的響應和請求。
從處理流程上來看,WebWork與Struts 1非常類似,它們的核心都由控制器組成,其中控制器都由兩個部分組成:
Ø       核心控制器ServletDispatcher,該控制器框架提供。
Ø       業務邏輯控制器Action,該控制器由程序員提供。
相對Struts 1的Action與Servlet API緊緊耦合的弱點來說,WebWork的Action則完全與Servlet API分離,因而該Action更容易測試。
WebWork的Action可以與Servlet API分離,得益於它靈巧的設計,它使用一個攔截器鏈,負責將用戶請求數據轉發到Action,並負責將Action的處理結果轉換成對用戶的響應。
當用戶向Web應用發送請求時,該請求經過ActionContextCleanUp、SiteMesh等過濾器過濾,由WebWork的核心控制器攔截,如果用戶請求需要WebWork的業務邏輯控制器處理,該控制器則調用Action映射器,該映射器將用戶請求轉發到對應的業務邏輯控制器。值得注意的是,此時的業務邏輯控制器並不是開發者實現的控制器,而是WebWork創建的控制器代理。
創建控制器代理時,WebWork需要得到開發者定義的xwork.xml配置文件,控制器代理以用戶實現的控制器作爲目標,以攔截器鏈中的攔截器作爲處理(Advice)。
*      提示:WebWork中創建控制器代理的方式,就是一種AOP(面向切面編程)編程方式,只是這種AOP中的攔截器由系統提供,因此無需用戶參與。
開發者自己實現的業務邏輯控制器只是WebWork業務控制器的目標——這就是爲什麼開發者自己實現的Action可以與Servlet API分離的原因。當開發者自己的Action處理完HTTP請求後,該結果只是一個普通字符串,該字符串將對應到指定的視圖資源。
指定的試圖資源經過攔截器鏈的處理後,生成對客戶端的響應輸出。
 上面整個過程的數據流圖如圖1.8所示。
 
              圖1.8  WebWork的數據流圖
與前面的Struts 1框架對比,不難發現WebWork在很多地方確實更優秀。
相對Struts 1的種種缺點而言,WebWork存在如下優點:
Ø       Action無需與Servlet API耦合,更容易測試
相對於Struts 1框架中的Action出現了大量Servlet API而言,WebWork的Action更像一個普通Java對象,該控制器代碼中沒有耦合任何Servlet API。看下面的WebWork的Action示例:
public class LoginAction implements Action
{
//該字符串常量將作爲Action的返回值
private final static String LOGINFAIL=”loginfail”;
 //該Action封裝的兩個請求參數
private String password;
private String username;
//password請求參數對應的getter方法
public String getPassword()
{
return password;
}
//password請求參數對應的setter方法
public void setPassword(String password)
{
this.password = password;
                        }
//username請求參數對應的getter方法
public String getUsername()
{
return username;
               }
//username請求參數對應的setter方法
public void setUsername(String username)
{
this.username = username;
                        }
 //處理用戶請求的execute方法
public String execute() throws Exception
{
if (”yeeku”.equalsIgnoreCase(getUsername())
&& “password”.equals(getPassword()))
{
ActionContext ctx = ActionContext.getContext();
//將當前登錄的用戶名保存到Session
Map session = ctx.getSession();
session.put(”username”,getUsername());
return SUCCESS;
              }
else
{
return LOGINFAIL;
                }
        }
}
在上面的Action代碼中,我們看不到任何的Servlet API,當系統需要處理兩個請求參數:username和password時,Action並未通過HttpServletRequest對象來獲得請求參數,而是直接調用訪問該Action的username和password成員屬性——這兩個屬性由Action攔截器負責初始化,以用戶請求參數爲其賦值。
即使Action中需要訪問HTTP Session對象,依然沒有在代碼中直接出現HttpSession API,而是以一個Map對象代表了HTTP Session對象。
當我們將WebWork的Action和Struts 1的Action進行對比時,不難發現Struts 1的Action確實太臃腫了,確實不如WebWork的Action那麼優雅。
如果需要測試上面的Action代碼,測試用例的書寫將非常容易,因爲execute方法中沒有包含任何Servlet API,甚至沒有WebWork的API。
Ø       Action無需與WebWork耦合,代碼重用率高
在上面的Action代碼中,不難發現WebWork中的Action其實就是一個POJO,該Action僅僅實現了WebWork的Action接口,包含了一個execute方法。
Struts 1中的Action類需要繼承Struts 1的Action類。我們知道,實現一個接口和繼承一個類是完全不同的概念:實現一個接口對類的污染要小得多,該類也可以實現其他任意接口,還可以繼承一個父類;但一旦已經繼承一個父類,則意味着該類不能再繼承其他父類。
除此之外,Struts 1中Action也包含了一個execute方法,但該方法需要4個參數,類型分別是ActionMapping、ActionForm、HttpServletRequest和HttpServletResponse,一個包含了這4個參數的方法,除了在Struts 1框架下有用外,筆者難以想象出該代碼還有任何複用價值。但WebWork的execute方法則完全不同,該方法中沒有出現任何Servlet API,也沒有出現任何WebWork API,這個方法在任何環境下都有重用的價值。
得益於WebWork靈巧的設計,WebWork中的Action無需與任何Servlet API、WebWork API耦合,從而具有更好的代碼重用率。
Ø       支持更多的表現層技術,有更好的適應性。
正如從圖1.8所見到的,WebWork對多種表現層技術:JSP、Velocity和FreeMarker等都有很好的支持,從而給開發更多的選擇,提供了更好的適應性。
2.3  Struts 2起源
經過五年多的發展,Struts 1已經成爲一個高度成熟的框架,不管是穩定性還是可靠性,都得到了廣泛的證明。但由於它太“老”了,一些設計上的缺陷成爲它的硬傷。面對大量新的MVC框架蓬勃興起,Struts 1也開始了血液的更新。
目前,Struts已經分化成兩個框架:第一個框架就是傳統Struts 1和WebWork結合後的Struts 2框架。Struts 2雖然是在Struts 1的基礎上發展起來的,但實質上是以WebWork爲核心,Struts 2爲傳統Struts 1注入了WebWork的設計理念,統一了Struts 1和WebWork兩個框架,允許Struts 1和WebWork開發者同時使用Struts 2框架。
Struts分化出來的另外一個框架是Shale,這個框架遠遠超出了Struts 1原有的設計思想,它與原有的Struts 1的關聯很少,它使用全新的設計思想。Shale更像一個新的框架,而不是Struts的升級。
Shale 在很多方面與Struts存在不同之處,其中有兩點最爲突出:
Ø       Struts與JSF集成,而Shale則是建立在JSF之上。
Ø       Struts實質上是一個巨大的、複雜的請求處理器;而Shale則是一組能以任何方式進行組合的服務,簡單地說,Shale是一種SOA(面向服務架構)架構。
在後面的介紹中,我們會發現,Struts 2非常類似於WebWork框架,而不像Struts 1框架,因爲Struts 2是以WebWork爲核心,而不是以Struts 1爲核心的。正因爲此,許多WebWork開發者會發現,從WebWork過渡到Struts 2是一件非常簡單的事情。
當然,對於傳統的Struts 1開發者,Struts 2也提供了很好的向後兼容性,Struts 2可與Struts 1有機整合,從而保證Struts 1開發者能平穩過渡到Struts 2。
3. Struts 2體系介紹
Struts 2的體系與Struts 1體系的差別非常大,因爲Struts 2使用了WebWork的設計核心,而不是使用Struts 1的設計核心。Struts 2大量使用攔截器來處理用戶請求,從而允許用戶的業務邏輯控制器與Servlet API分離。
3.1 Struts 2框架架構
從數據流圖上來看,Struts 2與WebWork相差不大,Struts 2同樣使用攔截器作爲處理(Advice),以用戶的業務邏輯控制器爲目標,創建一個控制器代理。
控制器代理負責處理用戶請求,處理用戶請求時回調業務控制器的execute方法,該方法的返回值將決定了Struts 2將怎樣的視圖資源呈現給用戶。
圖1.9顯示了Struts 2的體系概圖。
   
    圖1.9  Struts 2的體系概圖
Struts 2框架的大致處理流程如下:
Ø       瀏覽器發送請求,例如請求/mypage.action、/reports/myreport.pdf等。
Ø       核心控制器FilterDispatcher根據請求決定調用合適的Action。
Ø       WebWork的攔截器鏈自動對請求應用通用功能,例如workflow、validation或文件上傳等功能。
Ø       回調Action的execute方法,該execute方法先獲取用戶請求參數,然後執行某種數據庫操作,既可以是將數據保存到數據庫,也可以從數據庫中檢索信息。實際上,因爲Action只是一個控制器,它會調用業務邏輯組件來處理用戶的請求。
Ø       Action的execute方法處理結果信息將被輸出到瀏覽器中,可以是HTML頁面、圖像,也可以是PDF文檔或者其他文檔。此時支持的視圖技術非常多,既支持JSP,也支持Velocity、FreeMarker等模板技術。
3.2 Struts 2的配置文件
當Struts 2創建系統的Action代理時,需要使用Struts 2的配置文件。
Struts 2的配置文件有兩份:
Ø       配置Action的struts.xml文件。
Ø       配置Struts 2全局屬性的struts.properties文件。
struts.xml文件內定義了Struts 2的系列Action,定義Action時,指定該Action的實現類,並定義該Action處理結果與視圖資源之間的映射關係。
下面是struts.xml配置文件的示例:
<struts>
<!– Struts 2的Action都必須配置在package裏 –>
<package name=”default” extends=”struts-default”>
<!– 定義一個Logon的Action,實現類爲lee.Logon –>
<action name=”Logon” class=”lee.Logon”>
<!– 配置Action返回input時轉入/pages/Logon.jsp頁面 –>
<result name=”input”>/pages/Logon.jsp</result>
<!– 配置Action返回cancel時重定向到Welcome的Action–>
<result name=”cancel” type=”redirect-action”>Welcome</result>
<!– 配置Action返回success時重定向到MainMenu的Action –>
<result type=”redirect-action”>MainMenu</result>
<!– 配置Action返回expired時進入ChangePassword的Action鏈 –>
<result name=”expired” type=”chain”>ChangePassword</result>
</action>
<!– 定義Logoff的Action,實現類爲lee.Logoff –>
<action name=”Logoff” class=” lee.Logoff”>
<!– 配置Action返回success時重定向到MainMenu的Action –>
<result type=”redirect-action”>Welcome</result>
</action>
</package>
</struts>
在上面的struts.xml文件中,定義了兩個Action。定義Action時,不僅定義了Action的實現類,而且的定義Action的處理結果時,指定了多個result,result元素指定execute方法返回值和視圖資源之間的映射關係。對於如下配置片段:
<result name=”cancel” type=”redirect-action”>Welcome</result>
表示當execute方法返回cancel的字符串時,跳轉到Welcome的Action。定義result元素時,可以指定兩個屬性:type和name。其中name指定了execute方法返回的字符串,而type指定轉向的資源類型,此處轉向的資源可以是JSP,也可以是FreeMarker等,甚至是另一個Action——這也是Struts 2可以支持多種視圖技術的原因。
除此之外,Struts 2還有一個配置Struts 2全局屬性的Properties文件:struts.properties。該文件的示例如下:
#指定Struts 2處於開發狀態
struts.devMode = false
//指定當Struts 2配置文件改變後,Web框架是否重新加載Struts 2配置文件struts.configuration.xml.reload=true
正如上面見到的,struts.properties文件的形式是系列的key、value對,它指定了Struts 2應用的全局屬性。
3.3 Strut 2的標籤庫
Struts 2的標籤庫也是Struts 2的重要組成部分,Struts 2的標籤庫提供了非常豐富的功能,這些標籤庫不僅提供了表現層數據處理,而且提供了基本的流程控制功能,還提供了國際化、Ajax支持等功能。
通過使用Struts 2的標籤,開發者可以最大限度地減少頁面代碼的書寫。
看下面的JSP頁面的表單定義片段:
<!–  定義一個Action –>
<form method=”post” action=”basicvalid.action”>
<!– 下面定義三個表單域 –>
    名字:<input type=”text” name=”name”/><br>
    年紀:<input type=”text” name=”age”/><br>
    喜歡的顏色:<input type=”text” name=”favorite”/><br>
                                 <!– 定義一個輸出按鈕 –>
    <input type=”submit” value=”提交“/>
</form>
上面頁面使用了傳統的HTML標籤定義表單元素,還不具備輸出校驗信息的功能,但如果換成如下使用Struts 2標籤的定義方式:
<!– 使用Struts 2標籤定義一個表單 –>
<s:form method=”post” action=”basicvalid.action”>
<!– 下面使用Struts 2標籤定義三個表單域 –>
<s:textfield label=”名字” name=”name”/>
<s:textfield label=”年紀” name=”age”/>
<s:textfield label=”喜歡的顏色” name=”answer”/>
<!– 定義一個提交按鈕 –>
<s:submit/>
</s:form>
則頁面代碼更加簡潔,而且有更簡單的錯誤輸出。圖1.10是上面使用Struts 2標籤執行數據校驗後的輸出。
圖1.10  使用Struts 2標籤的效果
*      提示:Struts 2的標籤庫的功能非常複雜,該標籤庫幾乎可以完全替代JSTL的標籤庫。而且Struts 2的標籤支持表達式語言,這種表達式語言支持一個強大和靈活的表達式語言:OGNL(Object Graph Notation Language),因此功能非常強大。
3.4 Struts 2的控制器組件
Struts 2的控制器組件是Struts 2框架的核心,事實上,所有MVC框架都是以控制器組件爲核心的。正如前面提到的,Struts 2的控制器由兩個部分組成:FilterDispatcher和業務控制器Action。
實際上,Struts 2應用中起作用的業務控制器不是用戶定義的Action,而是系統生成的Action代理,但該Action代理以用戶定義的Action爲目標。
下面是Struts 2的Action代碼示例:
public class LoginAction
{
//封裝用戶請求參數的username屬性
private String username;
//封裝用戶請求參數的password屬性
private String password;
 //username屬性的getter方法
public String getUsername()
{
return username;
               }
//username屬性的setter方法
public void setUsername(String username)
{
this.username = username;
                        }
//password屬性的getter方法
public String getPassword()
{
return password;
               }
//password屬性的setter方法
public void setPassword(String password)
{
this.password = password;
                        }
//處理用戶請求的execute方法
public String execute() throws Exception
{
//如果用戶名爲scott,密碼爲tiger,則登錄成功
if (getUsername().equals(”scott” )
&& getPassword().equals(”tiger” ) )
{
return “success”;
                }
else
{
return “error”;
              }
       }
}
通過查看上面的Action代碼,發現該Action比WebWork中的Action更徹底,該Action無需實現任何父接口,無需繼承任何Struts 2基類,該Action類完全是一個POJO(普通、傳統的Java對象),因此具有很好的複用性。
歸納起來,該Action類有如下優勢:
Ø       Action類完全是一個POJO,因此具有很好的代碼複用性。
Ø       Action類無需與Servlet API耦合,因此進行單元測試非常簡單。
Ø       Action類的execute方法僅返回一個字符串作爲處理結果,該處理結果可映射到任何的視圖,甚至是另一個Action。
4. Struts 2與Struts 1的對比
經過上面簡要介紹,不難發現,Struts 2確實在Struts 1上做出了巨大的改進,的確是一個非常具有實用價值的MVC框架。          
Struts 1和Struts 2在各方面的簡要對比。
Ø       在Action實現類方面的對比:Struts 1要求Action類繼承一個抽象基類;Struts 1的一個具體問題是使用抽象類編程而不是接口。Struts 2 Action類可以實現一個Action接口,也可以實現其他接口,使可選和定製的服務成爲可能。Struts 2提供一個ActionSupport基類去實現常用的接口。即使Action接口不是必須實現的,只有一個包含execute方法的POJO類都可以用作Struts 2的Action。
Ø       線程模式方面的對比:Struts 1 Action是單例模式並且必須是線程安全的,因爲僅有Action的一個實例來處理所有的請求。單例策略限制了Struts 1 Action能做的事,並且要在開發時特別小心。Action資源必須是線程安全的或同步的;Struts 2 Action對象爲每一個請求產生一個實例,因此沒有線程安全問題。
Ø       Servlet依賴方面的對比:Struts 1 Action依賴於Servlet API,因爲Struts 1 Action的execute方法中有HttpServletRequest和HttpServletResponse方法。Struts 2 Action不再依賴於Servlet API,從而允許Action脫離Web容器運行,從而降低了測試Action的難度。 當然,如果Action需要直接訪問HttpServletRequest和HttpServletResponse參數,Struts 2 Action仍然可以訪問它們。但是,大部分時候,Action都無需直接訪問HttpServetRequest和HttpServletResponse,從而給開發者更多靈活的選擇。
Ø       可測性方面的對比:測試Struts 1 Action的一個主要問題是execute方法依賴於Servlet API,這使得Action的測試要依賴於Web容器。爲了脫離Web容器測試Struts 1的Action,必須藉助於第三方擴展:Struts TestCase,該擴展下包含了系列的Mock對象(模擬了HttpServetRequest和HttpServletResponse對象),從而可以脫離Web容器測試Struts 1的Action類。Struts 2 Action可以通過初始化、設置屬性、調用方法來測試。
Ø       封裝請求參數的對比:Struts 1使用ActionForm對象封裝用戶的請求參數,所有的ActionForm必須繼承一個基類:ActionForm。普通的JavaBean不能用作ActionForm,因此,開發者必須創建大量的ActionForm類封裝用戶請求參數。雖然Struts 1提供了動態ActionForm來簡化ActionForm的開發,但依然需要在配置文件中定義ActionForm;Struts 2直接使用Action屬性來封裝用戶請求屬性,避免了開發者需要大量開發ActionForm類的煩瑣,實際上,這些屬性還可以是包含子屬性的Rich對象類型。如果開發者依然懷念Struts 1 ActionForm的模式,Struts 2提供了ModelDriven模式,可以讓開發者使用單獨的Model對象來封裝用戶請求參數,但該Model對象無需繼承任何Struts 2基類,是一個POJO,從而降低了代碼污染。
Ø       表達式語言方面的對比:Struts 1整合了JSTL,因此可以使用JSTL表達式語言。這種表達式語言有基本對象圖遍歷,但在對集合和索引屬性的支持上則功能不強;Struts 2可以使用JSTL,但它整合了一種更強大和靈活的表達式語言:OGNL(Object Graph Notation Language),因此,Struts 2下的表達式語言功能更加強大。
Ø       綁定值到視圖的對比:Struts 1使用標準JSP機制把對象綁定到視圖頁面;Struts 2使用“ValueStack”技術,使標籤庫能夠訪問值,而不需要把對象和視圖頁面綁定在一起。
Ø       類型轉換的對比:Struts 1 ActionForm 屬性通常都是String類型。Struts 1使用Commons-Beanutils進行類型轉換,每個類一個轉換器,轉換器是不可配置的;Struts 2使用OGNL進行類型轉換,支持基本數據類型和常用對象之間的轉換。
Ø       數據校驗的對比:Struts 1支持在ActionForm重寫validate方法中手動校驗,或者通過整合Commons alidator框架來完成數據校驗。Struts 2支持通過重寫validate方法進行校驗,也支持整合XWork校驗框架進行校驗。
Ø       Action執行控制的對比:Struts 1支持每一個模塊對應一個請求處理(即生命週期的概念),但是模塊中的所有Action必須共享相同的生命週期。Struts 2支持通過攔截器堆棧(Interceptor Stacks)爲每一個Action創建不同的生命週期。開發者可以根據需要創建相應堆棧,從而和不同的Action一起使用。
5. WebWork和Struts 2對比
從某種程度上來看,Struts 2是WebWork的升級,而不是Struts 1的升級,甚至在Apache的Struts 2的官方文檔都提到:WebWork到Struts 2是一次平滑的過渡。實際上,Struts 2.0其實是WebWork 2.3而已,從WebWork 2.2遷移到Struts 2.0不會比從WebWork 2.1到2.2更麻煩。
在很多方面,Struts 2僅僅是改變了WebWork下的名稱,因此,如果開發者具有WebWork的開發經驗,將可以更加迅速地進入Struts 2的開發領域。
下面是Struts 2與WebWork命名上存在改變(見表1.1):
Struts 2成員     WebWork成員
com.opensymphony.xwork2.*     com.opensymphony.xwork.*
org.apache.Struts2.*     com.opensymphony.webwork.*
struts.xml     xwork.xml
struts.properties     webwork.properties
Dispatcher       DispatcherUtil
org.apache.Struts2.config.Settings     com.opensymphony.webwork.config.Configuration
表1.1  Struts 2和WebWork成員名稱的對應
除此之外,Struts 2也刪除了WebWork中少量特性:
Ø       AroundInterceptor:Struts 2不再支持WebWork中的AroundInterceptor。如果應用程序中需要使用AroundInterceptor,則應該自己手動導入WebWork中的AroundInterceptor類。
Ø       副文本編輯器標籤:Struts 2不再支持WebWork的富文本編輯器,如果應用中需要使用富文本編輯器,則應該使用Dojo的富文本編輯器。
Ø       IoC容器支持:Struts 2不再支持內建的IoC容器,而改爲全面支持Spring的IoC容器,以Spring的IoC容器作爲默認的Object工廠。
6. 總 結
本節課大致介紹了Web應用的開發歷史,從歷史的角度介紹了Model 1和Model 2的簡要模型和特徵,進而介紹了MVC模式的主要策略和主要優勢。接着介紹了常用的MVC框架,包括JSF、Tapestry和Spring MVC,以及這些框架的基本知識和相關特徵。本節重點介紹了Struts 2的兩個前身:Struts 1和WebWork,以及這兩個框的架構和主要特徵,從而引申出對Struts 2起源的介紹。最後大致介紹了Struts 2框架的體系,包括Struts 2框架的架構、標籤庫、控制器組件等,並就Struts 1和Struts 2的相關方面進行了比較。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章