微服务通信之feign的注册、发现过程

共 7903字,需浏览 16分钟

 ·

2020-10-05 12:36

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

  作者 |  泥粑

来源 |  urlify.cn/amyIf2

66套java从入门到精通实战课程分享

前言

feign 是目前微服务间通信的主流方式,是springCloud中一个非常重要的组件。他涉及到了负载均衡、限流等组件。真正意义上掌握了feign可以说就掌握了微服务。

一、feign的使用

feign 的使用和dubbo的使用本质上非常相似。dubbo的理念是:像调用本地方法一样调用远程方法。那么套在feign上同样适用:像调用本地接口一样调用远程接口。
使用feign只需要2步:定义一个接口并用FeignClient注解说明接口所在服务和路径,服务启动类上添加@EnableFeignClients。如下所示

1.1,定义一个feign接口

@FeignClient(contextId = "order", name = "order", path = "/app")
public interface OrderApiFeignClient {

   /**
    * 获取订单列表
    * @return
    */
   @RequestMapping("order/list")
   BaseResponse> obtaining(@PathVariable("userId") Long userId);
}

1.2,再启动类上添加注解

@EnableSwagger2
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients("com.xxx.*")
@ComponentScan(value={"com.xxx"})
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication .class, args);
    }
}

二、feign 接口如何被实例化到spring容器的?

首先按照一般的思路,我们会猜测基于接口生成代理类,然后对接口的调用实际上调的是代理对象,那真的是这样么?我们带着猜想往下看。

2.1 @EnableFeignClients 注解都做了些什么?

可以看到注解本身主要定义了要扫描的feign接口包路径以及配置,但是注解本身又有注解Import ,可以看到他引入了FeignClientsRegistrar到容器。从名字看这个类就应该是在将feign接口注册到容器中,接下来我们具体看一下这个类干了些什么。

/**
 * @author Spencer Gibb
 * @author Jakub Narloch
 * @author Venil Noronha
 * @author Gang Li
 */
class FeignClientsRegistrar
  implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {

可以看到FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,但凡是实现了这个接口的类被注入到容器后,spring容器在启用过程中都会去调用它的void registerBeanDefinitions(AnnotationMetadata var1, BeanDefinitionRegistry var2)方法,可以确定的是FeignClientsRegistrar肯定重写了此方法,我们接下来看一下该方法的实现。

可以看到在这个方法中做了两件事:1)注册feign配置, 2)注册feign接口。我们这里抓一下重点,看一下feign接口是怎么注册的?

public void registerFeignClients(AnnotationMetadata metadata,
   BeanDefinitionRegistry registry) {
  ClassPathScanningCandidateComponentProvider scanner = getScanner();
  scanner.setResourceLoader(this.resourceLoader);

  Set basePackages;

  Map attrs = metadata
    .getAnnotationAttributes(EnableFeignClients.class.getName());
  AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
    FeignClient.class);
  final Class[] clients = attrs == null ? null
    : (Class[]) attrs.get("clients");
  if (clients == null || clients.length == 0) {
                        // 限定只扫描FeingClient注解
   scanner.addIncludeFilter(annotationTypeFilter);
   basePackages = getBasePackages(metadata);
  }
  else {
   final Set clientClasses = new HashSet<>();
   basePackages = new HashSet<>();
   for (Class clazz : clients) {
    basePackages.add(ClassUtils.getPackageName(clazz));
    clientClasses.add(clazz.getCanonicalName());
   }
   AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
    @Override
    protected boolean match(ClassMetadata metadata) {
     String cleaned = metadata.getClassName().replaceAll("\\$"".");
     return clientClasses.contains(cleaned);
    }
   };
   scanner.addIncludeFilter(
     new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
  }

  for (String basePackage : basePackages) {
   Set candidateComponents = scanner
     .findCandidateComponents(basePackage);
   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");

     Map attributes = annotationMetadata
       .getAnnotationAttributes(
         FeignClient.class.getCanonicalName());

     String name = getClientName(attributes);
     registerClientConfiguration(registry, name,
       attributes.get("configuration"));
                                        // 这里生成bean并且注册到容器
     registerFeignClient(registry, annotationMetadata, attributes);
    }
   }
  }
 }

上面这段代码概括起来就是:先找了包路径basePackages , 然后在从这些包路径中查找带有FeignClient注解的接口,最后将注解的信息解析出来作为属性手动构建beanDefine注入到容器中。(这里有一个类ClassPathScanningCandidateComponentProvider,它可以根据filter扫描指定包下面的class对象,十分好用,建议收藏)。包路径的获取以及扫描feign相对简单,这里不做阐述,我们看一下它生成bean的过程,关注上面代码中的registerFeignClient方法。

