使用SBT構建Scala應用

使用SBT構建Scala應用

SBT簡介

SBT是Simple Build Tool的簡稱,如果讀者使用過Maven,那麼可以簡單將SBT看做是Scala世界的Maven,雖然二者各有優劣,但完成的工作基本是類似的。雖然Maven同樣可以管理Scala項目的依賴並進行構建, 但SBT的某些特性卻讓人如此着迷,比如:

  • 使用Scala作爲DSL來定義build文件(one language rules them all);
  • 通過觸發執行(trigger execution)特性支持持續的編譯與測試;
  • 增量編譯;(SBT的增量編譯支持因爲如此優秀,已經剝離爲Zinc,可被Eclipse, Maven,Gradle等使用)
  • 可以混合構建Java和Scala項目;
  • 並行的任務執行;
  • 可以重用Maven或者ivy的repository進行依賴管理;

SBT項目工程結構詳解

一般意義上講,SBT工程項目的目錄結構跟Maven的很像, 如果讀者接觸過Maven,那麼可以很容易的理解如下內容。

一個典型的SBT項目工程結構如下圖所示:
├── .idea
├── project
├── src
├── target
└── build.sbt

src目錄詳解

Maven用戶對src目錄的結構應該不會感到陌生,下面簡單介紹各個子目錄的作用。

  • src/main/java目錄存放Java源代碼文件
  • src/main/resources目錄存放相應的資源文件
  • src/main/scala目錄存放Scala源代碼文件
  • src/test/java目錄存放Java語言書寫的測試代碼文件
  • src/test/resources目錄存放測試起見使用到的資源文件
  • src/test/scala目錄存放scala語言書寫的測試代碼文件

build.sbt詳解

讀者可以簡單的將build.sbt文件理解爲Maven項目的pom.xml文件,它是build定義文件。 SBT運行使用兩種形式的build定義文件,一種就是放在項目的根目錄下,即build.sbt, 是一種簡化形式的build定義; 另一種放在project目錄下,採用純Scala語言編寫,形式更加複雜,當然,也更完備,更有表現力。

我們暫時先介紹build.sbt的定義格式,基於scala的build定義格式我們稍後再細說。

一個簡單的build.sbt文件內容如下:

name := "hello"      // 項目名稱

organization := "xxx.xxx.xxx"  // 組織名稱

version := "0.0.1-SNAPSHOT"  // 版本號

scalaVersion := "2.9.2"   // 使用的Scala版本號

// 其它build定義

其中, name和version的定義是必須的,因爲如果想生成jar包的話,這兩個屬性的值將作爲jar包名稱的一部分。

build.sbt的內容其實很好理解,可以簡單理解爲一行代表一個鍵值對(Key-Value Pair),各行之間以空行相分割。

當然,實際情況要比這複雜,需要理解SBT的Settings引擎纔可以完全領會, 以上原則只是爲了便於讀者理解build.sbt的內容。

除了定義以上項目相關信息,我們還可以在build.sbt中添加項目依賴:

// 添加源代碼編譯或者運行期間使用的依賴
libraryDependencies += "ch.qos.logback" % "logback-core" % "1.0.0"

libraryDependencies += "ch.qos.logback" % "logback-classic" % "1.0.0"

// 或者

libraryDependencies ++= Seq(
                            "ch.qos.logback" % "logback-core" % "1.0.0",
                            "ch.qos.logback" % "logback-classic" % "1.0.0",
                            ...
                            )

// 添加測試代碼編譯或者運行期間使用的依賴
libraryDependencies ++= Seq("org.scalatest" %% "scalatest" % "1.8" % "test") 

甚至於直接使用ivy的xml定義格式:

ivyXML :=
  <dependencies>
    <dependency org="org.eclipse.jetty.orbit" name="javax.servlet" rev="3.0.0.v201112011016">
        <artifact name="javax.servlet" type="orbit" ext="jar"/>
    </dependency>
    <exclude module="junit"/>
    <exclude module="activation"/>
    <exclude module="jmxri"/>
    <exclude module="jmxtools"/>
    <exclude module="jms"/>
    <exclude module="mail"/>
  </dependencies>

在這裏,我們排除了某些不必要的依賴,並且聲明瞭某個定製過的依賴聲明。

當然, build.sbt文件中還可以定義很多東西,比如添加插件,聲明額外的repository,聲明各種編譯參數等等,我們這裏就不在一一贅述了。

project目錄即相關文件介紹

project目錄下的幾個文件實際上都是非必須存在的,可以根據情況添加。

__build.properties__文件聲明使用的要使用哪個版本的SBT來編譯當前項目, 最新的sbt boot launcher可以能夠兼容編譯所有0.10.x版本的SBT構建項目,比如如果我使用的是0.12版本的sbt,但卻想用0.11.3版本的sbt來編譯當前項目,則可以在build.properties文件中添加sbt.version=0.11.3來指定。 默認情況下,當前項目的構建採用使用的sbt boot launcher對應的版本。

__plugins.sbt__文件用來聲明當前項目希望使用哪些插件來增強當前項目使用的sbt的功能,比如像assembly功能,清理ivy local cache功能,都有相應的sbt插件供使用, 要使用這些插件只需要在plugins.sbt中聲明即可,不用自己去再造輪子:

