8.3 创建可执行的Clojure程序

到目前为止,我们都是在Clojure交互式REPL shell中输入代码片段。前一章说过,Clojure没有自带独立的编译器。要创建可执行的Clojure程序,必须在代码中调用一个Clojure宏,让内置编译器生成JVM类文件。这个宏仅在Clojure编译代码时生成类文件,但在你执行已编译的代码时什么都不做。

8.3.1 在不使用Leiningen的情况下将代码编译成类文件

我们先在不使用构建工具的情况下创建一个可执行的类,了解差别后,你将更清楚Leiningen的好处。我们使用普通文本编辑器(而不是Eclipse IDE)来创建这个小型项目。创建根目录testproject1,用于存储示例文件,再在这个目录下创建如下必不可少的子目录:

  • com;
  • com\example;
  • classes。

要让Clojure将类文件写入这些目录,你必须生成JVM类。为此,一种选择是定义一个命名空间,并指定关键字:gen-class。请将如下源代码文件保存到目录com\example并将其命名为main.clj:

  1. (ns com.example.main
  2. (:gen-class :name com.example.Main))
  3. (defn -main [] (println "hi!"))
  4. (compile 'com.example.main)

命名空间是通过函数ns的第一个参数指定的,仅供Clojure使用,但最好让它与JVM包名一致。请注意,在Clojure中,一种最佳实践是包名全部小写。Clojure脚本的文件名和目录结构必须与Clojure命名空间匹配,这很重要。这就像Java要求源代码目录结构与包名匹配一样。因此,文件main.clj必须存储在目录com\example中。将关键字:gen-class:name作为参数传递给了函数ns,其中:name指定了JVM类的全限定名。为遵循JVM约定,将类名指定为了Main

接下来,在这个类中添加了方法main。通过在方法名前面加上-,告诉Clojure应将它加入到com.example.Main类中。实际上,也可使用其他的前缀,这为在一个文件中定义多个类提供了极大的便利。Clojure内置了将方法main编译为JVM变种static void main(String[] args)的逻辑。这个方法只是打印一条问候消息。

最后,调用了宏compile。调用这个宏时,必须通过参数指定要编译的Clojure命名空间。注意,指定命名空间时,在它前面加上了一个单引号,且没有与之匹配的单引号;这并非输入错误。在前面加上'后,名称将变成符号。对于符号,不会立即对其进行计算,而是直接将其传递给函数或宏。

要编译这些代码,在命令提示符或终端中确保位于项目的根目录(包含子目录classes和com的目录)下,再执行如下命令:

  1. java -cp ".;.\classes;c:\PATHTO\clojure\clojure-1.8.0.jar" clojure.main
  2. com\example\main.clj

请将PATHTO\clojure替换为Clojure JAR文件的路径,并将版本号替换为你安装的版本。如果你使用的是Linux或OS X,还需将类路径目录分隔符;替换为:

Clojure将编译这些代码,并将生成的类文件写入到目录classes中。目录classes必须包含在类路径中,否则编译器将崩溃。如果你查看目录classes,将发现Clojure生成了多个类文件,这是因为Clojure内部基础设施需要一些支持类。你无需关心这些支持类,只需确保在运行应用程序时在类路径中包含它们即可。该来运行这个应用程序了。为此,切换到目录classes并执行如下命令:

  1. java -cp ".;c:\PATHTO\clojure\clojure-1.8.0.jar" com.example.Main

对于这个命令中的Clojure安装路径和目录分隔符,像前面的Java命令一样进行处理。你将看到消息“hi!”。

这个过程之所以复杂,主要是因为需要设置类路径。下一节你将看到,Leiningen让这个过程简单得多。

8.3.2 使用Leiningen编译项目

这次我们让构建工具Leiningen负责处理所有的细节。我们先让Leiningen为应用程序创建空的项目框架,再编译并运行它。

我们首先来新建一个用作项目根目录的目录并确保它为当前目录。为此,执行下面的命令,它根据Leiningen提供的一个模板生成一个空的项目框架:

  1. lein new app testproject2

Leiningen将新建目录testproject2,其中包含基于模板app创建的项目文件。Leiningen还提供了其他模板,如用于创建库的模板(没有指定模板时,默认将使用它)。你还可以创建自定义模板。

请查看生成的目录的内容,其中有可用于存储文档的doc文件。还有用于存储项目源代码文件的目录src/testproject2,这个目录包含文件core.clj,其中包含类似于Hello World的脚本的代码。其他目录包含test和target,分别用于存储单元测试和编译后的文件。在项目的根目录中,有一个名为project.clj的文件,其中包含Leiningen用来构建项目的构建文件,后面将更详细地研究它。

确保当前目录为项目根目录(包含文件project.clj的目录),并执行如下命令来运行项目:

  1. lein run

这将显示“Hello, World”。如你所见,使用Leiningen来运行项目一点都不麻烦,根本不需要手动指定复杂的类路径。最后,我们来编译这个项目。模板app配置了便利的任务uberjar,这个任务不仅将代码编译成类文件,还将它们放到JAR文件中。同样,确保当前目录为项目根目录,并执行如下命令:

  1. lein uberjar

这将创建子目录target/uberjar,其中有两个jar文件,还有包含类文件的子目录classes。一个JAR文件较大,名为testproject-0.1.0-SNAPSHOT-standalone.jar,而另一个则小得多,名为testproject-0.1.0-SNAPSHOT.jar。差别在于较小的版本不包含Clojure运行时库,而较大的版本是个独立的JAR文件,包含所有必要的依赖。请切换到目录target/uberjar,并看看较大的版本能否运行:

  1. java -jar testproject-0.1.0-SNAPSHOT-standalone.jar

你将再次看到问候消息。

有趣的是,注意源代码文件src/testproject2/core.clj并不包含对Clojure函数compile的调用。使用Leiningen的编译任务时,无需调用函数compile,因为Leiningen会在内部处理编译任务。