SpringCloud 源码系列(12)— 服务调用Feign 之 基础使用篇

1,599 阅读12分钟

专栏系列文章:SpringCloud系列专栏

系列文章:

SpringCloud 源码系列(1)— 注册中心Eureka 之 启动初始化

SpringCloud 源码系列(2)— 注册中心Eureka 之 服务注册、续约

SpringCloud 源码系列(3)— 注册中心Eureka 之 抓取注册表

SpringCloud 源码系列(4)— 注册中心Eureka 之 服务下线、故障、自我保护机制

SpringCloud 源码系列(5)— 注册中心Eureka 之 EurekaServer集群

SpringCloud 源码系列(6)— 注册中心Eureka 之 总结篇

SpringCloud 源码系列(7)— 负载均衡Ribbon 之 RestTemplate

SpringCloud 源码系列(8)— 负载均衡Ribbon 之 核心原理

SpringCloud 源码系列(9)— 负载均衡Ribbon 之 核心组件与配置

SpringCloud 源码系列(10)— 负载均衡Ribbon 之 HTTP客户端组件

SpringCloud 源码系列(11)— 负载均衡Ribbon 之 重试与总结篇

Feign 概述

在使用 Spring Cloud 开发微服务应用时,各个服务提供者都是以HTTP接口的形式对外提供服务,因此在服务消费者调用服务提供者时,底层通过 HTTP Client 的方式访问。我们可以使用JDK原生的 URLConnection、Apache的 HttpClient、OkHttp、Spring 的 RestTemplate 去实现服务间的调用。但是最方便、最优雅的方式是通过 Spring Cloud OpenFeign 进行服务间的调用。

Feign 是一个声明式的 Web Service 客户端,它的目的就是让Web Service调用更加简单。Spring Cloud 对 Feign 进行了增强,使 Feign 支持 Spring MVC 的注解,并整合了 Ribbon、Hystrix 等。Feign还提供了HTTP请求的模板,通过编写简单的接口和注解,就可以定义好HTTP请求的参数、格式、地址等信息。Feign 会完全代理HTTP的请求,在使用过程中我们只需要依赖注入Bean,然后调用对应的方法传递参数即可。Feign 的首要目标是将 Java HTTP 客户端的书写过程变得简单。

Feign 的一些主要特性如下:

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解。
  • 支持可插拔的HTTP编码器和解码器。
  • 支持 Hystrix 和它的Fallback。支持Ribbon的负载均衡。
  • 支持HTTP请求和响应的压缩。

GitHub地址:

DEMO示例

还是使用前面研究 Eureka 和 Ribbon 时的 demo-producer、demo-consumer 服务来做测试。

1、首先,需要引入 openfeign 的依赖

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

spring-cloud-starter-openfeign 会帮我们引入如下依赖,包含了 OpenFeign 的核心组件。

2、在 demo-consumer 服务中,增加一个 Feign 客户端接口,来调用 demo-producer 的接口。

@FeignClient(value = "demo-producer")
public interface ProducerFeignClient {

    @GetMapping("/v1/user/{id}")
    ResponseEntity<User> getUserById(@PathVariable Long id, @RequestParam(required = false) String name);

    @PostMapping("/v1/user")
    ResponseEntity<User> createUser(@RequestBody User user);
}

3、在启动类加上 @EnableFeignClients 注解来启用 Feign 客户端。

@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {
    //....       
}

4、在接口中注入 ProducerFeignClient 就可以使用 Feign 客户端接口来调用远程服务了。

@RestController
public class FeignController {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private ProducerFeignClient producerFeignClient;

    @GetMapping("/v1/user/query")
    public ResponseEntity<User> queryUser() {
        ResponseEntity<User> result = producerFeignClient.getUserById(1L, "tom");
        User user = result.getBody();
        logger.info("query user: {}", user);
        return ResponseEntity.ok(user);
    }

    @GetMapping("/v1/user/create")
    public ResponseEntity<User> createUser() {
        ResponseEntity<User> result = producerFeignClient.createUser(new User(10L, "Jerry", 20));
        User user = result.getBody();
        logger.info("create user: {}", user);
        return ResponseEntity.ok(user);
    }
}

5、在 demo-producer 服务增加 UserController 接口供消费者调用

@RestController
public class UserController {
    private final Logger logger = LoggerFactory.getLogger(getClass());

