擷取失敗及系統狀態 (第一部分)

工程 | Alef Arendsen | 2008 年 1 月 7 日 | ...

在 Spring Experience 大會上,我主持了一個關於各個方面的議程。其中一個是Hibernate 同步面向,這是我上週描述過的。另一個是能夠擷取首次失敗及系統狀態的面向,有時稱為首次失敗資料擷取 (First-Failure Data Capture, FFDC)。我主持這次議程是為了展示一些非常有用的面向,但人們可能尚未在實務中接觸過。我經常聽到人們詢問除了日誌記錄、追蹤、交易管理和安全性之外的其他面向。我認為 Hibernate 同步面向和 FFDC 面向是不錯的例子。

簡介

FFDC 的目標是在發生錯誤時,擷取盡可能多的關於系統目前狀態的資訊。以下條目說明了這個面向如何運作,以及您如何在自己的應用程式中使用它。

讓我們設定以下兩個目標

  • 當失敗從業務服務中逸出時,我們希望記錄呼叫上下文,意即在這個業務服務執行上下文中發生的所有呼叫
  • 當失敗從業務服務中逸出時,我們希望記錄失敗的根本原因,意即不僅是最上層的例外 (從方法中逸出的例外),還有可能被包裝、吞噬或重新拋出的第一個例外

為了做到這一點,首先讓我們設計一個類別,能夠為我們追蹤想要記錄的資料。我們將這個類別稱為 CallContext。我省略了實際的實作。我將在我的下一篇文章中貼出程式碼,實作在這裡並不重要,而且除此之外,它無論如何都是一個相當直接的資料持有者。


public class CallContext {

	/**
	 * Registers the root call of this call context.
	 * We want to distinguish between the root call
	 * and all subsequent calls issued in the context
	 * of the root call.
	 */
	public void setRootCall(JoinPoint rootCall) { ... }
	
	/**
	 * Registers a call at a certain depth.
	 * @param the call to register
	 * @param the depth of the call
	 */ 
	public void registerCall(JoinPoint call, int depth) { ... }
	
	/**
	 * Registers the first failure of this call context.
	 * A first failure might already have occurred in which
	 * case subsequent registrations of the same or different
	 * failures will be ignored.
	 */
	public void setFirstFailure(Throwable t) { ... }
	
	/**
	 * Log the entire call context (i.e. output it to
	 * System.out).
	 */
	public void log(Throwable t) { ... }
}

如您所見,我們正在使用 AspectJ 的 JoinPoint 類型來識別程式中發生的事件。

定義四種情境

所以,我們已經準備好資料了。接下來,讓我們稍微改寫我們先前設定的兩個目標,並建立一個我們希望在程式中發生的事情的清單
  • 在呼叫業務服務之前,我們希望向目前的呼叫上下文註冊根呼叫
  • 在業務服務上下文中的呼叫之前,我們希望向目前的呼叫上下文註冊(以及目前的深度)呼叫
  • 當業務服務內部發生例外時,將其註冊為目前呼叫上下文中的第一個失敗
  • 在例外從業務服務中逸出之後,我們希望記錄目前的呼叫上下文

如您所見,我只是稍微切割和分析事情,使其以「在某事發生之前/之後,做某事」的形式開始出現。剩下要做的唯一事情是識別兩個某事,我們就完成了。讓我們分別處理三個不同的邏輯部分。

在業務服務之前,向目前的上下文註冊根呼叫

使用 AspectJ,這相對簡單。假設業務服務可以通過 @BusinessService 註解來識別,該註解可以添加到方法或類別。如果添加到類別,則該類別上的所有方法都是業務服務。如果添加到方法,則只有該方法是業務服務。換句話說:業務服務是定義在一個類別中的方法,而該類別本身又被 @BusinessService 註解,或一個本身被 @BusinessService 註解的方法。在 AspectJ 中,這歸結為以下內容(有關 AspectJ 切入點表達式語言的確切語法的更多資訊,請參閱 http://www.eclipse.org/aspectj//doc/released/progguide/semantics-pointcuts.html)。

pointcut businessService() : call(* (@BusinessService *..*).*(..)) || call(@BusinessService * *(..));

現在我們已經識別出業務服務,我們可以完成第一個情境


public aspect FirstFailureDataCapturer {

	public CallContext callContext = new CallContext();
	
	pointcut businessService() : call(@BusinessService *..*).*(..)) || 
			call(@BusinessService * *(..));
	
	before() : businessService() {
		// 'thisJoinPoint' is an implicit variable (just like 'this')
		// that represents the current join point
		this.callContext.setRootCall(thisJoinPoint);
	}
}

在業務服務上下文中的呼叫之前,將其註冊到目前的呼叫上下文中

