藉助 AOP 重構 DB2 數據庫訪問程序

傳統的 DB2 數據庫訪問程序
讓我們先來溫習幾段代碼,它們將從一個數據庫連接池中得到可用的數據庫連接,然後對數據庫進行一些操作,操作完畢後再將這個連接返還給連接池。在此基礎上,我們爲這些數據庫操作增加了對事務的支持,保證它們對數據庫的更改要麼同時成功,要麼同時恢復如初。

相信每位編寫過 DB2 數據庫訪問模塊的開發人員都設計過類似的代碼,我們長期以來一直都這麼做,可這是最好的麼?我們還能做得更好麼?一開始,先讓我們來分析一下這些代碼片斷的特點吧。

代碼片斷 1:DB2 數據庫連接池的接口

				
        
public interface DB2ConnectionPool {
		
	public Connection getConnection(String url, String userName, String password) throws SQLException;
		
	public boolean putConnection(Connection connection); 
	
	public void registerConnection(String url, String userName, String password);

}

      

真實環境中的 DB2 數據庫連接池會有着更爲複雜的接口和實現,這裏只是給出了最基本的操作:獲取一個數據庫連接,註冊一個數據庫連接以及釋放一個數據庫連接; 在很多工業項目中,常常會使用應用服務器產品諸如 WebSphere Application Server 來管理數據庫連接池。

代碼片斷 2:DB2 數據庫訪問程序的典型片斷

				
        
public void db2Manipulation() throws SQLException{
		
		SimpleDB2ConnectionPool _connPool =
			SimpleDB2ConnectionPool.getInstance();  // 獲取 DB2 數據庫連接池實例

		Connection connection = _connPool.getConnection("db2:jdbc:SAMPLE", "user", "password");
		if(null == connection)
		{
			connection = DriverManager.getConnection("db2:jdbc:SAMPLE", "user", "password");
		}   // 獲取 DB2 數據庫連接
		
		...
		// 進行和業務邏輯相關的數據庫操作
		...
		
		if(false == _connPool.putConnection(connection))  // 釋放 DB2 數據庫連接
		{
			connection.close();
		}
	}

      

代碼片斷中的 SimpleDB2ConnectionPool 是 DB2ConnectionPool 的一個簡單實現。這是個典型的 DB2 數據庫訪問程序,先是獲取數據庫連接,然後進行和業務邏輯相關的數據庫操作,最後釋放數據庫連接。在這個代碼片斷中,列出的代碼中相當部分其實都和應用中實際的業務邏輯沒有多大關係,但它們卻佔用了代碼的大段篇幅。

代碼片斷 3:增加事務處理支持的 DB2 數據庫訪問程序片斷

				
        
public void db2Manipulation() throws SQLException{
		// Get reference of Connection Pool
		SimpleDB2ConnectionPool _connPool =
			SimpleDB2ConnectionPool.getInstance();   // 獲取 DB2 數據庫連接池實例

		Connection connection = _connPool.getConnection("db2:jdbc:SAMPLE", "user", "password");
		if(null == connection)
		{
			connection = DriverManager.getConnection("db2:jdbc:SAMPLE", "user", "password");
		}          // 獲取 DB2 數據庫連接
		
		Statement stmt = null;
		try
		{
			connection.setAutoCommit(false);  // 設置數據庫連接不會自動提交事務
			
			stmt = connection.createStatement();  // 在數據庫連接基礎上創建語句對象
			
			stmt.executeUpdate("insertsql");  // 進行數據庫插入記錄操作
			
			stmt.executeUpdate("updatesql");  // 進行數據庫更新記錄操作
			
			stmt.executeUpdate("deletesql");  // 進行數據庫刪除記錄操作
			
			connection.commit();     // 提交數據庫事務
			
		} catch (Exception ex)
		{
			connection.rollback();  // 回滾數據庫事務
			// log                  // 記錄日誌
		} finally
		{
			stmt.close();           // 釋放語句對象
			// log                  // 記錄日誌
		}
		
		if(false == _connPool.putConnection(connection))    // 釋放DB2數據庫連接
		{
			connection.close();
		}
	}

      

