Clojure 和 Java 调试器

Clojure and the Java Debugger

Clojure 是一种在 JVM 上 运行 的语言。 Clojure 编译器编译并生成 JVM 字节代码。 'jdb' 是一个 jdk 工具,一个 Java 调试器工具,可用于设置断点、逐步执行代码和显示变量值。但是,当我在已编译的 Clojure class 文件上 运行 jdb 时,我收到一条错误消息,指出已编译的 classes 中没有行号信息。我以为 Clojure 将调试信息编译成 JVM 字节码。有谁知道为什么我会得到这个错误?

我已经使用另一个 jdk 工具 javap 来验证 class 文件中实际上没有调试信息。

详细来说,我想了解为什么 Clojure 中的编译函数默认无法附加行号。这似乎是文档所暗示的 - https://clojure.org/reference/compilation。这是简单的案例:

    (ns com.example.core
      (:gen-class
        :name com.example.core
        :main true))
    
    (defn -main [& args]
      (let [foo "foo"
            foo-cap "FOO"
            bar "bar"]
        bar)))

user=>(load "com/example/core")
user=>(compile 'com.example.core)

javap -cp ...com.example.core

你看到 LineNumberTable 了吗?

可以使用 jdb 调试 Clojure 字节码,但它不是很实用(请阅读 乏味 )并且可能缺少一些信息以从已编译的字节码映射到原始源文件,但我做了一个小测试来验证它是否有效(至少部分地,在进入方法时设置断点)。

我将使用 Leiningen 创建一个新的 Clojure 项目:lein new app demo。现在,我将使用以下内容更新文件 src/demo/core.clj

(ns demo.core
  (:gen-class))

(defn x2 [n]
  (println "Doubling" n)
  (let [x (* n 2)]
    x))

(defn -main
  [& args]
  (let [xs (mapv x2 (range 10))]
    (doseq [x xs]
      (println x))))

现在,让我们运行lein uberjar将源代码编译成字节码:

$ lein uberjar
Compiling demo.core
Created /tmp/demo/target/uberjar/demo-0.1.0-SNAPSHOT.jar
Created /tmp/demo/target/uberjar/demo-0.1.0-SNAPSHOT-standalone.jar

我将检查在target目录下生成的文件:

$ tree target
target
└── uberjar
    ├── classes
    │   ├── demo
    │   │   ├── core$fn__173.class
    │   │   ├── core$loading__6721__auto____171.class
    │   │   ├── core$_main.class
    │   │   ├── core$x2.class
    │   │   ├── core.class
    │   │   └── core__init.class
...

我们可以看到编译器使用内部 classes(名称中带有 core$ 的那些)并且我们的函数 x2 被编译为 class。

为了运行Clojure在jdb,我们需要构造一个class路径来包含我们的代码,在Clojure运行的时候,在Clojure 1.10+ 还有一些 Clojure 运行time (Spec) 的依赖项。您可以通过查看 lein classpath:

的输出来借用大部分路线
$ lein classpath
/tmp/demo/test:/tmp/demo/src:/tmp/demo/dev-resources:/tmp/demo/resources:/tmp/demo/target/default/classes:/home/denis/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar:/home/denis/.m2/repository/org/clojure/spec.alpha/0.2.176/spec.alpha-0.2.176.jar:/home/denis/.m2/repository/org/clojure/core.specs.alpha/0.2.44/core.specs.alpha-0.2.44.jar:/home/denis/.m2/repository/nrepl/nrepl/0.7.0/nrepl-0.7.0.jar:/home/denis/.m2/repository/clojure-complete/clojure-complete/0.2.5/clojure-complete-0.2.5.jar

我将删除其中一些 JAR,并使用 class demo.core 构建我的 class 到 运行 jdb 的路径,我知道这是条目点数:

$ jdb -classpath target/uberjar/classes:/home/denis/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar:/home/denis/.m2/repository/org/clojure/spec.alpha/0.2.176/spec.alpha-0.2.176.jar:/home/denis/.m2/repository/org/clojure/core.specs.alpha/0.2.44/core.specs.alpha-0.2.44.jar demo.core

在运行宁jdb之前,我想在某处打个断点来验证。 x2 函数应该是一个很好的起点,但我们需要稍微检查一下字节码,以了解在字节码中的何处放置断点。使用 javap 会给我们一些线索:

$ javap -l target/uberjar/classes/demo/core$x2.class 
Compiled from "core.clj"
public final class demo.core$x2 extends clojure.lang.AFunction {
  public demo.core$x2();
    LineNumberTable:
      line 4: 0

  public static java.lang.Object invokeStatic(java.lang.Object);
    LineNumberTable:
      line 4: 0
      line 6: 26
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
         30       3     1     x   Ljava/lang/Object;
          0      33     0     n   Ljava/lang/Object;

  public java.lang.Object invoke(java.lang.Object);
    LineNumberTable:
      line 4: 3

  public static {};
    LineNumberTable:
      line 4: 0
}

从上面,我会注意在方法 demo.core$x2.invokeStatic 中设置一个断点,这是值得注意的,因为它有局部变量。现在我们从之前的行开始 jdb

$ jdb -classpath target/uberjar/classes:/home/denis/.m2/repository/org/clojure/clojure/1.10.1/clojure-1.10.1.jar:/home/denis/.m2/repository/org/clojure/spec.alpha/0.2.176/spec.alpha-0.2.176.jar:/home/denis/.m2/repository/org/clojure/core.specs.alpha/0.2.44/core.specs.alpha-0.2.44.jar demo.core
Initializing jdb ...
>

在提示中,我会用stop in demo.core$x2.invokeStatic告诉jdb在相关方法中停止。您可以使用 jdb 命令的其余部分来步进、继续和显示本地值,如以下会话所示:

> stop in demo.core$x2.invokeStatic
Deferring breakpoint demo.core$x2.invokeStatic.
It will be set after the class is loaded.
> run
run demo.core
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
> 
VM Started: Set deferred breakpoint demo.core$x2.invokeStatic

Breakpoint hit: "thread=main", demo.core$x2.invokeStatic(), line=4 bci=0

main[1] locals
Method arguments:
n = instance of java.lang.Long(id=2743)
main[1] print n
 n = "0"
main[1] cont
> Doubling 0

Breakpoint hit: "thread=main", demo.core$x2.invokeStatic(), line=4 bci=0

main[1] locals
Method arguments:
n = instance of java.lang.Long(id=2749)
Local variables:
main[1] print n
 n = "1"
clear demo.core$x2.invokeStatic
Removed: breakpoint demo.core$x2.invokeStatic
main[1] cont
...
> Doubling 2
...
Doubling 9
0
2
4
...
16
18

The application exited

在开发过程中,这种风格比不上向 运行ning REPL 会话提交代码并获得即时反馈的交互体验,因此不实用(除了非常特殊的场景)。

我认为这也是我们在以前的团队中使用 JDWP 在 Eclipse 中调试 Clojure 应用程序时的经验类型,但过了一段时间后,很难跟踪 Java 字节码映射中的方法Java 代码中的哪些函数。