使用Sprint Boot創建微服務

jopen 9年前發布 | 168K 次閱讀 Spring JEE框架 Sprint Boot

來自:http://www.infoq.com/cn/articles/boot-microservices


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

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

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

與SOA架構一樣,微服務架構也必須通過某種通用的傳輸方式進行相互連接,而這些年以來,HTTP已經被證明是完成這一任務的一樣強大的手段。除此之外還存在著多種選擇,例如二進制傳輸協議以及消息代理,微服務架構中并沒有明顯地傾向于其中任何一種方式,主要的選擇依據是看那些能夠在服務之間建立互通信的類庫的成熟度與可用性。作為一種成熟的傳輸協議,幾乎每種編程語言與框架都提供了HTTP的客戶端類庫,因此它作為服務間互通信的協議是一個優秀的選擇。微服務架構對于與服務交互的無狀態性這一方面有著特別的要求,無論采用了哪種底層協議,微服務都應該保持通信的無狀態性,并且遵循RESTful范式以求實現這一點,這在業界基本已經達成了很好的共識。這就意味著對于某個微服務的每個請求與響應必須保證所調用的方法中的狀態必須始終保持可用。說得更明白一點,就是指該服務不能夠根據之前的交互行為對于每個請求中所需的數據進行任何假設。保證了正確的REST實現,也就意味著微服務本質上就是為了大規模而設計的,這也確保了對于任何一個服務的后續部署能夠將停機時間減至最低、甚至做到無停機時間。

要充分了解如何切分一個整體性的架構,并創建微服務可能會存在一些困難,尤其在遺留的代碼中,服務邊界之間的數據領域通常是緊耦合的。根據經驗來看,可以根據某個特定業務功能的邊界對基礎設施進行垂直切分。多個微服務能夠在某個垂直分片的上下文中以協作方式一起運行。舉例來說,設想某個電子商務網站的功能,從登陸頁面開始,到客戶與某個產品進行交互的頁面,再到客戶購買某個產品的頁面,這一連串的業務功能之間存在著清晰的界線。可以將這一套流程分解為多個垂直分片,包括查看產品詳細信息、將某個產品加入“購物車”、以及對一個或多個產品下訂單。在客戶查看產品信息的這個業務上下文中,可能會存在多個微服務,用于處理獲取某個特定產品并將其詳細信息展現給用戶的流程。再舉一個例子,在網站的登陸頁面中,可能會顯示大量產品的名稱、圖片以及價格。該頁面可以從兩個后臺微服務中獲取這些細節信息:一個微服務用于提供產品信息,另一個用于獲取每個產品的價格。當用戶選中某個特定的產品后,網站可以調用另外兩個微服務,它們將用于為用戶提供產品的評分與客戶的評價。因此,為了提供用于“查看產品詳細信息”業務功能在架構上的垂直分片,這個分片或許要通過四種后臺微服務的結合才得以實現。

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

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

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

Spring Boot

Sprint 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標注就可以實現。

設計API

對于“產品信息”這個微服務來說,提供簡單的CRUD式功能或許就已經足夠了,但也許它還需要提供一些擴展功能,例如分頁的結果集和數據過濾。可以通過一個簡單的控制器(controller)實現這個操作數據集的API,Spring會將該控制器映射到某個HTTP的路由上。下方的列表1.6中的代碼示例可以作為一個起點,這個API暴露了create與findAll方法,通過它可以實現之前那個原型中所演示的代碼功能。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/products")
public class ProductDetailController {
    private final ProductDetailRepository repository;
    @Autowired
    public ProductDetailController(ProductDetailRepository repository) {
        this.repository = repository;
    }
    @RequestMapping(method = RequestMethod.GET)
    public Iterable findAll() {
        return repository.findAll();
    }
    @RequestMapping(method = RequestMethod.POST)
    public ProductDetail create(@RequestBody ProductDetail detail) {
        return repository.save(detail);
    }
}

列表 1.6 —— Product Detail控制器類

