byteman
我正在与JBoss中的许多社区一起工作,有很多有趣的事情要谈论,以至于我无法将自己的每一分都缠住。 这就是为什么我非常感谢有机会不时地欢迎客座博客的主要原因。 今天是Jochen Mader,他是以代码为中心的书呆子群的一部分。 他目前花费大量的时间在基于Vert.x的中间件解决方案编码,为不同的出版物撰写文章以及在会议上发表演讲。 他的业余时间属于他的家人,山地车和桌面游戏。 您可以在Twitter @codepitbull上关注他。
有些工具通常是您不希望使用的,但在需要时会很高兴知道它们。 至少对我来说,Byteman属于这一类。 这是我个人的瑞士军刀,要处理一个大泥球或其中一个可怕的黑森贝格。 因此,获取当前的Byteman发行版 ,将其解压缩到您计算机上的某个位置,我们将进行一些繁琐的工作。
它是什么
Byteman是字节码操作和注入工具套件。 它使我们能够拦截和替换Java代码的任意部分,以使其表现不同或(故意)破坏它:
- 将所有线程卡在某个位置,并让它们同时继续(您好,竞争条件)
- 在意外的位置抛出异常
- 在执行过程中跟踪代码
- 更改返回值
还有更多的东西。
一个例子
让我们直接看一些代码来说明Byteman可以为您做些什么。
在这里,我们有一个很棒的Singleton和一个(可悲的)很好的示例代码,您可能在很多地方都可以找到。
public class BrokenSingleton {
private static volatile BrokenSingleton instance;
private BrokenSingleton() {
}
public static BrokenSingleton get() {
if (instance == null) {
instance = new BrokenSingleton();
}
return instance;
}
}
我们假装自己是可怜的人,负责调试一些旧代码,这些旧代码显示了生产中的怪异行为。 一段时间后,我们发现了这颗宝石,我们的直觉表明这里出了点问题。
首先,我们可以尝试如下操作:
public class BrokenSingletonMain {
public static void main(String[] args) throws Exception {
Thread thread1 = new Thread(new SingletonAccessRunnable());
Thread thread2 = new Thread(new SingletonAccessRunnable());
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
public static class SingletonAccessRunnable implements Runnable {
@Override
public void run() {
System.out.println(BrokenSingleton.get());
}
}
}
运行此命令,很少有机会看到实际的问题发生。 但是最有可能我们不会看到任何异常情况。 Singleton初始化一次,应用程序按预期执行。 很多时候,人们开始通过增加线程数来进行暴力破解,以期使问题得以解决。 但是我更喜欢一种结构化的方法。
输入Byteman。
DSL
Byteman提供了方便的DSL来修改和跟踪应用程序的行为。 在我的小示例中,我们将从跟踪调用开始。 看一下这段代码。
RULE trace entering
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AT ENTRY
IF true
DO traceln("entered get-Method")
ENDRULE
RULE trace read stacks
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AFTER READ BrokenSingleton.instance
IF true
DO traceln("READ:\n" + formatStack())
ENDRULE
Byteman脚本的核心构建模块是RULE。
它由几个组件组成(例如从Byteman-Docs中毫不客气地示例:
# rule skeleton
RULE <rule name>
CLASS <class name>
METHOD <method name>
BIND <bindings>
IF <condition>
DO <actions>
ENDRULE
每个规则都必须具有唯一的__规则名称__。 CLASS和METHOD的组合定义了我们希望将修改应用到的位置。 BIND允许我们将变量绑定到可以在IF和DO中使用的名称。 使用IF,我们可以添加触发规则的条件。 在DO中,实际的魔术发生了。
ENDRULE,它结束规则。
知道这一点,我的第一条规则很容易转换为:
当有人调用_de.codepitbull.byteman.BrokenSingleton.get()_时,我想在调用方法主体之前(即__AT ENTRY__转换为)打印字符串“ entered get-Method”。
我的第二条规则可以转换为:
读取(__AFTER READ__)之后,我想查看当前的调用堆栈。
抓取代码并将其放入名为_check.btm_的文件中。 Byteman提供了一个不错的工具来验证您的脚本。 使用__ <bytemanhome> /bin/bmcheck.sh -cp文件夹/包含/已编译/类/至/测试check.btm__来查看脚本是否可以编译。 每次更改时都要执行此操作,很容易弄错细节并花很长时间弄清楚它。
现在,脚本已保存并经过测试,现在可以在我们的应用程序中使用它了。
中介
脚本通过代理应用于运行代码。 打开__BrokenSingletonMain-class__的运行配置并添加
__-javaagent:<BYTEMAN_HOME>/lib/byteman.jar=script:check.btm__
到您的JVM参数。 这将注册代理并告诉它运行_check.btm_。
而当我们在这里时,还有更多选择:
如果您需要操纵一些核心Java东西,请使用
__-javaagent:<BYTEMAN_HOME>/lib/byteman.jar=script:appmain.btm,boot:<BYTEMAN_HOME>/lib/byteman.jar__
这会将Byteman添加到引导类路径中,并允许我们操纵_Thread _,_ String_之类的类……我的意思是,如果您想处理这种讨厌的事情……
也可以将代理附加到正在运行的进程。 我们__jps__查找您要附加并运行的进程ID
__<bytemanhome>/bin/bminstall.sh <pid>__
安装代理。 之后运行
__<bytemanhome>/bin/bmsubmit.sh check.btm__
回到我们眼前的问题。
使用修改后的run-Configuration运行我们的应用程序,应得到如下输出
entered get-Method
entered get-Method
READ:
Stack trace for thread Thread-0
de.codepitbull.byteman.BrokenSingleton.get(BrokenSingleton.java:14)
de.codepitbull.byteman.BrokenSingletonMain$SingletonAccessRunnable.run(BrokenSingletonMain.java:20)
java.lang.Thread.run(Thread.java:745)
READ:
Stack trace for thread Thread-1
de.codepitbull.byteman.BrokenSingleton.get(BrokenSingleton.java:14)
de.codepitbull.byteman.BrokenSingletonMain$SingletonAccessRunnable.run(BrokenSingletonMain.java:20)
java.lang.Thread.run(Thread.java:745)
恭喜,您刚刚操作了字节码。 输出还不是很有帮助,但这是我们要更改的东西。
线程混乱
现在,随着我们的基础架构的建立,我们可以开始更深入地挖掘。 我们非常确定我们的问题与某些多线程问题有关。 为了检验我们的假设,我们必须同时将多个线程放入关键部分。 使用纯Java,这几乎是不可能的,至少在不对我们要调试的代码进行大量修改的情况下。
使用Byteman可以轻松实现。
RULE define rendezvous
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AT ENTRY
IF NOT isRendezvous("rendezvous", 2)
DO createRendezvous("rendezvous", 2, true);
traceln("rendezvous created");
ENDRULE
该规则定义了一个所谓的集合点。 它允许我们指定多个线程必须到达的位置,直到允许它们继续前进(也称为aa障碍)。
这是规则的翻译:
当调用_BrokenSingleton.get()_时,创建一个新的集合点,当2个线程到达时将允许进度。 使集合点可重用,并仅在不存在时才创建它(IF NOT部分至关重要),否则,我们将在每次对_BrokenSingleton.get()_的调用上创建一个障碍。
定义此障碍后,我们仍然需要显式使用它。
RULE catch threads
CLASS de.codepitbull.byteman.BrokenSingleton
METHOD get
AFTER READ BrokenSingleton.instance
IF isRendezvous("rendezvous", 2)
DO rendezvous("rendezvous");
ENDRULE
翻译:读取_BrokenSingleton.get()_中的_instance_-member后,在集合点等待,直到第二个线程到达并继续。
现在,在实例空检查之后,我们停止来自同一花边中_BrokenSingletonMain_的两个线程。 这就是使比赛条件可再现的方法。 两个线程将继续认为_instance_为null,从而导致构造函数触发两次。
我将这个问题的解决方案留给您……
单元测试
我在撰写此博客文章时发现,有可能在我的单元测试中运行Byteman脚本。 它们的JUNit和TestNG集成很容易集成。
将以下依赖项添加到_pom.xml_
<dependency>
<groupId>org.jboss.byteman</groupId>
<artifactId>byteman-submit</artifactId>
<scope>test</scope>
<version>${byteman.version}</version>
</dependency>
现在,Byteman脚本可以在您的单元测试中执行,如下所示:
@RunWith(BMUnitRunner.class)
public class BrokenSingletonTest
{
@Test
@BMScript("check.btm")
public void testForRaceCondition() {
...
}
}
将此类测试添加到您的西装中会大大提高Byteman的有用性。 没有更好的办法阻止其他人重复这些错误,因为这些脚本是构建过程的一部分。
结束语
博客文章中只有这么多空间,我也不想开始重写他们的文档。 写这篇文章是一件很有趣的事情,因为我已经有一段时间没有使用Byteman了。 我不知道我如何忽略了单元测试的集成。 这将使我将来更多地使用它。
现在,我建议浏览他们的文档并开始进行注入,还有很多事情要做。
翻译自: https://www.javacodegeeks.com/2015/02/byteman-swiss-army-knife-byte-code-manipulation.html
byteman