天天看點

Spring Cloud開發實踐(六): 基于Consul和Spring Cloud 2023.示範項目

Spring Cloud開發實踐(六): 基于Consul和Spring Cloud 2023.示範項目

Consul 服務

啟動Consul服務, 在Win10下可以執行以下指令, 或者存成bat檔案運作, 保持視窗打開

consul agent -dev -client=0.0.0.0 -data-dir .\ -advertise 127.0.0.1 -ui -config-dir .\           

浏覽器通路 http://127.0.0.1:8500 , 用于觀察後面注冊的Node和Health情況

Spring Cloud 項目

這個示範項目使用的 Spring Boot 和 Spring Cloud 都不是最新版本, 因為最新版本最低要求 JDK17. 這裡選擇的是對應 JDK11 可用的最高版本, 各元件版本明細為

  • Consul 1.15
  • JDK 11
  • Spring Boot 2.7.11
  • Spring Cloud 2021.0.6

整體結構

這個用于示範的項目名稱為 Dummy, 包含3個子子產品, 分别是 dummy-common-api, dummy-common-impl 和 dummy-admin, 其中

  • dummy-common-api 和 dummy-common-impl 邏輯上屬于同一個子產品 dummy-common. api 是對外輸出的接口, impl是對應的實作
  • dummy-admin 依賴 dummy-common-api , 使用其提供的接口

打包後, 需要部署的是兩個jar: dummy-common.jar 和 dummy-admin.jar, 前者提供服務接口, 後者消費前者提供的接口, 并對外(例如前端, 小程式, APP)提供接口

項目的整體結構如下

│   pom.xml├───dummy-admin│   │   pom.xml│   ├───src│   │   ├───main│   │   │   ├───java│   │   │   └───resources│   │   │           application.yml│   │   └───test│   └───target├───dummy-common-api│   │   pom.xml│   ├───src│   │   ├───main│   │   │   ├───java│   │   │   └───resources│   │   └───test│   └───target└───dummy-common-impl    │   pom.xml    ├───src    │   ├───main    │   │   ├───java    │   │   └───resources    │   │           application.yml    │   └───test    └───target           

根子產品 Dummy

根子產品的 pom.xml 中,

  • 定義了子子產品, module标簽中的内容, 要和子子產品目錄名一緻.
  • 設定JDK版本 11
  • 引入全局 Spring Boot Dependencies, 版本 2.7.11
  • 引入全局 Spring Cloud Dependencies, 版本 2021.0.6
  • 還有一些是Plugin相關的版本, 略
