1. 程式人生 > >微服務--使用Spring Boot建立微服務

微服務--使用Spring Boot建立微服務

過去幾年以來,“微服務架構”的概念已經在軟體開發領域獲得了一個穩定的基礎。作為“面向服務架構”(SOA)的一個繼任者,微服務同樣也可以被歸類為“分散式系統”這一類,並且進一步發揚了SOA中的許多概念與實踐。不過,它們在不同之處在於每個單一服務所應承擔的責任範圍。在SOA中,每個服務將負責處理廣範圍的功能與資料領域,而微服務的一種通用指南則認為,它所負責的部分是管理一個單獨的資料領域,以及圍繞著該領域的相關功能。使用分散式系統方式的目的是將整體性的服務基礎設施解耦為個別的可擴充套件子系統,可以通過垂直分片的方式將這些子系統組織在一起,並通過一種通用的傳輸方式將它們進行相關連線。

在整體性的基礎設施中,構成系統的服務在邏輯上是在相同的程式碼基礎與部署單元中組織的。這就能夠通過相同的執行時對多個服務之間的相互依賴進行管理,同時也意味著在系統的多個元件中能夠共享通用的模型與資源。在整體性基礎設施中的子系統之間的相互連線性意味著,通過抽象與功能性函式,可以實現對業務邏輯與資料領域的極大的重用性。雖然這種重用通常是通過緊耦合的方式實現的,但它也存在著一個潛在的好處,就是易於確定某個單一的變更將會對整個系統帶來怎樣的影響。但為了實現這種便利性,要付出的代價就是犧牲了整個基礎設施中單個組織的可伸縮性,同時也意味著整個系統的能力受限於其可伸縮性最薄弱的環節。

在分散式系統中,整體性系統的元件被解耦為個別的部署單元,這些部署單元能夠獨立地根據可伸縮性的需求自行升級,而不必理會其它子系統的情況。這也意味著整個系統的資源能夠被更加有效地利用,並且由於元件之間的相互依賴性不再由執行時環境進行管理,因此它們之間可以通過相對靈活的契約進行相互互動。在傳統的SOA架構中,服務的邊界之內可以封裝有關某個業務邏輯的大量功能,並且可以潛在地將大量資料領域集中在一起。而微服務架構不僅繼承了系統分散式的概念,同時也承諾只對一個單一的業務功能和資料領域進行管理,這意味著從邏輯上控制某個子系統將變得非常容易。同時也意味著管理子系統的文件化與測試的範圍也將變得更簡單,因此在這兩方面的涵蓋程度理應有所提高。


要充分了解如何切分一個整體性的架構,並建立微服務可能會存在一些困難,尤其在遺留的程式碼中,服務邊界之間的資料領域通常是緊耦合的。根據經驗來看,可以根據某個特定業務功能的邊界對基礎設施進行垂直切分。多個微服務能夠在某個垂直分片的上下文中以協作方式一起執行。舉例來說,設想某個電子商務網站的功能,從登陸頁面開始,到客戶與某個產品進行互動的頁面,再到客戶購買某個產品的頁面,這一連串的業務功能之間存在著清晰的界線。可以將這一套流程分解為多個垂直分片,包括檢視產品詳細資訊、將某個產品加入“購物車”、以及對一個或多個產品下訂單。在客戶檢視產品資訊的這個業務上下文中,可能會存在多個微服務,用於處理獲取某個特定產品並將其詳細資訊展現給使用者的流程。再舉一個例子,在網站的登陸頁面中,可能會顯示大量產品的名稱、圖片以及價格。該頁面可以從兩個後臺微服務中獲取這些細節資訊:一個微服務用於提供產品資訊,另一個用於獲取每個產品的價格。當用戶選中某個特定的產品後,網站可以呼叫另外兩個微服務,它們將用於為使用者提供產品的評分與客戶的評價。因此,為了提供用於“檢視產品詳細資訊”業務功能在架構上的垂直分片,這個分片或許要通過四種後臺微服務的結合才得以實現。與SOA架構一樣,微服務架構也必須通過某種通用的傳輸方式進行相互連線,而這些年以來,HTTP已經被證明是完成這一任務的一樣強大的手段。除此之外還存在著多種選擇,例如二進位制傳輸協議以及訊息代理,微服務架構中並沒有明顯地傾向於其中任何一種方式,主要的選擇依據是看那些能夠在服務之間建立互通訊的類庫的成熟度與可用性。作為一種成熟的傳輸協議,幾乎每種程式語言與框架都提供了HTTP的客戶端類庫,因此它作為服務間互通訊的協議是一個優秀的選擇。微服務架構對於與服務互動的無狀態性這一方面有著特別的要求,無論採用了哪種底層協議,微服務都應該保持通訊的無狀態性,並且遵循RESTful正規化以求實現這一點,這在業界基本已經達成了很好的共識。這就意味著對於某個微服務的每個請求與響應必須保證所呼叫的方法中的狀態必須始終保持可用。說得更明白一點,就是指該服務不能夠根據之前的互動行為對於每個請求中所需的資料進行任何假設。保證了正確的REST實現,也就意味著微服務本質上就是為了大規模而設計的,這也確保了對於任何一個服務的後續部署能夠將停機時間減至最低、甚至做到無停機時間。

