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/ 版权全部,欢迎转载,转载请注明原文做者及出处。

相关文章
相关标签/搜索