resolvers += Resolver.url("git://github.com/jrudolph/sbt-dependency-graph.git")

resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/"

addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.1.0")

addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.6.0")

在筆者的項目中, 使用sbt-idea來生成IDEA IDE對應的meta目錄和文件,以便能夠使用IDEA來編寫項目代碼; 使用sbt-dependency-graph來發現項目使用的各個依賴之間的關係;

爲了能夠成功加載這些sbt插件,我們將他們的查找位置添加到resolovers當中。有關resolvers的內容,我們後面將會詳細介紹,這裏注意一個比較有趣的地方就是,sbt支持直接將相應的github項目作爲依賴或者插件依賴,而不用非得先將相應的依賴或者插件發佈到maven或者ivy的repository當中纔可以使用。

其它

以上目錄和文件通常是在創建項目的時候需要我們創建的,實際上, SBT還會在編譯或者運行期間自動生成某些相應的目錄和文件,比如SBT會在項目的根目錄下和project目錄下自動生成相應的target目錄,並將編譯結果或者某些緩存的信息置於其中, 一般情況下,我們不希望將這些目錄和文件記錄到版本控制系統中,所以,通常會將這些目錄和文件排除在版本管理之外。

比如, 如果我們使用git來做版本控制,那麼就可以在.gitignore中添加一行"target/"來排除項目根目錄下和project目錄下的target目錄及其相關文件。

TIPS

在sbt0.7.x時代, 我們只要創建項目目錄,然後在項目目錄下敲入sbt,則應該創建哪些需要的目錄和文件就會由sbt自動爲我們生成, 而sbt0.10之後,這項福利就沒有了。 所以,剛開始,我們可能會認爲要很苦逼的執行一長串命令來生成相應的目錄和文件:

$ touch build.sbt
$ mkdir src
$ mkdir src/main
$ mkdir src/main/java
$ mkdir src/main/resources
$ mkdir src/main/scala
$ mkdir src/test
$ mkdir src/test/java
$ mkdir src/test/resources
$ mkdir src/test/scala
$ mkdir project
$ ...
> 如果是Maven的用戶,是不是很想念Maven的Archetype特性那?! > 其實, SBT爲我們關了一扇窗,卻開了另一道門, 我們可以使用giter8來自動化以上步驟。 > giter8可以自動從github上抓取.g8項目模板,並自動在本地生成相應的項目結構, 比如筆者在github上創建了xsbt.g8項目模板,則直接執行`"g8 fujohnwang/xsbt"`就可以在本地自動生成一個sbt的項目。 有關giter8的更多信息可參考

SBT的使用

SBT支持兩種使用方式:

  1. 批處理模式(batch mode)
  2. 可交互模式(interactive mode)

批處理模式是指我們可以在命令行模式下直接依次執行多個SBT命令, 比如:

$ sbt compile test package

而可交互模式則直接運行sbt,後面不跟任何SBT命令,在這種情況下, 我們將直接進入sbt控制檯(console), 在sbt控制檯中,我們可以輸入任何合法的sbt命令並獲得相應的反饋:

$ sbt
> compile
[success] Total time: 1 s, completed Sep 3, 2012 9:34:58 PM
> test
[info] No tests to run for test:test
[success] Total time: 0 s, completed Sep 3, 2012 9:35:04 PM
> package
[info] Packaging XXX_XXX_2.9.2-0.1-SNAPSHOT.jar ...
[info] Done packaging.
[success] Total time: 0 s, completed Sep 3, 2012 9:35:08 PM

TIPS

在可交互模式的sbt控制檯下,可以輸入help獲取進一步的使用信息。

在以上實例中,我們依次執行了compile, test和package命令, 實際上, 這些命令之間是有依賴關係的,如果僅僅是爲了package,那麼,只需要執行package命令即可, package命令依賴的compile和test命令將先於package命令執行,以保證它們之間的依賴關係得以滿足。

除了compile,test和package命令, 下面列出了更多可用的sbt命令供讀者參考:

  • compile
  • test-compile
  • run
  • test
  • package

這些命令在某些情況下也可以結合SBT的觸發執行(Trigger Execution)機制一起使用, 唯一需要做的就只是在相應的命令前追加~符號,實際上,這個特性是讓筆者最着迷的, 比如:

$ sbt ~compile

以上命令意味着, 我更改了任何源代碼並且保存之後,將直接觸發SBT編譯相應的源代碼以及相應的依賴變更。 假如我們有2個顯示器, 左邊是命令行窗口,右邊是編輯器或者IDE窗口,那麼,我們只要在右邊的顯示器中編輯源代碼,左邊的顯示器就可以實時的反饋編譯結果, 這將極大加快開發的迭代速度, 聽起來並且看起來是不是很cool?!
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-l0noxgr9-1588741492376)(images/dual_screen_on_incremental_compile.jpg)]

NOTE

原則上, ~和相應命令之間應該用空格分隔,不過對於一般的命令來講,直接前綴~也是可以的,就跟我們使用~compile的方式一樣。

SBT的依賴管理

在SBT中, 類庫的依賴管理可以分爲兩類:

  1. unmanaged dependencies
  2. managed dependencies

