轉載自 http://downpour.iteye.com/blog/1335991java
Struts2中的設計模式
設計模式(Design pattern)是通過程序員反覆實踐後造成的一套代碼設計經驗的總結。設計模式隨着編程語言的發展,也由最初的「編程慣例」逐步發展成爲被反覆使用、併爲絕大多數程序員所知曉的、完善的理論體系。咱們使用設計模式(Design pattern)的初衷,是使代碼的重用度提升、讓代碼可以更容易被別人理解以及保證代碼的可靠性。毫無疑問,在程序中使用設計模式不管是對於程序員自身仍是對於應用程序都是共贏的結果。正確地使用設計模式,可以使咱們編程真正實現工程化和規範化,而且在必定程度上指導着框架的設計和實現。
在深刻探討Struts2所依賴的核心技術以前,咱們將首先帶領讀者領略一下在整個Struts2框架之中所使用到的一些最經常使用的設計模式。理解這些設計模式的運用場景和內部機理,也將爲往後咱們對這些核心技術的分析打下堅實的基礎。
4.1 ThreadLocal模式
ThreadLocal模式,嚴格意義上來講並不能稱之爲一種設計模式,由於它只是一個用來解決多線程程序中數據共享問題的一個解決方案。儘管如此,ThreadLocal模式卻貫穿了整個Struts2和XWork框架,成爲Struts2框架進行「解耦」設計的核心依賴技術。那麼,爲何要在Struts2中引入ThreadLocal模式呢?這不得不從Web開發中的線程安全問題談起。
4.1.1線程安全問題的由來
在傳統的Web開發中,咱們處理Http請求最經常使用的方式是經過實現Servlet對象來進行Http請求的響應。Servlet是J2EE的重要標準之一,規定了Java如何響應Http請求的規範。經過HttpServletRequest和HttpServletResponse對象,咱們可以輕鬆地與Web容器交互。
當Web容器收到一個Http請求時,Web容器中的一個主調度線程會從事先定義好的線程池中分配一個當前工做線程,將請求分配給當前的工做線程,由該線程來執行對應的Servlet對象中的service方法。若是這個工做線程正在執行的時候,Web容器收到另一個請求,主調度線程會一樣從線程池中選擇另外一個工做線程來服務新的請求。Web容器自己並不關心這個新的請求是否訪問的是同一個Servlet實例。所以,咱們能夠得出一個結論:對於同一個Servlet對象的多個請求,Servlet的service方法將在一個多線程的環境中併發執行。
因此,Web容器默認採用單實例(單Servlet實例)多線程的方式來處理Http請求。這種處理方式可以減小新建Servlet實例的開銷,從而縮短了對Http請求的響應時間。可是,這樣的處理方式會致使變量訪問的線程安全問題。也就是說,Servlet對象並非一個線程安全的對象。下面的測試代碼將證明這一點:
程序員
這裏參閱了網絡上一段著名的對Servlet線程安全性進行測試的代碼(http://zwchen.iteye.com/blog/91088)。運行以後,咱們能夠看一下這個例子的輸出:
web
經過上面的輸出,咱們能夠得出如下三個Servlet對象的運行特性:
1. Servlet對象是一個無狀態的單例對象(Singleton),由於咱們看到屢次請求的this指針所打印出來的hashcode值都相同
2. Servlet在不一樣的線程(線程池)中運行,如http-8081-Processor22和http-8081-Processor23等輸出值能夠明顯區分出不一樣的線程執行了同一段Servlet邏輯代碼。
3. Counter變量在不一樣的線程中共享,並且它的值被不一樣的線程修改,輸出時已經不是順序輸出。也就是說,其餘的線程會篡改當前線程中實例變量的值,針對這些對象的訪問不是線程安全的。
【有關線程安全的概念範疇】
談到線程安全,對於許多初學者來講很容易引發概念上的混淆。線程安全,指的是在多線程環境下,一個類在執行某個方法時,對類的內部實例變量的訪問安全與否。所以,對於下面列出來的2類變量,不存在任何線程安全的說法: 1)方法簽名中的任何參數變量。
2)處於方法內部的局部變量。
任何針對上述形式的變量的訪問都是線程安全的,由於它們都處於方法體的內部,由當前的執行線程獨自管理。
這就是線程安全問題的由來:在傳統的基於Servlet的開發模式中,Servlet對象內部的實例變量不是線程安全的。在多線程環境中,這些變量的訪問須要經過特殊的手段進行訪問控制。
解決線程安全訪問的方法不少,比較容易想到的一種方案是使用同步機制,可是出於對Web應用效率的考慮,這種機制在Web開發中的可行性很低,也違背了Servlet的設計初衷。所以,咱們須要另闢蹊徑來解決這一困擾咱們的問題。
4.1.2 ThreadLocal模式的實現機理
在JDK的早期版本中,提供了一種解決多線程併發問題的方案: java.lang.ThreadLocal類。ThreadLocal類在維護變量時,實際使用了當前線程(Thread)中的一個叫作ThreadLocalMap的獨立副本,每一個線程能夠獨立修改屬於本身的副本而不會互相影響,從而隔離了線程和線程,避免了線程訪問實例變量發生衝突的問題。
ThreadLocal自己並非一個線程,而是經過操做當前線程(Thread)中的一個內部變量來達到與其餘線程隔離的目的。之因此取名爲ThreadLocal,所指望表達的含義是其操做的對象是線程(Thread)的一個本地變量。若是咱們看一下Thread的源碼實現,就會發現這一變量,如代碼清單4-2所示:
數據庫
這是JDK中Thread源碼的一部分,從中咱們能夠看出ThreadLocalMap跟隨着當前的線程而存在。不一樣的線程Thread,擁有不一樣的ThreadLocalMap的本地實例變量,這也就是「副本」的含義。接下來咱們再來看看ThreadLocal.ThreadLocalMap是如何定義的,以及ThreadLocal如何來操做它,如代碼清單4-3所示:
apache
從上述代碼中,咱們看到了ThreadLocal類的大體結構和進行ThreadLocalMap的操做。咱們能夠從中得出如下的結論:
1. ThreadLocalMap變量屬於線程(Thread)的內部屬性,不一樣的線程(Thread)擁有徹底不一樣的ThreadLocalMap變量。
2. 線程(Thread)中的ThreadLocalMap變量的值是在ThreadLocal對象進行set或者get操做時建立的。
3. 在建立ThreadLocalMap以前,會首先檢查當前線程(Thread)中的ThreadLocalMap變量是否已經存在,若是不存在則建立一個;若是已經存在,則使用當前線程(Thread)已建立的ThreadLocalMap。
4. 使用當前線程(Thread)的ThreadLocalMap的關鍵在於使用當前的ThreadLocal的實例做爲key進行存儲。
ThreadLocal模式,至少從兩個方面完成了數據訪問隔離,有了橫向和縱向的兩種不一樣的隔離方式,ThreadLocal模式就能真正地作到線程安全:
縱向隔離 —— 線程(Thread)與線程(Thread)之間的數據訪問隔離。這一點由線程(Thread)的數據結構保證。由於每一個線程(Thread)在進行對象訪問時,訪問的都是各自線程本身的ThreadLocalMap。
橫向隔離 —— 同一個線程中,不一樣的ThreadLocal實例操做的對象之間的相互隔離。這一點由ThreadLocalMap在存儲時,採用當前ThreadLocal的實例做爲key來保證。編程
深刻比較TheadLocal模式與synchronized關鍵字設計模式
ThreadLocal模式synchronized關鍵字都用於處理多線程併發訪問變量的問題,只是兩者處理問題的角度和思路不一樣。安全
1)ThreadLocal是一個java類,經過對當前線程中的局部變量的操做來解決不一樣線程的變量訪問的衝突問題。因此,ThreadLocal提供了線程安全的共享對象機制,每一個線程都擁有其副本。網絡
2)Java中的synchronized是一個保留字,它依靠JVM的鎖機制來實現臨界區的函數或者變量的訪問中的原子性。在同步機制中,經過對象的鎖機制保證同一時間只有一個線程訪問變量。此時,被用做「鎖機制」的變量時多個線程共享的。session
同步機制(synchronized關鍵字)採用了「以時間換空間」的方式,提供一份變量,讓不一樣的線程排隊訪問。而ThreadLocal採用了「以空間換時間」的方式,爲每個線程都提供一份變量的副本,從而實現同時訪問而互不影響。
ThreadLocal模式並非什麼高深的學問,它甚至從JDK1.2開始就存在於Java世界中。因而可知,咱們掌握一種知識的最終目的是熟練而合理地運用它。
4.1.3 ThreadLocal模式的應用場景
在分析了ThreadLocal的源碼以後,咱們來看看ThreadLocal模式最合適的業務場景。在一個完整的「請求-響應」過程當中,主線程的執行過程老是貫穿始終。當這個主線程的執行過程當中被加入了ThreadLocal的讀寫時,會對整個過程產生怎樣的影響呢?咱們根據以前源碼分析的結果,並結合分層開發模式,把整個流程畫下來,如圖4-1所示:
從上面圖中咱們能夠看到,因爲ThreadLocal所操做的是維持於整個Thread生命週期的副本(ThreadLocalMap),因此不管在J2EE程序程序的哪一個層次(表示層、業務邏輯層或者持久層),只要在一個Thread的生命週期以內,存儲於ThreadLocalMap中的對象都是線程安全的(由於ThreadLocalMap自己僅僅隸屬於當前的執行線程,是執行線程內部的一個屬性變量。咱們用圖中的陰影部分來表示這個變量的存儲空間)。而這一點,正是被咱們用於來解決多線程環境中的變量共享問題的核心技術。ThreadLocal的這一特性也使其可以被普遍地應用於J2EE開發中的許多業務場景。
【數據共享 OR 數據傳遞?】
ThreadLocal模式因爲利用了Java自身的語法特性而顯得異常簡單和便利,於是被普遍應用於J2EE開發,尤爲是應對跨層次的資源共享,例如在Spring中,就有使用ThreadLocal模式來管理數據庫鏈接或者Hibernate的Session的範例。
在一些比較著名的論壇中,有着不少關於使用ThreadLocal模式來作數據傳遞的討論。事實上,這是對ThreadLocal模式的一個極大的誤解。讀者須要注意的是,ThreadLocal模式解決的是同一線程中隸屬於不一樣開發層次的數據共享問題,而不是在不一樣的開發層次中進行數據傳遞。
1)ThreadLocal模式的核心在於實現一個共享環境(類的內部封裝了ThreadLocal的靜態實例)。因此,在操做ThreadLocal時,這一共享環境會跨越多個開發層次而隨處存在。
2)隨處存在的共享環境形成了全部的開發層次的共同依賴,從而使得全部的開發層次都耦合在了一塊兒,從而變得沒法獨立測試。
3)數據傳遞應該經過接口函數的簽名顯式聲明,這樣纔可以從接口聲明中表達接口所表達的真正含義。ThreadLocal模式位於實現的內部,從而使得接口與接口之間沒法達成一致的聲明契約。
Struts2的解耦合的設計理念使得Struts2的MVC實現成爲了使用ThreadLocal模式的自然場所。在第三章中,咱們已經介紹了一些基本概念,Struts2經過引入XWork框架,將整個Http請求的過程拆分紅爲與Web容器有關和與Web容器無關的兩個執行階段。而這兩個階段的數據交互就是經過ThreadLocal模式中的線程共享副本安全地進行。在其中,咱們沒有看到數據傳遞,存在的只是整個執行線程的數據共享。
4.1.4 ThreadLocal模式的核心元素
仔細分析上一節的示意圖(圖4-1),咱們能夠發現,要完成ThreadLocal模式,其中最關鍵的地方就是建立一個任何地方均可以訪問到的ThreadLocal實例(也就是執行示意圖中的菱形部分)。而這一點,咱們能夠經過類的靜態實例變量來實現,這個用於承載靜態實例變量的類就被視做是一個共享環境。咱們來看一個例子,如代碼清單4-4所示:
在這個Counter類中,咱們實現了一個靜態的ThreadLocal變量,並經過get方法將ThreadLocal中存儲的值暴露出來。咱們還封裝了一個帶有業務邏輯的方法getNextCounter,操做ThreadLocal中的值,將其加1,並返回計算後的值。
此時,Counter類就變成了一個數據共享環境,咱們也擁有了實現ThreadLocal模式的關鍵要素。有了它,咱們來編寫一個簡單的測試,如代碼清單4-5所示:
這是一個簡單的線程類,循環輸出當前線程的名稱和getNextCounter的結果,因爲getNextCounter中的邏輯所操做的是ThreadLocal中的變量,因此不管同時有多少個線程在運行,返回的值將僅與當前線程的變量值有關,也就是說,在同一個線程中,變量值會被連續累加。這一點能夠經過以下的測試代碼證明:
咱們來運行一下上面的代碼,並看看輸出結果:
上面的輸出結果也證明了,counter的值在多線程環境中的訪問是線程安全的。從對例子的分析中咱們能夠再次體會到,ThreadLocal模式最合適的使用場景:在同一個線程(Thread)的不一樣開發層次中共享數據。
從上面的例子中,咱們能夠簡單總結出實現ThreadLocal模式的兩個主要步驟:
1. 創建一個類,並在其中封裝一個靜態的ThreadLocal變量,使其成爲一個共享數據環境。
2. 在類中實現訪問靜態ThreadLocal變量的靜態方法(設值和取值)。
創建在ThreadLocal模式的實現步驟之上,ThreadLocal的使用則更加簡單。在線程執行的任何地方,咱們均可以經過訪問共享數據類中所提供的ThreadLocal變量的設值和取值方法安全地得到當前線程中安全的變量值。
這兩個步驟,咱們以後會在Struts2的實現中屢次說起,讀者只要能充分理解ThreadLocal處理多線程訪問的基本原理,就能對Struts2的數據訪問和數據共享的設計有一個總體的認識。
講到這裏,咱們回過頭來看看ThreadLocal模式的引入,到底對咱們的編程模型有什麼重要的意義呢?
這一點,是由ThreadLocal模式的實現機理決定的。由於實現ThreadLocal模式的一個重要步驟,就是構建一個靜態的共享存儲空間。從而使得任何對象在任什麼時候刻均可以安全地對數據進行訪問。
這一點是ThreadLocal模式給咱們帶來的最爲核心的一個影響。由於在通常狀況下,Java對象之間的協做關係,主要經過參數和返回值來進行消息傳遞,這也是對象協做之間的一個重要依賴。而ThreadLocal模式完全打破了這種依賴關係,經過線程安全的共享對象來進行數據共享,能夠有效避免在編程層次之間造成數據依賴。這也成爲了XWork事件處理體系設計的核心。
Struts2的線程安全
通常狀況,咱們的ActionContext都是經過:ActionContext context = (ActionContext) actionContext.get();來獲取的。咱們再來看看這裏的actionContext對象的建立:
static ThreadLocal actionContext=new ActionContextThreadLocal();,
ActionContextThreadLocal是實現ThreadLocal的一個內部類。ThreadLocal能夠命名爲「線程局部變量」,它爲每個使用該變量的線程都提供一個變量值的副本,使每個線程均可以獨立地改變本身的副本,而不會和其它線程的副本衝突。這樣,咱們ActionContext裏的屬性只會在對應的當前請求線程中可見,從而保證它是線程安全的。
Struts2的action中就像一個POJO同樣,定義了不少的類變量。此時,就使用scope=prototype來指定是個原型模式,而不是單例,這樣就解決了線程安全問題。每一個線程都是一個新的實例
在Struts2.0中,Action已經與Servlet API徹底分離,這使得Struts2.0的Action具備了更加靈活和低耦合的特性,與Struts1.0相比較而言是個巨大的進步。雖然 Struts2.0的Action已經與Servlet API徹底分離,但咱們在實現業務邏輯處理時常常須要訪問Servlet中的對象,如Session、Application等。Struts2.0 提供了一個名字爲ActionContext的類,在Action中能夠經過該類得到Servlet API。
ActionContext是一個Action的上下文對象,Action運行期間所用到的數據都保存在ActionContext中(如Session,客戶端提交的參數等信息)。
在Action中能夠經過下面的代碼來建立和使用ActionContext類,關於該類的方法介紹以下所示:
ActionContext ac=ActionContext.getContext();
如下是ActionContext類的經常使用方法
1.Object get(Object key) :經過參數key來查找當前ActionContext中的值
2.Map getApplication() :返回一個Application級的Map對象
3.Static ActionContext getContext() :得到當前線程的ActionContext對象
4.Map getParameters() :返回一個包含全部HttpServletRequest參數信息的Map對象
5.Map getSession() :返回一個Map類型的HttpSession對象
6.Void put(Object key,Object value) :向當前ActionContext對象中存入名值對信息
7.Void setApplication(Map application) :設置Application上下文
8.Void setSession(Map session) :設置一個Map類型的Session值
Struts2 控制流程
1) 請求到來
2) 建立 ValueStack( Action 放棧頂),迕行初始化
3) 調用攔截器 Interceptor,在攔截器中是能夠訪問 ValueStack 的
4) 調用 Action,執行 execute()方法
5) 調用 Result, Result 負責把數據顯示給用戶
6) 最後到頁面,經過標記庫(Taglib)取出數據
當一個請求到達Servlet容器(Tomcat)後,將被傳遞給一個標準的過濾器鏈,在這個過濾器鏈中包括了可選的ActionContextCleanUp過濾器.當在Struts2 Web應用程序中集成SiteMesh時,纔會用到此鏈。接下來,必須的FilterDispatcher被調用,它輪詢ActonMapper(org.apache.struts2.dispatcher.mapper.ActionMapper),以便確認這個請求是否應該調用一個action。若是ActionMapper肯定了一個請求應該被調用,那麼FilterDispatcher就把控制權委派給ActionProxy(com.opensymphony.xwork2.ActionProxy),ActionProxy詢問框架的配置文件管理器(它從struts.xml文件中讀取配置信息),接下來,ActionProxy建立一個實現了命令模式的ActionInvocation,ActionInvocation在調用action以前會一次調用全部配置的鏈接器。一旦action執行返回,ActionInvocation就要struts.xml裏面配置),而後執行這個result,一般狀況下result會調用JSP或者freeMarker模板呈現頁面(但不老是這樣,result也能夠是一個action鏈).
這裏介紹一下Struts2框架的組成部分:
1.ActionMapper和ActionMapping:
org.apache.struts2.dispatcher.mapper.ActionMapper接口在HTTP請求和action調用請求之間提供了一個映射,當給定一個HTTP請求時,ActionMapper根據請求的URL來查找是否有對應的action調用。若是有則返回一個描述了action調用的
ActionMapping,若是沒有找到匹配的action調用請求,則返回null;
Struts2框架對該接口提供的默認實現是org.apacher.struts2.disspatcher.mapper.
DefaultActionMapper.
ActionMapping本質上是一個數據傳輸對象,它將Action類和要執行的方法的詳細資料收集在一塊兒,ActionMapping由org.apache.struts2.dispatcher.Dispatcher和各類用戶接口組合使用。
ActionMapping的完整類名是
org.apacher.struts2.dispatcher.mapper.ActionMapping.
2.ActionProxy&ActionInvocation
Action的一個代理,由ActionProxyFactory建立,它自己不包括Action實例,默認實現DefaultActionProxy是由ActionInvocation持有Action實例。ActionProxy做用是如何取得Action,不管是本地仍是遠程。而ActionInvocation的做用是如何執行Action,攔截器的功能就是在ActionInvocation中實現的。
ConfigurationProvider&Configuration
ConfigurationProvider就是Struts2中配置文件的解析器,Struts2中的配置文件主要是尤爲實現類XmlConfigurationProvider及其子類
StrutsXmlConfigurationProvider來解析。
3.ActionContext
是action執行的上下文,每個上下文都至關於一個action執行所須要的一組對象的容器,例如session,application,parameters,locale等.
ActionContext包含了大量執行期間有用的環境信息,這些信息由org.apacher.
struts2.dispatcher.Dispatcher類在建立ActionProxy前設置,
並封裝到一個Map對象extraContext中,
Map<String, Object> extraContext = createContextMap(request,
response, mapping, context);
該對象隨後被做爲參數傳遞給ActionProxyFactory的createActionProxy()方法.
ActionProxy proxy = config.getContainer().getInstance(ActionProxyFactory.class).createActionProxy(
namespace, name, method, extraContext, true, false);
ActionContext是線程本地的,這就意味着在ActionContext中存儲的值對於每一個線程都是惟一的,因此對於某個Action,若是要從ActionContext中獲取數據,不須要擔憂線程安全問題。
ActionContext是被存放在當前線程中的,獲取ActionContext也是從ThreadLocal中獲取的。因此在執行攔截器、 action和result的過程當中,因爲他們都是在一個線程中按照順序執行的,因此能夠能夠在任意時候在ThreadLocal中獲取 ActionContext。ActionContext包括了不少信息,好比Session、Application、Request、Locale、ValueStack等,其中 ValueStack能夠解析ognl表達式,來動態後去一些值,同時能夠給表達式提供對象。
ActionContext(com.opensymphony.xwork.ActionContext)是Action執行時的上下文,上下文能夠看做是一個容器 (其實咱們這裏的容器就是一個Map而已),它存放的是Action在執行時須要用到的對象. 通常狀況, 咱們的ActionContext都是經過: ActionContext context = (ActionContext) actionContext.get(); 來獲取的.咱們再來看看這裏的actionContext對象的建立:
static ThreadLocal actionContext = new ActionContextThreadLocal();
ActionContextThreadLocal是實現ThreadLocal的一個內部類.ThreadLocal能夠命名爲"線程局部變量",它爲每個使用該變量的線程都提供一個變量值的副本,使每個線程均可以獨立地改變本身的副本,而不會和其它線程的副本衝突.這樣,咱們 ActionContext裏的屬性只會在對應的當前請求線程中可見,從而保證它是線程安全的.經過ActionContext取得
HttpSession: Map session = ActionContext.getContext().getSession();
(經過Map模擬HttpServlet的對象,操做更方便)
ActionConext的完整類名是:org.opensymphony.xwork2.ActionContext.
4.result
com.opensymphony.xwork2.Result接口表明action執行後的結果。每個action 執行都須要返回一個String類型的結果碼,用於配置result元素列表中選擇對應的result。result在struts.xml文件中進行配置。
Result接口的不一樣實現表明了不一樣類型的結果輸出,Struts2框架提供的Result 實現包括了Servlet轉發,Servlet重定向,Velocity模板輸出,FreeMarker模板輸出,JasperReports(可生成PDF,CVS,XML等)和ActionChainResult(可用於從當前action 到其餘action的鏈式處理)等.
Struts2框架的調用流程:
1>當Servlet容器接收到一個請求以後,將請求交給了在web.xml文件中配置的過濾器 FilterDispatcher,調用它的doFilter()方法。
2>FilterDispatcher詢問ActionMapper,以便確認這個請求是否有對應的action調用.
3>ActionMapper返回一個描述了action調用的ActionMapping對象
4>FilterDispatcher調用Dispatcher類的serviceAction()方法。
5>FilterDispatcher調用ActionProxy的execute()方法。
6>ActionProxy設置ActionInvocation對象的執行上下文,而後調用其invoke()方法。
7>ActionInvocation的invoke()方法從攔截器映射中查找還沒有執行的攔截器,調用它的intercepet(invocation)方法,並將自身對象的引用做爲參數傳遞個攔截器。
8>攔截器完成某些預處理工做後,反過來調用ActionInvocation的invoke()方法,
ActionInvocation維護着本身的狀態,因此它知道哪些攔截器已經被執行,若是尚未執行的攔截器,就繼續執行它的intercept(invocation)方法。
9>若是全部的攔截器都已經執行過了,就調用action實例的execute()方法(若是在struts.xml文件中沒有被設置成其它方法的話).
10>ActionInvocation根據action執行返回的結果碼,查找對應的Result,調用result的execute(invocation),將結果頁面呈現給用戶.
11>ActionInvocation的invoke()方法將控制權返回給攔截器映射中的最後一個攔截器,該攔截器完成全部必須的後期處理工做,而後從intercepter(invocation)返回,容許前一個攔截器執行它本身的後處理工做。如此反覆,直到全部的攔截器都成功返回。
12>ActionInvocation的invoke0方法執行完畢後,向ActionProxy返回一個String類型的結果碼,最後ActionProxy清理狀態並返回.