SpringCloud全家桶---Feign

2,367 阅读13分钟

1.什么是Feign?

Feign是一个声明式的伪Http客户端,它使得写Http客户端变得更简单。 使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,并和Eureka结合

2.Feign有哪些注解

名称 解释
@RequestLine 方法上 定义HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表达式,可以通过在方法参数上使用
@Param 自动注入 @Param 方法参数 定义模板变量,模板变量的值可以使用名称的方式使用模板注入解析
@Headers 类上或者方法上 定义头部模板变量,使用@Param 注解提供参数值的注入。如果该注解添加在接口类上,则所有的请求都会携带对应的Header信息;如果在方法上,则只会添加到对应的方法请求上
@QueryMap 方法上 定义一个键值对或者 pojo,参数值将会被转换成URL上的 query 字符串上
@HeaderMap 方法上 定义一个HeaderMap, 与 UrlTemplate 和HeaderTemplate 类型,可以使用@Param 注解提供参数值
@FeignClient 注解指定调用的微服务名称,封装了调用 USER-API 的过程,作为消费方调用模板。
@EnableFeignClients 扫描声明它们是假装客户端的接口(@FeignClient )。配置组件扫描指令以供使用(@Configuration )类。
@SpringQueryMap Spring MVC相当于OpenFeign的{@QueryMap} 。

@FeignClient详解

名称 解释
name、value和serviceId 从源码可以得知,name是value的别名,value也是name的别名。两者的作用是一致的,name指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现。其中,serviceId和value的作用一样,用于指定服务ID,已经废弃。
qualifier 该属性用来指定@Qualifier注解的值,该值是该FeignClient的限定词,可以使用改值进行引用。
url url属性一般用于调试程序,允许我们手动指定@FeignClient调用的地址。
decode404 当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
configuration Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract。
fallback 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
fallbackFactory 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。
path path属性定义当前FeignClient的统一前缀。
primary 是否将伪代理标记为主Bean,默认为true。

@EnableFeignClients

名称 解释
value 为basePackages属性的别名,允许使用更简洁的书写方式。
basePackage 设置自动扫描带有@FeignClient注解的基础包路径。
basePackageClasses 该属性是basePackages属性的安全替代属性。该属性将扫描指定的每个类所在的包下面的所有被@FeignClient修饰的类;这需要考虑在每个包中创建一个特殊的标记类或接口,该类或接口除了被该属性引用外,没有其他用途。
defaultConfiguration 该属性用来自定义所有Feign客户端的配置,使用@Configuration进行配置。
clients 设置由@FeignClient注解修饰的类列表。如果clients不是空数组,则不通过类路径自动扫描功能来加载FeignClient。

2.feign基本用法

官网基础示例

interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
  @RequestLine("POST /repos/{owner}/{repo}/issues")
  void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

public class MyApp {
  public static void main(String... args) {
    GitHub github = Feign.builder().decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
  }
}

OpenFeign基础示例

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

@SpringBootApplication
@EnableFeignClients
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
@FeignClient("stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);
}

HTTP CLIENT 示例

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
# 配置
feign.httpclient.enabled = true

OkHttp 示例

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
# 配置
feign.okhttp.enabled = true

Spring Cloud Feign配置项

名称 解释
Logger.Level loggerLevel 日志级别,默认Logger.Level.NONE
Integer connectTimeout 连接超时时间 java.net.HttpURLConnection#getConnectTimeout(),如果使用Hystrix,该配置无效连接超时时间
Integer readTimeout 读取超时时间 java.net.HttpURLConnection#getReadTimeout(),如果使用Hystrix,该配置无效
Class retryer 重试接口实现类,默认实现 Retryer.NEVER_RETRY
Class errorDecoder 错误编码,默认ErrorDecoder.Default()
List<Class>requestInterceptors 请求拦截器
Boolean decode404 是否开启404编码
Class decoder 解码器, 将一个http响应转换成一个对象,Spring Cloud Feign 使用 ResponseEntityDecoder
Class encoder 编码器,将一个对象转换成http请求体中, Spring Cloud Feign 使用 SpringEncoder
Class contract 处理Feign接口注解,Spring Cloud Feign 使用SpringMvcContract 实现,处理Spring mvc 注解,也就是我们为什么可以用Springmvc 注解的原因。

