十一、Spring cloud服務呼叫(Feign)
一、服務呼叫 核心概念
- 遠端過程呼叫(RPC)
- 介面定義語言(IDL)
- 通訊協議(Protocol)
- Netflix Feign
(一)遠端過程呼叫(RPC)
遠端過程呼叫(RPC)是一個計算機通訊協議。該協議容許運行於一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計。如果涉及的軟體採用面向物件程式設計,那麼遠端呼叫亦可稱為遠端呼叫或遠端方法呼叫。
例如:
- Java RMI (二進位制協議)
- WebServices(文字協議)
1、訊息傳遞
RPC 是一種請求-響應協議,一次 RPC 在客戶端初始化,再由客戶端將請求訊息傳遞到遠端的伺服器,執行指定的帶有引數的過程。經過遠端伺服器執行過程後,將結果作為響應內容返回到客戶端。
2、存根(Stub)
存根(Stub)是在一次分散式計算 RPC 中,客戶端和伺服器轉換引數的一段程式碼。由於存根的引數轉化,RPC 執行過程如同本地執行函式呼叫。存根必須在客戶端和伺服器兩端均裝載,並且保持相容。
二、整合 Feign 框架圖
本次整合使用九、Spring cloud服務短路(Hystrix)中的3個專案:user-api、user-ribbon-client、user-service-provider。
(一)新增依賴
在專案 user-api 新增 feign 依賴:
<!-- 依賴 Spring Cloud Netflix Feign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
(二)申明 Feign 客戶端
這裡改造的是 user-api 專案下的 服務介面:UserService
/**
* 註解 @FeignClient:申明 Feign 客戶端
* @author 鹹魚
* @date 2018/11/11 16:37
*/
@FeignClient(name = "${user.service.name}")//利用佔位符,避免未來整合時硬編碼
public interface UserService {
/**
* 儲存使用者
* @param user 待儲存物件 {@link User}
* @return true 成功 false 失敗
*/
@PostMapping("/user/save")
boolean saveUser(User user);
/**
* 查詢所有使用者
* @return 使用者列表
*/
@GetMapping("/user/find/all")
List<User> findAll();
}
注意:在使用
@FeignClient
name
屬性儘量使用佔位符,避免硬編碼。否則,未來升級時,不得不升級客戶端版本。
對上面的改造做一個簡單的解析:
在以前,我們 客戶端(服務呼叫方) 呼叫 服務端(服務提供方) 提供的服務時,使用的是 restTemplate.getForObject("http://" + serviceProviderName + "/user/find/all",List.class)
,而我們這裡的改造,就是將其轉換成這段程式碼。
比如 @FeignClient(name = "${user.service.name}")
這裡的 ${user.service.name}
就是我們的 服務端(服務提供方)應用名,也就是上面的 serviceProviderName
;而 @GetMapping("/user/find/all")
也就是 上面的 "/user/find/all"
路徑。
在改造完成以後,Feign 框架會自動組裝 @FeignClient(name = "${user.service.name}")
和 @GetMapping("/user/find/all")
變為 "http://" + serviceProviderName + "/user/find/all"
。
(三)啟用 Feign 客戶端
需要在 客戶端(服務呼叫方) 啟用 Feign,這裡的服務呼叫方就是 user-ribbon-client
專案。
使用 @EnableFeignClients 啟用 Feign 客戶端。
/**
* 註解 @RibbonClient:啟用 Ribbon
* 註解 @EnableCircuitBreaker:啟用 服務短路
* 註解 @EnableFeignClients:啟用 Feign 客戶端
*/
@EnableFeignClients(clients = UserService.class)//clients 屬性:申明 UserService 介面作為 Feign Client 呼叫
@EnableCircuitBreaker
@SpringBootApplication
@RibbonClient("user-service-provider")//指定目標應用名稱
public class UserServiceClientApplication {
......
}
三、Spring Cloud 再整合
(一)整合負載均衡:Nertflix Ribbon
1、客戶端:啟用 @EnableFeignClients
UserService
/**
* 註解 @RibbonClient:啟用 Ribbon
* 註解 @EnableCircuitBreaker:啟用 服務短路
* 註解 @EnableFeignClients:啟用 Feign 客戶端
*/
@EnableFeignClients(clients = UserService.class)//clients 屬性:申明 UserService 介面作為 Feign Client 呼叫
@EnableCircuitBreaker
@SpringBootApplication
@RibbonClient("user-service-provider")//指定目標應用名稱
public class UserServiceClientApplication {
......
}
2、客戶端:配置 @FeignClient(name = “${user.service.name}”) 中的佔位符.
調整application.properties
#使用者 Ribbon 客戶端應用
spring.application.name=spring-cloud-user-service-client
#服務埠
server.port=8080
#關閉 Eureka Client,顯示地通過配置方式註冊 Ribbon 服務地址(未配置 Eureka 時使用)
eureka.client.enabled=false
#服務提供方名稱
service.provider-name=user-service-provider
service.provider.host=localhost
service.provider.port=9090
#定義 user-service-provider Ribbon 的伺服器地址
#為 RibbonLoadBalancerClient 提供服務列表
user-service-provider.ribbon.listOfServers=http://${service.provider.host}:${service.provider.port}
#擴充套件 IPing 實現
user-service-provider.ribbon.NFLoadBalancerPingClassName=org.pc.ping.MyPing
management.endpoints.web.exposure.include=*
#配置 @FeignClient(name = "${user.service.name}") 中的佔位符
#user.service.name 實際需要指定 UserService 介面的提供方,也就是 user-service-provider
user.service.name=${service.provider-name}
3、服務端:實現 UserService,即暴露 HTTP REST 服務
&emsp調整應用:user-service-provider
(1)增加 InMemoryUserServiceImpl
Bean 名稱
/**
* 記憶體實現{@link UserService}
* @author 鹹魚
* @date 2018/11/11 16:39
*/
@Service("inMemoryUserServiceImpl")//Bean 名稱
public class InMemoryUserServiceImpl implements UserService {
private Map<Long, User> userMap = new HashMap<>();
@Override
public boolean saveUser(User user) {
return userMap.put(user.getId(), user) == null;
}
@Override
public List<User> findAll() {
return new ArrayList(userMap.values());
}
}
(2)調整 UserServiceProviderController
實現 Feign 客戶端介面 UserService,否則需要Controller中的對映 URL 和 UserService 介面中的對映保持一致!!
方式一:調整 UserServiceProviderController
實現 Feign 客戶端介面 UserService
/**
* 使用者服務提供方 Controller
* @author 鹹魚
* @date 2018/11/12 18:42
*/
@RestController
public class UserServiceProviderController implements UserService {
@Autowired
@Qualifier("inMemoryUserServiceImpl") //實現 Bean :InMemoryUserServiceImpl
private UserService userService;
/**
* 通過方法繼承,URL 對映:"/user/save"
* @param user 待儲存物件 {@link User}
* @return
*/
@Override
public boolean saveUser(@RequestBody User user){
return userService.saveUser(user);
}
/**
* 通過方法繼承,URL 對映:"/user/find/all"
*/
@Override
public List<User> findAll() {
return userService.findAll();
}
}
方式二:調整Controller中的對映 URL 和 UserService 介面中的對映保持一致
/**
* 使用者服務提供方 Controller
* @author 鹹魚
* @date 2018/11/12 18:42
*/
@RestController
public class UserServiceProviderController {
@Autowired
@Qualifier("inMemoryUserServiceImpl") //實現 Bean :InMemoryUserServiceImpl
private UserService userService;
/**
* @param user 待儲存物件 {@link User}
*/
@PostMapping("/user/save")
public boolean saveUser(@RequestBody User user){
return userService.saveUser(user);
}
@GetMapping("/user/find/all")
public List<User> findAll() {
return userService.findAll();
}
}
4、客戶端:使用 UserService 直接呼叫遠端 HTTP REST 服務
方式一:Controller 實現 UserService 介面(不推薦)
/**
* 注意:官方建議 客戶端和服務端不要同時實現 Feign 介面,
* 這裡的程式碼只是一個說明,實際情況最好使用組合的方式,而不是繼承。
* 這裡的組合就是其中一方實現 Feign 介面,另一方使用對映!!!
* {@link UserService} 客戶端 {@link RestController}
*/
@RestController
public class UserServiceClientController implements UserService {
@Autowired
private UserService userService;
/**
* 通過方法繼承,URL 對映:"/user/save"
* @param user 待儲存物件 {@link User}
*/
@Override
public boolean saveUser(@RequestBody User user){
return userService.saveUser(user);
}
/**
* 通過方法繼承,URL 對映:"/user/find/all"
*/
@Override
public List<User> findAll() {
return userService.findAll();
}
}
方式二:直接加對映(推薦)
/**
* {@link UserService} 客戶端 {@link RestController}
* @author 鹹魚
* @date 2018/11/15 22:01
*/
@RestController
public class UserServiceClientController {
@Autowired
private UserService userService;
@PostMapping("/user/save")
public boolean saveUser(@RequestBody User user){
return userService.saveUser(user);
}
@GetMapping("/user/find/all")
public List<User> findAll() {
return userService.findAll();
}
}
(二)整合服務短路:Nertflix Hystrix
這裡的整合有兩種方式:
- 第一種:調整 user-api 應用中的服務介面,在 @FeignClient 註解中增加熔斷屬性類
- 第二種:直接在 服務端 介面的實現上,使用 @HystrixCommand 註解
方式一:
1、user-api 應用: UserService
Fallback實現
/**
* {@link UserService} Fallback 實現
* @author 鹹魚
* @date 2018/11/16 17:27
*/
public class UserServiceFallback implements UserService{
@Override
public boolean saveUser(User user) {
return false;
}
@Override
public List<User> findAll() {
return Collections.emptyList();
}
}
2、user-api 應用: 調整UserService
@FeignClient屬性
/**
* 註解 @FeignClient:申明 Feign 客戶端
* name:服務提供方應用名
* fallback:熔斷處理類(實現了 {@link UserService},為介面中的每一種方法都實現了熔斷處理)
* @author 鹹魚
* @date 2018/11/11 16:37
*/
@FeignClient(name = "${user.service.name}", fallback = UserServiceFallback.class)//利用佔位符,避免未來整合時硬編碼
public interface UserService {
/**
* 儲存使用者
* @param user 待儲存物件 {@link User}
* @return true 成功 false 失敗
*/
@PostMapping("/user/save")
boolean saveUser(User user);
/**
* 查詢所有使用者
* @return 使用者列表
*/
@GetMapping("/user/find/all")
List<User> findAll();
}
方式二:
服務端:在 UserServiceProviderController#findAll()
方法上整合 @HystrixCommand
/**
* 獲取所有使用者列表
*/
@HystrixCommand(
//Command 配置
commandProperties = {
//設定超時時間為 100ms
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "100")
},
//設定熔斷方法(PS:當異常發生後的處理方法)
fallbackMethod = "fallbackForGetUsers"
)
@GetMapping("/user/find/all")
public List<User> findAll() {
return userService.findAll();
}
(三)整合服務發現:Nertflix Eureka
1、建立 Eureka Server 子專案
(1)增加 Eureka Server 依賴
<!-- Eureka Server -->
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
(2)建立引導類 EurekaServerApplication
/**
* Eureka Server 引導類
* @author 鹹魚
* @date 2018/11/19 20:51
*/
@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
(3)配置Eureka Server 服務埠
#應用服務名
spring.application.name=user-eureka-server
#服務埠
server.port=7070
#是否需要向其他 Eureka 伺服器註冊(單機版需要設定 否)
eureka.client.register-with-eureka=false
#是否需要從其他 Eureka 伺服器獲取註冊資訊(單機版需要設定 否)
eureka.client.fetch-registry=false
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
2、客戶端:配置服務發現客戶端
配置應用:user-service-client
(1)增加 Eureka Client 依賴
<!-- 依賴 eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(2)啟用服務發現
/**
* 註解 @RibbonClient:啟用 Ribbon
* 註解 @EnableCircuitBreaker:啟用 服務短路
* 註解 @EnableFeignClients:啟用 Feign 客戶端
* 註解 @EnableDiscoveryClient:啟用 Eureka 客戶端
* @author 鹹魚
* @date 2018/11/11 18:05
*/
@EnableDiscoveryClient
@EnableFeignClients(clients = UserService.class)//clients 屬性:申明 UserService 介面作為 Feign Client 呼叫
@EnableCircuitBreaker
@SpringBootApplication
@RibbonClient("user-service-provider")//指定目標應用名稱
public class UserServiceClientApplication {
....
}
(3)配置 Eureka 註冊中心:application.properties
#使用者 Ribbon 客戶端應用
spring.application.name=spring-cloud-user-service-client
#服務埠
server.port=8080
#服務提供方名稱
service.provider-name=user-service-provider
#擴充套件 IPing 實現
user-service-provider.ribbon.NFLoadBalancerPingClassName=org.pc.ping.MyPing
#配置 @FeignClient(name = "${user.service.name}") 中的佔位符
#user.service.name 實際需要指定 UserService 介面的提供方,也就是 user-service-provider
user.service.name=${service.provider-name}
#Spring Cloud Eureka 客戶端 註冊到 Eureka 伺服器
eureka.client.service-url.defaultZone=http://localhost:7070/eureka
management.endpoints.web.exposure.include=*
2、服務端:配置服務發現客戶端
配置應用:user-service-provider
(1)增加 Eureka Client 依賴
<!-- 依賴 eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(2)啟用服務發現
/**
* 註解 @EnableHystrix:啟用 Hystrix
* 註解 @EnableDiscoveryClient:啟用 Eureka Client
*/
@EnableDiscoveryClient
@EnableHystrix
@SpringBootApplication
public class UserServiceProviderApplication {
....
}
(3)配置 Eureka 註冊中心:application.properties
#使用者服務提供方應用資訊
spring.application.name=user-service-provider
#服務埠
server.port=9090
#Spring Cloud Eureka 客戶端 註冊到 Eureka 伺服器
eureka.client.service-url.defaultZone=http://localhost:7070/eureka
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
(四)整合配置伺服器:Config Server
建立 Config Server 應用
1、增加 Config Server 依賴
<!-- 增加 Config Server 依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
2、整合 基於檔案系統(File System) 實現
注意:
user-service-client
application.properties
中以下內容將會被配置伺服器
中的user-service.properties
替代:
(1)啟用應用配置伺服器
在引導類上標註@EnableConfigServer
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
(2)建立本地目錄
理解 Java 中的 ${user.dir}
:簡單點說,就是當前專案所在物理路徑。比如專案所在目錄為 E:/spring-cloud
,那麼 user.dir = E:\springDemo\spring-cloud-basis
。
在IDEA 中\src\main\resources
目錄下,建立一個名為 configs 目錄,它的絕對路徑:${user.dir}\feign\config-server-feign\src\main\resources\configs
(3)建立 user-service.properties
#User Service 配置內容
#服務提供方名稱
service.provider-name=user-service-provider
#配置 @FeignClient(name = "${user.service.name}") 中的佔位符
#user.service.name 實際需要指定 UserService 介面的提供方,也就是 user-service-provider
user.service.name=${service.provider-name}
(4)在本地目錄中建立 git 倉庫
//進入本地目錄
cd E:\springdemo\spring-cloud-basis\feign\config-server-feign\src\main\resources\configs`
//建立 git 倉庫
git init .
//新增檔案進 git 倉庫,並提交
git add .
git commit -m "第一次提交"
(5)配置 git 本地倉庫 URI(在 application.properties中配置)
#Spring Cloud Config Server 應用名稱
spring.application.name=config-server-feign
#服務埠
server.port=6060
#配置伺服器檔案系統 git 倉庫(PS:user.dir = E:\springDemo\spring-cloud-basis 專案根目錄)
#使用 ${user.dir} 減少平臺檔案系統的不一致
spring.cloud.config.server.git.uri=${user.dir}/feign/config-server-feign/src/main/resources/configs
management.endpoints.web.exposure.include=*
management.endpoint.health.show-details=always
3、配置服務發現客戶端
(1)增加 Eureka Client 依賴
<!-- 依賴 eureka-client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
(2)啟用服務發現
@EnableDiscoveryClient
@EnableConfigServer
@SpringBootApplication
public class ConfigServerApplication {
....
}
(3)配置 Eureka 註冊中心:application.properties
#Spring Cloud Config Server 註冊到 Eureka 伺服器
eureka.client.service-url.defaultZone=http://localhost:7070/eureka
4、測試是否可以找到配置項:
http://localhost:6060/user-service/default
(五)整合配置客戶端:Config Client
調整應用 user-service-client
作為 Config Client。
1、增加 Config Client 依賴
<!-- 依賴 Config Client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
2、ClassPath 下建立 bootstrap.properties
3、配置 bootstrap.properties
(1)bootstrap.properties 配置以 spring.cloud.config. 開頭的配置資訊
#配置 客戶端應用 關聯的 應用(通過該選項與 服務端 相連)
# spring.cloud.config.name 是可選的,若未配置,採用 ${spring.application.name}
# 若要配置,就配置為 配置檔案(user-service.properties)的 name
spring.cloud.config.name=user-service
#關聯 profile
spring.cloud.config.profile=default
#關聯 label
spring.cloud.config.label=master
#配置 Config Server 伺服器URI
spring.cloud.config.uri=http://127.0.0.1:6060
#Config Server 伺服器應用名稱
spring.cloud.config.discovery.service-id=config-server-feign
4、配置 Config Client 服務發現客戶端
儘管應用 user-service-client
已經整合了 Eureka Client,但是在整合 Config Client 之後,還需要配置相關屬性,使應用 user-service-client
能成功註冊到 Eureka Server中去。
注意:
如果當前應用需要提前獲取應用資訊,那麼需要將 Eureka 客戶端 註冊到 Eureka 伺服器配置項“eureka.client.service-url.defaultZone”提前至 bootstrap.properties檔案。
原因:
我們在配置Config 伺服器的應用名稱時,實質是 Eureka 客戶端在 Eureka 伺服器中通過應用名稱,找到對應的 Config 伺服器,所以前提是,必須先將 Eureka 客戶端 註冊到 Eureka伺服器。而bootstrap 上下文是 Spring Boot 上下文的父上下文,它是最先載入的,所以需要將“eureka.client.service-url.defaultZone”配置項放到bootstrap.properties中。
(1)在 bootstrap.properties 啟用服務發現
#啟用 Config Server 服務發現
spring.cloud.config.discovery.enabled=true
(2)將eureka.client.service-url.defaultZone配置項由application.properties轉至bootstrap.propertiess
調整後的bootstrap.properties如下:
#使用者 Ribbon 客戶端應用
spring.application.name=spring-cloud-user-service-client
#配置 客戶端應用 關聯的 應用(通過該選項與 服務端 相連)
# spring.cloud.config.name 是可選的,若未配置,採用 ${spring.application.name}
# 若要配置,就配置為 配置檔案(user-service.properties)的 name
spring.cloud.config.name=user-service
#關聯 profile
spring.cloud.config.profile=default
#關聯 label
spring.cloud.config.label=master
#Config Server 伺服器應用名稱
spring.cloud.config.discovery.service-id=config-server-feign
#啟用 Config Server 服務發現
spring.cloud.config.discovery.enabled=true
#Spring Cloud Eureka 客戶端 註冊到 Eureka 伺服器
eureka.client.service-url.defaultZone=http://localhost:7070/eureka
調整後的application.properties如下:
#服務埠
server.port=8080
#擴充套件 IPing 實現
user-service-provider.ribbon.NFLoadBalancerPingClassName=org.pc.ping.MyPing
management.endpoints.web.exposure.include=*