Spring中提供的@RestController標注將通知該框架,讓框架為我們實現數據序列化與數據綁定的大部分繁重工作。此外,對于那些將為這個微服務生成數據的服務來說,我們只需為create()方法的參數標注為@RequestBody,Spring就能夠自動為我們生成該對象的內容。隨后就可以使用系統自動裝配的ProductDetailRepository對象保存相應的ProductDetail對象。Boot為Spring中內置提供的這些功能加入了一些額外的數據轉換器,它們將通過Jackson類庫,將ProductDetail對象序列化為JSON格式,以便微服務的API的調用者進行操作。在列表1.6中的控制器示例的基礎上,如果該服務的/products終結點收到了一個JSON格式的請求,那么該服務就會創建一個新的產品信息項,正如列表1.7中所描述的那樣。

{
    "productId": "DEF0000",
    "productName": "MakerBot",
    "shortDescription": "A product that makes other products",
    "longDescription": "This is an extended description for a makerbot, which is basically a product that makes other products.",
    "inventoryId": "00854321"
}

列表 1.7 —— 用于表現某個產品的JSON結構

通過對/products這個地址進行一個HTTP GET請求,可以刷新產品的詳細信息,并顯示新創建的產品細節內容。

在微服務的create()中基本上只有一個用例,就是進行數據綁定并保存到repository中。但在某些情況下,該服務還需要執行一些較復雜的業務邏輯,以確保保存到產品信息中的數據的準確性。我們可以通過使用Spring中內置的校驗框架,在進行數據綁定時確認產品信息中的數據符合微服務的業務邏輯。在列表1.8中的代碼展現了對ProductDetail校驗邏輯的一種實現,它將調用另一個微服務中的方法,以確定所提供的庫存ID的有效性。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.validation.*;
@Component
public class ProductDetailValidator implements Validator {
    private final InventoryService inventoryService;
    @Autowired
    public ProductDetailValidator(InventoryService inventoryService) {
        this.inventoryService = inventoryService;
    }
    @Override
    public boolean supports(Class<?>clazz) {
        return ProductDetail.class.isAssignableFrom(clazz);
    }
    @Override
    public void validate(Object target, Errors errors) {
        ProductDetail detail = (ProductDetail)target;
        if (!inventoryService.isValidInventory(detail.getInventoryId())) {
            errors.rejectValue("inventoryId", "inventory.id.invalid", "Inventory ID is invalid");
        }
    }
}

列表1.8 ——ProductDetail的校驗邏輯

這段示例代碼中的InventoryService中的邏輯有些生硬,但不難看出這種進行數據校驗的機制具有固有的靈活性,這也利益于該服務能夠對其它微服務進行查詢調用,以獲得其它微服務對于整個數據領域中某些子數據的信息。

為了在數據綁定時能夠使用ProductDetailValidator的功能,需要在Spring的數據綁定器中進行注冊,而注冊時機是特定于控制器的。在下方的列表1.9中對控制器的代碼進行了改動,展現了如何在控制器中對校驗器進行自動裝配,并通過initBinder()方法將其注冊進行數據綁定的過程。@InitBinder這個標注將通過Spring,我們將對這個類的默認數據綁定器進行自定義。此外,請注意thecreate()方法中的ProductDetail對象參數現在加上了一個@Valid標注,該標注的作用是通知數據綁定器,我們需要在數據綁定時對請求體進行校驗。而Spring中內置的校驗器也將提供JSR-303 與JSR-349這兩種數據校驗規范(Bean校驗)的字段級標注。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/products")
public class ProductDetailController {
    private final ProductDetailRepository repository;
    private final ProductDetailValidator validator;
    @Autowired
    public ProductDetailController(ProductDetailRepository repository, ProductDetailValidator validator) {
        this.repository = repository;
        this.validator = validator;
    }
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(validator);
    }
    @RequestMapping(method = RequestMethod.GET)
    public Iterable findAll() {
        return repository.findAll();
    }
    @RequestMapping(method = RequestMethod.POST)
    public ProductDetail create(@RequestBody @Valid ProductDetail detail) {
        return repository.save(detail);
    }
}

