Feign源码解析之注入IOC容器

/ Java / 没有评论 / 1551浏览

Feign源码解析之注入IOC容器

feign是springboot中特别重要的一部分,@FeignClient用来注解接口,通过处理注解的方式封装成http请求,从而通过调用服务的方式实现http请求。要在项目中使用@FeignClients首先需要在项目中加上@EnableFeignClients注解。

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

  /**
	 * Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
	 * declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
	 * {@code @ComponentScan(basePackages="org.my.pkg")}.
	 * @return the array of 'basePackages'.
	 */
  String[] value() default {};

  /**
	 * Base packages to scan for annotated components.
	 * <p>
	 * {@link #value()} is an alias for (and mutually exclusive with) this attribute.
	 * <p>
	 * Use {@link #basePackageClasses()} for a type-safe alternative to String-based
	 * package names.
	 *
	 * @return the array of 'basePackages'.
	 */
  String[] basePackages() default {};

  /**
	 * Type-safe alternative to {@link #basePackages()} for specifying the packages to
	 * scan for annotated components. The package of each class specified will be scanned.
	 * <p>
	 * Consider creating a special no-op marker class or interface in each package that
	 * serves no purpose other than being referenced by this attribute.
	 *
	 * @return the array of 'basePackageClasses'.
	 */
  Class<?>[] basePackageClasses() default {};

  /**
	 * A custom <code>@Configuration</code> for all feign clients. Can contain override
	 * <code>@Bean</code> definition for the pieces that make up the client, for instance
	 * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
	 *
	 * @see FeignClientsConfiguration for the defaults
	 */
  Class<?>[] defaultConfiguration() default {};

  /**
	 * List of classes annotated with @FeignClient. If not empty, disables classpath scanning.
	 * @return
	 */
  Class<?>[] clients() default {};
}

如上所示,@EnableFeignClients有5个属性,@EnableFeignClients通过扫描路径的方式将@Feign注解的接口注入IOC容器,其中value, basePackages, basePackageClasses和clients都是用来指定扫路径,defaultConfiguration指定feign相关的配置。

@EnableFeignClients通过@import引入FeignClientsRegistrar实现注入逻辑,FeignClientsRegistrar实现了ImportBeanDefinitionRegistrar接口,因此我们需要关注其核心方法registerBeanDefinitions。另外,FeignClientsRegistrar通过扫描路径的方式注入IOC容器,因此我们通过实现EnvironmentAware和ResourceLoaderAware接口来获取到项目的resourceLoader和environment。

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

registerBeanDefinitions又分为两步分别调用了两个方法来注入默认配置和feign客户端。

1. registerDefaultConfiguration方法

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

registerDefaultConfiguration方法逻辑并不复杂,其目的是注入@EnableFeignClients方法中的defaultConfiguration属性作为feign的默认配置。

如果@EnableFeignClients含有defaultConfiguration属性,我们有委托了registerClientConfiguration方法将其注入IOC容器,在进入registerClientConfiguration方法,我们组装了name参数,name参数根据@EnableFeignClients注解的类是否为top class分别采用了getClassName和 getEnclosingClassName。

关于hasEnclosingClass、getEnclosingClassName、getClassName的含义,可以参考Class对象的getXXXClass和getXXXName进行了解。

接着看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());
}

registerClientConfiguration根据传入的configuration对象和name参数组装成了BeanDefinition和beanName,然后调用registry.registerBeanDefinition方法实现最终的注入。registry.registerBeanDefinition可以说是spring中bean注入的通用方法,这儿不在展开。

2. registerFeignClients方法

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

  // 扫描的基本路径
  Set<String> basePackages;

  // 获取 EnableFeignClients 中的 clients属性
  Map<String, Object> attrs = metadata
    .getAnnotationAttributes(EnableFeignClients.class.getName());
  final Class<?>[] clients = attrs == null ? null
    : (Class<?>[]) attrs.get("clients");

  // 定义一个 Annotation 过滤器过滤 FeignClient.class
  AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
    FeignClient.class);
  if (clients == null || clients.length == 0) {
    // clients属性为空时,开启自动扫描
    scanner.addIncludeFilter(annotationTypeFilter);
    basePackages = getBasePackages(metadata);
  }
  else {
    // 根据clients属性过滤扫描
    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) {
    // scanner扫描
    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");
        // 获取FeignClient的属性
        Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(
          FeignClient.class.getCanonicalName());
        // 获取客户端名称
        String name = getClientName(attributes);
        // 注册配置信息
        registerClientConfiguration(registry, name, attributes.get("configuration"));
        // 注册对象
        registerFeignClient(registry, annotationMetadata, attributes);
      }
    }
  }
}

