SpringBoot-28 使用SBT構(gòu)建和發(fā)布基于SpringBoot的Scala應(yīng)用

SBT 是 Scala 生態(tài)圈里的經(jīng)典構(gòu)建工具,雖然很多人覺得 SBT 很復(fù)雜,還戲稱其為 SB Tool,但其全稱確是 Simple Build Tool。

實(shí)際上,很多產(chǎn)品(包括像 SBT 這樣的工具和技術(shù)產(chǎn)品)只有一個(gè)打動(dòng)用戶的特性就夠了, Triggered Execution 這一特性支持,就可以在一個(gè)屏幕上寫代碼,而在另一個(gè)屏幕(或者窗口)實(shí)時(shí)地獲得編譯結(jié)果反饋,如圖 1 所示,參與感十足。

結(jié)合雙屏使用SBT的工作場景示意圖

圖 1 結(jié)合雙屏使用SBT的工作場景示意圖

當(dāng)然,SBT 畢竟是一套新的工具體系,與 Maven 或者其他構(gòu)建工具在配置和使用上還是會有不小的差異,這就意味著,如果我們要使用 SBT 來構(gòu)建基于 Scala 的 SpringBoot 微服務(wù)項(xiàng)目,就需要針對 SBT 的特點(diǎn)做很多定制工作。

探索基于SBT的SpringBoot應(yīng)用開發(fā)模式

SpringBoot 團(tuán)隊(duì)圍繞 Maven 提供了很多便利和支持,比如我們只要使用 Maven 的繼承特性,將 spring-boot-starter-parent 作為當(dāng)前項(xiàng)目的 parent,就可以直接開發(fā) SpringBoot 微服務(wù)項(xiàng)目,代碼如下所示:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.3.2.RELEASE</version>
</parent> 

或者,如果當(dāng)前要開發(fā)的 SpringBoot 微服務(wù)項(xiàng)目需要繼承其他 parent 項(xiàng)目而不能使用 spring-boot-starter-parent,那么也可以使用 Maven 的依賴引入特性完成一系列 SpringBoot 相關(guān)依賴導(dǎo)入:

<dependencyManagement>
    <dependencies>
        <dependency> <!-- Import dependency management from Spring Boot -->
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>1.3.2.RELEASE</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement> 

當(dāng)使用 SBT 時(shí),這些圍繞 Maven 提供的便利就不復(fù)存在了,我們需要搞清楚這些便利背后都發(fā)生了什么,才能最終決定如何使用 SBT 進(jìn)行相應(yīng)的配置并最終達(dá)到相同的目的。

因?yàn)?spring-boot-starter-parent 項(xiàng)目只有一個(gè) pom.xml 定義,且其 parent 就是 spring-boot-dependencies,所以,我們先沿著 spring-boot-dependencies 開始尋找端倪。

但是我們發(fā)現(xiàn),spring-boot-dependencies 也只是一個(gè)只有 pom.xml 定義的“干癟”項(xiàng)目,其 pom.xml 中僅僅通過 dependencyManagement 和 pluginManagement 固定了 SpringBoot 項(xiàng)目的一系列依賴和版本號,并無實(shí)際有用信息,所以,我們需要回頭再從 spring-boot-starter-parent 的 pom.xml 中尋找線索。

不幸的是,spring-boot-starter-parent 也沒有提供什么有用的線索,它的 pom.xml 中也是定義了相應(yīng)的 pluginManagement 來規(guī)范依賴和依賴的版本,并在編譯期間進(jìn)行相應(yīng)資源和配置的過濾。

到了這個(gè)地步,我們就只能另尋他途了。不過,既然大部分的 SpringBoot 項(xiàng)目都會從依賴相應(yīng)的 starter 項(xiàng)目開始,那么,我們可以分析幾個(gè) spring-boot-starter-XXX 模塊來看看它們有什么共性。

假設(shè)我們關(guān)聯(lián)查看了 spring-boot-starter-web、spring-boot-starter-jdbc、spring-boot-starter-actuator 等模塊,就會發(fā)現(xiàn),除了這些模塊自身特定要提供的依賴,它們都同時(shí)依賴如下模塊:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency> 

