场景:假设有两个文件
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
这种场景在具有{{:aot :all}}的Leiningen项目中更为常见。所有文件都按字母顺序编译。在这种情况下,先编译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,这是另一个问题。但我们可能也该走向这个方向。
筛选:我不相信提出的补丁是一个好主意 - 它掩盖了症状而没有解决根本原因。我认为我们需要重新评估与编译路径(第3)和递归性(CLJ-322)(第5)有关的编译方式。但我认为我们应该在1.7之后做这件事。 - Alex
另请参阅:CLJ-1650