在“產品”這個垂直分片上的每個微服務都是對於“產品”這個領域中不同部分的實現,而每個微服務都具備根據系統的需求進行自我伸縮,併為系統所用的能力。可以想象,負責提供登陸頁面使用者體驗的服務需要應對的請求數量,要遠遠大於那些提供某個產品詳細資訊的服務所應對的請求。它們甚至可能是基於不同的技術決策所設計的,例如快取策略,而在展示產品評分與客戶評論的服務中就不會用到這種技術。因為微服務能夠根據功能選擇適當的技術決策,因此能夠更高效地利用資源。而在整體性的架構中,產品評分與客戶評價服務則不得不屈從於產品資訊與價格服務對於可伸縮性與可用性的需求。

不過,微服務的複雜度與程式碼的大小沒有任何聯絡。有一種常見的誤解認為,微服務的程式碼量也應該遵循“微”這個概念,但這種說法並不成立,只要你考慮一下微服務構架所試圖實現的目標就知道。這個目標是將服務分解為一種分散式系統,而每個服務的複雜度所需的程式碼量完全於它本身。“微”這個術語表示了這種將職責分散在不同的子系統中的模式,而不是指程式碼量。不過,由於一個微服務的職責只限制在系統的某個垂直分片中的某個單一功能,因此它的程式碼通常比較簡潔、易於理解、並且能夠通過較小的部署單元進行釋出。對於微服務有一種推薦的模式,就是將這些服務與執行它們所需的資源一起釋出。這也意味著微服務的可部署單元通常包含了它們自己的執行時,並且能夠單獨執行,這大大減少了與部署相關的運維工作。

過去,部署Java web應用程式的方式往往包括一些笨重的、經過預先配置的應用伺服器,這些伺服器將把檔案檔案進行解壓縮,部署在一個規定的、並且通常是有狀態的執行時環境中。為了解壓縮某個檔案檔案,並且開始執行新的應用程式程式碼,這些應用伺服器有可能會產生幾十分鐘的停機時間,這就造成對更新的迭代變得十分困難,並且從運維的角度來看,也很難接受對某個系統進行多個部署的流程。隨著各種各樣的框架開始不斷進化,以支援微服務的開發,對於程式碼進行打包以實現部署的流程也在不斷改變。在如今的Java世界中,基於微服務的web應用程式能夠很容易地將它們自身所需的執行時環境打包到一個可以執行的檔案檔案中。現代的嵌入時執行時,例如Tomcat和Jetty,是它們前身的應用伺服器所對應的輕量級版本,它們通常都能夠做到在幾秒鐘之內迅速啟動。所有安裝了Java的系統都能夠直接執行部署的程式,這也簡化了部署新變更的流程。

Spring Boot

Spring Boot這個框架在經歷了不斷的演變之後,如今已經能夠用於開發Java微服務了。Boot是基於Spring框架進行開發的,也繼承了Spring的成熟性。它通過一些內建的韌體封裝了底層框架的複雜性,以幫助使用者進行微服務的開發。Spring Boot的一大優點是提高開發者的生產力,因為它已經提供了許多通用的功能,例如RESTful HTTP以及嵌入式的web應用程式執行時,因此很容易進行裝配及使用。在許多方面上,它也是一種“微框架”,允許開發者選擇在整個框架中他們所需的那部分,而無需使用龐大的、或是不必要的執行時依賴。這也讓Boot應用程式能夠被打包為多個小單元以進行部署,並且該框架還能夠使用構建系統生成可部署檔案,例如可執行的Java檔案包。

