Spring Cloud Feign 声明式服务调用

Java框架

浏览数:148

2019-6-27

AD:资源代下载服务

一、Feign是什么?

​ 通过对前面Spring Cloud RibbonSpring Cloud Hystrix ,我们已经掌握了开发微服务应用时的两个重磅武器,学会了如何在微服务框架中进行服务间的调用和如何使用断路器来保护我们的服务,这两者被作为基础工具类框架广泛的应用在各个微服务框架中。既然这两个组件这么重要,那么有没有更高层次的封装来整合这两个工具以简化开发呢?Spring Cloud Feign就是这样的一个工具,它整合了Spring Cloud Ribbon 和 Spring Cloud Hystrix 来达到简化开发的目的。

​ 我们在使用Spring Cloud Ribbon时,通常都会使用RestTemplate的请求拦截来实现对依赖服务的接口调用,而RestTemplate已经实现了对Http请求的封装,形成了一套模板化的调用方法。在之前Ribbon的例子中,我们都是一个接口对应一个服务调用的url,那么在实际项目开发过程中,一个url可能会被复用,也就是说,一个接口可能会被多次调用,所以有必要把复用的接口封装起来公共调用。Spring Cloud Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务的接口定义。

二、Feign的快速搭建

​ 我们通过一个示例来看一下Feign的调用过程,下面的示例将继续使用之前的server-provider服务,这里我们通过Spring Cloud Feign提供的声明式服务绑定功能来实现对该服务接口的调用

  • 首先,搭建一个SpringBoot项目,取名为feign-consumer,并在pom.xml文件中引入spring-cloud-starter-eurekaspring-cloud-starter-feignn依赖,具体内容如下:
<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>1.3.7.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.feign.consumer</groupId>
    <artifactId>feign-consumer</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>feign-consumer</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

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

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

        <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>Brixton.SR5</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>
  • 搭建完成pom.xml之后,我们在feign-consumer的启动类上添加如下注解
    @EnableDiscoveryClient
  @EnableFeignClients
  @SpringBootApplication
  public class FeignConsumerApplication {

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

@EnableDiscoveryClient : 这个注解和@EnableEurekaClient 用法相同,表明这是一个Eureka客户端

@EnableFeignClients : 这个注解表明这个服务是一个Feign服务,能够使用@FeignClient 实现远程调用

  • 新建一个HelloService接口,在接口上加上__@FeignClient__注解,表明这个接口是可以进行远程访问的,也表明这个接口可以实现复用的接口,它提供了一些远程调用的方法,也相当于制定了一些规则。
        // 此处填写的是服务的名称
    @FeignClient(value = "server-provider")
    public interface HelloService {

        @RequestMapping(value = "hello")
        String hello();
    }

@FeignClient 后面的value值指向的是提供服务的服务名,这样就能够对spring.application.name = server.provider 的服务发起服务调用

  • 新建一个Controller,提供外界访问的入口,调用HelloService,完成一系列的服务请求-服务分发-服务调用
    @RestController
  public class ConsumerController {

      @Autowired
      HelloService helloService;

      @RequestMapping(value = "/feign-consumer", method = RequestMethod.GET)
      public String helloConsumer(){
          return helloService.hello();
      }
  }
  • 最后,为feign-consumer指定服务的端口号,服务的名称,并向注册中心注册自己
spring.application.name=feign-consumer
server.port=9001

eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

测试验证:

​ 像之前一样,启动四个服务: eureka-server, server-provider(8081,8082), feign-consumer

,启动http://localhost:9000/eureka/ 主页,发现主页上注册了四个服务

访问http://localhost:9001/feign-consumer 端口,发现 “Hello World” 能够返回

三、Feign的几种姿态

参数绑定

​ 在上一节的事例中,我们使用Spring Cloud Feign搭建了一个简单的服务调用的示例,但是实际的业务场景中要比它复杂很多,我们会在HTTP的各个位置传入不同类型的参数,并且返回的也是一个复杂的对象结构,下面就来看一下不同的参数绑定方法

  • 首先扩展一下server-provider中HelloController的内容
        
        @RequestMapping(value = "/hello1", method = RequestMethod.GET)
    public String hello1(@RequestParam String name){
        return "Hello " + name;
    }

    @RequestMapping(value = "/hello2", method = RequestMethod.GET)
    public User hello2(@RequestHeader Integer id,@RequestHeader String name){
        return new User(id,name);
    }

    @RequestMapping(value = "/hello3",method = RequestMethod.POST)
    public String hello3(@RequestBody User user){
        return "Hello " + user.getId() + ", " + user.getName();
    }
  • User 对象的定义入下,省略了get和set方法,需要注意的是,这里必须要有User的默认构造函数,否则反序列化的时候,会报Json解析异常
        
public class User {

    private Integer id;
    private String name;

    public User(){}
    public User(Integer id, String name) {
      this.id = id;
      this.name = name;
    }

   get and set...
}
  • feign-consumer中的HelloService中声明对服务提供者的调用
    @FeignClient(value = "server-provider")
  public interface HelloService {

      @RequestMapping(value = "hello")
      String hello();

      @RequestMapping(value = "/hello1", method = RequestMethod.GET)
      String hello1(@RequestParam("name") String name);

      @RequestMapping(value = "/hello2", method = RequestMethod.GET)
      User hello2(@RequestHeader("id") Integer id,@RequestHeader("name") String name);

      @RequestMapping(value = "/hello3", method = RequestMethod.POST)
      String hello3(@RequestBody User user);
  }

hello1 方法传递了一个参数为name的请求参数,它对应远程调用server-provider服务中的hello1方法

hello2 方法传递了一个请求头尾id 和 name的参数,对应远程调用server-provider服务中的hello2方法

hello3 方法传递了一个请求体为user的参数,对应远程调用呢server-provider服务中的hello3方法

  • 下面在ConsumerController类中定义一个helloConsumer1的方法,分别对hello1,hello2,hello3方法进行服务调用
        @RestController
    public class ConsumerController {

        @Autowired
        HelloService helloService;

        @RequestMapping(value = "/feign-consumer", method = RequestMethod.GET)
        public String helloConsumer(){
            return helloService.hello();
        }

        @RequestMapping(value = "/feign-consumer2", method = RequestMethod.GET)
        public String helloConsumer1(String name){
            StringBuilder builder = new StringBuilder();
            builder.append(helloService.hello()).append("\n");
            builder.append(helloService.hello1("lx")).append("\n");
            builder.append(helloService.hello2(23,"lx")).append("\n");
            builder.append(helloService.hello3(new User(24,"lx"))).append("\n");
            return builder.toString();
        }
    }

上面的helloConsumer1方法,分别调用了HelloServcie接口中的hello、hello1、hello2、hello3方法,传递对应的参数,然后对每一个方法进行换行

测试验证

在完成上述的改造之后,启动服务注册中心、两个 server-provider 服务以及我们改造过的feign-consumer。通 过发送GET请求到 <htttp://localhost:9001/feign-consumer2>, 触发 HelloService对新增接口的调用。最终,我们会获得如下输出,代表接口绑定和调用成功。

继承特性

​ 通过上述的示例,我们能够发现能够从服务提供方的Controller中依靠复制操作,构建出相应的服务客户端绑定接口。既然存在很多复制操作,我们自然考虑能否把公用的接口抽象出来?事实上也是可以的,Spring Cloud Feign提供了通过继承来实现Rest接口的复用,下面就来演示一下具体的操作过程

  • 首先为了演示Spring Cloud Feign的继承特性,我们新建一个maven 项目,名为feign-service-api,我们需要用到Spring MVC的注解,所以在pom.xml 中引入spring-boot-starter-web依赖,具体内容如下:
        <?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>

    <groupId>com.feign</groupId>
    <artifactId>feign-service-api</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.3.7.RELEASE</version>
        <relativePath />
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
</project>
    
  • 将User 对象复制到feign-service-api 中,如下
        public class User {

      private Integer id;
      private String name;

      // 必须加上
      public User(){}
      public User(Integer id, String name) {
          this.id = id;
          this.name = name;
      }

     get and set...
  }
  • 创建HelloService接口,并在接口中定义如下三个方法:
    @RequestMapping(value = "/refactor")
  public interface HelloService {

      @RequestMapping(value = "/hello4", method = RequestMethod.GET)
      String hello(@RequestParam("name")String name);

      @RequestMapping(value = "/hello5", method = RequestMethod.GET)
      User hello(@RequestHeader("id")Integer id,@RequestHeader("name")String name);

      @RequestMapping(value = "/hello6", method = RequestMethod.POST)
      String hello(@RequestBody User user);
  }
  • 定义完成后,使用idea 右侧的maven 工具,依次执行mvn clean ,mvn install,把feign-service-api打成jar包之后,现在切换项目至 server-provider ,并让server-provider依赖这个maven项目]

server-provider

  • server-provider 的pom.xml 添加 feign-service-api打包后的依赖
        <dependency>
            <groupId>com.feign</groupId>
            <artifactId>feign-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
  • 创建RefactorHelloController 实现feign-service-api中的HelloService 方法
    @RestController
  public class RefactorHelloController implements HelloService {

      // 注解没有带过来,这是自己加的
      @Override
      public String hello(@RequestParam("name") String name) {
          return "Hello " + name;
      }

      @Override
      public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name)             {
          return new User(id,name);
      }

      @Override
      public String hello(@RequestBody User user) {
          return "Hello " + user.getId() + ", " + user.getName();
      }
  }

