Byteman 使用指南(三)

FunTester發表於2025-01-24

規則在指令碼中定義,指令碼由一系列規則定義組成,並與註釋行交錯。註釋可以出現在規則定義的正文中,也可以在規則定義之前或之後,但必須與規則文字分開一行。註釋以 # 字元開頭:

######################################
# 示例規則集
#
# 單個規則定義
RULE example rule
# 規則正文中的註釋行,FunTester
. . .
ENDRULE

規則事件規範確定了與目標類相關聯的目標方法中的具體位置。目標方法可以是靜態方法、例項方法或建構函式。如果沒有指定詳細位置,預設位置是目標方法的入口。因此,單個規則的基本模式如下:

# 規則骨架
RULE <規則名稱>
CLASS <類名稱>
METHOD <方法名稱>
BIND <繫結>
IF  <條件>
DO  <動作>
ENDRULE

跟隨 RULE 關鍵字的規則名稱可以是任何自由形式的文字,限制是它必須至少包含一個非空白字元。規則名稱不需要是唯一的,但在除錯規則指令碼時,如果它們清楚地標識規則,則很有幫助。規則名稱在解析、型別檢查、編譯或執行過程中遇到錯誤時會列印出來。

跟隨 CLASSMETHOD 關鍵字的類名和方法名必須在同一行上。類名可以識別一個類,無論是否帶有包限定。方法名可以識別一個方法,無論是否帶有引數列表或返回型別。建構函式方法使用特殊名稱 <init> 識別,類初始化方法使用特殊名稱 <clinit> 識別。例如:

# 類和方法示例
RULE any commit on any coordinator engine
CLASS CoordinatorEngine
METHOD commit
. . .
ENDRULE

與名稱為 CoordinatorEngine 的任何類匹配,無論它屬於哪個包。當任何具有此名稱的類被載入時,代理將在任何名為 commit 的方法的開頭插入一個觸發點。如果存在幾種這種方法的不同簽名,則每種方法都會插入一個觸發點。

透過新增一個包括引數型別列表的簽名,可以保證更精確的匹配,可選地,還包括返回型別。例如:

# 類和方法示例 2
RULE commit with no arguments on wst11 coordinator engine
CLASS com.arjuna.wst11.messaging.engines.CoordinatorEngine
METHOD State commit()
AT LINE 324
. . .
ENDRULE

這條規則將只匹配 com.arjuna.wst11.messaging.engines 包中的 CoordinatorEngine 類,並且只匹配一個沒有引數並且返回型別名稱為 Statecommit 方法。請注意,在這個例子中,引數或返回型別的包被省略了。型別檢查器將從匹配的方法中推斷省略的引數或返回型別的包。前一個例子還使用了位置說明符 AT LINE。跟隨行關鍵字的文字必須能夠被解析為一個整數行號。這指示代理在原始碼中的特定行之前插入觸發呼叫。

注意:

  • Byteman 代理 不會 通常轉換 java.lang 包中的任何類,並且永遠不會轉換 org.jboss.byteman 包中的類,即 byteman 包本身(可以透過設定系統屬性來移除這些限制,但你需要非常確定你知道自己在做什麼 - 下文有詳細說明)。

  • 可以透過使用 (內部格式) $ 分隔符來指定內部類,以區分內部類和其封閉的外部類,例如 org.my.List$Cons, Map$Entry$Wrapper

類規則與介面規則

Byteman 規則可以附加到介面以及類。如果 CLASS 關鍵字被替換為關鍵字 INTERFACE,則規則適用於實現指定介面的任何類。例如,以下規則:

# 介面規則示例
RULE commit with no arguments on any engine
INTERFACE com.arjuna.wst11.messaging.engines.Engine
METHOD commit()
. . .
ENDRULE

附加到介面 Engine 的方法 commit 上。如果 Engine 被類 CoordinatorEngineParticipantEngine 實現,則規則意味著兩個觸發點,一個在方法 CoordinatorEngine.commit() 的開頭,另一個在方法 ParticipantEngine.commit() 的開頭。代理確保每個實現類都被轉換以包含規則的觸發呼叫。

覆蓋規則