配置示例

feign: 
  client: 
    default-config: my-config
    config:
      my-config:
       error-decoder: com.example.feign.MyErrorDecoder
       logger-level: full

配置项源码解析

// 配置类-FeignClientFactoryBean#configureFeign
protected void configureFeign(FeignContext context, Feign.Builder builder) {
  //配置文件,以feign.client开头
   FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
   if (properties != null) {
      if (properties.isDefaultToProperties()) {
          //使用java config 配置
         configureUsingConfiguration(context, builder);
          configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
         configureUsingProperties(properties.getConfig().get(this.name), builder);
      } else {
         configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
         configureUsingProperties(properties.getConfig().get(this.name), builder);
         configureUsingConfiguration(context, builder);
      }
   } else {
      configureUsingConfiguration(context, builder); // Feign默认配置
   }
}

配置项优先级

第一种:配置文件无配置

使用 java config 配置,优先级有低到高进行单个配置覆盖 1、FeignClientsConfiguration Spring Cloud Feign 全局默认配置。 2、@EnableFeignClients#defaultConfiguration 自定义全局默认配置。 3、FeignClient#configuration 单个Feign接口局部配置。

第二种:feign.client.default-to-properties=true(默认true)

java config 和application.properties(.yml)配置,优先级有低到高进行单个配置覆盖 1、FeignClientsConfiguration Spring Cloud Feign 全局默认配置。 2、@EnableFeignClients#defaultConfiguration 自定义全局默认配置。 3、FeignClient#configuration 单个Feign接口局部配置。 4、application.properties(.yml)配置文件全局默认配置,配置属性feign.client.default-config指定默认值(defult),如何使用,在application.properties(.yml)配置文件应用小节讲解 5、application.properties(.yml)配置文件局部配置,指定@FeignClient#name局部配置。

第三种:feign.client.default-to-properties=false(默认true)

java config 和application.properties(.yml)配置,优先级有低到高进行单个配置覆盖 1、application.properties(.yml)配置文件全局默认配置,配置属性feign.client.default-config指定默认值(defult),如何使用,在application.properties(.yml)配置文件应用小节讲解 2、application.properties(.yml)配置文件局部配置,指定@FeignClient#name局部配置。 3、FeignClientsConfiguration Spring Cloud Feign 全局默认配置。 4、@EnableFeignClients#defaultConfiguration 自定义全局默认配置。 5、FeignClient#configuration 单个Feign接口局部配置。

3.feign源码简析

主要流程步骤:

一、注册FeignClient配置类和FeignClient BeanDefinition

二、实例化Feign上下文对象FeignContext

三、创建 Feign.builder 对象

四、生成负载均衡代理类

五、生成默认代理类

六、注入到spring容器

Feign调用流程图

一、注册FeignClient配置类和FeignClient BeanDefinition

