Spring Boot與微服務(wù)
微服務(wù)架構(gòu)是當下構(gòu)建互聯(lián)網(wǎng)應(yīng)用的主流架構(gòu)。在Spring家族中,專門有著用于構(gòu)建微服務(wù)架構(gòu)的Spring Cloud框架。而Spring Cloud框架本身則是構(gòu)建在Spring Boot之上。
在本節(jié)中,我們將討論Spring Boot與SpringCloud的關(guān)系,并給出Spring微服務(wù)架構(gòu)的案例分析。
微服務(wù)架構(gòu)簡介
微服務(wù)架構(gòu)是一種架構(gòu)模式,區(qū)別于其他系統(tǒng)架構(gòu)的構(gòu)建方式和技術(shù)方案,微服務(wù)架構(gòu)具有其固有特點。微服務(wù)架構(gòu)的提出者Martin Fowler在其文章“Microservices”中定義了服務(wù)組件化、去中心化、基礎(chǔ)設(shè)施自動化等多個微服務(wù)架構(gòu)特點,正是這些特點為我們使用微服務(wù)架構(gòu)進行系統(tǒng)設(shè)計提供了主要的切入點。
從實施角度來講,我們可以基于這些微服務(wù)架構(gòu)的特點提煉出構(gòu)建微服務(wù)架構(gòu)三大要素,即服務(wù)建模、技術(shù)體系和研發(fā)過程。
微服務(wù)架構(gòu)設(shè)計首要的切入點在于服務(wù)建模,因為微服務(wù)架構(gòu)與傳統(tǒng)SOA等技術(shù)架構(gòu)的本質(zhì)區(qū)別就是其服務(wù)的粒度不同,以及服務(wù)本身有面向業(yè)務(wù)和組件化的特性。針對服務(wù)建模,我們首先需要明確服務(wù)的類別以及服務(wù)與業(yè)務(wù)之間的關(guān)系,盡可能明確領(lǐng)域的邊界。對服務(wù)建模,推薦使用領(lǐng)域驅(qū)動設(shè)計(DomAIn Driven Design,DDD)方法,通過識別領(lǐng)域中的各個子域、判斷這些子域是否獨立、考慮子域與子域的交互關(guān)系,明確各個限界上下文(Boundary Context)之間的邊界。
微服務(wù)架構(gòu)的第二大要素就是它的技術(shù)體系,這也是我們學(xué)習(xí)微服務(wù)架構(gòu)的主體內(nèi)容。同樣,不同的開發(fā)技術(shù)和框架都會基于自身的設(shè)計理念給出各自的技術(shù)體系類型及其實現(xiàn)方式。一個完整的微服務(wù)架構(gòu),通常需要包括服務(wù)通信、服務(wù)治理、服務(wù)路由、服務(wù)容錯、服務(wù)網(wǎng)關(guān)、服務(wù)配置、服務(wù)安全和服務(wù)監(jiān)控等核心技術(shù)體系,如圖13-3所示。

