如何解决Clojure 和 Java 调试器
Clojure 是一种在 JVM 上运行的语言。 Clojure 编译器编译并发出 JVM 字节码。 'jdb' 是一个 jdk 工具,一个 Java 调试器工具,可用于设置断点、单步执行代码和显示变量值。但是,当我在编译的 Clojure 类文件上运行 jdb 时,我收到一条错误消息,指出编译的类中没有行号信息。我以为Clojure将调试信息编译成JVM字节码。有谁知道为什么我会收到这个错误?
我使用了另一个 jdk 工具 javap 来验证,事实上,类文件中没有调试信息。
详细说明,我试图理解为什么 Clojure 中的 compile 函数默认无法附加行号。这似乎是文档所暗示的 - 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
...
我们可以看到编译器使用了内部类(名称中带有 core$
的类)并且我们的函数 x2
被编译为一个类。
为了在 jdb
中运行 Clojure,我们需要构建一个类路径,其中包含我们的代码、Clojure 运行时以及在 Clojure 1.10+ 中还有一些 Clojure 运行时 (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 并构建我的类路径以使用类 jdb
运行 demo.core
,我知道它是入口点:
$ 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 ...
>
在提示中,我会用 jdb
告诉 stop in demo.core$x2.invokeStatic
在相关方法中停止。您可以使用其余的 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",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
在开发过程中,这种风格无法与将代码提交到正在运行的 REPL 会话并获得即时反馈的交互体验相比,因此不实用(非常具体的场景除外)。
我认为这也是我们在 Eclipse 中使用 JDWP 调试 Clojure 应用程序时在前团队中的经验类型,但一段时间后,很难跟踪 Java 字节码中的哪些方法映射到 Java 中的哪些函数代码。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。