1. 程式人生 > >十一、Spring cloud服務呼叫(Feign)

十一、Spring cloud服務呼叫(Feign)

一、服務呼叫 核心概念

  • 遠端過程呼叫(RPC)
  • 介面定義語言(IDL)
  • 通訊協議(Protocol)
  • Netflix Feign

(一)遠端過程呼叫(RPC)
  遠端過程呼叫(RPC)是一個計算機通訊協議。該協議容許運行於一臺計算機的程式呼叫另一臺計算機的子程式,而程式設計師無需額外地為這個互動作用程式設計。如果涉及的軟體採用面向物件程式設計,那麼遠端呼叫亦可稱為遠端呼叫或遠端方法呼叫。

  例如:

  • Java RMI (二進位制協議)
  • WebServices(文字協議)

1、訊息傳遞
  RPC 是一種請求-響應協議,一次 RPC 在客戶端初始化,再由客戶端將請求訊息傳遞到遠端的伺服器,執行指定的帶有引數的過程。經過遠端伺服器執行過程後,將結果作為響應內容返回到客戶端。

2、存根(Stub)
  存根(Stub)是在一次分散式計算 RPC 中,客戶端和伺服器轉換引數的一段程式碼。由於存根的引數轉化,RPC 執行過程如同本地執行函式呼叫。存根必須在客戶端和伺服器兩端均裝載,並且保持相容。

二、整合 Feign 框架圖
整合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=*