J2EE應用下基於AOP的抓取策略

才智咖 人氣:2.68W

如何通過最精簡的SQL查詢獲取所需的資料。很多時候這可不是輕而易舉的事情。預設情況下,O/R Mapping工具會按需載入資料,除非你改變了其預設設定。延遲載入行為保證了依賴的資料只有在真正請求時才會被載入進來,這樣就可以避免建立無謂的物件。有時我們的業務並不會使用到依賴的那些元件,這時延遲載入就派上用場了,同時也無需載入那些用不上的元件了。

J2EE應用下基於AOP的抓取策略

典型情況下,我們的業務很清楚需要哪些資料。但由於使用了延遲載入,在執行大量Select查詢時資料庫的效能會降低,因為業務所需的資料並不是一下子獲得的。這樣,對於那些需要支援大量請求的應用來說可能會產生瓶頸(可伸縮性問題)。來看個例子吧,假設某個業務流程想要得到一個Person及其Address資訊。我們將Address元件配置成延遲載入,這樣要想得到所需的資料就需要更多的SQL查詢,也就是說首先查詢Person,然後再查詢Address。這增加了資料庫與應用之間的通訊成本。解決辦法就是在一個單獨的查詢中將 Person和Address都得到,因為我們知道這兩個元件都是業務流程所需的。如果在DAO/Repository及底層Service開發特定於業務的Fetching-API,對於那些擁有不同資料集的相同領域物件來說,我們就得編寫不同的API進行抓取並組裝了。這麼做會使Repository及底層Service過於膨脹,最終變成維護的夢魘。延遲抓取的另一個問題就是在獲取到請求的資料前要一直開啟資料庫連線,否則應用就會丟擲一個延遲載入異常。說明:如果在查詢中使用預先抓取來獲取二級快取中的資料時,我們將無法解決上面提出的問題。對於Hibernate來說,如果我們使用預先抓取來獲取二級快取中的資料,那麼它將從資料庫而不是快取中去獲取資料,哪怕是二級快取中已經存在該資料。這就說明Hibernate也沒有解決這個問題,從而表明我們不應該在查詢中通過預先抓取來獲得二級快取中的物件。對於那些可以讓我們調節查詢以獲取快取物件的O/R Mapping工具來說,如果快取中有物件就會從快取中獲取,否則採取預先抓取的方式。這就解決了上面提到的事務/DB連線問題,因為在查詢的執行過程中會同時獲取快取中的資料而不是按需讀取(也就是延遲載入)。通過下面的示例程式碼來了解一下延遲載入所面對的問題及解決辦法。考慮如下場景:某領域中有3個實體,分別是Employee、Department及Dependent。這三個實體之間的關係如下:Employee有0或多個dependents。

Department有0或多個employees。

Employee屬於0或1個department。

我們要執行三個操作:獲取employee的詳細資訊。

獲取employee及其dependent的詳細資訊。

獲取employee及其department的詳細資訊。

以上三個操作需要獲取並呈現不同的資料。使用延遲載入有如下弊端:如果對實體employee所關聯的dependent和department這兩個實體使用延遲載入,那麼在操作2和3中就會生成更多的SQL查詢語句。

在多個查詢語句的執行過程中需要保持資料庫連線,否則會丟擲一個延遲載入異常,這將導致資料出現問題。