代碼片斷 3 在代碼片斷 2 的基礎上增加了事務處理的支持。 默認情況下,數據庫連接會自動提交事務,所以需要首先將其設置爲手工提交事務;然後,在該事務管理範圍之內,進行各種數據庫更新操作,操作成功則提交事務,操作失敗則回滾事務,最後釋放數據庫連接。這裏判斷數據庫操作是否成功的原則是根據操作過程中是否發生異常。在代碼片斷 3 中,同樣會發現大段與應用邏輯沒有關係的代碼佔用了大量的開發時間和代碼量。

從上面這幾個代碼片斷,我們可以看出在 DB2 數據庫訪問程序中,常常會有這樣幾類與具體的業務邏輯關係不大,但卻是不能缺少的代碼:

  • 數據庫連接的獲取和釋放:沒有它們,我們就無法對數據庫做任何事情。
  • 增加數據庫事務的處理支持:少了事務的支持,會讓我們在修改數據庫的時候膽戰心驚。
  • 捕捉並且處理數據庫處理異常:這些是必需要有的,不然我們的程序很容易崩潰。
  • 記錄日誌:當程序運行時出了問題的時候,日誌其實是最好的診斷材料。

看來,是到了考慮重構這些代碼的時候了。

AOP 能夠給我們帶來什麼
面向過程編程離我們已經有些遙遠,面向對象編程正主宰着軟件世界。當每個新的軟件設計師都被要求掌握如何將需求功能轉化成一個個類,並且定義它們的數據成員、行爲,以及它們之間複雜的關係的時候,面向方面編程(Aspect-Oriented Programming,AOP)爲我們帶來了新的想法、新的思想、新的模式。

如果說面向對象編程是關注將需求功能劃分爲不同的並且相對獨立,封裝良好的類,並讓它們有着屬於自己的行爲,依靠繼承和多態等來定義彼此的關係的話;那麼面向方面編程則是希望能夠將通用需求功能從不相關的類當中分離出來,能夠使得很多類共享一個行爲,一旦發生變化,不必修改很多類,而只需要修改這個行爲即可。

面向方面編程是一個令人興奮不已的新模式。就開發軟件系統而言,它的影響力必將會和有着十數年應用歷史的面向對象編程一樣巨大。面向方面編程和麪向對象編程不但不是互相競爭的技術而且彼此還是很好的互補。面向對象編程主要用於爲同一對象層次的公用行爲建模。它的弱點是將公共行爲應用於多個無關對象模型之間。而這恰恰是面向方面編程適合的地方。有了 AOP,我們可以定義交叉的關係,並將這些關係應用於跨模塊的、彼此不同的對象模型。AOP 同時還可以讓我們層次化功能性而不是嵌入功能性,從而使得代碼有更好的可讀性和易於維護。它會和麪向對象編程合作得很好。

這是張非常經典的插圖,你幾乎可以在任何一本介紹 AOP 的書籍中找到它。它告訴我們,需求功能通過 AOP 的魔力三棱鏡的折射之後,就會變成彼此相對獨立的方面(Aspect),我們可以分別實現它們,然後再組合起來。透過這個三棱鏡,我們看到了從未看到的奇妙的景象。

AOP 的應用範圍
傳統的程序通常表現出一些不能自然地適合單一的程序模塊或者是幾個緊密相關的程序模塊的行爲,AOP 將這種行爲稱爲橫切,它們跨越了給定編程模型中的典型職責界限。橫切行爲的實現都是分散的,軟件設計師會發現這種行爲難以用正常的邏輯來思考、實現和更改。最常見的一些橫切行爲如下面這些:

  • 日誌記錄,跟蹤,優化和監控
  • 事務的處理
  • 持久化
  • 性能的優化
  • 資源池,如數據庫連接池的管理
  • 系統統一的認證、權限管理等
  • 應用系統的異常捕捉及處理
  • 針對具體行業應用的橫切行爲

