1. 概述
有时,我们想要获取有关应用程序运行时行为的信息,例如查找运行时可用的所有类。
在本教程中,我们将探讨如何在运行时查找 Java 包中的所有类的几个示例。
2. 类加载器
首先,我们将从Java 类加载器开始讨论 。 Java 类加载器是 Java 运行时环境 (JRE) 的一部分,它将 Java 类动态加载到 Java 虚拟机 (JVM) 中。 Java 类加载器将 JRE 与文件和文件系统的了解分离。 并非所有类都由单个类加载器加载 。
我们通过图解来了解一下Java中可用的类加载器:
Java 9 对类加载器进行了一些重大更改。随着模块的引入,我们可以选择在类路径旁边提供模块路径。系统类加载器加载模块路径上存在的类。
类加载器是动态的 。它们不需要告诉 JVM 它可以在运行时提供哪些类。因此,在包中查找类本质上是一种文件系统操作,而不是使用Java Reflection完成的操作。
但是,我们可以编写自己的类加载器或检查类路径以查找包内的类。
3. 查找Java包中的类
为了便于说明,我们创建一个包com.baeldung.reflection.access.packages.search
。
现在,让我们定义一个示例类:
public class ClassExample {
class NestedClass {
}
}
接下来,我们定义一个接口:
public interface InterfaceExample {
}
在下一节中,我们将了解如何使用系统类加载器和一些第三方库查找类。
3.1.系统类加载器
首先,我们将使用内置的系统类加载器*。* 系统类加载器 加载在 classpath 中找到的所有类 。这发生在 JVM 的早期初始化期间:
public class AccessingAllClassesInPackage {
public Set<Class> findAllClassesUsingClassLoader(String packageName) {
InputStream stream = ClassLoader.getSystemClassLoader()
.getResourceAsStream(packageName.replaceAll("[.]", "/"));
BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
return reader.lines()
.filter(line -> line.endsWith(".class"))
.map(line -> getClass(line, packageName))
.collect(Collectors.toSet());
}
private Class getClass(String className, String packageName) {
try {
return Class.forName(packageName + "."
+ className.substring(0, className.lastIndexOf('.')));
} catch (ClassNotFoundException e) {
// handle the exception
}
return null;
}
}
在上面的示例中,我们使用静态 getSystemClassLoader() 方法加载系统类加载器。
接下来,我们将在给定的包中查找资源。我们将使用 getResourceAsStream 方法以 URL 流的形式读取资源。为了获取包下的资源,我们需要将包名转换为URL字符串。因此,我们必须用路径分隔符(“/”)替换所有点(.)。
之后,我们将把流输入到 BufferedReader 并过滤所有带有 .class 扩展名的 URL。获得所需的资源后,我们将构造该类并将所有结果收集到一个 Set 中。 由于Java不允许lambda抛出异常,所以我们必须在 getClass 方法中处理它 。
现在让我们测试一下这个方法:
@Test
public void when_findAllClassesUsingClassLoader_thenSuccess() {
AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
Set<Class> classes = instance.findAllClassesUsingClassLoader(
"com.baeldung.reflection.access.packages.search");
Assertions.assertEquals(3, classes.size());
}
包中只有两个 Java 文件。但是,我们声明了三个类 - 包括嵌套类 NestedExample 。结果,我们的测试结果是三个班级。
请注意,搜索包与当前工作包不同。
3.2.思考库
Reflections是一个流行的库,它扫描当前的类路径并允许我们在运行时查询它。
让我们首先将 反射 依赖项添加到我们的 Maven 项目中:
<dependency>
<groupId>org.reflections</groupId>
<artifactId>reflections</artifactId>
<version>0.9.12</version>
</dependency>
现在,让我们深入研究代码示例:
public Set<Class> findAllClassesUsingReflectionsLibrary(String packageName) {
Reflections reflections = new Reflections(packageName, new SubTypesScanner(false));
return reflections.getSubTypesOf(Object.class)
.stream()
.collect(Collectors.toSet());
}
在此方法中,我们启动 SubTypesScanner 类并获取 Object 类的所有子类型。通过这种方法,我们在获取类时可以获得更多的粒度。
再次,让我们测试一下:
@Test
public void when_findAllClassesUsingReflectionsLibrary_thenSuccess() {
AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
Set<Class> classes = instance.findAllClassesUsingReflectionsLibrary(
"com.baeldung.reflection.access.packages.search");
Assertions.assertEquals(3, classes.size());
}
与我们之前的测试类似,此测试查找给定包中声明的类。
现在,让我们继续讨论下一个示例。
3.3.谷歌番石榴库
在本节中,我们将了解如何使用 Google Guava 库查找类。 Google Guava 提供了一个 ClassPath 实用程序类,它扫描类加载器的源并查找所有可加载的类和资源。
首先,让我们将 guava 依赖添加到我们的项目中:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>30.1.1-jre</version>
</dependency>
让我们深入研究代码:
public Set<Class> findAllClassesUsingGoogleGuice(String packageName) throws IOException {
return ClassPath.from(ClassLoader.getSystemClassLoader())
.getAllClasses()
.stream()
.filter(clazz -> clazz.getPackageName()
.equalsIgnoreCase(packageName))
.map(clazz -> clazz.load())
.collect(Collectors.toSet());
}
在上面的方法中,我们提供系统类加载器作为 ClassPath#from 方法的输入。 ClassPath 扫描到的所有类都会根据包名进行过滤。然后加载过滤后的类(但不链接或初始化)并收集到 Set 中。
现在让我们测试一下这个方法:
@Test
public void when_findAllClassesUsingGoogleGuice_thenSuccess() throws IOException {
AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
Set<Class> classes = instance.findAllClassesUsingGoogleGuice(
"com.baeldung.reflection.access.packages.search");
Assertions.assertEquals(3, classes.size());
}
此外,Google Guava 库提供了 getTopLevelClasses() 和 getTopLevelClassesRecursive() 方法*.*
需要注意的是,在上述所有示例中, 如果 package-info 存在于包下并使用一个或多个包级注释进行注释,则 package-info 包含在可用类列表中 。
下一节将讨论如何在模块化应用程序中查找类。
4. 在模块化应用程序中查找类
Java 平台模块系统 (JPMS) 通过模块向我们介绍了新级别的访问控制 。每个包都必须显式导出才能在模块外部访问。
在模块化应用程序中,每个模块可以是命名模块、未命名模块或自动模块之一。
对于命名模块和自动模块,内置系统类加载器将没有类路径。系统类加载器将使用应用程序模块路径搜索类和资源。
对于未命名的模块,它将把类路径设置为当前工作目录。
4.1.模块内
模块中的所有包对该模块中的其他包都具有可见性。模块内的代码可以对所有类型及其所有成员进行反射访问。
4.2.模块外部
由于 Java 强制执行最严格的访问限制,因此我们必须使用 导出 或 开放 模块声明显式声明包,以获得对模块内类的反射访问。
对于普通模块,导出包(但不是开放包)的反射访问仅提供对 公共 和 受保护 类型及其声明包的所有成员的访问。
我们可以构造一个模块来导出需要搜索的包:
module my.module {
exports com.baeldung.reflection.access.packages.search;
}
对于普通模块,开放包的反射访问提供对声明包的所有类型及其成员的访问:
module my.module {
opens com.baeldung.reflection.access.packages.search;
}
同样,开放模块授予对所有类型及其成员的反射访问,就好像所有包都已打开一样。现在让我们打开整个模块进行反射访问:
open module my.module{
}
最后,在确保为模块提供了用于访问包的正确模块化描述后,可以使用上一节中的任何方法来查找包内的所有可用类。
5. 结论
总之,我们了解了类加载器以及查找包中所有类的不同方法。此外,我们还讨论了在模块化应用程序中访问包。
与往常一样,所有代码都可以 在 GitHub 上获取。