Hibernate如何提升數據庫查詢的性能

數據庫查詢性能的提升也是涉及到開發中的各個階段,在開發中選用正確的查詢方法無疑是最基礎也最簡單的。

SQL語句的優化

       使用正確的SQL語句可以在很大程度上提高系統的查詢性能。獲得同樣數據而採用不同方式的SQL語句在性能上的差距可能是十分巨大的。

       由於Hibernate是對JDBC的封裝,SQL語句的產生都是動態由Hibernate自動完成的。Hibernate產生SQL語句的方式有兩種:一種是通過開發人員編寫的HQL語句來生成,另一種是依據開發人員對關聯對象的訪問來自動生成相應的SQL語句。

       至於使用什麼樣的SQL語句可以獲得更好的性能要依據數據庫的結構以及所要獲取數據的具體情況來進行處理。在確定了所要執行的SQL語句後,可以通過以下三個方面來影響Hibernate所生成的SQL語句:

●   HQL語句的書寫方法。

●   查詢時所使用的查詢方法。

●   對象關聯時所使用的抓取策略。

使用正確的查詢方法

       在前面已經介紹過,執行數據查詢功能的基本方法有兩種:一種是得到單個持久化對象的get()方法和load()方法,另一種是Query對象的list()方法和iterator()方法。在開發中應該依據不同的情況選用正確的方法。

       get()方法和load()方法的區別在於對二級緩存的使用上。load()方法會使用二級緩存,而get()方法在一級緩存沒有找到的情況下會直接查詢數據庫,不會去二級緩存中查找。在使用中,對使用了二級緩存的對象進行查詢時最好使用load()方法,以充分利用二級緩存來提高檢索的效率。

       list()方法和iterator()方法之間的區別可以從以下幾個方面來進行比較。

●   執行的查詢不同

       list()方法在執行時,是直接運行查詢結果所需要的查詢語句,而iterator()方法則是先執行得到對象ID的查詢,然後再根據每個ID值去取得所要查詢的對象。因此,對於list()方式的查詢通常只會執行一個SQL語句,而對於iterator()方法的查詢則可能需要執行N+1條SQL語句(N爲結果集中的記錄數)。

       iterator()方法只是可能執行N+1條數據,具體執行SQL語句的數量取決於緩存的情況以及對結果集的訪問情況。

●   緩存的使用

       list()方法只能使用二級緩存中的查詢緩存,而無法使用二級緩存對單個對象的緩存(但是會把查詢出的對象放入二級緩存中)。所以,除非重複執行相同的查詢操作,否則無法利用緩存的機制來提高查詢的效率。

       iterator()方法則可以充分利用二級緩存,在根據ID檢索對象的時候會首先到緩存中查找,只有在找不到的情況下才會執行相應的查詢語句。所以,緩存中對象的存在與否會影響到SQL語句的執行數量。

●   對於結果集的處理方法不同

       list()方法會一次獲得所有的結果集對象,而且它會依據查詢的結果初始化所有的結果集對象。這在結果集非常大的時候必然會佔據非常多的內存,甚至會造成內存溢出情況的發生。

       iterator()方法在執行時不會一次初始化所有的對象,而是根據對結果集的訪問情況來初始化對象。因此在訪問中可以控制緩存中對象的數量,以避免佔用過多緩存,導致內存溢出情況的發生。使用iterator()方法的另外一個好處是,如果只需要結果集中的部分記錄,那麼沒有被用到的結果對象根本不會被初始化。所以,對結果集的訪問情況也是調用iterator()方法時執行數據庫SQL語句多少的一個因素。

       所以,在使用Query對象執行數據查詢時應該從以上幾個方面去考慮使用何種方法來執行數據庫的查詢操作。

使用正確的抓取策略

       所謂抓取策略(fetching strategy)是指當應用程序需要利用關聯關係進行對象獲取的時候,Hibernate獲取關聯對象的策略。抓取策略可以在O/R映射的元數據中聲明,也可以在特定的HQL或條件查詢中聲明。

       Hibernate 3定義了以下幾種抓取策略。

●   連接抓取(Join fetching)

       連接抓取是指Hibernate在獲得關聯對象時會在SELECT語句中使用外連接的方式來獲得關聯對象。

●   查詢抓取(Select fetching)

       查詢抓取是指Hibernate通過另外一條SELECT語句來抓取當前對象的關聯對象的方式。這也是通過外鍵的方式來執行數據庫的查詢。與連接抓取的區別在於,通常情況下這個SELECT語句不是立即執行的,而是在訪問到關聯對象的時候纔會執行。

●   子查詢抓取(Subselect fetching)

       子查詢抓取也是指Hibernate通過另外一條SELECT語句來抓取當前對象的關聯對象的方式。與查詢抓取的區別在於它所採用的SELECT語句的方式爲子查詢,而不是通過外連接。

●   批量抓取(Batch fetching)

       批量抓取是對查詢抓取的優化,它會依據主鍵或者外鍵的列表來通過單條SELECT語句實現管理對象的批量抓取。

以上介紹的是Hibernate 3所提供的抓取策略,也就是抓取關聯對象的手段。爲了提升系統的性能,在抓取關聯對象的時機上,還有以下一些選擇。

●   立即抓取(Immediate fetching)

       立即抓取是指宿主對象被加載時,它所關聯的對象也會被立即加載。

●   延遲集合抓取(Lazy collection fetching)

       延遲集合抓取是指在加載宿主對象時,並不立即加載它所關聯的對象,而是到應用程序訪問關聯對象的時候才抓取關聯對象。這是集合關聯對象的默認行爲。

●   延遲代理抓取(Lazy proxy fetching)

       延遲代理抓取是指在返回單值關聯對象的情況下,並不在對其進行get操作時抓取,而是直到調用其某個方法的時候纔會抓取這個對象。

●   延遲屬性加載(Lazy attribute fetching)

       延遲屬性加載是指在關聯對象被訪問的時候才進行關聯對象的抓取。

       介紹了Hibernate所提供的關聯對象的抓取方法和抓取時機,這兩個方面的因素都會影響Hibernate的抓取行爲,最重要的是要清楚這兩方面的影響是不同的,不要將這兩個因素混淆,在開發中要結合實際情況選用正確的抓取策略和合適的抓取時機。

       抓取時機的選擇

       在Hibernate 3中,對於集合類型的關聯在默認情況下會使用延遲集合加載的抓取時機,而對於返回單值類型的關聯在默認情況下會使用延遲代理抓取的抓取時機。

       對於立即抓取在開發中很少被用到,因爲這很可能會造成不必要的數據庫操作,從而影響系統的性能。當宿主對象和關聯對象總是被同時訪問的時候纔有可能會用到這種抓取時機。另外,使用立即連接抓取可以通過外連接來減少查詢SQL語句的數量,所以,也會在某些特殊的情況下使用。

       然而,延遲加載又會面臨另外一個問題,如果在Session關閉前關聯對象沒有被實例化,那麼在訪問關聯對象的時候就會拋出異常。處理的方法就是在事務提交之前就完成對關聯對象的訪問。

       所以,在通常情況下都會使用延遲的方式來抓取關聯的對象。因爲每個立即抓取都會導致關聯對象的立即實例化,太多的立即抓取關聯會導致大量的對象被實例化,從而佔用過多的內存資源。

       抓取策略的選取

       對於抓取策略的選取將影響到抓取關聯對象的方式,也就是抓取關聯對象時所執行的SQL語句。這就要根據實際的業務需求、數據的數量以及數據庫的結構來進行選擇了。

在這裏需要注意的是,通常情況下都會在執行查詢的時候針對每個查詢來指定對其合適的抓取策略。指定抓取策略的方法如下所示:

       User user = (User) session.createCriteria(User.class)

                   .setFetchMode("permissions", FetchMode.JOIN)

                   .add( Restrictions.idEq(userId) )

                   .uniqueResult();

       本文介紹了查詢性能提升的方法,關鍵是如何通過優化SQL語句來提升系統的查詢性能。查詢方法和抓取策略的影響也是通過執行查詢方式和SQL語句的多少來改變系統的性能的。這些都屬於開發人員所應該掌握的基本技能,避免由於開發不當而導致系統性能的低下。

       在性能調整中,除了前面介紹的執行SQL語句的因素外,對於緩存的使用也會影響系統的性能。通常來說,緩存的使用會增加系統查詢的性能,而降低系統增加、修改和刪除操作的性能(因爲要進行緩存的同步處理)。所以,開發人員應該能夠正確地使用有效的緩存來提高數據查詢的性能,而要避免濫用緩存而導致的系統性能變低。在採用緩存的時候也應該注意調整自己的檢索策略和查詢方法,這三者配合起來纔可以達到最優的性能。

       另外,事務的使用策略也會影響到系統的性能。選取正確的事務隔離級別以及使用正確的鎖機制來控制數據的併發訪問都會影響到系統的性能。

posted @ 2009-07-19 21:36 jadmin 閱讀(1) 評論(0) 編輯

Hibernate的性能優化

       Hibernate是對JDBC的輕量級封裝,因此在很多情況下Hibernate的性能比直接使用JDBC存取數據庫要低。然而,通過正確的方法和策略,在使用Hibernate的時候還是可以非常接近直接使用JDBC時的效率的,並且,在有些情況下還有可能高於使用JDBC時的執行效率。

       在進行Hibernate性能優化時,需要從以下幾個方面進行考慮:

●   數據庫設計調整。

●   HQL優化。

●   API的正確使用(如根據不同的業務類型選用不同的集合及查詢API)。

●   主配置參數(日誌、查詢緩存、fetch_size、batch_size等)。

●   映射文件優化(ID生成策略、二級緩存、延遲加載、關聯優化)。

●   一級緩存的管理。

●   針對二級緩存,還有許多特有的策略。

●   事務控制策略。

       數據的查詢性能往往是影響一個應用系統性能的主要因素。對查詢性能的影響會涉及到系統軟件開發的各個階段,例如,良好的設計、正確的查詢方法、適當的緩存都有利於系統性能的提升。

       系統性能的提升設計到系統中的各個方面,是一個相互平衡的過程,需要在應用的各個階段都要考慮。並且在開發、運行的過程中要不斷地調整和優化才能逐步提升系統的性能。

posted @ 2009-07-19 21:30 jadmin 閱讀(1) 評論(0) 編輯

Hibernate查詢方法與緩存的關係

       在前面介紹了Hibernate的緩存技術以及基本的用法,在這裏就具體的Hibernate所提供的查詢方法與Hibernate緩存之間的關係做一個簡單的總結。

       在開發中,通常是通過兩種方式來執行對數據庫的查詢操作的。一種方式是通過ID來獲得單獨的Java對象,另一種方式是通過HQL語句來執行對數據庫的查詢操作。下面就分別結合這兩種查詢方式來說明一下緩存的作用。

       通過ID來獲得Java對象可以直接使用Session對象的load()或者get()方法,這兩種方式的區別就在於對緩存的使用上。

●   load()方法

       在使用了二級緩存的情況下,使用load()方法會在二級緩存中查找指定的對象是否存在。

在執行load()方法時,Hibernate首先從當前Session的一級緩存中獲取ID對應的值,在獲取不到的情況下,將根據該對象是否配置了二級緩存來做相應的處理。

       如配置了二級緩存,則從二級緩存中獲取ID對應的值,如仍然獲取不到則還需要根據是否配置了延遲加載來決定如何執行,如未配置延遲加載則從數據庫中直接獲取。在從數據庫獲取到數據的情況下,Hibernate會相應地填充一級緩存和二級緩存,如配置了延遲加載則直接返回一個代理類,只有在觸發代理類的調用時才進行數據庫的查詢操作。

       在Session一直打開的情況下,並在該對象具有單向關聯維護的時候,需要使用類似Session.clear(),Session.evict()的方法來強制刷新一級緩存。

●   get()方法

       get()方法與load()方法的區別就在於不會查找二級緩存。在當前Session的一級緩存中獲取不到指定的對象時,會直接執行查詢語句從數據庫中獲得所需要的數據。

       在Hibernate中,可以通過HQL來執行對數據庫的查詢操作。具體的查詢是由Query對象的list()和iterator()方法來執行的。這兩個方法在執行查詢時的處理方法存在着一定的差別,在開發中應該依據具體的情況來選擇合適的方法。

●   list()方法

       在執行Query的list()方法時,Hibernate的做法是首先檢查是否配置了查詢緩存,如配置了則從查詢緩存中尋找是否已經對該查詢進行了緩存,如獲取不到則從數據庫中進行獲取。從數據庫中獲取到後,Hibernate將會相應地填充一級、二級和查詢緩存。如獲取到的爲直接的結果集,則直接返回,如獲取到的爲一些ID的值,則再根據ID獲取相應的值(Session.load()),最後形成結果集返回。可以看到,在這樣的情況下,list()方法也是有可能造成N次查詢的。

       查詢緩存在數據發生任何變化的情況下都會被自動清空。

●   iterator()方法

       Query的iterator()方法處理查詢的方式與list()方法是不同的,它首先會使用查詢語句得到ID值的列表,然後再使用Session的load()方法得到所需要的對象的值。

       在獲取數據的時候,應該依據這4種獲取數據方式的特點來選擇合適的方法。在開發中可以通過設置show_sql選項來輸出Hibernate所執行的SQL語句,以此來了解Hibernate是如何操作數據庫的。

posted @ 2009-07-19 21:29 jadmin 閱讀(2) 評論(0) 編輯

Hibernate查詢緩存

查詢緩存

       查詢緩存是專門針對各種查詢操作進行緩存。查詢緩存會在整個SessionFactory的生命週期中起作用,存儲的方式也是採用key-value的形式來進行存儲的。

       查詢緩存中的key是根據查詢的語句、查詢的條件、查詢的參數和查詢的頁數等信息組成的。而數據的存儲則會使用兩種方式,使用SELECT語句只查詢實體對象的某些列或者某些實體對象列的組合時,會直接緩存整個結果集。而對於查詢結果爲某個實體對象集合的情況則只會緩存實體對象的ID值,以達到緩存空間可以共用,節省空間的目的。

       在使用查詢緩存時,除了需要設置hibernate.cache.provider_class參數來啓動二級緩存外,還需要通過hibernate.cache.use_query_cache參數來啓動對查詢緩存的支持。

       另外需要注意的是,查詢緩存是在執行查詢語句的時候指定緩存的方式以及是否需要對查詢的結果進行緩存。

       下面就來了解一下查詢緩存的使用方法及作用。

       修改Hibernate配置文件

       首先需要修改Hibernate的配置文件,增加hibernate.cache.use_query_cache參數的配置。配置方法如下:

       <property name="hibernate.cache.use_query_cache">true</property>

       Hibernate配置文件的詳細內容請參考配套光盤中的hibernate\src\cn\hxex\ hibernate\cache\hibernate.cfg.xml文件。

       編寫主測試程序

       由於這是在前面二級緩存例子的基礎上來開發的,所以,對於EHCache的配置以及視圖對象的開發和映射文件的配置工作就都不需要再重新進行了。下面就來看一下主測試程序的實現方法,如清單14.11所示。

       清單14.11    主程序的實現

……

    public void run() {

           SessionFactory sf = QueryCacheMain.getSessionFactory();

           Session session = sf.getCurrentSession();

           session.beginTransaction();

           Query query = session.createQuery( "from User" );

           Iterator it = query.setCacheable( true ).list().iterator();

           while( it.hasNext() ) {

                  System.out.println( it.next() );

           }

           User user = (User)session.get( User.class, "1" );

           System.out.println( user );

           session.getTransaction().commit();

    }

       public static void main(String[] args) {

              QueryCacheMain main1 = new QueryCacheMain();

              main1.start();

              try {

                     Thread.sleep( 2000 );

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

              QueryCacheMain main2 = new QueryCacheMain();

              main2.start();

       }

}

       主程序在實現的時候採用了多線程的方式來運行。首先將“from User”查詢結果進行緩存,然後再通過ID取得對象來檢查是否對對象進行了緩存。另外,多個線程的執行可以看出對於進行了緩存的查詢是不會執行第二次的。

       運行測試主程序

       接着就來運行測試主程序,其輸出結果應該如下所示:

Hibernate: select user0_.userId as userId0_, user0_.name as name0_, user0_.age as age0_ from USERINFO user0_

ID: 1

Namge:   galaxy

Age:       32

ID: 1

Namge:   galaxy

Age:       32

ID: 1

Namge:   galaxy

Age:       32

ID: 1

Namge:   galaxy

Age:       32

       通過上面的執行結果可以看到,在兩個線程執行中,只執行了一個SQL查詢語句。這是因爲根據ID所要獲取的對象在前面的查詢中已經得到了,並進行了緩存,所以沒有再次執行查詢語句。

posted @ 2009-07-19 21:25 jadmin 閱讀(1) 評論(0) 編輯

Hibernate二級緩存

二級緩存

       與Session相對的是,SessionFactory也提供了相應的緩存機制。SessionFactory緩存可以依據功能和目的的不同而劃分爲內置緩存和外置緩存。

       SessionFactory的內置緩存中存放了映射元數據和預定義SQL語句,映射元數據是映射文件中數據的副本,而預定義SQL語句是在Hibernate初始化階段根據映射元數據推導出來的。SessionFactory的內置緩存是隻讀的,應用程序不能修改緩存中的映射元數據和預定義SQL語句,因此SessionFactory不需要進行內置緩存與映射文件的同步。

       SessionFactory的外置緩存是一個可配置的插件。在默認情況下,SessionFactory不會啓用這個插件。外置緩存的數據是數據庫數據的副本,外置緩存的介質可以是內存或者硬盤。SessionFactory的外置緩存也被稱爲Hibernate的二級緩存。

       Hibernate的二級緩存的實現原理與一級緩存是一樣的,也是通過以ID爲key的Map來實現對對象的緩存。

       由於Hibernate的二級緩存是作用在SessionFactory範圍內的,因而它比一級緩存的範圍更廣,可以被所有的Session對象所共享。

二級緩存的工作內容

       Hibernate的二級緩存同一級緩存一樣,也是針對對象ID來進行緩存。所以說,二級緩存的作用範圍是針對根據ID獲得對象的查詢。

       二級緩存的工作可以概括爲以下幾個部分:

●   在執行各種條件查詢時,如果所獲得的結果集爲實體對象的集合,那麼就會把所有的數據對象根據ID放入到二級緩存中。

●   當Hibernate根據ID訪問數據對象的時候,首先會從Session一級緩存中查找,如果查不到並且配置了二級緩存,那麼會從二級緩存中查找,如果還查不到,就會查詢數據庫,把結果按照ID放入到緩存中。

●   刪除、更新、增加數據的時候,同時更新緩存。

二級緩存的適用範圍

       Hibernate的二級緩存作爲一個可插入的組件在使用的時候也是可以進行配置的,但並不是所有的對象都適合放在二級緩存中。

       在通常情況下會將具有以下特徵的數據放入到二級緩存中:

●   很少被修改的數據。

●   不是很重要的數據,允許出現偶爾併發的數據。

●   不會被併發訪問的數據。

●   參考數據。

       而對於具有以下特徵的數據則不適合放在二級緩存中:

●   經常被修改的數據。

●   財務數據,絕對不允許出現併發。

●   與其他應用共享的數據。

       在這裏特別要注意的是對放入緩存中的數據不能有第三方的應用對數據進行更改(其中也包括在自己程序中使用其他方式進行數據的修改,例如,JDBC),因爲那樣Hibernate將不會知道數據已經被修改,也就無法保證緩存中的數據與數據庫中數據的一致性。

二級緩存組件

       在默認情況下,Hibernate會使用EHCache作爲二級緩存組件。但是,可以通過設置hibernate.cache.provider_class屬性,指定其他的緩存策略,該緩存策略必須實現org.hibernate.cache.CacheProvider接口。

       通過實現org.hibernate.cache.CacheProvider接口可以提供對不同二級緩存組件的支持。

       Hibernate內置支持的二級緩存組件如表14.1所示。

表14.1    Hibernate所支持的二級緩存組件

posted @ 2009-07-19 21:23 jadmin 閱讀(0) 評論(0) 編輯

Hibernate一級緩存

       大家都知道,Hibernate是以JDBC爲基礎實現的持久層組件,因而其性能肯定會低於直接使用JDBC來訪問數據庫。因此,爲了提高Hibernate的性能,在Hibernate組件中提供了完善的緩存機制來提高數據庫訪問的性能。

       什麼是緩存

       緩存是介於應用程序和物理數據之間的,其作用是爲了降低應用程序對物理數據訪問的頻次從而提高應用系統的性能。緩存思想的提出主要是因爲對物理數據的訪問效率要遠遠低於對內存的訪問速度,因而採用了將部分物理數據存放於內存當中,這樣可以有效地減少對物理數據的訪問次數,從而提高系統的性能。

       緩存廣泛地存在於我們所接觸的各種應用系統中,例如數據庫系統、Windows操作系統等,在進行物理數據的訪問時無一例外地都使用了緩存機制來提高操作的性能。

       緩存內的數據是對物理數據的複製,因此一個緩存系統所應該包括的最基本的功能是數據的緩存和讀取,同時在使用緩存的時候還要考慮緩存中的數據與物理數據的同步,也就是要保持兩者是一致的。

       緩存要求對數據的讀寫速度很高,因此,一般情況下會選用內存作爲存儲的介質。但如果內存有限,並且緩存中存放的數據量非常大時,也會用硬盤作爲緩存介質。緩存的實現不僅僅要考慮存儲的介質,還要考慮到管理緩存的併發訪問和緩存數據的生命週期。

       爲了提高系統的性能,Hibernate也使用了緩存的機制。在Hibernate框架中,主要包括以下兩個方面的緩存:一級緩存和二級緩存(包含查詢緩存)。Hibernate中緩存的作用主要表現在以下兩個方面:

●   通過主鍵(ID)加載數據的時候

●   延遲加載中

一級緩存

       Hibernate的一級緩存是由Session提供的,因此它只存在於Session的生命週期中,也就是當Session關閉的時候該Session所管理的一級緩存也會立即被清除。

       Hibernate的一級緩存是Session所內置的,不能被卸載,也不能進行任何配置。

       一級緩存採用的是key-value的Map方式來實現的,在緩存實體對象時,對象的主關鍵字ID是Map的key,實體對象就是對應的值。所以說,一級緩存是以實體對象爲單位進行存儲的,在訪問的時候使用的是主關鍵字ID。

       雖然,Hibernate對一級緩存使用的是自動維護的功能,沒有提供任何配置功能,但是可以通過Session中所提供的方法來對一級緩存的管理進行手工干預。Session中所提供的干預方法包括以下兩種。

●   evict() :用於將某個對象從Session的一級緩存中清除。

●   clear() :用於將一級緩存中的對象全部清除。

       在進行大批量數據一次性更新的時候,會佔用非常多的內存來緩存被更新的對象。這時就應該階段性地調用clear()方法來清空一級緩存中的對象,控制一級緩存的大小,以避免產生內存溢出的情況。具體的實現方法如清單14.8所示。

       清單14.8    大批量更新時緩存的處理方法

Session session = sessionFactory.openSession();

Transaction tx = session.beginTransaction();

  

for ( int i=0; i<100000; i++ ) {

    Customer customer = new Customer(……);

    session.save(customer);

    if ( i % 20 == 0 ) {

        //將本批插入的對象立即寫入數據庫並釋放內存

        session.flush();

        session.clear();

    }

}

  

tx.commit();

session.close();

posted @ 2009-07-19 21:18 jadmin 閱讀(0) 評論(0) 編輯

併發控制、悲觀鎖、樂觀鎖

併發控制

       當數據庫系統採用Read Committed隔離級別時,會導致不可重複讀取和兩次更新丟失的併發問題,可以在應用程序中採用鎖機制來避免這類問題的產生。

       從應用程序的角度上看,鎖可以分爲樂觀鎖和悲觀鎖兩大類。

悲觀鎖

       在多個客戶端可能讀取同一筆數據或同時更新一筆數據的情況下,必須要有訪問控制的手段,防止同一個數據被修改而造成混亂,最簡單的手段就是對數據進行鎖定。在自己進行數據讀取或更新等動作時,鎖定其他客戶端不能對同一筆數據進行任何的動作。

       悲觀鎖(Pessimistic Locking),如其名稱所示,悲觀地認定每次資料存取時,其他的客戶端也會存取同一筆數據,因此將會鎖住該筆數據,直到自己操作完成後再解除鎖。

       悲觀鎖假定任何時刻存取數據時,都可能有另一個客戶也正在存取同一筆數據,因而對數據採取了數據庫層次的鎖定狀態,在鎖定的時間內其他的客戶不能對數據進行存取。對於單機或小系統而言,這並不成問題,然而如果是在網絡上的系統,同時間會有許多訪問的機器,如果每一次讀取數據都造成鎖定,其後繼的存取就必須等待,這將造成效能上的問題,造成後繼使用者的長時間等待。

       悲觀鎖通常透過系統或數據庫本身的功能來實現,依賴系統或數據庫本身提供的鎖機制。Hibernate即是如此,可以利用Query或Criteria的setLockMode()方法來設定要鎖定的表或列及其鎖模式,可設定的鎖模式有以下幾個。

       LockMode.UPGRADE:利用數據庫的for update子句進行鎖定。

       LockMode.UPGRADE_NOWAIT:使用for update nowait子句進行鎖定,在Oracle數據庫中使用。

       下面來實現一個簡單的例子,測試一下采用悲觀鎖時數據庫是如何進行操作的。

       首先來完成一個實體對象——User,該對象包含了id,name和age三個屬性,實現的方法如清單14.1所示。

       清單14.1    User對象的實現

package cn.hxex.hibernate.lock;

public class User {

       private String id;

       private String name;

       private Integer age;

      

       // 省略了getter和setter方法

       ……

}

       接下來就是映射文件的配置,由於該映射文件沒有涉及到任何與其他對象的關聯配置,所以實現的方法也非常簡單,代碼如清單14.2所示。

       清單14.2    User映射文件的實現

<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC

       "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

       "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="cn.hxex.hibernate.lock">

       <class name="User" table="USERINFO">

              <id name="id" column="userId">

                 <generator class="uuid.hex"/>

           </id>

             

              <property name="name" column="name" type="java.lang.String"/>

              <property name="age" column="age" type="java.lang.Integer"/>

       </class>

</hibernate-mapping>

       另外一件重要的工作就是Hibernate的配置文件了,在這個配置文件中包含了連接數據庫的參數以及其他一些重要的參數,實現的方法如清單14.3所示。

       清單14.3    Hibernate配置文件的實現

<?xml version='1.0' encoding='UTF-8'?>

<!DOCTYPE hibernate-configuration PUBLIC

          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"

          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

       <session-factory>

              <!-- 數據庫的URL -->

              <!-- property name="hibernate.connection.url">

              jdbc:oracle:thin:@192.168.10.121:1521:HiFinance</property-->

              <property name="hibernate.connection.url">

       jdbc:mysql://localhost:3306/lockdb?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;autoReconnectForPools=true

        </property>

             

              <!-- 數據庫的驅動程序 -->

              <!-- property name="hibernate.connection.driver_class">

              oracle.jdbc.driver.OracleDriver</property-->

              <property name="hibernate.connection.driver_class">

           com.mysql.jdbc.Driver

        </property>

              <!-- 數據庫的用戶名 -->

              <property name="hibernate.connection.username">lockdb</property>

              <!-- 數據庫的密碼 -->

              <property name="hibernate.connection.password">lockdb</property>

              <!-- 數據庫的Dialect -->

              <!-- property name="hibernate.dialect">

              org.hibernate.dialect.Oracle9Dialect</property -->

              <property name="hibernate.dialect">

              org.hibernate.dialect.MySQLDialect</property>

              <!-- 輸出執行的SQL語句 -->

              <property name="hibernate.show_sql">true</property>

             

              <property name="hibernate.current_session_context_class">thread</property>

             

              <property name="hibernate.hbm2ddl.auto">update</property>

              <!-- HBM文件列表 -->

              <mapping resource="cn/hxex/hibernate/lock/User.hbm.xml" />

       </session-factory>

</hibernate-configuration>

       最後要實現的就是測試主程序了,在測試主程序中包含了Hibernate的初始化代碼以及悲觀鎖的測試方法。測試主程序的實現方法如清單14.4所示。

       清單14.4    測試主程序的實現

package cn.hxex.hibernate.lock;

import java.net.URL;

import java.util.List;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.hibernate.LockMode;

import org.hibernate.Query;

import org.hibernate.Session;

import org.hibernate.SessionFactory;

import org.hibernate.cfg.Configuration;

public class LockMain {

    private static Log log = LogFactory.getLog( LockMain.class );

    // 靜態Configuration和SessionFactory對象的實例(全局唯一的)

    private static Configuration configuration;

    private static SessionFactory sessionFactory;

    static

    {

        // 從默認的配置文件創建SessionFactory

        try

        {

                          URL configURL = ClassLoader.getSystemResource(

                               "cn/hxex/hibernate/lock/hibernate.cfg.xml" );

                          // 創建默認的Configuration對象的實例

            configuration = new Configuration();

            // 讀取hibernate.properties或者hibernate.cfg.xml文件

            configuration.configure( configURL );

            // 使用靜態變量來保持SessioFactory對象的實例

            sessionFactory = configuration.buildSessionFactory();

        }

        catch (Throwable ex)

        {

            // 輸出異常信息

            log.error("Building SessionFactory failed.", ex);

            ex.printStackTrace();

            throw new ExceptionInInitializerError(ex);

        }

    }

      

    public static SessionFactory getSessionFactory() {

                  return sessionFactory;

    }

   

    public void testPessimisticLock() {

              SessionFactory sf = LockMain.getSessionFactory();

              Session session = sf.getCurrentSession();

              session.beginTransaction();

             

              Query query = session.createQuery("from User user");

              query.setLockMode("user", LockMode.UPGRADE);

              List users = query.list();

              for( int i=0; i<users.size(); i++ ) {

                     System.out.println( users.get( i ) );

              }

              session.getTransaction().commit();

    }

   

       public static void main(String[] args) {

             

              LockMain main = new LockMain();

              main.testPessimisticLock();

       }

}

       在上面的清單中,testPessimisticLock()方法就是測試悲觀鎖的方法,該方法在執行查詢之前通過Query對象的setLockMode()方法設置了訪問User對象的模式,這樣,這個程序在執行的時候就會使用以下的SQL語句:

       select user0_.userId as userId0_, user0_.name as name0_, user0_.age as age0_

       from USERINFO user0_ for update

       除了Query對象外,也可以在使用Session的load()或是lock()時指定鎖模式。

       除了前面所提及的兩種鎖模式外,還有三種Hibernate內部自動對數據進行加鎖的模式,但它的處理是與數據庫無關的。

       LockMode.WRITE:在insert或update時進行鎖定,Hibernate會在調用save()方法時自動獲得鎖。

       LockMode.READ:在讀取記錄時Hibernate會自動獲得鎖。

       LockMode.NONE:沒有鎖。

       如果數據庫不支持所指定的鎖模式,Hibernate會選擇一個合適的鎖替換,而不是拋出一個異常。

樂觀鎖

       樂觀鎖(Optimistic Locking)認爲資料的存取很少發生同時存取的問題,因而不做數據庫層次上的鎖定。爲了維護正確的數據,樂觀鎖是使用應用程序上的邏輯來實現版本控制的。

在使用樂觀鎖策略的情況下,數據不一致的情況一旦發生,有幾個解決方法,一種是先更新爲主,一種是後更新爲主,比較複雜的就是檢查發生變動的數據來實現,或是檢查所有屬性來實現樂觀鎖。

       Hibernate中通過檢查版本號來判斷數據是否已經被其他人所改動,這也是Hibernate所推薦的方式。在數據庫中加入一個version字段記錄,在讀取數據時連同版本號一同讀取,並在更新數據時比較版本號與數據庫中的版本號,如果等於數據庫中的版本號則予以更新,並遞增版本號,如果小於數據庫中的版本號就拋出異常。

       下面就來在前面例子的基礎上進行Hibernate樂觀鎖的測試。

       首先需要修改前面所實現的業務對象,在其中增加一個version屬性,用來記錄該對象所包含數據的版本信息,修改後的User對象如清單14.5所示。

       清單14.5    修改後的User對象

package cn.hxex.hibernate.lock;

public class User {

       private String id;

       private Integer version; // 增加版本屬性

       private String name;

       private Integer age;

      

       // 省略了getter和setter方法

       ……

}

       然後是修改映射文件,增加version屬性的配置。在這裏需要注意的是,這裏的version屬性應該使用專門的<version>元素來進行配置,這樣才能使其發揮樂觀鎖的作用。如果還使用<property>元素來進行配置,那麼Hibernate只會將其作爲一個普通的屬性來進行處理。

修改後的映射文件如清單14.6所示。

       清單14.6    修改後的映射文件

<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC

       "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

       "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="cn.hxex.hibernate.lock">

       <class name="User" table="USERINFO" optimistic-lock="version">

              <id name="id" column="userId">

                 <generator class="uuid.hex"/>

           </id>

             

              <version name="version" column="version" type="java.lang.Integer"/>

             

              <property name="name" column="name" type="java.lang.String"/>

              <property name="age" column="age" type="java.lang.Integer"/>

       </class>