目前,前面幾種橫切行爲都已經得到了密切的關注,也出現了各種有價值的應用,但也許今後幾年,AOP 對針對具體行業應用的貢獻會成爲令人關注的焦點。

AOP 的具體實現
AOP 是一個概念,一個規範,本身並沒有設定具體語言的實現,這實際上提供了非常廣闊的發展的空間。本文中採用的是一個很悠久的實現 ― AspectJ,它能夠和 Java 配合起來使用。

介紹 AspectJ 的使用和編碼不是本文的目的,你可以在 Google 上找到很多有關它的材料。

這裏只是重溫 AspectJ 中幾個必須要了解的概念:

  • Aspect: Aspect 聲明類似於 Java 中的類聲明,在 Aspect 中會包含着一些 Pointcut 以及相應的 Advice。
  • Joint point:表示在程序中明確定義的點,典型的包括方法調用,對類成員的訪問以及異常處理程序塊的執行等等,它自身還可以嵌套其它 joint point。
  • Pointcut:表示一組 joint point,這些 joint point 或是通過邏輯關係組合起來,或是通過通配、正則表達式等方式集中起來,它定義了相應的 Advice 將要發生的地方。
  • Advice:Advice 定義了在 pointcut 裏面定義的程序點具體要做的操作,它通過 before、after 和 around 來區別是在每個 joint point 之前、之後還是代替執行的代碼。

如果你平時使用基於 Eclipse 的開發工具,可以在 http://eclipse.org/ajdt/ 下載到最新的支持Eclipse 的 AspectJ 插件,安裝起來並不困難,它同時還提供了不錯的幫助文檔和範例代碼,你可以從那裏開始 AspectJ 的學習。

下面就給出了一些 AspectJ 的片斷,來幫助重構我們的代碼。

代碼片斷 4:橫切 DB2 數據庫連接的獲取和釋放( 中有這個 Aspect 的全部代碼) 附件 1 中有這個 Aspect 的全部代碼)

				
        
	pointcut connectionCreation(
		String url,
		String username,
		String password) : call(
		Connection DriverManager.getConnection(String, String, String))
		&& args(url, username, password);

Connection around(String url, String userName, String password)
		throws SQLException : connectionCreation(url, userName, password) {
		Connection connection =
			_connPool.getConnection(url, userName, password);
		if (connection == null) {
			connection = proceed(url, userName, password);
			_connPool.registerConnection(connection, url, userName, password);
		}
		return connection;
	}

      

分析:connectionCreation 定義的是所有涉及到調用 DriverManager.getConnection(String, String, String) 的代碼,這裏還列出了它的參數列表;相應地,在它的 around advice 處理代碼中判斷數據庫連接是否爲空;如果爲空,將重新申請數據庫連接並且在連接池中註冊它。這些與業務邏輯無關但又是必須的代碼就會從原來的程序中分離出來。


	pointcut connectionRelease(Connection connection) : call(
		void Connection.close())
		&& target(connection);
	void around(Connection connection) : connectionRelease(connection) {

		if (!_connPool.putConnection(connection)) {
			proceed(connection);
		}
	}

分析:connectionRelease 定義了所有有關釋放數據庫連接的代碼;相應的around advice會將其釋放到數據庫連接池當中。這樣,從原來的代碼看來,不用考慮數據庫連接池的存在,而只需正常地關閉一個數據庫連接。

代碼片斷 5:橫切 DB2 數據庫事務支持代碼以及異常捕捉( 中有這個 Aspect 的全部代碼) 附件 2 中有這個 Aspect 的全部代碼)

				
        
	pointcut getConnection() : call(
		Connection *.getConnection(String url, String user, String password));
