Spring 类路径下 Bean 扫描实现分析

简介: 前言接上篇 Spring 5 启动性能优化之 @Indexed,上篇提到 Spring 可以在编译时生成索引文件,在应用上下文启动时可以通过索引文件查找所需要的注册的 Bean,如果不存在索引文件或者配置了不处理索引文件的参数,则不会从索引文件获取元数据。这时,Spring 便需要从指定的包中扫描 bean。

前言


接上篇 Spring 5 启动性能优化之 @Indexed,上篇提到 Spring 可以在编译时生成索引文件,在应用上下文启动时可以通过索引文件查找所需要的注册的 Bean,如果不存在索引文件或者配置了不处理索引文件的参数,则不会从索引文件获取元数据。这时,Spring 便需要从指定的包中扫描 bean。


要获取类的信息,我们第一反应一般是通过反射获取,因为类加载器将类加载到虚拟机中,会读取 class 文件,而 class 文件中包含着完整的类的信息。那么 Spring 是怎么做的呢?是否还有其他更高效的方式?跟随源码,让我们一探究竟。


Spring Bean 扫描分析


要想成为 Spring 的 bean,我们通常会在类上添加注解 @Component、@Controller、@Service、@Repository 等注解。并且 Spring 从 4.x 版本开始 Spring 开始支持条件注解 @Conditional,满足条件的类才会注册为 Spring 的 bean 。因此在分析源码之前,我们可以大概猜到具有这么一个流程。首先 Spring 从类路径中读取到所有的类的元数据,然后选择存在特定注解的类,最后如果存在条件注解还需要满足条件才能注册为 bean。实际上是不是这样呢?


扫描流程概览


我们仍将 AnnotationConfigApplicationContext 应用上下文的扫描方法作为入口进行分析。源码如下:


public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
  // 用来注册 bean
  private final AnnotatedBeanDefinitionReader reader;
  // 用来扫描并注册 bean
  private final ClassPathBeanDefinitionScanner scanner;
  public AnnotationConfigApplicationContext() {
    this.reader = new AnnotatedBeanDefinitionReader(this);
    this.scanner = new ClassPathBeanDefinitionScanner(this);
  }
  //从给定的包中扫描
  @Override
  public void scan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    this.scanner.scan(basePackages);
  }
}


scan 是扫描包的方法,实现异常简单,其委托给 ClassPathBeanDefinitionScanner 进行扫描,这是 Spring 内部实现经常用的一个组合模式,使类的职责更为清晰。查看 ClassPathBeanDefinitionScanner 扫描的源码如下:


public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {
  public int scan(String... basePackages) {
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();
    // 扫描并注册 bean
    doScan(basePackages);
    // 如果有必要的话,注册处理注解的 BeanFactoryPostProcessor 
    if (this.includeAnnotationConfig) {
      AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }
    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
  }
}


这里扫描分成了两个步骤,一个是扫描,另一个是注册处理注解配置的处理器 BeanFactoryPostProcessor ,应用上下文在其生命周期中会调用


BeanFactoryPostProcessor 的方法,这给予对 bean 进行额外处理的机会,如将 @Configuration 标注的类中标注 @Bean 注解的方法的返回对象注册为 Spring 中的 bean 。 注册的处理器及其实现在后面进行探讨,这里让我们把重点放到扫描过程。 跟踪 doScan 方法的源码如下:


  protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) {
      // 循环从包中查找候选组件
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      for (BeanDefinition candidate : candidates) {
        ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
        candidate.setScope(scopeMetadata.getScopeName());
        // 获取或生成 bean 的名称
        String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
        // 对候选组件进行再处理,设置必要的信息
        if (candidate instanceof AbstractBeanDefinition) {
          postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
        }
        if (candidate instanceof AnnotatedBeanDefinition) {
          AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
        }
        // 检查候选组件是否与现有 bean冲突或需要注册
        if (checkCandidate(beanName, candidate)) {
          BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
          definitionHolder =
              AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
          beanDefinitions.add(definitionHolder);
          // 注册 bean
          registerBeanDefinition(definitionHolder, this.registry);
        }
      }
    }
    return beanDefinitions;
  }