</hibernate-mapping>

       接下來還要進行測試主程序的修改。由於需要模擬兩個人同時修改同一個記錄的情況,所以在這裏需要將主程序修改爲是可以多線程執行的,然後在run()方法中,調用對User對象的修改程序。

       實現後的主測試程序如清單14.7所示。

       清單14.7    修改後的測試主程序

package cn.hxex.hibernate.lock;

import java.net.URL;

import java.util.List;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.hibernate.LockMode;

import org.hibernate.Query;

import org.hibernate.Session;

import org.hibernate.SessionFactory;

import org.hibernate.Transaction;

import org.hibernate.cfg.Configuration;

public class LockMain extends Thread{

……

    public void testOptimisticLock() {

           SessionFactory sf = LockMain.getSessionFactory();

           Session session = sf.openSession();

           Transaction tx = session.beginTransaction();

          

           User userV1 = (User)session.load( User.class, "1" );

          

           // 等第二個進程執行

           try {

                     sleep( 3000 );

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

          

              userV1.setAge(new Integer(32));

              tx.commit();

              session.close();

    }

   

    public void run() {

                  testOptimisticLock();

    }

   

       public static void main(String[] args) {

             

              // LockMain main = new LockMain();

              // main.testPessimisticLock();

             

              LockMain main1 = new LockMain();

              main1.start();

              LockMain main2 = new LockMain();

              main2.start();

       }

}

