Cloud-OpenFeign 认识、使用及调用流程源码解析

feign 是cloud 体系中除了网关、注册、配置中心之外的,最基础的大三件之一,它的使用场景就是各个微服务之间的相互调用,而openFeign 是对feign 一种封装后的产品,它比feign 更加迎合市场,所以目前大部分企业使用的也是openFeign,但是它的本质还是feign,所以后面看源码的时候,不要纠结两者的区别,它们本质是一个东西。

feign 和openFeign 的概述及区别

上述也说了feign 和openFeign 的本质都是前者,它们的使用场景也都是:微服务之间的相互调用。它们是将对服务的调用转换成了对本地接口的调用,同时内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。

feign

feign 的使用方式是:使用feign 的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。

feign 本身不支持Spring MVC的注解,它有一套自己的注解,所以相应的使用它会增加一定的学习成本,导致它后面也慢慢的被市场所淘汰,当然也不是全面退出了市场,目前也有一些企业在使用这个,可以它的[官方api 文档](https://github.com/OpenFeig n/feign) 进行学习。

openFeign

OpenFeign 是Spring Cloud 在Feign 的基础上支持了Spring MVC 的注解,如@RequesMapping 等,是一个轻量级的Http 封装工具对象,大大简化了Http 请求,使得我们对服务的调用转换成了对本地接口方法的调用。

openFeign 的使用

一些概念性的知识,也就是八股文之类的就不介绍了,面试有需求的还是后面自己网上搜吧,我实在编不下去了,我们下面直接看代码。

首先要使用openFeign 还是先引入依赖,因为这里后面我们看源码的时候,源码版本是2.2.1 的,所以我引入的也是2.2.1 的starter。

<!--配置feign-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>

然后需要有一个@FeignClient 注解修饰对象,这里编写了调用对于服务端暴露出来的api 服务。

注意啊:这个对象最好不要放在本身的微服务项目中,比如下面这个,就最好不要放在hailtaxi-driver 微服务中,而是提供一个新的微服务用于中间转发调用,因为这个对象所在的微服务是需要被客户端引入的。

@FeignClient(value = "hailtaxi-driver") // value = 项目名称
public interface ServiceFeignClientApi {

    @RequestMapping(value = "/driver/info/{id}")
    Driver info(@PathVariable(value = "id")String id);
}

上面也说了,客户端需要引入被@FeignClient 修饰对象所在的微服务,所以这里直接先修改客户端的pom 文件。

<dependency>
    <groupId>com.itheima</groupId>
    <artifactId>hailtaxi-api</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

到了这一步,其实就已经可以编写客户端的调用接口了。

@RequestMapping(value = "/order")
public class OrderInfoController {
    @Autowired
    private ServiceFeignClientApi service;

    @PostMapping
    public OrderInfo add(){
        service.info("1");
        return new OrderInfo();
    }
}

如果使用的是idea,上面service 对象会有一个标注提示,因为我们还没有声明这是一个feign 的服务,最后一步就是需要在客户端的启动类上添加一个注释修饰@EnableFeignClients。basePackages 的值是上面service 接口对象所在的项目包。

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.itheima.driver.feign")
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class,args);
    }
}

这样一个完整的openFeign 使用配置流程就结束了,可以说非常简单,所有的复杂内容全部都已经被框架包装了,我们使用非常简单,可以说跟gateway 的使用一样简单。

小结:openFeign 的使用主要就是两个注解,一个被调用微服务的@FeignClient(注意:这里不是指服务端的微服务),另一个是客户端启动类上的@EnableFeignClients,有这两个注解之后,服务之间的调用才会交给openFeign 来完成。

还有一点啊,这两个注解都是openFeign 提供的,但是后面看源码的时候会发现,真正的底层调用还是feign 的源码。

源码分析

如果对于这种服务之间调用有过研究的话,比如dubbo 框架。其实看源码的时候就是分为两个部分:

  1. 客户端启动的时候,需要将对应的接口服务注入spring 的IOC 容器,如果没有接口对应的实现,那么就是注入一个代理对象,如果使用的是spring boot 那么就需要提供自动配置的内容;
  2. 客户端调用接口的时候,这里就是调用spring IOC 中已经注入的代理对象或者实现对象,一般情况下都是代理,通过代理对象进过一系列的逻辑,最后调用远端的服务端。

我们这里看openFeign 的调用流程源码也是分两个部分,客户端启动的时候、客户端调用接口的时候。

代码引入

上面描写使用的时候,就已经将版本限定在了2.2.1,所以这里就不用修改pom 文件了。

然后将openFeign 的2.2.1 版本源码下载 下来,我这里的是有点注释的,不需要的话,可以直接去官网下载。

