通過(guò)前幾篇文章的積累,我們現(xiàn)在可以來(lái)動(dòng)手搭建一個(gè)完整的Spring Cloud Demo項(xiàng)目了捎谨。為了更清楚的說(shuō)明Spring Cloud的結(jié)構(gòu)特點(diǎn)症革,我們的demo項(xiàng)目還是遵循由淺入深的原則,一開(kāi)始只加入一些基本的特性强戴,后面再來(lái)逐步完善悉患。
業(yè)務(wù)背景
本來(lái)演示技術(shù)點(diǎn)的demo残家,弄一些sayHello的方法出來(lái)也無(wú)可厚非。但Spring Cloud的很多特性都是與業(yè)務(wù)的實(shí)際需求緊密結(jié)合的售躁,脫離業(yè)務(wù)談技術(shù)難免顯得有些空洞跪削,所以我們也需要為demo弄一個(gè)簡(jiǎn)單的業(yè)務(wù)背景谴仙。就用常見(jiàn)的訂單管理和倉(cāng)儲(chǔ)管理來(lái)舉例子吧迂求,一個(gè)基本的業(yè)務(wù)流程就是客戶創(chuàng)建訂單 碾盐,如下圖:
為此我們需要?jiǎng)?chuàng)建兩個(gè)微服務(wù)模塊:訂單服務(wù)和倉(cāng)儲(chǔ)服務(wù),訂單服務(wù)提供創(chuàng)建訂單的接口揩局,創(chuàng)建訂單的同時(shí)需要調(diào)用倉(cāng)儲(chǔ)服務(wù)提供的接口來(lái)對(duì)庫(kù)存進(jìn)行更新毫玖。
組件選擇和環(huán)境搭建
之前我們介紹過(guò),每一種Spring Cloud的特性的實(shí)現(xiàn)都有好幾種不同的框架和工具進(jìn)行選擇凌盯,根據(jù)官方的推薦和技術(shù)的流行度付枫,demo主要采用了以下的框架和工具:
- Consul:用于服務(wù)的注冊(cè)和發(fā)現(xiàn)
- OpenFeign:聲明式HTTP客戶端,服務(wù)間調(diào)用
- Spring Cloud Loadbalancer:客戶端負(fù)載均衡
- Hystrix:斷路保護(hù)
- Spring Cloud Gateway:智能路由驰怎,服務(wù)網(wǎng)關(guān)
這里面需要提前安裝配置的組件只有一個(gè)Consul阐滩,可以參考我之前的文章 Consul的架構(gòu)和配置
新建Maven項(xiàng)目
我們首先為所有的服務(wù)模塊創(chuàng)建一個(gè)Parent項(xiàng)目,用于管理各種公共的依賴包县忌,pom.xml如下掂榔,需要注意的地方請(qǐng)查看注釋:
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.github.davidfantasy</groupId>
<artifactId>spring-cloud-demo</artifactId>
<version>1.0.0</version>
<modules>
<!--訂單管理服務(wù)-->
<module>order-service</module>
<!--倉(cāng)儲(chǔ)管理服務(wù)-->
<module>storage-service</module>
</modules>
<name>spring-cloud-demo</name>
<description>Demo project for Spring Cloud</description>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR3</spring-cloud.version>
<spring-boot.version>2.2.6.RELEASE</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
<!--如果不排除的話,默認(rèn)會(huì)使用ribbon作為負(fù)載均衡器症杏,會(huì)有警告日志-->
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<!--如果不排除的話装获,默認(rèn)會(huì)使用ribbon作為負(fù)載均衡器,會(huì)有警告日志-->
<exclusions>
<exclusion>
<artifactId>spring-cloud-netflix-ribbon</artifactId>
<groupId>org.springframework.cloud</groupId>
</exclusion>
</exclusions>
</dependency>
<!--為服務(wù)提供默認(rèn)的健康檢查實(shí)現(xiàn)厉颤,否則需要自定義Consul的健康檢查策略-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--管理spring-cloud的相關(guān)組件的版本號(hào)-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--管理spring-boot的相關(guān)組件的版本號(hào)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
然后新建兩個(gè)子模塊:order-service和storage-service穴豫,子模塊目前并不需要引入什么額外的依賴。接下來(lái)需要為每個(gè)模塊創(chuàng)建一個(gè)啟動(dòng)類和添加一個(gè)系統(tǒng)配置文件逼友,這部分兩個(gè)模塊其實(shí)都大同小異精肃,以order-service為例:
服務(wù)啟動(dòng)類
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
配置文件:application.yml
server:
port: 9001
spring:
application:
name: order-service
cloud:
consul:
host: 192.168.1.220
port: 8500
discovery:
#使用IP地址而不是HOSTNAME作為服務(wù)的訪問(wèn)地址
prefer-ip-address: true
feign:
hystrix:
#啟用hystrix短路保護(hù)
enabled: true
okhttp:
#feign使用okhttp來(lái)作為http客戶端
enabled: true
其實(shí)到這里,一個(gè)服務(wù)就已經(jīng)基本搭建完成了帜乞,只需要填充具體的業(yè)務(wù)邏輯司抱。這也體現(xiàn)了Spring Boot機(jī)制的強(qiáng)大之處,通過(guò)簡(jiǎn)單的配置加上各種starter挖函,就能快速的將各種功能特性整合到項(xiàng)目中來(lái)状植。我們先來(lái)驗(yàn)證一下現(xiàn)在的配置,運(yùn)行兩個(gè)啟動(dòng)類怨喘,順利的話津畸,就能夠在Consul的控制臺(tái)http://localhost:8500/ui 看到服務(wù)成功注冊(cè)的信息:
服務(wù)注冊(cè)的過(guò)程
service啟動(dòng)時(shí),spring-cloud-starter-consul-discovery組件會(huì)根據(jù)配置信息生成一段服務(wù)注冊(cè)的json報(bào)文必怜,然后調(diào)用Consul Server的REST接口進(jìn)行服務(wù)注冊(cè)肉拓,比如order-service的注冊(cè)信息是這樣的(控制臺(tái)有相應(yīng)的輸出信息):
{id='order-service-9001', name='order-service', tags=[secure=false], address='192.168.1.252', meta=null, port=9001, enableTagOverride=null, check=Check{script='null', interval='10s', ttl='null', http='http://192.168.1.252:9001/actuator/health', method='null', header={}, tcp='null', timeout='null', deregisterCriticalServiceAfter='null', tlsSkipVerify=null, status='null'}, checks=null}
從輸出信息可以看出Consul對(duì)服務(wù)進(jìn)行健康檢查的回調(diào)地址是
http://192.168.1.252:9001/actuator/health,間隔時(shí)間是10s梳庆,如果需要修改默認(rèn)的健康檢查信息暖途,可以通過(guò)設(shè)置相應(yīng)的參數(shù)卑惜,請(qǐng)查看 這里。我們可以通過(guò)引入spring-boot-starter-actuator組件自動(dòng)實(shí)現(xiàn)健康檢查的回調(diào)驻售,否則就需要自行定義了露久。
添加業(yè)務(wù)邏輯
首先在storage-service中添加一個(gè)API,用于模擬庫(kù)存的變更欺栗,核心代碼如下:
@RestController
@RequestMapping("/api/storage")
public class Controller {
@Autowired
private StorageService storageService;
@PostMapping("/change-inventory")
public Integer changeInventory(@RequestBody InventoryChangeDTO req) {
return storageService.changeInventory(req);
}
}
storageService.changeInventory方法模擬對(duì)庫(kù)存的扣減操作毫痕,并返回一個(gè)Integer表示當(dāng)前剩余的庫(kù)存數(shù)(默認(rèn)返回98)。然后在order-service中添加一個(gè)Feign Client接口迟几,用于聲明需要遠(yuǎn)程調(diào)用的storage-service的相關(guān)接口:
@FeignClient(name = "storage-service", fallback = StorageServiceFallback.class)
public interface StorageService {
@PostMapping("/api/storage/change-inventory")
Integer updateInventoryOfGood(InventoryChangeDTO inventoryChangeDTO);
}
這個(gè)接口類相當(dāng)于是order-service訪問(wèn)storage-service中API的一個(gè)代理消请,@FeignClient的name字段中指定的名稱需要與storage-service注冊(cè)的服務(wù)名稱保持一致,這樣feign會(huì)通過(guò)服務(wù)名查詢Consul中已注冊(cè)的服務(wù)类腮,并自動(dòng)獲取order-service的訪問(wèn)地址臊泰。如果order-service部署了多個(gè)實(shí)例,F(xiàn)eign會(huì)使用Spring Cloud Loadbalancer進(jìn)行相應(yīng)的負(fù)載均衡(這個(gè)不需要額外的配置蚜枢,只需要在依賴包中引入相應(yīng)的starter即可)缸逃。fallback = StorageServiceFallback.class 聲明了如果storage-service中相應(yīng)的接口不可用的時(shí)候,需要進(jìn)行相應(yīng)的降級(jí)處理祟偷,這個(gè)是利用到了Hystrix的熔斷保護(hù)特性察滑,需要在配置文件中聲明:
feign.hystrix.enabled=true
然后我們來(lái)構(gòu)造order-service的createOrder接口,用于用戶創(chuàng)建訂單:
@RestController
@RequestMapping("/api/order")
@Slf4j
public class Controller {
@Autowired
private OrderService orderService;
@Autowired
private StorageService storageService;
@PostMapping("/create-order")
public String createOrder(@RequestBody OrderDTO order) {
//創(chuàng)建新訂單
orderService.createNewOrder(order);
InventoryChangeDTO req = new InventoryChangeDTO();
req.setGoodCode(order.getGoodCode());
req.setQuantity(order.getQuantity());
//調(diào)用倉(cāng)儲(chǔ)服務(wù)變更庫(kù)存
Integer remainQuantity = storageService.updateInventoryOfGood(req);
log.info("剩余數(shù)量:" + remainQuantity);
return "ok";
}
}
這樣基本的業(yè)務(wù)流程已經(jīng)完成了修肠,我們來(lái)編寫(xiě)一個(gè)單元測(cè)試來(lái)測(cè)試一下訂單創(chuàng)建的接口:
@Test
public void testCreateOrder() throws Exception {
OrderDTO orderDTO = new OrderDTO();
orderDTO.setCustomerCode("cus001");
orderDTO.setGoodCode("tc-1");
orderDTO.setQuantity(100);
this.mvc.perform(post("/api/order/create-order").content(JsonUtil.obj2json(orderDTO))
.contentType("application/json"))
.andExpect(status().isOk())
.andExpect(content().string("ok"));
}
運(yùn)行之后贺辰,一切正常的話,就可以在控制臺(tái)看到輸出的剩余庫(kù)存日志信息:
2020-04-16 10:03:58.846 INFO 8276 --- [ main] c.g.d.s.o.controller.Controller : 剩余數(shù)量:98
熔斷降級(jí)
如果當(dāng)遠(yuǎn)程服務(wù)不可用的時(shí)候嵌施,需要做一些額外的處理饲化,比如加入重試隊(duì)列后期進(jìn)行重試,記錄錯(cuò)誤日志等等吗伤,就需要用到Hystrix提供的熔斷保護(hù)特性了吃靠。之前說(shuō)到的StorageServiceFallback就是用于這個(gè)目的的,我們來(lái)看一下StorageServiceFallback的代碼:
@Component
@Slf4j
public class StorageServiceFallback implements StorageService {
public Integer updateInventoryOfGood(InventoryChangeDTO inventoryChangeDTO) {
log.error("StorageServiceFallback.updateInventoryOfGood暫不可用");
return -1;
}
}
這里的業(yè)務(wù)處理很簡(jiǎn)單足淆,只是返回一個(gè)默認(rèn)數(shù)字-1巢块,并打印了一行錯(cuò)誤日志。現(xiàn)在先把StorageServiceApplication停止巧号,然后再執(zhí)行testCreateOrder方法族奢,就會(huì)看到如下的輸出日志:
2020-04-16 10:55:09.245 ERROR 6432 --- [ HystrixTimer-1] c.g.d.s.o.remote.StorageServiceFallback : StorageServiceFallback.updateInventoryOfGood暫不可用
2020-04-16 10:55:09.245 INFO 6432 --- [ main] c.g.d.s.o.controller.Controller : 剩余數(shù)量:-1
從日志上就可以看出降級(jí)服務(wù)已經(jīng)生效了。熔斷降級(jí)在一些中小型的系統(tǒng)中可能意義不太大丹鸿,但還是可以利用這個(gè)機(jī)制來(lái)做一些其它的應(yīng)用越走,比如對(duì)單個(gè)服務(wù)進(jìn)行單元測(cè)試時(shí),其依賴的遠(yuǎn)程服務(wù)都需要打樁,通過(guò)降級(jí)機(jī)制我們就可以很容易的生成遠(yuǎn)程服務(wù)的MOCK接口廊敌,定制自己的測(cè)試邏輯了铜跑。
本文的相關(guān)代碼可以查看這里 spring-cloud-demo