但另一方面,使用預先抓取也存在如下弊端:對employee所對應的dependents和department採取預先抓取會產生不必要的`資料。

無法在特定的場景下對查詢進行調優。

在Repository/DAO或底層服務中使用特定於操作的API可以解決上述問題,但卻會導致如下問題:程式碼膨脹——不管是Service還是Repository/DAO類都無法倖免。

維護的夢魘——不管是Service還是Repository/DAO層,只要有新的操作都需要增加新的API。

程式碼重複——有時底層服務需要在獲取的實體上增加某些業務邏輯,與之類似,還要在資料返回前檢查DAO/Repository層的查詢響應以驗證資料可用性。

為了解決上面這些問題,Repository/DAO層需要根據不同的業務情況執行不同的查詢來獲取實體。就像Aspect類所定義的那樣,我們可以根據特定的操作使用不同的抓取機制來覆蓋Repository/DAO類所定義的預設抓取模式。所有的抓取模式類都實現了相同的介面。

Repository類使用了上述的抓取模式來執行查詢,如下程式碼所示:public Employee findEmployeeById(int employeeId) {

List employee = (yEmployeeById(),

new Integer(employeeId));

if(() == 0)

return null;

return (Employee)(0);

}

Repository類中的employee的抓取策略需要根據實際情況進行調整。我們決定將Repository層的抓取策略調整到 Repository和Service層外,放在一個Aspect類中,這樣當需要增加新的業務邏輯時只需修改Aspect類並增加一個針對於 Repository的抓取策略實現即可。這裡我們使用了面向方面的程式設計(Aspect Oriented Programming)以根據業務的不同使用不同的抓取策略。什麼是面向方面的程式設計?面向方面的程式設計(AOP)可以通過模組化的形式實現實際應用中的橫切關注點,如日誌、追蹤、動態分析、錯誤處理、服務水平協議、策略增強、池化、快取、併發控制、安全、事務管理以及業務規則等等。對這些關注點的傳統實現方式需要我們將這些實現融合到模組的核心關注點中。憑藉AOP,我們可以在一個叫做方面(aspect)的獨立模組中實現這些關注點。模組化的結果就是設計簡化、易於理解、質量提升、開發時間降低以及對系統需求變更的快速響應。接下來讀者朋友們可以參考 Ramnivas Laddad所著的《AspectJ in Action》一書以詳細瞭解AspectJ的概念以及程式設計方式,還可以瞭解一下AspectJ的開發工具。Aspect在抓取策略實現上扮演著重要角色。抓取策略是個業務層的橫切關注點,它會隨著業務的變化而變化。Aspect對於特定的業務邏輯下使用何種抓取策略起到了至關重要的作用。這裡我們將對抓取策略的管理放在了底層服務和Respository層之外。任何新的業務都可能需要不同的抓取策略,這樣我們就無需修改底層服務或是Respository層的API就能應用新的抓取策略了。**

Identify the getEmployeeWithDepartmentDetails flow where you need to change the fetching

strategy at repository level

*/

pointcut empWithDepartmentDetail(): call(* EmployeeById(int))

&& cflow(execution(* mployeeWithDepartmentDetails(int)));

/**

When you are at the specified poincut before continuing further update the fetchingStrategy in

EmployeeRepositoryImpl to EmployeeWithDepartmentFetchingStrategy

*/

before(EmployeeRepositoryImpl r): empWithDepartmentDetail() && target(r) {

hingStrategy = new EmployeeWithDepartmentFetchingStrategy();

}

/**

Identify the getEmployeeWithDependentDetails flow where you need to change the fetching

staratergy at repository level

*/

pointcut empWithDependentDetail(): call(* EmployeeById(int))

&& cflow(execution(* mployeeWithDependentDetails(int)));

/**

When you are at the specified poincut before continuing further update the fetchingStrategy in

EmployeeRepositoryImpl to EmployeeWithDependentFetchingStrategy

*/

before(EmployeeRepositoryImpl r): empWithDependentDetail() && target(r) {

hingStrategy = new EmployeeWithDependentFetchingStrategy();

}

這樣,Repository到底要執行何種查詢就不是由Service和Repository層所決定了,而是由外面的Aspect決定,縱使增加了新的業務也無需修改底層服務和Repository層。決定執行何種查詢的邏輯就成為一個橫切關注點了,它被放在Aspect中。Aspect會根據業務規則的不同在Service層呼叫Repository層的API之前將抓取策略注入到Repository中。這樣我們就可以使用相同的Service和 Repository層API來滿足各種不同的業務規則了。來看個具體示例吧,該示例會同時抓取一個employee的Department和Dependent的詳細資訊。我們需要對業務層進行一些變更,增加一個方法:getEmployeeWithDepartmentAndDependentsDetails(int employeeId)。實現新的抓取策略類EmployeeWithDepartmentAndDependentFetchingStaratergy,後者又實現了EmployeeFetchingStrategy並重寫了queryEmployeeById方法,該方法會返回優化後的查詢,可以在一個SQL語句中獲取所需資料。由Aspect將上述的抓取策略注入到相關的業務中,如下所示:pointcut empWithDependentAndDepartmentDetail(): call(* EmployeeById(int))

&& cflow(execution(* mployeeWithDepartmentAndDependentsDetails(int)));

before(EmployeeRepositoryImpl r): empWithDependentAndDepartmentDetail() && target(r) {

hingStrategy = new EmployeeWithDepartmentAndDependentFetchingStaratergy();

}

如你所見,我們並沒有修改底層業務與Repository層而是使用Aspect和一個新的FetchingStrategy實現就完成了上述新增的業務。現在我們來談談關於二級快取的查詢優化問題。在上面的示例程式碼中,我們對department實體進行一些修改並配置在二級快取中。如果對 department實體採取預先抓取,那麼對於同樣的department例項,縱使它位於二級快取中,每次也都需要查詢資料庫。如果不在查詢中獲取 department實體,那麼業務層就需要參與到事務當中,因為我們並沒有將department實體快取起來而是通過延遲載入的方式得到它。這樣,事務宣告就從底層移到了業務層,雖然我們知道該業務需要哪些資料,但O/R Mapping工具卻沒有提供相應的機制來解決上面遇到的問題,即預先抓取快取中的資料。對於那些沒有快取的資料來說這種方式沒什麼問題,但對於快取資料來說,這就依賴於O/R Mapping工具了,因為只有它才能解決快取資料問題。該示例附帶的原始碼詳細解釋了抓取策略。該zip檔案含有一個工程示例,闡述了上面談到的所有場景。你可以使用任何IDE或是使用aspectj編譯器從命令列執行程式碼。在執行前請確保erties檔案與你機器上的資訊一致並建立示例應用所需的表。你可以使用Eclipse IDE以及AJDT外掛執行程式碼,請按照下面的步驟進行:解壓縮下載好的程式碼並將工程匯入到Eclipse中。

配置Resources/dbscript目錄下的erties檔案中的資料庫資訊。

完成上面的步驟後請執行resourcesdbscript指令碼,這將建立該示例應用所需的表。

以AspectJ/Java應用的方式執行檔案來建立預設資料並測試上面的抓取策略實現。

TAGS:J2EE AOP 策略