场景:假设有两个文件
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-path(它在类路径上)
- 断言通过
(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) 不要在类路径上放置 compile-path(这违反了当前的期望,但避免了加载 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