registerFeignclients方法较长,但逻辑也不复杂,主要包括两步:

2.1组装scanner对象

前文已经提过,feignclients的注入是通过scanner扫描实现注入的,因此scanner对象在feignclients的注入过程中至关重要。

2.1.1 scanner对象初始化

ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);

scanner对象初始化对应前两行,registerFeignClients通过getScanner新建scanner对象,然后注入resourceLoader,FeignClinetsRegistrar类通过实现EnvironmentAware和ResourceLoaderAware接口获得的environment和resourceLoader对象也是在这儿发挥作用。

protected ClassPathScanningCandidateComponentProvider getScanner() {
  return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
    @Override
    protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
      boolean isCandidate = false;
      if (beanDefinition.getMetadata().isIndependent()) {
        if (!beanDefinition.getMetadata().isAnnotation()) {
          isCandidate = true;
        }
      }
      return isCandidate;
    }
  };
}

可以看到,这儿使用的scanner的是ClassPathScanningCandidateComponentProvider对象,isCandidateComponent是否成立的条件是beanDefinitions对应的类是top class或者nested class且不是注解。

2.1.2 basePackages和includeFilters属性的设置

basePackages对应的是扫描的基本路径,includeFilters用于ClassPathScanningCandidateComponentProvider的protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException方法。 这里我们有根据@EnableFeignClients是个指定了clients属性分为了两种情况。

没有指定clients

此时,我们的includeFilters就是带有@FeignClient注解的类。

AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);

我们的basePackages根据@EnableFeignClients的values、basePackages和basePackageClasses属性获得,三者可以同时配置,并且可以同时生效。如果三者都没有配置,则采用@EnableFeignClients注解的类所在的包作为basePackages。

protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
  Map<String, Object> attributes = importingClassMetadata
    .getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());

  Set<String> basePackages = new HashSet<>();
  for (String pkg : (String[]) attributes.get("value")) {
    if (StringUtils.hasText(pkg)) {
      basePackages.add(pkg);
    }
  }
  for (String pkg : (String[]) attributes.get("basePackages")) {
    if (StringUtils.hasText(pkg)) {
      basePackages.add(pkg);
    }
  }
  for (Class<?> clazz : (Class[]) attributes.get("basePackageClasses")) {
    basePackages.add(ClassUtils.getPackageName(clazz));
  }

  if (basePackages.isEmpty()) {
    basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
  }
  return basePackages;
}

指定了clients属性

如果@EnbaleFeignClients指定了clients属性,则values、basePackages和basePackageClasses属性即使配置了也会失效。此时只有clients包含的类才需要被注入IOC容器。虽然有clients包含的类就是需要被注入IOC容器的类集合,但是为了兼容后面的扫描逻辑,我们还是需要将其配置成basePackages和includeFilters属性。 我们可以推出,basePackages就是clients中所有类所在包路径的集合,另外声明clientClasses变量存储clients中所有类的name,includeFilters除了需要要求类带有@FeignClients注解外,还需要其类名在clientClasses中。

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)));

在这儿,由于clientClasses存储的是class对象的getCanonicalName,进行判断clientClasses.contains(cleaned)时metadata获取的getClassName的时候对应的是class对象的getName,因此需要将$号替换成.号。 其实这儿我不明白的是为什么clientClasses不直接存储class对象的getName?想来想去只能理解成是为了排除anonymous class和local class这两种类型的类。

2.2 scanner扫描并注入IOC容器

这里,由于basePackages是一个set集合,可能还有多个值,因此我们通过循环扫描注入IOC容器的方式。

2.2.1 scanner扫描

在这儿,是用来ClassPathScanningCandidateComponentProvider的findCandidateComponents方法进行扫描。

Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);

我们应该能注意到,includeFilters用于ClassPathScanningCandidateComponentProvider的protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException方法,初始化的时候也重写了一个isCandidateComponent方法,这两者有什么关系和区别吗?

