1. 程式人生 > 其它 >【SpringCloud原理】萬字剖析OpenFeign之FeignClient動態代理生成原始碼

【SpringCloud原理】萬字剖析OpenFeign之FeignClient動態代理生成原始碼

 前面時候我釋出兩篇關於nacos原始碼的文章,一篇是聊一聊nacos是如何進行服務註冊的,另一篇是一文帶你看懂nacos是如何整合springcloud -- 註冊中心篇。今天就繼續接著剖析SpringCloud中OpenFeign元件的原始碼,來聊一聊OpenFeign是如何工作的。

一、@EnableFeignClinets作用原始碼剖析

我們都知道,要使用feign,必須要使用@EnableFeignClinets來啟用,這個註解其實就是整個feign的入口,接下來我們著重分析一下這個註解幹了什麼事。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.
class) public @interface EnableFeignClients { }

 這個註解通過@Import註解匯入一個配置類FeignClientsRegistrar.class,FeignClientsRegistrar實現了ImportBeanDefinitionRegistrar介面,所以Spring Boot在啟動的時候,會去呼叫FeignClientsRegistrar類中的registerBeanDefinitions來動態往spring容器中注入bean。如果有不懂小夥伴可以看一下我以前寫過的一篇文章 看Spring原始碼不得不會的@Enable模組驅動實現原理講解

,這裡詳細講解了@Import註解的作用。

接下來看一下registerBeanDefinitions的實現

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry)
    //這個方式是注入一些配置,就是對EnableFeignClients註解屬性的解析
    registerDefaultConfiguration(metadata, registry);
    //這個方法是掃秒加了@FeignClient註解
    registerFeignClients(metadata, registry);
}

這裡我們著重分析registerFeignClients,看一看是如何掃描@FeignClient註解的,然後掃描到之後又做了什麼。

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

    Set<String> basePackages;

    Map<String, Object> 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) {
      scanner.addIncludeFilter(annotationTypeFilter);
      basePackages = getBasePackages(metadata);
    }
    else {
      final Set<String> 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<BeanDefinition> 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<String, Object> attributes = annotationMetadata
              .getAnnotationAttributes(
                  FeignClient.class.getCanonicalName());

          String name = getClientName(attributes);
          registerClientConfiguration(registry, name,
              attributes.get("configuration"));

          registerFeignClient(registry, annotationMetadata, attributes);
        }
      }
    }
  }

這段程式碼我分析一下,先獲取到了一個ClassPathScanningCandidateComponentProvider這個物件,這個物件是按照一定的規則來掃描指定目錄下的類的,符合這個規則的每個類,會生成一個BeanDefinition,不知道BeanDefinition的小夥伴可以看我之前寫的關於bean生命週期的文章 Spring bean到底是如何建立的?(上)和 Spring bean到底是如何建立的?(下),裡面有過對BeanDefinition的描述。

獲取到ClassPathScanningCandidateComponentProvider物件,配置這個物件,指定這個物件需要掃描出來標有@FeignClient註解的類;隨後解析EnableFeignClients註解,獲取內部的屬性,獲取到指定的需要掃描包路徑下,如果沒有指定的,那麼就預設是當前註解所在類的所在目錄及子目錄。

然後就遍歷每個目錄,找到每個標有@FeignClient註解的類,對每個類就生成一個BeanDefinition,可以把BeanDefinition看成對每個標有@FeignClient註解的類資訊的封裝。

拿到一堆BeanDefinition之後,會遍歷BeanDefinition,然後呼叫registerClientConfiguration和registerFeignClient方法。

接下來我分別剖析一下這兩個方法的作用

registerClientConfiguration:

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
    BeanDefinitionBuilder builder = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientSpecification.class);
    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
    registry.registerBeanDefinition(
        name + "." + FeignClientSpecification.class.getSimpleName(),
        builder.getBeanDefinition());
  }

這裡的作用就是拿出你再@FeignClient指定的配置類,也就是configuration屬性,然後構建一個bean class為FeignClientSpecification,傳入配置。這個類的最主要作用就是將每個Feign的客戶端的配置類封裝成一個FeignClientSpecification的BeanDefinition,註冊到spring容器中。記住這個FeignClientSpecification,後面會有用。

registerFeignClient:

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> 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);

    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();

    boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
                                // null

    beanDefinition.setPrimary(primary);

    String qualifier = getQualifier(attributes);
    if (StringUtils.hasText(qualifier)) {
      alias = qualifier;
    }

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

registerFeignClient這個方法很重要我來說一下大概做了哪些事重新構造了一個BeanDefinition這個BeanDefinition的指定的class型別是FeignClientFactoryBean這個類實現了FactoryBean介面對spring有一定了解的小夥伴應該知道spring在生成bean的時候判斷BeanDefinition中bean的class如果是FactoryBean的實現的話會呼叫這個實現類的getObject來獲取物件這裡我就不展開講了