列表1.9 —— 經過修改后的Product Detail控制器,現在加入了校驗器

如果該API的調用者在POST提交的JSON結構中沒有包含一個有效的庫存ID,Spring將會產生一個校驗失敗的錯誤,并且為調用者返回一個“400 – Bad Request”的HTTP狀態碼。由于控制器的定義使用了RestController這個標注,因此Spring能夠將校驗失敗的信息進行正確地格式化,讓調用者能夠理解其內容。作為這個微服務的開發者,實現這一功能無需進行任何額外的配置。

對于電子商務網站這個示例來說,一個僅包含簡單的CRUD REST API的產品詳細信息微服務沒有什么太大的作用。這個服務還需要提供對產品信息結果列表進行分頁以及排序的功能,并且提供某種程序上的搜索功能。為了實現第一個需求,需要對ProductDetailController中的findAll()這個控制器action方法進行修改,讓它能夠接受由API使用者所定義的數據范圍所對應的查詢參數,然后該方法就可以使用Spring Data中內置的PagingAndSortingRepositorytype類,在findAll()方法中對repository進行調用時提供分頁及排序的參數。我們需要修改ProductDetailRepository,讓它繼承自這個新的類型,如列表1.10中的代碼所示。

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

列表1.10 —— 修改后的ProductDetailRepository提供了對分頁與排序的支持

列表1.11中的代碼展現了經過修改后的findAll這個控制器方法,它能夠利用repository中新的分頁與排序功能。如果某個對/products這個終結點的API調用提供了?page=0&count=20這個查詢字符串,該方法就能夠返回數據庫中的前20條結果。在這個示例中的代碼還利用了Spring的功能,為查詢參數賦予了默認值,因此這些參數中的大部分都成為可選參數了。

@RequestMapping(method = RequestMethod.GET)
public Iterable findAll(@RequestParam(value = "page", defaultValue = "0", required = false) int page,
@RequestParam(value = "count", defaultValue = "10", required = false) int count,
@RequestParam(value = "order", defaultValue = "ASC", required = false) Sort.Direction direction,
@RequestParam(value = "sort", defaultValue = "productName", required = false) String sortProperty) {
    Page result = repository.findAll(new PageRequest(page, count, new Sort(direction, sortProperty)));
    return result.getContent();
}

列表1.11 ——ProductDetailController中修改后的findAll方法,現在能夠支持分頁及排序功能