Spring 的方法一般不会太长,结构也较为清晰,这里我们可以看到,Spring 先查找候选组件,然后对候选组件设置必要的属性,最后检查候选组件与现存的 bean 定义如果无冲突,则进行注册,注册则是使用一个 map 来保存 bean 定义,后面再分析。跟踪其查找候选组件的方法 findCandidateComponents,源码如下:


  public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
      return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    } else {
      return scanCandidateComponents(basePackage);
    }
  }


查找候选组件根据是否存在索引文件进行了不同的处理,参见前文 Spring 5 启动性能优化之 @Indexed 查看索引文件的处理过程,这里把重点放到候选组件的扫描过程。跟踪 scanCandidateComponents 方法源码如下,其中省略了不重要的代码:


  private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    // 先将包名转换为 Resource
    String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
          resolveBasePackage(basePackage) + '/' + this.resourcePattern;
      Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);    
      for (Resource resource : resources) {
        // 然后获取元数据读取器
        MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
            // 如果读取到的类为候选组件则添加到候选组件列表
            if (isCandidateComponent(metadataReader)) {
              ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
              sbd.setSource(resource);
              if (isCandidateComponent(sbd)) {
                candidates.add(sbd);
              }
            }
      }
    return candidates;
  }


这里先将包名转换为 Resource,Resource 是 Spring 对资源的统一抽象,然后对 Resource 进行读取,转换为 MetadataReader ,MetadataReader 是元数据的读取器,可以读取到类的元信息,然后判断元信息对应的类为候选组件则添加到候选组件列表。后面我们关注两件事情,第一是 Resource 如何转换为 MetadataReader ,第二是如何判断类是否符合候选组件要求。


Resource 如何转换为 MetadataReader?


上述方法中先调用了 getMetadataReaderFactory() 获取 MetadataReaderFactory,然后使用 MetadataReaderFactory 获取 MetadataReader,那么我们就来看这几个类到底是做什么用的,包含了哪些信息。

查看 MetadataReaderFactory 源码如下:


public interface MetadataReaderFactory {
  // 根据类名获取 MetadataReader 
  MetadataReader getMetadataReader(String className) throws IOException;
  // 根据 Resource 获取 MetadataReader 
  MetadataReader getMetadataReader(Resource resource) throws IOException;
}


MetadataReaderFactory 是一个接口,其内部有两个工厂方法都是用来获取 MetadataReader ,再看什么是 MedataReader,源码如下:


public interface MetadataReader {
  // 获取 Resource
  Resource getResource();
  // 获取类的元数据
  ClassMetadata getClassMetadata();
  // 获取注解的元数据
  AnnotationMetadata getAnnotationMetadata();
}


MetadataReader 提供了获取 ClassMetadata 和 AnnotationMetadata 对象的方法,那么这两个类都是做什么用的呢?继续跟踪源码。ClassMetadata 源码如下:


public interface ClassMetadata {
  // 获取类名
  String getClassName();
  // 当前类是否为接口
  boolean isInterface();
  // 当前类是否表示注解
  boolean isAnnotation();
  // 当前类是否是抽象的
  boolean isAbstract();
  // 当前类是否可以创建
  default boolean isConcrete() {
    return !(isInterface() || isAbstract());
  }
  // 当前类是否标记为 final
  boolean isFinal();
  // 当前类是否是独立的,即为最顶部的类或为静态内部类
  boolean isIndependent();
  // 当前类是否定义在外部类中
  default boolean hasEnclosingClass() {
    return (getEnclosingClassName() != null);
  }
  // 获取外部类的名称
  @Nullable
  String getEnclosingClassName();
  // 当前类是否具有父类
  default boolean hasSuperClass() {
    return (getSuperClassName() != null);
  }
  // 获取当前类的父类的名称
  @Nullable
  String getSuperClassName();
  // 获取当前类实现的接口名称
  String[] getInterfaceNames();
  // 获取当前类的成员内部类名称
  String[] getMemberClassNames();
}


