1. 程式人生 > >Spring Boot + Spring Cloud 實現許可權管理系統 後端篇(十九):服務消費(Ribbon、Feign)

Spring Boot + Spring Cloud 實現許可權管理系統 後端篇(十九):服務消費(Ribbon、Feign)

技術背景

上一篇教程中,我們利用Consul註冊中心,實現了服務的註冊和發現功能,這一篇我們來聊聊服務的呼叫。單體應用中,程式碼可以直接依賴,在程式碼中直接呼叫即可,但在微服務架構是分散式架構,服務都執行在各自的程序之中,甚至部署在不同的主機和不同的地區。這個時候就需要相關的遠端呼叫技術了。

Spring Cloud體系裡應用比較廣泛的服務呼叫方式有兩種:

1. 使用 RestTemplate 進行服務呼叫,可以通過 Ribbon 註解 RestTemplate 模板,使其擁有負載均衡的功能。

2. 使用 Feign 進行宣告式服務呼叫,宣告之後就像呼叫本地方法一樣,Feign 預設使用 Ribbon實現負載均衡。

兩種方式都可以實現服務之間的呼叫,可根據情況選擇使用,下面我們分別用實現案例來進行講解。

服務提供者

新建專案

新建一個專案 kitty-producer,新增以下依賴。

Swagger:API文件。

Consul :註冊中心。

Spring Boot Admin:服務監控。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation
="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.louis</groupId> <artifactId>kitty-producer</artifactId> <version>${project.version}</version> <packaging>jar</packaging> <name>kitty-producer</name> <description>kitty-producer</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.version>1.0.0</project.version> <java.version>1.8</java.version> <swagger.version>2.8.0</swagger.version> <mybatis.spring.version>1.3.2</mybatis.spring.version> <druid.version>1.1.10</druid.version> <spring.boot.admin.version>2.0.0</spring.boot.admin.version> <spring-cloud.version>Finchley.RELEASE</spring-cloud.version> </properties> <dependencies> <!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- swagger --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>${swagger.version}</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>${swagger.version}</version> </dependency> <!--spring-boot-admin--> <dependency> <groupId>de.codecentric</groupId> <artifactId>spring-boot-admin-starter-client</artifactId> <version>${spring.boot.admin.version}</version> </dependency> <!--consul--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-consul-discovery</artifactId> </dependency> <!--test--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <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> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>

配置檔案

在配置檔案新增內容如下,將服務註冊到註冊中心並新增服務監控相關配置。

application.yml

server:
  port: 8003
spring:
  application:
    name: kitty-producer
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        serviceName: ${spring.application.name}    # 註冊到consul的服務名稱
  boot:
    admin:
      client:
        url: "http://localhost:8000"
# 開放健康檢查介面
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: ALWAYS

啟動類

修改啟動器類,新增 @EnableDiscoveryClient 註解,開啟服務發現支援。

KittyProducerApplication.java

package com.louis.kitty.producer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class KittyProducerApplication {

    public static void main(String[] args) {
        SpringApplication.run(KittyProducerApplication.class, args);
    }
}

新增服務

新建一個 HelloController,提供一個 hello 介面, 返回字串資訊。

package com.louis.kitty.producer.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "hello kitty !";
    }
}

為了模擬均衡負載,複製一份上面的專案,重新命名為 kitty-producer2 ,修改對應的埠為 8004,修改 hello 方法的返回值為:"hello kitty 2!"。

依次啟動註冊中心、服務監控和兩個服務提供者,啟動成功之後重新整理Consul管理介面,發現我們註冊的kitty-producer服務,並有2個節點例項。

訪問: http://localhost:8500, 檢視兩個服務提供者已經註冊到註冊中心。

訪問: http://localhost:8000, 檢視兩個服務提供者已經成功顯示在監控列表中。

訪問 http://localhost:8003/hello,返回結果如下。

訪問 http://localhost:8004/hello,返回結果如下。

服務消費者

新建專案

新建一個專案 kitty-producer,新增以下依賴。

Swagger:API文件。

Consul :註冊中心。

Spring Boot Admin:服務監控。

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.louis</groupId>
    <artifactId>kitty-consumer</artifactId>
    <version>${project.version}</version>
    <packaging>jar</packaging>

    <name>kitty-consumer</name>
    <description>kitty-consumer</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <project.version>1.0.0</project.version>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <!-- web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <!--spring-boot-admin-->
           <dependency>
            <groupId>de.codecentric</groupId>
            <artifactId>spring-boot-admin-starter-client</artifactId>
            <version>${spring.boot.admin.version}</version>
        </dependency>
        <!--consul-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
        <!--test-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <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>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

新增配置

修改配置檔案如下。

application.yml

server:
  port: 8005
spring:
  application:
    name: kitty-consumer
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        serviceName: ${spring.application.name}    # 註冊到consul的服務名稱
  boot:
    admin:
      client:
        url: "http://localhost:8000"
# 開放健康檢查介面
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: ALWAYS

啟動類

修改啟動器類,新增 @EnableDiscoveryClient 註解,開啟服務發現支援。

KittyConsumerApplication.java