當該電子商務網站的用戶進行登陸頁面時,該網頁會通過貪婪查詢方式加載10條或20條結果,隨后當滾動條到達頁面上的某個位置,或是經過一段時間后,通過延遲加載的方式獲取之后的50條結果。通過這個內置的分頁功能,調用者就能夠控制每次調用需要返回的數據量。列表1.12中描述了ProductDetailController的完整實現。

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.*;
import org.springframework.http.*;
import org.springframework.validation.DataBinder;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.io.IOException;
@RestController
@RequestMapping("/products")
public class ProductDetailController {
    private final ProductDetailRepository repository;
    private final ProductDetailValidator validator;
    private final ObjectMapper objectMapper;
    @Autowired
    public ProductDetailController(ProductDetailRepository repository, ProductDetailValidator validator,
                                   ObjectMapper objectMapper) {
        this.repository = repository;
        this.validator = validator;
        this.objectMapper = objectMapper;
    }
    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.addValidators(validator);
    }
    @RequestMapping(method = RequestMethod.GET)
    public Iterable findAll(@RequestParam(value = "page", defaultValue = "0", required = false) int page,
         @RequestParam(value = "count", defaultValue = "10", required = false) int count,   
        @RequestParam(value = "order", defaultValue = "ASC", required = false) Sort.Direction direction,
        @RequestParam(value = "sort", defaultValue = "productName", required = false) String sortProperty) {
        Page result = repository.findAll(new PageRequest(page, count, new Sort(direction, sortProperty)));
        return result.getContent();
    }
    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public ProductDetail find(@PathVariable String id) {
        ProductDetail detail = repository.findOne(id);
        if (detail == null) {
            throw new ProductNotFoundException();
        } else {
            return detail;
        }
    }
    @RequestMapping(method = RequestMethod.POST)
    public ProductDetail create(@RequestBody @Valid ProductDetail detail) {
        return repository.save(detail);
    }
    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    public HttpEntity update(@PathVariable String id, HttpServletRequest request) throws IOException {
        ProductDetail existing = find(id);
        ProductDetail updated = objectMapper.readerForUpdating(existing).readValue(request.getReader());
        MutablePropertyValues propertyValues = new MutablePropertyValues();
        propertyValues.add("productId", updated.getProductId());
        propertyValues.add("productName", updated.getProductName());
        propertyValues.add("shortDescription", updated.getShortDescription());
        propertyValues.add("longDescription", updated.getLongDescription());
        propertyValues.add("inventoryId", updated.getInventoryId());
        DataBinder binder = new DataBinder(updated);
        binder.addValidators(validator);
        binder.bind(propertyValues);
        binder.validate();
        if (binder.getBindingResult().hasErrors()) {
            return new ResponseEntity<>(binder.getBindingResult().getAllErrors(), HttpStatus.BAD_REQUEST);
        } else {
            return new ResponseEntity<>(updated, HttpStatus.ACCEPTED);
        }
    }
    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public HttpEntity delete(@PathVariable String id) {
        ProductDetail detail = find(id);
        repository.delete(detail);
        return new ResponseEntity<>(HttpStatus.ACCEPTED);
    }
    @ResponseStatus(HttpStatus.NOT_FOUND)
    static class ProductNotFoundException extends RuntimeException {
    }
}

列表1.12 —— ProductDetailController的完整實現

毫無疑問,除了數據的分頁與排序之外,這個電子商務網站還需要提供一些類似于搜索引擎一樣的功能。由于在垂直分片中的每個微服務都對自己的數據領域子集進行維護,因此它也理應負責自身的搜索功能。這也讓調用者能夠異步地對整個數據領域中很大一部分的屬性進行搜索。

Spring Data允許對repository的接口附加某個方法簽名,在其中加入自定義的查詢。這表示repository能夠使用一種預先確定的JPA查詢,它將對每個產品信息對象中的一個屬性子集進行查詢,這就讓微服務能夠具備一些原始的搜索功能。在列表1.13中所描述的代碼中對ProductDetailRepository進行了修改,其中加入了一個search()方法,它能夠接受查詢語句,并嘗試對productName或longDescription字段進行大小寫無關的匹配,并將一個結果列表返回給調用 者。

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface ProductDetailRepository extends PagingAndSortingRepository<ProductDetail, String> {
    @Query("select p from ProductDetail p where UPPER(p.productName) like UPPER(?1) or " +
            "UPPER(p.longDescription) like UPPER(?1)")
    List search(String term);
}

列表1.1.3 ——ProductDetailRepository中的自定義查詢

為了公開這個搜索功能,我們將創建另一個RestController,并將它映射到/search這個終結點,如列表1.1.4中所示。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/search")
public class ProductDetailSearchController {
    private final ProductDetailRepository repository;
    @Autowired
    public ProductDetailSearchController(ProductDetailRepository repository) {
        this.repository = repository;
    }
    @RequestMapping(method = RequestMethod.GET)
    public List search(@RequestParam("q") String queryTerm) {
        List productDetails = repository.search("%"+queryTerm+"%");
        return productDetails == null ? new ArrayList<>() : productDetails;
    }
}

列表1.14 —— 用于對ProductDetail進行搜索的控制器 Search controller for ProductDetails

將來還可以進一步增加這個ProductDetailSearchController的功能,可以讓它在查詢時實現與ProductDetailController相同的分布及排序功能。

配置

Spring Boot中豐富的應用程序配置能夠讓創建的微服務具有強大的能力,而且在某些場合下完全不需要修改這些配置。當準備將服務進行部署時,也許要根據部署環境或某些外部的影響的結果對某些配置指令進行調整,例如在哪個端口上運行內嵌的容器。Boot為微服務的開發者提供了多種方式以重寫這些默認的配置,而且該框架也支持由多個不同的因素決定實際的配置。