private void registerFeignClient(BeanDefinitionRegistry registry,
   AnnotationMetadata annotationMetadata, Map attributes) {
  String className = annotationMetadata.getClassName();
  BeanDefinitionBuilder definition = BeanDefinitionBuilder
    .genericBeanDefinition(FeignClientFactoryBean.class);
  validate(attributes);
  definition.addPropertyValue("url", getUrl(attributes));
  definition.addPropertyValue("path", getPath(attributes));
  String name = getName(attributes);
  definition.addPropertyValue("name", name);
  String contextId = getContextId(attributes);
  definition.addPropertyValue("contextId", contextId);
  definition.addPropertyValue("type", className);
  definition.addPropertyValue("decode404", attributes.get("decode404"));
  definition.addPropertyValue("fallback", attributes.get("fallback"));
  definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
  definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
                // 这里省略部分代码
  BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
 }

代码中通过BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class)生成的BeanDefine(请记住这里设置得FeignClientFactoryBean.type就是feign接口对应得class对象)。那么所有的feign接口最终注册到容器中的都是FeignClientFactoryBean对应的一个实例(注意实际上注册到容器中压根就不是FeignClientFactoryBean对应的实例化对象,具体原因看下文),到此feign接口对应的实例注册过程已经完成。那么回到一开始的问题为什么我们调用接口的方法最终发起了请求?是否有代理类的生成呢?我们接下来看看FeignClientFactoryBean类的特殊之处

2.2 FeignClientFactoryBean 类,feign接口代理生成类

由上文知,每一个feign接口实际上最终都会生成FeignClientFactoryBean ,最终由FeignClientFactoryBean生成具体的bean实例注册到容器中。

/**
 * @author Spencer Gibb
 * @author Venil Noronha
 * @author Eko Kurniawan Khannedy
 * @author Gregor Zurowski
 */
class FeignClientFactoryBean
  implements FactoryBean, InitializingBean, ApplicationContextAware 

可以看到该类实现了FactoryBean接口,这意味着当Spring注册该bean实例到容器中时,实际是调用其getObject方法,那么FeignClientFactoryBean一定是重写了getObject()方法,接下来我们看一下getObject()干了什么事情:

 public Object getObject() throws Exception {
  return getTarget();
 }

我们继续追踪getTarget()方法:

  T getTarget() {
  FeignContext context = this.applicationContext.getBean(FeignContext.class);
  Feign.Builder builder = feign(context);
  
                // 省略部分代码...
   
  Targeter targeter = get(context, Targeter.class);
  return (T) targeter.target(this, builder, context,
    new HardCodedTarget<>(this.type, this.name, url));
 }

显然最终的bean是通过target.target()方法生成,我们继续往下看:

 @Override
 public  T target(FeignClientFactoryBean factory, Feign.Builder feign,
   FeignContext context, Target.HardCodedTarget target) {
  return feign.target(target);
 }

显然最终的bean是通过feign.target(target)生成。我们继续往下看:

    public  T target(Target target) {
      return build().newInstance(target);
    }

显然最终得bean是通过build().newInstance(target)生成。我们继续往下看:

  public  T newInstance(Target target) {
    // 省略部分代码
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
      defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
  }

可以看到Proxy.newProxyInstance这个熟悉得身影了,没错他就是基于JDK原生得动态代理生成了FeignClientFactoryBean.type属性对应得class对应得代理类。从前文我们知道FeignClientFactoryBean.type就是feign接口得class对象。所以最终我们调用feign接口得方法实际上调用得是InvocationHandler方法。

三、小结

总结起来,就是常常我们挂在口头的东西就是将feign接口生成代理类,然后调用代理接口方法其实调用的代理类得方法,具体是为什么?不知道大家是否清楚。希望通过本文的阅读能让大家阅读源码的能力得到提升,也不在对feign有一种黑盒子的感觉。可能篇幅看起来较少,其实feign的注册过程牵涉到框架层面的知识还是蛮多的,包括springIoc、BeanDefine、动态代理等等,仔细看明白的话收获应该还是有蛮多的。哈哈,懂得都懂。顺手提一句:读源码一定要对SPI等等特别熟悉,要不然你会无从下手,没有方向,抓不到重点。后续会更新文章讲feign怎么实现负载均衡、熔断等。





粉丝福利:108本java从入门到大神精选电子书领取

???

?长按上方锋哥微信二维码 2 秒
备注「1234」即可获取资料以及
可以进入java1234官方微信群



感谢点赞支持下哈 


浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报