我們已經完成了第一個情境,讓我們處理第二個情境。我們已經識別出成為業務服務的意義。我們想要捕獲業務服務上下文中的所有呼叫。任意呼叫可以識別如下

pointcut methodCall() : call(* *(..));

如果我們使用這個切入點,我們將使情境適用於所有方法,但我們只想將其應用於業務服務內的方法。因此,我們需要限制這個切入點的範圍。我們可以通過使用 cflow 切入點設計符來做到這一點。cflow 切入點設計符 採用另一個切入點,並將其限制為在該切入點上下文中發生的事情。讓我們看看如何使用它來解決手頭的問題。將以下內容理解為:「業務服務中的方法呼叫是一個方法呼叫(參見上面定義的切入點),同時(和)在業務服務的控制流程中(參見先前定義的其他切入點)。


pointcut methodCallInBusinessService() : methodCall() && cflow(businessService());

讓我們更進一步,假設我們不想註冊所有方法呼叫,而只註冊有限的集合。以下定義了一個可追蹤的方法,僅識別我認為相關的方法。它還排除了面向本身中定義的方法(或面向控制流程中的方法)。後者防止了無限循環的發生。讓我們也大聲朗讀一下:可追蹤的方法是業務服務中的方法呼叫(參見上面定義的切入點),同時不(和不)在 FirstFailureDataCapturer 中定義的執行建議的控制流程中,並且它也不應該是對 equals()、hashCode() 或 getClass() 的呼叫。


pointcut traceableMethod() : 
	methodCallInBusinessService() &&
	!cflow(within(FirstFailureDataCapturer) && adviceexecution()) &&
	!call(* equals(Object)) && !call(* hashCode()) && !call(* getClass());

讓我們使用這個切入點來實作我們已經識別出的第二個情境。在上面情境的描述中,我們沒有指定我們也需要追蹤目前的深度。我們正在使用前置建議來記錄目前的呼叫。讓我們也使用相同的建議來追蹤深度,並使用後置建議將深度重置為之前的狀態。


public aspect FirstFailureDataCapturer {

	public CallContext callContext = new CallContext();
	
	public int currentDepth = 0;
	
	// other pointcuts and advices omitted

	pointcut methodCallInBusinessService() : methodCall() && cflow(businessService());
	
	pointcut traceableMethod() : 
		methodCallInBusinessService() &&
		!cflow(within(FirstFailureDataCapturer) && adviceexecution())) &&
		!call(* equals(Object)) && !call(* hashCode()) && !call(* getClass());
		
	before() : traceableMethod() {
		currentDepth++;
		callContext.registerCall(thisJoinPoint, currentDepth);
	}
	
	after() : traceableMethod() {
		currentDepth--;
	}
}

當業務服務內部發生例外時,將其註冊為目前的呼叫上下文

既然我們已經完成了第二個情境,我們幾乎捕獲了所有我們想要捕獲的狀態。我們想要捕獲的最後一件事是在同一個業務服務上下文中發生的第一個例外。

潛在的失敗點是 a) 從方法中逸出的例外,或 b) 方法內部的例外處理常式(然後被包裝、吞噬、可能重新拋出等等)。讓我們使用這個定義來實作我們的第三個情境。第一個切入點只是使用可追蹤的方法切入點來識別潛在的失敗點。我們稍後將使用後置拋出建議來完成我們情境的一部分。第二個更有趣一點。它定義了一個切入點,用於識別業務服務控制流程中的例外處理常式(catch 區塊)。使用這個切入點,我們可以識別被捕獲、包裝和重新拋出的例外(例如,或被捕獲和吞噬的例外)。


pointcut potentialFailurePoint() : traceableMethod();
	
pointcut exceptionHandler(Throwable t) : handler(*) && args(t) && cflow(businessService());

我們將使用前置和後置建議來完成第三個情境。首先:在例外處理常式之前,記錄例外


public aspect FirstFailureDataCapturer {

	private CallContext context = new CallContext();

	// other members omitted

	before(Throwable t) : exceptionHandler(t) {
		this.callContext.setFirstFailure(t);
	}
}

現在,讓我們定義另一個建議


public aspect FirstFailureDataCapturer {

	private CallContext context = new CallContext();

	// other members omitted

	after() throwing(Throwable t) : potentialFailurePoint() {
		this.callContext.setFirstFailure(t);
	}
}

在例外從業務服務中逸出之後,記錄目前的呼叫上下文

我們需要做的最後一件事是在業務服務執行導致例外的情況下記錄目前的呼叫上下文。我們已經擁有所有必要的成分(切入點)可以直接跳到建議,所以讓我們繼續

