规则在脚本中定义,脚本由一系列规则定义组成,并与注释行交错。注释可以出现在规则定义的正文中,也可以在规则定义之前或之后,但必须与规则文本分开一行。注释以 #
字符开头:
######################################
# 示例规则集
#
# 单个规则定义
RULE example rule
# 规则正文中的注释行,FunTester
. . .
ENDRULE
规则事件规范确定了与目标类相关联的目标方法中的具体位置。目标方法可以是静态方法、实例方法或构造函数。如果没有指定详细位置,默认位置是目标方法的入口。因此,单个规则的基本模式如下:
# 规则骨架
RULE <规则名称>
CLASS <类名称>
METHOD <方法名称>
BIND <绑定>
IF <条件>
DO <动作>
ENDRULE
跟随 RULE
关键字的规则名称可以是任何自由形式的文本,限制是它必须至少包含一个非空白字符。规则名称不需要是唯一的,但在调试规则脚本时,如果它们清楚地标识规则,则很有帮助。规则名称在解析、类型检查、编译或执行过程中遇到错误时会打印出来。
跟随 CLASS
和 METHOD
关键字的类名和方法名必须在同一行上。类名可以识别一个类,无论是否带有包限定。方法名可以识别一个方法,无论是否带有参数列表或返回类型。构造函数方法使用特殊名称 <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
类,并且只匹配一个没有参数并且返回类型名称为 State
的 commit
方法。请注意,在这个例子中,参数或返回类型的包被省略了。类型检查器将从匹配的方法中推断省略的参数或返回类型的包。前一个例子还使用了位置说明符 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
被类 CoordinatorEngine
和 ParticipantEngine
实现,则规则意味着两个触发点,一个在方法 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 被考虑注入到 AbstractList
和 ArrayList
中。
请注意,当一个类扩展一个超类和接口扩展一个超接口时,这些情况之间存在微妙的差别。让我们以接口 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 原创精华