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

技術背景

上一篇教程中,咱們利用Consul註冊中心,實現了服務的註冊和發現功能,這一篇咱們來聊聊服務的調用。單體應用中,代碼能夠直接依賴,在代碼中直接調用便可,但在微服務架構是分佈式架構,服務都運行在各自的進程之中,甚至部署在不一樣的主機和不一樣的地區。這個時候就須要相關的遠程調用技術了。前端

Spring Cloud體系裏應用比較普遍的服務調用方式有兩種:java

1. 使用 RestTemplate 進行服務調用,能夠經過 Ribbon 註解 RestTemplate 模板,使其擁有負載均衡的功能。git

2. 使用 Feign 進行聲明式服務調用,聲明以後就像調用本地方法同樣,Feign 默認使用 Ribbon實現負載均衡。web

兩種方式均可以實現服務之間的調用,可根據狀況選擇使用,下面咱們分別用實現案例來進行講解。算法

服務提供者

新建項目

新建一個項目 kitty-producer,添加如下依賴。spring

Swagger:API文檔。apache

Consul :註冊中心。後端

Spring Boot Admin:服務監控。瀏覽器

pom.xmlmybatis

<?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/ 版權全部,歡迎轉載,轉載請註明原文做者及出處。

相關文章
相關標籤/搜索