圖13-3 微服務(wù)架構(gòu)的核心技術(shù)體系
過程轉(zhuǎn)變與技術(shù)體系建設(shè)有密切關(guān)聯(lián),所以對于微服務(wù)架構(gòu)而言,最后一個要素就是研發(fā)過程。Martin Fowler在介紹微服務(wù)架構(gòu)時,同樣也提出了“圍繞業(yè)務(wù)功能組織團隊”的研發(fā)管理理念。
本書關(guān)注Spring Boot及其生態(tài)系統(tǒng),因此,我們的重點還是如何基于Spring家族來開發(fā)微服務(wù)架構(gòu)。在Spring家族中,為開發(fā)人員提供了開發(fā)微服務(wù)架構(gòu)整套解決方案的就是Spring Cloud框架。
Spring Cloud與Spring Boot
Spring Cloud具備一個天生的優(yōu)勢,它是Spring家庭的一員,而Spring在JAVA EE開發(fā)領(lǐng)域的強大地位給Spring Cloud起到很好的推動作用。同時,Spring Cloud基于Spring Boot構(gòu)建,而Spring Boot已經(jīng)成為Java EE領(lǐng)域中最流行的開發(fā)框架,用來簡化Spring應(yīng)用程序的框架搭建和開發(fā)過程。
在微服務(wù)架構(gòu)中,我們將通過Spring Boot來開發(fā)單個微服務(wù)。同樣作為Spring家族的新成員,Spring Boot提供了令人興奮的特性,這些特性主要體現(xiàn)在開發(fā)過程的簡單化,包括支持快速構(gòu)建項目、不依賴外部容器獨立運行、開發(fā)部署效率高以及與云平臺天然集成等,我們已經(jīng)在前面的章節(jié)中對這些功能特性做了詳細的介紹。而在微服務(wù)架構(gòu)中,Spring Cloud構(gòu)建在Spring Boot之上,繼承了Spring Boot簡單配置和快速開發(fā)的特點,讓原本復(fù)雜的架構(gòu)工作變得相對容易上手。
技術(shù)組件的完備性是Spring Cloud的主要優(yōu)勢。Spring Cloud是一系列框架的有序集合。它利用Spring Boot開發(fā)的便利性巧妙地簡化了微服務(wù)系統(tǒng)基礎(chǔ)設(shè)施的開發(fā)過程,如服務(wù)發(fā)現(xiàn)注冊、API網(wǎng)關(guān)、配置中心、消息總線、負載均衡、熔斷器、數(shù)據(jù)監(jiān)控等都可以使用Spring Boot的開發(fā)風(fēng)格做到一鍵啟動和部署。
在對微服務(wù)的各項技術(shù)組件進行設(shè)計和實現(xiàn)的過程中,Spring Cloud也有一些自己的特色。一方面,它對微服務(wù)架構(gòu)開發(fā)所需的技術(shù)組件進行了抽象,提供了符合開發(fā)需求的獨立組件,包括用于配置中心的Spring CloudConfig、用于API網(wǎng)關(guān)的Spring Cloud Gateway等。另一方面,Spring Cloud也沒有重復(fù)造輪子,它將目前各家公司比較成熟、經(jīng)得起實踐考驗的服務(wù)框架組合起來,使用Spring Boot開發(fā)風(fēng)格進行了再次封裝。這部分主要指的是Spring Cloud.NETflix組件。Spring Cloud Netflix基于Spring Boot集成了Netflix OSS中的諸多核心組件,其中與服務(wù)治理相關(guān)的除了用于服務(wù)注冊和發(fā)現(xiàn)的Eureka之外,實際上還有用于實現(xiàn)客戶端負載均衡的Ribbon和用于實現(xiàn)聲明式REST客戶端的Feign等。Netflix OSS、Spring Boot、Spring CloudNetflix以及Spring Cloud之間的關(guān)系如圖13-4所示。