通常,Byteman 只將規則程式碼注入到在 CLASS 子句中識別的類中定義的方法。這有時並不是很有幫助。例如,以下規則並沒有多大用處:

RULE trace Object.finalize
CLASS java.lang.Object
METHOD finalize
IF TRUE
DO System.out.println("Finalizing " + $0)
ENDRULE

print 語句被插入到方法 Object.finalize() 中。然而,JVM 只在物件的類覆蓋了 Object.finalize() 時才呼叫 finalize。所以,這條規則不會實現預期的目的,因為覆蓋方法不會被修改。(n.b.這不是全部故事 - 直接覆蓋 Object.finalize 的方法實現 and 呼叫 super.finalize() 將會觸發規則)。

有許多其他情況可能希望將程式碼注入到覆蓋方法實現中。例如,類 Socket 被各種類專門化,這些類提供了自己的 bind, accept 等方法實現。所以,附加到 Socket.bind() 的規則在呼叫這些子類的 bind 方法時不會被觸發(除非子類方法呼叫 super.bind())。

當然,總是可以為每個覆蓋類定義一個特定規則。然而,這是乏味的,並且當程式碼庫更改時可能會錯過一些情況。因此,Byteman 提供了一個簡單的語法,用於指定規則也應該注入到覆蓋實現中。

RULE trace Object.finalize
CLASS ^java.lang.Object
METHOD finalize
IF TRUE
DO System.out.println("Finalizing " + $0)
ENDRULE

類名前面的 ^ 字首告訴代理規則應該適用於由類 Object 或任何擴充套件 Object 的類定義的 finalize 實現。這個字首也可以與介面規則一起使用,要求代理將規則程式碼注入到實現介面的方法中,並且也注入到實現類的子類中的覆蓋方法中。

請注意,如果覆蓋方法呼叫超方法,則這種樣式的注入可能會導致注入的規則程式碼被觸發多次。特別是,注入到建構函式中(這不可避免地會呼叫某種形式的超建構函式)通常會導致規則多次觸發。這很容易避免,在規則中新增一個條件來檢查呼叫者方法的名稱。例如,上述規則最好被重寫為:

RULE trace Object.finalize at initial call
CLASS ^java.lang.Object
METHOD finalize
IF NOT callerEquals("finalize")
DO System.out.println("Finalizing " + $0)
ENDRULE

這條規則使用了內建方法 callerEquals,它可以被呼叫具有多種替代簽名(下面有詳細描述)。這個版本呼叫 String.equals() 比較呼叫觸發方法的方法名稱與它的字串引數,並返回結果。條件使用 NOT 運算子否定了這一點(這是 Java ! 運算子的另一種寫法)。所以,當透過 finalizer 執行緒的 runFinalizer() 方法呼叫 finalize 實現時,這個條件評估為 true 並且規則觸發。當它透過 super.finalize() 呼叫時,條件評估為 false 並且規則不會觸發。

覆蓋介面規則

^ 字首也可以與 INTERFACE 規則結合使用。通常,介面規則只注入到直接實現介面方法的類中。這可能意味著一個簡單的 INTERFACE 規則並不總是被注入到你感興趣的類中。

例如,類 ArrayList 擴充套件了類 AbstractList,後者又實現了介面 List。附加到 INTERFACE List 的規則 will 被考慮注入到 AbstractList 中,但 not 被考慮注入到 ArrayList 中。這是有意義的,因為 AbstractList 將包含 List 中的每個方法的實現(其中一些方法可能是抽象的)。所以,類 ArrayList 中的任何重新實現介面的方法都被認為是覆蓋方法。然而,^ 字首可以用來實現預期的效果。如果規則附加到 INTERFACE ^List,那麼它 will 被考慮注入到 AbstractListArrayList 中。

請注意,當一個類擴充套件一個超類和介面擴充套件一個超介面時,這些情況之間存在微妙的差別。讓我們以介面 Collection 為例,它被介面 List 擴充套件。當規則附加到 INTERFACE Collection 時,它被考慮注入到任何實現 Collection 的類中,以及實現 Collection 擴充套件的任何類中。由於 List 擴充套件了 Collection,這意味著實現類如 AbstractList 將是規則的候選。這是因為 AbstractList 是從 Collection 透過 List 鏈到達的第一個類,所以它是類層次結構中第一個可以找到 Collection 方法實現的點(即使它只是一個抽象方法)。類 ArrayList 不會是注入的候選,因為它的方法重新實現了宣告的 Collection 方法,仍然只會覆蓋在 AbstractList 中實現的方法。如果你想讓規則注入到這些在類 ArrayList 中定義的覆蓋方法中,那麼可以透過將規則附加到 INTERFACE ^Collection 來實現。

