Spring 5 启动性能优化之 @Indexed

简介: 背景Spring 经过近20年的发展,目前版本已经迭代到了5.x,每个版本 Spring 都有不同的改进,版本 5.x 中,Spring 把重心放到了性能优化上。我们知道,Spring 注解驱动编程中,Spring 启动时需要对类路径下的包进行扫描,以便发现所需管理的 bean。如果在应用启动前能够确定 Spring bean,不再进行扫描,那么性能就会大大提高,Spring 5 对此进行了实现。

背景

Spring 经过近20年的发展,目前版本已经迭代到了5.x,每个版本 Spring 都有不同的改进,版本 5.x 中,Spring 把重心放到了性能优化上。我们知道,Spring 注解驱动编程中,Spring 启动时需要对类路径下的包进行扫描,以便发现所需管理的 bean。如果在应用启动前能够确定 Spring bean,不再进行扫描,那么性能就会大大提高,Spring 5 对此进行了实现。


@Indexed 与 spring-context-indexer


spring-framework 在版本 5.x 中新增了一个模块 spring-context-indexer,作用就是在编译时扫描 @Indexed 注解,确定 bean,生成索引文件。先看如下 Spring 官网 候选组件索引生成 一节的介绍。


尽管类路径扫描已经非常快了,仍可以在编译时创建静态的候选列表来提高大型应用的启动性能。在这种模式下,组件扫描的所有目标的模块都必须使用这种机制。


现有的 @ComponentScan 或 <context:component-scan 指令必须保留,以便让上下文扫描包中的候选对象。当 ApplicationContext 扫描到这样的索引,将自动使用它,而不是扫描类路径。


要生成索引,需要向包含组件的每个模块中添加附件的依赖,这些组件是组件扫描的目标。以下示例展示了如何在 maven 中使用。


<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.6.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>


对于 Gradle 4.5 或更早的版本,依赖关系应该在 compileOnly 配置中声明。如下例所示。


dependencies {
    compileOnly "org.springframework:spring-context-indexer:5.2.6.RELEASE"
}


对于 Gradle 4.6 或以后的版本,依赖应该被定义在 annotationProcessor 配置中。如下例所示。


dependencies {
    annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
}


编译处理过程将生成 META-INF/spring.components 文件到 jar 包中。


在 IDE 中使用这种模式时,必须将 spring-context-indexer 注册为注解处理器以确保当候选组件被更新的时候索引是最新的。


当 META-INF/spring.components 在类路径下时索引自动启用。如果索引在一些库(或用例)有效,但不能为整个应用构建索引,可以通过设置 spring.index.ignore 为 true 来回退到常规的类路径处理,参数可以是系统属性或在根路径的 spring.properties 文件中。


@Indexed 如何提高 Spring 启动性能


根据上面官网的介绍我们知道 spring-context-indexer 可以生成 META-INF/spring.components 文件到类路径,那么文件中到底保存了哪些信息?文件中的的所有内容都会被用到吗?Spring 又是如何读取文件的?


Indexed 注解的定义


Indexed 源码如下。


@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Indexed {
}


可以看到,@Indexed 注解只能标注在类型上。@Indexed 标注的注解如果标注或元标注在类上,则这个类将成为候选的对象。


META-INF/spring.components 如何生成?


spring-context-indexer 定义了一个注解处理器(参见前面的文章 Java 注解处理器及其应用 ),因此只需要跟踪注解处理的逻辑即可了解其内部的实现,确定写入的文件内容。事实上 spring-context-indexer 模块中的类也确实没有几个,其项目结构如下图所示。


image.png


CandidateComponentsIndexer 就是注解处理器,其核心源码如下所示。