在進行微服務的配置時,要仔細考慮某個重要的因素,即該服務的運行時環境。如果服務是部署在某個靜態的基礎設施中,那么對某些配置項進行預定義或許是可行的。為了更加清晰地說明這個問題,再來看看上面的那個示例,這個微服務的數據源只是一個簡單的內嵌的h2數據庫。而在生產環境中,該微服務將指向某個持久化的數據源,例如某個MySQL或Oracle數據庫,因此應用程序的配置必須包含正確的JDBC URL、用戶名、密碼,并且使用適當的JDBC驅動類。在靜態基礎設施中,可以對這些配置進行預定義,并直接打包在應用程序中。而Boot自身就提供了從Java屬性文件、XML配置文件或YAML配置文件中解析配置的功能,并相應地在classpath的根目錄中尋找名為application.properties、application.xml或application.yml(或是application.yaml)的配置文件。列表1.15中的配置文件展現了如何使用配置指令覆蓋默認的數據源配置。

spring.datasource.url=jdbc:mysql://prod-mysql/product
spring.datasource.username=root
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

列表1.15 —— 在配置文件中指定數據源

Boot中的配置機制有一個很重要的能力,它能夠在啟動時使用Java系統屬性對配置進行重寫。在JVM啟動中提供的配置將覆蓋在classpath中指定的application.properties中的內容。這就說明運行時環境能夠基于某些在對微服務進行打包時未知的信息對配置進行自定義。舉例來說,如果該微服務運行在某個非靜態環境,例如云端部署環境中,那么也許要根據VM或容器的地點來決定數據庫的托管。應用程序可以通過系統環境變量訪問這些信息。可以通過JVM的啟動參數,或是直接在配置中方便地調用這些環境變量。在后一種方式里,可以使用Spring中的屬性占位符獲取某個配置指令的引用。在列表1.16中展現的配置文件對列表1.15進行了一些修改,它使用了屬性占位符,并且包含一個默認值。

spring.datasource.url=${JDBC_URL:jdbc:mysql://prod-mysql/product}
spring.datasource.username=${JDBC_USER:root}
spring.datasource.password=${JDBC_PASS:}
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

列表1.16 —— 修改后的配置文件使用了帶有默認值的系統變量

Boot也將對文件系統進行查找,以尋找一個相對于啟動路徑的,名為“config”的目錄,并在其中尋找與之前所述相同的一系列配置文件,如果一旦找到,那么在應用classpath中找到的任何配置之前,它會首先應用這些配置。而spring.config.location這個Java系統屬性也能夠告訴Spring配置文件的地址。比方說,如果微服務的配置文件地址是/etc/spring/boot.yml,那么通過 –D spring.config.location=/etc/spring/boot.yml指令,其中的配置就能夠覆蓋文件系統中的配置信息。通過同樣的方式也可以使用classpath中定義的資源,只需在屬性的值前面加上classpath:前綴即可。

利用同樣的配置機制,通過server.port這個鍵,還可以自定義內嵌的容器的服務器端口。如果在某種PaaS云端環境,例如Heroku中運行微服務時,這一點尤為重要。這個鍵將通過某個環境變量映射端口的范圍,并將其暴露給應用程序。在列表1.16中的配置指令也能夠用于映射PORT環境變量。列表1.17就展現了這種配置的方式。

server.port=${PORT:8080}

列表1.17 —— 將啟動端口映射為某個環境變量的配置 Configuration to map the startup port to an environment var

打包

當微服務的部署已經準備就緒后,就可以使用Boot中提供的構建系統的工具,以生成一個輕量級的、可運行的部署文件。正如本文之前所說的一樣,Boot為Gradle和 Maven提供了插件,因此可以通過它們創建一個可運行的JAR文件用于發布。只需使用在前文中提到的Gradle構建腳本,就能夠通過調用項目的構造任務gradle build,簡單地生成JAR文件。Boot將對jar任務進行攔截,并將常規方式生成的文件進行重寫打包,在其中加入所有的依賴項,以生成所謂的“fat”或“uber”JAR文件。而在Maven項目的配置中,Boot插件也能夠攔截打包過程,并進行相同的重新打包操作。

Boot的Gradle插件還有一個額外的優點,就是它能夠與應用程序的插件進行交互,它將生成一個可發布的tarball文件,其中已經重新打包了所有依賴,以及在多種Unix及Windows上的啟動腳本。這種打包方式對于部署來說非常理想,因為微服務中所有的啟動腳本都已經寫入包里了。只需在目標服務器上將tarball解壓縮,就能夠在bin文件夾中以項目名稱命名的腳本中直接啟動微服務了。

雖然對于微服務來說,使用單一的可部署文件是受推薦的方式,也是一種接受度最高的可部署單元,但絕不是說必須強制使用單一的部署文件。Boot的功能更進一步,它能夠將應用程序打包為WAR文件,并部署到某個應用程序容器中。為了使用war插件,需要對Gradle的構建腳本進行一些修改,如列表1.18所示。與之前的示例相似,該構建任務將生成web文件。

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.RELEASE'
  }
}
apply plugin: 'spring-boot'
apply plugin: 'war'
repositories {
  jcenter()
}
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 'mysql:mysql-connector-java:5.1.34'
}

