Spring Cloud筆記(4)構建Spring Cloud Demo

通過前幾篇文章的積累,我們現在可以來動手搭建一個完整的Spring Cloud Demo項目了。爲了更清楚的說明Spring Cloud的結構特點,我們的demo項目還是遵循由淺入深的原則,一開始只加入一些基本的特性,後面再來逐步完善。

業務背景

本來演示技術點的demo,弄一些sayHello的方法出來也無可厚非。但Spring Cloud的很多特性都是與業務的實際需求緊密結合的,脫離業務談技術難免顯得有些空洞,所以我們也需要爲demo弄一個簡單的業務背景。就用常見的訂單管理和倉儲管理來舉例子吧,一個基本的業務流程就是客戶創建訂單 ,如下圖:


爲此我們需要創建兩個微服務模塊:訂單服務和倉儲服務,訂單服務提供創建訂單的接口,創建訂單的同時需要調用倉儲服務提供的接口來對庫存進行更新。

組件選擇和環境搭建

之前我們介紹過,每一種Spring Cloud的特性的實現都有好幾種不同的框架和工具進行選擇,根據官方的推薦和技術的流行度,demo主要採用了以下的框架和工具:

  • Consul:用於服務的註冊和發現
  • OpenFeign:聲明式HTTP客戶端,服務間調用
  • Spring Cloud Loadbalancer:客戶端負載均衡
  • Hystrix:斷路保護
  • Spring Cloud Gateway:智能路由,服務網關

這裏面需要提前安裝配置的組件只有一個Consul,可以參考我之前的文章 Consul的架構和配置

新建Maven項目

我們首先爲所有的服務模塊創建一個Parent項目,用於管理各種公共的依賴包,pom.xml如下,需要注意的地方請查看註釋:

<?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>
        <!--訂單管理服務-->
        <module>order-service</module>
        <!--倉儲管理服務-->
        <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>
            <!--如果不排除的話,默認會使用ribbon作爲負載均衡器,會有警告日誌-->
            <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>
            <!--如果不排除的話,默認會使用ribbon作爲負載均衡器,會有警告日誌-->
            <exclusions>
                <exclusion>
                    <artifactId>spring-cloud-netflix-ribbon</artifactId>
                    <groupId>org.springframework.cloud</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <!--爲服務提供默認的健康檢查實現,否則需要自定義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的相關組件的版本號-->
            <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的相關組件的版本號-->
            <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>

然後新建兩個子模塊:order-service和storage-service,子模塊目前並不需要引入什麼額外的依賴。接下來需要爲每個模塊創建一個啓動類和添加一個系統配置文件,這部分兩個模塊其實都大同小異,以order-service爲例:

服務啓動類
@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作爲服務的訪問地址
        prefer-ip-address: true
feign:
  hystrix:
    #啓用hystrix短路保護
    enabled: true
  okhttp:
    #feign使用okhttp來作爲http客戶端
    enabled: true

其實到這裏,一個服務就已經基本搭建完成了,只需要填充具體的業務邏輯。這也體現了Spring Boot機制的強大之處,通過簡單的配置加上各種starter,就能快速的將各種功能特性整合到項目中來。我們先來驗證一下現在的配置,運行兩個啓動類,順利的話,就能夠在Consul的控制檯http://localhost:8500/ui 看到服務成功註冊的信息:

服務註冊的過程

service啓動時,spring-cloud-starter-consul-discovery組件會根據配置信息生成一段服務註冊的json報文,然後調用Consul Server的REST接口進行服務註冊,比如order-service的註冊信息是這樣的(控制檯有相應的輸出信息):

{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對服務進行健康檢查的回調地址是
http://192.168.1.252:9001/actuator/health,間隔時間是10s,如果需要修改默認的健康檢查信息,可以通過設置相應的參數,請查看 這裏。我們可以通過引入spring-boot-starter-actuator組件自動實現健康檢查的回調,否則就需要自行定義了。

添加業務邏輯

首先在storage-service中添加一個API,用於模擬庫存的變更,核心代碼如下:

@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方法模擬對庫存的扣減操作,並返回一個Integer表示當前剩餘的庫存數(默認返回98)。然後在order-service中添加一個Feign Client接口,用於聲明需要遠程調用的storage-service的相關接口:

@FeignClient(name = "storage-service", fallback = StorageServiceFallback.class)
public interface StorageService {

    @PostMapping("/api/storage/change-inventory")
    Integer updateInventoryOfGood(InventoryChangeDTO inventoryChangeDTO);

}

這個接口類相當於是order-service訪問storage-service中API的一個代理,@FeignClient的name字段中指定的名稱需要與storage-service註冊的服務名稱保持一致,這樣feign會通過服務名查詢Consul中已註冊的服務,並自動獲取order-service的訪問地址。如果order-service部署了多個實例,Feign會使用Spring Cloud Loadbalancer進行相應的負載均衡(這個不需要額外的配置,只需要在依賴包中引入相應的starter即可)。fallback = StorageServiceFallback.class 聲明瞭如果storage-service中相應的接口不可用的時候,需要進行相應的降級處理,這個是利用到了Hystrix的熔斷保護特性,需要在配置文件中聲明:

feign.hystrix.enabled=true

然後我們來構造order-service的createOrder接口,用於用戶創建訂單:

@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) {
        //創建新訂單
        orderService.createNewOrder(order);
        InventoryChangeDTO req = new InventoryChangeDTO();
        req.setGoodCode(order.getGoodCode());
        req.setQuantity(order.getQuantity());
        //調用倉儲服務變更庫存
        Integer remainQuantity = storageService.updateInventoryOfGood(req);
        log.info("剩餘數量:" + remainQuantity);
        return "ok";
    }

}

這樣基本的業務流程已經完成了,我們來編寫一個單元測試來測試一下訂單創建的接口:

    @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"));
    }

運行之後,一切正常的話,就可以在控制檯看到輸出的剩餘庫存日誌信息:

2020-04-16 10:03:58.846  INFO 8276 --- [           main] c.g.d.s.o.controller.Controller          : 剩餘數量:98

熔斷降級

如果當遠程服務不可用的時候,需要做一些額外的處理,比如加入重試隊列後期進行重試,記錄錯誤日誌等等,就需要用到Hystrix提供的熔斷保護特性了。之前說到的StorageServiceFallback就是用於這個目的的,我們來看一下StorageServiceFallback的代碼:

@Component
@Slf4j
public class StorageServiceFallback implements StorageService {

    public Integer updateInventoryOfGood(InventoryChangeDTO inventoryChangeDTO) {
        log.error("StorageServiceFallback.updateInventoryOfGood暫不可用");
        return -1;
    }

}

這裏的業務處理很簡單,只是返回一個默認數字-1,並打印了一行錯誤日誌。現在先把StorageServiceApplication停止,然後再執行testCreateOrder方法,就會看到如下的輸出日誌:

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          : 剩餘數量:-1

從日誌上就可以看出降級服務已經生效了。熔斷降級在一些中小型的系統中可能意義不太大,但還是可以利用這個機制來做一些其它的應用,比如對單個服務進行單元測試時,其依賴的遠程服務都需要打樁,通過降級機制我們就可以很容易的生成遠程服務的MOCK接口,定製自己的測試邏輯了。

本文的相關代碼可以查看這裏 spring-cloud-demo

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章