<?xml version="1.0" encoding="UTF-8"?>    ...    <name>Dummy: Root</name>    <modules>        <module>dummy-common-api</module>        <module>dummy-common-impl</module>        <module>dummy-admin</module>    </modules>     <properties>        <!-- Global encoding -->        <project.jdk.version>11</project.jdk.version>        <project.source.encoding>UTF-8</project.source.encoding>         <!-- Global dependency versions -->        <spring-boot.version>2.7.11</spring-boot.version>        <spring-cloud.version>2021.0.6</spring-cloud.version>    </properties>     <dependencyManagement>        <dependencies>            <!-- Spring Boot Dependencies -->            <dependency>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-dependencies</artifactId>                <version>${spring-boot.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>            <!-- Spring Cloud Dependencies -->            <dependency>                <groupId>org.springframework.cloud</groupId>                <artifactId>spring-cloud-dependencies</artifactId>                <version>${spring-cloud.version}</version>                <type>pom</type>                <scope>import</scope>            </dependency>         </dependencies>    </dependencyManagement>     <build>        ...    </build> </project>           

Dummy Common API 子產品

這個子產品用于生成依賴的jar包, 作用非常重要. 以下詳細說明

pom.xml 中除了定義和父子產品的關系, 需要引入 openfeign

<?xml version="1.0" encoding="UTF-8"?>    ...    <parent>        <groupId>com.rockbb.test</groupId>        <artifactId>dummy</artifactId>        <version>1.0-SNAPSHOT</version>        <relativePath>../pom.xml</relativePath>    </parent>     <artifactId>dummy-common-api</artifactId>    <packaging>jar</packaging>    <version>1.0-SNAPSHOT</version>     <name>Dummy: Commons API</name>     <dependencies>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-openfeign</artifactId>        </dependency>        ...    </dependencies>     <build>        ...    </build></project>           

定義一個 UserDTO, 這個是用于傳輸的資料對象

@Datapublic class UserDTO implements Serializable {    private Long id;    private String name;}           

對應的服務接口. 這裡用到了 @FeignClient 注解

  • @FeignClient 是給 dummy-admin 子產品用的name= CommonConstant.SERVICE_NAME 就是 "dummy-common", 因為這個API子產品中所有Service接口都使用同樣的名稱, 這邊做成常量contextId = "userDTOService" 如果不加這個參數, 多個 FeignClient 使用同樣的 name 時, 就會沖突. 這個一般直接定義為這個 service 的bean名稱path = "/userDTOService" 用于指定目前類中所有接口的請求字首. 在更早的版本中, 可以将 @RequestMapping 和 @FeignClient 聯用, 這個是定義在 @RequestMapping 中的, 後來不允許了, 因為有安全風險.
  • @GetMapping 和 @PostMapping 同時用于 dummy-admin 和 dummy-common對于 dummy-admin, 這就是 FeignClient 的請求路徑對于 dummy-common, 這就是 Contoller 方法的服務路徑需要注意 @GetMapping 請求的接口形式, 必須顯式添加 @RequestParam("id") 這類 GET 模式的參數注解, 否則使用 @GetMapping 的 Feign 請求也會被轉為 POST 而導緻請求錯誤.
@FeignClient(name = CommonConstant.SERVICE_NAME, contextId = "userDTOService", path = "/userDTOService")public interface UserDTOService {     @GetMapping("/get")    UserDTO get(@RequestParam("id") long id);     @PostMapping("/add")    int add(@RequestBody UserDTO dto);}           

在 dummy-admin 中, 這個接口會被執行個體化為 feign 代理, 在子產品中可以像普通 service 一樣調用, 而在 dummy-common 中, 不引入 feign 依賴, 或者在 @EnableFeignClients 的 basePackages 中避開本包路徑, 就會忽略這個注解, 進而實作子產品間接口的關聯.

與現在很多 Spring Cloud 項目中單獨拆出一個 Service 子產品的做法, 這種實作有很多的優點

  • 開發過程友好. 與單機開發幾乎一樣的代碼量, 唯一差別是要注意 Get 和 Post 對請求參數的格式和個數的限制
  • 易重構易擴充. 可以借助 IDE 的代碼分析能力, 改動自動标紅, 避免人為錯誤和遺漏
  • 性能開銷小, 如果 DTO 直接映射到資料庫字段, 可以全程使用一個類.

Dummy Common Impl 子產品

子產品的 pom.xml

  • 引入 spring-boot-starter-web, 因為要提供 RestController 的能力
  • 引入 spring-cloud-starter-consul-discovery 或 spring-cloud-starter-consul-all, 因為要接 Consul
  • 引入 dummy-common-api 依賴, 因為 Controller 請求定義在 API 中
  • 打包使用 spring-boot-maven-plugin 的 repackage, 因為要打 fat jar, 在伺服器上實作單包部署
<?xml version="1.0" encoding="UTF-8"?>    ...    <name>Dummy: Common Implementation</name>     <dependencies>        <!-- Spring Boot Dependencies -->        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>        <!-- Spring Cloud Dependencies  consul-discovery 和 consul-all 二選一 -->        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-consul-discovery</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-consul-all</artifactId>        </dependency>        ...        <dependency>            <groupId>com.rockbb.test</groupId>            <artifactId>dummy-common-api</artifactId>            <version>${project.version}</version>        </dependency>    </dependencies>     <build>        <finalName>dummy-common</finalName>        <resources>            ...        </resources>        <plugins>            ...            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <executions>                    <execution>                        <goals>                            <goal>repackage</goal>                        </goals>                    </execution>                </executions>            </plugin>        </plugins>    </build></project>           

配置部分 application.yml

  • 定義服務端口 8762
  • 定義 servlet 路徑, 必須定義, 否則不會配置 Controller 請求
  • spring.application.name: dummy-common 定義了本服務的名稱, 這個名稱就是在 FeignClient 中引用的服務名稱, 需要與 FeignClient 中的值一緻
  • spring.config.import 如果使用這個設定, 依賴要使用 consul-all, 因為 consul-discovery 中不帶 consul-config. 使用這個設定後, 會自動使用預設的 Consul 位址和端口
  • cloud.consul.host 和 port 如果使用了config.import, 在這裡可以修改預設的值, 如果不使用config.import, 則必須配置 host 和 port, 依賴可以換成 consul-discovery
  • cloud.consul.discovery.health-check-path 用于更改預設的 health 檢查請求路徑, 預設的是 /actuator/health, 這裡改為 /health
  • cloud.consul.discovery.instance-id 用于定義目前執行個體在 Consul 裡的執行個體ID. 預設使用 application.name-port, 如果正好這個服務在兩個伺服器上分别跑了一個執行個體, 且執行個體端口一樣, 就會産生沖突, 可以改為 application.name-[随機串] 的形式避免沖突
server:  port: 8762  tomcat:    uri-encoding: UTF-8  servlet:    context-path: / spring:  application:    name: dummy-common   config:    import: 'optional:consul:' #This will connect to the Consul Agent at the default location of "http://localhost:8500" #  cloud:#    consul:#      host: 127.0.0.1#      port: 8500#      discovery:#        health-check-path: /health # replace the default /actuator/health#        instance-id: ${spring.application.name}:${random.value}           

代碼部分, 首先是實作 health 檢查的處理方法, 這部分是普通的 RestController 方法. 傳回字元串可以任意指定, 隻要傳回的 code 是 200 就可以

@RestControllerpublic class HealthCheckServiceImpl {     @GetMapping("/health")    public String get() {        return "SUCCESS";    }}           

服務接口的實作類, 這裡實作了兩個接口方法 get 和 add

  • 使用 @RestController 注解, 與 API Service 中方法上的 @GetMapping 和 @PostMapping 配合, 将 Service 方法映射為 Controller 方法
  • 在類上的 @RequestMapping("userDTOService") 方法是必須的, 因為在 API Service 中與 @FeignClient 沖突無法定義, 隻能在這裡定義
  • 方法和參數上除了 @Override 不需要任何注解, 因為都在 API Service 上定義過了. 這裡加上注解也沒問題, 但是要手工保持一緻.
@RestController@RequestMapping("userDTOService")public class UserDTOServiceImpl implements UserDTOService {     @Autowired    private UserRepo userRepo;     @Override    public UserDTO get(long id) {        log.debug("Get user: {}", id);        UserDTO user = new UserDTO();        user.setId(id);        user.setName("dummy");        return user;    }     @Override    public int add(UserDTO dto) {        log.debug("Add user: {}", dto.getName());        return 0;    }}           

dummy-common 子產品運作後會将接口注冊到 Consul, 啟動後注意觀察兩部分:

  1. Consul 的日志輸出和控制台顯示, 在-dev模式下, 節點注冊後 Consul 日志會顯示子產品的名稱和心跳檢測記錄, 面闆上會顯示新的 Node
  2. Consul 控制台中顯示的 Health Checks 是否正常, 如果不正常, 需要檢查 /health 路徑為什麼通路失敗

Dummy Admin 子產品

dummy-admin 是調用接口, 并對外提供服務的子產品

pom.xml 和 dummy-common 基本一樣, 因為都要連接配接 Consul, 都要提供 Controller 方法

<?xml version="1.0" encoding="UTF-8"?>    ...    <name>Dummy: Admin API</name>     <dependencies>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-web</artifactId>        </dependency>        <dependency>            <groupId>org.springframework.boot</groupId>            <artifactId>spring-boot-starter-test</artifactId>            <scope>test</scope>        </dependency>        <dependency>            <groupId>org.springframework.cloud</groupId>            <artifactId>spring-cloud-starter-consul-discovery</artifactId>        </dependency>         <dependency>            <groupId>com.rockbb.test</groupId>            <artifactId>dummy-common-api</artifactId>            <version>${project.version}</version>        </dependency>    </dependencies>     <build>        <finalName>dummy-admin</finalName>        <resources>           ...        </resources>        <plugins>            <plugin>                <groupId>org.springframework.boot</groupId>                <artifactId>spring-boot-maven-plugin</artifactId>                <executions>                    <execution>                        <goals>                            <goal>repackage</goal>                        </goals>                    </execution>                </executions>            </plugin>            ...        </plugins>    </build></project>           

在主應用入口, 除了 @SpringBootApplication 以外, 還需要增加兩個注解

  • @EnableDiscoveryClient(autoRegister=false) 連接配接到 Consul 并使用服務發現, 預設會将目前節點也注冊到 Consul 作為服務. 對于純消費節點, 不對其它節點提供接口的, 使用 autoRegister=false 可以避免将自己注冊到 Consul
  • @EnableFeignClients(basePackages = {"com.rockbb.test.dummy.common.api"}) 掃描對應的包, 對 @FeignClient 注解執行個體化接口代理
/* Attach to discovery service without registering itself */@EnableDiscoveryClient(autoRegister=false)@EnableFeignClients(basePackages = {"com.rockbb.test.dummy.common.api"})@SpringBootApplicationpublic class AdminApp {    public static void main(String[] args) {        SpringApplication.run(AdminApp.class, args);    }}           

在調用方法的地方, 按普通 Service 注入和調用

@Slf4j@RestControllerpublic class IndexController {     @Autowired    private UserDTOService userDTOService;     @GetMapping(value = "/user_get")    public String doGetUser() {        UserDTO user = userDTOService.get(100L);        return user.getId() + ":" + user.getName();    }     @GetMapping(value = "/user_add")    public String doAddUser() {        UserDTO user = new UserDTO();        user.setName("foobar");        int result = userDTOService.add(user);        return String.valueOf(result);    }           

可以通過注入的 DiscoveryClient 對象, 檢視對應服務的服務位址(一般不需要)

@Autowiredprivate DiscoveryClient discoveryClient; @GetMapping("/services")public Optional<URI> serviceURL() {    return discoveryClient.getInstances(CommonConstant.SERVICE_NAME)            .stream()            .map(ServiceInstance::getUri)            .findFirst();}