这里有一个问题,当继承了HelloService 之后,@RestController,@RequestParam,@RequestHeader,@RequestBody 注解都没有带过来, 但是书上说是只有 @RestController 注解是带不过来的,余下三个都是可以的。这里未查明是何原因 ……

feign-consumer

  • 在完成了对server-provoder的构建之后,下面来构建feign-consumer服务,像server-provider 一样,在pom.xml 中添加对feign-service-api的依赖
        <dependency>
            <groupId>com.feign</groupId>
            <artifactId>feign-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
  • 创建RefactorHelloService 接口,继承feign-service-api中的 HelloService接口
    @FeignClient(value = "server-provider")
  public interface RefactorHelloService extends HelloService {}
  • ConsumerController类注入 RefactorHelloService,并测试 feign-service-api 中的方法,远程调用server-provider 中的 /hello4 /hello5 /hello6方法。
        @RequestMapping(value = "/feign-consumer3", method = RequestMethod.GET)
    public String helloConsumer3(String name){
        StringBuilder builder = new StringBuilder();
        builder.append(refactorHelloService.hello("lx")).append("\n");
        builder.append(refactorHelloService.hello(new com.feignservice.api.User(24,"lx"))).append("\n");
        builder.append(refactorHelloService.hello(23,"lx")).append("\n");
        return builder.toString();
    }

测试验证

​ 依次启动服务服务注册中心server-provider的两个实例,feign-consumer服务,在http://localhost:1111/ 主页能够发现如下几个服务

访问 http://localhost:9001/feign-consumer3( 使用Postman 访问),发现能够显示出来如下内容

Hello lx
Hello 24, lx
com.feignservice.api.User@5865261

优点和缺点

​ 使用Spring Cloud Feign的优点很多,可以将接口的定义从Controller 中剥离,同时配合Maven 构建就能轻易的实现接口的复用,实现在构建期的接口绑定,从而有效的减少服务客户端的绑定配置。但是这种配置使用不当也会带来副作用就是:你不能忽略频繁变更接口带来的影响。所以,如果团队打算采用这种方式来构建项目的话,最好在开发期间就严格遵守面向对象的开闭原则。避免牵一发而动全身,造成不必要的维护量。

四、其他配置

Ribbon 配置

​ 由于Spring Cloud Feign的客户端负载均衡是通过Spring Cloud Ribbon实现的,所以我们可以通过配置Spring Cloud Feign 从而配置 Spring Cloud Ribbon 。

全局配置

​ 全局配置的方法很简单,我们可以使用如下配置来设置全局参数

        ribbon.ConnectTimeout=5000
        ribbon.ReadTimeout=5000

指定服务配置

​ 大多数情况下,我们对于服务的调用时间可能会根据实际服务特性来做一些调整,所以仅仅依靠全局的配置是不行的,因为Feign 这个组件是整合了 Ribbon和 Hystrix的,所以通过设置Feign的属性来达到属性传递的目的。在定义Feign 客户端的时候,我们使用了@FeignClient()注解,其实在创建@FeignClient(value = server-provider)的时候,同时也创建了一个名为server-provider的ribbon 客户端,所以我们就可以使用@FeignClient中的nane 和value 值来设置对应的Ribbon 参数。

    # 使用feign-clients 中的注解的value值设置如下参数
  # HttpClient 的连接超时时间
  server-provider.ribbon.ConnectTimeout=500
  # HttpClient 的读取超时时间
  server-provider.ribbon.ReadTimeout=2000
  # 是否可以为此客户端重试所有操作
  server-provider.ribbon.OkToRetryOnAllOperations=true
  # 要重试的下一个服务器的最大数量(不包括第一个服务器)
  server-provider.ribbon.MaxAutoRetriesNextServer=2
  # 同一个服务器上的最大尝试次数(不包括第一个)
  server-provider.ribbon.MaxAutoRetries=1

重试机制

​ Spring Cloud Feign 中实现了默认的请求重试机制,我们可以通过修改server-provider中的示例做一些验证:

  • server-provider应用中的/hello接口实现中,增加一些随机延迟,比如
        @RequestMapping(value = "hello", method = RequestMethod.GET)
    public String hello() throws Exception{
        ServiceInstance instance = discoveryClient.getLocalServiceInstance();
        log.info("instance.host = " + instance.getHost() + "instance.service = " +  instance.getServiceId()
                + "instance.port = " + instance.getPort());
        log.info("Thread sleep ... ");
        int sleepTime = new Random().nextInt(3000);
        log.info("sleepTime = " + sleepTime);
        Thread.sleep(sleepTime);
        System.out.println("Thread awake");

        return "Hello World";
    }
  • feign-consumer 应用中增加上文提到的重试配置参数,来解释一下上面的配置