Spring Boot團隊提供了一種便利的機制,讓開發者能夠簡單地上手建立應用程式,也就是所謂的Spring Initializr。這個頁面的作用是引導基於Boot的web應用程式的構件配置,並且允許開發者在多個分類中選擇在專案中需要使用的類庫。開發者只需要輸入專案的一些元資料、選擇所需的依賴項、並且單擊“生成專案”按鈕,就能夠生成一個基於Maven或Gradle的Spring Boot專案的壓縮檔案了。檔案裡提供了用於開始設計專案的腳手架程式碼,對於首次使用這個框架的開發者來說是個絕佳的起點。

作為一個框架,Boot中內建了一些聚合模組,通常稱為“啟動者”。這些啟動模組中是一些類庫的已知的、良好的、具備互操作性的版本的組合,這些類庫能夠為應用程式提供某些方面的功能。Boot能夠通過應用程式的配置對這些類庫的進行設定,這也為整個開發週期中帶來了配置勝於約定的便利性。這些啟動模組中有許多是專門用於進行微服務架構開發的,它們為應用程式的開發者帶來了一些免費的關鍵功能。在Spring Boot中實現一個基於HTTP的RESTful微服務,只需簡單地加入actuator與web啟動模組就足夠了。web模組將提供嵌入式的執行時,而且能夠讓使用者基於RESTful HTTP控制器進行微服務API的開發,而actuator模組則為對外暴露的試題、配置引數和內部元件的對映提供了基本功能與RESTful HTTP終結點,因而使微服務能夠正常運轉,同時也為除錯提供了極大的便利。

作為一個微服務框架,Boot的很大一部分價值在於它能夠無縫地為基於Maven和Gradle的專案提供各種構建工具。通過使用Spring Boot外掛,就能夠利用該框架的能力,將專案打包為一個輕量級的、可執行的部署包,而除此之外幾乎不需要進行任何額外的配置。在列表1中的程式碼展示了一個Gradle的構建指令碼,可作為執行某個Spring Boot微服務的起點。此外,也可在Spring Initializr網站上選擇使用較繁瑣的Maven POM的示例,同時需要將應用程式的啟動類的地址告訴該外掛。而在使用Gradle時則無需進行這方面的配置,因為外掛本身就能夠找到這個類的地址。


buildscript {
  repositories {
    jcenter()
  }
  dependencies { 
   classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.RELEASE'
  }
}
apply plugin: 'spring-boot'
repositories { jcenter()
}
dependencies {
  compile "org.springframework.boot:spring-boot-starter-actuator"
  compile "org.springframework.boot:spring-boot-starter-web"
}

列表 1 – Gradle的構建指令碼

如果選擇使用Spring Initializr上的專案,就需要讓專案結構符合常規的需求,只需遵循Maven風格的專案結構就能夠實現這一點。程式碼必須被儲存在src/main/java資料夾下,這樣才能夠正確地編譯。該專案隨後還要提供一個應用程式的入口點。在Spring Initializr的腳手架程式碼中有一個名為DemoApplication.java的檔案,它的作用正是該專案的main類。可以隨意對這個類進行重新命名,通常來說將其命名為“Main”就可以了。列表1.1的示例描述了開始開發一個微服務所需的最少程式碼。


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@EnableAutoConfiguration
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class);
    }
}

列表 1.1 - Spring Boot應用

通過在Main類中使用“EnableAutoConfiguration”標註,該框架就能夠進行行為的配置,以引導應用程式的啟動與執行。這些行為很大程度上是通過約定用於配置的方式確定的,為此Boot將對classpath進行掃描,以確定微服務中需要具備哪些功能。在上面的示例中,該微服務選擇了actuator與web這兩個啟動模組,因此該框架能夠確定這個專案是一個微服務,引導某個嵌入的Tomcat容器的啟動,並通過某個預先配置的終結點提供該服務。在該示例中的程式碼並沒有進行太多工作,但只需簡單地啟動該示例,就能夠使actuator模組所暴露的終結點開始執行。只需將該專案匯入任何IDE,隨後為“Main”類建立一個“作為Java應用程式執行”的配置,就能夠啟動這個微服務了。此外,也可以選擇在命令列中執行gradle bootRun這個Gradle任務,或是針對Maven的mvn spring-boot:run命令,也能夠啟動該應用程式,具體的命令取決於你選擇了哪種專案配置。

操作資料

接下來我們要實現之前所說的那個“產品的垂直分片”,考慮一下“產品詳細資訊”這個服務,它與“產品價格”這個服務一起提供了登入頁面體驗的詳細資訊。至於微服務的職責,它的資料領域應當是與某個“產品”相關的屬性的子集,包括產品名稱、簡短描述、詳細描述、以及一個庫存id。可以使用Java bean對這些資訊進行建模,正如列表1.2中的程式碼所描述的一樣。


