(作者:俞良松 2002年09月26日 本文選自:開放系統世界)
Struts是源代碼開放的企業級Web應用開發框架,它的設計目的是從整體上減輕構造企業Web應用的負擔。本文通過一個Struts應用的實例,幫助你迅速掌握Struts。
Struts是在Jakarta項目下開發的源代碼開放軟件,由一系列的框架類、輔助類和定制的JSP標記庫構成,定位在基于Model 2設計模式的J2EE應用開發。Model 2體系是MVC(Model-View-Controller)體系的一種應用。在MVC體系中,數據模型(Model)、表現邏輯(View)和控制邏輯(Controller)是分離的組件,但它們可以互相通信。Struts力圖從整體上減輕構造企業級Web應用的負擔,并提供國際化和數據庫連接池支持。
Struts體系可以看成兩個相對獨立的部分:第一個部分是Struts API,用于編寫支持Struts的應用組件;第二部分是Struts的JSP標記庫,由html、bean、logic和template四個標記組成。Struts的兩個部分有著各自不同的用戶。對于規模較小的項目,同一個用戶可能同時使用這兩個部分;但對于規模較大的項目,通常開發者使用API組件,而負責HTML頁面布局的人使用標記庫。
Struts的設計目標是為Model 2 Web應用開發提供一個強大的框架。同時,Struts還包含了一些實用組件,例如Digest,但這些組件并不從屬于上面提到的兩個部分。
Struts應用的體系結構
對于從傳統編程環境轉入Web開發的人來說,Web編程中令人很不習慣的一個特點是缺乏“程序”。傳統的應用總是有主入口點、流程控制和出口點。但在Web網站上,用戶可能從任何地方進入,按照一種完全隨機的次序訪問各個頁面,甚至可能跳過多個頁面,也可能在一、兩個小時內毫無動靜。這是HTTP訪問的基本特征,無論是Struts還是其他Web編程框架,都無法改變這一點。然而,Struts能夠隱藏Web訪問固有的“混亂”,幫助開發者建立起清晰和明確的秩序和規則。
在Struts應用中,有一個稱為ActionServlet的主調度程序(或稱為分配器),如圖1所示。不過,并非所有的請求都必須通過ActionServlet。用戶的請求目標可以是非Struts的頁面,也可以是那些使用了Struts標記庫但不使用Struts請求分配服務的頁面。這正是Struts體系的優點之一:按需使用。許多編程框架要求你要么不用,要么全部使用,而且一旦你決定使用,以后要悔改從前的錯誤就會付出高昂的代價。Struts按需使用的優點與這類系統形成了強烈對比。

圖1 Struts框架中的請求處理
Struts應用由下面這些基本模塊構成:
1.配置信息;
2.Servlet,主要是Struts的ActionServlet;
3.動作類(Action),執行邏輯和控制(請求分配)功能,它們由ActionServlet調用;
4.JSP頁面(屬于View),常常通過動作類分派;
5.JSP標記庫,根據需要使用;
6.各種形式的JavaBean,包括用戶定義的JavaBean。
典型的Struts應用要用到三種配置文件:web.xml、struts-config.xml和可選的應用資源文件。
web.xml是Web應用的標準配置文件,是所有J2EE Web應用必需的組成部分。應用服務器通過該配置文件把URL映射到Servlet和JSP,通過該配置文件為Servlet和JSP指定啟動參數。為Struts應用提供的基本web.xml文件很簡單,真正必需的只有一個主ActionServlet定義,以及一個確保Struts請求傳遞到ActionServlet的映射。按照慣例,以“.do”結尾的URL都是Struts請求,例如/login.do。應用服務器利用web.xml文件中的映射,把該請求傳遞給ActionServlet。接著,ActionServlet決定如何分配該請求。ActionServlet的決定依據是struts-config.xml中定義的規則,和/或是通過ActionServlet派生類額外定義的分配邏輯。
struts-config.xml稱為Struts配置文件。Struts應用是一個依靠struts-config.xml文件把組件連接起來的網絡。struts-config.xml文件為Web應用的組件定義了邏輯名稱,也定義了它們在Struts框架下的屬性和關系,就像web.xml文件在Web應用框架之內定義組件一樣。struts-config.xml文件包含了與Struts框架有關的應用信息,這些信息分四個類:
1.數據源信息,它是可選的。在這里可以指定一個或者多個JDBC數據源,使得數據庫定義信息集中化。對于數據庫訪問,Struts還有一個額外的優點,即支持基本的數據庫連接池功能。
2. Form Bean是JavaBean的一種特殊類型,它簡化了Web表單的處理。
3. Global Forwards是全局性的轉發定義信息。Struts動作按照一種“請求—轉發”機制運行。為了最大限度地分 離動作模塊與轉發目標,這里使用了一種映射機制,允許通過同義詞引用轉發目標。一些目標頁面可能被多個動作類引用,例如登錄頁面,因此可以在全局轉發定義部分把邏輯目標頁面映射到物理目標頁面,避免把這部分信息加入到動作定義部分。
4. Actions定義了Struts應用體系的請求分配信息,它們是核心分配器的補充定義,負責處理各種具體的請求類型。
一個簡單的應用
基于Struts的Web應用和普通Web應用有著許多同樣的要求,但Struts應用也有自己特殊的需求。一個可部署的Web應用應該可組織和構成一個WAR文件。WAR文件是帶有圖2所示目錄結構的JAR包。對于Struts Web應用來說,Web-INF目錄下還要加上一些額外的文件,例如struts-confg.xml文件和標記庫描述器(TLD)文件。注意:應用的資源應該放入應用的類路徑下,也就是Web-INF/lib目錄或Web-INF/class目錄下的JAR包內。對于大多數簡單的Struts頁面,我們只用到Struts標記庫,而按照MVC的術語就不需要涉及Model和Controller部分,只涉及View。請看圖3所示的主頁例子。雖然這個頁面沒有表單,但Struts仍能夠在設計這類頁面時提供幫助。

