场景:考虑两个文件
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 ;; 预期 true dispatch.dispatch
这个场景在 leiningen 项目中更常见,尤其是设置有 {{:aot :all}}。文件按字母顺序编译。在这种情况下,将首先编译 dispatch.core,然后编译 dispatch.dispatch。
原因
(compile 'dispatch.core)
- 递归编译 dispatch.dispatch
- 将 .class 文件写入 编译路径(在类路径上)
- 断言通过
(compile 'dispatch.dispatch)
- 由于之前的编译,dispatch.dispatch__init 通过 appclassloader 加载
- ->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 中的内容相符,这是一个另一个麻烦事。但我们可能正是要走向的方向。
筛选:我相信提出的补丁不是一个好主意 - 它只是掩盖了症状而没有解决根本原因。我认为我们需要重新评估编译如何与 compile-path(#3)和递归(CLJ-322,#5)一起工作,但在 1.7 之后我们应该这样做。 - Alex
另请参阅:CLJ-1650