列表1.18 —— Gradle的構建腳本,使用了Boot以及War插件

在Maven項目中,可以通過修改項目的pom.xml文件中的打包配置實現war打包。列表1.19中的片段展現了經過修改后的配置。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.infoq</groupId>
    <artifactId>sb-microservices</artifactId>
    <version>0.1.0</version>
    <packaging>war</packaging>
    <!-- ...remaining omitted for brevity... -->
</project>

列表1.19 —— 可以用于War打包的Maven pom.xml的起始部分代碼

網關API

在之前的章節中,我們已經深入地探討了產品信息這個微服務的開發,這套模板同樣也能夠以相似的方式應用于這個電子商務網站的垂直分片中的其它每一種服務中。當系統的每個垂直分片中的各個組件被分解為一系列微服務的集合之后,該系統就可被視為一種完全分布式的微服務架構。但對于外部調用者,例如該電子商務網站的頁面來說,這種方式也會產生一些復雜性,因為這些調用者從系統中獲取的數據可能會橫跨多個不同的微服務。如果不通過某種機制,將這些服務綜合地重新組織為一種看起來具有整體性的API,那么每個客戶端的調用者將不得不承擔這種職責,它們將分別調用這些不同的數據集,并將它們重新組織為一種可重用的結構。這種方式對于調用者產生了很大的負責,因為他們必須建立大量的HTTP連接,以實現對某些數據集的聚合。這也意味著如果某個服務不可用或掉線,那么每個調用者將負責對數據缺失這一場景進行適當的處理。

用于微服務基礎設施中的某種模式正在逐漸浮現,這種模式體現了一種網關API服務的概念,這種服務處于各個不同的后端服務的前方,并為調用者提供一種全面的、易于使用的API。繼續以電子商務網站的例子進行講解,當網站的某個訪問者打算查看某個產品的詳細信息時,為了生成產品信息視圖的數據,需要四種服務參與其中。該網頁不再對這些服務分別進行調用,而是訪問該網關服務的某個聚合API的終結點,網關服務會代為調用底層服務,并將結果集進行合并,返回頁面進行顯示。從網頁的角度來看,它只是發送了一個調用請求,就獲得了顯示頁面所必須的完整數據。

這種方式還具有一個額外的好處,就是可以在調用者與后臺服務之間更好地管理數據的傳輸。比方說,該網關服務在它的服務層可以實現某些邏輯,當對某個產品的信息的訪問量很大的時候,它將不會在每個請求中都去調用相應的產品信息微服務,而是選擇在某個預定義的時間段之內直接返回緩存中的數據。這種方式能夠顯著地提升性能,并減少網絡負載。