大部分情況下,我們會採用managed dependencies方式來管理依賴關係,但也不排除爲了快速構建項目環境等特殊情況下,直接使用unmanaged dependencies來管理依賴關係。

Unmanaged Dependencies簡介

要使用unmanaged dependencies的方式來管理依賴其實很簡單,只需要將想要放入當前項目classpath的jar包放到__lib__目錄下即可。

如果對默認的lib目錄看着不爽, 我們也可以通過配置來更改這個默認位置,比如使用3rdlibs:

unmanagedBase <<= baseDirectory { base => base / "3rdlibs" }

這裏可能需要解釋一下以上配置。 首先unmanagedBase這個Key用來表示unmanaged dependencies存放第三方jar包的路徑, 具體的值默認是__lib__, 我們爲了改變這個Key的值, 採用<<=操作符, 根據baseDirectory的值轉換並計算出一個新值賦值給unmanagedBase這個Key, 其中, baseDirectory指的是當前項目目錄,而<<=操作符(其實是Key的方法)則負責從已知的某些Key的值計算出新的值並賦值給指定的Key。

關於Unmanaged dependencies,一般情況下,需要知道的基本上就這些。

Managed Dependencies詳解

sbt的managed dependencies採用Apache Ivy的依賴管理方式, 可以支持從Maven或者Ivy的Repository中自動下載相應的依賴。

簡單來說,在SBT中, 使用managed dependencies基本上就意味着往__libraryDependencies__這個Key中添加所需要的依賴, 添加的一般格式如下:

libraryDependencies += groupID % artifactID % revision

比如:

libraryDependencies += “org.apache.derby” % “derby” % “10.4.1.3”

這種格式其實是簡化的常見形式,實際上,我們還可以做更多微調, 比如:

(1) libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % "test"
(2) libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" exclude("org", "artifact")
(3) libraryDependencies += "org.apache.derby" %% "derby" % "10.4.1.3" 

(1)的形式允許我們限定依賴的範圍只限於測試期間; (2)的形勢允許我們排除遞歸依賴中某些我們需要排除的依賴; (3)的形式則會在依賴查找的時候,將當前項目使用的scala版本號追加到artifactId之後作爲完整的artifactId來查找依賴,比如如果我們的項目使用scala2.9.2,那麼(3)的依賴聲明實際上等同於"org.apache.derby" %% "derby_2.9.2" % "10.4.1.3",這種方式更多是爲了簡化同一依賴類庫存在有多個Scala版本對應的發佈的情況。

如果有一堆依賴要添加,一行一行的添加是一種方式,其實也可以一次添加多個依賴:

libraryDependencies ++= Seq("org.apache.derby" %% "derby" % "10.4.1.3",
                            "org.scala-tools" %% "scala-stm" % "0.3", 
                            ...)

Resovers簡介

對於managed dependencies來說,雖然我們指定了依賴哪些類庫,但有沒有想過,SBT是如何知道到哪裏去抓取這些類庫和相關資料那?!

實際上,默認情況下, SBT回去默認的Maven2的Repository中抓取依賴,但如果默認的Repository中找不到我們的依賴,那我們可以通過resolver機制,追加更多的repository讓SBT去查找並抓取, 比如:

resolvers += “Sonatype OSS Snapshots” at “https://oss.sonatype.org/content/repositories/snapshots”

at1之前是要追加的repository的標誌名稱(任意取),at後面則是要追加的repository的路徑。

除了可遠程訪問的Maven Repo,我們也可以將本地的Maven Repo追加到resolver的搜索範圍:

resolvers += “Local Maven Repository” at “file://”+Path.userHome.absolutePath+"/.m2/repository"

SBT進階篇

.scala形式的build定義

對於簡單的項目來講,.sbt形式的build定義文件就可以滿足需要了,但如果我們想要使用SBT的一些高級特性,比如自定義Task, 多模塊的項目構建, 就必須使用.scala形式的build定義了。 簡單來講,.sbt能幹的事情,.scala形式的build定義都能幹,反之,則不然。

要使用.scala形式的build定義,只要在當前項目根目錄下的project/子目錄下新建一個.scala後綴名的scala源代碼文件即可,比如Build.scala(名稱可以任意,一般使用Build.scala):

import sbt._
import Keys._

object HelloBuild extends Build {
	override lazy val settings = super.settings ++ Seq(..)
	
	lazy val root = Project(id = "hello",
                            base = file("."),
                            settings = Project.defaultSettings ++ Seq(..))
}