我这里使用是idea,所以是找到项目的settings,然后通过modules 引入下载下来的项目即可,这里跟gateway 的引入是一样的。

还有一点需要注意,这里要使用2.2.1 版本的话,cloud 的版本也需要修改为Hoxton.SR1 版本的。

客户端启动流程分析

开始之前有点需要说明,这里涉及到spring boot 的内容,如果spring boot 自动配置的源码流程没有研究过的话,建议先去研究一下,回头在看这个,也可以看看我对于spring boot 的源码解析的文章。

构建BeanDefinition 的过程

spring boot 的内容这里就简单过,主要看openFeign 的逻辑。既然是自动配置,那么我们直接去看@EnableFeignClients 注解。

这里可以看到重点了,@Import 注解修饰,直接跟进FeignClientsRegistrar 对象的registerBeanDefinitions 方法。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class) // 开启了 `FeignClient`扫描
public @interface EnableFeignClients

这里分两步,框架的一些配置注册,还有就是注册被@FeignClient 注解修饰的接口。注意这里并不是将信息注入IOC 的单例池中,而是构建出对应的BeanDefinition 对象,然后存入到beanFactory 中的BeanDefinitionMap 集合中。

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 完成 Feign 框架相关的配置注册
    registerDefaultConfiguration(metadata, registry);
    // 注册由 @FeignClient 修饰的接口 bean *****核心*****
    registerFeignClients(metadata, registry);
}

分开看,首先是registerDefaultConfiguration 方法中,不用深究,这里就是构建了一个beanDefinition 存入了beanFactory 中,具体对象是FeignClientSpecification

重点看registerFeignClients 方法,这里代码太多,我们分段看重点。

首先第一步获取到@EnableFeignClients 注解的扫描路径,也就是EnableFeignClients 注解中的value 或basePackages 之类属性的值。

// 获取 @EnableFeignClients中配置的 @FeignClient 接口扫描路径
basePackages = getBasePackages(metadata);

获取到之后重点来了,这里没有的话,就算不是openFeign 的内容,就没有后面的内容了。

  1. 首先是将路径下所有被@FeignClient 注解修饰的对象转换为BeanDefinition;
  2. 然后判断这些BeanDefinition 都要是接口;
  3. 再将@FeignClient 注解的所有属性信息拿到;
  4. 然后就是registerClientConfiguration 方法,根据上面拿到name 属性再次构建对应的一个FeignClientSpecification 对象的beanDefinition 并存入了beanFactory;
  5. 最后就是registerFeignClient 方法,将接口上的所有方法,和@FeignClient 注解的信息都传入,然后分别去构建对应的代理对象的beanDefinition 并存入了beanFactory。
// 拿到  @EnableFeignClients 中配置的 @FeignClient 接口扫描路径 后开始 扫描
for (String basePackage : basePackages) {
    // 查找 basePackage 包路径下所有 由 @FeignClient 修饰的候选bean,返回其 BeanDefinition 的集合
    Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);

    // 针对每个标注了 @FeignClient 的候选 BeanDefinition (接口的BeanDefinition) 准备向容器中注册
    for (BeanDefinition candidateComponent : candidateComponents) {
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // verify annotated class is an interface
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            Assert.isTrue(annotationMetadata.isInterface(),"@FeignClient can only be specified on an interface");// @FeignClient标注的必须是接口
            // 获取 @FeignClient 注解的相关属性信息
            Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
            // 获取@FeignClient(value = "hailtaxi-driver"),name属性,name属性和value属性是相同的含义,都是配置服务名
            String name = getClientName(attributes);// name = hailtaxi-driver
            registerClientConfiguration(registry, name, attributes.get("configuration"));
            //  针对当前标注了 @FeignClient 注解的候选接口 BeanDefinition   向容器中注册bean信息
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

这里我们继续跟进registerFeignClient 方法,这里其实就做了一件事情,将传进来的方法构建成一个FeignClientFactoryBean 对象的beanDefinition 然后存入beanFactory 中,我们关注三行代码就行了。

开头第二行代码,构建一个FeignClientFactoryBean 类型的BeanDefinitionBuilder 对象,来完成后续构建BeanDefinition 的前提。

BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);

倒数第二行就是将构建完成的BeanDefinition 封装成一个BeanDeinitionHodler。

BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,new String[] { alias });

最后就是通过BeanDefinitionReaderUtils 来存入到beanFactory。

BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

小结:到这里@EnableFeignClients 注解的解析就结束了,后续就是开始构建bean 然后存入IOC 的单例池中了。

构建bean 并存入IOC 单例池过程

这里又涉及到了spring IOC 的一些内容,还是老话,没有研究过这个的,还是先去研究一下再来看这个,IOC 的东西我也详细写过文章,也可以直接去看我的。

