讀古今文學網 > Java程序員修煉之道 > 12.6 Leiningen >

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-lein
Created new project in: /Users/boxcat/projects/hello-lein
ariel:projects boxcat$ cd hello-lein/
ariel:hello-lein boxcat$ ls
README 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 deps
Downloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.pom from central
Downloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.pom from clojure
Downloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.pom from clojars
Transferring 2K from clojars
Downloading: joda-time/joda-time/1.6/joda-time-1.6.pom from clojure
Downloading: joda-time/joda-time/1.6/joda-time-1.6.pom from clojars
Transferring 5K from clojars
Downloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.jar from central
Downloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.jar from clojure
Downloading: clj-time/clj-time/0.3.0/clj-time-0.3.0.jar from clojars
Transferring 7K from clojars
Downloading: joda-time/joda-time/1.6/joda-time-1.6.jar from clojure
Downloading: joda-time/joda-time/1.6/joda-time-1.6.jar from clojars
Transferring 522K from clojars
Copying 4 files to /Users/boxcat/projects/hello-lein/lib
ariel: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 repl
REPL started; server listening on localhost:10886.

hello-lein.core=> (isodate-to-millis-since-epoch \"1970-01-02\")
86400000
hello-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 test
Testing hello-lein.test.core
FAIL in (replace-me) (core.clj:6)
No tests have been written.
expected: false
    actual: false
Ran 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 test
Testing hello-lein.test.core
Ran 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 jar
Copying 4 files to /Users/boxcat/projects/hello-lein/lib
Created /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 2011
META-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.clj
ariel:hello-lein boxcat$
  

其中最明顯的就是Leiningen的基本命令把Clojure源文件,而不是編譯後的.class文件發出去了。這是Lisp代碼的傳統,因為系統的讀時組件和宏會因為要處理編譯後的代碼而受到阻礙。

現在,我們來看看用lein uberjar會發生什麼。它所產生的JAR不僅包含代碼,還有依賴項。

ariel:hello-lein boxcat$ lein uberjar
Cleaning up.
Copying 4 files to /Users/boxcat/projects/hello-lein/lib
Copying 4 files to /Users/boxcat/projects/hello-lein/lib
Created /Users/boxcat/projects/hello-lein/hello-lein-1.0.0-SNAPSHOT.jar
Including hello-lein-1.0.0-SNAPSHOT.jar
Including clj-time-0.3.0.jar
Including clojure-1.2.1.jar
Including clojure-contrib-1.2.0.jar
Including joda-time-1.6.jar
Created /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:46
hello-lein-1.0.0-SNAPSHOT-standalone.jar
-rw-r--r-- 1 boxcat staff 1.7K 16 Jul 13:46
hello-lein-1.0.0-SNAPSHOT.jar
  

你可以這樣理解lein jarlein uberjar:如果要構建一個類庫(構建在其他類庫之上),或者要將它作為依賴項,就用lein jar。如果是構建一個最終用戶使用的Clojure應用程序,而不是交給用戶去擴展的工件,就用lein uberjar

你已經見過用Leiningen如何開始、管理、構建和部署Clojure項目了。Leiningen還有很多內置的實用命令,還有一個強大的插件系統讓你可以對它進行定制。想要對Leiningen能做什麼有更多瞭解,只要調用時不帶命令就可以了,單用lein

我們在下一章構建Clojure的Web應用時還會遇到Leiningen。