build的定義只要擴展sbt.Build,然後添加相應的邏輯即可,所有代碼都是標準的scala代碼,在Build定義中,我們可以添加更多的settings, 添加自定義的task,添加相應的val和方法定義等等, 更多代碼實例可以參考SBT Wiki(https://github.com/harrah/xsbt/wiki/Examples),另外,我們在後面介紹SBT的更多高級特性的時候,也會引入更多.scala形式的build定義的使用。

NOTE

.sbt和.scala之間不得不說的那些事兒

實際上, 兩種形式並不排斥,並不是說我使用了前者,就不能使用後者,對於某些單一的項目來說,我們可以在.sbt中定義常用的settings,而在.scala中定義自定義的其它內容, SBT在編譯期間,會將.sbt中的settings等定義與.scala中的定義合併,作爲最終的build定義使用。

只有在多模塊項目構建中,爲了避免多個.sbt的存在引入過多的繁瑣,纔會只用.scala形式的build定義。

.sbt和.scala二者之間的settings是可互相訪問的, .scala中的內容會被import到.sbt中,而.sbt中的settings也會被添加到.scala的settings當中。默認情況下,.sbt中的settings會被納入Project級別的Scope中,除非明確指定哪些Settings定義的Scope; .scala中則可以將settings納入Build級別的Scope,也可以納入Project級別的Scope。

SBT項目結構的本質

在瞭解了.sbt和.scala兩種形式的build定義形式之後, 我們就可以來看看SBT項目構建結構的本質了。

首先, 一個SBT項目,與構建相關聯的基本設施可以概況爲3個部分, 即:

  1. 項目的根目錄, 比如hello/, 用來界定項目構建的邊界;
  2. 項目根目錄下的*.sbt文件, 比如hello/build.sbt, 用來指定一般性的build定義;
  3. 項目根目錄下的project/*.scala文件, 比如hello/project/Build.scala, 用來指定一些複雜的, *.sbt形式的build定義文件不太好搞的設置;

也就是說, 對於一個SBT項目來說,SBT在構建的時候,只關心兩點:

  1. build文件的類型(是*.sbt還是*.scala);
  2. build文件的存放位置(*.sbt文件只有存放在項目的根目錄下, SBT纔會關注它或者它們, 而*.scala文件只有存放在項目根目錄下的project目錄下,SBT纔不會無視它或者它們)2

在以上基礎規則的約束下,我們來引入一個推導條件, 即:

項目的根目錄下的project/目錄,其本身也是一個標準的SBT項目。

在這個條件下,我們再來仔細分析hello/project/目錄,看它目錄下的各項artifacts到底本質上應該是什麼。

我們說項目根目錄下的project/子目錄下的*.scala文件是當前項目的build定義文件, 而根據以上的推導條件, project/目錄本身又是一個SBT項目,我們還知道,SBT下面下的*.scala都是當前項目的源代碼,所以project/下的*.scala文件, 其實都是project這個目錄下的SBT項目的源代碼,而這些源代碼中,如果有人定義了sbt.Build,那麼就會被用作project目錄上層目錄界定的SBT項目的build定義文件, right?!

那麼,來想一個問題,如果project/目錄下的*.scala是源代碼文件,而project目錄整體又是一個標準的SBT項目, 假如我們這些*.scala源碼文件中需要依賴其他三方庫,通常我們會怎麼做?

對, 在當前項目的根目錄下新建一個build.sbt文件,將依賴添加進去,所以,我們就有了如下的項目結構:

hello/
	*.scala
	build.sbt
	project/
		*.scala
		build.sbt

也就是說,我們可以在書寫當前項目的build定義的時候(因爲build定義也是用scala來寫),借用第三方依賴來完成某些工作,而不用什麼都重新去寫,在project/build.sbt下添加項目依賴,那麼就可以在project/*.scala裏面使用,進而構建出hello/項目的build定義是什麼, 即hello/project/這個SBT項目,支撐了上一層hello/這個項目的構建!

現在再來想一下,如果hello/project/這個項目的構建要用到其它SBT特性,比如自定義task或者command啥的,我們該怎麼辦?!

既然hello/project/也是一個SBT項目,那麼按照慣例,我們就可以再其下再新建一個project/目錄,在這個下一層的project/目錄下再添加*.scala源文件作爲hello/project/這個SBT項目的build定義文件, 整個項目又變成了:

hello/
	*.scala
	build.sbt
	project/
		*.scala
		build.sbt
		/project
			*.scala

而如果hello/project/project/下的源碼又要依賴其他三方庫那?! God, 再添加*.sbt或更深一層的project/*.scala!

也就是說, 從第一層的項目根目錄開始, 其下project/目錄內部再嵌套project/目錄,可以無限遞歸,而且每一層的project/目錄都界定了一個SBT項目,而每一個下層的project目錄界定的SBT項目其實都是對上一層的SBT項目做支持,作爲上一層SBT項目的build定義項目,這就跟俄羅斯娃娃這種玩具似的, 遞歸嵌套,一層又包一層:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-7zDDZ5JM-1588741492379)(images/matpewka_doll.png)]

一般情況下,我們不會搞這麼多嵌套,但理解了SBT項目的這個結構上的本質,可以幫助我們更好的理解後面的內容,如果讀者看一遍沒能理解,那不妨多看幾次,多參考其他資料,多揣摩揣摩吧!

自定義SBT Task

大部分情況下,我們都是使用SBT內建的Task,比如compile, run等,實際上, 除了這些,我們還可以在build定義中添加更多自定義的Task。

自定義SBT的Task其實很簡單,就跟把大象關冰箱裏一樣簡單, 概況來說其實就是:

  1. 定義task;
  2. 將task添加到項目的settings當中;
  3. 使用自定義的task;

定義task

Task的定義分兩部分,第一部分就是要定義一個TaskKey來標誌Task, 第二部分則是定義Task的執行邏輯。

假設我們要定義一個簡單的打印"hello, sbt~"信息的task,那第一步就是先定義它的Key,如下代碼所示:

val hello = TaskKey[Unit]("hello", "just say hello")

TaskKey的類型指定了對應task的執行結果,因爲我們只想打印一個字符串,不需要返回什麼數據,所以定義的是TaskKey[Unit]。 定義TaskKey最主要的一點就是要指定一個名稱(比如第一個參數“hello”),這個名稱將是我們調用該task的標誌性建築。 另外,還可以可選擇的通過第二個參數傳入該task的相應描述和說明。

有了task對應的Key之後,我們就要定義task對應的執行邏輯,並通過:=方法將相應的key和執行邏輯定義關聯到一起:

hello := {
	println("hello, sbt~")
}

完整的task定義代碼如下所示:

val hello = TaskKey[Unit]("hello", "just say hello")

hello := {
	println("hello, sbt~")
}
NOTE

:= 只是簡單的將task的執行邏輯和key關聯到一起, 如果之前已經將某一執行邏輯跟同一key關聯過,則後者將覆蓋前者,另外,如果我們想要服用其他的task的執行邏輯,或者依賴其他task,只有一個:=就有些力不從心了。這些情況下,可以考慮使用~=或者<<=等方法,他們可以藉助之前的task來映射或者轉換新的task定義。比如(摘自sbt wiki):
// These two settings are equivalent
intTask <<= intTask map { (value: Int) => value + 1 }
intTask ~= { (value: Int) => value + 1 }

將task添加到項目的settings當中

光完成了task的Key和執行邏輯定義還不夠,我們要將這個task添加到項目的Settings當中才能使用它,所以,我們稍微對之前的代碼做一補充:

object ProjectBuild extends Build {

  val hello = TaskKey[Unit]("hello", "just say hello")

  val helloTaskSetting = hello := {
    println("hello, sbt~")
  }

  lazy val root = Project(id = "", base = file(".")).settings(Defaults.defaultSettings ++ Seq(helloTaskSetting): _*)

}

將Key與task的執行邏輯相關聯的過程實際上是構建某個Setting的過程,雖然我們也可以將以上定義寫成如下形式:

  lazy val root = Project(id = "", base = file(".")).settings(Defaults.defaultSettings ++ Seq(hello := {
    println("hello, sbt~")
  }): _*)

但未免代碼就太不雅觀,也不好管理了(如果要添加多個自定義task,想想,用這種形式是不是會讓代碼醜陋不堪那?!),所以,我們引入了helloTaskSetting這個標誌常量來幫助我們淨化代碼結構 :)

測試和運行定義的task

萬事俱備之後,就可以使用我們的自定義task了,使用定義Key的時候指定的task名稱來調用它即可:

$ sbt hello
hello, sbt~
// 或者
$ sbt
> hello
hello, sbt~
[success] Total time: 0 s, completed Oct 4, 2012 2:48:48 PM

怎麼樣? 在SBT中自定義task是不是很簡單那?!

SBT Plugins

每個項目最終都要以相應的形式發佈3,比如二進制包, 源碼包,甚至直接可用的部署包等等, 假設我們想把當前的SBT項目打包成可直接解壓部署的形式,我們可以使用剛剛介紹的自定義task來完成這一工作:

object ProjectBuild extends Build {

  import Tasks._

  lazy val root = Project(id = "", base = file(".")).settings(Defaults.defaultSettings ++ Seq(distTask, helloTaskSetting): _*)

}

object Tasks {

  val hello = TaskKey[Unit]("hello", "just say hello")

  val helloTaskSetting = hello := {
    println("hello, sbt~")
  }
  
  val dist = TaskKey[Unit]("dist", "distribute current project as zip or gz packages")

  val distTask = dist <<= (baseDirectory, target, fullClasspath in Compile, packageBin in Compile, resources in Compile, streams) map {
    (baseDir, targetDir, cp, jar, res, s) =>
      s.log.info("[dist] prepare distribution folders...")
      val assemblyDir = targetDir / "dist"
      val confDir = assemblyDir / "conf"
      val libDir = assemblyDir / "lib"
      val binDir = assemblyDir / "bin"
      Array(assemblyDir, confDir, libDir, binDir).foreach(IO.createDirectory)

      s.log.info("[dist] copy jar artifact to lib...")
      IO.copyFile(jar, libDir / jar.name)

      s.log.info("[dist] copy 3rd party dependencies to lib...")
      cp.files.foreach(f => if (f.isFile) IO.copyFile(f, libDir / f.name))

      s.log.info("[dist] copy shell scripts to bin...")
      ((baseDir / "bin") ** "*.sh").get.foreach(f => IO.copyFile(f, binDir / f.name))

      s.log.info("[dist] copy configuration templates to conf...")
      ((baseDir / "conf") * "*.xml").get.foreach(f => IO.copyFile(f, confDir / f.name))

      s.log.info("[dist] copy examples chanenl deployment...")
      IO.copyDirectory(baseDir / "examples", assemblyDir / "examples")

      res.filter(_.name.startsWith("logback")).foreach(f => IO.copyFile(f, assemblyDir / f.name))
  }
}

這種方式好是好,可就是不夠通用,你我應該都不想每個項目裏的Build文件裏都拷貝粘帖一把這些代碼吧?! 況且, 哪些artifacts要打包進去,打包之前哪些參數可以調整,以這種形式來看,都不方便調整(如果你不煩每次都添加修改代碼的話), 那SBT有沒有更好的方式來支持類似的需求那?! 當然有咯, SBT的Plugins機制就是爲此而生的!

SBT Plugin簡介

SBT Plugin機制允許我們擴展SBT項目的build定義, 這裏的擴展基本可以理解爲允許我們向項目的build定義裏添加各種所需的Settings, 比如自定義Task,瞅一眼Plugin的代碼就更明瞭了:

trait Plugin {
  def settings: Seq[Setting[_]] = Nil
}

我們知道如果項目位於hello目錄下的話, 該項目的build定義可以位於hello/\*.sbt或者hello/project/*.scala兩種位置,既然Plugin是對build定義的擴展,那麼, 我們就可以認爲項目的build定義依賴這些plugin的某些狀態或者行爲,即plugin屬於項目build定義的某種依賴,從這個層次來看,plugin的配置和使用跟library dependency的配置和使用是一樣的(具體有稍微的差異)。不過,既然plugin是對build定義的擴展(及被依賴),那麼,我們應該在build定義對應的SBT項目的build定義中配置它(聽起來是不是有些繞? 如果讀者感覺繞,看不明白的話,不妨回頭看看"SBT項目結構的本質"一節的內容),即hello/project/\*.sbt或者hello/project/project/\*.scala, 大多數情況下,我們會直接在像hello/project/plugins.sbt4配置文件中配置和添加Plugin:

resolvers += Resolver.url("git://github.com/jrudolph/sbt-dependency-graph.git")

resolvers += "sbt-idea-repo" at "http://mpeltonen.github.com/maven/"

addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.1.0")

addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.6.0")

因爲Plugin實現一般也都是以三方包的形式發佈的,addSbtPlugin所做的事情實際上就是根據Plugin發佈時使用的artifactId等標誌性信息5,將它們轉換成Setting添加到當前項目build定義中。

如果Plugin是發佈到SBT默認會查找的Maven或者Ivy Repository中,則只需要addSbtPlugin就行了, 否則, 需要將Plugin發佈到的Repository添加到resolvers以便SBT可以發現並加載成功。

有哪些現成的Plugin可以用嗎?

在前面的配置實例中,我們已經看到兩個筆者常用的Plugin:

  1. sbt-idea
    • 筆者使用IntelliJ IDEA來開發scala應用, sbt-idea這個插件可以幫助我從SBT的配置中生成IDEA這個IDE對應的classpath,項目信息等元數據, 這樣,我只要運行sbt gen-idea這一命令之後,就可以在IDEA中直接打開當前的SBT項目了。如果讀者使用其他的IDEA,那也可以使用類似的插件,比如sbteclipse或者sbt-netbeans-plugin
  2. sbt-dependency-graph
    • 項目的依賴越多, 依賴關係就越複雜, 這個插件可以幫助我們理清楚項目各個依賴之間的關係,完成跟Maven的dependency:tree類似的功能

除了這些, 讀者還可以在SBT的Plugins List中找到更多有用的Plugin,比如:

  1. xsbt-web-plugin

    • 看名字就能猜到是幹啥的了
  2. sbt-assembly

    • 可以將當前項目的二進制包以及依賴的所有第三方庫都打包成一個jar包發佈,即one-jar, 對於那種直接運行的應用程序很方便
  3. sbt-aether-deploy

    • 使用aether來部署當前項目, aethor是Maven中管理Repository的API,現在單獨剝離出來更通用了
  4. sbt-dirty-money

    • SBT會將項目的依賴抓取到本地的ivy cache中緩存起來,避免頻繁的update對帶寬造成的浪費,但有些時候需要對緩存裏失效的內容進行清理,使用這個插件可以避免自己手動遍歷目錄逐一刪除相應的artifacts
      關於現成可用的Plugin就介紹這些,更多的好東西還是有待讀者自己去發掘吧!

    TIPS

    如果某些SBT Plugin個人經常用到,那麼,可以將這些Plugin配置爲Global plugin, 即在用戶的home目錄下的".sbt/plugins/"目錄下新建一個plugins.sbt文件(名稱無所謂,類型有所謂,你懂的哦),然後將這些常用的插件配置進去,之後,在任何的SBT項目下,就都可以使用這些插件了,“配置一次,到處運行”!

    不過, 筆者建議, 跟項目相關的SBT Plugin還是應該配置到當前項目中,這樣,走版本控制,別人都可統一使用這些Plugin,只有哪些自己常用,而與具體項目綁定關係不是很強的Plugin才配置爲global的plugin, 道理跟git和svn處理ignore的做法差異是類似的!

想寫個自己的SBT Plugin該咋整?!

編寫一個自己的SBT Plugin其實並不複雜,一個SBT Plugin工程跟一般的SBT項目並無質上的差異,唯一的差別就在於我們需要在SBT Plugin項目的build定義中指定一項Setting用來表明當前項目是一個SBT Plugin項目,而不是一個一般的SBT項目,這項Setting即:

sbtPlugin := true

有了這項Setting, SBT在編譯和發佈當前這個Plugin項目的時候就會做兩個事情:

  1. 將SBT API加入當前項目的classpath中(這樣我們就可以在編寫Plugin的時候使用到SBT的API);
  2. 在發佈(publish-local, publish)當前項目的時候,SBT會搜尋sbt.Plugin的實現,然後將這些實現添加到sbt/sbt.plugins這個文件中,並將這個文件與當前Plugin項目的其它artifacts一起打包到jar中發佈;

Plugin項目發佈之後,就可以在其他項目中引用它們,怎麼用,前面詳細介紹過了,這裏不再贅述。

有了編寫SBT Plugin理論指導,我們就可以着手實踐了, 我們先把hello這個自定義task轉換爲Plugin實現如下:

// HelloSBT.scala

import sbt._

object HelloSBT extends Plugin {
  val helloSbt = TaskKey[Unit]("hello-sbt", "just say hello")

  val helloSbtSetting = helloSbt := {
    println("hello, sbt~")
  }
}

然後,我們爲其配置build定義:

// ${project.root}/build.sbt

name := "hello_sbt_plugin"

organization := "com.github.fujohnwang"

version := "0.0.1"

sbtPlugin := true

scalaVersion := "2.9.2"

編譯併發布到本地的ivy庫中(測試無誤後,可以直接發佈到其他共享範圍更大的repo中),執行:

sbt publish-local

之後,我們就可以在其他SBT項目的build定義中使用到這個SBT Plugin了,比如我們將其添加到某個SBT項目的${project.root}/project/plugins.sbt(名稱不重要,還記得吧? 注意路徑):

addSbtPlugin("com.github.fujohnwang" % "hello_sbt_plugin" % "0.0.1")

並且將__helloSbtSetting__手動配置到${project.root}/build.sbt中:

HelloSBT.helloSbtSetting

好啦,現在算是萬事大吉了,在當前SBT項目直接運行sbt hello-sbt試試吧!

理論和實踐我們都闡明瞭,現在該說一下在編寫SBT Plugin的時候應該注意的一些事情了。

首先,回頭查看HelloSBT的代碼實現,讀者應該發現,我們將SettingKey的名稱和變量名都做了改變,這是因爲我們在編寫Plugin的時候,要儘量避免命名衝突,所以,通過引入當前Plugin的名稱前綴來達到這一目的;

其次,我們沒有將helloSbtSetting加入到Plugin.settings(這裏同樣注意添加了前綴的命名方式),這就意味着,我們在使用這一Plugin的時候,要手動將這一Setting加到目標項目的build定義中才能使用到它,原因在於,雖然我們可以override並將helloSbtSetting加入Plugin.settings,這樣可以讓SBT自動加載到當前項目,但一般情況下我們不會這樣做,因爲在多模塊的項目中,這一setting也會自動加載到所有項目上,除非是command類型的Plugin,否則,這種行爲是不合適的, 故此,大部分Plugin實現都是提供自己的Setting,並讓用戶決定是否加載使用;

其實,編寫一個SBT Plugin還要注意很多東西,但這裏就不一一列舉了,大家可以參考Best PracticesPlugins Best Practices這兩份SBT Wiki文檔,裏面詳細說明了編寫SBT Plugin的一些最佳實踐,不過,作爲結束,我補充最基本的一點, 即"不要硬編碼Plugin實現的任何配置"! 讀者可以嘗試將dist自定義task轉換成一個SBT Plugin,在轉換過程中,不妨爲"dist"啦, "conf"啦, "bin"啦這些目標目錄設立相應的SettingKey並給予默認值,這樣就不會像我們的自定義task裏似的,直接硬編碼這些目錄名稱了,而且,插件的使用者也可以在使用插件的項目中通過override相應的Plugin的這些SettingKey標誌的Setting來提供自定義的值, 怎麼樣? 動手嘗試一把?!

NOTE

要編寫一個SBT Plugin還需要修煉一下SBT的內功,包括搞清楚SBT的Setting系統,Configuration,Command等深層次概念, 這樣,在編寫SBT Plugin的時候纔不會感覺“侷促”,^_^

多模塊工程管理(Multi-Module Project)

對於Maven用戶來說, 多模塊的工程管理早就不是什麼神祕的特性了吧?! 但筆者實際上對於這種工程實踐卻一直持保留意見,因爲很多時候,架構項目結構的人並沒有很好的理解項目中各種實體的粒度與邊界之間的合理關係, 很多明明在package層次/粒度可以搞定的事情也往往被納入到了子工程的粒度中去,這種不合適的粒度和邊界選擇,一方面反映了最初規劃項目結構的人對自身項目的理解不足,另一方面也會爲後繼的開發和維護人員帶來些許的繁瑣。所以很多時候,如果某些關注點足以設立一個項目來管理,那我寧願直接爲其設立獨立的項目結構,然後讓需要依賴的項目依賴它即可以了(大部分時候,我們要解決的就是各個項目之間的依賴關係,不是嗎?),當然, 這種做法並非絕對,只是更多的在強調粒度和邊界選擇的合理性上。

扯多了,現在讓我們來看看在SBT中我們是如何來規劃和組織多模塊的項目結構的吧!

包含多個子模塊或者子項目的SBT項目跟一個標準的獨立的SBT項目相差不多,唯一的差別在於:

  1. build定義中多了對多個子模塊/工程的關係的描述;
  2. 項目的根目錄下多了多個子模塊/工程相應的目錄;

下面是一個多模塊工程的典型結構:

${project.root}
	- build.sbt
	+ src/main/scala
	+ project
		- Build.scala
	+ module1
		- build.sbt
		+ src/main/scala
	+ module2
		- build.sbt
		+ src/main/scala
	+ module3
		- build.sbt
		+ src/main/scala

我們可以發現,除了多了各個子模塊/工程相應的目錄,其它方面跟一個標準獨立的SBT項目無異, 這些子模塊/工程與當前項目或者其它子模塊/工程之間的關係由當前項目的build定義來“說明”, 當然這種關係的描述是如此的“糾纏”,只能在*.scala形式的build定義中聲明, 例如在${project.root}/project/Build.scala)中我們可以簡單的定義多個子模塊/工程之間的關係如下:

import sbt._
import Keys._

object MultipleModuleProjectBuild extends Build {
	lazy val root = Project(id = "root",
                            base = file(".")) aggregate(sub1, sub2)
	lazy val sub1 = Project(id = "m1",
                            base = file("module1"))
	lazy val sub2 = Project(id = "m2",
                            base = file("module2")) dependsOn(sub3) 
	lazy val sub3 = Project(id = "m3",
                            base = file("module3")) 
}

在當前項目的build定義中,我們聲明瞭多個Project實例來對應相應的子項目,並通過Porject的aggregate和dependsOn來進一步申明各個項目之間的關係。 aggregate指明的是一種並行的相互獨立的行爲,只是這種行爲是隨父項目執行相應動作而觸發的, 比如,在父項目中執行compile,則會同時觸發module1和module2兩個子模塊的編譯,只不過,兩個子模塊之間的執行順序等行爲並無聯繫; 而dependsOn則說明一種強依賴關係, 像module2這個子項目,因爲它依賴module3,所以,編譯module2子模塊/項目的話,會首先編譯module3,然後纔是module2,當然,在module3的相應artifact也會加入到module2的classpath中。

我們既然已經瞭解瞭如何在父項目中定義各個子模塊/項目之間的關係,下面我們來看一下各個子模塊/項目內部的細節吧! 簡單來講, 每個子模塊/項目也可以看作一個標準的SBT項目,但一個很明顯的差異在於: 每個子模塊/項目下不可以再創建project目錄下其下相應的*.scala定義文件(實際上可以創建,但會被SBT忽略)。不過, 子模塊/項目自己下面還是可以使用.sbt形式的build定義的,在各自的.sbt build定義文件中可以指定各個子模塊/項目各自的Setting或者依賴,比如version和LibrarayDependencies等。

一般情況下, SBT下的多模塊/工程的組織結構就是如此,即由父項目來規劃組織結構和關係,而由各個子模塊/項目自治的管理各自的設置和依賴。但這也僅僅是倡導,如果讀者願意,完全可以在父項目的Build定義中添加任何自己想添加的東西,比如將各個子模塊/項目的Settings直接挪到父項目的.scala形式的build定義中去(只不過,可能會讓這個.scala形式的build定義看起來有些臃腫或者複雜罷了), 怎麼做? 自己查sbt.Project的scaladoc文檔去 :)

總的來說,多子模塊/工程的項目組織主體上還是以父項目爲主體,各個子模塊/項目雖然有一定的“經濟”獨立性,但並非完全自治, 貌似跟未成年人在各自家庭裏的地位是相似吧?! 哈~

TIPS

在Maven的多子模塊/項目的組織結構中,我們很喜歡將所有項目可以重用或者共享的一些設定或者依賴放到父項目的build定義中,在SBT中也是可以的,只要在父項目的.sbt或者.scala形式的build定義中添加即可,不過,要將這些共享的設定的scope設定爲ThisBuild, 例如:

	scalaVersion in ThisBuild := "2.9.2"

這樣,就不用在每一個項目/模塊的build定義中逐一聲明要使用的scala版本了。其它的共享設定可以依法炮製哦~

SBT擴展篇 - 使用SBT創建可自動補全的命令行應用程序


  1. at實際上是String類型進行了隱式類型轉換(Implicit conversion)後目標類型的方法名 ↩︎

  2. 實際上,只有那些定義了擴展自sbt.Build類的scala文件,纔會被認爲是build定義 ↩︎

  3. 這裏的發佈更多是指特殊的發佈形式,比如提供完整的下載包給用戶,直接打包成部署包等。一般情況下,如果用Maven或者SBT,可以直接publish到相應的Maven或者Ivy Repository中 ↩︎

  4. plugins.sbt的名稱只是便於識別,實際上,SBT只關注.sbt的後綴,具體名稱是不關心的,因爲plugins.sbt本質上也是一個SBT項目的build定義文件,除了在其中配置Plugin,我們同樣可以添加第三方依賴, 追加其他Setting等,跟一般的.sbt配置文件無異 ↩︎

  5. 在SBT中,使用ModuleID來抽象和標誌相應三方庫的標誌 ↩︎

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章