同樣重要的一點還在于對后臺服務的可用性進行抽象。一旦發生某個后臺服務不可用的情況下,網關服務能夠明智地決定應該提供怎樣的數據。現實這一點的方式有多種,而在網關服務中確保分布式系統的持久性這方面最引人注目的一種機制是由Netflix開發的名為Hystrix的類庫。在Hystrix中有許多功能能夠確保對故障的適應性,并且對于海量請求提供了性能方面的優化,但其中最吸引人的特性大概要數它對斷路設計模式的實現了。具體來說,Hystrix能夠觀察到對某個后端服務的連接斷開,在這種情況下它不會選擇持續訪問這個下線的服務,因為這會造成網絡阻塞以及等待超時,而是打開這個服務的回路,將后續的請求委托給某個“后備”方法,讓它接管這些調用。在底層,Hystrix會間隔式地檢查該連接,查看該后臺服務是否已經恢復了正常操作狀態,如果服務已經恢復,那么它就會重新建立通信連接。

當回路打開的期間,網關服務能夠任意選擇返回給調用者的響應。可以使用某些“最后一次正常運行時”的正確數據集,也可以返回一個空響應,在頭信息中告知調用者后端回路已經打開,或是以上兩者的某種結合。Hystrix提供的適應性在任何一個具有一定復雜性的分布式系統中都是一種關鍵的組件。為了更直白地理解Hystrix的能力,我們再回頭來看看這個電子商務中的產品垂直分片,其中必須調用四種服務,以獲得產品信息視圖中對應的數據。列表1.20展現了使用網關API服務的ProductService的代碼。

import com.netflix.hystrix.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.concurrent.*;
@Service
public class ProductService {
    private static final String GROUP = "products";
    private static final int TIMEOUT = 60000;
    private final ProductDetailService productDetailService;
    private final ProductPricingService productPricingService;
    private final ProductRatingService productRatingService;
    private final ProductReviewService productReviewService;
    @Autowired
    public ProductService(ProductDetailService productDetailService, ProductPricingService productPricingService,
                          ProductRatingService productRatingService, ProductReviewService productReviewService) {
        this.productDetailService = productDetailService;
        this.productPricingService = productPricingService;
        this.productRatingService = productRatingService;
        this.productReviewService = productReviewService;
    }
    public Map<String, Map<String, Object>> getProductSummary(String productId) {
        List<Callable<AsyncResponse>> callables = new ArrayList<>();
        callables.add(new BackendServiceCallable("details", getProductDetails(productId)));
        callables.add(new BackendServiceCallable("pricing", getProductPricing(productId)));
        return doBackendAsyncServiceCall(callables);
    }
    public Map<String, Map<String, Object>> getProduct(String productId) {
        List<Callable<AsyncResponse>> callables = new ArrayList<>();
        callables.add(new BackendServiceCallable("details", getProductDetails(productId)));
        callables.add(new BackendServiceCallable("pricing", getProductPricing(productId)));
        callables.add(new BackendServiceCallable("ratings", getProductRatings(productId)));
        callables.add(new BackendServiceCallable("reviews", getProductReviews(productId)));
        return doBackendAsyncServiceCall(callables);
    }
    private static Map<String, Map<String, Object>> doBackendAsyncServiceCall(List<Callable<AsyncResponse>> callables) {
        ExecutorService executorService = Executors.newFixedThreadPool(4);
        try {
            List<Future<AsyncResponse>> futures = executorService.invokeAll(callables);
            executorService.shutdown();
            executorService.awaitTermination(TIMEOUT, TimeUnit.MILLISECONDS);
            Map<String, Map<String, Object>> result = new HashMap<>();
            for (Future<AsyncResponse> future : futures) {
                AsyncResponse response = future.get();
                result.put(response.serviceKey, response.response);
            }
            return result;
        } catch (InterruptedException|ExecutionException e) {
            throw new RuntimeException(e);
        }
    }
    @Cacheable
    private HystrixCommand<Map<String, Object>> getProductDetails(String productId) {
        return new HystrixCommand<Map<String, Object>>(
                HystrixCommand.Setter
                        .withGroupKey(HystrixCommandGroupKey.Factory.asKey(GROUP))
                        .andCommandKey(HystrixCommandKey.Factory.asKey("getProductDetails"))
                        .andCommandPropertiesDefaults(
                                HystrixCommandProperties.Setter()
.withExecutionIsolationThreadTimeoutInMilliseconds(TIMEOUT)
                        )
        ) {
            @Override
            protected Map<String, Object> run() throws Exception {
                return productDetailService.getDetails(productId);
            }
            @Override
            protected Map getFallback() {
                return new HashMap<>();
            }
        };
    }
    private HystrixCommand<Map<String, Object>> getProductPricing(String productId) {
        // ... snip, see getProductDetails() ...
    }
    private HystrixCommand<Map<String, Object>> getProductRatings(String productId) {
        // ... snip, see getProductDetails() ...
    }
    private HystrixCommand<Map<String, Object>> getProductReviews(String productId) {
        // ... snip, see getProductDetails() ...
    }
    private static class AsyncResponse {
        private final String serviceKey;
        private final Map<String, Object> response;
        AsyncResponse(String serviceKey, Map<String, Object> response) {
            this.serviceKey = serviceKey;
            this.response = response;
        }
    }
    private static class BackendServiceCallable implements Callable<AsyncResponse> {
        private final String serviceKey;
        private final HystrixCommand<Map<String, Object>> hystrixCommand;
        public BackendServiceCallable(String serviceKey, HystrixCommand<Map<String, Object>> hystrixCommand) {
            this.serviceKey = serviceKey;
            this.hystrixCommand = hystrixCommand;
        }
        @Override
        public AsyncResponse call() throws Exception {
            return new AsyncResponse(serviceKey, hystrixCommand.execute());
        }
    }
}

