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, 啟動後注意觀察兩部分:
- Consul 的日志輸出和控制台顯示, 在-dev模式下, 節點注冊後 Consul 日志會顯示子產品的名稱和心跳檢測記錄, 面闆上會顯示新的 Node
- 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();}