Byteman 使用指南(二)

FunTester發表於2025-01-21

Byteman 擴充套件

另一個值得注意的特性是,Byteman 規則的內建操作集並非固定不變。規則引擎透過將規則中使用到的內建操作對映到與之關聯的幫助類的公共例項方法來實現這一功能。預設情況下,幫助類為 org.jboss.byteman.rule.helper.Helper,它提供了一系列標準的內建操作,旨在簡化多執行緒應用程式中的執行緒管理。例如,在前文提到的示例規則中,內建操作 createCountDown()countDown() 實際上就是 Helper 類的公共方法。透過為規則指定一個替代的幫助類,可以靈活地擴充套件或修改規則中可用的內建操作集。

任何非抽象且非最終的類都可以被指定為幫助類。該類的公共例項方法將自動成為規則中事件、條件和動作部分的內建操作。例如,透過指定一個擴充套件了預設 Helper 類的自定義幫助類,規則既可以繼續使用現有的內建操作,也可以引入特定於規則或應用程式的自定義操作。因此,儘管 Byteman 的預設規則語言主要面向多執行緒測試中獨立執行緒的行為編排,但其靈活的架構使其能夠輕鬆適應更廣泛的應用程式需求。

代理轉換

Byteman 的位元組碼修改功能是透過 Java 代理程式 實現的。JVM 的類載入機制為代理提供了在位元組碼編譯之前修改載入的類的能力(有關 Java 代理的工作原理,可參考 java.lang.Instrumentation 包)。Byteman 代理在 JVM 啟動時讀取規則指令碼,隨後監控方法程式碼的載入過程,尋找與規則事件中指定的位置相匹配的 觸發點

代理會在與規則事件匹配的程式碼點插入 觸發呼叫。觸發呼叫是對規則執行引擎的呼叫,它會識別以下內容:

  • 觸發方法:即包含觸發點的方法。
  • 匹配的規則:與觸發點匹配的規則。
  • 觸發方法的引數:傳遞給觸發方法的引數。

如果多個規則匹配同一個觸發點,則會生成一系列觸發呼叫,每個匹配的規則對應一個觸發呼叫。通常情況下,規則會按照它們在指令碼中出現的順序依次觸發。唯一的例外是那些指定了 AFTER 位置的規則(例如 AFTER READ myFieldAFTER INVOKE someMethod),它們會按照相反的順序執行。

當觸發呼叫發生時,規則執行引擎會找到相關規則並執行它。引擎會為規則事件中提到的變數建立繫結,然後評估規則條件。如果條件評估為 true,則會觸發規則,並按順序執行每個規則動作。

觸發呼叫會將方法的接收者(this)和引數傳遞給規則引擎。這些值可以在條件和動作中透過標準命名約定(如 $0$1 等)引用。事件規範還可以為額外的變數引入繫結。這些變數的繫結可以透過字面資料、呼叫方法或操作引數和/或靜態資料來初始化。在事件中繫結的變數可以透過名稱直接在條件或動作中引用。繫結機制允許在觸發上下文中使用任意資料進行條件測試,以決定是否觸發規則,並作為規則動作的目標或引數。需要注意的是,當觸發程式碼使用相關除錯選項編譯時,代理能夠將觸發點範圍內的區域性變數作為引數傳遞給觸發呼叫,使它們作為預設繫結可用。規則可以透過在變數名前加上 $ 字元來引用這些區域性變數(例如 $this$arg1$i 等)。

代理還會在觸發呼叫周圍編譯異常處理程式碼,以處理規則執行過程中可能丟擲的異常。這裡的異常處理並不是為了捕獲規則執行引擎內部的錯誤(這些錯誤應被引擎內部捕獲並處理),而是為了改變觸發方法的控制流。通常情況下,觸發執行緒在觸發呼叫返回後會繼續執行原始方法程式碼。然而,規則可以使用 returnthrow 等內建動作來指定從觸發方法中提前返回或丟擲異常。規則語言透過在觸發呼叫下方丟擲其私有的內部異常來實現這一點。編譯到觸發方法中的異常處理程式碼會捕獲這些內部異常,然後返回給呼叫者或遞迴丟擲執行時異常或應用程式特定的異常。這樣可以避免觸發方法主體中剩餘程式碼的正常執行。如果觸發點還有其他待處理的觸發呼叫,這些呼叫也會被跳過。

如果當前載入的類與上傳的規則匹配,代理會重新轉換這些類,修改相關目標方法以包含必要的觸發呼叫。如果上傳的規則替換了現有規則,則在刪除舊規則時,與之相關的所有觸發呼叫也會從受影響的目標方法中移除。需要注意的是,重新轉換類並不會將新的類物件與現有類的例項關聯起來,它只是為這些類的方法安裝了不同的實現。

在代理引導期間,重新轉換可能會自動發生,而無需顯式上傳規則。這一點非常重要,因為 JVM 需要先載入其自身的引導類,然後才能啟動代理並允許其註冊轉換器。一旦代理處理了初始規則集並註冊了轉換器,它會掃描所有當前載入的類,並識別那些與規則集中的規則匹配的類。代理會自動重新轉換這些類,從而使得後續對引導程式碼的呼叫能夠觸發規則處理。