位置說明符

上述示例要麼使用 AT LINE 將觸發點的確切位置指定為特定行號,要麼預設為方法的開頭。顯然,行號可以用來指定幾乎任何執行點,並且易於在不受更改的程式碼中使用。然而,這種方法對於測試自動化並不是很有用,因為被測試的程式碼可能會被修改。顯然,當程式碼被編輯時,相關的測試需要被修改。但是,程式碼庫的修改很容易移動未修改程式碼的行號,從而使與編輯無關的測試指令碼無效。幸運的是,有幾種其他方法可以指定觸發點應該插入目標方法的位置。例如:

# 位置說明符示例
RULE countdown at commit
CLASS CoordinatorEngine
METHOD commit
AFTER WRITE $current
. . .
ENDRULE

名稱 current 前面帶有 $ 符號,標識一個區域性變數,或者可能是一個方法引數。在這種情況下,current 是在方法 CoordinatorEngine.commit 的開頭宣告和初始化的區域性變數,其型別是列舉 State

public State commit()
{
  final State current ;
  synchronized(this)
  {
    current = this.state ;
    if (current == State.STATE_PREPARED_SUCCESS) {
      . . .

所以,觸發點將被插入到位元組碼中的第一個寫入操作之後(istore),該操作更新用於儲存 current 的棧位置。這實際上與說觸發點發生在原始碼中區域性變數 current 被初始化的點一樣,即同步塊內的第一行。

相比之下,以下規則將在欄位 recovered 的第一次讀取之後定位觸發點:

# 位置說明符示例 2
RULE add countdown at recreate
CLASS CoordinatorEngine
METHOD <init>
AT READ CoordinatorEngine.recovered
. . .
ENDRULE

請注意,在最後一個例子中,欄位型別是限定的,以確保寫入是針對屬於類 CoordinatorEngine 的欄位。如果沒有型別限定,則規則將匹配任何讀取具有名稱 recovered 的欄位。

完整的位置說明符集如下:

  • AT ENTRY
  • AT EXIT
  • AT LINE number
  • AT READ [type .] field [count | ALL ]
  • AT READ $var-or-idx [count | ALL ]
  • AFTER READ [ type .] field [count | ALL ]
  • AFTER READ $var-or-idx [count | ALL ]
  • AT WRITE [ type .] field [count | ALL ]
  • AT WRITE $var-or-idx [count | ALL ]
  • AFTER WRITE [ type .] field [count | ALL ]
  • AFTER WRITE $var-or-idx [count | ALL ]
  • AT INVOKE [ type .] method [ ( argtypes ) ] [count | ALL ]
  • AFTER INVOKE [ type .] method [ ( argtypes ) ][count | ALL ]
  • AT NEW [ type ] [ [] ] * [count | ALL ]
  • AFTER NEW [ type ] [ [] ] * [count | ALL ]
  • AT SYNCHRONIZE [ count | ALL ]
  • AFTER SYNCHRONIZE [ count | ALL ]
  • AT THROW [count | ALL ]
  • AT EXCEPTION EXIT

如果提供了位置說明符,它必須緊接在 METHOD 說明符之後。如果沒有提供位置說明符,則預設為 AT ENTRY

AT ENTRY

AT ENTRY 說明符通常將觸發點定位在觸發方法中的第一個可執行指令之前。一個例外是建構函式方法,在這種情況下,觸發點位於呼叫超建構函式或重定向呼叫替代建構函式之後的第一個指令之前。這是為了確保規則不會在例項構造之前嘗試繫結和操作例項。

AT EXIT

AT EXIT 說明符在觸發方法中每個正常返回控制的位置定位觸發點(即在隱式或顯式返回的地方,而不是在丟擲退出方法的地方)。

FunTester 原創精華

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

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

相關文章