【问题标题】:Byte Buddy Advice breaks Eclipse debuggerByte Buddy Advice 破坏了 Eclipse 调试器
【发布时间】:2019-02-03 15:41:30
【问题描述】:

我使用 net.bytebuddy.asm.Advice 在经过适当注释的方法之前和之后添加代码,以启动和停止计时器。修改后的类在引用它们的原始类之前手动加载到目标类加载器中,从而取代它们。我正在使用 OSGi (Equinox)。

相当不错,但是当我在目标方法的断点处停止 Eclipse (Photon 4.8.0) 调试器时,变量视图仅显示:

com.sun.jdi.InternalException:得到错误代码回复:35 发生 从堆栈帧中检索“this”。

这是不可避免的和不可避免的吗?如果这会使检测代码无法调试,那有点毁了我的用例:(

(我已禁用选项“在步骤操作后显示方法结果(如果 VM 支持;可能会很慢”。)

示例

我相信我可能发现生成的字节码存在一些问题。

要检测的类:

 1 package com.tom.test;
 2
 3 import com.tom.instrument.Instrumented;
 4 import com.tom.instrument.Timed;
 5
 6 @Instrumented(serviceType = "blah")
 7 public class Test {
 8
 9 @Timed
10 public void writeName() {
11   final String myLocal = "Tom";
12      System.out.println(myLocal);  
13   }
14
15 }

“建议”:

package com.tom.instrument;

import net.bytebuddy.asm.Advice.OnMethodEnter;

public class Instrumentation {

    @OnMethodEnter
    public static void onMethodEnter() {
        System.out.println("Enter");
    }

}

致电字节好友:

new ByteBuddy()
    .redefine(type, ClassFileLocator.ForClassLoader.of(this.classLoader))
    .visit(Advice.to(Instrumentation.class)
    .on(isAnnotatedWith(Timed.class)))
    .make().saveIn(new File("instrumented"));

javap 中的结果:

Compiled from "Test.java"
...

  public void writeName();
    Code:
       0: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #40                 // String Enter
       5: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: goto          11
      11: ldc           #17                 // String Tom
      13: astore_1
      14: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      17: ldc           #17                 // String Tom
      19: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      22: return
    LineNumberTable:
      line 11: 0
      line 12: 14
      line 13: 22
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         11      12     0  this   Lcom/tom/test/Test;
         14       9     1 myLocal   Ljava/lang/String;
}

如果我在 Test.java 的第 11 行设置断点,那么 Eclipse 调试视图会显示:<unknown receiving type>(Test).writeName() line: 11

变量视图显示:com.sun.jdi.InternalException: Got error code in reply:35 occurred retrieving 'this' from stack frame.

如果我破解字节码,在 0x2A2 处将 00 更改为 0B,那么行号表如下所示:

LineNumberTable:
  line 11: 11
  line 12: 14
  line 13: 22

然后一切都很好!这对我来说似乎是正确的,但我不是这里的专家。

如果我也使用@OnMethodExit,那就有点复杂了。将以下内容添加到Instrumentation.class:

@OnMethodExit
public static void onMethodExit() {
    System.out.println("Exit");
}

javap 给出:

Compiled from "Test.java"
...

  public void writeName();
    Code:
       0: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #40                 // String Enter
       5: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: goto          11
      11: aload_0
      12: astore_1
      13: ldc           #17                 // String Tom
      15: astore_2
      16: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      19: ldc           #17                 // String Tom
      21: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      24: goto          27
      27: getstatic     #19                 // Field java/lang/System.out:Ljava/io/PrintStream;
      30: ldc           #42                 // String Exit
      32: invokevirtual #25                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      35: goto          38
      38: return
    LineNumberTable:
      line 11: 0
      line 12: 16
      line 13: 24
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         13      14     1  this   Lcom/tom/test/Test;
         16      11     2 myLocal   Ljava/lang/String;
}

要解决这个问题,我必须更新行号表和局部变量表。像这样:

LineNumberTable:
  line 11: 13
  line 12: 16
  line 13: 24
LocalVariableTable:
  Start  Length  Slot  Name   Signature
     13      14     0  this   Lcom/tom/test/Test;
     16      11     1 myLocal   Ljava/lang/String;

区别:

也许这是 Eclipse 调试器期望 this 始终位于插槽 0 中的错误?或者也许这就是它应该的方式。错误代码 35 来自 JVM。