       最後,執行測試主程序,在控制檯中應該看到類似下面的輸出:

Hibernate: select user0_.userId as userId0_0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from USERINFO user0_ where user0_.userId=?

Hibernate: select user0_.userId as userId0_0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from USERINFO user0_ where user0_.userId=?

Hibernate: update USERINFO set version=?, name=?, age=? where userId=? and version=?

Hibernate: update USERINFO set version=?, name=?, age=? where userId=? and version=?

2006-10-3 21:27:20 org.hibernate.event.def.AbstractFlushingEventListener performExecutions

嚴重: Could not synchronize database state with session

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [cn.hxex.hibernate.lock.User#1]

……

       在Hibernate所執行的UPDATE語句中可以看到,version字段是作爲更新的條件來執行的。對於第二個進程來說,由於數據庫中的記錄已經被第一個進程更新(更新的同時會導致version自動增加),就必然會導致第二個進程操作的失敗。Hibernate正是利用這種機制來避免兩次更新問題的出現。

posted @ 2009-07-19 21:11 jadmin 閱讀(6) 評論(0) 編輯

Hibernate中的事務處理

       在現在的B/S體系結構的軟件開發中,對於數據庫事務處理中最常使用的方式是每個用戶請求一個事務。也就是說,當服務器端接收到一個用戶請求後,會開始一個新的事務,直到對用戶請求的所有處理都進行完畢並且完成了響應用戶請求的所有輸出之後纔會關閉這個事務。

       對於使用Hibernate實現持久化功能的系統來說,事務的處理是這樣的:服務器端在接收到用戶的請求後,會創建一個新的Hibernate Session對象,然後通過該Session對象開始一個新的事務並且之後所有對數據庫的操作都通過該Session對象來進行。最後,完成將響應頁面發送到客戶端的工作後再提交事務並且關閉Session。

       Session的對象是輕型的,非線程安全的,所以在每次用戶請求時創建,請求處理完畢後丟棄。

       那麼,該如何實現這種方式的事務處理呢?處理的難點在於如何在業務處理之前創建Session並開始事務以及在業務處理之後提交事務並關閉Session。對於現在的Web應用來說,通常情況下是通過ServletFilter來完成事務處理的操作。這樣,就可以輕鬆地實現在用戶請求到達服務器端的時候創建Session並開始事務,而服務器端響應處理結束之前提交事務並關閉Session。

       另外一個問題是,在ServletFilter中創建的Session是如何傳遞給業務處理方法中的呢?處理的方法是通過一個ThreadLocal變量來把創建的Session對象綁定到處理用戶請求的線程上去,這樣就可以使任何的業務處理方法可以輕鬆得到Session對象。

       Hibernate中事務處理的具體方法可以參照前面的網絡博客的實例。

       但是這種事務處理的方式還是會遇到一些問題,其中最突出的就是更新衝突的問題。例如,某個操作人員進入了用戶信息的修改頁面,在經過一段時間的對用戶信息的修改後,進行提交操作,而與此同時可能會有另外一個操作人員也進行了相同的操作,這樣在處理提交的時候就會產生衝突。

       產生這個衝突的原因在於在開發中需要使用多個數據庫事務來實現一個應用事務。也就是說,在應用程序層,應該將讀取用戶信息、顯示修改頁面以及用戶提交工作來作爲一個事務進行處理,在處理的過程中應該避免其他操作人員進行類似的操作。

       回想前面的介紹,我們對於數據庫事務所採取的策略是每個用戶請求一個事務,而上面的業務處理則至少需要兩個請求才能完成。這樣,兩者之間就存在着一定的矛盾,這也就導致了不可重複讀取和兩次更新問題的發生。

       爲了解決併發中數據訪問的問題,通常會採用鎖的機制來實現數據訪問的排他性,從而避免兩次更新問題的發生。

posted @ 2009-07-19 21:07 jadmin 閱讀(0) 評論(0) 編輯

事務的隔離級別

       爲了避免上面出現的幾種情況,在標準SQL規範中,定義了4個事務隔離級別,不同的隔離級別對事務的處理不同。

●   未授權讀取(Read Uncommitted):允許髒讀取,但不允許更新丟失。如果一個事務已經開始寫數據,則另外一個數據則不允許同時進行寫操作,但允許其他事務讀此行數據。該隔離級別可以通過“排他寫鎖”實現。

●   授權讀取(Read Committed):允許不可重複讀取,但不允許髒讀取。這可以通過“瞬間共享讀鎖”和“排他寫鎖”實現。讀取數據的事務允許其他事務繼續訪問該行數據,但是未提交的寫事務將會禁止其他事務訪問該行。

●   可重複讀取(Repeatable Read):禁止不可重複讀取和髒讀取,但是有時可能出現幻影數據。這可以通過“共享讀鎖”和“排他寫鎖”實現。讀取數據的事務將會禁止寫事務(但允許讀事務),寫事務則禁止任何其他事務。

●   序列化(Serializable):提供嚴格的事務隔離。它要求事務序列化執行,事務只能一個接着一個地執行,但不能併發執行。如果僅僅通過“行級鎖”是無法實現事務序列化的,必須通過其他機制保證新插入的數據不會被剛執行查詢操作的事務訪問到。

       隔離級別越高,越能保證數據的完整性和一致性,但是對併發性能的影響也越大。對於多數應用程序,可以優先考慮把數據庫系統的隔離級別設爲Read Committed,它能夠避免髒讀取,而且具有較好的併發性能。儘管它會導致不可重複讀、虛讀和第二類丟失更新這些併發問題,在可能出現這類問題的個別場合,可以由應用程序採用悲觀鎖或樂觀鎖來控制。

       通過前面的介紹已經知道,通過選用不同的隔離等級就可以在不同程度上避免前面所提及的在事務處理中所面臨的各種問題。所以,數據庫隔離級別的選取就顯得尤爲重要,在選取數據庫的隔離級別時,應該注意以下幾個處理的原則:

       首先,必須排除“未授權讀取”,因爲在多個事務之間使用它將會是非常危險的。事務的回滾操作或失敗將會影響到其他併發事務。第一個事務的回滾將會完全將其他事務的操作清除,甚至使數據庫處在一個不一致的狀態。很可能一個已回滾爲結束的事務對數據的修改最後卻修改提交了,因爲“未授權讀取”允許其他事務讀取數據,最後整個錯誤狀態在其他事務之間傳播開來。

       其次,絕大部分應用都無須使用“序列化”隔離(一般來說,讀取幻影數據並不是一個問題),此隔離級別也難以測量。目前使用序列化隔離的應用中,一般都使用悲觀鎖,這樣強行使所有事務都序列化執行。

       剩下的也就是在“授權讀取”和“可重複讀取”之間選擇了。我們先考慮可重複讀取。如果所有的數據訪問都是在統一的原子數據庫事務中,此隔離級別將消除一個事務在另外一個併發事務過程中覆蓋數據的可能性(第二個事務更新丟失問題)。這是一個非常重要的問題,但是使用可重複讀取並不是解決問題的唯一途徑。

       假設使用了“版本數據”,Hibernate會自動使用版本數據。Hibernate的一級Session緩存和版本數據已經爲你提供了“可重複讀取隔離”絕大部分的特性。特別是,版本數據可以防止二次更新丟失的問題,一級Session緩存可以保證持久載入數據的狀態與其他事務對數據的修改隔離開來,因此如果使用對所有的數據庫事務採用授權讀取隔離和版本數據是行得通的。

       “可重複讀取”爲數據庫查詢提供了更好的效率(僅對那些長時間的數據庫事務),但是由於幻影讀取依然存在,因此沒必要使用它(對於Web應用來說,一般也很少在一個數據庫事務中對同一個表查詢兩次)。

       也可以同時考慮選擇使用Hibernate的二級緩存,它可以如同底層的數據庫事務一樣提供相同的事務隔離,但是它可能弱化隔離。假如在二級緩存大量使用緩存併發策略,它並不提供重複讀取語義(例如,後面章節中將要討論的讀寫,特別是非嚴格讀寫),很容易可以選擇默認的隔離級別:因爲無論如何都無法實現“可重複讀取”,因此就更沒有必要拖慢數據庫了。另一方面,可能對關鍵類不採用二級緩存,或者採用一個完全的事務緩存,提供“可重複讀取隔離”。那麼在業務中需要使用到“可重複讀取”嗎?如果你喜歡,當然可以那樣做,但更多的時候並沒有必要花費這個代價。

posted @ 2009-07-19 21:04 jadmin 閱讀(0) 評論(0) 編輯

Hibernate事務

       數據庫的事務處理是在進行數據庫應用開發中必須進行處理的一個問題。那麼對於選擇Hibernate作爲持久層組件,瞭解Hibernate的事務處理機制就顯得尤爲重要了。

事務的基本概念

       事務(Transaction)是併發控制的基本單位。所謂的事務,它是一個操作序列,這些操作要麼都執行,要麼都不執行,它是一個不可分割的工作單位。例如,銀行轉賬工作:從一個賬號扣款並使另一個賬號增款,這兩個操作要麼都執行,要麼都不執行。所以,應該把它們看成一個事務。事務是數據庫維護數據一致性的單位,在每個事務結束時,都能保持數據一致性。

       針對上面的描述可以看出,事務的提出主要是爲了解決併發情況下保持數據一致性的問題。

       事務具有以下4個基本特徵。

●   Atomic(原子性):事務中包含的操作被看做一個邏輯單元,這個邏輯單元中的操作要麼全部成功,要麼全部失敗。

●   Consistency(一致性):只有合法的數據可以被寫入數據庫,否則事務應該將其回滾到最初狀態。

●   Isolation(隔離性):事務允許多個用戶對同一個數據進行併發訪問,而不破壞數據的正確性和完整性。同時,並行事務的修改必須與其他並行事務的修改相互獨立。

●   Durability(持久性):事務結束後,事務處理的結果必須能夠得到固化。

       數據庫肯定是要被廣大客戶所共享訪問的,那麼在數據庫操作過程中很可能出現以下幾種不確定情況。

●   更新丟失(Lost update):兩個事務都同時更新一行數據,但是第二個事務卻中途失敗退出,導致對數據的兩個修改都失效了。這是因爲系統沒有執行任何的鎖操作,因此併發事務並沒有被隔離開來。

●   髒讀取(Dirty Reads):一個事務開始讀取了某行數據,但是另外一個事務已經更新了此數據但沒有能夠及時提交。這是相當危險的,因爲很可能所有的操作都被回滾。

●   不可重複讀取(Non-repeatable Reads):一個事務對同一行數據重複讀取兩次,但是卻得到了不同的結果。例如,在兩次讀取的中途,有另外一個事務對該行數據進行了修改,並提交。

●   兩次更新問題(Second lost updates problem):無法重複讀取的特例。有兩個併發事務同時讀取同一行數據,然後其中一個對它進行修改提交,而另一個也進行了修改提交。這就會造成第一次寫操作失效。

●   虛讀(Phantom Reads):事務在操作過程中進行兩次查詢,第二次查詢的結果包含了第一次查詢中未出現的數據(這裏並不要求兩次查詢的SQL語句相同)。這是因爲在兩次查詢過程中有另外一個事務插入數據造成的。

posted @ 2009-07-19 21:04 jadmin 閱讀(2) 評論(0) 編輯

Spring整合Hibernate(2)

6.5.4 使用HibernateCallBack

HibernateTemplate還提供了一種更加靈活的方式來操作數據庫,通過這種方式可以完全使用Hibernate的操作方式。HibernateTemplate的靈活訪問方式可通過如下兩個方法完成:

   ● Object execute(HibernateCallback action)。

   ● List execute(HibernateCallback action)。

這兩個方法都需要一個HibernateCallback的實例,HibernateCallback實例可在任何有效的Hibernate數據訪問中使用。程序開發者通過HibernateCallback,可以完全使用Hibernate靈活的方式來訪問數據庫,解決Spring封裝Hibernate後靈活性不足的缺陷。

HibernateCallback是一個接口,該接口包含一個方法doInHibernate(org.hibernate. Session session),該方法只有一個參數Session。在開發中提供HibernateCallback實現類時,必須實現接口裏包含的doInHibernate方法,在該方法體內即可獲得Hibernate Session的引用,一旦獲得了Hibernate Session的引用,就可以完全以Hibernate的方式進行數據庫訪問。

注意:doInHibernate方法內可以訪問Session,該Session對象是綁定在該線程的Session實例。該方法內的持久層操作,與不使用Spring時的持久層操作完全相同。這保證了對於複雜的持久層訪問,依然可以使用Hibernate的訪問方式。

下面的代碼對HibernateDaoSupport類進行擴展(雖然Spring 2.0的HibernateTemplate提供了一個分頁方法setMaxResults,但僅此一個方法依然不能實現分頁查詢),這種擴展主要是爲該類增加了3個分頁查詢的方法,分頁查詢時必須直接調用Hibernate的Session完成,因此,必須藉助於HibernateCallBack的幫助。

public class YeekuHibernateDaoSupport extends HibernateDaoSupport

{

    /**

    * 使用hql 語句進行分頁查詢操作

    * @param hql 需要查詢的hql語句

    * @param offset 第一條記錄索引

    * @param pageSize 每頁需要顯示的記錄數

    * @return 當前頁的所有記錄

    */

    public List findByPage(final String hql,

        final int offset, final int pageSize)

    {

        //HibernateDaoSupport已經包含了getHibernateTemplate()方法

        List list = getHibernateTemplate().executeFind(new
        HibernateCallback()

            {

                public Object doInHibernate(Session session)

                    throws HibernateException, SQLException

                //該方法體內以Hibernate方法進行持久層訪問

                {

                    List result = session.createQuery(hql)

                                        .setFirstResult(offset)

                                         .setMaxResults(pageSize)

                                        .list();

                    return result;

                }

            });

        return list;

    }

    /**

    * 使用hql 語句進行分頁查詢操作

    * @param hql 需要查詢的hql語句

    * @param value 如果hql有一個參數需要傳入,value就是傳入的參數

    * @param offset 第一條記錄索引

    * @param pageSize 每頁需要顯示的記錄數

    * @return 當前頁的所有記錄

    */

    public List findByPage(final String hql , final Object value ,

        final int offset, final int pageSize)

    {

        List list = getHibernateTemplate().executeFind(new
        HibernateCallback()

            {

                public Object doInHibernate(Session session)

                    throws HibernateException, SQLException

                {

                    //下面查詢的是最簡單的Hiberante HQL查詢

                    List result = session.createQuery(hql)

                                         .setParameter(0, value)

                                        .setFirstResult(offset)

                                         .setMaxResults(pageSize)

                                        .list();

                    return result;

                }

            });

        return list;

    }

    /**

    * 使用hql 語句進行分頁查詢操作

    * @param hql 需要查詢的hql語句

    * @param values 如果hql有多個參數需要傳入,values就是傳入的參數數組

    * @param offset 第一條記錄索引

    * @param pageSize 每頁需要顯示的記錄數

    * @return 當前頁的所有記錄

    */

    public List findByPage(final String hql, final Object[] values,

        final int offset, final int pageSize)

    {

        List list = getHibernateTemplate().executeFind(new
        HibernateCallback()

            {

                public Object doInHibernate(Session session)

                    throws HibernateException, SQLException

                {

                    Query query = session.createQuery(hql);

                    for (int i = 0 ; i < values.length ; i++)

                    {

                        query.setParameter( i, values[i]);

                    }

                    List result = query.setFirstResult(offset)

                                       .setMaxResults(pageSize)

                                       .list();

                    return result;

                }

            });

        return list;

    }

}

在上面的代碼實現中,直接使用了getHibernateTemplate()方法,這個方法由Hibernate- DaoSupport提供。而YeekuHibernateDaoSupport是HibernateDaoSupport的子類,因此,可以直接使用該方法。

當實現doInHibernate(Session session)方法時,完全以Hibernate的方式進行數據庫訪問,這樣保證了Hibernate進行數據庫訪問的靈活性。

注意:Spring提供的XxxTemplate和XxxCallBack互爲補充,二者體現了Spring框架設計的用心良苦:XxxTemplate對通用操作進行封裝,而XxxCallBack解決了封裝後靈活性不足的缺陷。

6.5.5 實現DAO組件

爲了實現DAO組件,Spring提供了大量的XxxDaoSupport類,這些DAO支持類對於實現DAO組件大有幫助,因爲這些DAO支持類已經完成了大量基礎性工作。

Spring爲Hibernate的DAO提供了工具類HibernateDaoSupport。該類主要提供如下兩個方法以方便DAO的實現:

   ● public final HibernateTemplate getHibernateTemplate()。

   ● public final void setSessionFactory(SessionFactory sessionFactory)。

其中,setSessionFactory方法可用於接收Spring的ApplicationContext的依賴注入,可接收配置在Spring的SessionFactory實例,getHibernateTemplate方法用於返回通過SessionFactory產生的HibernateTemplate實例,持久層訪問依然通過HibernateTemplate實例完成。

下面實現的DAO組件繼承了Spring提供的HibernateDaoSupport類,依然實現了PersonDao接口,其功能與前面提供的PersonDao實現類完全相同。其代碼如下:

public class PersonDaoHibernate extends HibernateDaoSupport implements PersonDao

{

    /**

     * 加載人實例

     * @param id 需要加載的Person實例的主鍵值

     * @return 返回加載的Person實例

     */

    public Person get(int id)

    {

        return (Person)getHibernateTemplate().get(Person.class, new
        Integer(id));

    }

    /**

     * 保存人實例

     * @param person 需要保存的Person實例

     */   

    public void save(Person person)

    {

        getHibernateTemplate().save(person);

    }

    /**

     * 修改Person實例

     * @param person 需要修改的Person實例

     */

    public void update(Person person)

    {

        getHibernateTemplate().update(person);

    }

    /**

     * 刪除Person實例

     * @param id 需要刪除的Person的id

     */

    public void delete(int id)

    {

        getHibernateTemplate().delete(getHibernateTemplate(). 
        get(Person.class, new Integer(id)));

    }

    /**

     * 刪除Person實例

     * @param person 需要刪除的Person實例

     */

    public void delete(Person person)

    {

        getHibernateTemplate().delete(person);

    }

    /**

     * 根據用戶名查找Person

     * @param name 用戶名

     * @return 用戶名對應的全部用戶

     */

    public List findByPerson(String name)

    {

        return getHibernateTemplate().find("from Person p where p.name 
        like ?" , name);       

    }

    /**

    * 返回全部的Person實例

    * @return 全部的Person實例

    */

    public List findAllPerson()

    {

        return getHibernateTemplate().find("from Person ");

    }

}

上面的代碼與前面的PersonDAOImpl對比會發現,代碼量大大減少。事實上,DAO的實現依然藉助於HibernateTemplate的模板訪問方式,只是HibernateDaoSupport將依賴注入SessionFactory的工作已經完成,獲取HibernateTemplate的工作也已完成。該DAO的配置必須依賴於SessionFactory,配置文件與前面部署DAO組件的方式完全相同,此處不再贅述。

在繼承HibernateDaoSupport的DAO實現裏,Hibernate Session的管理完全不需要打開代碼,而由Spring來管理。Spring會根據實際的操作,採用“每次事務打開一次session”的策略,自動提高數據庫訪問的性能。

6.5.6 使用IoC容器組裝各種組件

至此爲止,J2EE應用所需要的各種組件都已經出現了,從MVC層的控制器組件,到業務邏輯組件,以及持久層的DAO組件,已經全部成功實現。應用程序代碼並未將這些組件耦合在一起,代碼中都是面向接口編程,因此必須利用Spring的IoC容器將他們組合在一起。

從用戶角度來看,用戶發出HTTP請求,當MVC框架的控制器組件攔截到用戶請求時,將調用系統的業務邏輯組件,而業務邏輯組件則調用系統的DAO組件,而DAO組件則依賴於SessionFactory和DataSource等底層組件實現數據庫訪問。

從系統實現角度來看,IoC容器先創建SessionFactory和DataSource等底層組件,然後將這些底層組件注入給DAO組件,提供一個完整的DAO組件,並將此DAO組件注入給業務邏輯組件,從而提供一個完整的業務邏輯組件,而業務邏輯組件又被注入給控制器組件,控制器組件負責攔截用戶請求,並將處理結果呈現給用戶——這一系列的銜接都由Spring的IoC容器提供實現。

下面給出關於如何在容器中配置J2EE組件的大致模板,其模板代碼如下:

<?xml version="1.0" encoding="GBK"?>

<!-- beans是Spring配置文件的根元素,並且指定了Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
    destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="com.mysql.jdbc.Driver"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="jdbc:mysql://localhost/j2ee"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="root"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="32147"/>

        <!-- 指定連接數據庫連接池的最大連接數 -->

        <property name="maxPoolSize" value="40"/>

        <!-- 指定連接數據庫連接池的最小連接數 -->

        <property name="minPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的初始化連接數 -->

        <property name="initialPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的連接最大空閒時間 -->

        <property name="maxIdleTime" value="20"/>

    </bean>

    <!-- 定義Hibernate的SessionFactory Bean -->

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3. 
    LocalSessionFactoryBean">

        <!-- 依賴注入數據源,注入的正是上文中定義的dataSource -->

        <property name="dataSource" ref="dataSource"/>

        <!-- mappingResources屬性用來列出全部映射文件 -->

        <property name="mappingResources">

            <list>

                <!-- 以下用來列出所有的PO映射文件 -->

                <value>lee/Person.hbm.xml</value>

                <!-- 此處還可列出更多的PO映射文件 -->

            </list>

        </property>

          <!-- 定義Hibernate的SessionFactory屬性 -->

        <property name="hibernateProperties">

             <props>

                <!-- 指定Hibernate的連接方言 -->

                <prop key="hibernate.dialect">org.hibernate.dialect. 
                MySQLDialect</prop>

                <!-- 指定啓動應用時,是否根據Hibernate映射文件創建數據表 -->

                  <prop key="hibernate.hbm2ddl.auto">update</prop>

             </props>

        </property>

    </bean>

    <!-- 配置Person持久化類的DAO Bean -->

    <bean id="personDao" class="lee.PersonDaoImpl">

        <!-- 採用依賴注入來傳入SessionFactory的引用 -->

        <property name="sessionFactory" ref="sessionFactory"/>

    </bean>

    <!-- 下面能以相同的方式配置更多的持久化Bean -->

    ...

    <bean id="myService" class="lee.MyServiceImp">

        <!-- 注入業務邏輯組件所必需的DAO組件 -->

        <property name="peronDdao" ref=" personDao "/>

        <!-- 此處可採用依賴注入更多的DAO組件 -->

        ...

    </bean>

    <!-- 配置控制器Bean,設置起作用域爲Request -->

    <bean name="/login" class="lee.LoginAction" scope="request">

        <!-- 依賴注入控制器所必需的業務邏輯組件 -->

        <property name="myService" ref=" myService "/>

    </bean>

</beans>

在上面的配置文件中,同時配置了控制器Bean、業務邏輯組件Bean、DAO組件Bean以及一些基礎資源Bean。各組件的組織被解耦到配置文件中,而不是在代碼層次的低級耦合。

當客戶端的HTTP請求向/login.do發送請求時,將被容器中的lee.LoginAction攔截,LoginAction調用myService Bean,myService Bean則調用personDao等系列DAO組件,整個流程將系統中的各組件有機地組織在一起。

注意:在實際應用中,很少會將DAO組件、業務邏輯組件以及控制組件都配置在同一個文件中。而是在不同配置文件中,配置相同一組J2EE應用組件。

6.5.7 使用聲明式事務

在上面的配置文件中,部署了控制器組件、業務邏輯組件、DAO組件,幾乎可以形成一個完整的J2EE應用。但有一個小小的問題:事務控制。系統沒有任何事務邏輯,沒有事務邏輯的應用是不可想象的。

Spring提供了非常簡潔的聲明式事務控制,只需要在配置文件中增加事務控制片段,業務邏輯代碼無須任何改變。Spring的聲明式事務邏輯,甚至支持在不同事務策略之間切換。

配置Spring聲明式事務時,通常推薦使用BeanNameAutoProxyCreator自動創建事務代理。通過這種自動事務代理的配置策略,增加業務邏輯組件,只需要在BeanNameAutoProxyCreator Bean配置中增加一行即可,從而避免了增量式配置。

在上面的配置模板文件中增加如下配置片段,系統的myService業務邏輯組件將變成事務代理Bean,從而爲業務邏輯方法增加事務邏輯。

    <!-- 配置Hibernate的局部事務管理器 -->

    <!-- 使用HibernateTransactionManager類,該類是PlatformTransactionManager
    接口,針對採用Hibernate持久化連接的特定實現 -->

    <bean id="transactionManager"

          class="org.springframework.orm.hibernate3.
         HibernateTransactionManager">

        <!-- HibernateTransactionManager Bean需要依賴注入一個SessionFactory
        bean的引用 -->

         <property name="sessionFactory" ref="sessionFactory"/>

    </bean>

    <!-- 配置事務攔截器Bean -->

    <bean id="transactionInterceptor"

        class="org.springframework.transaction.interceptor. 
        TransactionInterceptor">

        <!-- 事務攔截器bean需要依賴注入一個事務管理器 -->

        <property name="transactionManager" ref="transactionManager"/>

        <property name="transactionAttributes">

            <!-- 下面定義事務傳播屬性 -->

            <props>

                <prop key="insert*">PROPAGATION_REQUIRED</prop>

                <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>

                <prop key="*">PROPAGATION_REQUIRED</prop>

            </props>

        </property>

    </bean>

    <!-- 定義BeanNameAutoProxyCreator的Bean後處理器 -->

    <bean class="org.springframework.aop.framework.autoproxy.
    BeanNameAutoProxyCreator">

    <!-- 指定對滿足哪些bean name的bean自動生成業務代理 -->

        <property name="beanNames">

            <!-- 下面是所有需要自動創建事務代理的Bean -->

            <list>

                <value>myService</value>

                <!-- 下面還可增加需要增加事務邏輯的業務邏輯Bean -->

                ...

            </list>

            <!-- 此處可增加其他需要自動創建事務代理的Bean -->

        </property>

        <!-- 下面定義BeanNameAutoProxyCreator所需的攔截器 -->

        <property name="interceptorNames">

            <list>

                <value>transactionInterceptor</value>

                <!-- 此處可增加其他新的Interceptor -->

            </list>

        </property>

    </bean>

一旦增加了如上的配置片段,系統中的業務邏輯方法就有了事務邏輯。這種聲明式事務配置方式可以在不同的事務策略之間自由切換。

提示:儘量使用聲明式事務配置方式,而不要在代碼中完成事務邏輯。

posted @ 2009-07-19 10:24 jadmin 閱讀(1) 評論(0) 編輯

Spring整合Hibernate(1)

6.5 Spring整合Hibernate

時至今日,可能極少有J2EE應用會直接以JDBC方式進行持久層訪問。畢竟,用面向對象的程序設計語言來訪問關係型數據庫,是一件讓人沮喪的事情。大部分時候,J2EE應用都會以ORM框架來進行持久層訪問,在所有的ORM框架中,Hibernate以其靈巧、輕便的封裝贏得了衆多開發者的青睞。

Spring具有良好的開放性,能與大部分ORM框架良好整合。下面將詳細介紹Spring與Hibernate的整合。

6.5.1 Spring提供的DAO支持

DAO模式是一種標準的J2EE設計模式,DAO模式的核心思想是,所有的數據庫訪 問,都通過DAO組件完成,DAO組件封裝了數據庫的增、刪、改等原子操作。而業務邏輯組件則依賴於DAO組件提供的數據庫原子操作,完成系統業務邏輯的實現。

對於J2EE應用的架構,有非常多的選擇,但不管細節如何變換,J2EE應用都大致可分爲如下3層:

   ● 表現層。

   ● 業務邏輯層。

   ● 數據持久層。

輕量級J2EE架構以Spring IoC容器爲核心,承上啓下。其向上管理來自表現層的Action,向下管理業務邏輯層組件,同時負責管理業務邏輯層所需的DAO對象。各層之間負責傳值的是值對象,也就是JavaBean實例。

圖6.5精確地描繪了輕量級J2EE架構的大致情形。

DAO組件是整個J2EE應用的持久層訪問的重要組件,每個J2EE應用的底層實現都難以離開DAO組件的支持。Spring對實現DAO組件提供了許多工具類,系統的DAO組件可通過繼承這些工具類完成,從而可以更加簡便地實現DAO組件。

Spring的DAO支持,允許使用相同的方式、不同的數據訪問技術,如JDBC、Hibernate或JDO。Spring的DAO在不同的持久層訪問技術上提供抽象,應用的持久層訪問基於Spring的DAO抽象。因此,應用程序可以在不同的持久層技術之間切換。

Spring提供了一系列的抽象類,這些抽象將被作爲應用中DAO實現類的父類。通過繼承這些抽象類,Spring簡化了DAO的開發步驟,能以一致的方式使用數據庫訪問技術。不管底層採用JDBC、JDO或Hibernate,應用中都可採用一致的編程模型。

圖6.5 輕量級J2EE應用架構

應用的DAO類繼承這些抽象類,會大大簡化應用的開發。最大的好處是,繼承這些抽象類的DAO能以一致的方式訪問數據庫,意味着應用程序可以在不同的持久層訪問技術中切換。

除此之外,Spring提供了一致的異常抽象,將原有的Checked異常轉換包裝成Runtime異常,因而,編碼時無須捕獲各種技術中特定的異常。Spring DAO體系中的異常,都繼承DataAccessException,而DataAccessException異常是Runtime的,無須顯式捕捉。通過DataAccessException的子類包裝原始異常信息,從而保證應用程序依然可以捕捉到原始異常信息。

Spring提供了多種數據庫訪問技術的DAO支持,包括Hibernate、JDO、TopLink、iBatis、OJB等。Spring可以使用相同的訪問模式、不同的數據庫訪問技術。就Hibernate的持久層訪問技術而言,Spring提供瞭如下3個工具類(或接口)來支持DAO組件的實現:

   ● HibernateDaoSupport。

   ● HibernateTemplate。

   ● HibernateCallBack。

6.5.2 管理Hibernate的SessionFactory

前面介紹Hibernate時已經知道,在通過Hibernate進行持久層訪問時,Hibernate的SessionFactory是一個非常重要的對象,它是單個數據庫映射關係編譯後的內存鏡像。大部分情況下,一個J2EE應用對應一個數據庫,也即對應一個SessionFactory對象。

在純粹的Hibernate訪問中,應用程序需要手動創建SessionFactory實例,可想而知,這不是一個優秀的策略。在實際開發中,希望以一種聲明式的方式管理SessionFactory實例,直接以配置文件來管理SessionFactory實例,在示範Struts的PlugIn擴展點時,大致示範了這種方式(請參閱2.12.1節的內容)。

Spring的IoC容器則提供了更好的管理方式,它不僅能以聲明式的方式配置Session- Factory實例,也可充分利用IoC容器的作用,爲SessionFactory注入數據源引用。

下面是Spring配置文件中配置Hibernate SessionFactory的示範代碼:

<?xml version="1.0" encoding="GBK"?>

<!-- beans是Spring配置文件的根元素,並且指定了Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0. ComboPooledDataSource" 
    destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="com.mysql.jdbc.Driver"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="jdbc:mysql://localhost/j2ee"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="root"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="32147"/>

        <!-- 指定連接數據庫連接池的最大連接數 -->

        <property name="maxPoolSize" value="40"/>

        <!-- 指定連接數據庫連接池的最小連接數 -->

        <property name="minPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的初始化連接數 -->

        <property name="initialPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的連接最大空閒時間 -->

        <property name="maxIdleTime" value="20"/>

    </bean>

    <!-- 定義Hibernate的SessionFactory -->

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3.
    LocalSessionFactoryBean">

        <!-- 依賴注入數據源,正是上文定義的dataSource -->

        <property name="dataSource" ref="dataSource"/>

        <!-- mappingResources屬性用來列出全部映射文件 -->

        <property name="mappingResources">

            <list>

                <!-- 以下用來列出所有的PO映射文件 -->

                <value>lee/MyTest.hbm.xml</value>

            </list>

        </property>

          <!-- 定義Hibernate的SessionFactory屬性 -->

        <property name="hibernateProperties">

             <props>

                <!-- 指定Hibernate的連接方言 -->

                <prop key="hibernate.dialect">org.hibernate.dialect. 
                MySQLDialect</prop>

                <!-- 配置啓動應用時,是否根據Hibernate映射自動創建數據表 -->

                  <prop key="hibernate.hbm2ddl.auto">update</prop>

           </props>

        </property>

    </bean>

</beans>

一旦在Spring的IoC容器中配置了SessionFactory Bean,它將隨應用的啓動而加載,並可以充分利用IoC容器的功能,將SessionFactory Bean注入任何Bean,比如DAO組件。一旦DAO組件獲得了SessionFactory Bean的引用,就可以完成實際的數據庫訪問。

當然,Spring也支持訪問容器數據源。如果需要使用容器數據源,可將數據源Bean修改成如下配置:

<!-- 此處配置JNDI數據源 -->

<bean id="myDataSource" class="org.springframework.jndi.JndiObjectFactoryBean">

    <property name="jndiName">

        <!-- 指定數據源的JNDI -->

        <value>java:comp/env/jdbc/myds</value>

    </property>

</bean>

可見,以聲明式的方式管理SessionFactory實例,可以讓應用在不同數據源之間切換。如果應用更換數據庫等持久層資源,只需對配置文件進行簡單修改即可。

提示:以聲明式的方式管理SessionFactory,非常類似於早期將數據庫服務的相關信息放在web.xml文件中進行配置。這種方式是爲了提供更好的適應性,當持久層服務需要更改時,應用代碼無須任何改變。

6.5.3 使用HibernateTemplate

HibernateTemplate提供持久層訪問模板,使用HibernateTemplate無須實現特定接口,它只需要提供一個SessionFactory的引用就可執行持久化操作。SessionFactory對象既可通過構造參數傳入,也可通過設值方式傳入。HibernateTemplate提供如下3個構造函數:

   ● HibernateTemplate()。

   ● HibernateTemplate(org.hibernate.SessionFactory sessionFactory)。

   ● HibernateTemplate(org.hibernate.SessionFactory sessionFactory, boolean allowCreate)。

第一個構造函數,構造一個默認的HibernateTemplate實例。因此,使用Hibernate- Template實例之前,還必須使用方法setSessionFactory(SessionFactory sessionFactory)來爲HibernateTemplate傳入SessionFactory的引用。

第二個構造函數,在構造時已經傳入SessionFactory引用。

第三個構造函數,其boolean型參數表明,如果當前線程已經存在一個非事務性的Session,是否直接返回此非事務性的Session。

在Web應用中,通常啓動時自動加載ApplicationContext,SessionFactory和DAO對象都處在Spring上下文管理下,因此無須在代碼中顯式設置,可採用依賴注入完成Session- Factory和DAO的解耦,依賴關係通過配置文件來設置,如下所示:

<?xml version="1.0" encoding="GBK"?>

<!-- beans是Spring配置文件的根元素,並且指定了Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" 
    destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="com.mysql.jdbc.Driver"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="jdbc:mysql://localhost/j2ee"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="root"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="32147"/>

        <!-- 指定連接數據庫連接池的最大連接數 -->

        <property name="maxPoolSize" value="40"/>

        <!-- 指定連接數據庫連接池的最小連接數 -->

        <property name="minPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的初始化連接數 -->

        <property name="initialPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的連接最大空閒時間 -->

        <property name="maxIdleTime" value="20"/>

    </bean>

    <!-- 定義Hibernate的SessionFactory Bean -->

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3. 
    LocalSessionFactoryBean">

        <!-- 依賴注入數據源,注入的正是上文中定義的dataSource -->

        <property name="dataSource" ref="dataSource"/>

        <!-- mappingResources屬性用來列出全部映射文件 -->

        <property name="mappingResources">

            <list>

                <!-- 以下用來列出所有的PO映射文件 -->

                <value>lee/Person.hbm.xml</value>

            </list>

        </property>

          <!-- 定義Hibernate的SessionFactory屬性 -->

        <property name="hibernateProperties">

             <props>

                <!-- 指定Hibernate的連接方言 -->

                <prop key="hibernate.dialect">org.hibernate.dialect. 
                MySQLDialect</prop>

                <!-- 指定啓動應用時,是否根據Hibernate映射文件創建數據表 -->

                  <prop key="hibernate.hbm2ddl.auto">update</prop>

             </props>

        </property>

    </bean>

    <!-- 配置Person持久化類的DAO bean -->

    <bean id="personDao" class="lee.PersonDaoImpl">

        <!-- 採用依賴注入來傳入SessionFactory的引用 -->

        <property name="sessionFactory" ref="sessionFactory"/>

    </bean>

</beans>

在PersonDao組件中,所有的持久化操作都通過HibernateTemplate實例完成,而HibernateTemplate操作數據庫非常簡潔,大部分CRUD操作都可通過一行代碼解決問題。下面介紹如何通過HibernateTemplate進行持久層訪問。

HibernateTemplate提供了非常多的常用方法來完成基本的操作,比如通常的增加、刪除、修改、查詢等操作,Spring 2.0更增加了對命名SQL查詢的支持,也增加了對分頁的支持。大部分情況下,使用Hibernate的常規用法,就可完成大多數DAO對象的CRUD操作。下面是HibernateTemplate的常用方法簡介:

   ● void delete(Object entity),刪除指定持久化實例。

   ● deleteAll(Collection entities),刪除集合內全部持久化類實例。

   ● find(String queryString),根據HQL查詢字符串來返回實例集合。

   ● findByNamedQuery(String queryName),根據命名查詢返回實例集合。

   ● get(Class entityClass, Serializable id),根據主鍵加載特定持久化類的實例。

   ● save(Object entity),保存新的實例。

   ● saveOrUpdate(Object entity),根據實例狀態,選擇保存或者更新。

   ● update(Object entity),更新實例的狀態,要求entity是持久狀態。

   ● setMaxResults(int maxResults),設置分頁的大小。

下面是一個完整DAO類的源代碼:

public class PersonDaoImpl implements PersonDao

{

    //執行持久化操作的HibernateTemplate實例

    private HibernateTemplate ht = null;

    private SessionFactory sessionFactory;

    //該DAO組件持久化操作所需的SessionFactory對象

    public void setSessionFactory(SessionFactory sessionFactory)

    {

        this.sessionFactory = sessionFactory;

    }

    //用於根據SessionFactory實例返回HibernateTemplate實例的方法

    private HibernateTemplate getHibernateTemplate()

    {

        if (ht == null)

        {

            ht = new HibernateTemplate(sessionFactory);

        }

        return ht;

    }

    /**

     * 加載人實例

     * @param id 需要加載的Person實例的主鍵值

     * @return 返回加載的Person實例

     */

    public Person get(int id)

    {

        return (Person)getHibernateTemplate().get(Person.class, new 
        Integer(id));

    }

    /**

     * 保存人實例

     * @param person 需要保存的Person實例

     */   

    public void save(Person person)

    {

        getHibernateTemplate().save(person);

    }

    /**

     * 修改Person實例

     * @param person 需要修改的Person實例

     */

    public void update(Person person)

    {

        getHibernateTemplate().update(person);

    }

    /**

     * 刪除Person實例

     * @param id 需要刪除的Person的id

     */

    public void delete(int id)

    {

        getHibernateTemplate().delete(getHibernateTemplate().get(Person. 
        class,new Integer(id)));

    }

    /**

     * 刪除Person實例

     * @param person 需要刪除的Person實例

     */

   public void delete(Person person)

    {

        getHibernateTemplate().delete(person);

    }

    /**

     * 根據用戶名查找Person

     * @param name 用戶名

     * @return 用戶名對應的全部用戶

     */

    public List findByName(String name)

    {

        return getHibernateTemplate().find("from Person p where p.name
        like ?" , name);

    }

    /**

    * 返回全部的Person實例

    * @return 全部的Person實例

    */

    public List findAllPerson()

    {

        return getHibernateTemplate().find("from Person ");

    }

}

通過上面實現DAO組件的代碼可以看出,通過HibernateTemplate進行持久層訪問的代碼如此清晰,大部分CRUD操作一行代碼即可完成,完全無須Hibernate訪問那些繁瑣的步驟。而且,一旦DAO組件獲得了SessionFactory的引用,即可很輕易地創建HibernateTemplate實例。

提示:HibernateTemplate是Spring衆多模板工具類之一,Spring正是通過這種簡便地封裝,完成了開發中大量需要重複執行的工作。

posted @ 2009-07-19 10:24 jadmin 閱讀(4) 評論(0) 編輯

Spring整合Struts(2)

Struts的plug-in配置部分明確指出,Spring的配置文件有兩個:applicationContext.xml和action-servlet.xml。其實,完全可以使用一個配置文件。通常,習慣將Action Bean配置在控制器的context內。action-servlet.xml用於配置表現層上下文,其詳細配置信息如下:

<?xml version="1.0" encoding="gb2312"?>

<!-- 指定Spring配置文件的根元素,以及對應的Schame信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 每個request請求產生一個新實例,將所有該請求的作用域配置成request -->

    <bean name="/login" class="lee.LoginAction" scope="request">

        <property name="vb" ref="vb"/>

    </bean>

</beans>

因爲每次請求都應該啓動新的Action處理用戶請求,因此,應將Action的作用域配置成Request。

注意:ActionServlet轉發請求時,是根據Bean的name屬性,而不是id屬性。因此,此處確定的name屬性與Struts的action屬性相同。

applicationContext.xml只有一個bean配置,即vb bean。其詳細配置如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring 配置文件的根元素,以及對應的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置ValidBean實例 -->

    <bean id="vb" class="lee.ValidBeanImpl"/>

</beans>

ValidBeanImpl是一個業務邏輯bean,本示例程序中僅作簡單的判斷,ValidBeanImpl的源代碼如下:

//面向接口編程,實現ValidBean接口

public class ValidBeanImpl implements ValidBean

{

    //根據輸入的用戶名和密碼判斷是否有效

    public boolean valid(String username,String pass)

    {

        //有效,返回true

        if (username.equals("scott") && pass.equals("tiger"))

        {

            return true;

        }

        return false;

    }

}

注意:上面的業務邏輯組件非常簡單,它只是一個示意。如果是真實的應用,業務邏輯組件應該通過DAO組件來實現業務邏輯方法。

應用的業務邏輯控制器,Action則負責調用業務邏輯組件的方法,並根據業務邏輯組件方法的返回值,確定如何響應用戶請求。下面是該示例應用控制器的代碼:

//業務控制器繼承Action

public class LoginAction extends Action

{

    //action控制器將調用的業務邏輯組件

    private ValidBean vb;

    //依賴注入業務邏輯組件的setter方法

    public void setVb(ValidBean vb)

    {

        this.vb = vb;

    }

    //必須重寫該核心方法,該方法actionForm將表單的請求參數封裝成值對象

    public ActionForward execute(ActionMapping mapping, ActionForm form,

       HttpServletRequest request, HttpServletResponse response)throws
       Exception

    {

        //form由ActionServlet轉發請求時創建,封裝了所有的請求參數

        LoginForm loginForm = (LoginForm)form;

        //獲取username請求參數

        String username = loginForm.getUsername();

        //獲取pass請求參數

        String pass = loginForm.getPass();

        //下面爲服務器端的數據校驗

        String errMsg = "";

        //判斷用戶名不能爲空

        if (username == null || username.equals(""))

        {

            errMsg += "您的用戶名丟失或沒有輸入,請重新輸入";

        }

        //判斷密碼不能爲空

        else if(pass == null || pass.equals(""))

        {

            errMsg += "您的密碼丟失或沒有輸入,請重新輸入";

        }

        //如果用戶名和密碼不爲空,才調用業務邏輯組件

        else

        {

            //vb是業務邏輯組件,由容器注入

            if (vb.valid(username,pass))

            {

                return mapping.findForward("welcome");

            }

            else

            {

                errMsg = "您的用戶名和密碼不匹配";

            }

        }

        //判斷是否生成了錯誤信息

        if (errMsg != null && !errMsg.equals(""))

        {

            //如果有錯誤信息,將錯誤信息保存在request裏,並跳轉到input對應的
            forward對象

            request.setAttribute("err" , errMsg);

            return mapping.findForward("input");

        }

        else

        {

            //如果沒有錯誤信息,跳轉到welcome對應的forward對象

            return mapping.findForward("welcome");

        }

    }

}

在本應用中,使用了Struts的客戶端數據校驗,讓Action繼承ValidatorActionForm即可。ActionForm的代碼非常簡單,此處不再贅述。

爲了完成數據校驗,還應該編寫數據校驗規則文件。在struts-config.xml文件的尾部,另有一個plug-in用來加載校驗文件,其中validator-rules.xml文件位於struts壓縮包的lib下,直接複製過來即可使用,而validator.xml必須自己編寫,validator.xml文件如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 驗證規則文件的文件頭,包括DTD等信息 -->

<!DOCTYPE form-validation PUBLIC

          "-//Apache Software Foundation//DTD Commons Validator Rules
          Configuration 1.1.3//EN"

          "http://jakarta.apache.org/commons/dtds/validator_1_1_3.dtd">

<!-- 驗證文件的根元素 -->

<form-validation>

    <!-- 所有需要驗證的form都放在formset裏 -->

<formset>

    <!-- 需要驗證的form名,該名與struts裏配置的名相同 -->

    <form name="loginForm">

        <!-- 指定該form的username域必須滿足的規則:必填、模式匹配 -->

        <field property="username" depends="required,mask">

            <arg key="loginForm.username" position="0"/>

            <var>

                <!-- 確定匹配模式的正則表達式 -->

            <var-name>mask</var-name>

            <var-value>^[a-zA-Z]+$</var-value>

            </var>

        </field>

        <!-- 指定該form的pass域必須滿足的規則:必填 -->

        <field property="pass" depends="required">

            <msg name="required" key="pass.required"/>

            <arg key="loginForm.pass" position="0"/>

        </field>

    </form>

</formset>

</form-validation>

上面示例程序的結構非常清晰:表現層組件(Action)配置在action-servlet.xml文件中,而業務邏輯層組件(vb)配置在applicationContext.xml文件中,如果應用中有DAO組件,將DAO組件配置在dao-context.xml文件中。將3個文件放在plug-in元素裏一起加載。

文本框:圖6.3  DelegatingRequestProcessor整合策略的登錄失敗效果DelegatingRequestProcessor會將請求轉發到Action,該Action已經處於IoC容器管理之下,因此,可以方便地訪問容器中的其他Bean。

通過配置文件可以看出,Action根本無須type屬性,即struts-config.xml中Action根本沒有實例化過,DelegatingRequestProcessor將請求轉發給Spring容器中的同名Bean。這種轉發的時機非常早,避免了創建struts-config.xml配置文件中的Action,因而性能非常好。

圖6.3是採用這種整合策略的執行效果。

6.4.4 使用DelegatingActionProxy

使用DelegatingRequestProcessor簡單方便,但有一個缺點,RequestProcessor是Struts的一個擴展點,也許應用程序本身就需要擴展RequestProcessor,而DelegatingRequest- Processor已經使用了這個擴展點。

爲了重新利用Struts的RequestProcessor這個擴展點,有兩個做法:

   ● 應用程序的RequestProcessor不再繼承Struts的RequestProcessor,改爲繼承DelegatingRequestProcessor。

   ● 使用DelegatingActionProxy。

前者常常有一些未知的風險,而後者是Spring推薦的整合策略。使用Delegating- ActionProxy與DelegatingRequestProcessor的目的都只有一個,將請求轉發給Spring管理的Bean。

DelegatingRequestProcessor直接替換了原有的RequestProcessor,在請求轉發給action之前,轉發給Spring管理的Bean;而DelegatingActionProxy則被配置成Struts的Action,即所有的請求先被ActionServlet截獲,請求被轉發到對應的Action,而action的實現類全都是DelegatingActionProxy,DelegatingActionProxy再將請求轉發給Spring容器的Action。

可以看出,使用DelegatingActionProxy比使用DelegatingRequestProcessor要晚一步轉發到Spring的context。但通過這種方式可以避免佔用擴展點。

與使用DelegatingRequestProcessor相比,使用DelegatingActionProxy僅需要去掉controller配置元素,並將所有的action實現類改爲DelegatingActionProxy即可。詳細的配置文件如下:

<!-- XML文件的版本和編碼集 -->

<?xml version="1.0" encoding="gb2312"?>

<!-- struts配置文件的文件頭,包括DTD等信息 -->

<!DOCTYPE struts-config PUBLIC

          "-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"

          "http://struts.apache.org/dtds/struts-config_1_2.dtd">

<!-- struts配置文件的根元素 -->

<struts-config>

    <!-- 配置formbean,所有的formbean都放在form-beans元素裏定義 -->

    <form-beans>

        <!-- 定義了一個formbean,確定formbean名和實現類 -->

        <form-bean name="loginForm" type="lee.LoginForm"/>

    </form-beans>

    <!-- 定義action部分,所有的action都放在action-mapping元素裏定義 -->

    <action-mappings>

        <!-- 這裏只定義了一個action。必須配置action的type元素爲
        DelegatingActionProxy -->

        <action path="/login" type="org.springframework.web.struts. 
        DelegatingActionProxy"

            name="loginForm" scope="request" validate="true" input= 
            "/login.jsp" >

            <!-- 定義action內的兩個局部forward元素 -->

            <forward name="input" path="/login.jsp"/>

            <forward name="welcome" path="/welcome.html"/>

        </action>

    </action-mappings>

    <!-- 加載國際化的資源包 -->

    <message-resources parameter="mess"/>

    <!-- 裝載驗證的資源文件 -->

    <plug-in className="org.apache.struts.validator.ValidatorPlugIn">

        <set-property property="pathnames" value="/WEB-INF/validator- 
        rules.xml,/WEB-INF/validation.xml" />

        <set-property property="stopOnFirstError" value="true"/>

    </plug-in>

    <!-- 裝載Spring配置文件,隨應用啓動創建ApplicationContext實例 -->

    <plug-in className="org.springframework.web.struts. ContextLoaderPlugIn">
        <set-property property="contextConfigLocation"

            value="/WEB-INF/applicationContext.xml,

                   /WEB-INF/action-servlet.xml"/>

    </plug-in>

</struts-config>

DelegatingActionProxy接收ActionServlet轉發過來的請求,然後轉發給Application- Context管理的Bean,這是典型的鏈式處理。

通過配置文件可以看出,struts-config.xml文件中配置了大量DelegatingActionProxy實例,Spring容器中也配置了同名的Action。即Struts的業務控制器分成了兩個部分:第一個部分是Spring的DelegatingActionProxy,這個部分沒有實際意義,僅僅完成轉發;第二個部分是用戶的Action實現類,該實現類負責真實的處理。

這種策略的性能比前一種策略的效果要差一些,因爲需要多創建一個Delegating- ActionProxy實例。而且,J2EE應用中Action非常多,這將導致大量創建DelegatingActionProxy實例,使用一次之後,等待垃圾回收機制回收——這對性能的影響不可避免。

圖6.4是DelegatingActionProxy的執行效果。

文本框:圖6.4  DelegatingActionProxy整合策略的登錄成功效果注意:使用DelegatingActionProxy的整合策略,可避免佔用Struts的RequestProcessor擴展點,但降低了整合性能。

6.4.5 使用ActionSupport代替Action

前面已經介紹了,Spring與Struts的整合還有一種策略,讓Struts的Action顯式獲取Spring容器中的Bean。在這種策略下,Struts的Action不接受IoC容器管理,Action的代碼與Spring API部分耦合,造成代碼污染。這種策略也有其好處:代碼的可讀性非常強,Action的代碼中顯式調用業務邏輯組件,而無須等待容器注入。

Action中訪問ApplicationContext有兩種方法:

   ● 利用WebApplicationContextUtils工具類。

   ● 利用ActionSupport支持類。

通過WebApplicationContextUtils,可以顯式獲得Spring容器的引用(請參閱6.4.1節的內容),而ActionSupport類則提供了一個更簡單的方法getWebApplicationContext(),該方法可直接獲取Spring容器的引用。

所謂ActionSupport類,是指Spring提供了系列擴展。Spring擴展了Struts的Action,在Struts的Action後加上Support後綴,Spring擴展的Action有如下幾個:

   ● ActionSupport。

   ● DispatchActionSupport。

   ● LookupDispatchActionSupport。

   ● MappingDispatchActionSupport。

下面的示例將展示這種整合策略,在這種整合策略下,Struts的Action改爲繼承Spring擴展後的Action,下面是應用的Action代碼:

//新的業務控制器,繼承Spring的ActionSupport類

public class LoginAction extends ActionSupport

{

    //依然將ValidBean作爲成員變量

    private ValidBean vb;

    //構造器,注意:不可在構造器中調用getWebApplicationContext()方法

    public LoginAction()

    {

    }

    //完成ValidBean的初始化

    public ValidBean getVb()

    {

        return(ValidBean)getWebApplicationContext().getBean("vb");

    }

    //必須重寫該核心方法,該方法actionForm將表單的請求參數封裝成值對象

    public ActionForward execute(ActionMapping mapping, ActionForm form,

       HttpServletRequest request, HttpServletResponse response)throws
       Exception

    {

        //form由ActionServlet轉發請求時創建,封裝了所有的請求參數

        LoginForm loginForm = (LoginForm)form;

        //獲取username請求參數

        String username = loginForm.getUsername();

        //獲取pass請求參數

        String pass = loginForm.getPass();

        //下面爲服務器端的數據校驗

        String errMsg = "";

        //判斷用戶名不能爲空

        if (username == null || username.equals(""))

        {

            errMsg += "您的用戶名丟失或沒有輸入,請重新輸入";

        }

        //判斷密碼不能爲空

        else if(pass == null || pass.equals(""))

        {

            errMsg += "您的密碼丟失或沒有輸入,請重新輸入";

        }

        //如果用戶名和密碼不爲空,才調用業務邏輯組件

        else

        {

            //vb是業務邏輯組件,通過上面的初始化方法獲得

            if (getVb().valid(username,pass))

            {

                return mapping.findForward("welcome");

            }

            else

            {

                errMsg = "您的用戶名和密碼不匹配";

            }

        }

        //判斷是否生成了錯誤信息

        if (errMsg != null && !errMsg.equals(""))

        {

            //如果有錯誤信息,將錯誤信息保存在request裏,並跳轉到input對應的
            //forward對象

            request.setAttribute("err" , errMsg);

            return mapping.findForward("input");

        }

        else

        {

            //如果沒有錯誤信息,跳轉到welcome對應的forward對象

            return mapping.findForward("welcome");

        }

    }

}

在上面的Action代碼中,Action顯式獲取容器中的業務邏輯組件,而不是依靠Spring容器的依賴注入。在這種整合策略下,表現層的控制器組件不再接受IoC容器管理。因此,沒有控制器上下文,應將原有的action-servlet.xml文件刪除,並修改plug-in元素,不要加載該文件。還要修改Action配置,將Action配置的type元素修改成實際的處理類。這      種整合策略也有一個好處:代碼的可讀性更強,對傳統Struts應用開發的改變很小,容易使用。

將該Action部署在struts-config.xml中,Struts將負責創建該Action。struts-config.xml文件的源代碼如下:

<!-- XML文件的版本和編碼集 -->

<?xml version="1.0" encoding="gb2312"?>

<!-- Struts配置文件的文件頭,包括DTD等信息 -->

<!DOCTYPE struts-config PUBLIC

          "-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"

          "http://struts.apache.org/dtds/struts-config_1_2.dtd">

<!-- struts配置文件的根元素 -->

<struts-config>

    <!-- 配置formbean,所有的formbean都放在form-beans元素裏定義 -->

    <form-beans>

        <!-- 定義了一個formbean,確定formbean名和實現類 -->

        <form-bean name="loginForm" type="lee.LoginForm"/>

    </form-beans>

    <!-- 定義action部分,所有的action都放在action-mapping元素裏定義 -->

    <action-mappings>

        <!-- 這裏只定義了一個action。action的類型爲ActionSupport的子類 -->

        <action path="/login" type="type="lee.LoginAction"

            name="loginForm" scope="request" validate="true" input= 
            "/login.jsp" >

            <!-- 定義action內的兩個局部forward元素 -->

            <forward name="input" path="/login.jsp"/>

            <forward name="welcome" path="/welcome.html"/>

        </action>

    </action-mappings>

    <!-- 加載國際化的資源包 -->

    <message-resources parameter="mess"/>

    <!-- 裝載驗證的資源文件 -->

    <plug-in className="org.apache.struts.validator.ValidatorPlugIn">

        <set-property property="pathnames" value="/WEB-INF/validator- 
        rules.xml,/WEB-INF/validation.xml" />

        <set-property property="stopOnFirstError" value="true"/>

    </plug-in>

</struts-config>

此時,Spring無須使用配置Action的配置文件,這種配置方式非常簡單。只需要業務邏輯組件的配置文件,業務邏輯組件的配置文件與前面的示例沒有任何改變。

該配置文件中的業務邏輯組件由Spring容器負責實現,而ActionSupport能夠先定位Spring容器,然後獲得容器的業務邏輯組件。

這種整合策略的執行效果與前面兩種整合策略的執行效果完全相同。從代碼中分析可見,在這種整合策略下,業務控制器再次退回到Struts起初的設計。僅由strutsconfig.xml中Action充當,從而避免了像DelegatingActionProxy整合策略的性能低下,因爲可以只需要創建實際的Action實例。

注意:在這種整合策略下,Struts開發者的改變最小,最接近傳統Struts應用開發者的習慣。但這種整合策略會造成代碼污染,因爲Action類必須繼承Spring的ActionSupport類。

posted @ 2009-07-19 10:23 jadmin 閱讀(1) 評論(0) 編輯

Spring整合Struts(1)

6.4 Spring整合Struts

雖然Spring也提供了自己的MVC組件,但一來Spring的MVC組件過於繁瑣,二     來Struts的擁護者實在太多。因此,很多項目都會選擇使用Spring整合Struts框架。而且Spring確實可以無縫整合Struts框架,二者結合成一個更實際的J2EE開發平臺。

6.4.1 利用Struts的PlugIn來啓動Spring容器

使用Spring的Web應用時,不用手動創建Spring容器,而是通過配置文件聲明式地創建Spring容器。因此,在Web應用中創建Spring容器有如下兩個方式:

   ● 直接在web.xml文件中配置創建Spring容器。

   ● 利用第三方MVC框架的擴展點,創建Spring容器。

其實第一種創建Spring容器的方式更加常見。爲了讓Spring容器隨Web應用的啓動而自動啓動,有如下兩個方法:

   ● 利用ServletContextListener實現。

   ● 採用load-on-startup Servlet實現。

Spring提供ServletContextListener的一個實現類ContextLoaderListener,該類可以作爲Listener使用,會在創建時自動查找WEB-INF/下的applicationContext.xml文件,因此,如果只有一個配置文件,並且文件名爲applicationContext.xml,只需在web.xml文件中增加如下配置片段即可:

<listener>

   <listener-class>org.springframework.web.context. 
    ContextLoaderListener</listener-class>

</listener>

如果有多個配置文件需要載入,則考慮使用<context-param>元素來確定配置文件的文件名。ContextLoaderListener加載時,會查找名爲contextConfigLocation的參數。因此,配置context-param時,參數名字應該是contextConfigLocation。

帶多個配置文件的web.xml文件如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Web配置文件的根元素,以及相應的Schema信息 -->

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee   
    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"

    version="2.4">

    <!-- 確定多個配置文件 -->

    <context-param>

        <!-- 參數名爲contextConfigLocation -->

        <param-name>contextConfigLocation</param-name>

        <!-- 多個配置文件之間以“,”隔開 -->

        <param-value>/WEB-INF/daoContext.xml,/WEB-INF/ 
        applicationContext.xml</param-value>

    </context-param>

    <!-- 採用listener創建ApplicationContext實例 -->

    <listener>

        <listener-class>org.springframework.web.context. 
        ContextLoaderListener</listener-class>

    </listener>

</web-app>

如果沒有通過contextConfigLocation指定配置文件,Spring會自動查找application- Context.xml配置文件;如果有contextConfigLocation,則利用該參數確定的配置文件。如果無法找到合適的配置文件,Spring將無法正常初始化。

Spring根據bean定義創建WebApplicationContext對象,並將其保存在web應用的ServletContext中。大部分情況下,應用中的Bean無須感受到ApplicationContext的存在,只要利用ApplicationContext的IoC即可。

如果需要在應用中獲取ApplicationContext實例,可以通過如下代碼獲取:

//獲取當前Web應用的Spring容器

WebApplicationContext ctx =

    WebApplicationContextUtils.getWebApplicationContext(servletContext);

除此之外,Spring提供了一個特殊的Servlet類ContextLoaderServlet。該Servlet在啓動時,會自動查找WEB-INF/下的applicationContext.xml文件。

當然,爲了讓ContextLoaderServlet隨應用的啓動而啓動,應將此Servlet配置成load-on-startup的Servlet,load-on-startup的值小一點比較合適,這樣可以保證Application- Context更快的初始化。

如果只有一個配置文件,並且文件名爲applicationContext.xml,在web.xml文件中增加如下一段即可:

<servlet>

    <servlet-name>context</servlet-name>

    <servlet-class>org.springframework.web.context.ContextLoaderServlet
    </servlet-class>

    <load-on-startup>1</load-on-startup>

</servlet>

該Servlet用於提供“後臺”服務,主要用於創建Spring容器,無須響應客戶請求,因此無須配置servlet-mapping。

如果有多個配置文件,一樣使用<context-param>元素來確定多個配置文件。

事實上,不管是ContextLoaderServlet,還是ContextLoaderListener,都依賴於ContextLoader創建ApplicationContext實例。

在ContextLoader代碼的第240行,有如下代碼:

String configLocation = servletContext.getInitParameter 
(CONFIG_LOCATION_PARAM);

if (configLocation != null) {

    wac.setConfigLocations(StringUtils.tokenizeToStringArray
    (configLocation,

    ConfigurableWebApplicationContext.CONFIG_LOCATION_DELIMITERS));

}

其中,CONFIG_LOCATION_PARAM是該類的常量,其值爲contextConfigLocation。可以看出,ContextLoader首先檢查servletContext中是否有contextConfigLocation的參數,如果有該參數,則加載該參數指定的配置文件。

ContextLoaderServlet與ContextLoaderListener底層都依賴於ContextLoader。因此,二者的效果幾乎沒有區別。之間的區別不是它們本身引起的,而是由於Servlet規範,Listener比Servlet優先加載。因此,採用ContextLoaderListener創建ApplicationContext的時機更早。

當然,也可以通過ServletContext的getAttribute方法獲取ApplicationContext。但使用WebApplicationContextUtils類更便捷,因爲無須記住ApplicationContext的屬性名。即使ServletContext的WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRI- BUTE屬性沒有對應對象,WebApplicationContextUtils的getWebApplicationContext()方法將會返回空,而不會引起異常。

到底需要使用Listener,還是使用load-on-startup Servlet來創建Spring容器呢?通常推薦使用Listener來創建Spring容器。但Listerner是Servlet 2.3以上才支持的標準,因此,必須Web容器支持Listener纔可使用Listerner。

注意:使用Listener創建Spring容器之前,應先評估Web容器是否支持Listener標準。

還有一種情況,利用第三方MVC框架的擴展點來創建Spring容器,比如Struts。在第2章介紹Strust框架時,知道Struts有一個擴展點PlugIn。

實際上,Spring正是利用了PlugIn這個擴展點,從而提供與Struts的整合。Spring提供了PlugIn接口的實現類org.springframework.web.struts.ContextLoaderPlugIn。這個實現類可作爲Struts的PlugIn配置,Struts框架啓動時,將自動創建Spring容器。

爲了利用Struts的PlugIn創建Spring容器,只需在Struts配置文件中增加如下片段   即可:

<plug-in className="org.springframework.web.struts.ContextLoaderPlugIn">

<set-property property="contextConfigLocation"

      value="/WEB-INF/action-servlet.xml,/WEB-INF/applicationContext.
      xml"/>

</plug-in>

其中,指定contextConfigLocation屬性值時,即可以指定一個Spring配置文件的位置,可以指定多個Spring配置文件的位置。

6.4.2 MVC框架與Spring整合的思考

對於一個基於B/S架構的J2EE應用而言,用戶請求總是向MVC框架的控制器請求,而當控制器攔截到用戶請求後,必須調用業務邏輯組件來處理用戶請求。此時有一個問題,控制器應該如何獲得業務邏輯組件?

最容易想到的策略是,直接通過new關鍵字創建業務邏輯組件,然後調用業務邏輯組件的方法,根據業務邏輯方法的返回值確定結果。

實際的應用中,很少見到採用上面的訪問策略,因爲這是一種非常差的策略。不這樣做至少有如下3個原因:

   ● 控制器直接創建業務邏輯組件,導致控制器和業務邏輯組件的耦合降低到代碼層次,不利於高層次解耦。

   ● 控制器不應該負責業務邏輯組件的創建,控制器只是業務邏輯組件的使用者。無須關心業務邏輯組件的實現。

   ● 每次創建新的業務邏輯組件將導致性能下降。

答案是採用工廠模式或服務定位器。採用服務定位器的模式,是遠程訪問的場景。在這種場景下,業務邏輯組件已經在某個容器中運行,並對外提供某種服務。控制器無須理會該業務邏輯組件的創建,直接調用即可,但在調用之前,必須先找到該服務——這就是服務定位器的概念。經典J2EE應用就是這種結構的應用。

對於輕量級的J2EE應用,工廠模式則是更實際的策略。因爲輕量級的J2EE應用裏,業務邏輯組件不是EJB,通常就是一個POJO,業務邏輯組件的生成通常由工廠負責,而且工廠可以保證該組件的實例只需一個就夠了,可以避免重複實例化造成的系統開銷。

如圖6.2就是採用工廠模式的順序圖。

圖6.2 工廠模式順序圖

採用工廠模式,將控制器與業務邏輯組件的實現分離,從而提供更好的解耦。

在採用工廠模式的訪問策略中,所有的業務邏輯組件的創建由工廠負責,業務邏輯組件的運行也由工廠負責。而控制器只需定位工廠實例即可。

如果系統採用Spring框架,則Spring成爲最大的工廠。Spring負責業務邏輯組件的創建和生成,並可管理業務邏輯組件的生命週期。可以如此理解,Spring是一個性能非常優秀的工廠,可以生產出所有的實例,從業務邏輯組件,到持久層組件,甚至控制器。

現在的問題是,控制器如何訪問到Spring容器中的業務邏輯組件?爲了讓Action訪 問Spring的業務邏輯組件,有兩種策略:

   ● Spring管理控制器,並利用依賴注入爲控制器注入業務邏輯組件。

   ● 控制器顯式定位Spring工廠,也就是Spring的容器ApplicationContext實例,並從工廠中獲取業務邏輯組件實例的引用。

第一種策略,充分利用Spring的IoC特性,是最優秀的解耦策略。但不可避免帶來一些不足之處,歸納起來主要有如下不足之處:

   ● Spring管理Action,必須將所有的Action配置在Spring容器中,而struts-config.xml文件中的配置也不會減少,導致配置文件大量增加。

   ● Action的業務邏輯組件接收容器注入,將導致代碼的可讀性降低。

總體而言,這種整合策略是利大於弊。

第二種策略,與前面介紹的工廠模式並沒有太大的不同。區別是Spring容器充當了業務邏輯組件的工廠。控制器負責定位Spring容器,通常Spring容器訪問容器中的業務邏輯組件。這種策略是一種折衷,降低了解耦,但提高了程序的可讀性。

Spring完全支持這兩種策略,既可以讓Spring容器管理控制器,也可以讓控制器顯式定位Spring容器中的業務邏輯組件。

6.4.3 使用DelegatingRequestProcessor

這裏介紹的是第一種整合策略:讓Spring管理Struts的Action。那麼同樣有一個問題,讓Spring管理Struts的Action時,客戶端的HTTP 請求如何轉向Spring容器中的Action?

當使用Struts作爲MVC框架時,客戶端的HTTP請求都是直接向ActionServlet請求的,因此關鍵就是讓ActionServlet將請求轉發給Spring容器中的Action。這很明顯可以利用Spring的另一個擴展點:通過擴展RequestProcessor完成,使用擴展的RequestProcessor替換Struts的RequestProcessor。

Spring完成了這種擴展,Spring提供的DelegatingRequestProcessor繼承Request- Processor。爲了讓Struts使用DelegatingRequestProcessor,還需要在struts-config.xml文件中增加如下一行:

//使用spring的RequestProcessor替換struts原有的RequestProcessor

<controller processorClass="org.springframework.web.struts. 
DelegatingRequestProcessor"/>

完成這個設置後,Struts會將截獲到的用戶請求轉發到Spring context下的bean,根據bean的name屬性來匹配。而Struts中的action配置則無須配置class屬性,即使配置了class屬性也沒有任何用處,即下面兩行配置是完全一樣的:

//配置struts action時,指定了實現類

<action path="/user" type="lee.UserAction"/>

//配置struts action時,沒有指定實現類

<action path="/user"/>

下面的示例程序在上一個示例程序的基礎上稍作修改,增加了客戶端驗證和程序國際化部分。也調用了Spring的業務bean來驗證登錄。先看修改後的struts-config.xml文件:

<!-- XML文件版本,編碼集 -->

<?xml version="1.0" encoding="gb2312"?>

<!-- Struts配置文件的文件頭,包括DTD等信息 -->

<!DOCTYPE struts-config PUBLIC

          "-//Apache Software Foundation//DTD Struts Configuration 1.2//EN"

          "http://struts.apache.org/dtds/struts-config_1_2.dtd">

<!-- struts配置文件的根元素 -->

<struts-config>

    <!-- 配置formbean,所有的formbean都放在form-beans元素裏定義 -->

    <form-beans>

        <!-- 定義了一個formbean,確定formbean名和實現類 -->

        <form-bean name="loginForm" type="lee.LoginForm"/>

    </form-beans>

    <!-- 定義action部分,所有的action都放在action-mapping元素裏定義 -->

    <action-mappings>

        <!-- 這裏只定義了一個action。而且沒有指定該action的type元素 -->

        <action path="/login" name="loginForm"

            scope="request" validate="true" input="/login.jsp" >

            <!-- 定義action內的兩個局部forward元素 -->

            <forward name="input" path="/login.jsp"/>

            <forward name="welcome" path="/welcome.html"/>

        </action>

    </action-mappings>

    <!-- 使用DelegatingRequestProcessor替換RequestProcessor -->

    <controller processorClass="org.springframework.web.struts. 
    DelegatingRequestProcessor"/>

    <!-- 加載國際化的資源包 -->

    <message-resources parameter="mess"/>

    <!-- 裝載驗證的資源文件 -->

    <plug-in className="org.apache.struts.validator.ValidatorPlugIn">

        <set-property property="pathnames" value="/WEB-INF/validator- 
        rules.xml,/WEB-INF/validation.xml" />

        <set-property property="stopOnFirstError" value="true"/>

    </plug-in>

    <!-- 裝載Spring配置文件,隨應用的啓動創建ApplicationContext實例 -->

    <plug-in className="org.springframework.web.struts. 
    ContextLoaderPlugIn">

        <set-property property="contextConfigLocation"

            value="/WEB-INF/applicationContext.xml,

                   /WEB-INF/action-servlet.xml"/>

    </plug-in>

</struts-config>

修改後的struts-config.xml文件,增加加載國際化資源文件。配置Struts的action不需要class屬性,完成了ApplicationContext的創建。

然後考慮web.xml文件的配置,在web.xml文件中必須配置Struts框架的加載。除此之外,因爲使用了Spring管理Struts的Action,而Action是隨HTTP請求啓動的,因此,應將Action的作用域配置成Request,爲了使用Request作用域,必須在web.xml文件中增加適當的配置。

下面是web.xml文件的代碼:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Web配置文件的根元素,以及對應的Schema信息 -->

<web-app xmlns="http://java.sun.com/xml/ns/j2ee"

    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"

    version="2.4">

    <!-- 定義一個Filter,該Filter是使用Request作用域的基礎 -->

    <filter>

        <filter-name>requestContextFilter</filter-name>

        <filter-class>org.springframework.web.filter.
        RequestContextFilter </filter-class>

    </filter>

    <!-- 定義filter-mapping,讓上面的Filter過濾所有的用戶請求 -->

    <filter-mapping>

        <filter-name>requestContextFilter</filter-name>

        <url-pattern>/*</url-pattern>

    </filter-mapping>

    <!-- 定義Struts的核心Servlet -->

    <servlet>

        <servlet-name>action</servlet-name>

        <servlet-class>org.apache.struts.action.ActionServlet 
        </servlet-class>

        <load-on-startup>2</load-on-startup>

    </servlet>

    <!-- 定義Struts的核心Servlet攔截所有*.do請求 -->

    <servlet-mapping>

        <servlet-name>action</servlet-name>

        <url-pattern>*.do</url-pattern>

    </servlet-mapping>

    <!-- 關於Struts標籤庫的配置 -->

    <jsp-config>

        <!-- 配置bean標籤 -->

        <taglib>

            <taglib-uri>/tags/struts-bean</taglib-uri>

            <taglib-location>/WEB-INF/struts-bean.tld</taglib-location>

        </taglib>

        <!-- 配置html標籤 -->

        <taglib>

            <taglib-uri>/tags/struts-html</taglib-uri>

            <taglib-location>/WEB-INF/struts-html.tld</taglib-location>

        </taglib>

        <!-- 配置logic標籤 -->

        <taglib>

            <taglib-uri>/tags/struts-logic</taglib-uri>

            <taglib-location>/WEB-INF/struts-logic.tld</taglib-location>

        </taglib>

    </jsp-config>

</web-app>

posted @ 2009-07-19 10:22 jadmin 閱讀(2) 評論(0) 編輯

Spring的事務(2)

6.3.2 Spring事務策略的優勢

雖然在上面的配置片段中,僅僅配置了JDBC局部事務管理器、Hibernate局部事務管理器、JDBC全局事務管理器等。但Spring支持大部分持久化策略的事務管理器。

不論採用何種持久化策略,Spring都提供了一致的事務抽象,因此,應用開發者能在任何環境下,使用一致的編程模型。無須更改代碼,應用就可在不同的事務管理策略中切換。Spring同時支持聲明式事務管理和編程式事務管理。

使用編程式事務管理,開發者使用的是Spring事務抽象,而無須使用任何具體的底層事務API。Spring的事務管理將代碼從底層具體的事務API中抽象出來,該抽象可以使用在任何底層事務基礎之上。

使用聲明式策略,開發者通常書寫很少的事務管理代碼,因此,不依賴Spring或任何其他事務API。Spring的聲明式事務無須任何額外的容器支持,Spring容器本身管理聲明式事務。使用聲明事務策略,無須在業務代碼中書寫任何事務代碼,可以讓開發者更好地專注於業務邏輯的實現。Spring管理的事務支持多個事務資源的跨越,但無法支持跨越遠程調用的事務上下文傳播。

6.3.3 使用TransactionProxyFactoryBean創建事務代理

Spring同時支持編程式事務策略和聲明式事務策略,大部分時候,都推薦採用聲明式事務策略,使用聲明事務策略的優勢十分明顯:

   ● 聲明式事務能大大降低開發者的代碼書寫量。而且聲明式事務幾乎不需要影響應用的代碼。因此,無論底層事務策略如何變化,應用程序無須任何改變。

   ● 應用程序代碼無須任何事務處理代碼,可以更專注於業務邏輯的實現。

   ● Spring則可對任何POJO的方法提供事務管理,而且Spring的聲明式事務管理無須容器的支持,可在任何環境下使用。

   ● EJB的CMT無法提供聲明式回滾規則。而通過配置文件,Spring可指定事務在遇到特定異常時自動回滾。Spring不僅可在代碼中使用setRollbackOnly回滾事務,也可在配置文件中配置回滾規則。

   ● 由於Spring採用AOP的方式管理事務,因此,可以在事務回滾動作中插入用戶自己的動作,而不僅僅是執行系統默認的回滾。

提示:本節不打算全面介紹Spring的各種事務策略,因此本節不會介紹編程式事務。如果讀者需要更全面瞭解Spring事務的相關方面,請參閱筆者所著的《Spring2.0寶典》     一書。

對於採用聲明式事務策略,可以使用TransactionProxyFactoryBean來配置事務代理Bean。正如它的類名所暗示的,它是一個工廠Bean,工廠Bean用於生成一系列的Bean實例,這一系列的Bean實例都是Proxy。

可能讀者已經想到了,既然TransactionProxyFactoryBean產生的是代理Bean,可見這種事務代理正是基於Spring AOP組件的。配置TransactionProxyFactoryBean時,一樣需要指定目標Bean。

每個TransactionProxyFactoryBean爲一個目標Bean生成事務代理,事務代理的方法改寫了目標Bean的方法,就是在目標Bean的方法執行之前加入開始事務,在目標Bean的方法正常結束之前提交事務,如果遇到特定異常則回滾事務。

TransactionProxyFactoryBean創建事務代理時,需要了解當前事務所處的環境,該環境屬性通過PlatformTransactionManager實例傳入,而相關事務傳入規則在TransactionProxy- FactoryBean的定義中給出。

下面給出聲明式事務配置文件的完整代碼:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及Spring配置文件的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0. 
    ComboPooledDataSource" destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="com.mysql.jdbc.Driver"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="jdbc:mysql://localhost/j2ee"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="root"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="32147"/>

        <!-- 指定連接數據庫連接池的最大連接數 -->

        <property name="maxPoolSize" value="40"/>

        <!-- 指定連接數據庫連接池的最小連接數 -->

        <property name="minPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的初始化連接數 -->

        <property name="initialPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的連接最大空閒時間 -->

        <property name="maxIdleTime" value="20"/>

    </bean>

    <!-- 定義Hibernate的SessionFactory -->

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3. 
    LocalSessionFactoryBean">

        <!-- 依賴注入SessionFactory所需的數據源,正是上文定義的dataSource -->

        <property name="dataSource" <ref="dataSource"/>

        <!-- mappingResources屬性用來列出全部映射文件 -->

        <property name="mappingResources">

            <list>

                <!-- 以下用來列出所有的PO映射文件 -->

                <value>lee/Person.hbm.xml</value>

            </list>

        </property>

          <!-- 定義Hibernate的SessionFactory屬性 -->

        <property name="hibernateProperties">

             <props>

                <!-- 指定Hibernate的連接方言 -->

                <prop key="hibernate.dialect">org.hibernate.dialect. 
                MySQLDialect</prop>

                <!-- 是否根據Hiberante映射創建數據表時,選擇create、update、
                create-drop -->

                  <prop key="hibernate.hbm2ddl.auto">update</prop>

             </props>

        </property>

    </bean>

    <!-- 配置DAO Bean,該Bean將作爲目標Bean使用 -->

    <bean id="personDAOTarget" class="lee.PersonDaoImpl">

        <!-- 採用依賴注入來傳入SessionFactory的引用 -->

        <property name="sessionFactory" ref="sessionFactory"/>

    </bean>

    <!-- 配置Hibernate的事務管理器 -->

    <!-- 使用HibernateTransactionManager類,該類實現PlatformTransactionManager
    接口,針對採用Hibernate持久化連接的特定實現 -->

    <bean id="transactionManager"

        class="org.springframework.orm.hibernate3.
        HibernateTransactionManager">

        <!-- HibernateTransactionManager Bean,它需要依賴注入一個SessionFactory
        Bean的引用 -->

        <property name="sessionFactory" ref="sessionFactory"/>

    </bean>

    <!-- 配置personDAOTarget Bean的事務代理 -->

    <bean id="personDAO"

        class="org.springframework.transaction.interceptor. 
        TransactionProxyFactoryBean">

        <!-- 依賴注入PlatformTransactionManager的bean引用,此處使用
        Hibernate的bean -->

        <!-- 局部事務器,因此transactionManager 傳入Hibernate事務管理器的
        引用 -->

           <property name="transactionManager" ref="transactionManager"/>

        <!-- 需要生成代理的目標bean -->

           <property name="target" ref="personDAOTarget"/>

        <!-- 指定事務屬性 -->

           <property name="transactionAttributes">

               <props>

                <!-- 以下部分爲定義事務回滾規則 -->

                   <prop key="insert*">PROPAGATION_REQUIRED, 
                -MyCheckedException</prop>

                   <prop key="update*">PROPAGATION_REQUIRED</prop>

                   <prop key="*">PROPAGATION_REQUIRED,readOnly</prop>

            </props>

        </property>

    </bean>

</beans>

在上面的定義文件中,沒有對DAO對象採用Service層包裝。通常情況下,DAO層上應有一層Service層。事務代理則以Service層Bean爲目標Bean。此處爲了簡化配置,TransactionProxyFactoryBean直接以DAO bean作爲目標bean,這一點不會影響事務代理的生成。

事務回滾規則部分定義了三個回滾規則:

第一個回滾規則表示所有以insert開始的方法,都應該滿足該事務規則。PROPAGATION_REQUIRED事務傳播規則指明,該方法必須處於事務環境中,如果當前執行線程已處於事務環境下,則直接執行;否則,啓動新的事務然後執行該方法。該規則還指定,如果方法拋出MyCheckedException的實例及其子類的實例,則強制回滾。MyCheckedException前的“-”表示強制回滾;“+”則表示強制提交,即某些情況下,即使拋出異常也強制提交;

第二個回滾規則表示所有以update開頭的方法,都遵守PROPAGATION_REQUIRED的事務傳播規則;

第三個回滾規則表示除前面規定的方法外,其他所有方法都採用PROPAGATION_ REQUIRED事務傳播規則,而且只讀。

常見的事務傳播規則有如下幾個:

   ● PROPAGATION_MANDATORY,要求調用該方法的線程必須處於事務環境中,否則拋出異常。

   ● PROPAGATION_NESTED,如果執行該方法的線程已處於事務環境下,依然啓動新的事務,方法在嵌套的事務裏執行。如果執行該方法的線程序並未處於事務中,也啓動新的事務,然後執行該方法,此時與PROPAGATION_REQUIRED相同。

   ● PROPAGATION_NEVER,不允許調用該方法的線程處於事務環境下,如果調用該方法的線程處於事務環境下,則拋出異常。

   ● PROPAGATION_NOT_SUPPORTED,如果調用該方法的線程處在事務中,則先暫停當前事務,然後執行該方法。

   ● PROPAGATION_REQUIRED,要求在事務環境中執行該方法,如果當前執行線程已處於事務中,則直接調用;如果當前執行線程不處於事務中,則啓動新的事務後執行該方法。

   ● PROPAGATION_REQUIRES_NEW,該方法要求有一個線程在新的事務環境中執行,如果當前執行線程已處於事務中,先暫停當前事務,啓動新的事務後執行該方法;如果當前調用線程不處於事務中,則啓動新的事務後執行該方法。

   ● PROPAGATION_SUPPORTS,如果當前執行線程處於事務中,則使用當前事務,否則不使用事務。

程序裏原來使用personDAO的地方,無須變化。因爲,配置文件裏將personDAO目標Bean的id改成personDAOTarget,爲TransactionProxyFactoryBean工廠Bean所產生的代理Bean命名爲personDAO。該代理Bean會包含原有personDAO的所有方法,而且爲這些方法增加了不同的事務處理規則。

程序面向PersonDaoImpl類所實現的接口編程,TransactionProxyFactoryBean生成的代理Bean也會實現TransactionProxyFactoryBean接口。因此,原有的程序中調用DAO組件的代碼無須任何改變。程序運行時,由事務代理完成原來目標Bean完成的工作。

事實上,Spring不僅支持對接口的代理,整合CGLIB後,Spring甚至可對具體類生成代理。只要設置proxyTargetClass屬性爲true就可以。如果目標Bean沒有實現任何接口,proxyTargetClass屬性默認被設爲true,此時Spring會對具體類生成代理。當然,通常建議面向接口編程,而不要面向具體的實現類編程。

6.3.4 使用繼承簡化事務配置

仔細觀察配置文件中兩個事務代理Bean的配置時,發現兩個事務代理Bean的大部分配置完全相同,如果配置文件中包含大量這樣的事務代理Bean配置,配置文件將非常臃腫。考慮到大部分事務代理Bean的配置都大同小異,可以使用Bean繼承來簡化事務代理的配置。

正如前面部分介紹的,Bean繼承是將所有子Bean中相同的配置定義成一個模板,並將此模板Bean定義成一個抽象Bean。考慮所有事務代理Bean中,有如下部分是大致相   同的:

   ● 事務代理Bean所使用的事務管理器。

   ● 事務傳播規則。

因此,現在配置文件中定義如下的事務代理模板Bean,其配置代碼如下:

<!-- 定義所有事務代理Bean的模板 -->

<bean id="txProxyTemplate" abstract="true"

        class="org.springframework.transaction.interceptor.
        TransactionProxyFactoryBean">

    <!-- 爲事務代理Bean注入生成代理所需的PlatformTransactionManager實例 -->

    <property name="transactionManager" ref="transactionManager"/>

        <!-- 定義生成事務代理通用的事務屬性 -->

        <property name="transactionAttributes">

            <props>

                <!-- 所有的方法都應用PROPAGATION_REQUIRED的事務傳播規則 -->

                <prop key="*">PROPAGATION_REQUIRED</prop>

            </props>

    </property>

</bean>

而真正的事務代理Bean,則改爲繼承上面的事務模板Bean。考慮到將目標Bean定義在Spring容器中可能增加未知的風險,因此將目標Bean定義成嵌套Bean。

<!-- 讓事務代理Bean繼承模板Bean -->

<bean id="personDAO" parent="txProxyTemplate">

    <!-- 這裏採用嵌套Bean的方式來定義目標Bean,當然也可以引用已存在的Bean -->

    <property name="target">

        <bean class="lee.personDAO"/>

    </property>

</bean>

此時的personDAO Bean無須具體地定義事務屬性,它將在其父Bean txProxyTemplate中獲取事務定義屬性。此處採用嵌套Bean來定義目標Bean,因此,並未將目標Bean直接暴露在Spring的上下文中讓其他模塊調用。當然,也可採用一個已經存在的Bean作爲目標Bean;子Bean的事務屬性定義,完全可覆蓋事務代理模板裏的事務屬性定義。如下例所示:

<!-- 讓事務代理bean繼承模板Bean -->

<bean id="personDAO" parent="txProxyTemplate">

    <!-- 這裏,採用引用已存在的bean的方式來定義目標Bean -->

    <property name="target" ref ="personDAOTarget"/>

    <!-- 覆蓋事務代理模板bean中的事務屬性定義 -->

    <property name="transactionAttributes">

        <props>

            <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>

            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>

            <prop key="load*">PROPAGATION_REQUIRED,readOnly</prop>

         </props>

</property>

</bean>

可見,採用Bean繼承方式定義事務代理的方式,可以很好地簡化事務代理的配置,可以避免配置事務代理Bean時的冗餘配置。

提示:使用Bean繼承可以很好地簡化事務代理Bean的配置,通過將各事務代理Bean共同的配置信息提取成事務模板Bean,可以讓實際的事務代理Bean的配置更加簡潔;而且,配置方式相當直觀。儘量將目標Bean配置成嵌套Bean,這樣的方式可以保證更好的內聚性。

如果讀者還記得前面介紹的AOP知識,應該知道還有一種更加簡潔的配置,就是利用Bean後處理器,讓Bean後處理器爲容器中其他Bean自動創建事務代理。

6.3.5 使用自動創建代理簡化事務配置

回顧6.2.6節和6.2.7節,讀者可能已經想到如何自動創建代理。是的,正是通過6.2.6節和6.2.7節所給出的兩個自動代理創建類來生成事務代理。

正如前文已經提到的,使用BeanNameAutoProxyCreator和DefaultAdvisorAutoProxy- Creator來創建代理時,並不一定是創建事務代理,關鍵在於傳入的攔截器,如果傳入事務攔截器,將可自動生成事務代理。

下面是使用BeanNameAutoProxyCreator自動生成事務代理的配置文件:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及相應的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0. 
    ComboPooledDataSource" destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="com.mysql.jdbc.Driver"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="jdbc:mysql://localhost/j2ee"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="root"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="32147"/>

        <!-- 指定連接數據庫連接池的最大連接數 -->

        <property name="maxPoolSize" value="40"/>

        <!-- 指定連接數據庫連接池的最小連接數 -->

        <property name="minPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的初始化連接數 -->

        <property name="initialPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的連接最大空閒時間 -->

        <property name="maxIdleTime" value="20"/>

    </bean>

    <!-- 使用JDBC的局部事務策略 -->

    <bean id="transactionManager"

        class="org.springframework.jdbc.datasource.DataSource-
        TransactionManager">

        <!-- 爲事務管理器注入所需的數據源Bean -->

        <property name="dataSource" ref="dataSource"/>

    </bean>

    <!-- 配置目標Bean,該目標Bean將由Bean後處理器自動生成代理 -->

    <bean id="test1" class="lee.TransactionTestImpl">

        <!-- 依賴注入目標Bean所必需的數據源Bean -->

        <property name="ds" ref="dataSource"/>

    </bean>

    <!-- 配置目標Bean,該目標Bean將由Bean後處理器自動生成代理 -->

    <bean id="test2" class="lee.TestImpl">

        <!-- 依賴注入目標Bean所必需的數據源Bean -->

        <property name="ds" ref="dataSource"/>

    </bean>

    <!-- 配置事務攔截器Bean -->

    <bean id="transactionInterceptor"

        class="org.springframework.transaction.interceptor. 
        TransactionInterceptor">

        <!-- 事務攔截器bean需要依賴注入一個事務管理器 -->

        <property name="transactionManager" ref="transactionManager"/>

        <property name="transactionAttributes">

            <!-- 下面定義事務傳播屬性 -->

            <props>

                <prop key="insert*">PROPAGATION_REQUIRED</prop>

                <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>

                <prop key="*">PROPAGATION_REQUIRED</prop>

            </props>

        </property>

    </bean>

    <!-- 定義BeanNameAutoProxyCreator的Bean後處理器 -->

    <bean class="org.springframework.aop.framework.autoproxy. 
    BeanNameAutoProxyCreator">

    <!-- 指定對滿足哪些bean name的bean自動生成業務代理 -->

        <property name="beanNames">

            <!-- 下面是所有需要自動創建事務代理的Bean -->

            <list>

                <value>test1</value>

                <value>test2</value>

            </list>

            <!-- 此處可增加其他需要自動創建事務代理的Bean -->

        </property>

        <!-- 下面定義BeanNameAutoProxyCreator所需的攔截器 -->

        <property name="interceptorNames">

            <list>

                <value>transactionInterceptor</value>

                <!-- 此處可增加其他新的Interceptor -->

            </list>

        </property>

    </bean>

</beans>

如果配置文件中僅有兩個目標Bean,可能不能很清楚地看出這種自動創建代理配置方式的優勢,但如果有更多目標Bean需要自動創建事務代理,則可以很好地體會到這種配置方式的優勢:配置文件只需要簡單地配置目標Bean,然後在BeanNameAutoProxyCreator配置中增加一行即可。

提示:使用BeanNameAutoProxyCreator可以自動創建事務代理,使用DefaultAdvisor- AutoProxyCreator也可自動創建事務代理。關於後一個Bean後處理器的配置方式,請參看前面6.2.7節的內容。

posted @ 2009-07-19 10:18 jadmin 閱讀(5) 評論(0) 編輯

Spring的事務(1)

6.3 Spring的事務

Spring的事務管理不需與任何特定的事務API耦合。對不同的持久層訪問技術,編程式事務提供一致的事務編程風格,通過模板化的操作一致性地管理事務。聲明式事務基於Spring AOP實現,卻並不需要程序開發者成爲AOP專家,亦可輕易使用Spring的聲明式事務管理。

6.3.1 Spring支持的事務策略

Spring事務策略是通過PlatformTransactionManager接口體現的,該接口是Spring事務策略的核心。該接口的源代碼如下:

public interface PlatformTransactionManager

{

    //平臺無關的獲得事務的方法

    TransactionStatus getTransaction(TransactionDefinition definition)

        throws TransactionException;

    //平臺無關的事務提交方法

    void commit(TransactionStatus status) throws TransactionException;

    //平臺無關的事務回滾方法

    void rollback(TransactionStatus status) throws TransactionException;

}

PlatformTransactionManager是一個與任何事務策略分離的接口,隨着底層不同事務策略切換,應用必須採用不同的實現類。PlatformTransactionManager接口沒有與任何事務資源捆綁在一起,它可以適應於任何的事務策略,結合Spring的IoC容器,可以向PlatformTransactionManager注入相關的平臺特性。

PlatformTransactionManager接口有許多不同的實現類,應用程序面向與平臺無關的接口編程,對不同平臺的底層支持,由PlatformTransactionManager接口的實現類完成。從而,應用程序無須與具體的事務API耦合。因此,使用PlatformTransactionManager接口,可將代碼從具體的事務API中解耦出來。

即使使用特定容器管理的JTA,代碼依然無須執行JNDI查找,無須與特定的JTA資源耦合在一起。通過配置文件,JTA資源傳給PlatformTransactionManager的實現類。因此,程序的代碼可在JTA事務管理和非JTA事務管理之間輕鬆切換。

在PlatformTransactionManager接口內,包含一個getTransaction(TransactionDefinition definition)方法,該方法根據一個TransactionDefinition參數,返回一個TransactionStatus對象。TransactionStatus對象表示一個事務。TransactionStatus被關聯在當前執行的線程。

getTransaction(TransactionDefinition definition)返回的TransactionStatus對象,可能是一個新的事務,也可能是一個已經存在的事務對象。如果當前執行的線程已經處於事務管理下,返回當前線程的事務對象,否則,返回當前線程的調用堆棧已有的事務對象。

TransactionDefinition接口定義了一個事務規則,該接口必須指定如下幾個屬性值:

   ● 事務隔離,當前事務和其他事務的隔離程度。例如,這個事務能否看到其他事務未提交的數據等。

   ● 事務傳播,通常,在事務中執行的代碼都會在當前事務中運行。但是,如果一個事務上下文已經存在,有幾個選項可指定該事務性方法的執行行爲。例如,大多數情況下,簡單地在現有的事務上下文中運行;或者掛起現有事務,創建一個新的事務。Spring提供EJB CMT(Contain Manager Transaction,容器管理事務)中所有的事務傳播選項。

   ● 事務超時,事務在超時前能運行多久。事務的最長持續時間。如果事務一直沒有被提交或回滾,將在超出該時間後,系統自動回滾事務。

   ● 只讀狀態,只讀事務不修改任何數據。在某些情況下(例如使用Hibernate時),只讀事務是非常有用的優化。

TransactionStatus代表事務本身,它提供了簡單的控制事務執行和查詢事務狀態的方法。這些方法在所有的事務API中都是相同的。TransactionStatus接口的源代碼如下:

public interface TransactionStatus

{

    //判斷事務是否是新建的事務

    boolean isNewTransaction();

    //設置事務回滾

    void setRollbackOnly();

    //查詢事務是否已有回滾標誌

    boolean isRollbackOnly();

}

Spring的事務管理由PlatformTransactionManager的不同實現類完成。在Spring上下文中配置PlatformTransactionManager Bean時,必須針對不同環境提供不同的實現類。

下面提供不同的持久層訪問環境,及其對應的PlatformTransactionManager實現類的 配置。

JDBC數據源的局部事務策略:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及Spring配置文件的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

       http://www.springframework.org/schema/beans/spring-beans.xsd">

<!-- 定義數據源Bean,使用C3P0數據源實現 -->

<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">

    <!-- 指定連接數據庫的驅動 -->

    <property name="driverClass" value="com.mysql.jdbc.Driver"/>

    <!-- 指定連接數據庫的URL -->

    <property name="jdbcUrl" value="jdbc:mysql://localhost/j2ee"/>

    <!-- 指定連接數據庫的用戶名 -->

    <property name="user" value="root"/>

    <!-- 指定連接數據庫的密碼 -->

    <property name="password" value="32147"/>

    <!-- 指定連接數據庫連接池的最大連接數 -->

    <property name="maxPoolSize" value="40"/>

    <!-- 指定連接數據庫連接池的最小連接數 -->

    <property name="minPoolSize" value="1"/>

    <!-- 指定連接數據庫連接池的初始化連接數 -->

    <property name="initialPoolSize" value="1"/>

    <!-- 指定連接數據庫連接池的連接最大空閒時間 -->

    <property name="maxIdleTime" value="20"/>

</bean>

<!-- 配置JDBC數據源的局部事務管理器 -->

<!-- 使用DataSourceTransactionManager 類,該類實現PlatformTransactionManager接口 -->

<!-- 針對採用數據源連接的特定實現 -->

<bean id="transactionManager"

        class="org.springframework.jdbc.datasource.
        DataSourceTransactionManager">

        <!-- DataSourceTransactionManager bean需要依賴注入一個DataSource 
        bean的引用 -->

         <property name="dataSource" ref="dataSource"/>

    </bean>

</beans>

對於容器管理JTA數據源,全局事務策略的配置文件如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及Spring配置文件的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置JNDI數據源Bean -->

    <bean id="dataSource" class="org.springframework.jndi. 
    JndiObjectFactoryBean">

    <!-- 容器管理數據源的JNDI -->

         <property name="jndiName" value="jdbc/jpetstore"/>

    </bean>

    <!-- 使用JtaTransactionManager類,該類實現PlatformTransactionManager接
    口 -->

    <!-- 針對採用全局事務管理的特定實現 -->

    <!-- JtaTransactionManager不需要知道數據源,或任何其他特定資源 -->

    <!-- 因爲它使用容器的全局事務管理 -->

    <bean id="transactionManager"

        class="org.springframework.transaction.jta. 
        JtaTransactionManager" />

</beans>

對於採用Hibernate持久層訪問策略時,局部事務策略的配置文件如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及Spring配置文件的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0. 
    ComboPooledDataSource" destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="com.mysql.jdbc.Driver"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="jdbc:mysql://localhost/j2ee"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="root"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="32147"/>

        <!-- 指定連接數據庫連接池的最大連接數 -->

        <property name="maxPoolSize" value="40"/>

        <!-- 指定連接數據庫連接池的最小連接數 -->

        <property name="minPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的初始化連接數 -->

        <property name="initialPoolSize" value="1"/>

        <!-- 指定連接數據庫連接池的連接最大空閒時間 -->

        <property name="maxIdleTime" value="20"/>

    </bean>

    <!-- 定義Hibernate的SessionFactory -->

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3. 
    LocalSessionFactoryBean">

        <!-- 依賴注入SessionFactory所需的數據源,正是上文定義的dataSource -->

        <property name="dataSource" ref="dataSource"/>

        <!-- mappingResources屬性用來列出全部映射文件 -->

        <property name="mappingResources">

            <list>

                <!-- 以下用來列出所有的PO映射文件 -->

                <value>lee/MyTest.hbm.xml</value>

            </list>

        </property>

          <!-- 定義Hibernate的SessionFactory的屬性 -->

        <property name="hibernateProperties">

             <props>

                <!-- 指定Hibernate的連接方言 -->

                <prop key="hibernate.dialect">org.hibernate.dialect. 
                MySQLDialect</prop>

                <!-- 是否根據Hibernate映射創建數據表時,選擇create、update、
                create-drop -->

                  <prop key="hibernate.hbm2ddl.auto">update</prop>

             </props>

        </property>

    </bean>

    <!-- 配置Hibernate的局部事務管理器 -->

    <!-- 使用HibernateTransactionManager類,該類是PlatformTransactionManager
    接口,針對採用Hibernate持久化連接的特定實現 -->

    <bean id="transactionManager"

    class="org.springframework.orm.hibernate3. 
    HibernateTransactionManager">

            <!-- HibernateTransactionManager Bean需要依賴注入一個
            SessionFactorybean的引用 -->

         <property name="sessionFactory" ref="sessionFactory"/>

     </bean>

</beans>

對於採用Hibernate持久層訪問策略時,全局事務策略的配置文件如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及Spring配置文件的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置JNDI數據源Bean -->

    <bean id="dataSource" class="org.springframework.jndi. 
    JndiObjectFactoryBean">

        <!-- 容器管理數據源的JNDI -->

         <property name="jndiName" value="jdbc/jpetstore"/>

    </bean>

    <!--定義Hibernate的SessionFactory -->

    <bean id="sessionFactory" class="org.springframework.orm.hibernate3. 
    LocalSessionFactoryBean">

        <!-- 依賴注入SessionFactory所需的數據源,正是上文定義的dataSource Bean -->

        <property name="dataSource" ref="dataSource"/>

        <!-- mappingResources屬性用來列出全部映射文件 -->

        <property name="mappingResources">

            <list>

                  <!-- 以下用來列出所有的PO映射文件 -->

                <value>lee/MyTest.hbm.xml</value>

            </list>

        </property>

          <!-- 定義Hibernate的SessionFactory的屬性 -->

        <property name="hibernateProperties">

             <props>

                <!-- 指定Hibernate的連接方言 -->

                <prop key="hibernate.dialect">org.hibernate.dialect. 
                MySQLDialect</prop>

                <!-- 是否根據Hiberante映射創建數據表時,選擇create、update、
                create-drop -->

                  <prop key="hibernate.hbm2ddl.auto">update</prop>

             </props>

          </property>

    </bean>

    <!-- 使用JtaTransactionManager類,該類是PlatformTransactionManager接口,
            針對採用數據源連接的特定實現 -->

    <!-- JtaTransactionManager不需要知道數據源,或任何其他特定資源,

            因爲使用容器的全局事務管理 -->

    <bean id="transactionManager"

           class="org.springframework.transaction.jta.
           JtaTransactionManager" />

</beans>

不論採用哪種持久層訪問技術,只要使用JTA數據源,Spring事務管理器的配置都是一樣的,因爲它們都採用的是全局事務管理。

可以看到,僅僅通過配置文件的修改,就可以在不同的事務管理策略間切換,即使從局部事務到全局事務的切換。

提示:Spring所支持的事務策略非常靈活,Spring的事務策略允許應用程序在不同事務策略之間自由切換,即使需要在局部事務策略和全局事務策略之間切換,只需要修改配置文件,而應用程序的代碼無須任何改變。這種靈活的設計,又何嘗不是因爲面向接口編程帶來的優勢,可見面向接口編程給應用程序更好的適應性。

posted @ 2009-07-19 10:18 jadmin 閱讀(2) 評論(0) 編輯

Spring的AOP(2)

6.2.4 代理接口

當目標Bean的實現類實現了接口後,Spring AOP可以爲其創建JDK動態代理,而無須使用CGLIB創建的代理,這種代理稱爲代理接口。

創建AOP代理必須指定兩個屬性:目標Bean和處理。實際上,很多AOP框架都以攔截器作爲處理。因爲Spring AOP與IoC容器的良好整合,因此配置代理Bean時,完全可以利用依賴注入來管理目標Bean和攔截器Bean。

下面的示例演示了基於AOP的權限認證,它是簡單的TestService接口,該接口模擬Service組件,該組件內包含兩個方法:

   ● 查看數據。

   ● 修改數據。

接口的源代碼如下:

//Service組件接口

public interface TestService

{

    //查看數據

    void view();

    //修改數據

    void modify();

}

該接口的實現類實現兩個方法。因爲篇幅限制,本示例並未顯示出完整的查看數據和修改數據的持久層操作,僅僅在控制檯打印兩行信息。實際的項目實現中,兩個方法的實現則改成對持久層組件的調用,這不會影響示例程序的效果。實現類的源代碼如下:

TestService接口的實現類

public class TestServiceImpl implements TestService

{

    //實現接口必須實現的方法

    public void view()

    {

        System.out.println("用戶查看數據");

    }

    //實現接口必須實現的方法

    public void modify()

    {

        System.out.println("用戶修改數據");

    }

}

示例程序採用Around 處理作爲攔截器,攔截器中使用依賴注入獲得當前用戶名。實際Web應用中,用戶應該從session中讀取。這不會影響示例代碼的效果。攔截器源代碼如下:

public class AuthorityInterceptor implements MethodInterceptor

{

    //當前用戶名

    private String user;

    //依賴注入所必需的setter方法

    public void setUser(String user)

    {

        this.user = user;

    }

    public Object invoke(MethodInvocation invocation) throws Throwable

    {

        //獲取當前攔截的方法名

        String methodName = invocation.getMethod().getName();

        //下面執行權限檢查

        //對既不是管理員,也不是註冊用戶的情況

        if (!user.equals("admin") && !user.equals("registedUser"))

        {

            System.out.println("您無權執行該方法");

            return null;

        }

        //對僅僅是註冊用戶,調用修改數據的情況

        else if (user.equals("registedUser") && methodName.equals 
        ("modify"))

        {

            System.out.println("您不是管理員,無法修改數據");

            return null;

        }

        //對管理員或註冊用戶,查看數據的情況

        else

        {

            return invocation.proceed();

        }

    }

}

TestAction類依賴TestService。因篇幅關係,此處不給出TestAction的接口的源代碼,TestActionImpl的源代碼如下:

public class TestActionImpl

{

    //將TestService作爲成員變量,面向接口編程

    private TestService ts;

    //依賴注入的setter方法

    public void setTs(TestService ts)

    {

        this.ts = ts;

    }

    //修改數據

    public void modify()

    {

        ts.modify();

    }

    //查看數據

    public void view()

    {

        ts.view();

    }

}

配置文件如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及Spring配置文件的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置目標Bean -->

    <bean id="serviceTarget" class="lee.TestServiceImpl"/>

    <!-- 配置攔截器,攔截器作爲處理使用 -->

    <bean id="authorityInterceptor" class="lee.AuthorityInterceptor">

        <property name="user" value="admin"/>

    </bean>

    <!-- 配置代理工廠Bean,負責生成AOP代理 -->

    <bean id="service" class="org.springframework.aop.framework. 
    ProxyFactoryBean">

        <!-- 指定AOP代理所實現的接口 -->

        <property name="proxyInterfaces" value="lee.TestService"/>

        <!-- 指定AOP代理所代理的目標Bean -->

        <property name="target" ref="serviceTarget"/>

        <!-- AOP代理所需要的攔截器列表 -->

        <property name="interceptorNames">

            <list>

                <value>authorityInterceptor</value>

            </list>

        </property>

    </bean>

    <!-- 配置Action Bean,該Action依賴TestService Bean -->

    <bean id="testAction" class="lee.TestActionImpl">

        <!-- 此處注入的是依賴代理Bean -->

        <property name="ts" ref="service"/>

    </bean>

</beans>

主程序請求testAction Bean,然後調用該Bean的兩個方法,主程序如下:

public class BeanTest

{

    public static void main(String[] args)throws Exception

    {

        //創建Spring容器實例

        ApplicationContext ctx = new FileSystemXmlApplicationContext 
        ("bean.xml");

        //獲得TestAction bean

        TestAction ta = (TestAction)ctx.getBean("testAction");

        //調用bean的兩個測試方法

        ta.view();

        ta.modify();

    }

}

程序執行結果如下:

[java] 用戶查看數據

[java] 用戶修改數據

代理似乎沒有發揮任何作用。因爲配置文件中的當前用戶是admin,admin用戶具備訪問和修改數據的權限,因此代理並未阻止訪問。將配置文件中的admin修改成registed- User,再次執行程序,得到如下結果:

[java] 用戶查看數據

[java] 您不是管理員,無法修改數據

代理阻止了registedUser修改數據,查看數據可以執行。將registedUser修改成其他用戶,執行程序,看到如下結果:

[java] 您無權執行該方法

[java] 您無權執行該方法

代理阻止用戶對兩個方法的執行。基於AOP的權限檢查,可以降低程序的代碼量,因爲無須每次調用方法之前,手動編寫權限檢查代碼;同時,權限檢查與業務邏輯分離,提高了程序的解耦。

示例中的目標Bean被暴露在容器中,可以被客戶端代碼直接訪問。爲了避免客戶端代碼直接訪問目標Bean,可以將目標Bean定義成代理工廠的嵌套Bean,修改後的配置文件如下:

<?xml version="1.0" encoding="GBK"?>

<!-- 指定Spring配置文件的根元素,以及Spring配置文件的Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans

       http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置攔截器,攔截器作爲處理使用 -->

    <bean id="authorityInterceptor" class="lee.AuthorityInterceptor">

        <property name="user" value="admin"/>

    </bean>

    <!-- 配置代理工廠Bean,該工廠Bean將負責創建目標Bean的代理 -->

    <bean id="service" class="org.springframework.aop.framework. 
    ProxyFactoryBean">

        <!-- 指定AOP代理所實現的接口 -->

        <property name="proxyInterfaces" value="lee.TestService"/>

        <property name="target">

            <!-- 以嵌套Bean的形式定義目標Bean,避免客戶端直接訪問目標Bean -->

            <bean class="lee.TestServiceImpl"/>

        </property>

        <!-- AOP代理所需要的攔截器列表 -->

        <property name="interceptorNames">

            <list>

                <value>authorityInterceptor</value>

            </list>

        </property>

    </bean>

    <!-- 配置Action Bean,該Action依賴TestService Bean -->

    <bean id="testAction" class="lee.TestActionImpl">

        <!-- 此處注入的是依賴代理Bean -->

        <property name="ts" ref="service"/>

    </bean>

</beans>

由上面介紹的內容可見,Spring的AOP是對JDK動態代理模式的深化。通過Spring AOP組件,允許通過配置文件管理目標Bean和AOP所需的處理。

下面將繼續介紹如何爲沒有實現接口的目標Bean創建CGLIB代理。

6.2.5 代理類

如果目標類沒有實現接口,則無法創建JDK動態代理,只能創建CGLIB代理。如果需要沒有實現接口的Bean實例生成代理,配置文件中應該修改如下兩項:

   ● 去掉<property name="proxyInterfaces"/>聲明。因爲不再代理接口,因此,此處的配置沒有意義。

   ● 增加<property name="proxyTargetClass">子元素,並設其值爲true,通過該元素強制使用CGLIB代理,而不是JDK動態代理。

注意:最好面向接口編程,不要面向類編程。同時,即使在實現接口的情況下,也可強制使用CGLIB代理。

CGLIB代理在運行期間產生目標對象的子類,該子類通過裝飾器設計模式加入到Advice中。因爲CGLIB代理是目標對象的子類,則必須考慮保證如下兩點:

   ● 目標類不能聲明成final,因爲final類不能被繼承,無法生成代理。

   ● 目標方法也不能聲明成final,final方法不能被重寫,無法得到處理。

當然,爲了需要使用CGLIB代理,應用中應添加CGLIB二進制Jar文件。

6.2.6 使用BeanNameAutoProxyCreator自動創建代理

這是一種自動創建事務代理的方式,一旦在容器中配置了BeanNameAutoProxyCreator實例,該實例將會對指定名字的Bean實例自動創建代理。實際上,BeanNameAutoProxyCreator是一個Bean後處理器,理論上它會對容器中所有的Bean進行處理,實際上它只對指定名字的Bean實例創建代理。

BeanNameAutoProxyCreator根據名字自動生成事務代理,名字匹配支持通配符。

與ProxyFactoryBean一樣,BeanNameAutoProxyCreator需要一個interceptorNames屬性,該屬性名雖然是“攔截器”,但並不需要指定攔截器列表,它可以是Advisor或任何處理類型。

下面是使用BeanNameAutoProxyCreator的配置片段:

<!-- 定義事務攔截器bean -->

<bean id="transactionInterceptor"

    class="org.springframework.transaction.interceptor.
    TransactionInterceptor">

    <!-- 事務攔截器bean需要依賴注入一個事務管理器 -->

    <property name="transactionManager" ref="transactionManager"/>

    <property name="transactionAttributes">

        <!-- 下面定義事務傳播屬性 -->

        <props>

            <prop key="insert*">PROPAGATION_REQUIRED </prop>

            <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>

            <prop key="*">PROPAGATION_REQUIRED</prop>

        </props>

    </property>

    </bean>

<!-- 定義BeanNameAutoProxyCreator Bean,它是一個Bean後處理器,

        負責爲容器中特定的Bean創建AOP代理 -->

<bean class="org.springframework.aop.framework.autoproxy. 
    BeanNameAutoProxyCreator">

    <!-- 指定對滿足哪些bean name的bean自動生成業務代理 -->

    <property name="beanNames">

        <list>

            <!-- 下面是所有需要自動創建事務代理的Bean -->

            <value>core-services-applicationControllerSevice</value>

            <value>core-services-deviceService</value>

            <value>core-services-authenticationService</value>

            <value>core-services-packagingMessageHandler</value>

            <value>core-services-sendEmail</value>

            <value>core-services-userService</value>

            <!-- 此處可增加其他需要自動創建事務代理的Bean -->

        </list>

    </property>

    <!-- 下面定義BeanNameAutoProxyCreator所需的攔截器 -->

    <property name="interceptorNames">

        <list>

            <value>transactionInterceptor</value>

            <!-- 此處可增加其他新的Interceptor -->

        </list>

    </property>

</bean>

上面的片段是使用BeanNameAutoProxyCreator自動創建事務代理的片段。Transaction- Interceptor用來定義事務攔截器,定義事務攔截器時傳入事務管理器Bean,也指定事務傳播屬性。

通過BeanNameAutoProxyCreator定義Bean後處理器,定義該Bean後處理器時,通過beanNames屬性指定有哪些目標Bean生成事務代理;還需要指定“攔截器鏈”,該攔截器鏈可以由任何處理組成。

定義目標Bean還可使用通配符,使用通配符的配置片段如下所示:

<!-- 定義BeanNameAutoProxyCreator Bean,它是一個Bean後處理器,

        負責爲容器中特定的Bean創建AOP代理 -->

<bean class="org.springframework.aop.framework.autoproxy. 
BeanNameAutoProxyCreator">

    <!-- 指定對滿足哪些bean name的bean自動生成業務代理 -->

    <property name="beanNames">

        <!-- 此處使用通配符確定目標bean -->

        <value>*DAO,*Service,*Manager</value>

    </property>

    <!-- 下面定義BeanNameAutoProxyCreator所需的事務攔截器 -->

    <property name="interceptorNames">

        <list>

            <value>transactionInterceptor</value>

            <!-- 此處可增加其他新的Interceptor -->

        </list>

    </property>

</bean>

上面的配置片段中,所有名字以DAO、Service、Manager結尾的bean,將由該“bean後處理器”爲其創建事務代理。目標bean不再存在,取而代之的是目標bean的事務代理。通過這種方式,不僅可以極大地降低配置文件的繁瑣,而且可以避免客戶端代碼直接調用目標bean。

注意:雖然上面的配置片段是爲目標對象自動生成事務代理。但這不是唯一的,如果有需要,可以爲目標對象生成任何的代理。BeanNameAutoProxyCreator爲目標對象生成怎樣的代理,取決於傳入怎樣的處理Bean,如果傳入事務攔截器,則生成事務代理Bean;否則將生成其他代理,在後面的實例部分,讀者將可以看到大量使用BeanNameAutoProxy- Creator創建的權限檢查代理。

6.2.7 使用DefaultAdvisorAutoProxyCreator自動創建代理

Spring還提供了另一個Bean後處理器,它也可爲容器中的Bean自動創建代理。相比之下,DefaultAdvisorAutoProxyCreator是更通用、更強大的自動代理生成器。它將自動應用於當前容器中的advisor,不需要在DefaultAdvisorAutoProxyCreator定義中指定目標Bean的名字字符串。

這種定義方式有助於配置的一致性,避免在自動代理創建器中重複配置目標Bean 名。

使用該機制包括:

   ● 配置DefaultAdvisorAutoProxyCreator bean定義。

   ● 配置任何數目的Advisor,必須是Advisor,不僅僅是攔截器或其他處理。因爲,必須使用切入點檢查處理是否符合候選Bean定義。

DefaultAdvisorAutoProxyCreator計算Advisor包含的切入點,檢查處理是否應該被應用到業務對象,這意味着任何數目的Advisor都可自動應用到業務對象。如果Advisor中沒有切入點符合業務對象的方法,這個對象就不會被代理。如果增加了新的業務對象,只要它們符合切入點定義,DefaultAdvisorAutoProxyCreator將自動爲其生成代理,無須額外   配置。

當有大量的業務對象需要採用相同處理,DefaultAdvisorAutoProxyCreator是非常有用的。一旦定義恰當,直接增加業務對象,而不需要額外的代理配置,系統自動爲其增加     代理。

<beans>

    <!-- 定義Hibernate局部事務管理器,可切換到JTA全局事務管理器 -->

    <bean id="transactionManager"

         class="org.springframework.orm.hibernate3. 
        HibernateTransactionManager">

        <!-- 定義事務管理器時,依賴注入SessionFactory -->

         <property name="sessionFactory" ref bean="sessionFactory"/>

    </bean>

    <!-- 定義事務攔截器 -->

    <bean id="transactionInterceptor"

        class="org.springframework.transaction.interceptor. 
        TransactionInterceptor">

         <property name="transactionManager" ref="transactionManager"/>

         <property name="transactionAttributeSource">

            <props>

                <prop key="*">PROPAGATION_REQUIRED</prop>

                  <prop key="find*">PROPAGATION_REQUIRED,readOnly</prop>

            </props>

        </property>

    </bean>

    <!-- 定義DefaultAdvisorAutoProxyCreator Bean,這是一個Bean後處理器 -->

    <bean class="org.springframework.aop.framework.autoproxy. 
    DefaultAdvisorAutoProxyCreator"/>

    <!-- 定義事務Advisor -->

    <bean class="org.springframework.transaction.interceptor. 
    TransactionAttributeSourceAdvisor">

         <property name="transactionInterceptor" ref= 
        "transactionInterceptor"/>

    </bean>

    <!-- 定義額外的Advisor>

    <bean id="customAdvisor" class="lee.MyAdvisor"/>

</beans>

DefaultAdvisorAutoProxyCreator支持過濾和排序。如果需要排序,可讓Advisor實現org.springframework.core.Ordered接口來確定順序。TransactionAttributeSourceAdvisor已經實現Ordered接口,因此可配置其順序,默認是不排序。

採用這樣的方式,一樣可以避免客戶端代碼直接訪問目標Bean,而且配置更加簡潔。只要目標Bean符合切入點檢查,Bean後處理器自動爲目標Bean創建代理,無須依次指定目標Bean名。

注意:Spring也支持以編程方式創建AOP代理,但這種方式將AOP代理所需要的目標對象和處理Bean等對象的耦合降低到代碼層次,因此不推薦使用。如果讀者需要深入瞭解如何通過編程方式創建AOP代理,請參閱筆者所著的《Spring2.0寶典》。

posted @ 2009-07-19 10:16 jadmin 閱讀(1) 評論(0) 編輯

Spring的AOP(1)

6.2 Spring的AOP

AOP(Aspect Orient Programming),也就是面向切面編程,作爲面向對象編程的一種補充。問世的時間並不太長,甚至在國內的翻譯還不太統一(有些書翻譯成面向方面編程),但它確實極好地補充了面向對象編程的方式。面向對象編程將程序分解成各個層次的對象,而面向切面編程將程序運行過程分解成各個切面。

可以這樣理解,面向對象編程是從靜態角度考慮程序結構,面向切面編程是從動態角度考慮程序運行過程。

Spring AOP是Spring框架的一個重要組件,極好地補充了Spring IoC容器的功能。Spring AOP將Spring IoC容器與AOP組件緊密結合,豐富了IoC容器的功能。當然,即使不使用AOP組件,依然可以使用Spring的IoC容器。

6.2.1 AOP的基本概念

AOP從程序運行角度考慮程序的流程,提取業務處理過程的切面。AOP面向的是程序運行中各個步驟,希望以更好的方式來組合業務處理的各個步驟。

AOP框架並不與特定的代碼耦合,AOP框架能處理程序執行中的特定點,而不是某個具體的程序。AOP框架具有如下兩個特徵:

   ● 各步驟之間的良好隔離性。

   ● 源代碼無關性。

下面是關於面向切面編程的一些術語:

   ● 切面,業務流程運行的某個特定步驟,就是運行過程的關注點,關注點可能橫切多個對象。

   ● 連接點,程序執行過程中明確的點,如方法的調用或異常的拋出。Spring AOP中,連接點總是方法的調用,Spring並沒有顯式地使用連接點。

   ● 處理(Advice),AOP框架在特定的連接點執行的動作。處理有around、before和throws等類型。大部分框架都以攔截器作爲處理模型。

   ● 切入點,系列連接點的集合,它確定處理觸發的時機。AOP框架允許開發者自己定義切入點,如使用正則表達式。

   ● 引入,添加方法或字段到被處理的類。Spring允許引入新的接口到任何被處理的對象。例如,可以使用一個引入,使任何對象實現IsModified接口,以此來簡化緩存。

   ● 目標對象,包含連接點的對象。也稱爲被處理對象或被代理對象。

   ● AOP代理,AOP框架創建的對象,包含處理。簡單地說,代理就是對目標對象的加強。Spring中的AOP代理可以是JDK動態代理,也可以是CGLIB代理。前者爲實現接口的目標對象的代理,後者爲不實現接口的目標對象的代理。

注意:面向切面編程是比較前沿的知識,而國內大部分翻譯人士翻譯計算機文獻時,總是一邊開着各種詞典和翻譯軟件,一邊逐詞去看文獻,不是先從總體上把握知識的架構。因此,難免導致一些術語的翻譯詞不達意,例如,Socket被翻譯成“套接字”等。在面向切面編程的各術語翻譯上,也存在較大的差異。對於Advice一詞,有翻譯爲“通知”的,有翻譯爲“建議”的,如此種種,不一而足。實際上,Advice指AOP框架在特定切面所做的事情,故而筆者翻譯爲“處理”,希望可以表達Advice的真正含義。

6.2.2 AOP的代理

所謂AOP代理,就是AOP框架動態創建的對象,這個對象通常可以作爲目標對象的替代品,而AOP代理提供比目標對象更加強大的功能。真實的情形是,當應用調用AOP代理的方法時,AOP代理會在自己的方法中回調目標對象的方法,從而完成應用的調用。

關於AOP代理的典型例子就是Spring中的事務代理Bean。通常,目標Bean的方法不是事務性的,而AOP代理包含目標Bean的全部方法,而且這些方法經過加強變成了事務性方法。簡單地說,目標對象是藍本,AOP代理是目標對象的加強,在目標對象的基礎上,增加屬性和方法,提供更強大的功能。

目標對象包含一系列切入點。切入點可以觸發處理連接點集合。用戶可以自己定義切入點,如使用正則表達式。AOP代理包裝目標對象,在切入點處加入處理。在切入點加入的處理,使得目標對象的方法功能更強。

Spring默認使用JDK動態代理實現AOP代理,主要用於代理接口。也可以使用CGLIB代理。實現類的代理,而不是接口。如果業務對象沒有實現接口,默認使用CGLIB代理。但面向接口編程是良好的習慣,儘量不要面向具體類編程。因此,業務對象通常應實現一個或多個接口。

下面是一個簡單動態代理模式的示例,首先有一個Dog的接口,接口如下:

public interface Dog

{

    //info方法聲明

    public void info();

    //run方法聲明

    public void run();

}

然後,給出該接口的實現類,實現類必須實現兩個方法,源代碼如下:

public class DogImpl implements Dog

{

    //info方法實現,僅僅打印一個字符串

    public void info()

    {

        System.out.println("我是一隻獵狗");

    }

    //run方法實現,僅僅打印一個字符串

    public void run()

    {

        System.out.println("我奔跑迅速");

    }

}

上面的代碼沒有絲毫獨特之處,是典型的面向接口編程的模型,爲了有更好的解耦,採用工廠來創建Dog實例。工廠源代碼如下:

public class DogFactory

{

    //工廠本身是單態模式,因此,將DogFactory作爲靜態成員變量保存

    private static DogFactory df;

    //將Dog實例緩存

    private Dog gundog;

    //默認的構造器,單態模式需要的構造器是private

    private DogFactory()

    {

    }

    //單態模式所需的靜態方法,該方法是創建本類實例的唯一方法點

    public static DogFactory instance()

    {

        if (df == null)

        {

            df = new DogFactory();

        }

        return df;

    }

    //獲得Dog實例

    public Dog getDog(String dogName)

    {

        //根據字符串參數決定返回的實例

        if (dogName.equals("gundog"))

        {

            //返回Dog實例之前,先判斷緩存的Dog是否存在,如果不存在才創建,

            否則直接返回緩存的Dog實例

            if (gundog == null )

            {

                gundog = new DogImpl();

            }

            return gundog;

         }

        return null;

    }

}

下面是一個通用的處理類,該處理類沒有與任何特定的類耦合,它可以處理所有的目標對象。從JDK 1.3起,Java的import java.lang.reflect下增加InvocationHandler接口,該接口是所有處理類的根接口。

該類處理類的源代碼如下:

public class ProxyHandler implements InvocationHandler

{

    //需被代理的目標對象

    private Object target;

    //執行代理的目標方法時,該invoke方法會被自動調用

    public Object invoke(Object proxy, Method method, Object[] args)throws
    Exception

    {

        Object result = null;

        if (method.getName().equals("info"))

        {

            System.out.println("======開始事務...");

            result =method.invoke(target, args);

            System.out.println("======提交事務...");

        }

        else

        {

            result =method.invoke(target, args);

        }

        return result;

    }

    //通過該方法,設置目標對象

    public void setTarget(Object o)

    {

        this.target = o;

    }

}

該處理類實現InvocationHandler接口,實現該接口必須實現invoke(Object proxy, Method method, Object[] args)方法,程序調用代理的目標方法時,自動變成調用invoke方法。

該處理類並未與任何接口或類耦合,它完全是通用的,它的目標實例是Object類型,可以是任何的類型。

在invoke方法內,對目標對象的info方法進行簡單加強,在開始執行目標對象的方法之前,先打印開始事務,執行目標對象的方法之後,打印提交事務。

通過method對象的invoke方法,可以完成目標對象的方法調用,執行代碼如下:

result =method.invoke(target, args);

下面是代理工廠:

public class MyProxyFactory

{

    /**

      * 實例Service對象

     * @param serviceName String

     * @return Object

     */

    public static Object getProxy(Object object)

    {

       //代理的處理類

        ProxyHandler handler = new ProxyHandler();

       //把該dog實例託付給代理操作

        handler.setTarget(object);

        //第一個參數是用來創建動態代理的ClassLoader對象,只要該對象能訪問Dog接口
        即可

        //第二個參數是接口數組,正是代理該接口數組

        //第三個參數是代理包含的處理實例

        return Proxy.newProxyInstance(DogImpl.class.getClassLoader(),

            object.getClass().getInterfaces(),handler);

    }

}

代理工廠裏有一行代碼:

Proxy.newProxyInstance(object.getClass().getClassLoader(),object.getClass().getInterfaces(),handler);

Proxy.newProxyInstance()方法根據接口數組動態創建代理類實例,接口數組通過object.getClass().getInterfaces()方法獲得,創建的代理類是JVM在內存中動態創建的,該類實現傳入接口數組的全部接口。

因此,Dynamic Proxy要求被代理的必須是接口的實現類,否則無法爲其構造相應的動態類。因此,Spring對接口實現類採用Dynamic Proxy實現AOP,而對沒有實現任何接口的類,則通過CGLIB實現AOP代理。

下面是主程序:

public class TestDog

{

    public static void main(String[] args)

    {

        Dog dog = null;

        //創建Dog實例,該實例將作爲被代理對象

        Dog targetObject = DogFactory.instance().getDog("gundog");

        //以目標對象創建代理

        Object proxy = MyProxyFactory.getProxy(targetObject);

        if (proxy instanceof Dog)

        {

            dog = (Dog)proxy;

        }

        //測試代理的方法

        dog.info();

        dog.run();

    }

}

代理實例會實現目標對象實現的全部接口。因此,代理實例也實現了Dog接口,程序運行結果如下:

[java] ======開始事務...

[java] 我是一隻獵狗

[java] ======提交事務...

[java] 我奔跑迅速

代理實例加強了目標對象的方法——僅僅打印了兩行字符串。當然,此種加強沒有實際意義。試想一下,若程序中打印字符串的地方,換成真實的事務開始和事務提交,則代理實例的方法爲目標對象的方法增加了事務性。

6.2.3 創建AOP代理

通過前面的介紹,AOP代理就是由AOP框架動態生成的一個對象,該對象可作爲目標對象使用,AOP代理包含了目標對象的全部方法。但AOP代理中的方法與目標對象的方法存在差異:AOP方法在特定切面插入處理,在處理之間回調目標對象的方法。

AOP代理所包含的方法與目標對象所包含的方法的示意圖,如圖6.1所示。

文本框:圖6.1  AOP代理的方法與目標對象的方法Spring中AOP代理由Spring的IoC容器負責生成和管理,其依賴關係也由IoC容器負責管理。因此,AOP代理能夠引用容器中的其他Bean實例,這種引用由IoC容器的依賴注入提供。

Spring的AOP代理大都由ProxyFactoryBean工廠類產生,如圖6.1所示,產生一個AOP代理至少有兩個部分:目標對象和AOP框架所加入的處理。因此,配置ProxyFactoryBean時需要確定如下兩個屬性:

   ● 代理的目標對象。

   ● 處理(Advice)。

注意:關於Advice的更多知識,此處由於篇幅原因,無法深入討論。實際上,Spring也提供了很多種Advice的實現,如需要更深入瞭解Spring AOP,請讀者參考筆者所著的《Spring2.0寶典》。

所有代理工廠類的父類是org.springframework.aop.framework.ProxyConfig。因此,該類的屬性是所有代理工廠的共同屬性,這些屬性也是非常關鍵的,包括:

   ● proxyTargetClass,確定是否代理目標類,如果需要代理目標是類,該屬性設爲true,此時需要使用CGLIB生成代理;如果代理目標是接口,該屬性設爲false,默認是false。

   ● optimize,確定是否使用強優化來創建代理。該屬性僅對CGLIB代理有效;對JDK 動態代理無效。

   ● frozen,確定是否禁止改變處理,默認是false。

   ● exposeProxy,代理是否可以通過ThreadLocal訪問,如果exposeProxy屬性爲true,則可通過AopContext.currentProxy()方法獲得代理。

   ● aopProxyFactory,所使用的AopProxyFactory具體實現。該參數用來指定使用動態代理、CGLIB或其他代理策略。默認選擇動態代理或CGLIB。一般不需要指定該屬性,除非需要使用新的代理類型,才指定該屬性。

配置ProxyFactoryBean工廠bean時,還需要指定它的特定屬性,ProxyFactoryBean的特定屬性如下所示:

   ● proxyInterfaces,接口名的字符串數組。如果沒有確定該參數,默認使用CGLIB代理。

   ● interceptorNames,處理名的字符串數組。此處的次序很重要,排在前面的處理,優先被調用。此處的處理名,只能是當前工廠中處理的名稱,而不能使用bean引用。處理名字支持使用通配符(*)。

   ● singleton,工廠是否返回單態代理。默認是true,無論 getObject()被調用多少次,將返回相同的代理實例。如果需要使用有狀態的處理——例如,有狀態的mixin,可改變默認設置,prototype處理。

posted @ 2009-07-19 10:15 jadmin 閱讀(1) 評論(0) 編輯

Spring的兩種後處理器

6.1 兩種後處理器

Spring 框架提供了很好的擴展性,除了可以與各種第三方框架良好整合外,其IoC容器也允許開發者進行擴展。這種擴展並不是通過實現BeanFactory或ApplicationContext的子類,而是通過兩個後處理器對IoC容器進行擴展。Spring提供了兩種常用的後處理器:

   ● Bean後處理器,這種後處理器會對容器中特定的Bean進行定製,例如功能的    加強。

   ● 容器後處理器,這種後處理器對IoC容器進行特定的後處理。

下面將介紹這兩種常用的後處理器以及兩種後處理器相關知識。

6.1.1 Bean後處理器

Bean後處理器是一種特殊的Bean,這種特殊的Bean並不對外提供服務,它無須id屬性,但它負責對容器中的其他Bean執行後處理,例如爲容器中的目標Bean生成代理。這種Bean可稱爲Bean後處理器,它在Bean實例創建成功後,對其進行進一步的加強     處理。

Bean後處理器必須實現BeanPostProcessor接口。

BeanPostProcessor接口包含兩個方法:

   ● Object postProcessBeforeInitialization(Object bean, String name)throws BeansExce- ption,該方法的第一個參數是系統即將初始化的Bean實例,第二個參數是Bean實例的名字。

   ● Object postProcessAfterInitialization(Object bean, String name)throws BeansExce- ption,該方法的第一個參數是系統剛完成初始化的Bean實例,第二個參數是Bean實例的名字。

實現該接口的Bean必須實現這兩個方法,這兩個方法會對容器的Bean進行後處理。兩個方法會在目標Bean初始化之前和初始化之後分別調用。這兩個方法用於對系統完成的默認初始化進行加強。

注意:Bean後處理器是對IoC容器一種極好的擴展,Bean後處理器可以對容器中的Bean進行後處理,這種後處理完全由開發者決定。

下面將定義一個簡單的Bean後處理器,該Bean後處理器將對容器中其他Bean進行後處理。Bean後處理器的代碼如下:

//自定義Bean後處理器,負責後處理容器中所有的Bean

public class MyBeanPostProcessor implements BeanPostProcessor

{

    //在初始化bean之前,調用該方法

    public Object postProcessBeforeInitialization(Object bean , String
    beanName)throws BeansException

    {

        //僅僅打印一行字符串

        System.out.println("系統正在準備對" + beanName + "進行初始化...");

        return bean;

    }

    //在初始化bean之後,調用該方法

    public Object postProcessAfterInitialization(Object bean , String
    beanName)throws BeansException

    {

        System.out.println("系統已經完成對" + beanName + "的初始化");

        //如果系統剛完成初始化的bean是Chinese

        if (bean instanceof Chinese)

        {

            //爲Chinese實例設置name屬性

            Chinese c = (Chinese)bean;

            c.setName("wawa");

         }

        return bean;

    }

}

下面是Chinese的源代碼,該類實現了InitializingBean接口,還額外提供了一個初始化方法,這兩個方法都由Spring容器控制回調。

public class Chinese implements Person,InitializingBean

{

    private Axe axe;

    private String name;

    public Chinese()

    {

        System.out.println("Spring實例化主調bean:Chinese實例...");

    }

    public void setAxe(Axe axe)

    {

        System.out.println("Spring執行依賴關係注入...");

        this.axe = axe;

    }

    public void setName(String name)

    {

        this.name = name;

    }

    public void useAxe()

    {

        System.out.println(name + axe.chop());

    }

    public void init()

    {

        System.out.println("正在執行初始化方法   init...");

    }

   public void afterPropertiesSet() throws Exception

    {

       System.out.println("正在執行初始化方法 afterPropertiesSet...");

    }

}

配置文件如下:

<?xml version="1.0" encoding="gb2312"?>

<!-- 指定Spring 配置文件的dtd>

<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"

    "http://www.springframework.org/dtd/spring-beans.dtd">

<!-- Spring配置文件的根元素 -->

<beans>

    <!-- 配置bean後處理器,可以沒有id屬性,此處id屬性爲了後面引用 -->

    <bean id="beanPostProcessor" class="lee.MyBeanPostProcessor"/>

    <bean id="steelAxe" class="lee.SteelAxe"/>

    <bean id="chinese" class="lee.Chinese" init-method="init">

        <property name="axe" ref="steelAxe"/>

    </bean>

</beans>

本應用的chinese具有兩個初始化方法:

   ● init-method指定初始化方法。

   ● 實現InitializingBean接口,提供了afterPropertiesSet初始化方法。

MyBeanPostProcessor類實現了BeanPostProcessor接口,並實現了該接口的兩個方法,這兩個方法分別在初始化方法調用之前和之後得到回調。

注意:上面的配置文件配置Bean後處理器時,依然爲Bean處理器指定了id屬性,指定id屬性是爲了方便程序通過該id屬性訪問Bean後處理器。大部分時候,程序無須手動訪問該Bean後處理器,因此無須爲其指定id屬性。

主程序如下:

public class BeanTest

{

    public static void main(String[] args)throws Exception

    {

        //CLASSPATH路徑下的bean.xml文件創建Resource對象

        ClassPathResource isr = new ClassPathResource("bean.xml");

        //以Resource對象作爲參數,創建BeanFactory的實例

        XmlBeanFactory factory = new XmlBeanFactory(isr);

        //獲取Bean後處理器實例

        MyBeanPostProcessor beanProcessor =

            (MyBeanPostProcessor)factory.getBean("beanPostProcessor");

        //註冊BeanPostProcessor實例

        factory.addBeanPostProcessor(beanProcessor);

        System.out.println("程序已經實例化BeanFactory...");

        Person p = (Person)factory.getBean("chinese");

        System.out.println("程序中已經完成了chinese bean的實例化...");

        p.useAxe();

    }

}

如果使用BeanFactory作爲Spring容器,必須手動註冊Bean後處理器,因此在程序中先獲取Bean後處理器實例,然後手動註冊——這就是在配置文件中指定Bean後處理器id屬性的原因。通過BeanFactory的addBeanPostProcessor可以註冊BeanPostProcessor實例。程序執行結果如下:

[java] 程序已經實例化BeanFactory...

[java] Spring實例化主調bean:Chinese實例...

[java] Spring實例化依賴bean:SteelAxe實例...

[java] 系統正在準備對steelAxe進行初始化...

[java] 系統已經完成對steelAxe的初始化

[java] Spring執行依賴關係注入...

[java] 系統正在準備對chinese進行初始化...

[java] 正在執行初始化方法 afterPropertiesSet...

[java] 正在執行初始化方法   init...

[java] 系統已經完成對chinese的初始化

[java] 程序中已經完成了chinese bean的實例化...

[java] wawa鋼斧砍柴真快

在配置文件中配置chinese實例時,並未指定name屬性值。但程序執行時,name屬性有了值,這就是Bean後處理器完成的,在Bean後處理器中判斷Bean是否是Chinese實例,然後設置它的name屬性。

容器中一旦註冊了Bean後處理器,Bean後處理器會自動啓動,在容器中每個Bean創建時自動工作,完成加入Bean後處理器需要完成的工作。

實現BeanPostProcessor接口的Bean後處理器可對Bean進行任何操作,包括完全忽略這個回調。BeanPostProcessor通常用來檢查標記接口或將Bean包裝成一個Proxy的事情。Spring的很多工具類,就是通過Bean後處理器完成的。

從主程序中看到,採用BeanFactory作爲Spring容器時,必須手動註冊BeanPost- Processor。而對於ApplicationContext,則無須手動註冊。ApplicationContext可自動檢測到容器中的Bean後處理器,自動註冊。Bean後處理器會在Bean實例創建時,自動啓動。即主程序採用如下代碼,效果完全一樣:

public class BeanTest

{

    public static void main(String[] args)throws Exception

    {

        ApplicationContext ctx = new ClassPathXmlApplicationContext 
        ("bean.xml");

        Person p = (Person)factory.getBean("chinese");

        System.out.println("程序中已經完成了chinese bean的實例化...");

        p.useAxe();

    }

}

使用ApplicationContext作爲容器,無須手動註冊BeanPostProcessor。因此,如果需要使用Bean後處理器,Spring容器建議使用ApplicationContext,而不是BeanFactory。

6.1.2 Bean後處理器的用處

上一節介紹了一個簡單的Bean後處理器,上面的Bean後處理器負責對容器中的Chinese Bean進行後處理,不管Chinese Bean如何初始化,總是將Chinese Bean的name屬性設置爲wawa。這種後處理看起來作用並不是特別大。

實際上,Bean後處理器完成的工作更加實際,例如生成Proxy。Spring框架本身提供了大量的Bean後處理器,這些後處理器負責對容器中的Bean進行後處理。

下面是Spring提供的兩個常用的後處理器:

   ● BeanNameAutoProxyCreator,根據Bean實例的name屬性,創建Bean實例的代理。

   ● DefaultAdvisorAutoProxyCreator,根據提供的Advisor,對容器中所有的Bean實例創建代理。

上面提供的兩個Bean後處理器,都用於根據容器中配置的攔截器創建目標Bean代理,目標代理就在目標Bean的基礎上修改得到。

注意:如果需要對容器中某一批Bean進行特定的處理,可以考慮使用Bean後處理器。

6.1.3 容器後處理器

除了上面提供的Bean後處理器外,Spring還提供了一種容器後處理器。Bean後處理器負責後處理容器生成的所有Bean,而容器後處理器則負責後處理容器本身。

容器後處理器必須實現BeanFactoryPostProcessor接口。實現該接口必須實現如下一個方法:

void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)

實現該方法的方法體就是對Spring容器進行的處理,這種處理可以對Spring容器進行任意的擴展,當然也可以對Spring容器不進行任何處理。

類似於BeanPostProcessor,ApplicationContext可自動檢測到容器中的容器後處理器,並且自動註冊容器後處理器。但若使用BeanFactory作爲Spring容器,則必須手動註冊後處理器。

下面定義了一個容器後處理器,這個容器後處理器實現BeanFactoryPostProcessor接口,但並未對Spring容器進行任何處理,只是打印出一行簡單的信息。該容器後處理器的代碼如下:

public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor

{

    //容器後處理器對容器進行的處理在該方法中實現

    public void postProcessBeanFactory(ConfigurableListableBeanFactory
    beanFactory)

        throws BeansException

    {

        System.out.println("程序對Spring所做的BeanFactory的初始化沒有意
        見...");

    }

}

將該Bean作爲普通Bean部署在容器中,然後使用ApplicationContext作爲容器,容器會自動調用BeanFactoryPostProcessor處理Spring容器。程序執行效果如下:

[java] 程序對Spring所做的BeanFactory的初始化沒有意見...

實現BeanFactoryPostProcessor接口的Bean後處理器不僅可對BeanFactory執行後處理,也可以對ApplicationContext容器執行後處理。容器後處理器還可用來註冊額外的屬性編輯器。

注意:Spring沒有提供ApplicationContextPostProcessor。也就是說,對於Application- Context容器,一樣使用BeanFactoryPostProcessor作爲容器後處理器。

Spring已提供如下兩個常用的容器後處理器,包括:

   ● PropertyResourceConfigurer,屬性佔位符配置器。

   ● PropertyPlaceHolderConfigurer,另一種屬性佔位符配置器。

下面將詳細介紹這兩種常用的容器後處理器。

6.1.4 屬性佔位符配置器

Spring提供了PropertyPlaceholderConfigurer,它是一個容器後處理器,負責讀取Java屬性文件裏的屬性值,並將這些屬性值設置到Spring容器定義中。

通過使用PropertyPlaceholderConfigurer後處理器,可以將Spring配置文件中的部分設置放在屬性文件中設置。這種配置方式當然有其優勢:可以將部分相似的配置(如數據庫的urls、用戶名和密碼)放在特定的屬性文件中,如果只需要修改這部分配置,則無須修改Spring配置文件,修改屬性文件即可。

下面的配置文件配置了PropertyPlaceholderConfigurer後處理器,在配置數據源Bean時,使用了屬性文件中的屬性值。配置文件的代碼如下:

<?xml version="1.0" encoding="GBK"?>

<!-- beans是Spring配置文件的根元素,並且指定了Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置一個容器後處理器Bean -->

    <bean id="propertyConfigurer"

        class="org.springframework.beans.factory.config. 
        PropertyPlaceholderConfigurer">

        <!-- locations屬性指定屬性文件的位置 -->

        <property name="locations">

            <list>

                <value>dbconn.properties</value>

                <!-- 如果有多個屬性文件,依次在下面列出來 -->

            </list>

        </property>

    </bean>

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0. 
    ComboPooledDataSource" destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="${jdbc.driverClassName}"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="${jdbc.url}"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="${jdbc.username}"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="${jdbc.password}"/>

    </bean>

</beans>

在上面的配置文件中,配置driverClass和jdbcUrl等信息時,並未直接設置這些屬性的屬性值,而是設置了${jdbc.driverClassName}和${jdbc.url}屬性值。這表明Spring容器將從propertyConfigurer指定屬性文件中搜索這些key對應的value,併爲該Bean的屬性值設置這些value值。

如前所述,ApplicationContext會自動檢測部署在容器的容器後處理器,無須額外的註冊,容器自動註冊。因此,只需提供如下Java Properties文件:

jdbc.driverClassName=com.mysql.jdbc.Driver

jdbc.url=jdbc:mysql://localhost:3306/j2ee

jdbc.username=root

jdbc.password=32147

通過這種方法,可從主XML配置文件中分離出部分配置信息。如果僅需要修改數據庫連接屬性,則無須修改主XML配置文件,只需要修改屬性文件即可。採用屬性佔位符的配置方式,可以支持使用多個屬性文件。通過這種方式,可將配置文件分割成多個屬性文件,從而降低修改配置的風險。

注意:對於數據庫連接等信息集中的配置,可以將其配置在Java屬性文件中,但不要過多地將Spring配置信息抽離到Java屬性文件中,否則可能會降低Spring配置文件的可讀性。

6.1.5 另一種屬性佔位符配置器(PropertyOverrideConfigurer)

PropertyOverrideConfigurer是Spring提供的另一個容器後處理器,這個後處理器的額作用與上面介紹的容器後處理器作用大致相同。但也存在些許差別:PropertyOverride- Configurer使用的屬性文件用於覆蓋XML配置文件中的定義。即PropertyOverride- Configurer允許XML配置文件中有默認的配置信息。

如果PropertyOverrideConfigurer的屬性文件有對應配置信息,XML文件中的配置信息被覆蓋;否則,直接使用XML文件中的配置信息。使用PropertyOverrideConfigurer的屬性文件,應是如下的格式:

beanName.property=value

beanName是屬性佔位符試圖覆蓋的Bean名,property是試圖覆蓋的屬性名。看如下配置文件:

<?xml version="1.0" encoding="GBK"?>

<!-- beans是Spring配置文件的根元素,並且指定了Schema信息 -->

<beans xmlns="http://www.springframework.org/schema/beans"

       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <!-- 配置一個屬性佔位符Bean。ApplictionContext能自動識別
    PropertyPlaceholderConfigurer Bean -->

    <bean id="propertyOverrider"

        class="org.springframework.beans.factory.config.
        PropertyOverrideConfigurer">

        <property name="locations">

            <list>

                <value>dbconn.properties</value>

                <!-- 如果有多個屬性文件,依次在下面列出來 -->

            </list>

        </property>

    </bean>

    <!-- 定義數據源Bean,使用C3P0數據源實現 -->

    <bean id="dataSource" class="com.mchange.v2.c3p0. 
    ComboPooledDataSource" destroy-method="close">

        <!-- 指定連接數據庫的驅動 -->

        <property name="driverClass" value="dd"/>

        <!-- 指定連接數據庫的URL -->

        <property name="jdbcUrl" value="xx"/>

        <!-- 指定連接數據庫的用戶名 -->

        <property name="user" value="dd"/>

        <!-- 指定連接數據庫的密碼 -->

        <property name="password" value="xx"/>

    </bean>

</beans>

上面的配置文件中,指定數據源Bean的各種屬性值時,只是隨意指定了幾個屬性值,很明顯通過這幾個屬性值無法連接到數據庫服務。

但因爲Spring容器中部署了一個PropertyOverrideConfigurer的容器後處理器,而且Spring容器使用ApplicationContext作爲容器,它會自動檢測容器中的容器後處理器,無須額外的註冊,容器自動註冊該後處理器。

PropertyOverrideConfigurer後處理器讀取dbconn.properties文件中的屬性,用於覆蓋目標Bean的屬性。因此,如果屬性文件中有dataSource Bean屬性的設置,則配置文件中指定的屬性值將沒有任何作用。

dbconn.properties屬性文件如下:

dataSource.driverClassName=com.mysql.jdbc.Driver

dataSource.url=jdbc:mysql://wonder:3306/j2ee

dataSource.username=root

dataSource.password=32147

注意屬性文件的格式必須是:

beanName.property=value

也就是說,dataSource必須是容器中真實存在的bean名,否則程序將出錯。

注意:程序無法知道BeanFactory定義是否被覆蓋。僅僅通過察看XML配置文件,無法知道配置文件的配置信息是否被覆蓋。如有多個PorpertyOverrideConfigurer對同一Bean屬性定義了覆蓋,最後一個覆蓋獲勝。

posted @ 2009-07-19 10:12 jadmin 閱讀(2) 評論(0) 編輯

Struts與Hibernate的整合策略

4.9 Struts與Hibernate的整合策略

前面介紹了Hibernate的一些相關知識點,距離Hibernate進入實際開發還有一段路要走。Hibernate作爲持久層解決方案,必須與其他表現層技術組合在一起纔可形成一個J2EE開發框架。經常看到網上一些朋友給出的Hibernate入門示例,居然在JSP頁面中訪問Hibernate Configuratioin對象。甚至看到某些所謂的精通J2EE書籍,也居然在JSP頁面中訪問Hibernate的Configuration對象——這種現狀非常讓人擔憂,Hibernate並不是萬金油,並不是說項目中使用Hibernate就怎麼了不起了,而是通過使用Hibernate,可以讓J2EE應用架構更科學,可以讓開發者以更好的面向對象的方式進行項目開發。

反過來說,即使不使用Hibernate,而使用普通的JDBC持久化解決方案,也不應該在JSP(表現層)訪問到JDBC API(持久層API)。下面介紹如何讓Hibernate和Struts進行整合,整合Spring部分將在後面章節介紹。

4.9.1 工廠模式介紹

工廠模式是指當應用程序中A組件需要B組件協助時,並不是直接創建B組件的實例,而是通過B組件的工廠——該工廠可以生成某一個類型組件的實例。在這種模式下,A組件無須與B組件以硬編碼方式耦合在一起,而只需要與B組件的工廠耦合。

對於A組件而言,它只關心工廠生產的實例是否滿足某種規範,即實現了某個接口(滿足接口規範,即可供自己正常調用)。這種模式提供了對象之間清晰的角色劃分,降低了程序的耦合。

接口產生的全部實例通常實現相同接口,接口裏定義全部實例共同擁有的方法,這些方法在不同的實現類中實現方式不同。程序調用者無須關心方法的具體實現,從而降低了系統異構的代價。

下面是工廠模式的示例代碼:

//Person接口定義

public interface Person

{  

    /**

    * @param name 對name打招呼

    * @return 打招呼的字符串

    */

    public String sayHello(String name);

    /**

    * @param name 對name告別

    * @return 告別的字符串

    */

    public String sayGoodBye(String name);

}

該接口定義Person的規範,該接口必須擁有兩個方法:能打招呼、能告別。規範要求實現該接口的類必須具有這兩個方法:

//American類實現Person接口

public class American implements Person

{

    /**

    * @param name 對name打招呼

    * @return 打招呼的字符串

    */

    public String sayHello(String name)

    {

        return name + ",Hello";

    }

    /**

    * @param name 對name告別

    * @return 告別的字符串

    */

    public String sayGoodBye(String name)

    {

        return name + ",Good Bye";

    }

}

下面是實現Person接口的另一個實現類Chinese

public class Chinese implements Person

{

    /**

    * @param name 對name打招呼

    * @return 打招呼的字符串

    */

    public String sayHello(String name)

    {

        return name + ",您好";

    }

    /**

    * @param name 對name告別

    * @return 告別的字符串

    */

    public String sayGoodBye(String name)

    {

        return name + ",下次再見";

    }

}

然後看Person工廠的代碼:

public class PersonFactory

{

    /**

    * 獲得Person實例的工廠方法

    * @ param ethnic 調用該實例工廠方法傳入的參數

    * @ return返回Person實例

    */

    public Person getPerson(String ethnic)

    {

        //根據參數返回Person接口的實例

        if (ethnic.equalsIgnoreCase("chin"))

        {

            return new Chinese();

        }

        else

        {

            return new American();

        }

    }

}

最簡單的工廠模式的框架基本如上所示。

主程序部分僅僅需要與工廠耦合,而無須與具體的實現類耦合在一起。下面是主程序部分:

public class FactroyTest

{

    public static void main(String[] args)

    {

        //創建PersonFactory的實例,獲得工廠實例

        PersonFactory pf = new PersonFactory();

        //定義接口Person的實例,面向接口編程

        Person p = null;

        //使用工廠獲得Person的實例

        p = pf.getPerson("chin");

        //下面調用Person接口的方法

        System.out.println(p.sayHello("wawa"));

        System.out.println(p.sayGoodBye("wawa"));

        //使用工廠獲得Person的另一個實例

        p = pf.getPerson("ame");

        //再次調用Person接口的方法

        System.out.println(p.sayHello("wawa"));

        System.out.println(p.sayGoodBye("wawa"));

    }

}

主程序從Person接口的具體類中解耦出來,而且程序調用者無須關心Person的實例化過程,角色劃分清晰。主程序僅僅與工廠服務定位結合在一起,獲得工廠的引用,程序將可獲得所有工廠產生的實例。具體類的變化,重要接口不發生任何改變,調用者程序代碼部分幾乎無須發生任何改動。

4.9.2 使用DAO模式

第1章介紹了J2EE應用的架構,最上面的表現層,表現層與MVC框架的控制器交互,控制器負責調用業務邏輯組件的業務邏輯方法來處理用戶請求,而業務邏輯組件則依賴於DAO組件提供的數據庫原子操作,這種模式也被稱爲DAO模式。

由上面關於J2EE應用架構的介紹可見,控制器總是依賴於業務邏輯組件,而業務邏輯組件總是依賴於DAO組件。也就是說,控制器需要調用業務邏輯組件的方法,而業務邏輯組件需要調用DAO組件的方法。

DAO模式的分層非常清晰,持久層訪問被封裝在DAO層下,而決不會擴散到業務邏輯層,更不會在JSP頁面(表現層)中進行持久層訪問。

注意:即使在早期的Model 1(使用JSP + JavaBean創建應用的模式,沒有使用MVC設計模式)模式下,持久層訪問也被封裝在JavaBean中完成,而不是直接在JSP頁面中進行數據庫訪問。對於直接在JSP中訪問持久層API的做法,可以說根本不瞭解J2EE開發。

那麼控制器採用怎樣的方式訪問業務邏輯組件呢?應該採用工廠模式,讓控制器與業務邏輯組件的實現類分離,僅與業務邏輯工廠耦合;同樣,業務邏輯組件也應該採用工廠模式訪問DAO模式,而不是直接與DAO實現類耦合。

後面的案例部分會介紹更實際的整合策略,此處僅僅介紹DAO模式下兩個工廠模式策略。

4.9.3 DAO組件的工廠模式

在J2EE應用開發中,可擴展性是一個隨時需要關注的問題。而DAO組件是經常需要增加的項目組件,如果每次需要增加一個DAO組件都需要修改代碼是相當讓人沮喪的事情。爲了避免這種情況,採用XML配置文件來管理所有的DAO組件,這種DAO組件配置文件的代碼如下:

<?xml version="1.0" encoding="GBK"?>

<daoContext>

    <!-- 配置應用需要的sonDao組件 -->

    <dao id="sonDao" class="org.yeeku.dao.impl.SonDaoImpl"/>

    <!-- 配置應用需要的personDao組件 -->

    <dao id="personDao" class="org.yeeku.dao.impl.PersonDaoImpl"/>

</daoContext>

查看上面的配置文件可以看出,應用中有配置了兩個DAO組件,因爲每個DAO組件在J2EE應用中僅需要一個實例就足夠了,因此DAO工廠類提供了一個緩存池來緩存每個DAO實例,並負責在應用啓動時創建所有的DAO。

下面是DAO工廠類的代碼:

public class DaoFactory

{

    //用於緩存DAO實例的Map對象

    private Map<String, Dao> daoMap = new HashMap<String , Dao>();

    //將DAO工廠寫成單態模式

    private static DaoFactory df;

    //DAO工廠的構造器

    private DaoFactory()throws Exception

    {

        //使用SAXReader來負責解析daoContext.xml配置文檔

        Document doc = new SAXReader().read(new File(ConstantsUtil.realPath

        + "\\daoContext.xml"));

        //獲取文檔的根文檔

        Element root = doc.getRootElement();

        //獲取daoContext根元素的所有子元素

        List el = root.elements();

        for (Iterator it = el.iterator();it.hasNext() ; )

        {

            //每個子元素對應一個DAO組件

            Element em = (Element)it.next();

            String id = em.attributeValue("id");

            //獲取實現類

            String impl = em.attributeValue("class");

            //通過反射,根據類名創建DAO組件的實例

            Class implClazz = Class.forName(impl);

            Dao d = (Dao)implClazz.newInstance();

            //將創建的DAO組件放入緩存池中

            daoMap.put(id, d);

        }

    }

    //單態模式必須提供一個入口方法來創建DAO工廠的方法

    public static DaoFactory instance()throws Exception

    {

        //如果DAO工廠還未創建

        if (df == null)

        {

            df = new DaoFactory();

        }

        return df;

    }

    //下面的方法用於根據DAO組件ID獲取DAO組件

    public Dao getDao(String id)

    {

        return daoMap.get(id);

    }

}

通過上面的工廠類代碼可以看出,DAO工廠負責初始化所有的DAO組件。系統每增加一個DAO組件,無須再修改任何代碼,僅僅需要在daoContext.xml配置文件中增加配置即可。

注意:這種整合策略非常優秀。可擴展性很好,如果應用需要增加一個DAO組件,只需要修改配置文件,並提供相應的DAO組件實現即可。而且,如果有一天需要重構DAO組件,只須提供修改過的DAO組件實現類,而業務邏輯組件無須任何改變。

業務邏輯組件代碼無須與DAO實現類耦合,業務邏輯組件的代碼面向DAO組件的接口編程,將業務邏輯組件和DAO組件的耦合降低到接口層次。

4.9.4 業務邏輯組件的工廠模式

與此類似的是,業務邏輯組件完全可以採用這種編程模式,業務邏輯組件的配置文件代碼如下:

<?xml version="1.0" encoding="GBK"?>

<appContext>

    <!-- 配置應用需要的業務邏輯組件,每個業務邏輯組件對應一個app元素 -->

    <app id="wawa" class="org.yeeku.service.impl.WawaServiceImpl"/>

</appContext>

業務邏輯組件工廠同樣可根據該配置文件來初始化所有業務邏輯組件,並將業務邏輯組件放入緩存池中,讓控制器與業務邏輯組件的耦合降低到接口層次。業務邏輯組件的工廠類代碼如下:

public class AppFactory

{

    private Map<String , Object> appMap = new HashMap<String , Object>();

    //業務邏輯組件工廠採用單態模式

    private static AppFactory df;

    //業務邏輯組件工廠的私有構造器

    private AppFactory()throws Exception

    {

        //使用SAXReader來負責解析appContext.xml配置文檔

        Document doc = new SAXReader().read(new File(ConstantsUtil.realPath

        + "\\appContext.xml"));

        //獲取文檔的根文檔

        Element root = doc.getRootElement();

        //獲取appContext根元素的所有子元素

        List el = root.elements();

        for (Iterator it = el.iterator();it.hasNext() ; )

        {

            //每個app元素對應一個業務邏輯組件

            Element em = (Element)it.next();

            String id = em.attributeValue("id");

            //根據配置文件指定的業務邏輯組件實現類來創建業務邏輯組件實例

            String impl = em.attributeValue("class");

            Class implClazz = Class.forName(impl);

            Object d = implClazz.newInstance();

            //將業務邏輯組件放入緩存池中

            appMap.put(id , d);

        }

    }

    //單態模式必須提供入口方法,用於創建業務邏輯組件工廠

    public static AppFactory instance()throws Exception

    {

        //如果業務邏輯組件工廠爲空

        if (df == null)

        {

            df = new AppFactory();

        }

        return df;

    }

    //根據業務邏輯組件的id屬性獲取業務邏輯組件

    public Object getApp(String id)

    {

        //直接從緩存池中取出業務邏輯組件,並返回

        return appMap.get(id);

    }

}

從某種程度上來講,這種方式與後來Spring的控制反轉(Inversion of Control,IoC)容器有異曲同工之妙,但Spring的IoC容器則提供了更多的功能。

上面的兩個類中都用到了一個ConstantsUtil,它僅用於保存一個全局變量,有一個public static的realPath屬性,該屬性用於保存應用在服務器中的路徑。

posted @ 2009-07-19 10:08 jadmin 閱讀(1) 評論(0) 編輯

Hibernate的事件機制

4.8 事 件 機 制

通常,Hibernate執行持久化過程中,應用程序無法參與其中。所有的數據持久化操作,對用戶都是透明的,用戶無法插入自己的動作。

通過事件框架,Hibernate允許應用程序能響應特定的內部事件,從而允許實現某些通用的功能,或對Hibernate功能進行擴展。

Hibernate的事件框架由兩個部分組成:

   ● 攔截器機制,對於特定動作攔截,回調應用中的特定動作。

   ● 事件系統,重寫Hibernate的事件監聽器。

4.8.1 攔截器

通過Interceptor接口,可以從Session中回調應用程序的特定方法,這種回調機制可讓應用程序在持久化對象被保存、更新、刪除或加載之前,檢查並修改其屬性。

通過Interceptor接口,可以在數據進入數據庫之間,對數據進行最後的檢查,如果數據不符合要求,可以修改數據,從而避免非法數據進入數據庫。當然,通常無須這樣做,只是在某些特殊的場合下,才考慮使用攔截器完成檢查功能。

使用攔截器可按如下步驟進行:

(1)定義實現Interceptor接口的攔截器類;

(2)通過Session啓用攔截器,或者通過Configuration啓用全局攔截器。

下面是一個攔截器的示例代碼,該攔截器沒有進行任何實際的操作,僅僅打印出標誌代碼:

public class MyInterceptor extends EmptyInterceptor

{

    //更新的次數

    private int updates;

    //插入的次數

    private int creates;

    //刪除數據時,將調用onDelete方法

    public void onDelete(Object entity,Serializable id,Object[]

    state,String[] propertyNames, Type[] types)

    {

        //do nothing

    }

    //同步Session和數據庫中的數據

    public boolean onFlushDirty(Object entity, Serializable id, Object[]

    currentState, Object[] previousState, String[] propertyNames, Type[]

                            types)

    {

        //每同步一次,修改的累加器加1

        updates++;

        for ( int i=0; i < propertyNames.length; i++ )

        {

            if ( "lastUpdateTimestamp".equals( propertyNames[i] ) )

            {

                currentState[i] = new Date();

                return true;

            }

        }

        return false;

        }

    //加載持久化實例時,調用該方法

    public boolean onLoad(Object entity,Serializable id,Object[]

    state,String[] propertyNames,Type[] types)

    {

        System.out.println("========================");

        for ( int i=0; i < propertyNames.length; i++ )

        {

            if ( "name".equals( propertyNames[i] ) )

            {

                System.out.println(state[i]);

                state[i] = "aaa";

                return true;

            }

        }

        return false;

    }

    //保存持久化實例時,調用該方法

    public boolean onSave(Object entity,Serializable id,Object[]

    state,String[] propertyNames,Type[] types)

    {

        creates++;

        for ( int i=0; i<propertyNames.length; i++ )

        {

            if ( "createTimestamp".equals( propertyNames[i] ) )

            {

                state[i] = new Date();

                return true;

            }

        }

        return false;

    }

    //提交刷新

    public void postFlush(Iterator entities)

    {

        System.out.println("創建的次數: " + creates + ", 更新的次數: " +

    updates);

    }

    public void preFlush(Iterator entities)

    {

        updates=0;

        creates=0;

    }

    //事務提交前,觸發該方法

    public void beforeTransactionCompletion(Transaction tx)

    {

        System.out.println("事務即將結束");

    }

    //事務提交後,觸發該方法

    public void afterTransactionCompletion(Transaction tx)

    {

        System.out.println("事務已經結束");

    }

}

在上面的攔截器實現類中,實現了很多方法,這些方法都是在Hibernate執行特定動作時自動調用。

完成了攔截器的定義,下面是關於攔截器的使用。攔截器的使用有兩種方法:

   ● 通過SessionFactory的openSession(Interceptor in)方法打開一個帶局部攔截器的Session。

   ● 通過Configuration的setInterceptor(Interceptor in)方法設置全局攔截器。

下面是使用局部攔截器的示例代碼:

public class HibernateUtil

{

    //靜態類屬性 SessionFactory

    public static final SessionFactory sessionFactory;

    //靜態初始化塊,完成靜態屬性的初始化

    static

    {

        try

        {

            //採用默認的hibernate.cfg.xml來啓動一個Configuration的實例

            Configuration configuration=new Configuration().configure();

            //由Configuration的實例來創建一個SessionFactory實例

            sessionFactory = configuration.buildSessionFactory();

        }

        catch (Throwable ex)

        {

            System.err.println("初始化sessionFactory失敗." + ex);

            throw new ExceptionInInitializerError(ex);

        }

    }

    //ThreadLocal是隔離多個線程的數據共享,不存在多個線程之間共享資源,因此不再需要

    對線程同步   

    public static final ThreadLocal session = new ThreadLocal();

    //不加攔截器的打開Session方法

    public static Session currentSession() throws HibernateException

    {

        Session s = (Session) session.get();

        //如果該線程還沒有Session,則創建一個新的Session

        if (s == null)

        {

            s = sessionFactory.openSession();

            //將獲得的Session變量存儲在ThreadLocal變量的Session裏

            session.set(s);

        }

        return s;

    }

    //加攔截器的打開Session方法

    public static Session currentSession(Interceptor it) throws

    HibernateException

    {

        Session s = (Session) session.get();

        //如果該線程還沒有Session,則創建一個新的Session

        if (s == null)

        {

            //以攔截器創建Session對象

            s = sessionFactory.openSession(it);

            //將獲得的Session變量存儲在ThreadLocal變量的Session裏

            session.set(s);

            }

        return s;

    }

    //關閉Session對象

    public static void closeSession() throws HibernateException

    {

        Session s = (Session) session.get();

        if (s != null)

            s.close();

        session.set(null);

    }

}

上面的Hibernate工具類提供了兩個currentSession方法,分別用於不使用攔截器獲取Session對象和使用攔截器獲取Session對象。

下面是主程序使用攔截器的代碼片段:

private void testUser()

{

    //以攔截器開始Session

    Session session = HibernateUtil.currentSession(new MyInterceptor());

    //開始事務

    Transaction tx = session.beginTransaction();

    //執行下面的代碼時,可以看到系統回調onSave等方法

    /*

    User u = new User();

    u.setName("Yeeku Lee");

    u.setAge(28);

    u.setNationality("中國");

    session.persist(u);

    u.setAge(29);

    u.setAge(30);

    session.persist(u);

    */

    //執行下面的代碼時,可以看到系統回調onLoad等方法

    Object o = session.load(User.class , new Integer(1));

    System.out.println(o);

    User u = (User)o;

    System.out.println(u.getName());

    //提交事務時,可以看到系統回調事務相關方法

    tx.commit();

    HibernateUtil.closeSession();

}

4.8.2 事件系統

Hibernate 3的事件系統是功能更強大的事件框架,事件系統可以替代攔截器,也可以作爲攔截器的補充來使用。

基本上,Session接口的每個方法都有對應的事件。如LoadEvent和FlushEvent等。當Session調用某個方法時,Hibernate Session會生成對應的事件,並激活對應的事件監聽器。

系統默認監聽器實現的處理過程,完成了所有的數據持久化操作,包括插入和修改等操作。如果用戶定義了自己的監聽器,則意味着用戶必須完成對象的持久化操作。

例如,可以在系統中實現並註冊LoadEventListener監聽器,該監聽器負責處理所有調用Session的load()方法的請求。

監聽器是單態模式對象,即所有同類型的事件處理共享同一個監聽器實例,因此監聽器不應該保存任何狀態,即不應該使用成員變量。

使用事件系統可按如下步驟進行:

(1)實現自己的事件監聽器類;

(2)註冊自定義事件監聽器,代替系統默認的事件監聽器。

實現用戶的自定義監聽器有如下3個方法:

   ● 實現對應的監聽器接口,這是不可思議的,實現接口必須實現接口內的所有方法,關鍵是必須實現Hibernate對應的持久化操作,即數據庫訪問,這意味着程序員完全取代了Hibernate的底層操作。

   ● 繼承事件適配器,可以選擇性地實現需要關注的方法,但依然試圖取代Hibernate完成數據庫的訪問,這也不太現實。

   ● 繼承系統默認的事件監聽器,擴展特定方法。

實際上,前兩種方法很少使用。因爲Hibernate的持久化操作也是通過這些監聽器實現的,如果用戶取代了這些監聽器,則應該自己實現所有的持久化操作,這意味着用戶放棄了Hibernate的持久化操作,而改爲自己完成Hibernate的核心操作。

通常推薦採用第三種方法實現自己的事件監聽器。Hibernate默認的事件監聽器都被聲明成non-final,從而方便用戶繼承。

下面是用戶自定義監聽器的示例:

//自定義LoadListener,繼承默認的DefaultLoadEventListener實現類

public class MyLoadListener extends DefaultLoadEventListener

{

    //在LoadEventListener接口僅僅定義了這個方法

    public Object onLoad(LoadEvent event, LoadEventListener.LoadType

    loadType)throws HibernateException

    {

        //先調用父類的onLoad方法,從而完成默認的持久化操作

        Object o = super.onLoad(event, loadType);

        //加入用戶的自定義處理

        System.out.println("自定義的load事件");

        System.out.println(event.getEntityClassName() + "==========" +

        event.getEntityId());

        return o;

    }

}

下面還有一個MySaveListener,用於監聽SaveEvent事件:

//自定義SavaListener,繼承默認的DefaultSaveEventListener實現類

public class MySaveListener extends DefaultSaveEventListener

{

    //該方法完成實際的數據插入動作

    protected Serializable performSaveOrUpdate(SaveOrUpdateEvent event)

    {

        //先執行用戶自定義的操作

        System.out.println(event.getObject());

        //調用父類的默認持久化操作

        return super.performSaveOrUpdate(event);

    }

}

注意:擴展用戶自定義監聽器時,別忘了在方法中調用父類的對應方法。

註冊用戶自定義監聽器也有兩種方法:

   ● 編程式,通過使用Configuration對象編程註冊。

   ● 聲明式,在Hibernate的XML格式配置文件中進行聲明,使用Properties格式的配置文件將無法配置自定義監聽器。

下面的示例代碼,通過編程方式使用自定義監聽器:

public class HibernateUtil2

{

    //靜態類屬性 SessionFactory

    public static final SessionFactory sessionFactory;

    //靜態初始化塊,完成靜態屬性的初始化

    static

    {

        try

        {

            Configuration cfg = new Configuration();

            //註冊loadEventListener監聽器

            cfg.getSessionEventListenerConfig().setLoadEventListener

            ( new MyLoadListener() );

            //註冊saveListener監聽器

            cfg.getSessionEventListenerConfig().setSaveEventListener

            (new MySaveListener() );

            //由Configuration實例來創建一個SessionFactory實例

            sessionFactory = cfg.configure().buildSessionFactory();

        }

        catch (Throwable ex)

        {

            System.err.println("初始化sessionFactory失敗." + ex);

            throw new ExceptionInInitializerError(ex);

        }

    }

    //ThreadLocal是隔離多個線程的數據共享,不存在多個線程之間共享資源,因此不再需要

    對線程同步

    public static final ThreadLocal session = new ThreadLocal();

    //不加攔截器的打開Session方法

    public static Session currentSession() throws HibernateException

    {

        Session s = (Session) session.get();

        //如果該線程還沒有Session,則創建一個新的Session

        if (s == null)

        {

            s = sessionFactory.openSession();

            //將獲得的Session變量存儲在ThreadLocal變量的Session裏

            session.set(s);

        }

        return s;

    }

    //關閉Session對象

    public static void closeSession() throws HibernateException

    {

        Session s = (Session) session.get();

        if (s != null)

            s.close();

        session.set(null);

    }

}

如果不想修改代碼,也可以在配置文件中使用事件監聽器,註冊事件監聽器的Hibernate配置文件代碼如下:

<?xml version='1.0' encoding='GBK'?>

<!-- Hibernate配置文件的文件頭,包含DTD等信息 -->

<!DOCTYPE hibernate-configuration PUBLIC

        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"

        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.

        dtd">

<!-- Hibernate配置文件的根元素 -->

<hibernate-configuration>

    <session-factory>

        <!—設置數據庫驅動 -->

        <property name="connection.driver_class">com.mysql.jdbc.Driver

        </property>

        <!-- 數據庫服務的url -->

        <property name="connection.url">jdbc:mysql://localhost/hibernate

        </property>

        <!-- 數據庫服務的用戶名 -->

        <property name="connection.username">root</property>

        <!-- 數據庫服務的密碼 -->

        <property name="connection.password">32147</property>

        <!-- JDBC connection pool (use the built-in) -->

        <property name="connection.pool_size">5</property>

        <!-- 設置數據庫方言 -->

        <property name="dialect">org.hibernate.dialect.MySQLDialect

        </property>

        <!-- 顯示Hibernate生成的SQL語句 -->

        <property name="show_sql">true</property>

        <!-- 配置應用啓動時,是否啓動自動建表 -->

        <property name="hbm2ddl.auto">update</property>

        <!-- 列出所有的持久化映射文件 -->

        <mapping resource="User.hbm.xml"/>

        <!-- 註冊事件監聽器 -->

        <listener type="load" class="lee.MyLoadListener"/>

        <listener type="save" class="lee.MySaveListener"/>

    </session-factory>

</hibernate-configuration>

使用配置文件註冊事件監聽器雖然方便,但也有不利之處,通過配置文件註冊的監聽器不能共享實例。如果多個<listener/>元素中使用了相同的類,則每一個引用都將產生一個新的攔截器實例。如果需要在多個事件之間共享監聽器的實例,則必須使用編程方式註冊事件監聽器。

注意:雖然監聽器類實現了特定監聽器的接口,在註冊的時候還要明確指出註冊的事件。這是因爲一個類可能實現多個監聽器的接口,註冊時明確指定要監聽的事件,可以使得啓用或者禁用某個事件監聽的配置工作更簡單。

posted @ 2009-07-19 09:42 jadmin 閱讀(2) 評論(0) 編輯

Hibernate事務控制

4.7 事 務控 制

每個業務邏輯方法都是由一系列的數據庫訪問完成,這一系列的數據訪問可能會修改多條數據記錄,這系列的修改應該是一個整體,絕不能僅修改其中的幾條。也就是說,多個數據庫原子訪問應該綁定成一個整體——這就是事務。事務是一個最小的邏輯執行單元,整個事務不能分開執行,要麼同時執行,要麼同時放棄執行。

4.7.1 事務的概念

事務是一步或幾步基本操作組成的邏輯執行單元,這些基本操作作爲一個整體執行單元,它們要麼全部執行,要麼全部取消,絕不能僅僅執行部分。一般而言,每次用戶請求,對應一個業務邏輯方法,一個業務邏輯方法往往具有邏輯上的原子性,應該使用事務。例如,一個轉賬操作,對應修改兩個賬戶的餘額,這兩個賬戶的修改要麼同時生效,要麼同時取消——同時生效是轉賬成功,同時取消是轉賬失敗;但不可只修改其中一個賬戶,那將破壞數據庫的完整性。

通常來講,事務具備如下4個特性:原子性(atomicity)、一致性(consistency)、隔離性(isolation)和持續性(durability)。這4個特性也簡稱爲ACID性。

   ● 原子性:事務是應用中最小執行單位,就如原子是自然界最小顆粒,具有不可再分的特徵一樣。事務是應用中不可再分的最小邏輯執行體。

   ● 一致性:事務執行的結果,必須使數據庫從一個一致性狀態,變到另一個一致性狀態。當數據庫只包含事務成功提交的結果時,數據庫處於一致性狀態。如果系統運行發生中斷,某個事務尚未完成而被迫中斷,而該未完成的事務對數據庫所做的修改已被寫入數據庫,此時,數據庫就處於一種不正確的狀態。比如銀行在兩個賬戶之間轉賬,從A賬戶向B賬戶轉入1000元。系統先減少A賬戶的1000元,然後再爲B賬戶增加1000元。如果全部執行成功,數據庫處於一致性狀態。如果僅執行完A賬戶金額的修改,而沒有增加B賬戶的金額,則數據庫就處於不一致性狀態。因此,一致性是通過原子性來保證的。

   ● 隔離性:各個事務的執行互不干擾,任意一個事務的內部操作對其他併發的事務,都具有隔離性。也即併發執行的事務之間不能互相影響。

   ● 持續性:持續性也稱爲持久性(persistence),指事務一旦提交,對數據所做的任何改變,都要記錄到永久存儲器中,通常保存進物理數據庫。

4.7.2 Hibernate的事務

Hibernate直接使用JDBC連接和JTA資源,不添加任何附加鎖定行爲。Hibernate只添加自動版本管理,而不會鎖定內存中的對象,也不會改變數據庫事務的隔離級別。基本上,使用 Hibernate就好像直接使用JDBC(或者JTA/CMT)進行數據庫訪問。

Hibernate中SessionFactory對象的創建代價很高,它是線程安全的對象,被設計成可以爲所有的應用程序線程所共享。通常,SessionFactory會在應用程序啓動時創建,一旦創建了SessionFactory將不會輕易關閉,只有當應用關閉時,SessionFactory纔會關閉。

而Session的對象是輕量級的,它也是線程不安全的。對於單個業務進程單個工作單元而言,Session只被使用一次。創建Session時,並不會立即打開與數據庫之間的連接,Session只在需要進行數據庫操作時,纔會獲取JDBC連接。因此,打開和關閉Session,並不會對性能造成很大的影響。甚至即使無法確定一個請求是否需要數據訪問,也可以打開Session對象,因爲如果不進行數據庫訪問,Session不會獲取JDBC連接。

相反,數據庫事務應該儘可能的短。從而,降低數據庫鎖定造成的資源爭用。數據庫長事務會導致應用程序無法承載高併發的負荷。

由上面的介紹可知,Hiberante的Session和事務是緊密相關的,因爲事務是通過Session來打開的。那麼事務的範圍是多大?單個Session可以跨越多個數據庫事務嗎?事務和Session的對應關係又如何呢?下面將介紹Hibernate Session和事務的關係。

4.7.3 事務和Session

數據庫操作必須在Hibernate的Session管理下進行,但不推薦因爲一次簡單的數據庫原子調用,就打開和關閉一次Session,數據庫事務也是如此。因爲,對於一次原子操作打開的事務沒有任何意義——事務應該是將多個操作步驟組合成一個邏輯整體。

事務是按順序發送並組成一個邏輯整體的原子操作單元。

注意:也就是說單個的SQL語句發送之後,自動事務提交模式失效了。這種自動提交模式僅爲SQL控制檯設計,在實際項目沒有太大的實用價值。Hibernate禁止事務立即自動提交模式,或者讓應用服務器禁止事務自動提交。

通常,建議每個請求對應一個Session。在這種模式下,來自客戶端的請求被髮送到服務器端,此處可能對應一個業務邏輯方法。在這個業務邏輯方法內,一個新的Hibernate Session被打開,然後開始事務,在事務內執行這個操作單元中所有的數據庫操作。一旦操作完成,需要發送給客戶端的響應也準備就緒。此時,提交事務,然後關閉Session。在這種模式下,Session和用戶請求是一對一的關係,這是一種理想的Session管理模式。

爲了達到這種效果,推薦使用一個ThreadLocal變量,把Session綁定到處理客戶端請求的線程上去。這種方式可以讓運行在該線程上的所有程序代碼輕鬆地訪問Session。也可以在一個ThreadLocal變量中保持事務上下文環境,不過這依賴於所選擇的數據庫事務劃分機制。這種實現模式被稱之爲ThreadLocal Session和Open Session in View。

下面是一個HibernateUtil類,該類將Hibernate Session存放在一個ThreadLocal變量中,對於同一個線程的請求,將可以輕鬆訪問該Session。

public class HibernateUtil

{

    public static final SessionFactory sessionFactory;

    //靜態初始化塊,使用該類時使用該代碼塊

    static

    {

        try

        {

            //採用默認的hibernate.cfg.xml來啓動一個Configuration的實例

            Configuration configuration=new Configuration().configure();

            //由Configuration的實例來創建一個SessionFactory實例

            sessionFactory = configuration.buildSessionFactory();

        }

        catch (Throwable ex)

        {

            System.err.println("初始化sessionFactory失敗." + ex);

            throw new ExceptionInInitializerError(ex);

        }

    }

    //ThreadLocal是隔離多個線程的數據共享,不存在多個線程之間共享資源,因此不再需要

    對線程同步

    public static final ThreadLocal session = new ThreadLocal();

    //該方法用於獲取當前線程的Session對象

    public static Session currentSession() throws HibernateException

    {

        Session s = (Session) session.get();

        //如果該線程還沒有Session,則創建一個新的Session

        if (s == null)

        {

            s = sessionFactory.openSession();

            //將獲得的Session變量存儲在ThreadLocal變量的Session裏

            session.set(s);

        }

        return s;

    }

    //該方法用於關閉當前線程裏的Session

    public static void closeSession() throws HibernateException

    {

        Session s = (Session) session.get();

        if (s != null)

            s.close();

        session.set(null);

    }

}

在上面的代碼中,Hibernate Session被綁定到當前線程。當調用currentSession方法時,如果當前線程中的Session已經創建出來,那麼將返回這個已經存在的Session實例。

每次請求對應一個Session的模式不僅可以用於設計操作單元,甚至很多業務處理流程都需要組合一系列的用戶操作,即用戶對數據庫的交叉訪問。

但是,對於企業應用,跨用戶交互的數據庫事務是無法接受的。例如,在第一個頁面,用戶打開對話框,打開一個特定Session裝入的數據,可以隨意修改對話框中的數據,修改完成後,將修改結果存入數據庫。

從用戶的角度來看,這個操作單元被稱爲應用程序長事務。在一個J2EE應用實現中,可以有很多方法來實現這種應用程序長事務。

一個比較差的做法是,當用戶思考時,應用程序保持Session和數據庫事務是打開的,並保持數據庫鎖定,以阻止併發修改,從而保證數據庫事務隔離級別和原子操作。這種數據庫鎖定會導致應用程序無法擴展併發用戶的數目。

因此,不要使用每個應用對應一次Hibernate Session的模式,也不要使用每次Http Session對應一次Hibernate Session的模式。

注意:幾乎所有情況下,都不要使用每個應用對應一次Hibernate Session的模式,也不要使用每次Http Session對應一次Hibernate Session的模式。

對於這種情況,Hibernate主要有如下兩種模式來解決這個問題:

   ● 脫管對象,如果採用每次用戶請求對應一次Session的模式。那麼,前面載入的實例在用戶思考的過程中,始終與Session脫離,處於脫管狀態。都處於與Session脫離的狀態。Hibernate允許把脫管對象重新關聯到Session上,並且對修改進行持久化。在這種模式下,自動版本化被用來隔離併發修改。這種模式也被稱爲使用脫管對象的每個請求對應一個Hibernate Session。

   ● 長生命週期Session,Session可以在數據庫事務提交之後,斷開和底層的JDBC連接。當新的客戶端請求到來時,它又重新連接上底層的JDBC連接。這種模式被稱爲每個應用程序事務對應一個Session,因爲應用程序事務相當長(跨越多個用戶請求),所以也被稱爲每次應用事務對應一個Hibernate Session。

posted @ 2009-07-19 09:11 jadmin 閱讀(5) 評論(0) 編輯

Hibernate的數據過濾查詢

數據過濾並不是一種常規的數據查詢方法,而是一種整體的篩選方法。數據過濾也可對數據進行篩選,因此,將其放在Hibernate的數據查詢框架中介紹。

如果一旦啓用了數據過濾器,則不管數據查詢,還是數據加載,該過濾器將自動作用於所有數據,只有滿足過濾條件的記錄纔會被選出來。

過濾器與定義在類和集合映射文件上的“where”屬性非常相似。它們的區別是過濾器可以帶參數,應用程序可以在運行時決定是否啓用指定的過濾器,以及使用什麼樣的參數值。而映射文件上的“where”屬性將一直生效,且無法動態傳入參數。

過濾器的用法很像數據庫視圖,區別是視圖在數據庫中已經定義完成,而過濾器則還需在應用程序中確定參數值。

過濾器的使用分成三步:

(1)定義過濾器。使用filter-def元素定義過濾器;

(2)使用過濾器。使用filter元素使用過濾器;

(3)在代碼中啓用過濾器。

前兩個步驟都是在Hibernate的映射文件中完成的,其中filter-def是hibernate-mapping元素的子元素,而filter元素是class集合元素的子元素。

filter-def元素用於定義一個過濾器,filter則將指定的過濾器應用到指定的持久化類。

一個持久化類或集合可以使用多個過濾器,而一個過濾器也可以作用於多個持久化類或集合。

看下面的映射文件示例:

<?xml version="1.0"?>

<!-- Hibernate配置文件的文件頭,包含DTD等信息 -->

<!DOCTYPE hibernate-mapping

    PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

    "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<!-- Hibernate 配置文件的根元素 -->

<hibernate-mapping >

    <!-- 每個class元素定義一個持久化類 -->

    <class name="Category" table="category">

        <!-- 定義標識屬性 -->

        <id name="id" column="category_id" >

            <!-- 指定主鍵生成器策略 -->

            <generator class="native"/>

        </id>

        <!-- 映射name屬性 -->

        <property name="name" type="string"/>

        <!-- 映射effectiveStartDate屬性 -->

        <property name="effectiveStartDate" column="eff_start_date"

        type="java.util.Date"/>

        <!-- 映射effectiveEndDate屬性 -->

        <property name="effectiveEndDate" column="eff_end_date"

        type="java.util.Date"/>

        <!-- 映射N-N關聯屬性 -->

        <set cascade="none" inverse="true" name="products"

        table="product_category">

            <!-- 定義關聯屬性的key,對應連接表中的外鍵列 -->

            <key column="category_id"/>

            <!-- 定義關聯屬性 -->

            <many-to-many column="product_id" class="Product"/>

        </set>

        <!-- 使用過濾器,並設置過濾器條件 -->

        <filter name="effectiveDate" condition=":asOfDate BETWEEN

        eff_start_date and eff_end_date"/>

    </class>

    <!-- 定義第二個持久化類 -->

    <class name="Product" table="product">

        <!-- 定義標識屬性 -->

        <id name="id" column="product_id" >

            <!-- 指定主鍵生成器策略 -->

            <generator class="native"/>

        </id>

        <!-- 映射name屬性 -->

        <property name="name" type="string"/>

        <!-- 映射stockNumber屬性 -->

        <property name="stockNumber" column="stock_number" type="int"/>

        <!-- 映射effectiveStartDate屬性 -->

        <property name="effectiveStartDate" column="eff_start_date"

        type="java.util.Date"/>

        <!-- 映射effectiveEndDate屬性 -->

        <property name="effectiveEndDate" column="eff_end_date"

        type="java.util.Date"/>

        <!-- 映射N-N關聯屬性 -->

        <set cascade="all" name="categories" fetch="join"

        table="product_category" >

            <!-- 定義關聯屬性的key,對應連接表中的外鍵列 -->

            <key column="product_id"/>

            <!-- 定義關聯屬性 -->

            <many-to-many column="category_id"

                        class="Category" fetch="join">

                <!-- 對關聯屬性使用第一個過濾器 -->

                <filter name="effectiveDate"

                    condition=":asOfDate BETWEEN eff_start_date and

                    eff_end_date"/>

                <!-- 對關聯屬性使用第二個過濾器 -->

                <filter name="category" condition="category_id = :catId"/>

            </many-to-many>

        </set>

        <filter name="effectiveDate" condition=":asOfDate BETWEEN

        eff_start_date AND eff_end_date"/>

    </class>

    <!-- 定義第一個過濾器,該過濾器包含一個date類型的參數 -->

    <filter-def name="effectiveDate">

        <filter-param name="asOfDate" type="date"/>

    </filter-def>

    <!-- 定義第二個過濾器,該過濾器包含一個long類型的參數 -->

    <filter-def name="category">

        <filter-param name="catId" type="long"/>

    </filter-def>

</hibernate-mapping>

在上面的配置文件中,定義了兩個過濾器,過濾器的定義通過filter-def元素完成。定義過濾器時,只需要指定過濾器的名字,以及過濾器的參數即可。如Java裏的一個方法聲明,只有方法名和參數列表,具體的方法實現是沒有的。

過濾器的過濾條件是使用過濾器時才確定的,使用過濾器通過filter元素確定,filter的condition屬性用於確定過濾條件,滿足該條件的記錄纔會被抓取到。

系統默認不啓用過濾器,必須顯式通過enableFilter(String filterName)纔可以啓用過濾器,該方法返回一個Filter實例,Filter包含setParameter方法用於爲過濾器參數賦值。

一旦啓用了過濾器,過濾器在整個Session內有效,所有的數據加載將自動應用該過濾條件,直到調用disableFilter方法。

看下面的使用過濾器的示例代碼:

private void test() throws Exception

{

    //獲取Hibernate Session對象

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //啓用第一個過濾器

    session.enableFilter("effectiveDate")

            //爲過濾器設置參數

            .setParameter("asOfDate", new Date());

    //啓動第二個過濾器

    session.enableFilter("category")

            //爲過濾器設置參數

            .setParameter("catId", new Long(2));

    //執行查詢,該查詢沒有任何的查詢條件

    Iterator results = session.createQuery("from Product as p")

                          .iterate();

    //遍歷結果集

    while (results.hasNext())

    {

        Product p = (Product)results.next();

        System.out.println(p.getName());

        //此處獲取Product關聯的種類,過濾器也將自動應用過濾

        Iterator it = p.getCategories().iterator();

        System.out.println(p.getCategories().size());

        while (it.hasNext())

        {

            Category c = (Category)it.next();

            System.out.println(c.getName());

        }

    }

    tx.commit();

    HibernateUtil.closeSession();

}

通過使用過濾器定義常用的數據篩選規則,如果是臨時的數據篩選,還是使用常規查詢比較好。對於從前使用行列表達式視圖的地方,此處可以考慮使用過濾器。

posted @ 2009-07-19 09:08 jadmin 閱讀(0) 評論(0) 編輯

Hibernate的SQL查詢

4.5 SQL查詢

Hibernate還支持使用SQL查詢,使用SQL查詢可以利用某些數據庫的特性,或者用於將原有的JDBC應用遷移到Hibernate應用上。使用命名的SQL查詢還可以將SQL語句放在配置文件中配置,從而提高程序的解耦,命名SQL查詢還可以用於調用存儲過程。

如果是一個新的應用,通常不要使用SQL查詢。

SQL查詢是通過SQLQuery接口來表示的,SQLQuery接口是Query接口的子接口,因此完全可以調用Query接口的方法:

   ● setFirstResult(),設置返回結果集的起始點。

   ● setMaxResults(),設置查詢獲取的最大記錄數。

   ● list(),返回查詢到的結果集。

但SQLQuery比Query多了兩個重載的方法:

   ● addEntity,將查詢到的記錄與特定的實體關聯。

   ● addScalar,將查詢的記錄關聯成標量值。

執行SQL查詢的步驟如下:

(1)獲取Hibernate Session對象;

(2)編寫SQL語句;

(3)以SQL語句作爲參數,調用Session的createSQLQuery方法創建查詢對象;

(4)如果SQL語句包含參數,調用Query的setXxx方法爲參數賦值;

(5)調用SQLQuery對象的addEntity或addScalar方法將選出的結果與實體或標量值關聯;

(6)調用Query的list方法返回查詢的結果集。

看下面的SQL查詢示例:

private void test()

{

    //獲取Hibernate Session對象

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //編寫SQL語句

    String sqlString = "select {s.*} from student s where s.name like '馬軍'";

    //以SQL語句創建SQLQuery對象

    List l = session.createSQLQuery(sqlString)

                    //將查詢到的記錄與特定實體關聯起來

                    .addEntity("s",Student.class)

                    //返回全部的記錄集

                    .list();

    //遍歷結果集

    Iterator it = l.iterator();

    while (it.hasNext())

    {

        //因爲將查詢結果與Student類關聯,因此返回的是Student集合

        Student s = (Student)it.next();

        Set enrolments = s.getEnrolments();

        Iterator iter = enrolments.iterator();

        while(iter.hasNext())

        {

            Enrolment e = (Enrolment)iter.next();

            System.out.println(e.getCourse().getName());

        }

    }

    //提交事務

    tx.commit();

    //關閉Session

    HibernateUtil.closeSession();

}

上面的示例顯示了將查詢記錄關聯成一個實體的示例。事實上,SQL查詢也支持將查詢結果轉換成標量值,轉換成標量值可以使用addScalar方法,如:

Double max = (Double) session.createSQLQuery("select max(cat.weight) as maxWeight from cats cat")

        .addScalar("maxWeight", Hibernate.DOUBLE);

        .uniqueResult();

使用SQL查詢,如果需要將查詢到的結果轉換成特定實體,就要求爲選出的字段命名別名。這別名不是隨意命名的,而是以“/”實例名.屬性名“/”的格式命名,例如:

//依次將多個選出的字段命名別名,命名別名時都以ss作爲前綴,ss是關聯實體的別名

String sqlStr = "select stu.studentId as {ss.studentNumber},"

        + "stu.name as {ss.name} from "

        + "student as stu where stu.name like '楊海華'";

List l = session.createSQLQuery(sqlStr)

            //將查詢出的ss實例,關聯到Student類

            .addEntity("ss",Student.class)

            .list();

在第一個示例中,以{s.*}代表該表的全部字段,且關聯實例的別名也被指定爲s。

注意:如果不使用{s.*}的形式,就可讓實體別名和表別名互不相同。關聯實體的類型時,被關聯的類必須有對應的setter方法。

4.5.1 命名SQL查詢

可以將SQL語句不放在程序中,而放在配置文件中,這種方式以鬆耦合的方式配置SQL語句,可以提高程序解耦。

在Hibernate的映射文件中定義查詢名,然後確定查詢所用的SQL語句,然後就可以直接調用該命名SQL查詢。在這種情況下,不需要調用addEntity()方法,因爲在配置命名SQL查詢時,已經完成了查詢結果與實體的關聯。

下面是命名SQL查詢的配置片段:

<!-- 每個sql-query元素定義一個命名SQL查詢 -->

<sql-query name="mySqlQuery">

    <!-- 關聯返回的結果與實體類 -->

    <return alias="s" class="Student"/>

        <!-- 定義命名SQL查詢的SQL語句 -->

         SELECT {s.*}

        from student s WHERE s.name like'楊海華'

</sql-query>

sql-query元素是hibernate-mapping元素的子元素。因此,sql-query定義的名可以直接通過Session訪問,上面定義的mySqlQuery查詢可以直接訪問,下面是使用該命名SQL查詢的示例代碼:

private void testNamedSQl()

{

    //獲取Hibernate Session對象

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //調用命名查詢,直接返回結果

    List l = session.getNamedQuery("mySqlQuery")

                         .list();

    //遍歷結果集

    Iterator it = l.iterator();

    while (it.hasNext())

    {

        //在定義SQL查詢時,已經將結果集與Student類關聯起來

        //因此,集合裏的每個元素都是Student實例

        Student s = (Student)it.next();

        Set enrolments = s.getEnrolments();

        Iterator iter = enrolments.iterator();

        while(iter.hasNext())

        {

            Enrolment e = (Enrolment)iter.next();

            System.out.println("=====================================");

            System.out.println(e.getCourse().getName());

            System.out.println("=====================================");

        }

    }

    tx.commit();

    HibernateUtil.closeSession();

}

4.5.2 調用存儲過程

Hibernate 3增加了存儲過程的支持,該存儲過程只能返回一個結果集。

下面是Oracle 9i的存儲過程示例:

CREATE OR REPLACE FUNCTION selectAllEmployments

    RETURN SYS_REFCURSOR

AS

    st_cursor SYS_REFCURSOR;

BEGIN

    OPEN st_cursor FOR

SELECT EMPLOYEE, EMPLOYER,

STARTDATE, ENDDATE,

REGIONCODE, EID, VALUE, CURRENCY

FROM EMPLOYMENT;

      RETURN st_cursor;

END;

如果需要使用該存儲過程,可以先將其定義成命名SQL查詢,例如:

<!-- 定義命名SQL查詢,name屬性指定命名SQL查詢名 -->

<sql-query name="selectAllEmployees_SP" callable="true">

    <!-- 定義返回列與關聯實體類屬性之間的映射 -->

    <return alias="emp" class="Employment">

        <!-- 依次定義每列與實體類屬性的對應 -->

        <return-property name="employee" column="EMPLOYEE"/>

        <return-property name="employer" column="EMPLOYER"/>

        <return-property name="startDate" column="STARTDATE"/>

        <return-property name="endDate" column="ENDDATE"/>

        <return-property name="regionCode" column="REGIONCODE"/>

        <return-property name="id" column="EID"/>

        <!-- 將兩列值映射到一個關聯類的組件屬性 -->

        <return-property name="salary">

            <!-- 映射列與組件屬性之間的關聯 -->

            <return-column name="VALUE"/>

            <return-column name="CURRENCY"/>

        </return-property>

    </return>

    { ? = call selectAllEmployments() }

</sql-query>

調用存儲過程還有如下需要注意的地方:

   ● 因爲存儲過程本身完成了查詢的全部操作,所以調用存儲過程進行的查詢無法使用setFirstResult()/setMaxResults()進行分頁。

   ● 存儲過程只能返回一個結果集,如果存儲過程返回多個結果集,Hibernate將僅處理第一個結果集,其他將被丟棄。

   ● 如果在存儲過程裏設定SET NOCOUNT ON,將有更好的性能表現。當然也可以沒有該設定。

posted @ 2009-07-19 09:02 jadmin 閱讀(1) 評論(0) 編輯

Hibernate的條件查詢

4.4 條 件 查 詢

條件查詢是更具面向對象特色的數據查詢方式。條件查詢可通過如下3個類完成:

   ● Criteria,代表一次查詢。

   ● Criterion,代表一個查詢條件。

   ● Restrictions,產生查詢條件的工具類。

執行條件查詢的步驟如下:

(1)獲得Hibernate的Session對象。

(2)以Session對象創建Criteria對象。

(3)增加Criterion查詢條件。

(4)執行Criteria的list等方法返回結果集。

看下面的條件查詢示例:

private void test()

{

    //獲取Hibernate Session對象

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //創建Criteria和添加查詢條件同步完成

    //最後調用list方法,返回查詢到的結果集

    List l = session.createCriteria(Student.class)

        //此處增加限制條件必須是Student已經存在的屬性

        .add( Restrictions.gt("studentNumber" , new Long(20050231) ) )

        //如果要增加對Student的關聯類的屬性的限制則必須重新createCriteria()

        /如果此關聯屬性是集合,則只要集合裏任意一個對象的屬性滿足下面條件

        .createCriteria("enrolments")即可

        .add( Restrictions.gt("semester" , new Short("2") ) )

        .list();

        Iterator it = l.iterator();

    //遍歷查詢到的記錄

    while (it.hasNext())

    {

        Student s = (Student)it.next();

        System.out.println(s.getName());

        Set enrolments = s.getEnrolments();

        Iterator iter = enrolments.iterator();

        while(iter.hasNext())

        {

            Enrolment e = (Enrolment)iter.next();

            System.out.println(e.getCourse().getName());

       }

    }

    tx.commit();

    ibernateUtil.closeSession();

}

在條件查詢中,Criteria接口代表一次查詢,該查詢本身不具備任何的數據篩選功能,Session調用createCriteria(Class clazz)方法對某個持久化類創建條件查詢實例。

Criteria包含如下兩個方法:

   ● Criteria setFirstResult(int firstResult),設置查詢返回的第一行記錄。

   ● Criteria setMaxResults(int maxResults),設置查詢返回的記錄數。

這兩個方法與Query的這兩個方法用法相似,都用於完成查詢分頁。

而Criteria還包含如下常用方法:

   ● Criteria add(Criterion criterion),增加查詢條件。

   ● Criteria addOrder(Order order),增加排序規則。

   ● List list(),返回結果集。

Criterion接口代表一個查詢條件,該查詢條件由Restrictions負責產生,Restrictions是專門用於產生查詢條件的工具類,它的方法大部分都是靜態方法,常用的方法如下:

   ● static Criterion allEq(Map propertyNameValues),判斷指定屬性(由Map參數的key指定)和指定值(由Map參數的value指定)是否完全相等。

   ● static Criterion between(String propertyName,Object lo, Object hi),判斷屬性值在某個值範圍之內。

   ● static Criterion ilike(String propertyName, Object value),判斷屬性值匹配某個字符串。

   ● static Criterion ilike(String propertyName, String value,MatchMode matchMode),判斷屬性值匹配某個字符串,並確定匹配模式。

   ● static Criterion in(String propertyName,Collection values),判斷屬性值在某個集合內。

   ● static Criterion in(String propertyName,Object[] values),判斷屬性值是數組元素的其中之一。

   ● static Criterion isEmpty(String propertyName),判斷屬性值是否爲空。

   ● static Criterion isNotEmpty(String propertyName),判斷屬性值是否不爲空。

   ● static Criterion isNotNull(String propertyName),判斷屬性值是否爲空。

   ● static Criterion isNull(String propertyName),判斷屬性值是否不爲空。

   ● static Criterion not(Criterion expression),對Criterion求否。

   ● static Criterion sizeEq(String propertyName, int size),判斷某個屬性的元素個數是否與size相等。

   ● static Criterion sqlRestriction(String sql),直接使用SQL語句作爲篩選條件。

   ● static Criterion sqlRestriction(String sql, Object[] values, Type[] types),直接使用帶參數佔位符的SQL語句作爲條件,並指定多個參數值。

   ● static Criterion sqlRestriction(String sql, Object value, Type type),直接使用帶參數佔位符的SQL語句作爲條件,並指定參數值。

Order實例代表一個排序標準,Order有如下構造器:

Order(String propertyName, boolean ascending),根據propertyName排序,是否採用升序,如果後一個參數爲true,採用升序排序,否則採用降序排序。

如果需要使用關聯類的屬性來增加查詢條件,則應該對屬性再次使用createCriteria方法。看如下示例:

session.createCriteria(Person.class)

    .add(Restrictions.like("name" , "dd%"))

    .createCriteria("addresses")

    .add(Restrictions.like("addressdetail" , "上海%"))

    .list();

上面的代碼表示建立Person類的條件查詢,第一個查詢條件是直接過濾Person的屬性,即選出name屬性以dd開始的Person實例,第二個查詢條件則過濾Person關聯實例的屬性,其中addresses是Person類的關聯持久化類Address,而addressdetail則是Address類的屬性。值得注意的是,查詢並不是查詢Address持久化類,而是查詢Person持久化類。

注意:使用關聯類的條件查詢,依然是查詢原有持久化類的實例,而不是查詢被關聯類的實例。

posted @ 2009-07-19 08:59 jadmin 閱讀(5) 評論(0) 編輯

Hibernate的HQL查詢

4.3 使用HQL查詢

Hibernate提供了異常強大的查詢體系,使用Hibernate有多種查詢方式。可以選擇使用Hibernate的HQL查詢,或者使用條件查詢,甚至可以使用原生的SQL查詢語句,此外還提供了一種數據過濾功能,這些都可用於篩選目標數據。

下面分別介紹Hibernate的4種數據篩選方法:

4.3.1 HQL查詢

HQL是Hibernate Query Language的縮寫,HQL的語法很像SQL的語法,但HQL是一種面向對象的查詢語言。因此,SQL的操作對象是數據表和列等數據對象,而HQL的操作對象是類、實例、屬性等。

HQL是完全面向對象的查詢語言,因此可以支持繼承和多態等特徵。

HQL查詢依賴於Query類,每個Query實例對應一個查詢對象。使用HQL查詢可按如下步驟進行:

(1)獲取Hibernate Session對象;

(2)編寫HQL語句;

(3)以HQL語句作爲參數,調用Session的createQuery方法創建查詢對象;

(4)如果HQL語句包含參數,調用Query的setXxx方法爲參數賦值;

(5)調用Query對象的list等方法遍歷查詢結果。

看下面的查詢示例:

public class HqlQuery

{

    public static void main(String[] args)throws Exception

    {

        HqlQuery mgr = new HqlQuery();

        //調用查詢方法

        mgr.findPersons();

        //調用第二個查詢方法

        mgr.findPersonsByHappenDate();

        HibernateUtil.sessionFactory.close();

    }

    //第一個查詢方法

    private void findPersons()

    {

        //獲得Hibernate Session

        Session sess = HibernateUtil.currentSession();

        //開始事務

        Transaction tx = sess.beginTransaction();

        //以HQL語句創建Query對象.

        //執行setString方法爲HQL語句的參數賦值

        //Query調用list方法訪問查詢的全部實例

        List pl = sess.createQuery("from Person p where p.myEvents.title

        = :eventTitle")

                        .setString("eventTitle","很普通事情")

                        .list();

        //遍歷查詢的全部結果

        for (Iterator pit = pl.iterator() ; pit.hasNext(); )

        {

            Person p = ( Person )pit.next();

            System.out.println(p.getName());

        }

        //提交事務

        tx.commit();

        HibernateUtil.closeSession();

    }

    //第二個查詢方法

    private void findPersonsByHappenDate()throws Exception

    {

        //獲得Hibernate Session對象

        Session sess = HibernateUtil.currentSession();

        Transaction tx = sess.beginTransaction();

        //解析出Date對象

        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

        Date start = sdf.parse("2005-01-01");

        System.out.println("系統開始通過日期查找人" + start);

        //通過Session的createQuery方法創建Query對象

        //設置參數

        //返回結果集

        List pl = sess.createQuery(

            "from Person p where p.myEvents.happenDate between :firstDate

            and :endDate")

                        .setDate("firstDate",start)

                        .setDate("endDate",new Date())

                        .list();

        //遍歷結果集

        for (Iterator pit = pl.iterator() ; pit.hasNext(); )

        {

            Person p = ( Person )pit.next();

            System.out.println(p.getName());

        }

        tx.commit();

        HibernateUtil.closeSession();

    }

}

通過上面的示例程序,可看出查詢步驟基本相似。Query對象可以連續多次設置參數,這得益於Hibernate Query的設計。

通常,setXxx方法的返回值都是void,但Hibernate Query的setXxx方法返回值是Query本身。因此,程序通過Session創建Query後,直接多次調用setXxx方法爲HQL語句的參數賦值,再直接調用list方法返回查詢到的全部結果即可。

Query還包含兩個方法:

   ● setFirstResult(int firstResult),設置返回的結果集從第幾條記錄開始。

   ● setMaxResults(int maxResults),設置本次查詢返回的結果數。

這兩個方法用於實現Hibernate分頁。

下面簡單介紹HQL語句的語法。

HQL語句本身是不區分大小寫的。也就是說,HQL語句的關鍵字和函數都是不區分大小寫的。但HQL語句中所使用的包名、類名、實例名和屬性名都區分大小寫。

4.3.2 HQL查詢的from子句

from子句是最簡單的HQL語句,也是最基本的HQL語句。from關鍵字後緊跟持久化類的類名。例如:

from Person

表明從Person持久化類中選出全部的實例。

大部分時候,推薦爲該Person的每個實例起別名。例如:

from Person as p

在上面的HQL語句中,Person持久化類中的實例的別名爲p,既然 p是實例名,因此也應該遵守Java的命名規則:第一個單詞的首字母小寫,後面每個單詞的首字母大寫。

命名別名時,as關鍵字是可選的,但爲了增加可讀性,建議保留。

from後還可同時出現多個持久化類,此時將產生一個笛卡兒積或跨表的連接。

4.3.3 HQL查詢的select子句

select子句用於確定選擇出的屬性,當然select選擇的屬性必須是from後持久化類包含的屬性。例如:

select p.name from Person as p

select可以選擇任意屬性,不僅可以選擇持久化類的直接屬性,還可以選擇組件屬性包含的屬性,例如:

select p.name.firstName from Person as p

select也支持將選擇出的屬性存入一個List對象中,例如:

select new list(p.name , p.address) from Person as p

甚至可以將選擇出的屬性直接封裝成對象,例如:

select new ClassTest(p.name , p.address) from Person as p

前提是ClassTest支持p.name和p.address的構造器,假如p.name的數據類型是           String,p.address的數據類型是String,則ClassTest必須有如下的構造器:

ClassTest(String s1, String s2)

select還支持給選中的表達式命名別名,例如:

select p.name as personName from Person as p

這種用法與new map結合使用更普遍。如:

select new map(p.name as personName) from Person as p

在這種情形下,選擇出的是Map結構,以personName爲key,實際選出的值作爲value。

4.3.4 HQL查詢的聚集函數

HQL也支持在選出的屬性上,使用聚集函數。HQL支持的聚集函數與SQL完全相同,有如下5個:

   ● avg,計算屬性平均值。

   ● count,統計選擇對象的數量。

   ● max,統計屬性值的最大值

   ● min,統計屬性值的最小值。

   ● sum,計算屬性值的總和。

例如:

select count(*) from Person

select max(p.age) from Person as p

select子句還支持字符串連接符、算術運算符以及SQL函數。如:

select p.name || "" || p.address from Person as p

select子句也支持使用distinct和all關鍵字,此時的效果與SQL中的效果完全相同。

4.3.5 多態查詢

HQL語句被設計成能理解多態查詢,from後跟的持久化類名,不僅會查詢出該持久化類的全部實例,還會查詢出該類的子類的全部實例。

如下面的查詢語句:

from Person as p

該查詢語句不僅會查詢出Person的全部實例,還會查詢出Person的子類,如Teacher的全部實例,前提是Person和Teacher完成了正確的繼承映射。

HQL支持在from子句中指定任何Java類或接口,查詢會返回繼承了該類的持久化子類的實例或返回實現該接口的持久化類的實例。下面的查詢語句返回所有被持久化的對象:

from java.lang.Object o

如果Named接口有多個持久化類,下面的語句將返回這些持久化類的全部實例:

from Named as n

注意:後面的兩個查詢將需要多個SQL SELECT語句,因此無法使用order by子句對結果集進行排序,從而,不允許對這些查詢結果使用Query.scroll()方法。

4.3.6 HQL查詢的where子句

where子句用於篩選選中的結果,縮小選擇的範圍。如果沒有爲持久化實例命名別名,可以直接使用屬性名引用屬性。

如下面的HQL語句:

from Person where name like 'tom%'

上面HQL語句與下面的語句效果相同:

from Person as p where p.name like "tom%"

在後面的HQL語句中,如果爲持久化實例命名了別名,則應該使用完整的屬性名。兩個HQL語句都可返回name屬性以tom開頭的實例。

複合屬性表達式加強了where子句的功能,例如如下HQL語句:

from Cat cat where cat.mate.name like "kit%"

該查詢將被翻譯成爲一個含有內連接的SQL查詢,翻譯後的SQL語句如下:

select * from cat_table as table1 cat_table as table2 where table1.mate =

table2.id and table1.name like "kit%"

再看下面的HQL查詢語句:

from Foo foo where foo.bar.baz.customer.address.city like"guangzhou%"

翻譯成SQL查詢語句,將變成一個四表連接的查詢。

=運算符不僅可以被用來比較屬性的值,也可以用來比較實例:

from Cat cat, Cat rival where cat.mate = rival.mate

select cat, mate

from Cat cat, Cat mate

where cat.mate = mate

特殊屬性(小寫)id可以用來表示一個對象的標識符。(也可以使用該對象的屬性名。)

from Cat as cat where cat.id = 123

from Cat as cat where cat.mate.id = 69

第二個查詢是一個內連接查詢,但在HQL查詢語句下,無須體會多表連接,而完全使用面向對象方式的查詢。

id也可代表引用標識符。例如,Person類有一個引用標識符,它由country屬性 與medicareNumber兩個屬性組成。

下面的HQL語句有效:

from Person as person

where person.id.country = 'AU'

    and person.id.medicareNumber = 123456

from Account as account

where account.owner.id.country = 'AU'

    and account.owner.id.medicareNumber = 123456

第二個查詢跨越兩個表Person和Account。是一個多表連接查詢,但此處感受不到多表連接查詢的效果。

在進行多態持久化的情況下,class關鍵字用來存取一個實例的鑑別值(discriminator value)。嵌入where子句中的Java類名,將被作爲該類的鑑別值。例如:

from Cat cat where cat.class = DomesticCat

where子句中的屬性表達式必須以基本類型或java.lang.String結尾,不要使用組件類型屬性結尾,例如Account有Person屬性,而Person有Name屬性,Name有firstName屬性。

看下面的情形:

from Account as a where a.person.name.firstName like "dd%" //正確

from Account as a where a.person.name like "dd%" //錯誤

4.3.7 表達式

HQL的功能非常豐富,where子句後支持的運算符異常豐富,不僅包括SQL的運算符,還包括EJB-QL的運算符等。

where子句中允許使用大部分SQL支持的表達式:

   ● 數學運算符+、–、*、/ 等。

   ● 二進制比較運算符=、>=、<=、<>、!=、like等。

   ● 邏輯運算符and、or、not等。

   ● in、not in、between、is null、is not null、is empty、is not empty、member of和not member of等。

   ● 簡單的case、case ... when ... then ... else ... end和case、case when ... then ... else ...       end等。

   ● 字符串連接符value1 || value2或使用字符串連接函數concat(value1 , value2)。

   ● 時間操作函數current_date()、current_time()、current_timestamp()、second()、minute()、hour()、day()、month()、year()等。

   ● HQL還支持EJB-QL 3.0所支持的函數或操作substring()、trim()、lower()、upper()、length()、locate()、abs()、sqrt()、bit_length()、coalesce()和nullif()等。

   ● 還支持數據庫的類型轉換函數,如cast(... as ...),第二個參數是Hibernate的類型名,或者extract(... from ...),前提是底層數據庫支持ANSI cast() 和extract()。

   ● 如果底層數據庫支持如下單行函數sign()、trunc()、rtrim()、sin()。則HQL語句也完全可以支持。

   ● HQL語句支持使用?作爲參數佔位符,這與JDBC的參數佔位符一致,也可使用命名參數佔位符號,方法是在參數名前加冒號 :,例如 :start_date和:x1等。

   ● 當然,也可在where子句中使用SQL常量,例如'foo'、69、'1970-01-01 10:00:         01.0'等。

   ● 還可以在HQL語句中使用Java public static final 類型的常量,例如eg.Color.TABBY。

除此之外,where子句還支持如下的特殊關鍵字用法。

   ● in與between...and可按如下方法使用:

from DomesticCat cat where cat.name between 'A' and 'B'

from DomesticCat cat where cat.name in ( 'Foo','Bar','Baz')

   ● 當然,也支持not in和not between...and的使用,例如:

from DomesticCat cat where cat.name not between 'A' and 'B'

from DomesticCat cat where cat.name not in ( 'Foo','Bar','Baz' )

   ● 子句is null與is not null可以被用來測試空值,例如:

from DomesticCat cat where cat.name is null;

from Person as p where p.address is not null;

如果在Hibernate配置文件中進行如下聲明:

<property name="hibernate.query.substitutions">true 1, false 0</property>

上面的聲明表明,HQL轉換SQL語句時,將使用字符1和0來取代關鍵字true和false。然後將可以在表達式中使用布爾表達式,例如:

from Cat cat where cat.alive = true

   ● size關鍵字用於返回一個集合的大小,例如:

from Cat cat where cat.kittens.size > 0

from Cat cat where size(cat.kittens) > 0

   ● 對於有序集合,還可使用minindex與maxindex函數代表最小與最大的索引序數。同理,可以使用minelement與maxelement函數代表集合中最小與最大的元素。         例如:

from Calendar cal where maxelement(cal.holidays) > current date

from Order order where maxindex(order.items) > 100

from Order order where minelement(order.items) > 10000

   ● 可以使用SQL函數any、some、all、exists、in操作集合裏的元素,例如:

//操作集合元素

select mother from Cat as mother, Cat as kit

where kit in elements(foo.kittens)

//p的name屬性等於集合中某個元素的name屬性

select p from NameList list, Person p

where p.name = some elements(list.names)

//操作集合元素

from Cat cat where exists elements(cat.kittens)

from Player p where 3 > all elements(p.scores)

from Show show where 'fizard' in indices(show.acts)

注意這些結構變量size、elements、indices、minindex、maxindex、minelement、maxelement 等,只能在where子句中使用。

   ● where子句中,有序集合的元素(arrays, lists, maps)可以通過[ ]運算符訪問。例如:

//items是有序集合屬性,items[0]代表第一個元素

from Order order where order.items[0].id = 1234

//holidays是map集合屬性,holidays[national day]代表其中一個元素

select person from Person person, Calendar calendar

where calendar.holidays['national day'] = person.birthDay

and person.nationality.calendar = calendar

//下面同時使用list 集合和map集合屬性

select item from Item item, Order order

where order.items[ order.deliveredItemIndices[0] ] = item and order.id = 11

select item from Item item, Order order

where order.items[ maxindex(order.items) ] = item and order.id = 11

在[]中的表達式甚至可以是一個算術表達式,例如:

select item from Item item, Order order

where order.items[ size(order.items) - 1 ] = item

藉助於HQL,可以大大簡化選擇語句的書寫,提高查詢語句的可讀性,看下面的HQL語句:

select cust

from Product prod,

    Store store

    inner join store.customers cust

where prod.name = 'widget'

    and store.location.name in ( 'Melbourne', 'Sydney' )

    and prod = all elements(cust.currentOrder.lineItems)

如果翻譯成SQL語句,將變成如下形式:

SELECT cust.name, cust.address, cust.phone, cust.id, cust.current_order

FROM customers cust,

    stores store,

    locations loc,

    store_customers sc,

    product prod

WHERE prod.name = 'widget'

    AND store.loc_id = loc.id

    AND loc.name IN ( 'Melbourne', 'Sydney' )

    AND sc.store_id = store.id

    AND sc.cust_id = cust.id

    AND prod.id = ALL(

        SELECT item.prod_id

        FROM line_items item, orders o

        WHERE item.order_id = o.id

            AND cust.current_order = o.id

    )

4.3.8 order by子句

查詢返回的列表(list)可以根據類或組件屬性的任何屬性進行排序,例如:

from Person as p

order by p.name, p.age

還可使用asc或desc關鍵字指定升序或降序的排序規則,例如:

from Person as p

order by p.name asc , p.age desc

如果沒有指定排序規則,默認採用升序規則。即是否使用asc關鍵字是沒有區別的,加asc是升序排序,不加asc也是升序排序。

4.3.9 group by子句

返回聚集值的查詢可以對持久化類或組件屬性的屬性進行分組,分組所使用的group by子句。看下面的HQL查詢語句:

select cat.color, sum(cat.weight), count(cat)

from Cat cat

group by cat.color

類似於SQL的規則,出現在select後的屬性,要麼出現在聚集函數中,要麼出現在group by的屬性列表中。看下面示例:

//select後出現的id出現在group by之後,而name屬性則出現在聚集函數中

select foo.id, avg(name), max(name)

from Foo foo join foo.names name

group by foo.id

having子句用於對分組進行過濾,如下:

select cat.color, sum(cat.weight), count(cat)

from Cat cat

group by cat.color

having cat.color in (eg.Color.TABBY, eg.Color.BLACK)

注意:having子句用於對分組進行過濾,因此having子句只能在有group by子句時纔可以使用,沒有group by子句,不能使用having子句。

Hibernate的HQL語句會直接翻譯成數據庫SQL語句。因此,如果底層數據庫支持的having子句和group by子句中出現一般函數或聚集函數,HQL語句的having子句和order by 子句中也可以出現一般函數和聚集函數。

例如:

select cat

from Cat cat

join cat.kittens kitten

group by cat

having avg(kitten.weight) > 100

order by count(kitten) asc, sum(kitten.weight) desc

注意:group by子句與 order by子句中都不能包含算術表達式。

4.3.10 子查詢

如果底層數據庫支持子查詢,則可以在HQL語句中使用子查詢。與SQL中子查詢相似的是,HQL中的子查詢也需要使用()括起來。如:

from Cat as fatcat

where fatcat.weight > ( select avg(cat.weight) from DomesticCat cat )

如果select中包含多個屬性,則應該使用元組構造符:

from Cat as cat

where not ( cat.name, cat.color ) in (

    select cat.name, cat.color from DomesticCat cat

)

4.3.11 fetch關鍵字

對於集合屬性,Hibernate默認採用延遲加載策略。例如,對於持久化類Person,有集合屬性scores。加載Person實例時,默認不加載scores屬性。如果Session被關閉,Person實例將無法訪問關聯的scores屬性。

爲了解決該問題,可以在Hibernate映射文件中取消延遲加載或使用fetch join,例如:

from Person as p join p.scores

上面的fetch語句將會初始化person的scores集合屬性。

如果使用了屬性級別的延遲獲取,可以使用fetch all properties來強制Hibernate立即抓取那些原本需要延遲加載的屬性,例如:

from Document fetch all properties order by name

from Document doc fetch all properties where lower(doc.name) like '%cats%'

4.3.12 命名查詢

HQL查詢還支持將查詢所用的HQL語句放入配置文件中,而不是代碼中。通過這種方式,可以大大提供程序的解耦。

使用query元素定義命名查詢,下面是定義命名查詢的配置文件片段:

<!-- 定義命名查詢 -->

<query name="myNamedQuery">

    <!-- 此處確定命名查詢的HQL語句 -->

    from Person as p where p.age > ?

</query>

該命名的HQL查詢可以直接通過Session訪問,調用命名查詢的示例代碼如下:

private void findByNamedQuery()throws Exception

{

    //獲得Hibernate Session對象

    Session sess = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = sess.beginTransaction();

    System.out.println("執行命名查詢");

    //調用命名查詢

    List pl = sess.getNamedQuery("myNamedQuery")

                        //爲參數賦值

                       .setInteger(0 , 20)

                        //返回全部結果

                       .list();

    //遍歷結果集

    for (Iterator pit = pl.iterator() ; pit.hasNext(); )

    {

        Person p = ( Person )pit.next();

        System.out.println(p.getName());

    }

    //提交事務

    tx.commit();

    HibernateUtil.closeSession();

}

posted @ 2009-07-19 08:48 jadmin 閱讀(4) 評論(0) 編輯

Hibernate的批量處理

Hibernate的批量處理

Hibernate完全以面向對象的方式來操作數據庫,當程序裏以面向對象的方式操作持久化對象時,將被自動轉換爲對數據庫的操作。例如調用Session的delete()方法來刪除持久化對象,Hibernate將負責刪除對應的數據記錄;當執行持久化對象的set方法時,Hibernate將自動轉換爲對應的update方法,修改數據庫的對應記錄。

問題是如果需要同時更新100 000條記錄,是不是要逐一加載100 000條記錄,然後依次調用set方法——這樣不僅繁瑣,數據訪問的性能也十分糟糕。對這種批量處理的場景,Hibernate提供了批量處理的解決方案,下面分別從批量插入、批量更新和批量刪除3個方面介紹如何面對這種批量處理的情形。

1) 批量插入

如果需要將100 000條記錄插入數據庫,通常Hibernate可能會採用如下做法:

Session session = sessionFactory.openSession();

Transaction tx = session.beginTransaction();

for ( int i=0; i<100000; i++ ) {

    User u = new User (.....);

    session.save(customer);

}

tx.commit();

session.close();

但隨着這個程序的運行,總會在某個時候運行失敗,並且拋出OutOfMemoryException(內存溢出異常)。這是因爲Hibernate的Session持有一個必選的一級緩存,所有的User實例都將在Session級別的緩存區進行了緩存的緣故。

爲了解決這個問題,有個非常簡單的思路:定時將Session緩存的數據刷新入數據庫,而不是一直在Session級別緩存。可以考慮設計一個累加器,每保存一個User實例,累加器增加1。根據累加器的值決定是否需要將Session緩存中的數據刷入數據庫。

下面是增加100 000個User實例的代碼片段:

private void testUser()throws Exception

{

    //打開Session

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //循環100 000次,插入100 000條記錄

    for (int i = 0 ; i < 1000000 ; i++ )

    {

        //創建User實例

        User u1 = new User();

        u1.setName("xxxxx" + i);

        u1.setAge(i);

        u1.setNationality("china");

        //在Session級別緩存User實例

        session.save(u1);

        //每當累加器是20的倍數時,將Session中的數據刷入數據庫,並清空Session緩存

        if (i % 20 == 0)

        {

            session.flush();

            session.clear();

            tx.commit();

            tx = session.beginTransaction();

        }

    }

    //提交事務

    tx.commit();

    //關閉事務

    HibernateUtil.closeSession();

}

上面代碼中,當i%20 == 0時,手動將Session處的緩存數據寫入數據庫,並手動提交事務。如果不提交事務,數據將依然緩存在事務處——未進入數據庫,也將引起內存溢出的異常。

這是對Session級別緩存的處理,還應該通過如下配置來關閉SessionFactory的二級      緩存。

hibernate.cache.use_second_level_cache false

注意:除了要手動清空Session級別的緩存外,最好關閉SessionFactory級別的二級緩存。否則,即使手動清空Session級別的緩存,但因爲在SessionFactory級別還有緩存,也可能引發異常。

2) 批量更新

上面介紹的方法同樣適用於批量更新數據,如果需要返回多行數據,可以使用scroll()方法,從而可充分利用服務器端遊標所帶來的性能優勢。下面是進行批量更新的代碼片段:

private void testUser()throws Exception

{

    //打開Session

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //查詢出User表中的所有記錄

    ScrollableResults users = session.createQuery("from User")

        .setCacheMode(CacheMode.IGNORE)

        .scroll(ScrollMode.FORWARD_ONLY);

    int count=0;

    //遍歷User表中的全部記錄

    while ( users.next() )

    {

        User u = (User) users.get(0);

        u.setName("新用戶名" + count);

        //當count爲20的倍數時,將更新的結果從Session中flush到數據庫

        if ( ++count % 20 == 0 )

        {

            session.flush();

            session.clear();

        }

    }

    tx.commit();

    HibernateUtil.closeSession();

}

通過這種方式,雖然可以執行批量更新,但效果非常不好。執行效率不高,而且需要先執行數據查詢,然後再執行數據更新,並且這種更新將是逐行更新,即每更新一行記錄,都需要執行一條update語句,性能非常低下。

爲了避免這種情況,Hibernate提供了一種類似於SQL的批量更新和批量刪除的HQL語法。

3) SQL風格的批量更新/刪除

Hibernate提供的HQL語句也支持批量的UPDATE和DELETE語法。

批量UPDATE和DELETE語句的語法格式如下:

UPDATE | DELETE FROM? ClassName [WHERE WHERE_CONDITIONS]

關於上面的語法格式有以下四點值得注意:

   ● 在FROM子句中,FROM關鍵字是可選的。即完全可以不寫FROM關鍵字。

   ● 在FROM子句中只能有一個類名,該類名不能有別名。

   ● 不能在批量HQL語句中使用連接,顯式的或隱式的都不行。但可以在WHERE子句中使用子查詢。

   ● 整個WHERE子句是可選的。

假設,需要批量更改User類實例的name屬性,可以採用如下代碼片段完成:

private void testUser()throws Exception

{

    //打開Session

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //定義批量更新的HQL語句

    String hqlUpdate = "update User set name = :newName";

    //執行更新

    int updatedEntities = session.createQuery( hqlUpdate )

                           .setString( "newName", "新名字" )

                           .executeUpdate();

    //提交事務

    tx.commit();

    HibernateUtil.closeSession();

}

從上面代碼中可以看出,這種語法非常類似於PreparedStatement的executeUpdate語法。實際上,HQL的這種批量更新就是直接借鑑了SQL語法的UPDATE語句。

注意:使用這種批量更新語法時,通常只需要執行一次SQL的UPDATE語句,就可以完成所有滿足條件記錄的更新。但也可能需要執行多條UPDATE語句,這是因爲有繼承映射等特殊情況,例如有一個Person實例,它有Customer的子類實例。當批量更新Person實例時,也需要更新Customer實例。如果採用joined-subclass或union-subclass映射策略,Person和Customer實例保存在不同的表中,因此可能需要多條UPDATE語句。

執行一個HQL DELETE,同樣使用 Query.executeUpdate() 方法,下面是一次刪除上面全部記錄的代碼片段:

private void testUser()throws Exception

{

    //打開Session實例

    Session session = HibernateUtil.currentSession();

    //開始事務

    Transaction tx = session.beginTransaction();

    //定義批量刪除的HQL語句

    String hqlUpdate = "delete User";

    //執行批量刪除

    int updatedEntities = session.createQuery( hqlUpdate )

                           .executeUpdate();

    //提交事務

    tx.commit();

    //關閉Session

    HibernateUtil.closeSession();

}

由Query.executeUpdate()方法返回一個整型值,該值是受此操作影響的記錄數量。實際上,Hibernate的底層操作是通過JDBC完成的。因此,如果有批量的UPDATE或DELETE操作被轉換成多條UPDATE或DELETE語句,該方法返回的是最後一條SQL語句影響的記錄行數。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章