而繼續(xù)追蹤下去到 spring-boot-starter 的 pom.xml 定義內(nèi)部,最終才“柳暗花明又一村”:

...
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-core</artifactId>
        <exclusions>
            <exclusion>
                <groupId>commons-logging</groupId>
                <artifactId>commons-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.yaml</groupId>
        <artifactId>snakeyaml</artifactId>
        <scope>runtime</scope>
    </dependency>
</dependencies>
... 

所以,一個(gè) SpringBoot 項(xiàng)目得以成型,如下依賴基本是必備的:

  • org.springframework.boot:spring-boot
  • org.springframework.boot:spring-boot-autoconfigure
  • org.springframework.boot:spring-boot-starter-logging
  • org.springframework:spring-core
  • org.yaml:snakeyaml

作為 SpringBoot 項(xiàng)目,spring-boot 依賴自然不可少。

  • spring-boot-autoconfigure 依賴可以幫助我們進(jìn)行自動(dòng)配置,SpringBoot 項(xiàng)目的便利的核心就在于此。
  • spring-boot-starter-logging 則配置默認(rèn)的日志依賴。
  • SpringBoot 項(xiàng)目就是一個(gè) Spring 項(xiàng)目,所以 org.springframework:spring-core 也是必備依賴。
  • org.yaml:snakeyaml,是運(yùn)行期依賴,主要提供針對 yml 格式配置文件的支持。

至此,我們基本已經(jīng)了解了 SpringBoot 項(xiàng)目背后的故事全貌,接下來就要介紹 SBT 了。

構(gòu)建在 SPRING INITIALIZR 之上的 http://start.spring.io 可以在每次創(chuàng)建新的 SpringBoot 項(xiàng)目的時(shí)候提供“腳手架”功能,創(chuàng)建 SBT 項(xiàng)目其實(shí)也有這樣的“腳手架”工具,即 Typesafe Activator(https://www.typesafe.com/activator/download)。

我們使用 Typesafe Activator 來構(gòu)建一個(gè)新的 SBT 項(xiàng)目,用于構(gòu)建同樣功能的一個(gè)匯率查詢 Web API 微服務(wù),Typesafe Activator 安裝完成后執(zhí)行如下命令:

$ activator new currency-webapi-with-scala-sbt minimal-scala

其中,currency-webapi-with-scala-sbt 為我們要生成的 SBT 項(xiàng)目名,而 minimal-scala 為我們選擇要使用的“腳手架”模板項(xiàng)目名。

初始生成的 SBT 項(xiàng)目的構(gòu)建文件內(nèi)容如下:

name := """currency-webapi-with-scala-sbt"""
version := "1.0"scalaVersion:= "2.11.7"
// Change this to another test framework if you
preferlibraryDependencies += "org.scalatest" %
% "scalatest" % "2.2.4" %"test"
// Uncomment to use Akka
// libraryDependencies += "com.typesafe.akka" %
% "akka-actor" % "2.3.11"

我們需要對其進(jìn)行定制和豐富一些內(nèi)容:

organization := "com.keevol.springboot.chapter5"
name := """currency-webapi-with-scala-sbt"""
version := "1.0.0-SNAPSHOT"
scalaVersion := "2.11.7"
resolvers += "Local Maven Repository" at
"file://"+Path.userHome.absolutePath+"/.m2/repository"
libraryDependencies += "org.springframework.boot" % "spring-boot-starter-web" %
"1.3.2.RELEASE"
libraryDependencies += "com.keevol.springboot" % "currency-rates-service" % "1.0-SNAPSHOT"
// Change this to another test framework if you
preferlibraryDependencies += "org.scalatest" %
%"scalatest" %
"2.2.4" % "test" 

因?yàn)槲覀円呀?jīng)了解了 SpringBoot 項(xiàng)目依賴的傳遞關(guān)系,所以,現(xiàn)在只要將 spring-boot-starter-web 作為核心依賴配置到 build.sbt 中,就可以信心滿滿地著手開發(fā)了(畢竟 spring-boot-starter-web 的依賴會一路上接力傳遞,必需的基礎(chǔ)依賴一個(gè)都不會少)。

