场景:假设有两个文件
src/dispatch/core.clj
(ns dispatch.core (:require [dispatch.dispatch]))
src/dispatch/dispatch.clj
(ns dispatch.dispatch) (deftype T []) (def t (->T)) (println "T = (class t):" (= T (class t)))
先编译 core,然后编译 dispatch
java -cp src:target/classes:clojure.jar -Dclojure.compile.path=target/classes clojure.main user=> (compile 'dispatch.core) T = (class t): true dispatch.core user=> (compile 'dispatch.dispatch) T = (class t): false ;; expected true dispatch.dispatch
这种情况在具有 {{:aot :all}} 的 leiningen 项目中更为常见。使用 :all 时,文件按字母顺序编译。在这种情况下,dispatch.core 将先编译,然后是 dispatch.dispatch。
原因
(compile 'dispatch.core)
- 递归编译 dispatch.dispatch
- 将 .class 文件写入 编译路径(它在类路径上)
- 断言通过
(compile 'dispatch.dispatch)
- 由于先前编译,通过应用类加载器加载 dispatch__init
- ->T 构造函数将使用新字节码实例化一个 T 实例 - 这使用 appclassloader,从磁盘上编译的 T 加载
- 但是,T 类字面量使用 RT.classForName 解析,它检查动态类加载器缓存,因此使用旧的运行时版本 T,而不是磁盘上的版本
在 1.6 中,RT.classForName() 没有检查动态类加载器缓存,因此像实例一样从磁盘加载 T。这种变化是在 CLJ-979 中为了支持其他重定义和 AOT 混合用途而做出的。
方法
1) 以反向依赖顺序编译,以避免两次编译。
交换第一个例子中编译的顺序,或在 project.clj 中指定顺序
:aot [dispatch.dispatch dispatch.core]
这是一个短期解决方案。
2) 将 deftype 移动到用于它的独立命名空间中,以便在第二次编译时不会被重定义。这是另一种短期解决方案。
3) 不要在类路径上将编译路径(这违反了当前预期,但避免了加载 dispatch__init)
(set! *compile-path* "foo") (compile 'dispatch.core) (compile 'dispatch.dispatch)
目前通过 Leiningen 设置起来并不容易。
4) 使用独立的 Clojure 运行时编译每个文件 - 避免在 DCL 中使用类字面量的缓存类。
在 Leiningen 或其他方式下实际操作可能有些棘手。
5) 使编译非递归。这类似于 CLJ-322,这是一个另一个问题。但我们可能也应该朝这个方向发展。
筛选:我认为提出的补丁不是一个好主意——它只是掩饰了症状,而没有触及根本原因。我认为我们需要重新评估关于编译路径 (#3) 和传递性 (CLJ-322) (#5) 的编译方式,但我觉得我们应该在1.7之后做这件事。——Alex
另请参阅:CLJ-1650