public aspect FirstFailureDataCapturer {

	private CallContext context = new CallContext();

	// other members omitted

	after() throwing(Throwable t) : businessService() {
		this.callContext.log(t);
	}
}

使用 CarPlant 作為範例

在 Spring Experience 大會的議程中,我使用了我的(聲名狼藉的)CarPlant 範例來展示 FirstFailureDataCapturer。CarPlant 是一個相對較小的系統,能夠製造汽車。製造汽車是一個兩步驟的過程:1) 從 CarPartsInventory 系統取得零件,以及 2) 要求 CarAssemblyLine 將零件組裝成汽車。CarPlant 本身

@BusinessService public Car manufactureCar(CarModel model) {
	Set <Part> parts = inventory.getPartsForModel(model);
	
	return assemblyLine.assembleCarFromParts(model, parts);
}

在這個範例中,CarPartsInventory 是一個存根,實際上沒有做任何有用的事情


public Set<Part> getPartsForModel(CarModel model) {
	return new HashSet<Part>();
}

這裡有趣的部分是 CarAssemblyLine。正如您在下面的程式碼中看到的,CarAssemblyLine 中有一些奇怪的程式碼。它首先拋出一個例外,自己捕獲它,然後將其重新拋出為一個相當無意義的例外 MeaninglessException。


public Car assembleCarFromParts(CarModel model, Set<Part> parts) {
		
	try {
		throw new OnStrikeException("The workers are on strike!");
	} catch (OnStrikeException e) {
		throw new MeaninglessException();
	}
}

顯然,在正常情況下,問題的真正原因,根本原因永遠不會在這種情況下被識別出來(它被捕獲,沒有記錄... 拋出了一個不同的例外,並且根本原因沒有被傳遞下去),而且我們也永遠無法在真正和第一次失敗(OnStrikeException)發生的確切時間點註冊系統狀態。幸運的是,現在我們有了 FirstFailureDataCapturer,我們可以註冊根本原因並記錄它。下面您可以找到一個序列圖和我運行的一些測試的輸出。正如您所見,我們不僅獲得了呼叫堆疊,而且還獲得了 在這個業務服務執行上下文中發生的所有呼叫,或者換句話說:整個呼叫樹。

ffdc.png

擷取系統狀態

如果您仔細觀察,您可以看到第一個被轉儲的例外是 MeaninglessException。然而,在 MeaninglessException 被轉儲之後,立即有一條訊息說存在一個與 MeaninglessException 不同的根本原因,然後轉儲了真正的例外。堆疊追蹤也提到真正的例外是由第 18 行引起的,而 MeaninglessException 起源於第 20 行。

現在我們已經識別出真正發生失敗的點,我們也可以開始擷取系統狀態。您可以想像,CarPlant:18 的系統狀態可能與 CarPlant:20 的系統狀態不同,而我們的 FirstFailureDataCapturer 允許我們在正確的時間點註冊系統狀態。

那麼系統狀態到底是什麼呢?嗯,這完全取決於執行時期、您的特定應用程式以及您感興趣的內容。以下是一些範例

  • 目前登入的使用者
  • 目前的汽車製造請求
  • 任何技術系統狀態(執行緒數量、快取統計資訊等等)
  • 發生此例外的節點

擷取系統狀態現在非常容易,我們可以在 CallContext.setFirstFailure() 方法內部執行此操作,例如。

第二部分呢??

這個面向還沒完成!第一次完整的面向出現在圖片中時,它的程式碼如下

public aspect FirstFailureDataCapturer {

	public CallContext callContext = new CallContext();
	
	pointcut businessService() : call(@BusinessService * *(..)) || call(* (@BusinessService *..*).*(..)) || call(@BusinessService * *(..));
	
	before() : businessService() {
		// 'thisJoinPoint' is an implicit variable (just like 'this')
		// that represents the current join point
		this.callContext.setRootCall(thisJoinPoint);
	}
}

如您所見,呼叫上下文是在 FirstFailureDataCapturer 實例化時實例化的。現在的問題當然是:FirstFailureDataCapturer 將在何時以及多少次被實例化?而且,當您回答了這個問題後,另一個問題可能會浮現在腦海中:如果這個面向在多執行緒環境中使用會發生什麼情況?在下一部分中,我將討論所有這些內容,並對面向進行一些其他更改以使其更加完善。同時,您始終可以嘗試在評論中回答這些問題 :)!我還將在下一部分中提供面向的原始碼。

取得 Spring 電子報

隨時掌握 Spring 電子報的最新資訊

訂閱

領先一步

VMware 提供培訓和認證,以加速您的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔案。

了解更多

即將到來的活動

查看 Spring 社群中所有即將到來的活動。

查看全部