一、从启动类注开始,来看下@EnableFeignClients注解:@Import(FeignClientsRegistrar.class)  
二、我们分解别来看一下上面registerBeanDefinitions中的两个方法:  
1) 注册默认配置方法:registerDefaultConfiguration:  
上述方法为读取启动类上面@EnableFeignClients注解中声明feign相关配置类,默认name为default,一般情况下无需配置。用默认的FeignAutoConfiguration
即可。该方法中有比较重要的方法:注册配置registerClientConfiguration,启动流程一共有两处读取feign的配置类,第一处将bean配置类包装成FeignClientSpecification
,注入到容器。该对象非常重要,包含FeignClient需要的重试策略,超时策略,日志等配置,如果某个服务没有设置,则读取默认的配置。  
2)扫描FeignClient:  
        registerFeignClients方法主要是扫描类路径,对所有的FeignClient生成对应的BeanDefinition,这里第二处调了用registerClientConfiguration
        注册配置的方法,这里主要是将扫描的目录下,每个项目的配置类加载的容器当中。SpringCloud 
        FeignClient其实是利用了spring的代理工厂来生成代理类,所以这里将所有的feignClient的描述信息BeanDefinition设定为FeignClientFactoryBean类型,该类又继承FactoryBean,很明显,这是一个代理类。在spring中,FactoryBean是一个工厂bean,用作创建代理bean,所以得出结论,feign将所有的feignClient bean包装成FeignClientFactoryBean。扫描方法到此结束。  
三、代理类什么时候会触发生成呢?  
       在spring刷新容器时,当实例化我们的业务service时,如果发现注册了FeignClient,spring就会去实例化该FeignClient,同时会进行判断是否是代理bean,如果为代理bean,则调用FeignClientFactoryBean的getObject()方法生成代理bean。

二、实例化Feign上下文对象FeignContext

主要流程步骤:  
一、实例化FeignContex,实例化Feign上下文对象FeignContext  
二、可以看到feign的配置类设置到feign的容器当中,而集合中的元素 正是上面我们提到的两处调用registerClientConfiguration方法添加进去的,前后呼应。
       然而,当我们引入了sleuth之后,获取的feignContext确是TraceFeignClientAutoConfiguration中配置的实例sleuthFeignContext:可以看到上面创建了一个TraceFeignContext实例,因为该对象继承FeignContext,同时通过FeignContextBeanPostProcessor后处理器的postProcessAfterInitialization方法返回TraceFeignContext ,所以在上面第2步中通过类型获取:
applicationContext.getBean(FeignContext.class),最终拿到的是TraceFeignContext。

三、创建 Feign.builder 对象

1、Feign.Builder builder = get(context, Feign.Builder.class) ,又会有以下三种情况

1)单独使用Feign,通过加载FeignClientsConfiguration的配置创建Feign .Builder  
2)引入了hystrix,没有引入sleuth,通过加载FeignClientsConfiguration的配置创建HystrixFeign .Builder  
3)同时引入hystrix 和 sleuth,加载TraceFeignClientAutoConfiguration的配置创建HystrixFeign.Builder  

2、设置重试策略,log等组件

1)Feign.builder在获取之后又分别指定了重试策略,日志级别,错误代码code等  
2)将FeignContext做了缓存,每个服务对应一个FeignContext,服务名作为key。  

3、加载配置的顺序为:先加载每个服务的配置类,然后加载启动类注解上的配置类,最后加载默认的配置类。这样做有什么好处?

 spring刷新容器的方法也是对所有的bean进行了缓存,如果已经创建,则不再实例化。
 所以优先选取每个FeignClient的配置类,最后默认的配置类兜底。

四、生成负载均衡代理类

有个重要判断:判断FeignClient声明的url是否为空,来判断具体要生成的代理类

1)如果为空,则默认走Ribbon代理,也就是这个入口,会有加载ribbon的处理。
@FeignClient("MyFeignClient")
2)如果不为空,指定url,则走默认生成代理类的方式,也就是所谓的硬编码。
@FeignClient(value = "MyFeignClient",url = "http://localhost:8081")

这样处理方便开发人员进行测试,无需关注注册中心,直接http调用,是个不错的开发小技巧 接下来Client client = getOptional(context, Client.class);这里会从FeignContext上下文中获取Client对象 这里又会有三种情况:

1)没有整合ribbon、sleuth:获取默认的Client:Default实例。
2)整合了ribbon,没有整合sleuth: 获取LoadBalanceFeignClient实例。
3)整合了ribbon 和 sleuth: 会获取TraceFeignClient实例

feign初始化结构为动态代理的整个过程

