在上篇文章中,我們已經從較高層解釋了整個框架的結構,請 求流程的基礎,配置方式和Struts2和Struts1的不同之處。了解這 些後從Struts 應用 遷移到 Struts 2 不再是難事。
在這篇文章中,我們將會更詳細地講述如何由Struts 的action 轉為Struts 2的action。
一個應用的例子
這個例子選擇了大家都熟悉的 - weblog. 簡單地介紹下這例子 的功能需求:
增加一個新的日志
察看一個日志
修改一個日志
刪除一個日志
列出所有日至
增刪修改(CRUD),是項目中最為普遍的應用。
業務邏輯類在Struts 和 Struts2 應用都是可共用的。如:
public class BlogService ...{
private static List<Blog> blogs = new ArrayList<Blog>();
public List<Blog> list() ...{ ... }
public Blog create(Blog blog) ...{ ... }
public void update(Blog blog) ...{ ... }
public void delete(int id) ...{ ... }
public Blog findById(int id) ...{ ... }
}
BlogService 只是個簡單的業務邏輯類,並不是接口,Struts 和 Struts2 的action皆可調用其實例。雖然這樣設計在實際項目 中會帶來不必要的耦合,但我們的例子只是集中在討論web層上, 所以無關重要。
QUOTE:
工具箱: 在第一篇文章中,我們談論了在Struts2 actions中的 依賴注入的接口注入方式。這個是servlet 相關類 (HttpServletRequest, HttpServletResponse, PrincipalProxy, 等.)的主要注入方式,但這並不是唯一的方式。
Struts2 可以使用Spring框架作為默認的容器時,依賴注入的 setter方法就可用了。通過在action中加入setter方法(如下演示) , Struts2 框架將能從Spring框架中獲得正確的信息,並通過 setter加載在action中。
public void setBlogService(BlogService service) ...{
this.blogService = service;
}
和接口注入方式類似,我們需要一個攔截器來幫助我們完成任 務,這就是 ActionAutowiringInterceptor 攔截器。這樣我們的 業務邏輯類就通過Spring框架管理自動在action被調用之前注入到 Struts2得action中。有多種的配置參數(如by name, by type 或 automatically)可供選擇,可以讓對象和setter匹配的注入的方式 根據你的需要而定。
Struts 應用中的代碼
我們首先從Struts講起。在Struts中,最普遍的做法是,對於 每個需求用例(如save,update,remove,list)來說都會有對應的 action類,同時也會有相應的action form類。在我們的應用中的 這個方式或許不是最佳的實現方式(其他的解決方案包括使用 dynamic form或者使用request來分發action),但我們例子中的做 法是所有Struts開發者最熟悉的一種形式。了解了這種簡單的實現 方法,你有能力在遷移到Struts2時,使用更為優秀的方法。
在第一篇文章中我們談過Struts 和 Struts2 中action的區別 。現在我們從UML中再次看看他們的差別。一般來說form在Struts action中的表現形式是: 下圖 image1.jpg
這action form將會在多個action中使用,讓我們來看看它:
public class BlogForm extends ActionForm ...{
private String id;
private String title;
private String entry;
private String created;
// public setters and getters for all properties
}
如UML中展示的那樣,其中一個限制就是必須繼承ActionForm類 ,另外一個限制就是form中所有屬性都必須是String類型,所以所 有的getter和setter都必須只能接受String參數和返回String結果 。
然後我們來看看action。我們這個例子中的action有view, create 和 update action。
The View Action:
The Create Action:
public class ViewBlogAction extends Action ... {
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception ...{
BlogService service = new BlogService();
String id = request.getParameter("id");
request.setAttribute("blog",service.findById (Integer.parseInt(id)));
return (mapping.findForward("success"));
}
}
public class SaveBlogEntryAction extends Action ...{
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception ...{
BlogService service = new BlogService();
BlogForm blogForm = (BlogForm) form;
Blog blog = new Blog();
BeanUtils.copyProperties( blog, blogForm );
service.create( blog );
return (mapping.findForward("success"));
}
}
public class UpdateBlogEntryAction extends Action ... {
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception ...{
BlogService service = new BlogService();
BlogForm blogForm = (BlogForm) form;
Blog blog = service.findById( Integer.parseInt (blogForm.getId()));
BeanUtils.copyProperties( blog, blogForm );
service.update( blog );
request.setAttribute("blog",blog);
return (mapping.findForward("success"));
}
}
這三個action都跟隨著同一個模式:
產生一個新的業務邏輯對象實例 - 如前面所提到的,我們使用 最直接的方式在action中使用業務邏輯對象,這表示在每個action 中都會產生新的業務邏輯對象實例。
從請求中獲得數據 - 這是兩種形式之一。在view action 中,"id"是從HttpServletRequest 對象中直接獲取的。而在 create 和 update action 中,則從ActionForm 中取值。 ActionForm 與 HttpServletRequest 的調用方式其實很相似,唯 一不同的ActionForm 是bean的從field中取值。
調用業務邏輯- 現在開始生成調用業務邏輯所需的參數並調用 邏輯。如果參數(在view action中)是一個簡單對象類型,則轉換 值時會自動轉為正確的類型(如從String轉到Integer)。如果參數 是復雜的對象類型,,則ActionForm 需要通過BeanUtil 來幫忙轉 成相應的對象。
設定返回的數據 - 如果需要把數據返回顯示給用戶,那則要把 這個數據設在HttpServletRequest 的attribute 中返回。返回一 個 ActionForward - 所有 Struts action的最後都需要找到並返 回其相應的 ActionForward 對象.
最後的兩個action,remove和list action, 只有很少的差別。 remove action如下所示,沒有用BlogForm類. 通過從request的 attribute中獲取"id"(和view action相似),就能調用業務邏輯完 成其需要的工作。在下面我們介紹配置時,你可以看到它並沒有返 回任何數據,因為它的"success"返回結果其實是執行remove後再 執行了list action來返回信息的。
public class RemoveBlogEntryAction extends Action ...{
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception ...{
BlogService service = new BlogService();
String id = request.getParameter("id");
service.delete(Integer.parseInt(id));
return (mapping.findForward("success"));
}
}
list action並不需要任何的用戶輸入,它只是簡單地調用了業 務邏輯的無參方法,同時返回所有的Blog對象。
public class ListBlogsAction extends Action ... {
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception ...{
BlogService service = new BlogService();
request.setAttribute("bloglist",service.list());
return (mapping.findForward("success"));
}
}
向 Struts2 遷移
在Struts2中,可選的實現方式有很多,可以像Struts那樣每個 需求用例對應一個action,也可以用一個action對應所有需求用例 。但在我們的例子中,使用的方法是我認為最佳的解決方案 - 在一 個action類中實現整套CRUD功能。
也許你人為把list需求用例也同樣地整合到同一個action類裡 會比較好,而我認為把list的功能分到另外一個action中,會減少 容易產生的混淆,因為list用例中並不需要Blog這個類作為屬性, 而在其他用例中則需要。
對於 Struts2的例子, 它的UML模型展示如下:
下圖image2.jpg
每個用例在action中都有自己所對應的方法。從上圖中我們可 以看到,在BlogAction 中我們有save, update 和 remove三個方 法。而ListBlogAction中,沒有list這個方法,因為 ListBlogAction繼承了ActionSupport 類,實際上就是在默認的 execute 方法中實現list功能。
為了更容易看,圖中的BlogAction並沒有畫出它所實現了的三 個接口。它們分別是ServletRequestAware 接口, Prepareable 接口和 ModelDriven 接口。
首先回顧一下ServletRequestAware, 我們在第一篇文章中已經 詳細介紹它了。這個ParametersInterceptor 攔截器提供了把 HttpServletRequest 自動set到action中的功能,讓我們能通過 request, 把所需的值傳回到JSPs。
接著看看Preparable 接口, 它會聯合PrepareInterceptor攔 截器一起工作,讓action在執行execute() 方法前, 執行一個 prepare()方法,實現在執行前設定,配置或預設一些值在action 中。 在我們的例子裡,prepare方法會檢查blogId 屬性,如果為零 則這是一個新日志,非零則該日志已經存在,根據blogId取出日志 。
最後我們說說ModelDriven 接口,在上一篇文章中,我們已經 了解到 Struts action的很大的不同在於它是需要線程安全的,而 在Struts2中則沒有這個限制,因為每次的請求都會有一次action 對象的初始化和調用。沒有了這個限制,能允許Struts2使用類級 別的屬性變量(特別是getters和setters),從而獲得更多編碼優勢 。
和攔截器的功能結合起來, 把HttpServletRequest 中的 attribute 注入action中的流程如下所示:
循環讀取HTTP request中的attribute
查找當前request attribute中是否有和action中的setter中的 屬性匹配的
有則根據attribute從HttpServletRequest 裡取出其值
把取出來的值由String轉成setter中相應的類型
調用setter把該轉換後的值注入action中
QUOTE:
提示: 當調用action時,如果發現不明原因使不能正確地通過 setter注入值情況下,第一步最好是先檢查下各攔截器,確保它們 都已作用於該action。因為這些意外通常有時由攔截器設置不當形 成的,檢查是否各個攔截器都已起作用,並看看它們作用的順序, 因為有些情況下它們間會相互影響而產生錯誤。
現在我們已經有基於String類型的form bean中取值的方法或者 是自動把request的attributes 注入到action的方法,那下一步就 是如何把值傳入 domain object 或 value / transfer object的 屬性中去。其實這很簡單,你只需要實現ModelDriven 接口(即實 現getModel()方法)就可以了,確保ModelDrivenInterceptor 攔截 器已作用於action。
除了會調用action中的setter外,model 首先檢查是否有和 setter可以匹配當前的attribute名。如果在model中沒有這個 attribute相應的setter,則會再在action上找相應的setter來設 值。
在BlogAction 的例子中我們可以看到如何很靈活地使用這些方 法,首先通過prepare() 方法根據Id獲取相應的 Blog model object 或新建一個instance, 然後再根據把request中相應的屬性 注入Blog instance中和action中。
以上的兩個功能使得現在調用action那麼簡單 - 調用具體的業 務邏輯,和把數據設在HttpServletRequest供返回用。
public class BlogAction extends ActionSupport
implements ModelDriven, Preparable, ServletRequestAware ...{
private int blogId;
private Blog blog;
private BlogService service = new BlogService();
private HttpServletRequest request;
public void setServletRequest(HttpServletRequest httpServletRequest) ...{
this.request = httpServletRequest;
}
public void setId(int blogId) ...{
this.blogId = blogId;
}
public void prepare() throws Exception ...{
if( blogId==0 ) ...{
blog = new Blog();
} else ...{
blog = service.findById(blogId);
}
}
public Object getModel() ...{
return blog;
}
public String save() ...{
service.create(blog);
return SUCCESS;
}
public String update() ...{
service.update(blog);
request.setAttribute("blog",blog);
return SUCCESS;
}
public String remove() ...{
service.delete(blogId);
return SUCCESS;
}
public String execute() ...{
request.setAttribute("blog",blog);
return SUCCESS;
}
}
最後就是說說 list這個用例了。它同樣需要訪問 HttpServletRequest對象去返回數據給JSP,所以也需要實現 ServletRequestAware 接口。但是,因為它並不需要任何輸入值, 所以就不需要實現其他的接口了。以下是它的具體實現:
public class ListBlogsAction extends ActionSupport implements ServletRequestAware ...{
private BlogService service = new BlogService();
private HttpServletRequest request;
public void setServletRequest(HttpServletRequest httpServletRequest) ...{
this.request = httpServletRequest;
}
public String execute() ...{
request.setAttribute("bloglist",service.list());
return SUCCESS;
}
}
這樣就完成了我們該實現的action代碼了。 在下 一篇文章中,當我們新的Struts2用戶界面結合時,我們還會進一 步簡化action的代碼。
在我們調用action之前,我們必須通過XML配置文件去配置它們 。
在Struts中, 我們習慣用在WEB-INF 目錄的"struts- config.xml"配置文件,在這裡我們需要配置action form和action 屬性。在Struts2中, 用的是在classpath中的"struts.xml"配置 文件, 它看起來好象會稍微復雜一些,因為它需要在配置action 的同時也配置其攔截器。
在Struts中配置 form-beans 節點很容易, 只需要一個唯一的 名字,還有就是繼承ActionForm類的class作為type。
<struts-config>
<form-beans>
<form-bean name="blogForm"
type="com.fdar.articles.infoq.conversion.struts.BlogForm"/ >
</form-beans>
...
</struts-config>
在我們的例子中,我們的配置 文件有3點不相同:
1. 重定向配置
在Struts的配置中,每個mapping 都需要提供調用action時所 需要對應的路徑,Struts默認為".do", 例如paht是"/struts/add" 對應於URL"/struts/add.do"。同時也需要一個forward 屬性來提 供給URL去轉向,如"/struts/add.jsp".
<struts-config>
...
<action-mappings>
<action path="/struts/add" forward="/struts/add.jsp"/>
...
</action-mappings>
</struts-config>
而Struts2的需要更多的一些配置,如:
首先你會注意到的是,代替action-mappings 節點的是include 和package 節點。Struts2可以把配置細分到任意數目的配置文件 中,來實現配置可模塊化管理。每個配置文件的結構其實都是一樣 的,不同的只是文件名。
include 節點中,以文件名作為file 屬性,可把所include的 文件內容包含到當前文件中。
package 節點把actions組成一組,其name 屬性的值必須是唯 一的。
在 Struts action的配置中, paht屬性需要指定完整的URL路 徑。而在Struts2中,URL是通過package節點中的namespace屬性, 還有在action 節點中的name 屬性, 和action擴展(默認 是".action")共同起作用的。在上面的例子中,則URL 為"/struts2/add.action"時會調用action。
package節點除了可以分離命名空間外, package 節點中的 extends 屬性,還提供了某種可復合的組成結構。通過繼承另外一 個package節點,你就能繼承那個節點的配置,包括其actions, results, interceptors, exception,等值。在我們的例子 中,"struts2" package節點繼承了 "struts-default" package 節點(在"struts-default.xml" 文件裡定義了該節點) ,注意這個 是主要的include文件,所以必須在所有配置之前的第一行中寫出 。 這個功能有助於大大減少你重復性輸入默認配置所浪費的時間 。
最後是result 節點, 它只是存放你這個action所需要轉向的 URL. 在這裡我們沒有提及name 和 type 屬性。如果你不想改變它 們的默認屬性的話,你能忽略不寫它們,讓你的配置文件看起來更 清晰。從action返回的 "success" 的結果將組成這個JSP顯示給用 戶。
2. Action 配置
在Struts 中forward 節點指定了action處理後,結果將重定向 到哪個相應的頁面。type屬性指定了action的類,scope 屬性保證 了form beans只在request范圍內。
<struts-config>
...
<action-mappings>
<action path="/struts/list" scope="request"
type="com.fdar.articles.infoq.conversion.struts.ListBlogsA ction" >
<forward name="success" path="/struts/list.jsp"/>
</action>
...
</action-mappings>
</struts-config>
Struts2 的 XML配置和上面提到的基本相同。唯一不同的就是 通過class屬性為action節點提供了它所需要調用的類的完整路徑
<struts>
...
<package name="struts2" extends="struts-default" namespace="/struts2">
<default-interceptor-ref name="defaultStack"/>
<action name="list"
class="com.fdar.articles.infoq.conversion.struts2.ListBlog sAction">
<result>/struts2/list.jsp</result>
<interceptor-ref name="basicStack"/>
</action>
...
</package>
</struts>
如果是用其他的方法而不是用默認的execute 方法去調用 action(在BlogAction 類中大多數方法如此), 則需要在action節 點的 method 屬性裡加入方法名,下面就是個例子,這時候update 方法將會被調用。
<action name="update" method="update"
class="com.fdar.articles.infoq.conversion.struts2.BlogActi on" >
...
</action>
default-interceptor-ref 和 interceptor-ref 節點有幾點不 同。在第一篇文章中,我們看到在action被調用之前必須通過一系 列的攔截器,而這兩個節點就是用來配置攔截器組的。default- interceptor-ref 節點為該package提供了默認的攔截器組。當在 action節點中提供 interceptor-ref節點時 ,它就會覆蓋默認的 攔截器(interceptor-ref 節點能夠和單獨一個攔截器相關聯,或 者跟一個攔截器組相關聯),在action節點中可以存在多個 interceptor-ref節點,處理攔截器組的順序會和該節點列出的順 序一致。
3. 再重定向配置
當我們提交表格的時候,我們需要重定向到更新後的結果頁面 。這個通常稱為 "post-redirect pattern" 或, 最近出現的, "flash scope."
由於這是一個form, 所以在Struts中我們需要為Struts指定一 個ActionForm。需要在name屬性中提供form的名稱,同樣地,我們 也需要在forward 節點中舉加入redirect屬性為true。
<struts-config>
...
<action-mappings>
<action path="/struts/save"
type="com.fdar.articles.infoq.conversion.struts.SaveBlogEn tryAction"
name="blogForm" scope="request">
<forward name="success" redirect="true" path="/struts/list.do"/>
</action>
...
</action-mappings>
</struts-config>
Struts2 在result 節點裡提供了type 屬性, 默認情況下 是"dispatch", 如果需要重定向,則需要設為 "redirect"。
<struts>
...
<package name="struts2" extends="struts-default" namespace="/struts2">
<action name="save" method="save"
class="com.fdar.articles.infoq.conversion.struts2.BlogActi on" >
<result type="redirect">list.action</result>
<interceptor-ref name="defaultStack"/>
</action>
...
</package>
</struts>
總結
我們並不可能在這篇文章中覆蓋所有的內容,如果你需要更好 的了解整個框架,還有其他的實現方式和選項,這裡有幾點可以供 你參考:
配置攔截器和攔截器組 - 以Struts2-core JAR 包裡 的"struts-default.xml" 文件作為例子。"struts-default.xml" 演示了如何配置你自己的攔截器組,包含新的攔截器,你可以嘗試 實現自己的攔截器。
配置文件中的通配符模式 - 你可以選擇使用Struts2中的通配 符模式來簡化你的配置。
通過 ParameterAware 接口把form值傳入maps中 - 你可以在 Struct2中配置,讓所有request的form屬性都存於action的一個 map中,這樣就不需要專門再為action指定model / transfer / value object了。這和Struts的dynamic form特點很相似。
也許到現在為,也許你有個疑問,"遷移後我們的界面是否可以 完全重用呢?",答案是yes。你能從這裡, 下載到我這篇文章中的 完整源代碼,你可以自己嘗試把URL的擴展名由".do" 改為 ".action",使用的頁面時一樣的。除此之外,其實用JSTL來代替 Struts taglib也是很容易的。