import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class ProductDetail {
    @Id
    private String productId;
    private String productName;
    private String shortDescription;
    private String longDescription;
    private String inventoryId;
    public String getProductId() {
        return productId;
    }
    public void setProductId(String productId) {
        this.productId = productId;
    }
    public String getProductName() {
        return productName;
    }
    public void setProductName(String productName) {
        this.productName = productName;
    }
    public String getShortDescription() {
        return shortDescription;
    }
    public void setShortDescription(String shortDescription) {
        this.shortDescription = shortDescription;
    }
    public String getLongDescription() {
        return longDescription;
    }
    public void setLongDescription(String longDescription) {
        this.longDescription = longDescription;
    }
    public String getInventoryId() {
        return inventoryId;
    }
    public void setInventoryId(String inventoryId) {
        this.inventoryId = inventoryId;
    }
}

列表1.2 ——產品詳細資訊的POJO物件

在ProductDetail這個Java bean中有一點要特別注意,這個類使用了JPA標註,以表示它是一個實體。Spring Boot中專門提供了一個可用於JPA實體與關係型資料庫資料來源的啟動模組。考慮一下列表1中的構建指令碼,我們可以在其中的“依賴”一節中加入這個Boot的啟動模組,以用於持久化資料集,如列表1.3中的程式碼所示。


dependencies {
  compile "org.springframework.boot:spring-boot-starter-actuator"
  compile "org.springframework.boot:spring-boot-starter-web"
  compile "org.springframework.boot:spring-boot-starter-data-jpa"
  compile 'com.h2database:h2:1.4.184'
}

列表 1.3 ——在構建指令碼中設定Spring Boot的依賴

出於演示與原型的目的,該專案中現在還包括了內嵌的h2資料庫型別。Boot的自動配置機制能夠檢測到classpath中存在h2,隨後為ProductDetail實體生成必要的表結構。在內部,Boot會呼叫Spring Data進行物件實體對映操作,有了它之後,我們就可以利用它的約定和機制與資料庫打交道了。Spring Data中提供了一個便捷的抽象,也就是“repository”的概念,它本質上就是一種資料訪問物件(DAO),該物件在啟動時會由框架為我們自動裝配。為了實現ProductDetail實體的CRUD功能,我們只需要建立一個介面,擴充套件在Spring Data中內建的CrudRepository即可,正如列表1.4中的程式碼所示。


import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductDetailRepository extends CrudRepository <ProductDetail, String>{
}

列表 1.4 ——產品資訊的資料訪問物件(Spring Data Repository

在介面定義中的@Repository標註將通知Spring,這個類的作用是一個DAO。這個標註也是一種特別的機制,我們可以通過這種機制通知框架,讓框架自動將其進行裝配,並分配到微服務的配置中,從而讓我們可以使用依賴注入的方式訪問它。為了在Spring中應用這一特性,我們還必須在列表1.1中定義的Main類上新增@ComponentScan這個額外的標註。當微服務啟動之後,Spring將會對專案的classpath進行掃描以尋找各種元件,並且將這些元件作為應用程式中需要自動裝配的備選元件。

為了展現微服務的新能力,請仔細閱讀列表1.5中的程式碼,這裡我們利用了一個先決條件,就是Boot會在main()方法中為我們提供一個指向Spring的ApplicationContext的引用。


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Main.class);
        ProductDetail detail = new ProductDetail();
        detail.setProductId("ABCD1234");
        detail.setProductName("Dan's Book of Writing");
        detail.setShortDescription("A book about writing books.");
        detail.setLongDescription("In this book about writing books, Dan will show you how to write a book.");
        detail.setInventoryId("009178461");
        ProductDetailRepository repository = ctx.getBean(ProductDetailRepository.class);
        repository.save(detail);
        for (ProductDetail productDetail : repository.findAll()) {
            System.out.println(productDetail.getProductId());
        }
    }
}

列表 1.5 ——展現載入資料的功能

在這個簡單的示例中,我們為一個ProductDetail物件載入了某些資料,我們通過呼叫ProductDetailRepository的方法將產品資訊進行儲存,隨後再次呼叫這個repository物件,從資料庫中取回產品的資訊。到目前為止,對於在微服務使用持久化資料,沒有進行任何額外的配置。我們可以使用列表1.5中的這個原型程式碼作為定義RESTful HTTP API契約的基礎,通過Spring中提供的@RestController標註就可以實現。