规则在脚本中定义,脚本由一系列规则定义组成,并与注释行交错。注释可以出现在规则定义的正文中,也可以在规则定义之前或之后,但必须与规则文本分开一行。注释以 # 字符开头:

######################################
# 示例规则集
#
# 单个规则定义
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 规则可以附加到接口以及类。如果 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 的字段。

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

如果提供了位置说明符,它必须紧接在 METHOD 说明符之后。如果没有提供位置说明符,则默认为 AT ENTRY

AT ENTRY

AT ENTRY 说明符通常将触发点定位在触发方法中的第一个可执行指令之前。一个例外是构造函数方法,在这种情况下,触发点位于调用超构造函数或重定向调用替代构造函数之后的第一个指令之前。这是为了确保规则不会在实例构造之前尝试绑定和操作实例。

AT EXIT

AT EXIT 说明符在触发方法中每个正常返回控制的位置定位触发点(即在隐式或显式返回的地方,而不是在抛出退出方法的地方)。

FunTester 原创精华

【连载】从 Java 开始性能测试


↙↙↙阅读原文可查看相关链接,并与作者交流