Connection around()
		throws SQLException : getConnection() && cflow(transactedOperation()) {
		if (null == _connection) {
			_connection = proceed();
			_connection.setAutoCommit(false);
		}
		return _connection;
	}

      

分析:getConnection() && cflow(transactedOperation()) 對應的 around advice 則是做了獲取數據庫連接的操作,另外顯式地設置了 setAutoCommit 屬性爲 false,使得數據庫事務的提交能夠由代碼來控制。


pointcut transactedOperation() : execution(needTransactionFunction.execute (String sqlstr));
	pointcut topLevelTransactedOperation() : transactedOperation()
		&& !cflowbelow(transactedOperation());
	Object around() : topLevelTransactedOperation() {
		Object operationResult;
		try {
			operationResult = proceed();
			if (null != _connection) {
				_connection.commit();
			}
		} catch (Exception ex) {
			if (null != _connection) {
				_connection.rollback();
			}
		} finally {
			if (null != _connection) {
				_connection.close();
			}
		}
		return operationResult;
	}

分析:topLevelTransactedOperation 定義了所有最頂層的涉及到需要數據庫事務支持的函數調用;在其相應的 around advice 中,會先執行原來的函數並且得到它的返回值。如果返回值非空,則提交數據庫事務;如果返回值爲空,則回滾數據庫事務;在最後釋放該數據庫連接。而在覈心的代碼中,我們不用增加有關數據庫事務支持的任何代碼,只需要更改這裏 Aspect 的定義即可。


private static aspect SoftenSQLException {
		declare soft : java.sql.SQLException : (
			call(void Connection.rollback()) || call(void Connection.close()))
			&& within(DB2TransactionAspect);
	}

分析:SoftenSQLException 定義了調用 Connection.rollback() 和 Connection.close() 以及 DB2TransactionAspect 中的所有 Point Cut 範圍之內所發生的 SQLException 異常,這裏只是將其聲明爲 soft,並沒有提供異常發生後的處理。這爲統一處理 SQLException 提供了一種方法。


	pointcut illegalConnectionManagement() : (
		call(void Connection.close())
			|| call(void Connection.commit())
			|| call(void Connection.rollback())
			|| call(void Connection.setAutoCommit(boolean)))
		&& !within(DB2TransactionAspect);
	void around() : illegalConnectionManagement() {
	}

分析:illegalConnectionManagement 定義了一個有趣的規則,它限制在 DB2TransactionAspect 所規定的 Point Cut 範圍之外不允許調用有關數據庫連接的操作。這個規則非常有用,會在我們不小心寫錯代碼的時候提醒我們!

代碼片斷 6:橫切程序運行日誌的記錄( 中有這個 Aspect 的全部代碼) 附件 3 中有這個 Aspect 的全部代碼)

				
        
	public pointcut connectionActivities(Connection conn) : (
		call(* Connection.commit(..)) || call(* Connection.rollback(..)))
		&& target(conn);
		
    before(Connection conn) : connectionActivities(conn) {
		Signature sig = thisJoinPointStaticPart.getSignature();
		System.out.println("[" + sig.getName() + "] " + conn);
	}

      

分析:connectionActivities 會在每一個涉及到事務提交和回滾的調用時將有關當前的數據庫連接的相應信息記錄下來。


	public pointcut updateActivities(Statement stmt) : call(
		* Statement.executeUpdate(..))
		&& target(stmt);
		
    before(Statement stmt) throws SQLException : updateActivities(stmt) {
		Signature sig = thisJoinPointStaticPart.getSignature();
		System.out.println("[" + sig.getName() + "] " + stmt.getConnection());
	}

分析:updateActivities 會在每一個涉及到數據庫更新的操作調用時將有關當前語句對象的信息記錄下來。

程序運行日誌的記錄是一個健壯的應用程序的重要模塊,這裏將程序中重要的方法分爲兩類,一類是和數據庫連接相關的操作,一類是與數據庫更新操作有關的操作。在系統運行時,可以通過記錄的日誌跟蹤系統內部運行的情況。