列表1.20 – 某個異步網關API服務的示例,其中使用了Hystrix

以上示例中的服務可以用于RESTful HTTP客戶端,這些客戶端可以基于Spring中的RestTemplate進行創建,或是使用其它的某種HTTP客戶框架,例如Retrofit。getProductSummary()方法對后端服務發起了一個異步調用,該服務將用于獲取登陸頁面所需的產品信息。與之類似,getProduct()方法將從所有相關的后端服務中獲取某個產品的詳細信息,并將信息進行合并,以便API的調用者使用。在這個示例中,產品的信息很少會發生變化,該網關服務也應該盡量減少對于后端服務的調用次數,因此getProductDetails()方法利用了Spring提供的@Cacheable標注,在一段合理的時間之內對調用進行緩存。網關服務隨后將通過某個映射到/products路由上的RestController獲取綜合的產品信息。對于微服務架構中的其它垂直分片,也可以采用類似的方式設計終結點,系統API的調用者也能夠以一種在更傳統的整體性應用程序中相同的方式來訪問新的終結點。

結論

Spring Boot很早就意識到將整體性服務分解為分布式微服務所帶來的優點,它的設計宗旨是讓開發與創建微服務成為一種節省資源的,專注于開發者的流程。通過框架中所提供的啟動模塊以啟用自動配置機制,應用程序就能夠方便地充分利用強大的功能子集,否則開發者將不得不進行明確的配置,并通過編程方式進行組裝。這些自動配置的模塊可以作為開發一個完整的微服務架構的基礎,其中已經內置了一種網關API服務。

關于作者

使用Sprint Boot創建微服務 Daniel Woods是一位技術狂熱者,尤其是在企業級的Java、Groovy,和Grails開發方面。他在JVM軟件開發方面已經具有超過十年以上的經驗,并且通過對GrailsRatpack web框架這樣的開源項目進行貢獻的方式分享他的經驗。Dan也是Gr8conf和SpringOne 2GX會議上的演講者,他在會議上展現了他在JVM的企業級應用架構上的專業知識。

 

查看英文原文:Building Microservices with Spring Boot

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!