目錄
- Spring Cloud開發實踐(一): 簡介和根模塊
- Spring Cloud開發實踐(二): Eureka服務和介面定義
- Spring Cloud開發實踐(三): 介面實作和下游呼叫
- Spring Cloud開發實踐(四): Docker部署
- Spring Cloud開發實踐(五): Consul - 服務注冊的另一個選擇
- Spring Cloud開發實踐(六): 基于Consul和Spring Cloud 2021.0的演示專案
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, 這個是用于傳輸的資料物件
@Data
public 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 就可以
@RestController
public 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"})
@SpringBootApplication
public class AdminApp {
public static void main(String[] args) {
SpringApplication.run(AdminApp.class, args);
}
}
在呼叫方法的地方, 按普通 Service 注入和呼叫
@Slf4j
@RestController
public class IndexController {
@Autowired
private UserDTOService userDTOService;
@GetMapping(value = "https://www.cnblogs.com/user_get")
public String doGetUser() {
UserDTO user = userDTOService.get(100L);
return user.getId() + ":" + user.getName();
}
@GetMapping(value = "https://www.cnblogs.com/user_add")
public String doAddUser() {
UserDTO user = new UserDTO();
user.setName("foobar");
int result = userDTOService.add(user);
return String.valueOf(result);
}
可以通過注入的 DiscoveryClient 物件, 查看對應服務的服務地址(一般不需要)
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/services")
public Optional<URI> serviceURL() {
return discoveryClient.getInstances(CommonConstant.SERVICE_NAME)
.stream()
.map(ServiceInstance::getUri)
.findFirst();
}
參考
- Consul和Spring Boot非常詳細的配置教程 https://medium.com/javarevisited/hands-on-consul-with-spring-boot-1ebf2918165c
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/552769.html
標籤:Java
上一篇:Spring回圈依賴那些事兒(含Spring詳細流程圖)
下一篇:返回列表
