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 中的哪些函数代码。

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...