領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多在 The Spring Experience 大會上,我主持了一個關於各個方面的會議。其中之一是我上週描述的 Hibernate 同步方面。另一個是能夠捕獲首次故障和系統狀態的方面,有時稱為首次故障資料捕獲(FFDC)。我主持這個會議是為了展示一些非常有用的方面,但人們可能在實踐中還沒有遇到過。我經常聽到人們詢問日誌、跟蹤、事務管理和安全性之外的方面。Hibernate 同步方面和 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 型別來識別程式中發生的事件。
如您所見,我只是稍微對事物進行切分和整理,使得“在某事發生之前/之後,做某事”的句子開始出現。剩下的唯一事情就是識別這兩個某事,我們就完成了。讓我們分別處理這三段不同的邏輯。
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 中定義的正在執行的 advice 的控制流中,並且它也不應該是對 equals()、hashCode() 或 getClass() 的呼叫。
pointcut traceableMethod() :
methodCallInBusinessService() &&
!cflow(within(FirstFailureDataCapturer) && adviceexecution()) &&
!call(* equals(Object)) && !call(* hashCode()) && !call(* getClass());
讓我們使用這個切點來實現我們識別的第二個場景。在上面場景的描述中,我們沒有指定還需要跟蹤當前深度。我們使用一個 before advice 來記錄當前呼叫。讓我們也使用相同的 advice 來跟蹤深度,並使用一個 after advice 將深度重置為其先前的狀態。
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) 方法內部的異常處理程式(然後被包裝、吞噬、可能重新丟擲等等)。讓我們使用這個定義來實現我們的第三個場景。第一個切點只是使用可追蹤方法切點來識別潛在的故障點。我們將在稍後使用 after throwing advice 來完成我們場景的一部分。第二個更有趣。它定義了一個切點,用於識別業務服務控制流中的異常處理程式(catch 塊)。使用這個切點,我們可以識別例如被捕獲、包裝和重新丟擲的異常(或被捕獲和吞噬的異常)。
pointcut potentialFailurePoint() : traceableMethod();
pointcut exceptionHandler(Throwable t) : handler(*) && args(t) && cflow(businessService());
我們將使用 before 和 after advice 來完成第三個場景。首先:在異常處理程式之前,記錄異常。
public aspect FirstFailureDataCapturer {
private CallContext context = new CallContext();
// other members omitted
before(Throwable t) : exceptionHandler(t) {
this.callContext.setFirstFailure(t);
}
}
現在,讓我們定義另一個 advice。
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);
}
}
@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,我們可以記錄根本原因並將其記錄下來。下面您可以看到我執行的測試的序列圖和一些輸出。正如您所看到的,我們不僅獲得了呼叫棧,還獲得了在此業務服務執行上下文中發生的所有呼叫,換句話說:整個呼叫樹。
既然我們已經識別了故障發生的真正點,我們也可以開始捕獲系統狀態。正如您所想象的,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 將何時以及例項化多少次?而且,當您回答了這個問題後,另一個問題可能會浮現:如果在多執行緒環境中使用此方面會發生什麼?在下一部分中,我將討論所有這些,並對方面進行其他一些更改以使其更完善。在此期間,您當然可以隨時在評論中嘗試回答這些問題!我也會在下一部分提供此方面的原始碼。