重構小記

半年前入職新公司以來,一直參與一個維護型的大型JAVA項目。項目是一個交易系統,基於一個非常老的框架搭建的(大概98年的一個框架),框架本身的設計思想不錯,沒有應用任何開源框架,簡單明瞭。但是公司平時在設計和代碼質量把控方面做得不夠到位,導致現在代碼的質量可以說是高偶合、低類聚的典型,隨便改點什麼都無法簡單評估影響範圍,大部分需求變更都不得不把業務和開發人員集中起來,開個大半天的會議討論。經過無數次的吐槽與溝通,終於說服了領導,來次翻天覆地的重構。


寫這篇博客的目的並不是關於重構的,而是在重構完成後遇到的技術問題,在此記錄下來與大家分享。


首先,不得不先簡單介紹下項目的情況,項目是一個交易系統,技術上很簡單,就是一個servlet,對外提供接口。外部調用方封裝報文參數以http方式請求,系統內部解析報文,然後交由一個個的step依次處理,最後返回結果。就這麼簡單,大致流程如下圖所示:

                  

調用方的一次請求我們稱之爲一次交易,每個交易根據報文參數來決定使用哪些Step來完成不同的業務。Parser負責解析外部調用方傳入的參數;每個Step相當於一個獨立的、可複用的業務模塊。比如創建一個用戶,Step1負責創建用戶的基本信息,Step2負責創建用戶的安全信息,其它Step可以負責創建會員的資產信息等或者做一些權限驗證、日誌記錄等,這樣的設計初衷是爲了複用Step;Wrapper把內部處理封裝成結果返回給調用方。整個過程很容易用一段簡單代碼來表示:

public void process() {
	long start = System.currentTimeMillis();
	Connection conn = null;
	try {
		conn = ds.getConnection();
		
		processStep1(conn);
		processStep2(conn);
		...
		processStepN(conn);
		
		long end = System.currentTimeMillis();
		if (end - start > limit) {
			conn.rollback();
			return;
		}
		
		conn.commit();
		
	} catch (SQLException e) {
		if (null != conn) 
			conn.rollback();
	} finally {
		if (null != conn) conn.close();
	}
}

從代碼上看,框架確實很簡單,一開始從數據源拿到一個DB Connection,然後把Connection傳入一個個的Step(Step內部直接使用這個Connection對象操作數據庫)順序處理,最後判斷是否超時來決定是否提交事務。既然這麼簡單了,爲什麼還要重構呢?那是因爲業務都在這些Step裏面,有大量的Step,而且裏面的代碼...這裏省略1000字。代碼質量是重構一原因之一,最主要的是目前項目的代碼風格是完全面向過程式的,沒有一丁點的面向對象的感覺,維護的成本實在太高,所以重構是採用的是Domain-Driven Design這種純面向對象的設計方式進行的,關於DDD大家可以自行搜索一下相關的文章,網上不少。


因爲是一個大型項目,所以不可能一下子完全重構,而是一個模塊一個模塊、循序漸進地重構,還需要考慮新舊代碼的兼容性,所以第一輪重構後的代碼大致如下:

try {
	conn = ds.getConnection();
	
	processStep1(conn);
	processStep2(conn);
	...
	//重構後某個Step變成了
	Factory factory = ContextLoader
		.getCurrentWebApplicationContext().getBean("factory", Factory.class);
    DomainObject doObj = factory.create..();
    doObj.doSomething();
    
	processStepN(conn);
	
	long end = System.currentTimeMillis();
	if (end - start > limit) {
		conn.rollback();
		return;
	}
	
	conn.commit();
	
}

上面只是重構了某個Step,但完成整個交易的功能,必須兼容原來的代碼。從代碼可以看出,這裏我們使用了spring(持久層使用了spring jdbc),從Context中取得工廠,通過工廠創建一個領域對象doObj,之後就可以使用領域對象去實現具體的業務了。等全部的Step都重構完成後,就可以完全丟掉現有的框架設計了。


不知道大家有沒有看出問題,我們有個超時的判斷,超過響應時間是要回滾的。但是使用spring jdbc我們一般只是聲明一個DataSource,它自己通過data source去獲取connection,這樣spring jdbc使用的connection與我們代碼中使用的connection肯定不是同一個對象,這樣代碼中的connection就無法控制doObj對數據庫的提交與回滾了,起碼超時回滾這個業務就肯定會出問題了。