可以看到,ClassMetadata 是获取类的元数据的一个接口,其上面的信息也可以通过反射获取。再看 AnnotationMetadata ,源码如下:


public interface AnnotationMetadata extends ClassMetadata, AnnotatedTypeMetadata {
  // 获取注解上的元注解类型名称
  default Set<String> getMetaAnnotationTypes(String annotationName) {
    ...省略部分代码
  }
  // 给定的注解是否存在于类上
  default boolean hasAnnotation(String annotationName) {
    ...省略部分代码
  }
  // 给定的元注解是否存在于类上
  default boolean hasMetaAnnotation(String metaAnnotationName) {
    ...省略部分代码
  }
  // 给定的注解是否存在于类上方法上
  default boolean hasAnnotatedMethods(String annotationName) {
    ...省略部分代码
  }
  // 获取所有被给定注解或元注解标注的方法元数据
  Set<MethodMetadata> getAnnotatedMethods(String annotationName);
  // 使用反射创建给定类的注解元数据
  static AnnotationMetadata introspect(Class<?> type) {
    return StandardAnnotationMetadata.from(type);
  }


可以看到,AnnotationMetadata 继承了 ClassMetadata ,并且包含一些有关注解的元数据。既然 MetadataReader 是使用 MetadataReaderFactory 获取到的,那么跟踪 getMetadataReaderFactory() 方法查看如何获取 MetadataReaderFactory 的实现。源码如下:


  public final MetadataReaderFactory getMetadataReaderFactory() {
    if (this.metadataReaderFactory == null) {
      this.metadataReaderFactory = new CachingMetadataReaderFactory();
    }
    return this.metadataReaderFactory;
  }


这里获取到的是 CachingMetadataReaderFactory 的实例,它会调用父类 SimpleMetadataReaderFactory 的 getMetadataReader 方法,然后把获取到的 MetaReader 缓存,再跟踪 SimpleMetadataReaderFactory 的 getMetadataReader 方法的实现,源码如下:


  @Override
  public MetadataReader getMetadataReader(Resource resource) throws IOException {
    return new SimpleMetadataReader(resource, this.resourceLoader.getClassLoader());
  }


这里看到实际上就是 new 了一个 SimpleMetadataReader,查看其构造方法源码如下:


  SimpleMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException {
    SimpleAnnotationMetadataReadingVisitor visitor = new SimpleAnnotationMetadataReadingVisitor(classLoader);
    getClassReader(resource).accept(visitor, PARSING_OPTIONS);
    this.resource = resource;
    this.annotationMetadata = visitor.getMetadata();
  }
  @Override
  public ClassMetadata getClassMetadata() {
    return this.annotationMetadata;
  }
  @Override
  public AnnotationMetadata getAnnotationMetadata() {
    return this.annotationMetadata;
  }


这里看到 SimpleMetadataReader 使用了 asm 框架中的 ClassReader 读取类的信息,将 SimpleAnnotationMetadataReadingVisitor 作为访问者,接收 asm 访问到的类元信息,然后再转换为 AnnotationMetadata 保存,这样就获取到了 MetadataReader 中保存的元数据。通过上面的流程我们发现,Spring 实际并没有使用反射,因此不要进行类加载,使用 asm 框架直接读取 class 信息,大大提高了性能。


如何判断一个类是否满足组件要求

读取到类的元数据后就需要判断类是否满足组件的要求,跟踪 ClassPathBeanDefinitionScanner 的 isCandidateComponent 方法,其方法为父类 ClassPathScanningCandidateComponentProvider 的方法,源码如下:


  protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
    for (TypeFilter tf : this.excludeFilters) {
      if (tf.match(metadataReader, getMetadataReaderFactory())) {
        return false;
      }
    }
    for (TypeFilter tf : this.includeFilters) {
      if (tf.match(metadataReader, getMetadataReaderFactory())) {
        return isConditionMatch(metadataReader);
      }
    }
    return false;
  }


这里先使用 excludeFilters 排序满足条件的类,默认情况下 excludeFilters 为空,不会对扫描到的类进行排除。

排除不需要的类后就需要判断扫描的类是否满足条件。先使用 includeFilters 进行判断,满足条件的类有可能成为 Spring 的 bean。默认的 includeFilter 在 ClassPathBeanDefinitionScanner 实例化时注册,源码如下:


  public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
      Environment environment, @Nullable ResourceLoader resourceLoader) {
    Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
    this.registry = registry;
    if (useDefaultFilters) {
      // 注册默认的过滤器
      registerDefaultFilters();
    }
    setEnvironment(environment);
    setResourceLoader(resourceLoader);
  }
  protected void registerDefaultFilters() {
    ...省略部分代码
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
    this.includeFilters.add(new AnnotationTypeFilter(
          ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
    this.includeFilters.add(new AnnotationTypeFilter(
          ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
  }


可以看到,默认注册了支持 @Component、@ManagedBean、@Named 注解的 TypeFilter,只有类上存在这三个注解中的一个才可能成为候选组件。在 Spring 4.x 版本中,Spring 添加了条件注解,这里进行了支持。满足 includeFilter 后还使用 isConditionMatch 判断了条件是否满足,继续跟踪源码:


  private boolean isConditionMatch(MetadataReader metadataReader) {
    if (this.conditionEvaluator == null) {
      this.conditionEvaluator =
          new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver);
    }
    return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata());
  }


这里使用 ConditionEvaluator 对注解 @Conditional 进行处理,不满足则会跳过后面的处理。后续会专门对 @Conditional 进行分析。isConditionMatch 还具有另外一个重载的方法,只有两个条件都满足才会真正成为 Spring 的bean。这两个方法调用都在 ClassPathScanningCandidateComponentProvider#scanCandidateComponents ,相关源码前面已经进行了分析,这里再贴一次避免嵌套过深大家找不到。isConditionMatch 重载方法的源码如下:


  protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
    AnnotationMetadata metadata = beanDefinition.getMetadata();
    return (metadata.isIndependent() && (metadata.isConcrete() ||
        (metadata.isAbstract() && metadata.hasAnnotatedMethods(Lookup.class.getName()))));
  }


这里类成为 Spring 的 bean,还需要保证是独立能创建的。

整理流程如下:Spring 拿到扫描到的类的元信息,判断类上是否存在支持的注解,如果存在继续判断是否满足 @Conditional 注解,最后还要保证类是能够创建的,这样的一个类才完全的满足 Spring 组件的要求,成为 Spring 的 bean。


总结

本篇首先引出 Spring 在类路径下扫描 bean 的过程。通过分析,我们发现 Spring 使用 asm 框架直接对类的信息进行读取,并将读取到的信息映射为 Spring 中表示元数据的 ClassMetadata 及 AnnotationMetadata 。拿到所有给定包名下的类的元信息后,通过检查类上是否存在支持的注解,如 Component,然后继续判断类上是否存在 @Conditional 注解及是否满足条件,最后还要保证类能够创建才能成为 Spring 中组件的候选对象。至此,Spring 的 bean 扫描完成。


目录
相关文章
|
17天前
|
缓存 Java Spring
实战指南:四种调整 Spring Bean 初始化顺序的方案
本文探讨了如何调整 Spring Boot 中 Bean 的初始化顺序,以满足业务需求。文章通过四种方案进行了详细分析: 1. **方案一 (@Order)**:通过 `@Order` 注解设置 Bean 的初始化顺序,但发现 `@PostConstruct` 会影响顺序。 2. **方案二 (SmartInitializingSingleton)**:在所有单例 Bean 初始化后执行额外的初始化工作,但无法精确控制特定 Bean 的顺序。 3. **方案三 (@DependsOn)**:通过 `@DependsOn` 注解指定 Bean 之间的依赖关系,成功实现顺序控制,但耦合性较高。
实战指南:四种调整 Spring Bean 初始化顺序的方案
|
16天前
|
前端开发 Java 开发者
Spring生态学习路径与源码深度探讨
【11月更文挑战第13天】Spring框架作为Java企业级开发中的核心框架,其丰富的生态系统和强大的功能吸引了无数开发者的关注。学习Spring生态不仅仅是掌握Spring Framework本身,更需要深入理解其周边组件和工具,以及源码的底层实现逻辑。本文将从Spring生态的学习路径入手,详细探讨如何系统地学习Spring,并深入解析各个重点的底层实现逻辑。
40 9
|
1月前
|
XML Java 数据格式
Spring从入门到入土(bean的一些子标签及注解的使用)
本文详细介绍了Spring框架中Bean的创建和使用,包括使用XML配置文件中的标签和注解来创建和管理Bean,以及如何通过构造器、Setter方法和属性注入来配置Bean。
72 9
Spring从入门到入土(bean的一些子标签及注解的使用)
|
1月前
|
Java 测试技术 Windows
咦!Spring容器里为什么没有我需要的Bean?
【10月更文挑战第11天】项目经理给小菜分配了一个紧急需求,小菜迅速搭建了一个SpringBoot项目并完成了开发。然而,启动测试时发现接口404,原因是控制器包不在默认扫描路径下。通过配置`@ComponentScan`的`basePackages`字段,解决了问题。总结:`@SpringBootApplication`默认只扫描当前包下的组件,需要扫描其他包时需配置`@ComponentScan`。
|
2月前
|
XML Java 数据格式
spring复习02,xml配置管理bean
详细讲解了Spring框架中基于XML配置文件管理bean的各种方式,包括获取bean、依赖注入、特殊值处理、属性赋值、集合类型处理、p命名空间、bean作用域及生命周期和自动装配。
spring复习02,xml配置管理bean
|
1月前
|
Java BI API
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
这篇文章介绍了如何在Spring Boot项目中整合iTextPDF库来导出PDF文件,包括写入大文本和HTML代码,并分析了几种常用的Java PDF导出工具。
460 0
spring boot 整合 itextpdf 导出 PDF,写入大文本,写入HTML代码,分析当下导出PDF的几个工具
|
1月前
|
Java 开发者 Spring
Spring bean的生命周期详解!
本文详细解析Spring Bean的生命周期及其核心概念,并深入源码分析。Spring Bean是Spring框架的核心,由容器管理其生命周期。从实例化到销毁,共经历十个阶段,包括属性赋值、接口回调、初始化及销毁等。通过剖析`BeanFactory`、`ApplicationContext`等关键接口与类,帮助你深入了解Spring Bean的管理机制。希望本文能助你更好地掌握Spring Bean生命周期。
87 1
|
1月前
|
Java Spring
获取spring工厂中bean对象的两种方式
获取spring工厂中bean对象的两种方式
45 1
|
1月前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
182 2
|
1月前
|
Java 开发者 Spring
Spring bean的生命周期详解!
本文详细介绍了Spring框架中的核心概念——Spring Bean的生命周期,包括实例化、属性赋值、接口回调、初始化、使用及销毁等10个阶段,并深入剖析了相关源码,如`BeanFactory`、`DefaultListableBeanFactory`和`BeanPostProcessor`等关键类与接口。通过理解这些核心组件,读者可以更好地掌握Spring Bean的管理和控制机制。
88 1
下一篇
无影云桌面