1、初始化Feign.Builder传入参数,构造ReflectiveFeign
2、ReflectiveFeign通过内部类ParseHandlersByName的Contract属性,解析接口生成MethodMetadata
3、ParseHandlersByName根据MethodMetadata生成RequestTemplate工厂
4、ParseHandlersByName创建SynchronousMethodHandler,传入MethodMetadata、RequestTemplate工厂和Feign.Builder相关参数
5、ReflectiveFeign创建FeignInvocationHandler,传入参数SynchronousMethodHandler,绑定DefaultMethodHandler
6、ReflectiveFeign根据FeignInvocationHandler创建Proxy

接口调用过程:

1、接口的动态代理Proxy调用接口方法会执行的FeignInvocationHandler
2、FeignInvocationHandler通过方法签名在属性Map<Method, MethodHandler> dispatch中找到SynchronousMethodHandler,调用invoke方法
3、SynchronousMethodHandler的invoke方法根据传入的方法参数,通过自身属性工厂对象RequestTemplate.Factory创建RequestTemplate,工厂里面会用根据需要进行Encode
4、SynchronousMethodHandler遍历自身属性RequestInterceptor列表,对RequestTemplate进行改造
4、SynchronousMethodHandler调用自身Target属性的apply方法,将RequestTemplate转换为Request对象
5、SynchronousMethodHandler调用自身Client的execute方法,传入Request对象
6、Client将Request转换为http请求,发送后将http响应转换为Response对象
7、SynchronousMethodHandler调用Decoder的方法对Response对象解码后返回
8、返回的对象最后返回到Proxy

五、生成默认代理类

  理解了第四步的逻辑,生成默认代理类就很容易理解了,唯一不同点就是client的实现类为loadBalanceClient。 注意:不管是哪种代理类,最终发起请求还是由Feign.Default中的execute方法完成,默认使用HttpUrlConnection实现。

六、注入到spring容器

  总结:通过spring refresh()方法,触发FeignClientFactoryBean.getObject()方法获得了代理类,然后完成注入spring容器的过程。该实现方式同Dubbo的实现方式类似,有兴趣的可以自行研究噢。

4、总结

feign扩展点总结

名称 解释
Contract Contract的作用是解析接口方法,生成Rest定义。feign默认使用自己的定义的注解,还提供了JAXRSContract javax.ws.rs注解接口实现SpringContract是spring cloud提供SpringMVC注解实现方式
Client Client.Default 使用java api的HttpClientConnection发送http请求; ApacheHttpClient 使用apache的Http客户端发送请求; OkHttpClient 使用OKHttp客户端发送请求;RibbonClient 使用Ribbon进行客户端路由
Decoder json解码器 GsonDecoder、JacksonDecoder;XML解码器 JAXBDecoder;Stream流解码器 StreamDecoder
Encoder 默认编码器,只能处理String和byte[];json编码器GsonEncoder、JacksonEncoder;XML编码器JAXBEncoder
Target HardCodedTarget 默认Target,不做任何处理。LoadBalancingTarget 使用Ribbon进行客户端路由
Retryer 默认的策略是Retryer.Default,包含3个参数:间隔、最大间隔和重试次数,第一次失败重试前会sleep输入的间隔时间的,后面每次重试sleep时间是前一次的1.5倍,超过最大时间或者最大重试次数就失败
RequestInterceptor 调用客户端发请求前,修改RequestTemplate,比如为所有请求添加Header就可以用拦截器实现
InvocationHandler 通过InvocationHandlerFactory注入到Feign.Builder中,feign提供了Hystrix的扩展,实现Hystrix接入

优点

1.扩展性好,能有很多自定义的扩展点,如rxjava,hystrix等
2.轻量代码量少易学习
3.springclound默认使用的组件
4.api使用简单,简化了http调用api

缺点

1.组件很多默认的实现有一些坑,需要注意避免

本文使用 mdnice 排版