这里我们就不一步一步跟了,直接去看doCerateBean 的内容,注意这里的调用时机:是在客户端中调用openFeign 接口对象,注入IOC 的时候,我这里就是上面使用过程中涉及到的OrderInfoController 对象注入IOC 的时候触发的。

也不用看其他的,直接去看初始化过程中给OrderInfoController 对象中的接口进行注入的时候。也就是doCreateBean 方法中调用初始化属性赋值的时候,也就是populateBean 方法的调用。

上述内容中,我们可以知道所有被@FeignClient 注解修饰的接口构建成BeanDefinition 的时候,都是将FeignClientFactoryBean 作为了指向对应对象,因为它就是FactoryBean 对象,所以直接去拿对应的FeignClientFactoryBean 的getObject 方法。

跟进FeignClientFactoryBean 中的getObject 方法,直接就是return getTarget() 这里是调用getTarget 方法进行返回。这个里面也不用关心别的,直接去看loadBalance 方法。

这里可以看到抛开其他的内容,这里主要就是去用HystrixTargeter 对象来生成代理。

Client client = getOptional(context, Client.class);
if (client != null) {
   // 将 feign 的 Client 对象设置进 Feign.Builder
   builder.client(client);
   // Targeter默认是 HystrixTargeter 在 FeignAutoConfiguration 中有配置
   Targeter targeter = get(context, Targeter.class);
    
   /**
    * 实例创建(开启熔断后具有熔断降级效果)
    * this= FeignClientFactoryBean
    * builder= Feign$Builder
    * context = FeignContext
    * target = HardCodedTarget
    */
   return targeter.target(this, builder, context, target);
}

直接看target 方法。这里说的就是默认的是Feign 的代理生成,也就是生成的FeignInvocationHandler 代理对象,但是如果在配置中开启了hystrix,那么就会生成一个HystrixInvocationHandler 代理对象。

**小结:**这样就是将FeignInvocationHandler 或者HystrixInvocationHandler 代理对象注入到了OrderInfoController 对象中的接口属性中,后续在调用接口的时候,直接走到这两个其中一个的invoke 方法中即可。

客户端调用流程分析

上面在启动完成之后,我们直接就可以看调用部分了,也就是FeignInvocationHandler 或者HystrixInvocationHandler 代理对象的invoke 方法,我们这里只分析HystrixInvocationHandler 代理对象的,因为它们两者其实没有什么太大的区别,主要是后者进行了负载和熔断等机制的操作。

跟进invoke 方法,代码首先是剔除走进来的一些通用调用,比如equalshashCodetoString 之类的。

重点是new HystrixCommand<Object> 的代码调用。这里可以直接跟进invoke。

new HystrixCommand<Object>(setterMethodMap.get(method)) {
  @Override
  protected Object run() throws Exception {
    try {
      return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
    } catch (Exception e) {
      throw e;
    } catch (Throwable t) {
      throw (Error) t;
    }
  }
}

后续会走到SynchronousMethodHandler 对象的invoke 方法。然后就是executeAndDecode(template, options) 的调用。之后继续是client.execute(request, options); 调用。

最后会走到LoadBalancerFeignClient 对象的execute 方法。

到了这一步基本是已经走到最后了,再往下跟就是到了底层将请求封装调用发送的地方,后面就可以不用看了,主要就看这里。

URI 和URL 的封装就不用看了,大体跟gateway 的差不了太多,然后就是获取RibbonRequest 对象,用于后续的负载调用。

最后就是通过FeignLoadBalancer 对象的executeWithLoadBalancer 方法调用后续发送请求的逻辑。

public Response execute(Request request, Request.Options options) throws IOException {
   try {
      URI asUri = URI.create(request.url());// http://hailtaxi-driver/driver/status/1/2
      String clientName = asUri.getHost();// hailtaxi-driver
      URI uriWithoutHost = cleanUrl(request.url(), clientName);
      FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost);
      IClientConfig requestConfig = getClientConfig(options, clientName);//各种连接参数
      // 开始进行负载均衡调用
      return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
   }
   catch (ClientException e) {
      IOException io = findIOException(e);
      if (io != null) {
         throw io;
      }
      throw new RuntimeException(e);
   }
}

小结:上述基本就是简单的介绍完了客户端调用的全流程,就是通过注入IOC 的代理对象,然后封装解析请求信息,然后获取负载对象完善URL,最后用底层逻辑通过http 请求调用到别的服务。

总结

服务之间调用的组件在cloud 体系中是不可缺少的,而openFeign 的使用相对来可以说是非常简单,结合spring boot 体系也能做到快速开发,但是它也不是不可替代的,其它组件比如:dubbo、feign 之类的,甚至可以自己去开发一个类似的框架,怎么选择还是。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值