public class CandidateComponentsIndexer implements Processor {
  ...省略部分代码
  @Override
  public synchronized void init(ProcessingEnvironment env) {
    this.stereotypesProviders = getStereotypesProviders(env);
    this.typeHelper = new TypeHelper(env);
    this.metadataStore = new MetadataStore(env);
    this.metadataCollector = new MetadataCollector(env, this.metadataStore.readMetadata());
  }
  // 处理注解
  @Override
  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    // 标记元素已被处理
    this.metadataCollector.processing(roundEnv);
    // 处理元素
    roundEnv.getRootElements().forEach(this::processElement);
    if (roundEnv.processingOver()) {
      // 最后一轮处理写文件
      writeMetaData();
    }
    return false;
  }
  ...省略部分代码
  private void processElement(Element element) {
    addMetadataFor(element);
    staticTypesIn(element.getEnclosedElements()).forEach(this::processElement);
  }
  private void addMetadataFor(Element element) {
    Set<String> stereotypes = new LinkedHashSet<>();
    // 分别收集不同的元数据
    this.stereotypesProviders.forEach(p -> stereotypes.addAll(p.getStereotypes(element)));
    if (!stereotypes.isEmpty()) {
      // 元数据汇总,ItemMetadata 包含元素的类型和模式信息
      this.metadataCollector.add(new ItemMetadata(this.typeHelper.getType(element), stereotypes));
    }
  }
  ...省略部分代码
}


CandidateComponentsIndexer 一轮一轮的处理元素,到最后一轮时将获取到的元数据写入到文件中。StereotypesProvider 是元数据的收集器,用来收集元素中的模式信息,模式信息可能是注解等类型,也可能是其他的信息。收集到的每一项元数据保存到 ItemMetadata,包含类型和类型对应的模式列表。最终类型作为 key,模式列表以英文逗号分隔作为 value,以 properties 的形式保存到文件。StereotypesProvider 定义如下。


interface StereotypesProvider {
  /**
   * 返回给定元素的模式信息
   */
  Set<String> getStereotypes(Element element);
}


StereotypesProvider 有三个实现,具体如下。


IndexedStereotypesProvider: 用来处理 @Indexed 注解,事实上也只有这个类才处理 @Indexed 注解。其获取到的模式信息如下。

@Indexed 注解直接标注的类型完全限定名,包括父类和接口。


标注或元标注在类上的并且被 @Indexed 元标注的注解类型的完全限定名。

StandardStereotypesProvider:收集的模式信息为类上直接标注或通过 @Inherited 可以获取到的 Java 中包名以 javax 开头的注解的完全限定名。


PackageInfoStereotypesProvider:注解如果标注在 package-info.java 文件中的包名上,则收集的模式信息为 package-info 。

示例如下:


@Indexed
@Service
public interface Inter1 {
}
@Named
public interface Inter2 {
}
@Indexed
public class Parent {
}
@Component
@Indexed
public class Child extends Parent implements Inter1,Inter2 {
}
// package-info.java
@PackageAnnotation
package com.zzhkp;


编译后获取到的文件内容如下。


com.zzhkp=package-info
com.zzhkp.Child=org.springframework.stereotype.Component,com.zzhkp.Child,com.zzhkp.Parent,com.zzhkp.Inter1
com.zzhkp.Inter1=org.springframework.stereotype.Component,com.zzhkp.Inter1
com.zzhkp.Inter2=javax.inject.Named
com.zzhkp.Parent=com.zzhkp.Parent


可见 StereotypesProvider 已经正确收集了所需的信息,这些信息保存在文件,将作为索引,在 Spring 应用上下文启动时进行读取。


META-INF/spring.components 如何读取?

以 AnnotationConfigApplicationContext 应用上下文为例,其扫描 bean 的过程需要保证在刷新之前,其扫描的核心源码如下。


public class AnnotationConfigApplicationContext extends GenericApplicationContext implements AnnotationConfigRegistry {
  private final AnnotatedBeanDefinitionReader reader;
  //类路径下的 BeanDefinition 扫描器
  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);
  }
}


可以看到 AnnotationConfigApplicationContext 将扫描的功能委托给 ClassPathBeanDefinitionScanner 进行实现,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);
    // 加载 META-INF/spring.components 文件
    setResourceLoader(resourceLoader);
  }


