您的位置:首页 > 编程语言 > Java开发

Spring Cloud Feign 声明式服务调用

2019-05-28 14:40 2391 查看

目录

  • 四、其他配置

    一、Feign是什么?

    ​ 通过对前面

    Spring Cloud Ribbon
    Spring 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-eureka
      spring-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")
    开启的,
    Feign
    Ribbon
    进行了封装,所以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 微服务实战》

  • 内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: