(ns clojure.examples.hello
(:gen-class))
(defn -main
[greetee]
(println (str "Hello " greetee "!")))
Clojure 会将您加载的所有代码即时编译成 JVM 字节码,但在某些情况下,预先编译 (AOT) 更有优势。使用 AOT 编译的一些原因是
交付应用程序时无需源代码
加快应用程序启动速度
生成供 Java 使用的命名类
创建无需运行时字节码生成和自定义类加载器的应用程序
尽管 Java 存在代码重载限制,但 Clojure 的编译模型尽可能地保留了 Clojure 的动态特性。
源代码和类文件路径遵循 Java 类路径约定。
编译的目标是命名空间
每个文件、函数和 gen-class 将生成一个 .class 文件。
每个文件都会生成一个与文件名相同,并在后面附加 "__init" 的加载器类。
加载器类的静态初始化程序会产生与加载其源文件相同的效果。
通常您不需要直接使用这些类,因为 use、require 和 load 会在它们和更新的源代码之间进行选择。
当命名空间被编译时,如果其加载器 .class 文件比其源文件旧,则会为引用的每个文件生成一个加载器类。
提供了一个独立的 gen-class 功能来创建命名类,以便直接用作 Java 类,并提供以下功能:
命名生成的类
选择超类
指定任何实现的接口
指定构造函数签名
指定状态
声明其他方法
生成静态工厂方法
生成 main
控制映射到实现命名空间
公开继承的受保护成员
从单个文件生成多个命名类,并在一个或多个命名空间中实现
可以在 ns 声明中使用可选的 :gen-class 指令来生成与命名空间对应的命名类。当提供 (:gen-class …) 时,默认为 :name 对应于 ns 名称,:main 为 true,:impl-ns 与 ns 相同,以及 :init-impl-ns 为 true。支持 gen-class 的所有选项。
在不编译时,将忽略 gen-class 和 :gen-class 指令。
提供了一个独立的 gen-interface 功能来生成命名接口类,以便直接用作 Java 接口,并提供以下功能:
命名生成的接口
指定任何超接口
声明接口方法的签名
要编译库,请使用 compile 函数,并提供命名空间名称作为符号。对于某些在类路径中的 my/domain/lib.clj 文件中定义的命名空间 my.domain.lib,应发生以下情况:
将在 my/domain/lib__init.class
中生成一个加载器类文件,位于 *compile-path*
下,该路径必须位于类路径中。
将生成一组类文件,命名空间中的每个函数一个,名称例如 my/domain/lib$fnname__1234.class
。
对于每个 gen-class
将生成一个具有指定名称的存根类文件。
可以通过使用多个编译器标志来控制 Clojure 编译器。在运行时,这些标志存储在动态 var clojure.core/*compiler-options*
中,它是一个具有以下可选关键字键的映射:
:disable-locals-clearing
(布尔值)
:elide-meta
(关键字向量)
:direct-linking
(布尔值)
可以在对 compile
函数的调用周围进行动态绑定以更改编译器行为,从而更改这些编译器选项。
或者,也可以在启动时通过 Java 系统属性设置编译器选项:
-Dclojure.compiler.disable-locals-clearing=true
"-Dclojure.compiler.elide-meta=[:doc :file :line :added]"
-Dclojure.compiler.direct-linking=true
有关每个选项的更多信息,请参见下文。
默认情况下,Clojure 编译器生成的代码会急切地清除对局部绑定的 GC 引用。但是,当使用调试器时,局部变量将显示为 null,这使得调试变得困难。设置 disable-locals-clearing=true
将阻止清除局部变量。不建议禁用生产编译的局部变量清除。
Var 元数据(文档字符串、文件和行信息等)将编译成已编译类常量池中的字符串。为了减小类大小并加快类加载速度,可以省略元数据。此选项采用一个应删除的元数据关键字向量 - 一些常见的关键字包括 :doc
、:file
、:line
和 :added
。请注意,省略元数据可能会使某些功能无法使用(例如,如果已省略文档字符串,则 doc
无法返回文档字符串)。
通常,调用函数将导致对 var 进行解除引用以查找实现它的函数实例,然后调用该函数。通过 var 的这种间接方式是 Clojure 提供动态运行时环境的方式之一。但是,长期以来一直观察到,生产环境中大多数函数调用都不会以这种方式重新定义,从而导致不必要的重定向。
直接链接可用于将这种间接方式替换为对函数的直接静态调用。这将导致 var 调用更快。此外,编译器可以从类初始化中删除未使用的 var,而直接链接将使更多 var 变得未使用。通常,这会导致类大小更小,启动时间更快。
直接链接的一个后果是,已使用直接链接编译的代码将看不到 var 重新定义(因为直接链接避免了解除引用 var)。标记为 ^:dynamic
的 Var 永远不会直接链接。如果您希望将 var 标记为支持重新定义(但不是动态的),请使用 ^:redef
标记它以避免直接链接。
从 Clojure 1.8 开始,Clojure 核心库本身就是使用直接链接编译的。
由 Clojure 生成的类具有高度动态性。特别是,请注意,gen-class 中没有指定任何方法体或其他实现细节 - 它仅指定签名,并且它生成的类只是一个存根。此存根类将所有实现延迟到实现命名空间中定义的函数。在运行时,对生成的类的某个方法 foo 的调用将查找实现.命名空间/前缀foo 的 var 的当前值并调用它。如果 var 未绑定或为 nil,则它将调用超类方法,或者如果为接口方法,则生成 UnsupportedOperationException。
在最简单的情况下,提供了一个空的 :gen-class,并且已编译的类只有 main,它通过在命名空间中定义 -main 来实现。文件应保存在 src/clojure/examples/hello.clj 中。
(ns clojure.examples.hello
(:gen-class))
(defn -main
[greetee]
(println (str "Hello " greetee "!")))
要编译,请确保目标输出目录 classes
存在。
mkdir classes
并创建一个描述类路径的 deps.edn 文件。
{:paths ["src" "classes"]}
然后编译以生成如下所示的类:
$ clj
Clojure 1.10.1
user=> (compile 'clojure.examples.hello)
clojure.examples.hello
并且可以像普通的 Java 应用程序一样运行(确保包含输出类目录):
java -cp `clj -Spath` clojure.examples.hello Fred
Hello Fred!
这是一个使用更复杂的 :gen-class 以及对 gen-class 和 gen-interface 的独立调用的示例。在这种情况下,我们正在创建打算创建实例的类。clojure.examples.instance 类将实现 java.util.Iterator,这是一个特别棘手的接口,因为它要求实现是状态化的。此类将在其构造函数中获取一个字符串,并根据提供字符串中的字符来实现 Iterator 接口。:init 子句命名构造函数。:constructors 子句是构造函数签名到超类构造函数签名的映射。在这种情况下,超类默认为 Object,其构造函数不带参数。此对象将具有状态(称为 state)和一个 main,以便我们可以对其进行测试。
:init 函数(在本例中为 -init)是不寻常的,因为它们始终返回一个向量,其第一个元素是超类构造函数的参数向量 - 由于我们的超类不带参数,因此此向量为空。向量的第二个元素是实例的状态。由于我们将不得不修改状态(并且状态始终为 final),因此我们将使用一个指向包含字符串和当前索引的映射的 ref。
hasNext 和 next 是 Iterator 接口中方法的实现。虽然这些方法不带参数,但实例方法的实现函数始终会接收一个额外的第一个参数,该参数对应于调用方法的对象,按照惯例,此处称为“this”。请注意如何使用普通的 Java 字段访问来获取状态。
gen-interface 调用将创建一个名为 clojure.examples.IBar 的接口,该接口具有一个名为 bar 的方法。
独立的 gen-class 调用将生成另一个命名类 clojure.examples.impl,其实现命名空间将默认为当前命名空间。它实现了 clojure.examples.IBar。:prefix 选项会导致方法的实现绑定到以“impl-”开头的函数,而不是默认的“-”。:methods 选项定义了一个在任何超类/接口中都不存在的新方法 foo。
请注意,在 main 中如何创建类的实例以及如何使用普通的 Java 交互来调用方法。从 Java 使用它也将类似于普通方式。
(ns clojure.examples.instance
(:gen-class
:implements [java.util.Iterator]
:init init
:constructors {[String] []}
:state state))
(defn -init [s]
[[] (ref {:s s :index 0})])
(defn -hasNext [this]
(let [{:keys [s index]} @(.state this)]
(< index (count s))))
(defn -next [this]
(let [{:keys [s index]} @(.state this)
ch (.charAt s index)]
(dosync (alter (.state this) assoc :index (inc index)))
ch))
(gen-interface
:name clojure.examples.IBar
:methods [[bar [] String]])
(gen-class
:name clojure.examples.impl
:implements [clojure.examples.IBar]
:prefix "impl-"
:methods [[foo [] String]])
(defn impl-foo [this]
(str (class this)))
(defn impl-bar [this]
(str "I " (if (instance? clojure.examples.IBar this)
"am"
"am not")
" an IBar"))
(defn -main [s]
(let [x (new clojure.examples.instance s)
y (new clojure.examples.impl)]
(while (.hasNext x)
(println (.next x)))
(println (.foo y))
(println (.bar y))))
如上编译。
$ clj
Clojure 1.10.1
user=> (compile 'clojure.examples.instance)
clojure.examples.instance
并像普通的 Java 应用程序一样运行。
java -cp `clj -Spath` clojure.examples.instance asdf
a
s
d
f
class clojure.examples.impl
I am an IBar