Feign源碼解析系列-註冊套路

感謝不知名朋友的打賞,感謝你的支持!

開始

在追尋Feign源碼的過程中發現了一些套路,既然是套路,就可以舉一反三,所以值得關注。
這篇會詳細解析Feign Client配置和初始化的方式,這些方式大多依賴Spring的遊戲規則,在和Spring相關的各個組件中都可以看到類似的玩法,都是可以舉一反三。所以熟悉這些套路大有益處。

內容

在上一篇中,我們提到了註解FeignClient引入了FeignClientsRegistrar,它繼承ImportBeanDefinitionRegistrar。
在Spring中,使用ImportBeanDefinitionRegistrar動態組裝註冊BeanDefinition,就是套路之一,像FeignClientsRegistrar一樣的類還有很多,比如:org.springframework.cloud.netflix.ribbon.RibbonClientConfigurationRegistrar,org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesRegistrar

FeignClientsRegistrar實現ImportBeanDefinitionRegistrar的registerBeanDefinitions方法:

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
   registerDefaultConfiguration(metadata, registry);
   registerFeignClients(metadata, registry);
}

從入口代碼調用的兩個方法看,從方法名上也可以看出來,要做的事可以分爲兩個:

1,註冊@EnableFeignClients中定義defaultConfiguration屬性下的類,包裝成FeignClientSpecification,註冊到Spring容器。
private void registerDefaultConfiguration(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   Map<String, Object> defaultAttrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName(), true);
   if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
      String name;
      // 註解只用的類進行判斷是否爲內部類或者方法內的本地類
      if (metadata.hasEnclosingClass()) {
         name = "default." + metadata.getEnclosingClassName();
      }
      else {
         name = "default." + metadata.getClassName();
      }
      registerClientConfiguration(registry, name,
            defaultAttrs.get("defaultConfiguration"));
   }
}
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());
}

registerClientConfiguration方法中,使用FeignClientSpecification生成BeanDefinitionBuilder,放入構造函數的兩個參數,然後構造了bean註冊的名稱。
這裏的名稱是類似這樣的:default.xxx.TestApplication.FeignClientSpecification。
假如你還記得在@FeignClient中有一個屬性:configuration,這個屬性是表示各個FeignClient自定義的配置類,後面也會通過調用registerClientConfiguration方法來註冊成FeignClientSpecification到容器。
所以,這裏可以完全理解在@EnableFeignClients中配置的是做爲兜底的配置,在各個@FeignClient配置的就是自定義的情況。

2,對於每個@FeignClient進行解析,並將他們註冊到spring容器。
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);
         }
      }
   }
}

我們知道@FeignClient的掃描路徑在@EnableFeignClients上是可以通過basePackages,basePackageClasses,client這三個參數進行配置的。所以在掃描@FeignClient之前就都是這個邏輯。確認好basePackages後,就遍歷basePackages,利用掃描器掃出各個路徑下的@FeignClient註解。

特別注意,@FeignClient是需要在定義掃描位置才能被解析的,如果你的feign客戶端接口不在掃描範圍是不會被入住到容器中,從而無法被使用。而且沒有入口可以更改配置的掃描路徑,在實際開發中需要注意。

這個掃描器ClassPathScanningCandidateComponentProvider又是spring的套路,通過配置的filters,找出需要的結果類。這個能力在自定義註解+掃描路徑可配置的場景非常合適。
在確認好@FeignClient註解的是否爲接口後,最後會解析配置,先調用registerClientConfiguration方法,後調用registerFeignClient方法。

registerClientConfiguration方法,前面已經提到過,這裏會對每個FeignClient都進行調用,所以會把@FeignClient上配置的configuration包裝成FeignClientSpecification到容器中。
registerFeignClient方法把FeignClientFactoryBean注入到容器,FeignClientFactoryBean用於生產FeignClient,後續詳細。

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);
   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 = name + "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);
}

FeignClientSpecification繼承NamedContextFactory.Specification,而NamedContextFactory用與創建子上下文將NamedContextFactory.Specification放入其中。
NamedContextFactory中維護一個context的map,value是AnnotationConfigApplicationContext即子上下文。
在feign中定義了一個FeignContext繼承NamedContextFactory,來統一維護feign中各個feign客戶端相互隔離的上下文。

類相互依賴圖:

FeignContext的代碼:

public class FeignContext extends  <FeignClientSpecification> {
   public FeignContext() {
      super(FeignClientsConfiguration.class, "feign", "feign.client.name");
   }
}

NamedContextFactory中的defaultConfigType被設置爲FeignClientsConfiguration。
這裏我們先看一下NamedContextFactory中的createContext方法的實現:

protected AnnotationConfigApplicationContext createContext(String name) {
   // 每次調用new一個AnnotationConfigApplicationContext
   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
   //在子上下文上就註冊name對應的configuration
   if (this.configurations.containsKey(name)) {
      for (Class<?> configuration : this.configurations.get(name)
            .getConfiguration()) {
         context.register(configuration);
      }
   }
  //註冊default configuration
   for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
      if (entry.getKey().startsWith("default.")) {
         for (Class<?> configuration : entry.getValue().getConfiguration()) {
            context.register(configuration);
         }
      }
   }
   // 將this.defaultConfigType即FeignClientsConfiguration也註冊上
   context.register(PropertyPlaceholderAutoConfiguration.class,
         this.defaultConfigType);
   context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
         this.propertySourceName,
         Collections.<String, Object> singletonMap(this.propertyName, name)));
   // 父上下文設置,所有的子上下文都是一個父上下文,當子上下文找不到時,就去父上下文找
   if (this.parent != null) {
      // Uses Environment from parent as well as beans
      context.setParent(this.parent);
   }
   context.refresh();
   return context;
}

FeignContext註冊到容器是在FeignAutoConfiguration上完成的:

@Autowired(required = false)
private List<FeignClientSpecification> configurations = new ArrayList<>();
@Bean
public FeignContext feignContext() {
   FeignContext context = new FeignContext();
   context.setConfigurations(this.configurations);
   return context;
}

在初始化FeignContext時,會把configurations在容器中放入FeignContext中。configurations的來源就是在前面registerFeignClients方法中將@FeignClient的配置configuration。

結束

關鍵需要理解的是在feign中爲每一個client準備了FeignContext,內部維護這個自定義配置的內容比如Encoder,Decoder等,從而實現對每個client自定義能力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章