技术背景

上一篇教程中,我们利用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, 查看两个服务提供者已经注册到注册中心。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

访问: http://localhost:8000, 查看两个服务提供者已经成功显示在监控列表中。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

访问 http://localhost:8003/hello,返回结果如下。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

访问 http://localhost:8004/hello,返回结果如下。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

服务消费者

新建项目

新建一个项目 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,服务消费者已经成功注册到注册中心。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

访问:http://localhost:8000,服务消费者已经成功显示在监控列表中。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

访问 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,因为默认的负载均衡器是采用轮询的方式。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP       Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

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 依次往复返回结果如下:

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

负载均衡器(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内置负载均衡策略:

修改启动类

我们修改一下的启动器类,注入 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,依次返回结果如下:

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP   Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

说明 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,发现调用成功,且依次往复返回如下结果。

Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP Spring Boot + Spring Cloud 实现权限管理系统 后端篇(十九):服务消费(Ribbon、Feign)-LMLPHP

因为Feign是声明式调用,会产生一些相关的Feign定义接口,建议将Feign定义的接口都统一放置管理,以区别内部服务。

源码下载

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

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


作者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/ 
版权所有,欢迎转载,转载请注明原文作者及出处。

11-07 08:30