擺在面前的只有兩條路:

  1. 把某個交易中所涉及的全部Step都重構了,這個交易中完全去掉step,不再代碼中維護connection,而是把事務的控制交給spring。這個做法可行,而且也是重構的最終目標,但是時間有限,在短時間內重構完一個交易涉及的全部Step,工作量實在不小,而且測試風險也很大。
  2. 想辦法解決

各方面的壓力,使得我沒有選擇,只能想辦法解決。代碼中的connection相當於new出來的一個對象,而spring中的DataSource又是一個singleton對象,這還不是一個Ioc的問題,因爲spring jdbc需要的是一個DataSource,而我們只有Connection,所以我們必須實現一個新的DataSource,並且覆蓋它的getConnection方法使它返回我們的代碼中new出來的Connection對象。DataSource與我們的框架代碼之間好像沒有任何關係,怎麼能才讓它返回我們new出來的Connection呢?感謝我的同事老朱,討論中他提到了ThreadLocal類,是啊,一次交易過程就是一個線程的一次執行過程,我們可以把Connection保存在ThreadLocal對象中去。


關於ThreadLocal類,簡單說就是通過ThreadLocal,可以在同一程線內(不同的方法、代碼段等任何地方)共享信息。

首先,創建一個類,通過ThreadLocal用來存取Connection對象。

public class ConnThreadLocal {
	private static final ThreadLocal<Connection> local
		= new ThreadLocal<Connection>();
	
	public static void addConn(Connection conn) {
		local.set(conn);
	}
	
	public static Connection getConn() {
		return local.get();
	}
}

接着,修改框架代碼,在獲得Connection對象後,保存到ThreadLocal中去。

try {
	conn = ds.getConnection();
	ConnThreadLocal.addConn(conn); //把Connection對象保存到ThreadLocal中
	
	processStep1(conn);
	processStep2(conn);
	
	Factory factory = ContextLoader
		.getCurrentWebApplicationContext().getBean("factory", Factory.class);
    DomainObject doObj = factory.create..();
    doObj.doSomething();
    
	processStepN(conn);
	
	long end = System.currentTimeMillis();
	if (end - start > limit) {
		conn.rollback();
		return;
	}
	
	conn.commit();
}

最後,再實現一個新的DataSource,並且讓spring jdbc使用這個新的DataSource我們的問題就解決啦!!!

public class CustomDataSource extends AbstractDataSource {

	@Override
	public Connection getConnection() throws SQLException {
		return ConnThreadLocal.getConn();
	}

	@Override
	public Connection getConnection(String username, String password)
			throws SQLException {
		return this.getConnection();
	}

}

至此,一切就緒,上環境,測試。Duang.....出錯啦,Connection is closed....哭哭哭,這個錯誤出現在doObj.doSomething方法中第二次數據庫訪問,怎麼會這樣呢?網上查閱了一翻,原來spring jdbc在每次數據庫訪問之後,都會調用Connection的close方法把Connection還給連接池,之後可以再通過連接池獲取可用的連接。設計上非常合理,但是苦了我哎哭,只能想辦法讓CustomDataSource返回的鏈接不能被spring jdbc關閉,爲此,需要一個新的Connection類:

public class CustomConnection implements java.sql.Connection {
	
	private java.sql.Connection conn;
	
	public CustomConnection(java.sql.Connection conn) {
		this.conn = conn;
	}
	
	@Override
	public Statement createStatement() throws SQLException {
		return conn.createStatement();
	}
	
	@Override
	public void close() throws SQLException {
		//覆蓋close方法,不讓spring關閉
	}
	
}

CustomConnection類內部維護一個真正可用的Connection對象,除了close方法,所有其它方法都委託這個對象去做。再修改CustomDataSource,使之返回CustomConnection對象。

public class CustomDataSource extends AbstractDataSource {

	@Override
	public Connection getConnection() throws SQLException {
		return new CustomConnection(ConnThreadLocal.getConn());
	}

	@Override
	public Connection getConnection(String username, String password)
			throws SQLException {
		return this.getConnection();
	}

}

這樣一來,spring jdbc獲取的Connection對象就是我們框架代碼裏面的那個Connection對象了,並且spring jdbc關閉不了,至此,一切問題解決。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章