圖2 Struts應用的目錄結構

圖3 一個簡單的View
要管理會話,最簡單的途徑是使用Cookie。會話標識符被傳遞到客戶端之后,客戶端把它保存到Cookie,以后的每次請求就把Cookie發送到服務器。然而,和其他的許多Web解決方案一樣,Cookie方案也不是萬能的,因為一些用戶可能不信任Cookie,關閉瀏覽器的Cookie支持。由于這種情況,URL改寫技術就出現了。使用URL改寫技術時,整個網站的所有URL后面都將加上會話標識符。雖然這個方案不像采用Cookie方案那樣簡單、穩固,但它確實行得通。URL改寫技術的不穩固是有兩方面的原因。首先,和Cookie不同,URL沒有過期時間,如果一個帶有會話標識的URL被截取后又重新在以后的訪問中使用,那么這種URL不會很有用,因為會話一般在一定的時間后會被作廢。其次,如果有一個URL鏈接的后面沒有帶上會話標識符,整個鏈都會中斷,客戶程序無法再次獲取會話標識符,除非它備份了帶有會話標識符的URL訪問歷史。
Servlet能夠只通過一次方法調用完成URL改寫。從技術上講,JSP也一樣能夠辦到這一點,但一個好的JSP頁面應該不包含Java代碼,或包含盡量少的Java代碼。為此,Struts提供了一個鏈接標記。本例使用了該標記來維持客戶端和服務器之間的會話狀態信息。
綁定View、Model和Controller
前面的簡單頁面不需要Struts分配器,因為它只有簡單的鏈接。圖4顯示了一個比較復雜的“類別”頁面。它列舉出了數據庫中的類別條目,并將這些條目分別鏈接到對應的編輯頁面。為顯示這個頁面,我們就要用到Struts的ActionServlet分配機制。