    @PostMapping("/v1/user/{id}")
    public ResponseEntity<User> queryUser(@PathVariable Long id, @RequestParam String name) {
        logger.info("query params: id :{}, name:{}", id, name);
        return ResponseEntity.ok(new User(id, name, 10));
    }

    @PostMapping("/v1/user/{id}")
    public ResponseEntity<User> createUser(@RequestBody User user) {
        logger.info("create params: {}", user);
        return ResponseEntity.ok(user);
    }
}

6、测试

先把把注册中心启起来,然后 demo-producer 启两个实例,再启动 demo-consumer,调用 demo-consumer 的接口测试,会发现,ProducerFeignClient 的调用会轮询到 demo-consumer 的两个实例上。

通过简单的测试可以发现,Feign 使得 Java HTTP 客户端的书写过程变得非常简单,就像开发接口一样。另外,Feign 底层一定整合了 Ribbon@FeignClient 指定了服务名称,请求最终一定是通过 Ribbon 的 ILoadBalancer 组件进行负载均衡的。

FeignClient 注解

通过前面的DEMO可以发现,使用 Feign 最核心的应该就是 @EnableFeignClients@FeignClient 这两个注解,@FeignClient 加在客户端接口类上,@EnableFeignClients 加在启动类上,就是用来扫描加了 @FeignClient 接口的类。我们研究源码就从这两个入口开始。

要知道接口是不能直接注入和调用的,那么一定是 @EnableFeignClients 扫描到 @FeignClient 注解的接口后,基于这个接口生成了动态代理对象,并注入到 Spring IOC 容器中,才可以被注入使用。最终呢,一定会通过 Ribbon 负载均衡获取一个 Server,然后重构 URI,再发起最终的HTTP调用。

@EnableFeignClients 注解

首先看 @EnableFeignClients 的类注释,注释就已经说明了,这个注解就是用来扫描 @FeignClient 注解的接口的,那么核心的逻辑应该就是在 @Import 导入的类 FeignClientsRegistrar 中的。

EnableFeignClients 的主要属性有如下:

  • value、basePackages: 配置扫描 @FeignClient 的包路径
  • clients:直接指定扫描的 @FeignClient 接口
  • defaultConfiguration:配置 Feign 客户端全局默认配置类,从注释可以得知,默认的全局配置类是 FeignClientsConfiguration
package org.springframework.cloud.openfeign;

/**
 * Scans for interfaces that declare they are feign clients (via
 * {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
 * Configures component scanning directives for use with
 * {@link org.springframework.context.annotation.Configuration}
 * <code>@Configuration</code> classes.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

    // 指定扫描 @FeignClient 包所在目录
    String[] value() default {};

    // 指定扫描 @FeignClient 包所在目录
    String[] basePackages() default {};

    // 指定标记接口来扫描包
    Class<?>[] basePackageClasses() default {};

    // Feign 客户端全局默认配置类
    /**
     * A custom <code>@Configuration</code> for all feign clients. Can contain override
     * <code>@Bean</code> definition for the pieces that make up the client, for instance
     * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
     *
     * @see FeignClientsConfiguration for the defaults
     * @return list of default configurations
     */
    Class<?>[] defaultConfiguration() default {};

    // 直接指定 @FeignClient 注解的类,这时就会禁用类路径扫描
    Class<?>[] clients() default {};
}

@FeignClient 注解

首先看 @FeignClient 的类注释,注释说明 @FeignClient 注解就是声明一个 REST 客户端接口,而且会创建一个可以注入的组件,应该就是动态代理的bean。而且如果Ribbon可用,然后就可以用Ribbon做负载均衡,这个负载均衡可以用 @RibbonClient 定制配置类,名称一样就行。

FeignClient 注解被 @Target(ElementType.TYPE) 修饰,表示 FeignClient 注解的作用目标在类或接口上,后面从源码可以看到,@FeignClient 其实只能加在接口上面@Retention(RetentionPolicy.RUNTIME) 注解表明该注解会在 Class 字节码文件中存在,在运行时可以通过反射获取到。

@FeignClient 注解用于创建声明式 API 接口,该接口是 RESTful 风格的。Feign 被设计成插拔式的,可以注入其他组件和 Feign 一起使用。最典型的是如果 Ribbon 可用,Feign 会和Ribbon 相结合进行负载均衡。

