非托管依赖 为放在 lib 目录下的 jar 文件
托管依赖 配置在构建定义中,并且会自动从仓库(repository)中下载
非托管依赖
大多数人会用托管依赖而非非托管依赖。但是非托管依赖在起步阶段会简单很多。
非托管依赖像这样工作:将 jar 文件放在 lib 文件夹下,然后它们将会被添加到项目的 classpath 中。没有更多的事情了!
你也可以将测试依赖的 jar 文件放在 lib 目录下,比如 ScalaCheck,Specs2,ScalaTest。
lib 目录下的所有依赖都会在 classpaths(为了 compile, test, run 和 console)。如果你想对其中的一个改变 classpath, 你需要做适当调整,例如 dependencyClasspath in Compile 或者dependencyClasspath in Runtime。
如果用非托管依赖的话,不用往 build.sbt 文件中添加任何内容,不过你可以改变unmanagedBase key,如果你想用一个不同的目录而非 lib。
用 custom_lib 替代 lib:
unmanagedBase := baseDirectory.value / "custom_lib"
baseDirectory 是项目的根目录,所以在这里你依据 baseDirectory 的值改变了 unmanagedBase,通过在 更多关于设置 中介绍的一个特殊的 value 方法。
同时也有一个列举 unmanagedBase 目录下所有 jar 文件的 task 叫 unmanagedJars。如果你想用多个目录或者做一些更加复杂的事情,你可能需要用一个可以做其它事情的 task 替换整个unmanagedJars task, 例如清空 Compile configuration 的列表,不考虑 lib 目录下的文件:
unmanagedJars in Compile := Seq.empty[sbt.Attributed[java.io.File]]
托管依赖
sbt 使用 Apache Ivy 来实现托管依赖,所以如果你对 Ivy 或者 Maven 比较熟悉的话,你不会有太多的麻烦。
libraryDependencies Key
大多数时候,你可以很简单的在 libraryDependencies 设置项中列出你的依赖。也可以通过 Maven POM 文件或者 Ivy 配置文件来配置依赖,而且可以通过 sbt 来调用这些外部的配置文件。 你可以从这里获取更详细的内容。
像这样定义一个依赖,groupId, artifactId 和 revision 都是字符串:
libraryDependencies += groupID % artifactID % revision
或者像这样, 用字符串或者 Configuration val 当做 configuration:
libraryDependencies += groupID % artifactID % revision % configuration
libraryDependencies 在 Keys 中像这样声明:
val libraryDependencies = settingKeySeq[ModuleID]
方法 % 从字符串创建 ModuleID 对象,然后将 ModuleID 添加到 libraryDependencies 中。
当然,要让 sbt(通过 Ivy)知道从哪里下载模块。如果你的模块和 sbt 来自相同的某个默认的仓库,这样就会工作。例如,Apache Derby 在标准的 Maven2 仓库中:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3"
如果你在 build.sbt 中输入上面这些内容,然后执行 update,sbt 会将 Derby 下载到~/.ivy2/cache/org.apache.derby/。(顺便提一下, compile 依赖于 update,所以 大多数时候不需要手动的执行 update。)
当然,你也可以通过 ++= 一次将所有依赖作为一个列表添加:
libraryDependencies ++= Seq(
groupID % artifactID % revision,
groupID % otherID % otherRevision
)
在很少情况下,你也会需要在 libraryDependencies 上用 := 方法。
通过 %% 方法获取正确的 Scala 版本
如果你用是 groupID %% artifactID % revision 而不是 groupID % artifactID % revision(区别在于 groupID 后面是 %%),sbt 会在 工件名称中加上项目的 Scala 版本号。 这只是一种快捷方法。你可以这样写不用 %%:
libraryDependencies += "org.scala-tools" % "scala-stm_2.11.1" % "0.3"
假设这个构建的 scalaVersion 是 2.11.1,下面这种方式是等效的(注意 "org.scala-tools" 后面是 %%):
libraryDependencies += "org.scala-tools" %% "scala-stm" % "0.3"
这个想法是很多依赖都会被编译之后给多个 Scala 版本,然后你想确保和项目匹配的某一个是二进制兼容的。
实践中的复杂度在于通常一个依赖会和稍微不同的 Scala 版本一起工作;但是 %% 就没有那么智能了。所以如果一个依赖要求版本为 2.10.1,但是你使用的 scalaVersion := "2.10.4", 你不可能使用 %% 方法即使 2.10.1 的版本很有可能工作。如果 %% 停止工作了,只需要去检查那个依赖是基于哪个 Scala 版本构建的,然后硬编码你认为可以工作的版本号(假设已经有一个)。
参见 交叉构建 获取更多信息。
Ivy 修正
groupID % artifactID % revision 中的 revision 不需要是一个固定的版本号。Ivy 能够根据你指定的约束选择一个模块的最新版本。你指定 "latest.integration","2.9.+" 或者 "[1.0,)",而不是 一个固定的版本号,像 "1.6.1"。参看Ivy 修订文档获取详细内容。
解析器
不是所有的依赖包都放在同一台服务器上,sbt 默认使用标准的 Maven2 仓库。如果你的依赖不在默认的仓库中,你需要添加 resolver 来帮助 Ivy 找到它。
通过以下形式添加额外的仓库:
resolvers += name at location
在两个字符串中间有一个特殊的 at。
例如:
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
resolvers key 在 Keys 中像这样定义:
val resolvers = settingKeySeq[Resolver]
at 方法通过两个字符串创建了一个 Resolver 对象。
sbt 会搜索你的本地 Maven 仓库如果你将它添加为一个仓库:
resolvers += "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
或者,为了方便起见:
resolvers += Resolver.mavenLocal
参见解析器获取更多关于定义其他类型的仓库的内容。
覆写默认的解析器
resolvers 不包含默认的解析器,仅仅通过构建定义添加额外的解析器。
sbt 将 resolvers 和一些默认的仓库组合起来构成 externalResolvers。
然而,为了改变或者移除默认的解析器,你需要覆写externalResolvers 而不是 resolvers。
Per-configuration dependencies
通常一个依赖只被测试代码使用(在 src/test/scala 中,通过 Test configuration 编译)。
如果你想要一个依赖只在 Test configuration 的 classpath 中出现而不是 Compile configuration,像这样添加 % "test":
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % "test"
也可能也会像这样使用类型安全的 Test configuration:
libraryDependencies += "org.apache.derby" % "derby" % "10.4.1.3" % Test
现在,如果你在 sbt 的命令提示行里输入 show compile:dependencyClasspath,你不应该看到 derby jar。但是如果你输入 show test:dependencyClasspath, 你应该在列表中看到 derby jar。
通常,测试相关的依赖,如 ScalaCheck, Specs2和 ScalaTest 将会被定义为 % "test"。
库依赖更详细的内容和技巧在这里。
常用命令
actions – 显示对当前工程可用的命令
update – 下载依赖
compile – 编译代码
test – 运行测试代码
package – 创建一个可发布的jar包
publish-local – 把构建出来的jar包安装到本地的ivy缓存
publish – 把jar包发布到远程仓库(如果配置了的话)
更多命令
test-failed – 运行失败的spec
test-quick – 运行所有失败的以及/或者是由依赖更新的spec
clean-cache – 清除所有的sbt缓存。类似于sbt的clean命令
clean-lib – 删除lib_managed下的所有内容
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得以在Scala的世界里广受欢迎的印记。
SBT的发展可以分为两个阶段, 即SBT_0.7.x时代以及SBT_0.10.x以后的时代。
目前来讲, SBT_0.7.x已经很少使用, 大部分公司和项目都已经迁移到0.10.x以后的版本上来,最新的是0.12版本。 0.10.x之后的版本build定义采用了新的Settings系统,与最初0.7.x版本采用纯Scala代码来定义build文件大相径庭,虽然笔者在迁移之前很抵触(因为0.7.x中采用Scala定义build文件的做法可以体现很好的统一性),但还是升级并接纳了0.10.x以后的版本,并且也逐渐意识到, 虽然新的版本初看起来很复杂,但一旦了解了其设计和实现的哲学跟思路,就会明白这种设计可以更便捷的定义build文件。而且可选的build文件方式也同样运行采用Scala代码来定义,即并未放弃统一性的思想。
以上是SBT的简单介绍,如果读者已经急于开始我们的SBT之旅,那么让我们先从SBT的安装和配置开始吧!
SBT安装和配置
SBT的安装和配置可以采用两种方式,一种是所有平台都通用的安装配置方式,另一种是跟平台相关的安装和配置方式,下面我们分别对两种方式进行详细介绍。
所有平台通用的安装配置方式
所有平台通用的安装和配置方式只需要两步:
下载sbt boot launcher
本书采用最新的sbt0.12,其下载地址为http://typesafe.artifactoryonline.com/typesafe/ivy-releases/org.scala-sbt/sbt-launch/0.12.0/sbt-launch.jar;
创建sbt启动脚本(启动脚本是平台相关的)
如果是Linux/Unit系统,创建名称为sbt的脚本,并赋予其执行权限,并将其加到PATH路径中; sbt脚本内容类似于 java -Xms512M -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=384M -jar dirname $0
/sbt-launch.jar "$@", 可以根据情况调整合适的java进程启动参数;
如果是Windows系统,则创建sbt.bat命令行脚本,同样将其添加到PATH路径中。 脚本内容类似于set SCRIPT_DIR=%~dp0 \n java -Xmx512M -jar "%SCRIPT_DIR%sbt-launch.jar" %*
以上两步即可完成sbt的安装和配置。
平台相关的安装配置方式
笔者使用的是Mac系统,安装sbt只需要执行brew install sbt即可(因为我已经安装有homebrew这个包管理器),使用macport同样可以很简单的安装sbt - sudo port install sbt;
如果读者使用的是Linux系统,那么这些系统通常都会有相应的包管理器可用,比如yum或者apt,安装和配置sbt也同样轻松,只要简单的运行yum install sbt 或者 apt-get install sbt命令就能搞定(当然,通常需要先将有sbt的repository添加到包管理器的列表中);
Windows的用户也可以偷懒,只要下载MSI文件直接安装,MSI文件下载地址为http://scalasbt.artifactoryonline.com/scalasbt/sbt-native-packages/org/scala-sbt/sbt/0.12.0/sbt.msi。
以上方式基本上囊括三大主流操作系统特定的安装和配置方式,其它特殊情况读者可以酌情处理 ^_^
SBT基础篇
既然我们已经安装和配置好了SBT,那就让我们先尝试构建一个简单的Scala项目吧!
Hello, SBT
在SBT的眼里, 一个最简单的Scala项目可以极简到项目目录下只有一个.scala文件,比如HelloWorld.scala:
object HelloWorld{
def main(args: Array[String]) {
println("Hello, SBT")
}
}
假设我们HelloWorld.scala放到hello目录下,那么可以尝试在该目录下执行:
$ sbt
run
[info] Running HelloWorld
Hello, SBT
[success] Total time: 2 s, completed Sep 2, 2012 7:54:58 PM
怎么样,是不是很简单那? (画外音: 这岂止是简单,简直就是个玩具嘛,有啥用嘛?! 来点儿实在的行不?)
好吧, 笔者也承认这太小儿科了,所以,我们还是来点儿"干货"吧!
NOTE: 以上实例简单归简单,但可不要小看它哦,你可知道笔者开始就因为忽略了如此简单的小细节而"光阴虚度"? 该实例的项目目录下,没有定义任何的build文件,却依然可以正确的执行sbt命令, 实际上, 即使在一个空目录下执行sbt命令也是可以成功进入sbt的console的。 所以,只要了解了sbt构建的这个最低条件,那么,当你无意间在非项目的根目录下执行了相应sbt命令而出错的时候,除了检查build文件的定义,另外要注意的就是,你是否在预想的项目根目录下面执行的sbt命令!
SBT项目工程结构详解
一般意义上讲,SBT工程项目的目录结构跟Maven的很像, 如果读者接触过Maven,那么可以很容易的理解如下内容。
一个典型的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 :=
在这里,我们排除了某些不必要的依赖,并且声明了某个定制过的依赖声明。
当然, 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
$ ...
SBT的使用
SBT支持两种使用方式:
批处理模式(batch mode)
可交互模式(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
原则上, ~和相应命令之间应该用空格分隔,不过对于一般的命令来讲,直接前缀~也是可以的,就跟我们使用~compile的方式一样。
SBT的依赖管理
在SBT中, 类库的依赖管理可以分为两类:
unmanaged dependencies
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"
at^[at实际上是String类型进行了隐式类型转换(Implicit conversion)后目标类型的方法名]之前是要追加的repository的标志名称(任意取),at后面则是要追加的repository的路径。
除了可远程访问的Maven Repo,我们也可以将本地的Maven Repo追加到resolver的搜索范围:
resolvers += "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"