圖4 類別頁面
在web.xml文件中,放入一項表示所有以“.do”結尾的URL請求必須發送給Struts分配器的聲明。這里的分配器可以是org.apache.struts.action.ActionServlet或其擴展類。Struts分配器在啟動時讀取struts-config.xml文件,并構造出一個動作映射圖。本例指定了一個名為ShowCategories的動作類,來處理“ShowCategories”動作。可以看出,Struts應用的基本工作模式是:主分配器調用一個動作分配器,動作分配器確定或構造出Model部分(一個JavaBean或其它Java對象),并把它提供給View(通常是一個JSP頁面)。
本例使用Bean的情況稍微有點復雜,它有多個數據項,因此我們不是使用單個提供數據的Bean,而是要生成一組Bean。遺憾的是,JSP頁面以HTML為基礎,HTML沒有提供循環或其他控制邏輯。不過,Struts的logic:iterate允許對數組進行迭代操作,如下面的代碼片斷所示:
<table>
<logic:iterate id="category"
type="com.strutsdemo.Category"
name="<%= Constants.CATALOG_CATEGORIES %>"
scope="application">
<tr>
<td>
<html:link page="/editCategory.do"
name="category"
property="mapping">
編輯
</html:link>
<html:link page="/removeCategory.do"
name="category"
property="mapping">
刪除
</html:link>
<bean:write name="category"
property="category"/>
</td>
</tr>
</logic:iterate>
</table> |
上面討論了Model 2體系的Model和View部分,下面來看看Controller部分。Struts體系有一個主控制器,即ActionServlet。ActionServlet負責選擇和調用合適的動作控制器—即org.apache.struts.action.Action的擴展類。動作控制器實現了process()方法。process()方法分析從URL請求傳入的每一個參數,執行必要的業務邏輯,并返回一個指定了調用鏈中下一個鏈接的動作(通常是View)。在本例中,我們想要從數據庫提取數據,創建管理這些數據的JavaBean,把多個JavaBean整理成一個數組,再把數組保存到請求的上下文,從而使得作為View的JSP頁面能夠方便地進行頁面布局
為保證上述操作順利進行,在struts-config.xml文件中加入聲明,指定由哪一個動作處理器來處理指定的動作:
<action path="/showCategories"
scope="request"
type="com.strutsdemo.ShowCategoriesAction"
unknown="false"
validate="false">
<forward name="viewCategories" path="/??????
ShowCategories.jsp"/>
</action> |
表單處理 表單處理過程充分體現出Struts的優勢。在Web應用中,大部分復雜的HTML處理任務都涉及到表單。表單編輯過程具有類似圖5所示的請求或應答結構。更新操作的過程與創建操作的過程相似,但對于更新操作來說,“創建Model”這一步驟變成“裝入Model”,而“保存Model”變成了“更新Model”。請注意Web應用的特點:操作過程隨時可能中止,這既可能是因為用戶通過顯式的動作取消了當前的操作,也可能是因為用戶沒有提交表單,例如用戶跳轉到了一個不是用來處理當前表單的URL。