FeignClient 主要有如下属性:

  • name:指定 FeignClient 的名称,如果项目使用了 Ribbon,name 属性会作为微服务的名称,用于服务发现。
  • url:url 一般用于调试,可以手动指定 @FeignClient 调用的地址。
  • decode404:当发生404错误时,如果该字段为true,会调用 decoder 进行解码,否则抛出 FeignException。
  • configuration:FeignClient 配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contracto
  • fallback:定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback 指定的类必须实现 @FeignClient 标记的接口。
  • fallbackFactory:工厂类,用于生成 fallback 类实例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。
  • path:定义当前 FeignClient 的统一前缀。
package org.springframework.cloud.openfeign;

/**
 * Annotation for interfaces declaring that a REST client with that interface should be
 * created (e.g. for autowiring into another component). If ribbon is available it will be
 * used to load balance the backend requests, and the load balancer can be configured
 * using a <code>@RibbonClient</code> with the same name (i.e. value) as the feign client.
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {

    // 指定服务名称
    @AliasFor("name")
    String value() default "";

    // 指定服务名称,已过期
    @Deprecated
    String serviceId() default "";

    // FeignClient 接口生成的动态代理的bean名称
    String contextId() default "";

    // 指定服务名称
    @AliasFor("value")
    String name() default "";

    // @Qualifier 标记
    String qualifier() default "";

    // 如果不使用Ribbon负载均衡,就需要使用url返回一个绝对地址
    String url() default "";

    // 404 默认抛出 FeignExceptions 异常,设置为true则替换为404异常
    boolean decode404() default false;

    // Feign客户端配置类,可以定制 Decoder、Encoder、Contract
    /**
     * A custom configuration class for the feign client. Can contain override
     * <code>@Bean</code> definition for the pieces that make up the client, for instance
     * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
     *
     * @see FeignClientsConfiguration for the defaults
     * @return list of configurations for feign client
     */
    Class<?>[] configuration() default {};

    // FeignClient 接口的回调类,必须实现客户端接口,并注册为一个bean对象。
    // 求失败或降级时就会进入回调方法中
    /**
     * Fallback class for the specified Feign client interface. The fallback class must
     * implement the interface annotated by this annotation and be a valid spring bean.
     * @return fallback class for the specified Feign client interface
     */
    Class<?> fallback() default void.class;

    // 回调类创建工厂
    Class<?> fallbackFactory() default void.class;

    // URL前缀
    String path() default "";

    // 定义为 primary bean
    boolean primary() default true;
}

FeignClient 核心组件

从上面已经得知,FeignClient 的默认配置类为 FeignClientsConfiguration,这个类在 spring-cloud-openfeign-core 的 jar 包下,并且每个 FeignClient 都可以定义各自的配置类。

打开这个类,可以发现这个类注入了很多 Feign 相关的配置 Bean,包括 Retryer、FeignLoggerFactory、Decoder、Encoder、Contract 等,这些类在没有 Bean 被注入的情况下,会自动注入默认配置的 Bean。

package org.springframework.cloud.openfeign;

@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
    @Autowired
    private ObjectFactory<HttpMessageConverters> messageConverters;
    @Autowired(required = false)
    private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
    @Autowired(required = false)
    private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();
    @Autowired(required = false)
    private Logger logger;

    @Bean
    @ConditionalOnMissingBean
    public Decoder feignDecoder() {
        return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
    public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
        return springEncoder(formWriterProvider);
    }

    @Bean
    @ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
    @ConditionalOnMissingBean
    public Encoder feignEncoderPageable(
            ObjectProvider<AbstractFormWriter> formWriterProvider) {
        //...
        return encoder;
    }

    @Bean
    @ConditionalOnMissingBean
    public Contract feignContract(ConversionService feignConversionService) {
        return new SpringMvcContract(this.parameterProcessors, feignConversionService);
    }

    @Bean
    @ConditionalOnMissingBean
    public Retryer feignRetryer() {
        return Retryer.NEVER_RETRY;
    }

    @Bean
    @Scope("prototype")
    @ConditionalOnMissingBean
    public Feign.Builder feignBuilder(Retryer retryer) {
        return Feign.builder().retryer(retryer);
    }

    @Bean
    @ConditionalOnMissingBean(FeignLoggerFactory.class)
    public FeignLoggerFactory feignLoggerFactory() {
        return new DefaultFeignLoggerFactory(this.logger);
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
    protected static class HystrixFeignConfiguration {
        @Bean
        @Scope("prototype")
        @ConditionalOnMissingBean
        @ConditionalOnProperty(name = "feign.hystrix.enabled")
        public Feign.Builder feignHystrixBuilder() {
            return HystrixFeign.builder();
        }
    }
    //...
}