除此之外,currency-rates-service 屬于業(yè)務(wù)依賴,也需要一并遵循 SBT 的配置語法加以配置。因?yàn)閷?shí)例項(xiàng)目是使用本地開發(fā),所以,我們配置了相應(yīng)的 resolvers 指引 SBT 從本地的 Maven 倉庫中加載業(yè)務(wù)依賴。

實(shí)際情況下,大家可以根據(jù)需要,將自己公司內(nèi)部的 Maven 遠(yuǎn)程倉庫或者其他 Maven 倉庫也加入進(jìn)來。

SBT 的項(xiàng)目源碼結(jié)構(gòu)與 Maven 的項(xiàng)目源碼結(jié)構(gòu)約定基本相同,所以,在 build.sbt 中所有配置完成后,就可以在 src/main/scala 目錄下開始編寫 scala 代碼了:

// CurrencyRateQueryController.scala源碼文件
package com.mengma.controllers
import com.mengma.WebApiResponse
import com.keevol.springboot.services.currency.rates.CurrencyPair
import com.keevol.springboot.services.currency.rates.CurrencyRateService
import com.keevol.springboot.services.currency.rates.ExchangeRate
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.{RequestMethod, RequestMapping,
RestController}
@RestController
class CurrencyRateQueryController{
    @Autowired
    var currencyRateService: CurrencyRateService = _
    @RequestMapping(value = Array("/"), method = Array(RequestMethod.GET))
    def quote(symbol: String): WebApiResponse[ExchangeRate] = {
        val response: WebApiResponse[ExchangeRate] = new WebApiResponse [ExchangeRate]
        response.setCode(WebApiResponse.SUCCESS_CODE)
        response.setData(currencyRateService.quote(CurrencyPair.from (symbol)))
        response
    }
}
// CurrencyWebApiBootStrap.scala源碼文件
package com.mengma.springboot
import com.keevol.springboot.services.currency.rates.CurrencyRateRepository
import com.keevol.springboot.services.currency.rates.CurrencyRateService
import com.keevol.springboot.services.currency.rates.CurrencyRateServiceImpl
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.context.annotation.Bean
@SpringBootApplication
class CurrencyWebApiBootStrap{
    @Bean
    def currencyRateService:CurrencyRateService = {
        val service: CurrencyRateServiceImpl = new CurrencyRateServiceImpl
        service.setRateRepository(new CurrencyRateRepository) service
    }
}
object CurrencyWebApiBootStrap{
    def main(args: Array[String]) {
        SpringApplication.run(classOf[CurrencyWebApiBootStrap], args: _*)
    }
} 

開發(fā)完成后,運(yùn)行 sbt run 或者直接通過 sbt"run-main com.keevol.springboot.chapter5.CurrencyWebApiBootStrap"即可啟動(dòng)微服務(wù)。

探索基于 SBT 的 SpringBoot 應(yīng)用發(fā)布策略

針對我們基于 SBT 和 Scala 的 SpringBoot 微服務(wù)開發(fā)完成后,需要發(fā)布出去才能發(fā)揮其價(jià)值,要完成這種類型的項(xiàng)目發(fā)布,有幾種策略可以考慮。

一種策略是使用 SpringBoot 推薦的可執(zhí)行 jar 包發(fā)布形式,SpringBoot 團(tuán)隊(duì)提供的 spring-boot-maven-plugin 默認(rèn)對這種可執(zhí)行 jar 包有很好的支持。

但是,對于微服務(wù)來說,我們前面說了,不建議采用這種方式,除非零星個(gè)別的一些項(xiàng)目,不需要強(qiáng)大體系支撐,僅靠人工就可以滿足或者忍受。

不過,如果因?yàn)槟承┮蛩乇仨毷褂?SpringBoot 建議的可執(zhí)行 jar 包發(fā)布形式,那么,可以結(jié)合 spring-boot-maven-plugin 的源碼和邏輯,并實(shí)現(xiàn)一個(gè)相應(yīng)的 SBT 插件即可。

