天天看点

Java 应用使用 Reflections 库时提升启动性能

#头条创作挑战赛#

Reflections 是 Java 应用中进行反射操作时的常用库。比如,如果想要找到所有标注了 @Tag 注解的方法,可以用 Reflections 提供的 getMethodsAnnotatedWith 方法,如下面的代码所示。

public class TagLoader {

  Set<String> findTags() {
    var reflections = new Reflections(
        new ConfigurationBuilder()
            .forPackage("io.vividcode.reflections")
            .addScanners(Scanners.MethodsAnnotated));
    return reflections.getMethodsAnnotatedWith(Tag.class).stream()
        .map(method -> method.getAnnotation(
            Tag.class).value()).collect(Collectors.toSet());
  }
}           

Reflections 默认是在应用启动时对 classpath 进行扫描。在启动时,可以看到如下所示的日志,会输出 Reflections 扫描所花费的时间。

22:20:13.554 [main] INFO org.reflections.Reflections - Reflections took 75 ms to scan 1 urls, producing 1 keys and 2 values           

如果应用的classpath 上的类很多,那么 Reflections 扫描所花费的时间会很长,减慢应用的启动时间。

如果希望提升 Reflections 扫描的速度,可以把扫描工作移到应用构建时来完成。运行时直接读取扫描的结果即可。Reflections 可以把扫描结果保存为 XML 文件。我们只需要在扫描结果保存为 XML 文件,作为 JAR 的一部分。在运行时,Reflections 可以直接读取 XML 文件的内容。

在构建时,可以直接使用 Maven 的 Groovy 插件来运行 Reflections 进行扫描,并使用 save 方法保存结果。生成的 XML 文件保存在 META-INF/reflections 路径下,是 JAR 文件的一部分。

<plugin>
  <groupId>org.codehaus.gmavenplus</groupId>
  <artifactId>gmavenplus-plugin</artifactId>
  <version>2.1.0</version>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>execute</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <scripts>
      <script><![CDATA[
      import org.reflections.Reflections;
      import org.reflections.scanners.Scanners;
      import org.reflections.util.ConfigurationBuilder;

      new Reflections(
        new ConfigurationBuilder()
            .forPackage("io.vividcode.reflections")
            .addScanners(Scanners.MethodsAnnotated))
        .save("${project.build.outputDirectory}/META-INF/reflections/${project.artifactId}-reflections.xml")
    ]]></script>
    </scripts>
  </configuration>
  <dependencies>
    <dependency>
      <groupId>org.apache.groovy</groupId>
      <artifactId>groovy</artifactId>
      <version>4.0.6</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
</plugin>           

Reflections 生成的 XML 文件的内容如下所示。

<?xml version="1.0" encoding="UTF-8"?>

<Reflections>
  <MethodsAnnotated>
    <entry>
      <key>io.vividcode.reflections.Tag</key>
      <values>
        <value>io.vividcode.reflections.Tagged.someMethod()</value>
        <value>io.vividcode.reflections.Tagged.anotherMethod()</value>
      </values>
    </entry>
  </MethodsAnnotated>
</Reflections>           

生成了 XML 文件之后,在运行时,只需要读取该文件即可。Reflections.collect() 方法可以自动扫描 classpath 上的 XML 文件并读取。

修改之后的代码如下所示。首先通过 Reflections.collect() 来加载 XML 文件,再通过 reflections.getStore().isEmpty() 来检查是否从 XML 文件中加载了内容。如果没有的话,再进行运行时扫描。这样做的好处是,如果由于构建原因导致 XML 文件没有生成,代码仍然可以正常工作。

var reflections = Reflections.collect();
if (reflections.getStore().isEmpty()) {
  System.out.println("运行时扫描");
  reflections = new Reflections(
      new ConfigurationBuilder()
          .forPackage("io.vividcode.reflections")
          .addScanners(Scanners.MethodsAnnotated));
}
return reflections.getMethodsAnnotatedWith(Tag.class).stream()
    .map(method -> method.getAnnotation(
        Tag.class).value()).collect(Collectors.toSet());           

经过修改之后,再启动应用,就不会看到 Reflections 打印出的扫描日志了。应用的启动时间也被缩短了。

这种做法也有一定的局限性,要求在构建时的 classpath 与运行时的 classpath 不存在很大差别。如果在运行时会修改 classpath​ 再通过 Reflections 进行扫描,这种做法就不适用了。不过在大多数情况下,这种做法带来的启动速度的提升是显著的。

继续阅读