package com.louis.kitty.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@EnableDiscoveryClient
@SpringBootApplication
public class KittyConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(KittyConsumerApplication.class, args);
    }
}

服務消費者

新增消費服務測試類,新增兩個介面,一個查詢所有我們註冊的服務,另一個從我們註冊的服務中選取一個服務,採用輪詢的方式。

ServiceController.java

package com.louis.kitty.consumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class ServiceController {

    @Autowired
    private LoadBalancerClient loadBalancerClient;
    @Autowired
    private DiscoveryClient discoveryClient;

   /**
     * 獲取所有服務
     */
    @RequestMapping("/services")
    public Object services() {
        return discoveryClient.getInstances("kitty-producer");
    }

    /**
     * 從所有服務中選擇一個服務(輪詢)
     */
    @RequestMapping("/discover")
    public Object discover() {
        return loadBalancerClient.choose("kitty-producer").getUri().toString();
    }
}

新增完成之後,啟動專案,訪問:http://localhost:8500,服務消費者已經成功註冊到註冊中心。

訪問:http://localhost:8000,服務消費者已經成功顯示在監控列表中。

訪問 http://localhost:8005/services,返回兩個服務,分別是我們註冊的8003和8004。

[{
    "serviceId": "kitty-producer",
    "host": "GG20J1G2E.logon.ds.ge.com",
    "port": 8003,
    "secure": false,
    "metadata": {
        "secure": "false"
    },
    "uri": "http://GG20J1G2E.logon.ds.ge.com:8003",
    "scheme": null
}, {
    "serviceId": "kitty-producer",
    "host": "GG20J1G2E.logon.ds.ge.com",
    "port": 8004,
    "secure": false,
    "metadata": {
        "secure": "false"
    },
    "uri": "http://GG20J1G2E.logon.ds.ge.com:8004",
    "scheme": null
}]

反覆訪問 http://localhost:8005/discover,結果交替返回服務8003和8004,因為預設的負載均衡器是採用輪詢的方式。

       

8003 和 8004 兩個服務會交替出現,從而實現了獲取服務端地址的均衡負載。

大多數情況下我們希望使用均衡負載的形式去獲取服務端提供的服務,因此使用第二種方法來模擬呼叫服務端提供的 hello 方法。

建立 CallHelloController.java

package com.louis.kitty.consumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class CallHelloController {

    @Autowired
    private LoadBalancerClient loadBalancer;

    @RequestMapping("/call")
    public String call() {
        ServiceInstance serviceInstance = loadBalancer.choose("kitty-producer");
        System.out.println("服務地址:" + serviceInstance.getUri());
        System.out.println("服務名稱:" + serviceInstance.getServiceId());

        String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);
        System.out.println(callServiceResult);
        return callServiceResult;
    }

}

使用 RestTemplate 進行遠端呼叫。新增完之後重啟 kitty-consumer 專案。

在瀏覽器中訪問地址: http://localhost:8005/call 依次往復返回結果如下:

 

負載均衡器(Ribbon)

在上面的教程中,我們是這樣呼叫服務的,先通過 LoadBalancerClient 選取出對應的服務,然後使用 RestTemplate 進行遠端呼叫。

LoadBalancerClient 就是負載均衡器,預設使用的是 Ribbon 的實現 RibbonLoadBalancerClient,採用的負載均衡策略是輪詢。

1. 查詢服務,通過 LoadBalancer 查詢服務。

ServiceInstance serviceInstance = loadBalancer.choose("kitty-producer");

2.呼叫服務,通過 RestTemplate 遠端呼叫服務。

String callServiceResult = new RestTemplate().getForObject(serviceInstance.getUri().toString() + "/hello", String.class);

這樣就完成了一個簡單的服務呼叫和負載均衡。接下來我們說說Ribbon。

Ribbon是Netflix釋出的負載均衡器,它有助於控制HTTP和TCP的客戶端的行為。為Ribbon配置服務提供者地址後,Ribbon就可基於某種負載均衡演算法,自動地幫助服務消費者去請求。Ribbon預設為我們提供了很多負載均衡演算法,例如輪詢、隨機等。當然,我們也可為Ribbon實現自定義的負載均衡演算法。

ribbon內建負載均衡策略:

策略名 策略宣告 策略描述 實現說明
BestAvailableRule public class BestAvailableRule extends ClientConfigEnabledRoundRobinRule 選擇一個最小的併發請求的server 逐個考察Server,如果Server被tripped了,則忽略,在選擇其中ActiveRequestsCount最小的server
AvailabilityFilteringRule public class AvailabilityFilteringRule extends PredicateBasedRule 過濾掉那些因為一直連線失敗的被標記為circuit tripped的後端server,並過濾掉那些高併發的的後端server(active connections 超過配置的閾值) 使用一個AvailabilityPredicate來包含過濾server的邏輯,其實就就是檢查status裡記錄的各個server的執行狀態
WeightedResponseTimeRule public class WeightedResponseTimeRule extends RoundRobinRule 根據響應時間分配一個weight,響應時間越長,weight越小,被選中的可能性越低。 一個後臺執行緒定期的從status裡面讀取評價響應時間,為每個server計算一個weight。Weight的計算也比較簡單responsetime 減去每個server自己平均的responsetime是server的權重。當剛開始執行,沒有形成status時,使用roubine策略選擇server。
RetryRule public class RetryRule extends AbstractLoadBalancerRule 對選定的負載均衡策略機上重試機制。 在一個配置時間段內當選擇server不成功,則一直嘗試使用subRule的方式選擇一個可用的server
RoundRobinRule public class RoundRobinRule extends AbstractLoadBalancerRule roundRobin方式輪詢選擇server 輪詢index,選擇index對應位置的server
RandomRule public class RandomRule extends AbstractLoadBalancerRule 隨機選擇一個server 在index上隨機,選擇index對應位置的server
ZoneAvoidanceRule public class ZoneAvoidanceRule extends PredicateBasedRule 複合判斷server所在區域的效能和server的可用性選擇server 使用ZoneAvoidancePredicate和AvailabilityPredicate來判斷是否選擇某個server,前一個判斷判定一個zone的執行效能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate用於過濾掉連線數過多的Server。

修改啟動類

我們修改一下的啟動器類,注入 RestTemplate,並新增 @LoadBalanced 註解(用於攔截請求),以使用 ribbon 來進行負載均衡。

KittyConsumerApplication.java

package com.louis.kitty.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class KittyConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(KittyConsumerApplication.class, args);
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

新增服務

新建一個 RibbonHelloController 類,注入 RestTemplate,並呼叫服務提供者的hello服務。

package com.louis.kitty.consumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class RibbonHelloController {

    @Autowired
    private RestTemplate restTemplate;
    
    @RequestMapping("/ribbon/call")
    public String call() {
        // 呼叫服務, service-producer為註冊的服務名稱,LoadBalancerInterceptor會攔截呼叫並根據服務名找到對應的服務
        String callServiceResult = restTemplate.getForObject("http://kitty-producer/hello", String.class);
        return callServiceResult;
    }
}

測試效果

啟動消費者服務,訪問 http://localhost:8005/ribbon/call,依次返回結果如下:

   

說明 ribbon 的負載均衡已經成功啟動了。

負載策略

修改負載均衡策略很簡單,只需要在配置檔案指定對應的負載均衡器即可。如這裡把策略修改為隨機策略。

application.yml

#ribbon 負載均衡策略配置, service-producer為註冊的服務名
service-producer:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

如上,修改成隨機負載均衡策略之後,負載均衡器會隨機選取註冊的服務。

服務消費(Feign)

Spring Cloud Feign是一套基於Netflix Feign實現的宣告式服務呼叫客戶端。它使得編寫Web服務客戶端變得更加簡單。我們只需要通過建立介面並用註解來配置它既可完成對Web服務介面的繫結。它具備可插拔的註解支援,包括Feign註解、JAX-RS註解。它也支援可插拔的編碼器和解碼器。Spring Cloud Feign還擴充套件了對Spring MVC註解的支援,同時還整合了Ribbon來提供均衡負載的HTTP客戶端實現。

新增依賴

修改 kitty-consumer 的 pom 檔案,新增 feign 依賴。

pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

啟動類

修改啟動器類,新增 @EnableFeignClients 註解開啟掃描Spring Cloud Feign客戶端的功能:

KittyConsumerApplication.java

package com.louis.kitty.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class KittyConsumerApplication {

    public static void main(String[] args) {
        SpringApplication.run(KittyConsumerApplication.class, args);
    }

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

新增Feign介面

新增 KittyProducerService介面, 在類頭添加註解 @FeignClient("kitty-producer") ,kitty-producer是要呼叫的服務名。

新增跟呼叫目標方法一樣的方法宣告,只需要方法宣告,不需要具體實現,注意跟目標方法定義保持一致。

KittyProducerService.java

package com.louis.kitty.consumer.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;

@FeignClient(name = "kitty-producer")
public interface KittyProducerService {

    @RequestMapping("/hello")
    public String hello();
}

新增控制器

新增 FeignHelloController控制器,注入 KittyProducerService,就可以像使用本地方法一樣進行呼叫了。

FeignHelloController.java

package com.louis.kitty.consumer.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.louis.kitty.consumer.feign.KittyProducerService;

@RestController
public class FeignHelloController {

    @Autowired
    private KittyProducerService kittyProducerService;
    
    @RequestMapping("/feign/call")
    public String call() {
        // 像呼叫本地服務一樣
        return kittyProducerService.hello();
    }
}

測試效果

啟動成功之後,訪問 http://localhost:8005/feign/call,發現呼叫成功,且依次往復返回如下結果。

 

因為Feign是宣告式呼叫,會產生一些相關的Feign定義介面,建議將Feign定義的介面都統一放置管理,以區別內部服務。

 

原始碼下載

後端:https://gitee.com/liuge1988/kitty

前端:https://gitee.com/liuge1988/kitty-ui.git


作者:朝雨憶輕塵
出處:https://www.cnblogs.com/xifengxiaoma/ 
版權所有,歡迎轉載,轉載請註明原文作者及出處。