另一種策略是使用 SBT 生態(tài)圈中已經(jīng)形成一定口碑或者說基本上已經(jīng)是事實(shí)標(biāo)準(zhǔn)的方案,比如使用 sbt-native-packager(https://github.com/sbt/sbt-native-packager),通過 sbt-native-packager 這個(gè) SBT 插件,我們可以將當(dāng)前 SBT 項(xiàng)目發(fā)布為 ZIP、TAR、RPM、DEB 甚至 docker 等形式,相對于自己開發(fā)一個(gè) SBT 插件完成可執(zhí)行 jar 包形式的發(fā)布,顯然直接使用 sbt-native-packager 是更明智的做法。

針對我們基于 SBT 和 Scala 的匯率查詢 SpringBoot 微服務(wù),要使用 sbt-native-packager 進(jìn)行發(fā)布,我們只要對項(xiàng)目的 build.sbt 添加少許發(fā)布指引就可以了:

import sbt._
import com.typesafe.sbt.packager.universal.UniversalPlugin.autoImport._
import com.typesafe.sbt.packager.MappingsHelper._lazy
val root = project in file(".") enablePlugins([Java](http://c.biancheng.net/java/)AppPackaging)
organization := "com.mengma.springboot"\
name := """currency-webapi-with-scala-sbt"""
version := "1.0.0-SNAPSHOT"
scalaVersion := "2.11.7"
resolvers += "Local Maven Repository" at "file://"+Path.userHome.absolutePath+"/.m2/repository"
libraryDependencies += "org.springframework.boot" % "spring-boot-starter-web" %
"1.3.2.RELEASE"
libraryDependencies += "com.keevol.springboot" % "currency-rates-service" % "1.0-SNAPSHOT"
// Change this to another test framework if you prefer
libraryDependencies += "org.scalatest" % %"scalatest" % "2.2.4" % "test"
// 發(fā)布相關(guān)main
Class in Compile := Some("com.mengma.springboot.CurrencyWebApiBootStrap") mappings
in Universal += file("LICENSE") ->
"LICENSE"mappings in Universal ++= directory("config")

我們首先通過 import 引入相應(yīng)的依賴,然后聲明當(dāng)前項(xiàng)目以 Java 應(yīng)用(JavaAppPackaging)的形式發(fā)布(不是 Scala 應(yīng)用類型是因?yàn)榫幾g完運(yùn)行期都是 Java 字節(jié)碼的形式),最后通過 mainClass 來指定啟動(dòng)類是哪一個(gè),以及應(yīng)該將哪些其他文件或者目錄打入發(fā)布包之中。

在執(zhí)行發(fā)布命令之前,我們還有最后一步要做,就是將 sbt-native-packager 添加為當(dāng)前項(xiàng)目的依賴,在 SBT 項(xiàng)目結(jié)構(gòu)下,這需要我們在當(dāng)前項(xiàng)目根目錄下新建 project/plugins.sbt 文件,然后添加如下內(nèi)容:

resolvers += "Typesafe repository" at
"http://repo.typesafe.com/typesafe/releases/"resolvers +=
Resolver.url("fix-sbt-plugin-releases",
url("https://dl.bintray.com/sbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns)addMavenResolverPluginaddSbtPlugin("com.typesafe.sbt"
% "sbt-native-packager" % "1.0.0")

之后,build.sbt 才可以依賴到 sbt-native-packager 并且編譯成功,現(xiàn)在,只要我們運(yùn)行 sbt universal:packageBin 命令,就可以在 target/universal 目錄下獲得一個(gè) zip 形式的發(fā)布安裝包 currency-webapi-with-scala-sbt-1.0.0-SNAPSHOT.zip,安裝包格式包含以下內(nèi)容:

  • 自動(dòng)生成啟動(dòng)腳本的 bin 目錄。
  • 存放了項(xiàng)目所有依賴的 lib 目錄。
  • 以及在 build.sbt 中通過 mapping 添加的一系列希望打包的文件和目錄。

當(dāng)然,zip 不是微服務(wù)發(fā)布的理想形式,但基于 RPM 和 DEB 等發(fā)布包需要依賴復(fù)雜的本地環(huán)境準(zhǔn)備,故此,這里為了示例的簡化,使用 zip 發(fā)布形式來說明如何使用 sbt-native-packager,以輕松愉快的心情完成基于 SBT 和 Scala 的 SpringBoot 項(xiàng)目發(fā)布。

關(guān)于如何準(zhǔn)備 RPM、DEB 等發(fā)布包的編譯環(huán)境,以及如何配置 sbt-native-packager 使之相應(yīng)的將當(dāng)前項(xiàng)目發(fā)布為 RPM、DEB 等形式,可以參考 sbt-native-packager 插件的網(wǎng)站文檔(http://www.scala-sbt.org/sbt-native-packager/index.html)。

最后一種策略(或許還有)我認(rèn)為是最適合微服務(wù)的終極發(fā)布策略,即外部化(Externalization)的發(fā)布策略。

無論是在 Maven 項(xiàng)目的 pom.xml 中定義和使用 spring-boot-maven-plugin,還是在 SBT 項(xiàng)目的構(gòu)建配置中定義和使用 sbt-native-packager 插件,本質(zhì)上都是一種將通用邏輯分散到一個(gè)個(gè)單獨(dú)項(xiàng)目中的做法,如果只是小團(tuán)隊(duì)以及少量項(xiàng)目,這樣做是沒有太大問題的。

而一旦規(guī)模膨脹(對于微服務(wù)來說,數(shù)量、發(fā)布頻度等指標(biāo)規(guī)模膨脹很正常),加上人員流動(dòng)、時(shí)機(jī)錯(cuò)配等一系列的變化因素,這種通用邏輯最終很可能會在這些一個(gè)個(gè)離散的項(xiàng)目中被“玩壞”,造成發(fā)布的成品千差萬別,完全背離標(biāo)準(zhǔn)化的微服務(wù)這一理想軌道,進(jìn)而帶來整體微服務(wù)交付鏈路上的一系列不必要的負(fù)累。

所以,為了避免“該標(biāo)準(zhǔn)化的地方卻使用個(gè)性化”所可能引發(fā)的一系列問題,我們應(yīng)該將這些發(fā)布和部署邏輯從單一項(xiàng)目中剝離出來,即外部化掉(Externalize it),將這些邏輯以發(fā)布和部署平臺的形式抽象和固化,從而將原來因?yàn)楦鞣N因素導(dǎo)致的不確定性最大化地轉(zhuǎn)變成確定性。

發(fā)布和部署平臺是外部化的最終成果,每個(gè) SpringBoot 微服務(wù)項(xiàng)目,不管你使用的是什么語言開發(fā),也不管你使用的什么構(gòu)建工具,只要你想用并且能幫助你提高開發(fā)效率,在開發(fā)階段盡管用即可。

一旦項(xiàng)目要發(fā)布,直接將發(fā)布請求對接發(fā)布和部署平臺。對于 SpringBoot 項(xiàng)目的開發(fā)者來說,發(fā)布和部署平臺是訪問接口(Interface);而對于發(fā)布和部署平臺的開發(fā)者來說,則是服務(wù)提供方。服務(wù)提供方可以靈活替換和升級:

如果你的 SpringBoot 項(xiàng)目是使用 Maven 作為構(gòu)建工具,那么,發(fā)布和部署平臺實(shí)現(xiàn)層會自動(dòng)感知并構(gòu)建發(fā)布。

如果你的 SpringBoot 項(xiàng)目是使用 SBT 作為構(gòu)建工具,那么,發(fā)布和部署平臺實(shí)現(xiàn)層也會自動(dòng)感知并構(gòu)建發(fā)布。

如果組織內(nèi)部當(dāng)前的微服務(wù)體系是基于 RPM 標(biāo)準(zhǔn)統(tǒng)一發(fā)布形式,那么,所有 SpringBoot 項(xiàng)目不管你使用什么構(gòu)建工具,都將以 RPM 標(biāo)準(zhǔn)形式發(fā)布。

如果組織內(nèi)部要實(shí)現(xiàn) DevOps 體系的升級換代(比如 docker),那么,只要發(fā)布和部署平臺實(shí)現(xiàn)層進(jìn)行變更和升級即可,SpringBoot 項(xiàng)目的開發(fā)者無須感知,發(fā)布和部署平臺起到了很好的抽象和防火墻效果。

所以,對于一個(gè)擁有成熟微服務(wù)體系的組織來說,將發(fā)布等通用邏輯外部化到發(fā)布和部署平臺,應(yīng)該是最合適,也是目前最終極的策略。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容