这些其实就是 Feign 的核心组件了,对应的默认实现类如下。

Feign 配置

代码配置类

前面提到过 Feign 的核心组件,如果想自定义这些组件,可增加一个配置类,然后配置到 @FeignClient 的 configuration 上。

1、先定义一个配置类

public class ProducerFeignConfiguration {

    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default();
    }
}

2、配置到 @FeignClient 中

@FeignClient(value = "demo-producer", configuration = ProducerFeignConfiguration.class)
public interface ProducerFeignClient {

    //...
}

全局配置

前面已经了解到,@EnableFeignClientsdefaultConfiguration 可以配置全局的默认配置bean对象。也可以使用 application.yml 文件来配置。

feign:
  client:
    config:
      # 默认全局配置
      default:
        connectTimeout: 1000
        readTimeout: 1000
        loggerLevel: basic

指定客户端配置

@FeignClientconfiguration 可以配置客户端特定的配置类,也可以使用 application.yml 配置。

feign:
  client:
    config:
      # 指定客户端名称
      demo-producer:
        # 连接超时时间
        connectTimeout: 5000
        # 读取超时时间
        readTimeout: 5000
        # Feign日志级别
        loggerLevel: full
        # Feign的错误解码器
        errorDecoder: com.example.simpleErrorDecoder
        # 配置拦截器
        requestInterceptors:
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        # 404是否解码
        decode404: false
        #Feign的编码器
        encoder: com.example.simpleEncoder
        #Feign的解码器
        decoder: com.example.simpleDecoder
        #Feign的Contract配置
        contract: com.example.simpleContract

注意,如果通过Java代码的方式配置过 Feign,然后又通过属性文件的方式配置 Feign,属性文件中Feign的配置会覆盖Java代码的配置。但是可以配置 feign.client.default-to-properties=false 来改变Feign配置生效的优先级,使得优先使用Java代码的配置。

开启压缩配置

Spring Cloud Feign支持对请求和响应进行GZIP压缩,以提高通信效率。

feign:
  compression:
    request:
      # 配置请求GZIP压缩
      enabled: true
      # 配置压缩支持的 MIME TYPE
      mime-types: text/xml,application/xml,application/json
      # 配置压缩数据大小的下限
      min-request-size: 2048
    response:
      # 配置响应GZIP压缩
      enabled: true

FeignClient 开启日志

Feign 为每一个 FeignClient 都提供了一-个 feign.Logger 实例,可以在配置中开启日志。但是生产环境一般不要开启日志,因为接口调用可能会产生大量日志,一般在开发环境调试开启即可。

通过配置文件开启日志

首先设置客户端的 loggerLevel,然后配置 logging.level 日志级别为 debug。

feign:
  client:
    config:
      demo-producer:
        # Feign日志级别
        loggerLevel: full

logging:
  level:
    # 设置日志输出级别
    com.lyyzoo.sunny.register.feign: debug

之后调用 FeignClient 就可以看到接口调用日志了:

2020-12-30 15:33:02.459 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] ---> GET http://demo-producer/v1/user/1?name=tom HTTP/1.1
2020-12-30 15:33:02.459 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] ---> END HTTP (0-byte body)
2020-12-30 15:33:02.462 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] <--- HTTP/1.1 200 (3ms)
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] connection: keep-alive
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] content-type: application/json
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] date: Wed, 30 Dec 2020 07:33:02 GMT
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] keep-alive: timeout=60
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] transfer-encoding: chunked
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById]
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] {"id":1,"name":"tom","age":10}
2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] <--- END HTTP (30-byte body)
2020-12-30 15:33:02.463  INFO 2720 --- [nio-8020-exec-6] c.l.s.r.controller.FeignController       : query user: User{id=1, name='tom', age=10}

通过Java代码开启日志

首先还是需要设置日志输出级别:

logging:
  level:
    # 设置日志输出级别
    com.lyyzoo.sunny.register.feign: debug

然后配置一个 feign.Logger.Level 对象,指定输出所有日志就可以了。

@Bean
public feign.Logger.Level loggerLevel() {
    return Logger.Level.FULL;
}

Logger.Level 的具体级别如下:

public enum Level {
    // 不打印任何日志
    NONE,
    // 只打印请求的方法和URL,以及响应状态码和执行时间
    BASIC,
    // 在BASIC的基础上,打印请求头和响应头信息
    HEADERS,
    // 记录所有请求与相应的明细,包含请求头、请求体、元数据
    FULL
}