MaxAutoRetriesNextServer 设置为2 表示的是下一个服务器的最大数量,也就是说如果调用失败,会更换两次实例进行重试,MaxAutoRetries设置为1 表示的是每一个实例会进行一次调用,失败了再换为其他实例。OKToRetryOnAllOperations的意义是无论是请求超时或者socket read timeout都进行重试,

这里需要注意一点,Ribbon超时和Hystrix超时是两个概念,为了让上述实现有效,我们需要 让Hystrix的超时时间大于Ribbon的超时时间, 否则Hystrix命令超时后, 该命令直接熔断,重试机制就没有任何意义了。

Hystrix 配置

​ 在Spring Cloud Feign中,除了引入Spring Cloud Ribbon外,还引入了服务保护工具Spring Cloud Hystrix,下面就来介绍一下如何使用Spring Cloud Feign配置Hystrix属性实现服务降级。

全局配置

​ 对于Hystrix全局配置同Spring Cloud Ribbon 的全局配置一样,直接使用默认前缀 hystrix.command.default 就可以进行配置,比如设置全局的超时

    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000

​ 另外,在对Hystrix进行配置之前,我们需要确认feign.hystrix.enable参数没有设置为false,否则该参数设置会关闭Feign客户端的Hystrix支持。

    // 关闭Hystrix 功能(全局关闭)
    feign.hystrix.enabled=false
  // 关闭熔断功能
  hystrix.command.default.execution.timeout.enabled=false 

禁用hystrix

​ 如果不想全局地关闭Hystrix支持,而只想针对某个服务客户端关闭Hystrix支持,需要通过使用@Scope("prototype")注解为指定的客户端配置Feign.Builder 实例

  • 构建一个关闭Hystrix的配置类
    @Configuration
  public class DisableHystrixConfiguration {
    
      @Bean
      @Scope("prototype")
      public Feign.Builder builder(){
          return new Feign.Builder();
      }
  }
  • HelloService的@FeignClient注解中,通过Configuration参数引入上面实现的配置
    @FeignClient(value = "server-provider", fallback = DisableHystrixConfiguration.class)
  public interface RefactorHelloService extends HelloService {}

服务降级配置

​ Hystrix 提供的服务降级是服务容错的重要功能,之前我们开启Ribbon的服务降级是通过使用@HystrixCommand(fallbackMethod = "hystrixCallBack")开启的,FeignRibbon进行了封装,所以Feign 也提供了一种服务降级策略。下面我们就来看一下Feign 如何使用服务降级策略。我们在feign-consumer中进行改造

  • 服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类,比如为server-provider接口实现一个服务降级类 HelloServiceFallback,其中每个重写方法的逻辑都可以用来定义相应的服务降级逻辑,具体代码如下
    @Component
  public class FeignServiceCallback implements FeignService{

      @Override
      public String hello() {
          return "error";
      }

      @Override
      public String hello(@RequestParam("name") String name) {
          return "error";
      }

      @Override
      public User hello(@RequestHeader("id") Integer id, @RequestHeader("name") String name)             {
          return new User(0,"未知");
      }

      @Override
      public String hello(@RequestBody User user) {
          return "error";
      }
  }
  • 在服务绑定接口中,通过@FeignClient注解的fallback 属性来指定对应的服务降级类
  @FeignClient(value = "server-provider",fallback = FeignServiceCallback.class)
  public interface FeignService {

      @RequestMapping(value = "/hello")
      String hello();

      @RequestMapping(value = "/hello1", method = RequestMethod.GET)
      String hello(@RequestParam("name") String name);

      @RequestMapping(value = "/hello2", method = RequestMethod.GET)
      User hello(@RequestHeader("id") Integer id,@RequestHeader("name") String name);

      @RequestMapping(value = "/hello3", method = RequestMethod.POST)
      String hello(@RequestBody User user);
  }

测试验证

​ 下面我们来验证一下服务降级逻辑,启动注册中心Eureka-server,服务消费者feign-consumer,不启动server-provider,发送GET 请求到http://localhost:9001/feign-consumer2,该接口会分别调用FeignService中的四个接口,因为feign-consumer没有启动,会直接触发服务降级,使用Postman调用接口的返回值如下

    error
  error
  error
  com.feign.consumer.pojo.User@5ac0702f

后记: Spring Cloud Feign 声明式服务调用就先介绍到这里,下一篇介绍Spring Cloud Zuul服务网关

文章参考:

https://www.cnblogs.com/zhangjianbin/p/7228628.html

《Spring Cloud 微服务实战》

作者:c旋儿