registerDefaultFilters() 方法用来注册默认的过滤器,满足过滤器要求的类型才会被作为 bean,其源码如下。

  protected void registerDefaultFilters() {
    this.includeFilters.add(new AnnotationTypeFilter(Component.class));
    ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
    try {
      this.includeFilters.add(new AnnotationTypeFilter(
          ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
      logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
    } catch (ClassNotFoundException ex) {
      // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
    }
    try {
      this.includeFilters.add(new AnnotationTypeFilter(
          ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
      logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
    } catch (ClassNotFoundException ex) {
      // JSR-330 API not available - simply skip.
    }
  }


默认的过滤器是 AnnotationTypeFilter,用来匹配类上存在的注解或元注解。这里支持的注解是被 @Indexed 标注的 @Component 注解以及 javax 包下的 @ManagedBean、@Named 注解。由此可见,并不是所有 spring.components 文件内的类型都会被作为 bean 处理。


spring.components 文件的读取在 ClassPathBeanDefinitionScanner 的父类ClassPathScanningCandidateComponentProvider 的 setResourceLoader 方法中,源码如下。


  @Nullable
  private CandidateComponentsIndex componentsIndex;
  @Override
  public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
    this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
    this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
    this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(this.resourcePatternResolver.getClassLoader());
  }


可以看到读取文件最终是由CandidateComponentsIndexLoader.loadIndex完成,并将读取结果保存到 CandidateComponentsIndex 中。读取的逻辑也较为简单,判断类路径 spring.properties 文件内或系统参数 spring.index.ignore 是否设置为true,如果是则跳过读取,否则使用 ClassLoader 获取类路径下所有的 META-INF/spring.components 文件(参见前面的文章 Java 中如何获取 classpath 下资源文件? ),并加载为 Properties,然后保存到 CandidateComponentsIndex 。


那么读取到的文件内容都会被使用吗?事实并非如此。ClassPathBeanDefinitionScanner 获取候选组件的工作交给父类来完成,源码如下。


  public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    if (this.componentsIndex != null && indexSupportsIncludeFilters()) {
      // 从索引中添加候选组件
      return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    } else {
      // 从类路径下扫描候选组件
      return scanCandidateComponents(basePackage);
    }
  }


这个方法是查找候选组件的核心方法,这里加了判断,如果存在 CandidateComponentsIndex 并且 存在支持 @Indexed 的过滤器,则会从 spring.components 读取 bean,否则还是需要从类路径下进行组件扫描。从索引中添加候选组件的源码如下。


  private Set<BeanDefinition> addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) {
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
      Set<String> types = new HashSet<>();
      for (TypeFilter filter : this.includeFilters) {
        // 获取过滤器支持的模式
        String stereotype = extractStereotype(filter);
        if (stereotype == null) {
          throw new IllegalArgumentException("Failed to extract stereotype from " + filter);
        }
        // 根据包名和模式查找对应的类型
        types.addAll(index.getCandidateTypes(basePackage, stereotype));
      }
      boolean traceEnabled = logger.isTraceEnabled();
      boolean debugEnabled = logger.isDebugEnabled();
      for (String type : types) {
        // 读取类型信息
        MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(type);
        // 判断类型是否为候选组件
        if (isCandidateComponent(metadataReader)) {
          ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
          sbd.setSource(metadataReader.getResource());
          if (isCandidateComponent(sbd)) {
            if (debugEnabled) {
              logger.debug("Using candidate component class from index: " + type);
            }
            candidates.add(sbd);
          } else {
            if (debugEnabled) {
              logger.debug("Ignored because not a concrete top-level class: " + type);
            }
          }
        } else {
          if (traceEnabled) {
            logger.trace("Ignored because matching an exclude filter: " + type);
          }
        }
      }
    } catch (IOException ex) {
      throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    return candidates;
  }


includeFilters 是指满足条件的过滤器,满足条件才会被添加。上述 registerDefaultFilters() 方法注册了支持注解 @Component、@ManagedBean、@Named 的注解类型过滤器。 这里先获取过滤器支持的模式,然后根据所需的包名和模式从前面保存模式信息的 CandidateComponentsIndex 对象中获取模式对应的类型。然后判断类型是否为候选的组件,如果是则创建对应的 BeanDefinition 并添加到返回的列表中。至此 META-INF/spring.components 文件内满足条件的 bean 被转换为 BeanDefinition 。


@Indexed 使用注意事项


通过上面的描述可以知道,我们自定义的注解如果标注了 @Component 也会被作为 bean ,因为 @Component 上面标注了 @Indexed 注解。


@Indexed 并非可以任意使用。在没有其他模块依赖或者所依赖的模块都生成了 spring.components 文件时不会存在问题,然而如果依赖的模块只有部分模块存在 spring.components 文件,则其他模块的 bean 也不会被扫描,为避免这种问题,需要在类路径下 spring.properties 文件中或系统属性中的 spring.index.ignore 参数设置为 true,这样就会跳过 spring.components 文件的扫描,而转为重新扫描类路径下的 bean 。


总结

本篇首先通过介绍 @Indexed 实现启动性能优化的原理,然后从源码层面对索引文件的生成和读取进行了剖析,最后还介绍了 @Indexed 的注意事项。其中使用到的技术包括类路径下资源获取、注解处理器等,事实上 Spring 内部的实现都是依赖 Java 最基础的技术,只有提高最核心的能力,阅读 Spring 源码才能够得心应手。最后欢迎大家指导,共同进步。


目录
相关文章
|
IDE 网络协议 Java
2021最新 IDEA 启动失败 & 启动Spring boot 项目端口被占用问题 彻底解决方案
2021最新 IDEA 启动失败 & 启动Spring boot 项目端口被占用问题 彻底解决方案
833 0
2021最新 IDEA 启动失败 & 启动Spring boot 项目端口被占用问题 彻底解决方案
|
JSON SpringCloudAlibaba 负载均衡
【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
855 0
【微服务35】分布式事务Seata源码解析三:从Spring Boot特性来看Seata Client 启动时都做了什么
|
存储 前端开发 Java
【Spring MVC 系列】接口性能优化,还可以试试异步处理
背景 HTTP 作为一种无状态的协议采用的是请求-应答的模式,每当客户端发起的请求到达服务器,Servlet 容器通常会为每个请求使用一个线程来处理。为了避免线程创建和销毁的资源消耗,一般会采用线程池,而线程池中的线程数量是有限的,当线程池中的线程被全部使用,客户端只能等待有空闲线程处理请求。
443 0
【Spring MVC 系列】接口性能优化,还可以试试异步处理
|
Java Maven Spring
Elastic实战:项目中已经剔除了spring data elasticsearch依赖,但启动项目仍然会进行es健康检查
在实际开发中遇到一个问题:原本在springboot项目中引入了spring data elasticsearch的依赖,后因调整将这个依赖从这个服务中删除了,但是启动服务仍然会进行es的健康检查。也就导致一直有警告日志输出:connection refuse
223 0
Elastic实战:项目中已经剔除了spring data elasticsearch依赖,但启动项目仍然会进行es健康检查
|
Java Spring 容器
Spring Boot 启动时自动执行代码的几种方式。。
Spring Boot 启动时自动执行代码的几种方式。。
535 0
Spring Boot 启动时自动执行代码的几种方式。。
|
安全 IDE Java
一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式
一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式
一张图帮你记忆,Spring Boot 应用在启动阶段执行代码的几种方式
|
Java 索引 Spring
spring data elasticsearch:启动项目时自动创建索引
在springboot整合spring data elasticsearch项目中,当索引数量较多,mapping结构较为复杂时,我们常常希望启动项目时能够自动创建索引及mapping,这样就不用再到各个环境中创建索引了 所以今天咱们就来看看如何自动创建索引
1552 0
|
XML 前端开发 Java
03启动spring
1.配置Servlet初始化参数contextConfigLocation,FrameworkServlet获取设置的参数值 2.启动spring生命周期,并注册回调监听
152 0
|
Java Spring 容器
简析SpringBoot启动执行流程
简析一下SpringBoot的启动流程
简析SpringBoot启动执行流程