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 进行扫描,这种做法就不适用了。不过在大多数情况下,这种做法带来的启动速度的提升是显著的。