拋磚引玉
上面的 AspectJ 的代碼片斷,只是拋磚引玉,給讀者一些啓發,下面要討論的這些問題,也許正是接觸了 AOP 之後所困惑的。

AOP 幫助我們解決了新的問題了麼
AOP 並沒有幫助我們解決任何新的問題,它只是提供了一種更好的辦法,能夠用更少的工作量來解決現有的一些問題,並且使得系統更加健壯,可維護性更好。同時,它讓我們在進行系統架構和模塊設計的時候多了新的選擇和新的思路

AOP 和 OOP 到底是什麼關係
很多人在初次接觸 AOP 的時候可能會說,AOP 能做到的,一個定義良好的 OOP 的接口也一樣能夠做到,我想這個觀點是值得商榷的。AOP和定義良好的 OOP 的接口可以說都是用來解決並且實現需求中的橫切問題的方法。但是對於 OOP 中的接口來說,它仍然需要我們在相應的模塊中去調用該接口中相關的方法,這是 OOP 所無法避免的,並且一旦接口不得不進行修改的時候,所有事情會變得一團糟;AOP 則不會這樣,你只需要修改相應的 Aspect,再重新編織(weave)即可。 當然,AOP 也絕對不會代替 OOP。核心的需求仍然會由 OOP 來加以實現,而 AOP 將會和 OOP 整合起來,以此之長,補彼之短。

DB2 數據庫訪問程序還能做哪些進一步的改進
本文中所給出的範例並不一定是最優的解決方案,它只是用來啓發大家能夠有意識地發掘出自己應用中的橫切需求,並運用 AOP 加以實現,在實踐中不斷體會 AOP 給我們帶來的好處。在範例代碼的基礎上,我們還可以進行很多改進,比如讓我們支持的事務適合分佈式的應用,優化對 SQLException 異常的處理等等。

AOP 適合工業化的應用麼
這個問題很難回答,其實最好的答案就是嘗試,用成功的項目或是產品來回答。Jboss 4.0 就是完全採用 AOP 的思想來設計的 EJB 容器,它已經通過了 J2EE 的認證,並且在工業化應用中證明是一個優秀的產品。相信在不遠的將來,會出現更多采用 AOP 思想設計的產品和行業應用。

小結
AOP 正向我們走來,我們需要關注的是怎麼樣使得它能夠爲我們的軟件系統的設計和實現帶來幫助。本文選取了 DB2 數據庫訪問程序這樣一個角度,拋磚引玉,旨在給大家一點啓發,能夠在更多的領域更深入的應用 AOP 的思想。

  1. http://eclipse.org/aspectj/ 這裏是 AspectJ 的大本營,你可以從中得到 AspectJ 及其相關工具,還有很多有關 AOP 的有益的討論。
  2. http://eclipse.org/ajdt/ Eclipse 已經有了 AspectJ 的插件,你可以將它和 WSAD 集成起來使用。
  3. http://aosd.net/index.php 這裏你可以找到有關 AOP 更爲豐富的資源。
  4. http://www.ibm.com/developerworks/cn/java/j-aspectj/ 這是一篇不錯的介紹 AspectJ 的入門文章,你可以從中瞭解到有關 AOP 的基本概念。
  5. http://www.ibm.com/developerworks/cn/rational/r-interview/ 聆聽 Grady Booch 教誨,大師的思想,他本人非常看好 AOP 的前景。
  6. http://www.microsoft.com/china/MSDN/library/windev/COMponentdev/AspectOrientedProgrammingEnablesBetterCodeEncapsulationandReuse.mspx 從本文我們可以瞭解在 .NET 平臺下爲 AOP 提供了哪些寶貴的支持。
  7. 《AspectJ in Action》 這是本非常不錯的書,儘管篇幅有些長,是它把我領進 AOP 的大門的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章