圖13-4 Spring Cloud、Spring Cloud Netflix、Spring Boot與Netflix OSS之間的關(guān)系
Spring Cloud屏蔽了微服務(wù)架構(gòu)開發(fā)所需的復(fù)雜的配置和實現(xiàn)過程,最終給開發(fā)者提供了一套易理解、易部署和易維護的開發(fā)工具包。SpringCloud中的組件非常多,本書不對圖13-4中的所有組件詳細講解,而是通過一個案例來展示Spring Cloud的使用方法,這就是接下來的內(nèi)容。
Spring微服務(wù)架構(gòu)案例分析
本節(jié)將基于Spring Boot和Spring Cloud來演示微服務(wù)系統(tǒng)的構(gòu)建過程,我們將結(jié)合案例重點介紹注冊中心、配置中心以及API網(wǎng)關(guān)這三個核心的微服務(wù)技術(shù)組件,以及如何在這三個技術(shù)組件的基礎(chǔ)上完成整個微服務(wù)系統(tǒng)的實現(xiàn)。
1. 案例業(yè)務(wù)場景和服務(wù)拆分
現(xiàn)在,讓我們構(gòu)建一個精簡但又完整的系統(tǒng)來展示微服務(wù)架構(gòu)的設(shè)計理念和常見實現(xiàn)技術(shù)。本案例的業(yè)務(wù)場景是訂單管理,需要對互聯(lián)網(wǎng)應(yīng)用中最常見的商品、訂單業(yè)務(wù)做抽象。現(xiàn)實環(huán)境中訂單業(yè)務(wù)可以非常復(fù)雜,該案例的目的在于介紹系統(tǒng)實現(xiàn)的技術(shù)體系,不在于介紹具體業(yè)務(wù)邏輯,所以在業(yè)務(wù)領(lǐng)域建模上做了高度抽象和簡化。
按照實施微服務(wù)架構(gòu)的基本思路,服務(wù)識別和拆分是案例分析的第一步。案例包含的業(yè)務(wù)場景比較簡單,主要針對用戶的下單操作。在提交訂單的過程中,我們需要對商品和用戶信息進行驗證。因此,我們可以把該過程對應(yīng)的系統(tǒng)分成三個服務(wù),即商品服務(wù)(goods-service)、訂單服務(wù)(order-service)和用戶服務(wù)(user-service)。
以上三個微服務(wù)構(gòu)成了案例的業(yè)務(wù)服務(wù),而若要構(gòu)建一個完整的微服務(wù)系統(tǒng),我們還需要引入其他基礎(chǔ)設(shè)施類服務(wù),這些服務(wù)從不同的角度為實現(xiàn)微服務(wù)架構(gòu)提供支持。在本書中,我們重點介紹注冊中心、配置中心和API網(wǎng)關(guān)這三個基礎(chǔ)設(shè)施類服務(wù)。無論是業(yè)務(wù)服務(wù)、還是基礎(chǔ)設(shè)施類服務(wù),本質(zhì)上它們都是一個Spring Boot應(yīng)用程序。
2. 注冊中心
基于Spring Cloud實現(xiàn)注冊中心的方式有多種。在本文中,我們將采用Spring Cloud Netflix中的Eureka來構(gòu)建用于服務(wù)發(fā)現(xiàn)和服務(wù)注冊的注冊中
心。Eureka同時具備服務(wù)器端組件和客戶端組件,其中客戶端組件內(nèi)嵌在各個業(yè)務(wù)微服務(wù)中;而服務(wù)器端組件則是獨立的,所以需要構(gòu)建一個Eureka服務(wù),并將這個服務(wù)命名為eureka-server。
我們創(chuàng)建一個Spring Boot應(yīng)用程序,并在該應(yīng)用程序的pom文件中添加Eureka服務(wù)器端組件依賴包,如代碼清單13-23所示。
代碼清單13-23 Eureka服務(wù)器端依賴包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka
server</artifactId>
</dependency>
引入Maven依賴之后就可以創(chuàng)建Spring Boot的啟動類,在示例代碼中,我們把該啟動類命名為EurekaServerApplication,如代碼清單13-24所示。
代碼清單13-24 EurekaServerApplication類代碼
@SpringBootApplication
@EnableEurekaServerpublic class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
請注意,在上面的代碼中,我們在啟動類上加了一個@EnableEurekaServer注解。在Spring Cloud中,包含@EnableEurekaServer注解的服務(wù)意味著就是一個Eureka服務(wù)器組件。
構(gòu)建eureka-server的另一件事情是對Spring Boot的配置文件進行處理,并添加如代碼清單13-25所示的配置項。
代碼清單13-25 eureka-server配置
eureka:
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761
在這些配置項中,我們看到了三個以eureka.client開頭的客戶端配置項,分別是register-WithEureka、fetchRegistry和serviceUrl。從配置項的命名上不難看出,registerWithEureka用于指定是否把當前的客戶端實例注冊到Eureka服務(wù)器中,而fetchRegistry則指定是否從Eureka服務(wù)器上拉取服務(wù)注冊信息。這兩個配置項默認都是true,但這里都將其設(shè)置為false。而最后的serviceUrl配置項用于設(shè)置服務(wù)地址。
3. 配置中心
與Eureka一樣,基于Spring Cloud Config構(gòu)建的配置中心同樣存在服務(wù)器端組件和客戶端組件,其服務(wù)器端組件也需要構(gòu)建一個獨立的配置服務(wù)。我們將這個服務(wù)命名為config-server,并在pom文件中引入spring-cloudconfig-server和
spring-cloud-starter-config這兩個Maven依賴,其中前者包含了用于構(gòu)建配置服務(wù)器的各種組件,如代碼清單13-26所示。
代碼清單13-26 Spring Cloud Config服務(wù)器依賴包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
接下來我們在新建的config-server工程中添加一個Bootstrap類ConfigServerApplication,如代碼清單13-27所示。
代碼清單13-27 ConfigServerApplication類代碼
@SpringCloudApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
除了熟悉的@SpringCloudApplication注解之外,我們還看到這里添加了一個新的注解@EnableConfigServer。有了這個注解,配置服務(wù)器就可以將所存儲的配置信息轉(zhuǎn)化為RESTful接口數(shù)據(jù)供各個業(yè)務(wù)微服務(wù)在分布式環(huán)境下使用。
同樣,在config-server工程的配置文件中,我們也需要添加與配置中心相關(guān)的配置項,如代碼清單13-28所示。
代碼清單13-28 config-server配置
spring:
cloud:
config:
server:
native:
searchLocations: classpath: config/
classpath: config/userservice,
classpath: config/goodsservice,
classpath: config/orderservice
可以看到,這里通過searchLocations配置項設(shè)置了Spring CloudConfig從本地文件系統(tǒng)中讀取配置文件的目錄,分別是config目錄下的userservice、goodsservice和orderservice這三個子目錄。請注意這三個子目錄的名稱必須與各個服務(wù)自身的名稱完全一致。然后我們在這三個子目錄下面都放入以服務(wù)名稱命名的針對不同運行環(huán)境的.yml配置文件。
4. API網(wǎng)關(guān)
對于API服務(wù)而言,無論是使用Spring Cloud Netflix中的Zuul還是使用Spring自建的Spring Cloud Gateway,都需要構(gòu)建一個獨立的服務(wù)來承接路由、安全和監(jiān)控等各種功能。這里,我們以Spring Cloud Gateway為例構(gòu)建一個獨立的gateway-server服務(wù)。在技術(shù)上,Spring Cloud Gateway基于最新的Spring 5和Spring Boot 2,以及用于響應(yīng)式編程的Project Reactor框架,提供響應(yīng)式、非阻塞式I/O模型,所以Spring Cloud Gateway為開發(fā)人員提供了更好的性能支持。要想在微服務(wù)架構(gòu)中引入Spring Cloud Gateway,我們同樣需要構(gòu)建一個獨立的Spring Boot應(yīng)用程序,并在Maven中添加如代碼清單13-29所示的依賴項。
代碼清單13-29 Spring Cloud Gateway依賴包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
按照約定,我們把這個獨立的微服務(wù)命名為gateway-server,然后在作為Bootstrap類的GatewayApplication上添加@EnableDiscoveryClient注解即可,如代碼清單13-30所示。
代碼清單13-30 GatewayApplication類代碼
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
Spring Cloud Gateway中的核心概念有兩個,一個是過濾器,一個是謂詞(Predicate)。Spring Cloud Gateway中的過濾器用于在響應(yīng)HTTP請求之前或之后修改請求本身及對應(yīng)的響應(yīng)結(jié)果。而所謂謂詞,本質(zhì)上是一種判斷條件,用于將HTTP請求與路由進行匹配。接下來,我們通過一個完整的路由配置來展示謂詞和過濾器規(guī)則的配置方法,如代碼清單13-31所示。
代碼清單13-31 gateway-server配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: user-route
uri: lb://userservice
predicates:
- Path=/user/**
- id: goods-route
uri: lb://goodsservice
predicates:
- Path=/good/**
- id: order-route
uri: lb://orderservice
predicates:
- Path=/order/**
在上述配置中,有幾個注意點。首先我們使用id配置項為三個業(yè)務(wù)服務(wù)指定了路由信息的規(guī)則編號,分別為user-route、goods-route和orderroute。而uri配置項中的lb代表負載均衡LoadBalance,也就是說在訪問URL指定的服務(wù)名稱時需要集成負載均衡機制。請注意lb配置項中指定的服務(wù)名稱需要與保存在Eureka中的服務(wù)名稱完全一致。然后我們使用了謂詞來對請求路徑進行匹配,例如這里的Path=/order/**代表所有以/order開頭的請求都將被路由到這條路徑中。
5. 業(yè)務(wù)服務(wù)整合
接下來,我們基于前面構(gòu)建的基礎(chǔ)設(shè)施服務(wù)來實現(xiàn)各個業(yè)務(wù)服務(wù)。我們知道,各個業(yè)務(wù)服務(wù)本質(zhì)上都是一個Spring Boot應(yīng)用程序。在這些SpringBoot應(yīng)用程序中,首先需要引入Spring Cloud組件,如代碼清單13-32所示。
代碼清單13-32 Spring Cloud依賴包
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
同時,作為微服務(wù)系統(tǒng)中的單個微服務(wù),這些Spring Boot應(yīng)用程序都應(yīng)該注冊到Eureka注冊中心,并完成與配置中心的集成。因此,在pom文件中,我們也需要添加如代碼清單13-33所示的依賴項。
代碼清單13-33 注冊中心和配置中心客戶端依賴包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka
client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
對應(yīng)地,在配置文件中,我們也需要分別添加對注冊中心和配置中心的引用,如代碼清單13-34所示。
代碼清單13-34 注冊中心和配置中心客戶端配置項
eureka:
instance:
preferIpAddress: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
cloud:
config:
enabled: true
uri: http://localhost:8888
現(xiàn)在,order-service、user-service和goods-service這三個業(yè)務(wù)服務(wù)都融入了微服務(wù)架構(gòu)的體系中,接下來要做的就是根據(jù)業(yè)務(wù)邏輯實現(xiàn)業(yè)務(wù)服務(wù)之間的整合。在案例中,我們以最典型的下訂單業(yè)務(wù)流程為例,給出在微服務(wù)架構(gòu)下業(yè)務(wù)服務(wù)之間整合的實現(xiàn)過程。
通常,在用戶下訂單的過程中,我們需要傳入用戶信息以及商品信息,然后系統(tǒng)通過校驗用戶信息和商品信息的正確性來決定是否生成一個合法的訂單。在這個過程中,order-service需要基于REST API完成與user-service和goods-service之間的遠程交互,如圖13-5所示。

圖13-5 業(yè)務(wù)服務(wù)交互關(guān)系圖
在order-service中,我們將暴露一個用于生成訂單的HTTP端點,如代碼清單13-35所示。
代碼清單13-35 OrderController中的HTTP端點
@RestController
@RequestMapping(value = "orders")
public class OrderController {
@Autowired
OrderService orderService;
@GetMapping(value = "/{userId}/{goodsCode}")
public Order addOrder(@PathVariable("userId") String userId,
@PathVariable("goodsCode") String goodsCode) {
return orderService.generateOrder(userId, goodsCode);
}
}
可以看到,在上述addOrder()方法中,我們傳入用戶的ID以及商品的編號,并調(diào)用OrderService來生成訂單。在OrderService中,負責具體生成訂單信息的generateOrder()方法如代碼清單13-36所示。
代碼清單13-36 OrderService中的generateOrder()方法實現(xiàn)代碼
public Order generateOrder(String userId, String goodsCode) {
Order order = new Order();
//獲取遠程Goods信息
GoodMapper goods = getGoodsFromRemote(goodsCode);
if (goods == null) {
return order;
}
//獲取遠程User信息
UserMapper user = getUserFromRemote(userId);
if (user == null) {
return order;
}
order.setOrderNumber("DemoOrderNumber");
order.setDeliveryAddress("DemoDeliveryAddress");
order.setUserId(userId);
order.setGoodsName(goods.getName());
order.setCreateTime(new Date());
orderRepository.save(order);
return order;
}
上述方法的執(zhí)行流程非常明確,我們分別通過getGoodsFromRemote()和getUserFromRemote()方法遠程獲取商品信息和用戶信息。這個過程就涉及微服務(wù)之間的相互調(diào)用。這里以getUserFromRemote()方法為例,來看對應(yīng)的UserRestTemplateClient遠程訪問工具類的實現(xiàn)過程,如代碼清單13-37所示。
代碼清單13-37 UserRestTemplateClient類實現(xiàn)代碼
@Service
public class UserRestTemplateClient {
@Autowired RestTemplate restTemplate;
public UserMapper getUserById(String userId) {
ResponseEntity<UserMapper> result =
restTemplate.exchange("http://userservice/users/{userId}",
HttpMethod.GET, null, UserMapper.class, userId);
return result.getBody();
}
public UserMapper getUserByUserName(String userName) {
ResponseEntity<UserMapper> result =
restTemplate.exchange("http://userservice/users/userName/{userName}",
HttpMethod.GET, null, UserMapper.class, userName);
UserMapper user = result.getBody();
return user;
}
}
這里我們通過RestTemplate發(fā)起HTTP請求并獲取響應(yīng)結(jié)果,整個遠程調(diào)用過程與第4章介紹的內(nèi)容完全一致。唯一的區(qū)別在于這里請求URL使用的并不是一個具體的IP地址,而是對應(yīng)微服務(wù)的服務(wù)名userservice。能夠?qū)崿F(xiàn)這一效果的原因是Spring Cloud可以對默認的RestTemplate做改造,如代碼清單13-38所示。
代碼清單13-38 添加了@LoadBalanced注解的RestTemplate
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
可以看到,我們在創(chuàng)建的RestTemplate上添加了一個@LoadBalanced注解。@LoadBalanced注解用于修飾發(fā)起HTTP請求的RestTemplate工具類,在該工具類中自動與注冊中心集成,并嵌入客戶端負載均衡功能。這樣,開發(fā)人員不需要針對負載均衡做任何特殊的開發(fā)或配置,就能實現(xiàn)基于服務(wù)名進行遠程方法調(diào)用。
請注意,上述實現(xiàn)方式中我們并沒有集成API網(wǎng)關(guān)功能。想要發(fā)揮SpringCloud Gateway所引入的服務(wù)路由機制,基于前面配置的路由規(guī)則,我們可以采用如代碼清單13-39所示的實現(xiàn)方式。
代碼清單13-39 通過API網(wǎng)關(guān)執(zhí)行遠程服務(wù)調(diào)用的實現(xiàn)代碼
public UserMapper getUserById(String userId) {
ResponseEntity<UserMapper> result =
restTemplate.exchange("http://gatewayservice/user/users/{userId}",
HttpMethod.GET, null, UserMapper.class, userId);
return result.getBody();
}