圖5 表單處理流程
表單編輯過程分三個階段:這里分別稱之為準備(Preparation)、表現(Presentation)和存儲(Preservation)。準備和存儲階段都屬于Struts動作,而表現階段主要是客戶端的活動。表1顯示了該過程中涉及的各種部件:
表單Bean(Form Bean)是一種特殊的JavaBean類型,它簡化了表單處理。Form Bean從org.apache.struts. action.ActionForm類擴展而來。Form Bean有幾個有用的特點,例如,通過reset()方法可以把Bean的屬性設置成默認值,通過validate()方法讓Bean驗證屬性的合法性。更重要的是,ActionServlet確保Form Bean被創建且可供它的動作方法調用。HTML標記庫還能夠確保Form Bean被正確地初始化并從Form View獲取數據。
Form Bean應當屬于Model部分,然而,由于它有validate()方法,因此從某些特征來看它更接近分配器。不過,不必太在乎這些概念上的問題。Model 2并不完全等同于MVC,而且一些人已經在責難MVC不外乎是幾種簡單設計模式的混合物。不管怎樣,從應用實踐的角度來講,系統的穩定性遠比概念的嚴格性更重要。在本例中,這個問題更加富有代表性,因為我們把持久化機制也包裝到Form Bean里面。從技術上看,Bean數據的持久化副本就是一個View,因此,從這個意義上來講,我們現在有了一個結合了分配器和View特點的Model。這種設計方式看起來似乎否定了引入Struts之類框架的理由,但實際上,這種設計方式兩方面的特點彌補了許多遺憾。
首先,由于驗證代碼和SQL代碼在很大程度上依賴于Form Bean擁有的屬性,所以把它們作為一個單元管理會帶來很大的方便。由于這里只對Form Bean的屬性感興趣,“重量級”的分配器和View部件都得到了有效的隔離。其次,Form Bean與HTML標記庫一起使用時,Form Bean可以包含其他對象。這些對象可以通過“.”符號應用。使用預定義的Java對象時,“.”引用方式能夠帶來很大的方便,因為Java不支持多重繼承。“.”引用方式避免了手工編寫大量get/set代碼的繁雜工作。
當內部對象是EJB時,“.”引用方式帶來的方便更加突出,因為在JSP頁面中引用EJB時,EJB往往顯得很“笨重”。如果EJB嵌入到了Form Bean里面,許多這方面的遺憾就不再存在。更重要的是,它分離了Controller和Model,而且View持久化也簡縮到了最簡單的程度,因為EJB容器可以處理所有持久化方面的細節。這樣,Form Bean就幾乎成了一個純粹的分配器,一切都變得整潔和清晰。
如果EJB有大量的屬性,而且按照ActionServlet通常對Form Bean所做的那樣,按照每個屬性分別更新的方式進行更新,就會出現大量的RMI調用開銷。對于要求較高的應用,更好的選擇是利用EJB 2.0本地接口,或者在EJB之前加上一個傳統的JavaBean(通常是會話EJB),并把該Bean傳遞給實體Bean的UpdateAllProperties()業務方法。后面這種方案允許在單個RMI調用中完成所有的更新操作.
準備階段
一次典型的編輯會話要求有一個動作處理器準備View,即一個作為View的JSP頁面,還要求有第二個動作處理器存儲更新后的View。當然,存儲操作之后會有第二個屬于View的頁面被顯示,例如一個“數據已經更新,點擊此處繼續”的頁面(參見表1)。
表1:基于Form Bean的編輯過程要用到的部件
部件 | 說明 |
CatalogForm | Form Bean |
EditCategoryAction | 準備階段 |
EditCategory.jsp | 編輯 |
SaveCategoryAction | 存儲階段 |
EditDone.jsp | 確認數據已經保存 |
EditFailed.jsp | “數據沒有保存”錯誤 |
下面的代碼片斷顯示了如何在struts-config.xml文件中配置準備階段:
<action path="/editCategory"
scope="request"
name="catForm"
type="com.strutsdemo.EditCategoryAction"
unknown="false"
validate="false">
<forward name="success"
path="/EditCategory.jsp"/>
</action> |
在準備階段,容器嘗試從Session或Request找出指定的Form Bean,這是因為在動作中指定了“name=...”。ActionServlet在struts-config.xml文件的
區域尋找Form Bean的別名,利用Form Bean的別名尋找對應的Java類。如果用戶的請求帶有參數,其名字匹配Form Bean屬性名字的參數將被設置為屬性值。Struts擴展了“屬性名字”的含義,使得訪問Form Bean內嵌對象的屬性成為可能。本文的例子也用到了Struts的這一優點。
準備好Form Bean之后,ActionServlet接著調用動作的process()方法,Form Bean作為參數之一傳入process()方法。在這里,我們對Form Bean的屬性作最后的調整,調用業務方法,委派作為View的EditCategory,從而生成一個以Form Bean中合適數據為基礎的HTML頁面。這個頁面被傳遞給客戶端,接下來就進入了“表現”階段。
表現階段 這一階段用戶編輯表單并提交。如果服務器端的應用認為用戶提交的內容存在問題,它把表單再次顯示給用戶,加上適當的提示信息;重復該過程,直至用戶提交了合法的表單,或取消了表單處理過程。編輯過程的中止可能是由于用戶跳轉到了其他頁面,或者啟動了一個取消動作(例如點擊了一個由html:cancel標記定義的按鈕)。雖然在理論上,View的驗證和再次顯示操作應該屬于表現階段,但在Struts應用中,這部分功能在存儲階段實現最方便。
存儲階段 準備階段創建了一個帶有“name=”屬性定義的動作CatForm,存儲階段要加入另外兩個屬性,即:“validate=‘true’”和“input=”屬性。
<action path="/saveCategory"
scope="request"
name="catForm"
type="com.strutsdemo.SaveCategoryAction"
unknown="false"
input="/EditCategory1.jsp"
validate="true">
<forward name="success"
path="/CategoryUpdated.jsp"/>
</action> |
設置了“validate=‘true’”屬性選項之后,服務器端就會增加一個處理步驟。重新用來自View的數據構造出Form Bean,或更新From Bean的時候,Form Bean的validate()方法會被調用。validate()方法執行必要的合法性驗證操作。如果用戶的輸入數據中存在錯誤,validate()方法就創建一個或多個ActionError對象。這些ActionError對象包含了錯誤信息源ID和表單輸入域的名稱。這些ActionError對象被收集和整理到一個ActionErrors對象,隨后ActionErrors對象由validate()方法返回。如果用戶輸入的數據不包含錯誤,validate()返回null。
由于指定了“input=”屬性,一旦出現了錯誤,動作會被忽略,而“input=”指定的View被顯示。這個View既包含Form Bean,也包含當前出現的錯誤對象集合。一般地,這個輸入頁面就是原來執行編輯功能的JSP頁面。
大多數Struts的html標記有對應的HTML標記,但Struts有一個HTML沒有的標記,即
標記。要中止表單編輯過程,用戶既可以手工輸入URL,也可以點擊不指向存儲動作處理器的鏈接。因此,用標記定義的“取消”按鈕,不是取消編輯操作的唯一方法。
假設validate()方法沒有發現任何錯誤,且用戶沒有點擊“取消”按鈕,存儲動作的process()方法將被調用。在本例的process()方法中,我們調用了Form Bean的save()方法把數據寫入持久性存儲設備,然后根據寫入操作是否成功,顯示“存儲操作成功”或“存儲操作失敗”的View。構造和運行Struts應用
要構造和運行本文的示例應用,你必須了解如何使用Jakarta的Ant工具。如果你還不了解Ant,現在該是學習它的時候了!趕緊到網站下載Ant,通常要解開壓縮,設置一下ANT_HOME環境變量,然后把Ant加入到執行路徑就可以了。
本文示例的build.xml需要稍微定制一下,修改指示本地Tomcat位置的配置,使它能夠找到在Tomcat下編譯所必需的類。另外,你還要有一份Struts的JAR。你可以去下載最新的版本。
struts-config.xml文件是粘合Struts應用各個部分的配置文件。在部署完成后的Web應用中,struts-config.xml在Web-INF目錄下。你應該修改一下數據源配置,使之符合你當前使用的DBMS環境。數據模型和SQL模式文件在下載包的DBMS目錄下,SQL文件針對PostgreSQL DBMS編寫。
示例中src/com/strutsdemo/ShowCategoriesAction. java是一個簡單的分配器。ActionForward()是請求分配方法,從ActionServlet調用。該方法可以完成主要的工作,例如分析請求參數、執行計算,以及構造出View使用的JavaBean。另外,該方法還要根據處理結果,確定下一個要顯示的是什么頁面:可能是預設的多個頁面之一,也可能是一個錯誤信息頁面。
ActionForward()的請求分配過程
當然,最復雜的處理過程與表單有關。ActionForward方法的請求分配過程是:
1. ActionServlet,對請求進行解碼。由于為動作指定了Form Bean,ActionServlet處理Form Bean(參見下面有關“ActionServlet如何使用Form Bean”的說明)。然后,請求傳遞給了EditCategoryAction。
2. EditCategoryAction;準備處理View,或者從數據庫裝入現有數據,或者創建新的數據項。動作處理器利用Mapping.findForward把控制傳遞給EditCategory.jsp。
3. DitCategory.jsp,顯示出Form Bean,允許用戶編輯數據。用戶提交數據后,控制轉到ActionServlet。
4. ActionServlet,對請求進行解碼。這一次,Form Bean將從View的數據初始化,因為它是一個Struts的JSP表單頁面。由于有Form Bean,且struts-config.xml中指定了“validate=‘true’”,名為“catForm”的Form Bean的validate()方法被調用。如果用戶提交的數據未能通過合法性驗證,則控制轉到EditCategory1.jsp。
5. EditCategory1.jsp,它只是EditCategory.jsp略加修改后的一個版本。如果有必要,原始編輯頁面和帶有錯誤提示的編輯頁面可以使用同一個View。Struts的JSP標記能夠幫助我們輕松地辦到這一點。該頁面提交給/saveCategory.do。這樣,用戶就在這幾個頁面之間繞圈子,直到他跳轉到一個與編輯操作無關的頁面,或者他提交的數據通過了合法性驗證。
6. 如果Form Bean合法性驗證通過,ActionServlet把請求(包括Form Bean)傳遞給SaveCategoryAction。在這個例子中,“save”可能意味著創建操作,也可能意味著更新操作,具體由URL提供的選項決定。寫入數據的操作通過調用Form Bean的store()方法完成。注意:實際的應用應當使用某種類型的事務管理機制(或使用EJB,因為EJB有內建的事務管理機制),以避免并發訪問帶來的問題。
ActionServlet如何使用Form Bean
涉及Form Bean的ActionServlet處理過程包含六個步驟:
1. 找到或創建Form Bean;
2. 據從HTTP請求傳入的相應數據,更新Form Bean的各個屬性;
3. 檢查用戶是否點擊了“取消”按鈕。如是,跳過步驟4和步驟5;
4. 驗證Form Bean數據的合法性;
5. 如數據未能通過合法性驗證,發送“input=”參數中指定的View;
6. 否則,把Form Bean傳遞給動作處理器