事实上,这两者都用于在扫描过程中判断扫描到的对象是否符合要求,只不过判断针对的对象不同,一个针对的是MetadataReader对象,通过TypeFilter对类的元数据进行判断,一个针对的是AnnotatedBeanDefinition对象,对bean进行判断。在扫描过程中,我们首先根据扫描到的resource获取metadataReader,判断protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException方法是否成立,成立后再根据metadataReader获得ScannedGenericBeanDefinition,判断protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition)是否成立。

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
  Set<BeanDefinition> candidates = new LinkedHashSet<>();
  try {
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
      resolveBasePackage(basePackage) + '/' + this.resourcePattern;
    Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
    boolean traceEnabled = logger.isTraceEnabled();
    boolean debugEnabled = logger.isDebugEnabled();
    for (Resource resource : resources) {
      if (traceEnabled) {
        logger.trace("Scanning " + resource);
      }
      if (resource.isReadable()) {
        try {
          MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
          if (isCandidateComponent(metadataReader)) {
            ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
            sbd.setResource(resource);
            sbd.setSource(resource);
            if (isCandidateComponent(sbd)) {
              if (debugEnabled) {
                logger.debug("Identified candidate component class: " + resource);
              }
              candidates.add(sbd);
            }
            else {
              if (debugEnabled) {
                logger.debug("Ignored because not a concrete top-level class: " + resource);
              }
            }
          }
          else {
            if (traceEnabled) {
              logger.trace("Ignored because not matching any filter: " + resource);
            }
          }
        }
        catch (Throwable ex) {
          throw new BeanDefinitionStoreException(
            "Failed to read candidate component class: " + resource, ex);
        }
      }
      else {
        if (traceEnabled) {
          logger.trace("Ignored because not readable: " + resource);
        }
      }
    }
  }
  catch (IOException ex) {
    throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
  }
  return candidates;
}

2.2.2 对扫描结果进行校验

@FeignClinets只能用来对接口进行注解,因此,这儿进行了校验。

AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");

2.2.3注入@FeignClients中的configuration

name参数使用@FeignClients中的value、name、serviceId属性,优先级从先到后。 registerClientConfiguration前文已经分析,这儿不再重复。

String name = getClientName(attributes);
registerClientConfiguration(registry, name, attributes.get("configuration"));
private String getClientName(Map<String, Object> client) {
  if (client == null) {
    return null;
  }
  String value = (String) client.get("value");
  if (!StringUtils.hasText(value)) {
    value = (String) client.get("name");
  }
  if (!StringUtils.hasText(value)) {
    value = (String) client.get("serviceId");
  }
  if (StringUtils.hasText(value)) {
    return value;
  }

  throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
                                  + FeignClient.class.getSimpleName());
}

2.2.4注入feignClients

这儿对@FeignClients的属性进行校验,然后填充组成BeanDefinitionBuilder 对象。这儿需要注意的是feignClients对象的beanName是对应class对象的getName。可以通过@FeignClients注解的qualifier属性指定alias,如果没有该属性,则采用name + “FeignClient”作为alias。

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);
}

通过getName方法获取到name来指定http请求的目标服务。按照优先级依次通过@FeignClients的name和value属性获取,并且通过精心初步校验。

String getName(Map<String, Object> attributes) {
  String name = (String) attributes.get("serviceId");
  if (!StringUtils.hasText(name)) {
    name = (String) attributes.get("name");
  }
  if (!StringUtils.hasText(name)) {
    name = (String) attributes.get("value");
  }
  name = resolve(name);
  if (!StringUtils.hasText(name)) {
    return "";
  }

  String host = null;
  try {
    String url;
    if (!name.startsWith("http://") && !name.startsWith("https://")) {
      url = "http://" + name;
    } else {
      url = name;
    }
    host = new URI(url).getHost();

  }
  catch (URISyntaxException e) {
  }
  Assert.state(host != null, "Service id not legal hostname (" + name + ")");
  return name;
}

总结一下:

FeignClientsRegistrar一共在两个方法里进行了注入IOC容器的操作,分别是:

  1. registerClientConfiguration方法:EnableFeignClients中的defaultConfiguration属性和FeignClients中 的configuration属性
  1. registerFeignClient方法:被FeignClients注解的接口