zoukankan      html  css  js  c++  java
  • 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自定义能力。

  • 相关阅读:
    部分模块
    正则表达式和re模块
    软件开发的目录规范
    模块以及模块的导入
    迭代器、可迭代对象、迭代器对象、生成器、生成器表达式和相关的面试题
    内置方法补充
    递归、二分法、匿名函数和部分内置方法
    python的闭包和装饰器
    函数名本质、函数命名空间和作用域链
    函数简介和函数的结构分析
  • 原文地址:https://www.cnblogs.com/killbug/p/10447625.html
Copyright © 2011-2022 走看看