Agent Retransformation

Byteman 代理還允許在應用程式執行時動態上傳規則。這一功能可用於重新定義已載入的規則或動態引入新規則。如果當前載入的類與上傳的規則不匹配,代理僅會將新規則新增到當前規則集中。如果新規則與現有規則同名,則會替換舊規則。當後續載入與規則匹配的類時,代理會使用最新版本的規則對其進行轉換。

如果已載入的類與上傳的規則匹配,代理會重新轉換這些類,修改相關目標方法以包含必要的觸發呼叫。如果上傳的規則替換了現有規則,則在刪除舊規則時,與之關聯的所有觸發呼叫也會從受影響的目標方法中移除。需要注意的是,重新轉換類並不會將新的類物件與現有例項關聯,它只是為這些類的方法安裝了不同的實現。

在代理引導期間,重新轉換可能會自動發生,而無需顯式上傳規則。這一點非常重要,因為 JVM 需要先載入其自身的引導類,然後才能啟動代理並允許其註冊轉換器。一旦代理處理了初始規則集並註冊了轉換器,它會掃描所有當前載入的類,並識別那些與規則集中的規則匹配的類。代理會自動重新轉換這些類,從而使得後續對引導程式碼的呼叫能夠觸發規則處理。

ECA 規則引擎

Byteman 規則執行引擎由規則解析器、型別檢查器和直譯器/編譯器組成。在代理引導期間,解析器會被呼叫,以提供足夠的資訊供代理識別潛在的觸發點。

規則的型別檢查和編譯不會在觸發注入時立即進行,而是延遲到它們引用的類和方法位元組碼被載入時才會執行。型別檢查需要識別觸發類的屬性,有時還需要透過反射識別相關類的資訊。為了確保在型別檢查器訪問這些類之前,觸發類及其所有依賴類已被載入,規則會在首次觸發時進行型別檢查和編譯。這種延遲處理機制還避免了檢查和編譯那些實際未被呼叫的規則所帶來的額外開銷。

如果型別檢查或編譯操作失敗,規則引擎會列印錯誤資訊並停用相關觸發呼叫的執行。需要注意的是,在事件規範不明確的情況下,規則可能對某些觸發點成功透過型別檢查,但對其他觸發點則無法透過。只有在型別檢查失敗時,規則執行才會被停用。

解釋/編譯執行

在基本操作模式下,觸發呼叫透過解釋規則解析樹來執行規則。此外,規則還可以將其繫結、條件和動作翻譯成位元組碼,然後由 JIT 編譯器執行。儘管預設行為是使用直譯器,但可以透過在代理安裝時設定系統屬性來更改此預設值。

無論選擇哪種模式,規則的執行都由 Byteman 代理在執行時生成的輔助類(稱為 幫助介面卡)完成。這個類是與規則關聯的幫助類的子類(這也是為什麼使用者定義的幫助類不能是 final 的原因)。它繼承自幫助類,以便能夠執行幫助類定義的內建操作。使用子類的目的是為了新增規則系統所需的額外功能,其中最顯著的是 execute0 方法,該方法在觸發點被呼叫,以及一個區域性繫結欄位,用於儲存將方法引數和事件變數對映到其繫結值的雜湊表。

當規則被觸發時,規則引擎會建立規則的幫助介面卡類的例項,為觸發呼叫提供上下文(這也是為什麼使用者定義的幫助類不能是 abstract 的原因)。引擎使用 Byteman 代理生成的 setter 方法初始化規則和繫結欄位,然後呼叫介面卡例項的 execute 方法。由於每個規則觸發都由其自己的介面卡例項處理,這確保了來自不同執行緒的相同規則的併發觸發不會相互干擾(同時也確保遞迴觸發的相同規則保留它們自己的上下文)。

在解釋模式下,execute0 方法會定位觸發的規則,並從規則中獲取事件、條件和動作的解析樹。它遞迴遍歷這三個元件的解析樹,評估每個表示式。繫結在規則執行期間被查詢或分配,當它們在規則事件、條件或動作中被引用時。當 execute 方法遇到對內建操作的呼叫時,它會使用反射呼叫其幫助超類的繼承方法來執行該操作。

當啟用規則編譯時,Byteman 代理會生成一個包含從規則事件、條件和動作派生的內聯位元組碼的 execute 方法。這段程式碼直接編碼了規則中定義的所有操作和方法呼叫。它以與解釋程式碼相同的方式訪問繫結和執行內建操作,只不過對內建操作的呼叫被編譯為直接方法呼叫,而不是依賴於反射呼叫。

FunTester 原創精華

【連載】從 Java 開始效能測試

  • 混沌工程、故障測試、Web 前端
  • 服務端功能測試
  • 效能測試專題
  • Java、Groovy、Go
  • 白盒、工具、爬蟲、UI 自動化
  • 理論、感悟、影片
如果覺得我的文章對您有用,請隨意打賞。您的支援將鼓勵我繼續創作!
打賞支援
暫無回覆。