添加退出建议更改槽的原因似乎是因为它导致使用ForInstrumentedMethod.Default.Copying 而不是Simple。他们有不同的variable() 实现。

【问题讨论】:

  • 谢谢。我已经看到的不是那个错误,而是它链接到的帖子。我已禁用选项“在步骤操作后显示方法结果(如果 VM 支持;可能会很慢”以防万一它有帮助但没有区别。另外,我有 Eclipse 4.8.0,它应该包含似乎由您链接的错误导致。
  • 在构建期间使用 Maven 或 Gradle 插件应用建议时,是否也会出现此问题?如果可以执行字节码,我看不出这是由 Byte Buddy 引起的问题,而在我使用的 IntelliJ 中,我没有观察到类似的情况。鉴于这些观察,我认为这是由 Eclipse 调试器引起的问题。 Byte Buddy 可能会弄乱一些用于调试的元数据,如果您可以构建复制品,我可以看看,您也可以将其作为错误提交给 Eclipse。

标签: java eclipse osgi bytecode byte-buddy


【解决方案1】:

见Eclipse bug 531706:

当并非所有类都已检测时会出现此问题,请参阅comment #4 by Tobias Hirning:

...

现在我也得到了更清晰的画面:错误只出现在 方法在 jar 文件中的方法调用。而且我认为 他们没有被检测。

...

错误发生在 VM 中,而不是 Eclipse 中。当 Eclipse 通过调试接口请求变量时返回错误代码35 而不是值。由于提到的错误报告所做的更改是忽略它,请参阅comment #7 by Till Brychcy (who made the change):

...

我已经能够重现该问题并且只是忽略了 此代码路径中的 InternalException 改善了这种情况。

您有时会在 变量视图,但总的来说它似乎有效。

为避免此问题,您必须检测所有类。

【讨论】:

  • 感谢您的帮助;非常感谢。我明白了,也许这个错误比我意识到的要多一点。但是,我没有运行任何代理。仪器所有类是什么意思?我只想将计时代码添加到少数方法中。那么我将如何检测其余部分?我要调试的类引用的所有类?捆绑包中的所有类?应用程序中的所有类?这将是数千个 JAR 中的数百万个方法,其中许多是第三方库。
  • 啊,我有一个新理论——或许我终于明白你的意思了。当我在调试模式下运行时,Eclipse(JDT)是否会检测所有类,但它永远没有机会检测我的动态类。我在进程命令中看到-javaagent:../javaagent-shaded.jar。有什么方法可以检测我的动态类?
  • 如果instrumentation使用不当,当JVM通过Java Debug Interface (JDI)请求变量时,可能会在JVM内部发生错误。该问题可能是由您构建或运行应用程序的方式引起的。将 Byte Buddy 与其他代码修改框架(例如代码覆盖框架、AspectJ、Mockito、Lombok 等)一起使用时也可能出现问题。您能否举一个最小的示例来说明如何重现该问题?
  • 也许您会在 Byte Buddy 文档(例如 Javadoc of ByteBuddyAgent)或一些 Byte Buddy issue reports 中提供一些提示如何解决您的问题。
  • @committedandroider 这个问题是由同时修复的错误引起的。请升级到 Eclipse Photon (4.8) 或更高版本。建议使用最新的 Eclipse 版本,当前为 Eclipse 2019-03 (4.11)。
【解决方案2】:

解决方法

啊哈!我一直在寻找修补补丁的方法让我找到了我希望早点找到的东西。

  • 我可以使用@OnMethodEnter(prependLineNumber = false) 来避免行号问题。

  • 并且使用@OnMethodExit(backupArguments = false) 可以避免插槽问题。

这对我来说是个好消息!但是,大概这些不是默认值是有充分理由的。我还不明白使用这些选项是否会产生重要的负面影响。

【讨论】:

  • 对于未来的读者:如果在输入通知中抛出异常并且未捕获;它不会带有行号。此外,如果您尝试通过异常堆栈或堆栈遍历器捕获行号,则如果不预先添加行号,它将无法工作。如果您不备份参数,则检测方法中的任何参数重新分配都将在退出通知中可见。此外,某些语言和混淆器会弄乱局部变量数组,因此您可能会面临检测异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-09-18
  • 1970-01-01
  • 2021-06-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多