首页 » Java程序员修炼之道 » Java程序员修炼之道全文在线阅读

《Java程序员修炼之道》12.6 Leiningen

关灯直达底部

要成为对开发人员有用的构建工具,关键是要具备下面这几种能力:

  • 依赖管理;
  • 编译;
  • 测试自动化;
  • 部署打包。

Leiningen对此采取的策略是分而治之。它重用已有的Java技术实现了每种功能,但却没有把所有功能都放在一个包里。

这听上去可能比较复杂,还有点恐怖,但开发人员并不会受到这种复杂性的影响。实际上,甚至没用过底层Java工具的开发人员也能使用Leiningen。我们一开始先通过一个非常简单的过程来安装Leiningen。然后讨论Leiningen的组件和整体架构,最后用Hello World项目来小试牛刀。

你将看到如何开始一个新项目,添加依赖项,使用Leiningen提供的Clojure REPL内部依赖项。这自然会让我们转而讨论如何用Leiningen在Clojure内做TDD。作为本章的收尾,我们会看一下如何打包代码,产生一个应用程序部署或供人调用的类库。

我们来看看如何开始使用Leiningen吧。

12.6.1 Leiningen入门

Leiningen非常容易上手。对于类Unix系统(包括Linux和Mac OS X),开发人员可以从掌握lein脚本开始。在GitHub上可以找到它Leiningen(在https://github.com/页面中或用自己喜欢的搜索引擎搜索Leiningen)。

把lein脚本放到PATH中并设为可执行文件后,它就可以运行了。在第一次运行lein时,它会检查需要安装哪些依赖项(还有哪些已经装上了)。只要需要,它甚至会把其他不属于Leiningen核心部分的组件也给装上。因为要安装依赖项,首次运行可能比后续运行稍慢一些。

在下一节,我们会介绍Leiningen的架构,以及为它提供核心功能的Java技术。

在Windows上安装Leiningen

从一个Unix老黑客的角度来看,Windows的烦人之处是它没有为钟爱命令行的人提供赖以生存的、标准的、简单的工具。比如说,基本的Windows安装中没有通过HTTP下载文件的curl或wget工具(Leiningen需要用它们从Maven资源库中下载jar)。解决办法是用Leiningen Windows安装——带有lein.bat文件和预置的wget.ext压缩文件,为了让自行安装的lein正确工作,需要把它们放到Windows的PATH中的目录下。

12.6.2 Leiningen的架构

我们说过,Leiningen封装了一些主流的Java技术并做了简化。它封装的主要组件是Maven(版本2)、Ant和javac

如图12-16所示,Maven用来做依赖项解析和管理,javac和Ant用来构建、运行测试和完成构建过程中的其他工作。

图12-16 Leiningen及其组件

高级用户可以穿过抽象层,直接使用Leiningen的底层工具。但Leiningen的基本语法和应用非常简单,不需要使用者具备使用任何底层工具的经验。

我们来看一个简单的例子,看看project.clj文件如何工作,以及在Leiningen项目生命周期中如何使用那些基本的命令。

12.6.3 Hello Lein

把lein放在PATH上之后,我们可以用它的new命令开始一个新项目:

ariel:projects boxcat$ lein new hello-leinCreated new project in: /Users/boxcat/projects/hello-leinariel:projects boxcat$ cd hello-lein/ariel:hello-lein boxcat$ lsREADME project.clj src test  

这个命令创建了一个叫做hello-lein的项目。它有项目目录,里面有个简单的描述文件README、一个project.clj文件(马上就会详细讨论),还有并列的src和test目录。

如果你把Leiningen刚刚创建的项目导入Eclipse中(比如用CounterClockwise插件),项目的布局应该如图12-17所示。

图12-17 新创建的Leiningen项目

这个项目结构是直接从Java项目上照搬过来的:有带有core.clj文件的并列test和src结构(分别用于测试和顶层代码)。另外一个重要的文件是project.clj,Leiningen用它来控制构建、保存元数据。

我们来看一下lein的new命令生成的骨架文件。

(defproject hello-lein /"1.0.0-SNAPSHOT/"  :description /"FIXME: write description/"  :dependencies [[org.clojure/clojure /"1.2.1/"]])  

这个Clojure形式解析起来相当直白:有一个(defproject)的宏负责制作表示Leiningen项目的新值。这个宏需要知道项目名称(在这里是hello-lein),还需要知道项目的版本(默认是1.0.0-SNAPSHOT,12.3.1节讨论过的Maven版本号),然后是描述项目的元数据映射。

lein自带了两个元数据:一个描述字符串和一个依赖项向量,后者对于添加新的依赖项很方便。我们现在就来加一个clj-time类库。这个类库为Clojure提供Java日期和时间类库(Joda-Time,但在这个例子中你没必要知道这个Java类库)的访问接口。加上新的依赖项后,project.clj看起来应该是这样的:

(defproject hello-lein /"1.0.0-SNAPSHOT/"    :description /"FIXME: write description/"    :dependencies [[org.clojure/clojure /"1.2.1/"]                  [clj-time /"0.3.0/"]])  

向量的第二个元素描述了要用的新依赖项类库版本。如果Leiningen在本地依赖项资源库中找不到它,会按这个版本从外部资源库获取该依赖项。

Leiningen默认从位于http://clojars.org/的资源库获取缺失的类库。因为Leiningen底层用的是Maven,所以这本质上就是一个Maven资源库。Clojars提供了一个搜索工具,可以在你知道所需类库但不知道具体版本号时提供帮助。

在这个新的依赖项就位后,你需要更新本地构建环境,可以执行lein deps命令。

ariel:hello-lein boxcat$ lein depsDownloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.pom from centralDownloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.pom from clojureDownloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.pom from clojarsTransferring 2K from clojarsDownloading: joda-time/joda-time/1.6/joda-time-1.6.pom from clojureDownloading: joda-time/joda-time/1.6/joda-time-1.6.pom from clojarsTransferring 5K from clojarsDownloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.jar from centralDownloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.jar from clojureDownloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.jar from clojarsTransferring 7K from clojarsDownloading: joda-time/joda-time/1.6/joda-time-1.6.jar from clojureDownloading: joda-time/joda-time/1.6/joda-time-1.6.jar from clojarsTransferring 522K from clojarsCopying 4 files to /Users/boxcat/projects/hello-lein/libariel:hello-lein boxcat$  

Leiningen已经用Maven下载了Clojure的接口,还有底层的Joda-Time JAR。我们在代码中用一下它,展示在依赖项存在的情况下如何用Leiningen作为REPL进行开发。

需要把主要源文件src/hello_lein/core.clj改成下面这样:

(ns hello-lein.core)(use /'[clj-time.core :only (date-time)])(defn isodate-to-millis-since-epoch [x]    (.getMillis (apply date-time (map #(Integer/parseInt %) (.split x /"-/")))))  

它提供了一个Clojure函数,将ISO标准日期(格式为YYYY-MM-DD)转换成自Unix纪元(1970年)以来的毫秒数。

我们用Leiningen的REPL风格测试一下。先在project.clj文件中加上一行,改成下面这样:

(defproject hello-lein /"1.0.0-SNAPSHOT/"    :description /"FIXME: write description/"    :dependencies [[org.clojure/clojure /"1.2.1/"]                   [clj-time /"0.3.0/"]]    :repl-init hello-lein.core)  

加上这一行后,可以启动一个能访问所有依赖项的REPL,并且它已经把命名空间hello-lein.core中的函数引入了作用域:

ariel:hello-lein boxcat$ lein replREPL started; server listening on localhost:10886.hello-lein.core=> (isodate-to-millis-since-epoch /"1970-01-02/")86400000hello-lein.core=>  

这是以天为单位的日期的正确毫秒数,并且它阐明了在真实项目中使用REPL的核心原则。我们在这上面稍微展开一点,再看一个使用Leiningen REPL面向测试的工作方式。

12.6.4 用Leiningen做面向REPL的TDD

任何优秀的TDD方法,其核心都应该是一个用来开发新功能的简单基本的循环。具体到Clojure和Leiningen,其基本循环应该如下所示:

  1. 添加任何所需的新依赖项(并重新运行lein deps);
  2. 启动REPL(lein repl);
  3. 草拟一个新函数,并把它放到REPL的作用域中来;
  4. 在REPL内测试这个函数;
  5. 重复步骤3和4,直到该函数表现正确;
  6. 把该函数的最终版加到恰当的.clj文件上;
  7. 把刚才运行的测试用例加到测试集.clj文件中;
  8. 重启REPL,再次从第三步开始(或者第一步,如果需要新的依赖项)。

这是测试驱动开发的风格,但却避开了先写测试还是先写代码的问题,用REPL风格的TDD,这两件事是同时进行的。

之所以要在添加新函数时重启REPL(第八步),是为了能干净地编译新函数。创建新函数时,有时为了支持它,会对其他函数或环境做轻微的修改。而这些修改在把函数加入最终的源码库时很容易被忘掉。重启REPL能帮我们尽早记起这些被忘掉的修改。

这个过程清晰而简单,但还有个问题我们没提到,无论是在这里还是第11章讨论TDD时,我们都没讨论过怎么编写Clojure测试。好在这非常简单。我们来看一下用lein new创建新项目时它所提供的模板:

(ns hello-lein.test.core    (:use [hello-lein.core])    (:use [clojure.test]))(deftest replace-me ;; FIXME: write    (is false /"No tests have been written./"))  

我们就用 lein test命令来测试这个自动生成的用例,看看会发生什么(实际上你应该能猜出来)。

ariel:hello-lein boxcat$ lein testTesting hello-lein.test.coreFAIL in (replace-me) (core.clj:6)No tests have been written.expected: false    actual: falseRan 1 tests containing 1 assertions.1 failures, 0 errors.  

如你所见,自动生成的测试用例失败了,并且它絮叨着让你写些测试用例。那就写吧,在test文件夹里写个core.clj文件:

(ns hello-lein.test.core    (:use [hello-lein.core])    (:use [clojure.test]))(deftest one-day    (is true        (= 86400000 (isodate-to-millis-since-epoch /"1970-01-02/"))))  

这个测试非常简单:使用了(deftest)宏,给测试命名为(one-day),并且有一个跟断言语句非常相似的形式。Clojure代码的结构使得(is)形式读起来非常自然——几乎就像DSL一样。这个测试可以读作“自1970年1月2日以来的毫秒数等于86 400 000对吗?”我们来看一下这个测试的实际效果:

ariel:hello-lein boxcat$ lein testTesting hello-lein.test.coreRan 1 tests containing 1 assertions.0 failures, 0 errors.  

这里的关键包是clojure.test,它提供了一些在更复杂的环境或需要用到测试固件时用来构建测试用例的形式。如果想了解得更深入,请参考Amit Rathore写的Clojure in Action(Manning, 2011),其中对Clojure中的TDD有全面的论述。

在面向REPL的TDD流程就绪后,现在可以用Clojure构建一个具有相当规模且能用的应用程序了。但你终归要做一些需要跟人分享的东西。好在Leiningen有一些命令可以让你很容易地进行打包和部署。

12.6.5 用Leiningen打包和部署

Leiningen主要提供了两种代码分发办法。这两种办法本质上的区别是带不带依赖项。对应的命令分别是lein jarlein uberjar

我们先看一下lein jar

ariel:hello-lein boxcat$ lein jarCopying 4 files to /Users/boxcat/projects/hello-lein/libCreated /Users/boxcat/projects/hello-lein/hello-lein-1.0.0-SNAPSHOT.jar  

下面这些是被打包进JAR文件中的东西:

ariel:hello-lein boxcat$ jar tvf hello-lein-1.0.0-SNAPSHOT.jar    72 Sat Jul 16 13:38:00 BST 2011 META-INF/MANIFEST.MF  1424 Sat Jul 16 13:38:00 BST 2011 META-INF/maven/hello-lein/hello-lein/pom.xml    105 Sat Jul 16 13:38:00 BST 2011META-INF/maven/hello-lein/hello-lein/pom.properties    196 Fri Jul 15 21:52:12 BST 2011 project.clj    238 Fri Jul 15 21:40:06 BST 2011 hello_lein/core.cljariel:hello-lein boxcat$  

其中最明显的就是Leiningen的基本命令把Clojure源文件,而不是编译后的.class文件发出去了。这是Lisp代码的传统,因为系统的读时组件和宏会因为要处理编译后的代码而受到阻碍。

现在,我们来看看用lein uberjar会发生什么。它所产生的JAR不仅包含代码,还有依赖项。

ariel:hello-lein boxcat$ lein uberjarCleaning up.Copying 4 files to /Users/boxcat/projects/hello-lein/libCopying 4 files to /Users/boxcat/projects/hello-lein/libCreated /Users/boxcat/projects/hello-lein/hello-lein-1.0.0-SNAPSHOT.jarIncluding hello-lein-1.0.0-SNAPSHOT.jarIncluding clj-time-0.3.0.jarIncluding clojure-1.2.1.jarIncluding clojure-contrib-1.2.0.jarIncluding joda-time-1.6.jarCreated /Users/boxcat/projects/hello-lein/hello-lein-1.0.0-SNAPSHOT-standalone.jar  

看到了吧,这个JAR中不仅有代码,还有依赖项,以及依赖项的依赖项,这称为依赖的传递闭包图。也就是说它是一个可以完全独立运行的包。

当然,因为所有依赖项都打包了,所以这也意味着lein uberjar打包的文件要比lein jar的文件大很多。即便是我们这个简单的小例子,其差异也相当鲜明:

ariel:hello-lein boxcat$ ls -lh h*.jar-rw-r--r-- 1 boxcat staff 4.1M 16 Jul 13:46hello-lein-1.0.0-SNAPSHOT-standalone.jar-rw-r--r-- 1 boxcat staff 1.7K 16 Jul 13:46hello-lein-1.0.0-SNAPSHOT.jar  

你可以这样理解lein jarlein uberjar:如果要构建一个类库(构建在其他类库之上),或者要将它作为依赖项,就用lein jar。如果是构建一个最终用户使用的Clojure应用程序,而不是交给用户去扩展的工件,就用lein uberjar

你已经见过用Leiningen如何开始、管理、构建和部署Clojure项目了。Leiningen还有很多内置的实用命令,还有一个强大的插件系统让你可以对它进行定制。想要对Leiningen能做什么有更多了解,只要调用时不带命令就可以了,单用lein

我们在下一章构建Clojure的Web应用时还会遇到Leiningen。