Spring Cloud Feign——快速入门

基于Netflix Feign实现,整合了Ribbon与Hystrix,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义方式。
只需要创建一个接口并用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了在使用Ribbon时自行封装服务调用客户端的开发量。Feign具备可插拔的注解支持,包括Feign注解和JAX-RS注解。

快速入门

  1. 创建工程(feign-consumer),引入依赖

    1
    2
    compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client')
    compile('org.springframework.cloud:spring-cloud-starter-openfeign')
  2. 主类中添加@EnableFeignClients注解开启Spring Cloud Feign的支持功能

  3. 定义HelloService接口,通过@FeignClient注解指定服务名来绑定服务,再使用Spring MVC的注解来绑定具体该服务提供的REST接口(复用hello-service服务)。

    1
    2
    3
    4
    5
    @FeignClient("hello-service")
    public interface HelloService {
    @RequestMapping("/hello")
    String hello();
    }
  4. 创建ConsumerController来实现对Feign客户端的调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @RestController
    public class ConsumerController {
    @Autowired
    HelloService helloService;

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

参数绑定

在现实系统中的各种业务接口比较复杂,可能会在HTTP的各个位置传入各种不同类型的参数,并且返回请求响应的时候也可能是一个复杂的对象结构。下面详细介绍Feign中对几种不同形式参数的绑定方法。

  1. 先扩展服务提供方hello-service,增加包括带有Request参数的请求、带有Header信息的请求、RequestBody的请求以及请求响应体中是一个对象的请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RequestMapping(value = "/hello1", method = RequestMethod.GET)
    public String hello(@RequestParam String name) {
    return "Hello " + name;
    }
    @RequestMapping(value = "/hello2", method = RequestMethod.GET)
    public User hello(@RequestHeader String name, @RequestHeader Integer age) {
    return new User(name, age);
    }
    @RequestMapping(value = "/hello3", method = RequestMethod.POST)
    public String hello(@RequestBody User user) {
    return "Hello " + user.getName() + ", " + user.getAge();
    }
  2. 定义User对象,注意,这里必须要有User的默认构造函数。不然,Spring Cloud Feign根据JSON字符串转换User对象时会抛异常。

  3. 在feign-consumer应用中实现这些新增的请求的绑定。
    1
    2
    3
    4
    5
    6
    7
    8
    @RequestMapping(value = "/hello1", method = RequestMethod.GET)
    String hello(@RequestParam("name") String name);

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

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

注意,在定义各参数绑定时,@RequestParam、@RequestHeader等可以指定参数名称的注解,它们的value不能少。在feign中绑定参数必须通过value属性来指明具体的参数名,不然会抛出IllegalStateException异常。

  1. 在ConsumerController中新增一个/feign-consumer2接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @RequestMapping(value = "/feign-consumer2",method = RequestMethod.GET)
    public String helloConsumer2(){
    StringBuilder sb=new StringBuilder();
    sb.append(helloService.hello()).append("\n");
    sb.append(helloService.hello("DIDI")).append("\n");
    sb.append(helloService.hello("DIDI",30)).append("\n");
    sb.append(helloService.hello(new User("DIDI",30))).append("\n");
    return sb.toString();
    }
  2. 测试返回以下数据,代表接口绑定和调用成功

    1
    2
    3
    4
    Hello World 
    Hello DIDI
    name=DIDI, age=30
    Hello DIDI, 30

继承特性

当使用Spring MVC的注解来绑定服务接口时,我们几乎完全可以从服务提供方的Controller中依靠复制操作,构建出相应的服务客户端绑定接口。既然存在这么多复制操作,我们可以考虑它能否进一步的抽象,在Spring Cloud Feign中,针对该问题提供了继承特性来解决这些复制操作,以减少编码量。

  1. 为了能够复用DTO与接口定义,创建一个基础工程,命名为hello-service-api。
  2. 由于需要在hello-service-api中定义可同时复用于服务端和客户端的接口,要使用到Spring MVC的注解,所以添加spring-boot-starter-web依赖
  3. 将上一节实现的User对象复制到hello-service-api中,并在hello-service-api工程中创建HelloService接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @RequestMapping("/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("name") String name, @RequestParam("age") Integer age);

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

因为后面还要使用hello-service和feign-consumer来重构,为了避免接口混淆,将提供服务的三个接口更名为/hello4、/hello5、/hello6.

  1. 对hello-service进行重构,新增对hello-service-api的依赖。
    implementation('com.example.helloserviceapi:hello-service-api')
  2. 创建RefactorHelloController类继承hello-service-api中定义HelloService接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @RestController
    public class RefactorHelloController implements HelloService {
    @Override
    public String hello(@RequestParam("name") String name) {
    return "Hello " + name;
    }

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

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

可以看到通过继承的方式,在Controller中不再包含以往会定义的请求映射注解@RequestMapping,而参数的注解定义在重写的时候会自动带过来。在这里只需要再增加@RestController注解使该类成为一个REST接口类就可以了。

  1. 在服务消费者feign-consumer中同样新增对hello-service-api的依赖。
  2. 创建RefactorHelloService接口,并继承hello-service-api包中的HelloService接口,然后添加@FeignClient注解来绑定服务
  3. 最后,在ConsumerController中,注入RefactorHelloService的实例,并新增一个请求来触发对RefactorHelloService的实例调用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Autowired
    RefactorHelloService refactorHelloService;

    @RequestMapping(value = "/feign-consumer3",method = RequestMethod.GET)
    public String helloConsumer3(){
    StringBuilder sb=new StringBuilder();
    sb.append(refactorHelloService.hello("MIMI")).append("\n");
    sb.append(refactorHelloService.hello("MIMI",20)).append("\n");
    sb.append(refactorHelloService.hello(new com.example.helloserviceapi.entity.User("MIMI",20))).append("\n");
    return sb.toString();
    }

优点和缺点

优点:是可以将接口定义从Controller中剥离,可以有效减少服务客户端的绑定配置,实现接口定义共享。
缺点:由于接口在构建期间就建立起了依赖,那么接口变动就会对项目构建造成影响,可能服务提供方修改了一个接口定义,那么会直接导致客户端工程构建失败。

Ribbon配置

由于 Spring Cloud Feign 的客户端负载均衡是通过 Spring Cloud Ribbon 实现的,所以我们可以直接通过配置 Ribbon 的客户端的方式来自定义各个服务客户端调用的参数。

全局配置

可以直接使用 ribbon.<key>=<value>的方式来设置ribbon的各项默认参数,比如,修改默认的客户端调用超时时间:

1
2
3
ribbon:
ConnectionTimeout: 500
ReadTimeout: 5000

指定服务配置

大多数情况下,我们对于服务调用的超时时间等可能会根据实际服务的特性做一些调整,所以仅仅依靠默认配置是不行的,在使用Spring Cloud Feign的时候,针对各个服务客户端进行个性化的配置方式与使用Spring Cloud Ribbon时的配置方式是一样的,都采用<client>.ribbon.<key>=<value>的格式设置,<client> 为使用@FeignClient注解的name属性或者value属性指定的服务名,需要区分大小写,示例如下:

1
2
3
4
5
6
7
8
hello-service:
ribbon:
ConnectionTimeout: 500 //连接超时时间
ReadTimeout: 2000 //读取超时时间
OkToRetryOnAllOperatotions: true //对所有操作请求都进行重试
MaxAutoRetriesNextServer: 2 //切换服务器实例的重试次数
MaxAutoRetries: 1 //对当前实例的重试次数
ServerListRefreshInterval: 200 //刷新服务列表源的间隔时间

重试机制

在Feign中默认实现了请求的重试机制。
示例:

  1. 在hello-service服务的/hello接口实现中,增加一些随机延迟。

    1
    2
    3
    4
    5
    6
    7
    8
    @RequestMapping(value = "/hello", method = RequestMethod.GET)
    public String index() throws Exception {
    int sleepTime = new Random().nextInt(3000);
    System.out.println("sleepTime:"+sleepTime);
    Thread.sleep(sleepTime);
    System.out.println("/hello");
    return "Hello World";
    }
  2. 在feign-consumer中增加上文提到的重试配置参数。由于hello-service.ribbon.MaxAutoRetries=1,所以重试策略先尝试访问首选实例一次,失败后才更换实例访问,而更换实例访问的次数通过hello-service.ribbon.MaxAutoRetriesNextServer=2来设置。

  3. 最后,启动这些服务,并尝试访问几次http://localhost:9001/feign-consumer 接口,当请求发生超时的时候,在hello-service控制台中会输出如下内容 第二次请求超时,Feign客户端发起了重试。Feign客户端在进行服务调用时,虽然经历了两次次失败,但是通过重试机制,最终还是获得了请求结果。所以四次请求共输出了四次/hello。所以,重试机制的实现,对于构建高可用的服务集群来说非常重要。
    注意:
    Ribbon的超时与Hystrix的超时是两个概念。为了让上述实现有效,需要让Hystrix的超市时间大于Ribbon的超时时间,否则Hystrix命令超时后,该命令直接熔断,重试机制就没有意义了。

Hystrix配置

Brixton版本(1.3)中,默认情况下,Spring Cloud Feign会为将所有Feign客户端的方法都封装到Hystrix命令中进行服务保护。下面来了解如何在使用Feign时配置Hytrix属性以及如何实现服务降级。

全局配置

直接使用它的默认配置前缀hystrix.command.default进行设置

1
2
3
4
5
6
# 设置全局超时时间
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
# 关闭Hystrix功能(不要和上面的配置一起使用)
feign.hystrix.enabled=false
# 关闭熔断功能
hystrix.command.default.execution.timeout.enabled=false

禁用Hystrix

可以通过feign.hystrix.enabled=false来关闭Hystrix功能。另外,如果不想全局关闭Hystrix支持,而只想针对某个服务客户端关闭Hystrix支持时,需要通过@Scope(“prototype”)注解为指定的客户端配置Feign.Builder实例,步骤如下:

  1. 构建一个关闭Hystrix的配置类

    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class DisableHystrixConfiguration {
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder() {
    return Feign.builder();
    }
    }
  2. 在HelloService的@FeignClient注解中,通过configuration参数引入上面实现的配置。

    1
    2
    3
    4
    @FeignClient(name = "hello-service", configuration = DisableHystrixConfiguration.class)
    public interface HelloService {
    ...
    }

指定命令配置

采用hystrix.command.<commandKey>作为前缀。而<commandKey>默认情况下会采用Feign客户端中的方法名作为标识,所以针对前面实现的对/hello接口的熔断超时时间的配置可以通过其方法名作为<commandKey>来进行配置
hystrix.command.hello.execution.isolation.thread.timeoutInMilliseconds=5000
在使用指定命令配置的时候,需要注意,由于方法名可能重复,这时相同方法名的Hystrix配置会共用。当然,可以重写Feign.Builder的实现,并在应用主类中创建它的实例来覆盖自动化配置的HystrixFeign.Builder实现

服务降级配置

Feign在定义服务客户端的时候,HystrixCommand定义被封装了起来,无法通过@HystrixCommand注解的fallback参数来指定具体的服务降级处理方法。但是,Spring Cloud Feign提供了另外一种简单的定义方式,下面来改造feign-consumer工程。

  1. 服务降级逻辑的实现只需要为Feign客户端的定义接口编写一个具体的接口实现类。比如为HelloService接口实现一个服务降级类HelloServiceFallback,其中每个重写方法的实现逻辑都可以用来定义相应的服务降级逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component
    public class HelloServiceFallback implements HelloService {
    @Override
    public String hello() {
    return "error";
    }
    @Override
    public String hello(@RequestParam("name") String name) {
    return "error";
    }
    @Override
    public User hello(@RequestHeader("name") String name, @RequestHeader("age") Integer age) {
    return new User("未知", 0);
    }
    @Override
    public String hello(@RequestBody User user) {
    return "error";
    }
    }
  2. 在服务绑定接口HelloService中,通过@FeignClient注解的fallback属性来指定对应的服务降级实现类

    1
    @FeignClient(name = "hello-service", fallback = HelloServiceFallback.class)
  3. 测试验证
    启动服务,但是不启动服务提供者hello-service。发送GET请求到http://localhost:9001/feign-consumer2
    会触发服务降级,并获得下面的输出内容

    1
    2
    3
    error
    name=未知, age=0
    error

如果报错:

com.netflix.client.ClientException: Load balancer does not have available server for client: hello-service


可能原因有两个:

  1. 在一些SpringCloud版本中,Feign对Hystrix的支持默认是关闭的,如果想开启对Hystrix的支持,需要在application.yml中添加:
    1
    2
    3
    feign:
    hystrix:
    enabled: true

在Dalston版本中(1.5),还需要引入hystrix依赖
implementation('org.springframework.cloud:spring-cloud-starter-netflix-hystrix')

  1. 如果按照上面做了,还不生效,可能是上一节中禁用了Hystrix,禁用实现类:
    1
    2
    3
    4
    5
    6
    7
    8
    @Configuration
    public class DisableHystrixConfiguration {
    @Bean
    @Scope("prototype")
    public Feign.Builder feignBuilder() {
    return Feign.builder();
    }
    }

这个类是通过使用@Scope(“prototype”)注解为指定的客户端配置Feign.Builder实例,从而构建一个关闭Hytrix的配置类,接下来就是在HelloService的@FeignClient注解中,通过configuration参数引入上面实现的配置:
@FeignClient(name = "hello-service", configuration = DisableHystrixConfiguration.class)
现在我们想测试服务降级配置,所以开启对Hystrix的支持,然后认为把@FeignClient注解中的configuration参数去掉就行了,结果还是报上面的错。
原因就是写的那个配置类DisableHystrixConfiguration上面还有@Configuration注解,就算在@FeignClient注解中没有引用该配置类,他也会被启动类上的注解扫描到,所以这里把@Configuration注释掉,或者删除DisableHystrixConfiguration类。

附:Spring Boot 与Spring Cloud版本对应关系图

其他配置

请求压缩

Spring Cloud Feign支持对请求与响应进行GZIP压缩,以减少通信过程中的性能损耗,提高通信效率,配置方式如下:

1
2
3
4
5
6
7
8
# 开启请求压缩功能
feign.compression.request.enabled=true
# 开启响应压缩功能
feign.compression.response.enabled=true
# 指定压缩的请求数据类型(默认值)
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩的大小下限(默认值)
feign.compression.request.min-request-size=2048

日志配置

Feign为每一个FeignClient都提供了一个feign.Logger实例,可以利用该日志对象的DEBUG模式来帮助分析Feign的请求细节。可以在application.yml文件中使用logging.level.的参数配置格式来开启指定Feign客户端的DEBUG日志,其中为Feign客户端定义接口的完整路径,在配置中开启日志,开启方式分两步:

  1. application.yml中配置日志输出

    1
    2
    # 开启日志 格式为logging.level.+Feign客户端路径
    logging.level.com.example.feignconsumer.service.HelloService=DEBUG
  2. 入口类中配置日志Bean

    1
    2
    3
    4
    @Bean
    Logger.Level feignLoggerLevel() {
    return Logger.Level.FULL;
    }

访问http://localhost:9001/feign-consumer 接口,控制台可以看到以下请求详细日志:

对于Feign的Logger级别主要有下面4类

  • NONE:不记录任何信息。
  • BASIC:仅记录请求方法、URL以及响应状态码和执行时间。
  • HEADERS:除了记录BASIC级别信息外,还会记录请求和响应的头信息。
  • FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据等。