文章目录
- 一、写在前面
- 二、使用
-
- 1、ClassGraph配置参数
- 2、查找指定注解的类
- 3、扫描接口、父类的子类
- 4、查找类的方法、注解、字段
- 5、使用过滤器+并交集
- 6、读取类型注解
- [7、扫描特定 URL](#7、扫描特定 URL)
- 8、查找和读取资源文件
- 9、查找类路径或模块路径中的所有重复类定义
一、写在前面
开源地址:https://github.com/classgraph/classgraph
官方文档:https://github.com/classgraph/classgraph/wiki/Code-examples
参考文档:https://www.baeldung.com/classgraph
注意!ScanResult 实现了 AutoCloseable 接口,必须使用 try-with-resources 语法或手动调用 close() 方法释放资源,否则可能导致内存泄漏(尤其是在频繁扫描的场景中)。
java
// 正确用法
try (ScanResult result = new ClassGraph().scan()) {
// 使用 result
}
注意!避免无限制扫描整个类路径(默认行为),这会导致扫描速度慢且消耗大量内存。
xml
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.8.181</version>
</dependency>
classgraph 的
扫描过程本身不会初始化类
,只有当你显式加载类并执行触发初始化的操作时,类才会被初始化。这一点与 Java 反射中 "Class.forName() 可能触发初始化" 的行为不同(Class.forName(String) 会初始化类,而 Class.forName(String, false, ClassLoader) 可以控制不初始化)。
java
try (ScanResult result = new ClassGraph().enableClassInfo().scan()) {
ClassInfo classInfo = result.getClassInfo("com.example.MyClass");
// 此时类未加载,更未初始化
Class<?> clazz = classInfo.loadClass(); // 加载类(但不一定初始化)
// 以下操作会触发类初始化
Object instance = clazz.newInstance(); // 创建实例
// 或访问静态字段/方法
}
二、使用
1、ClassGraph配置参数
java
import io.github.classgraph.*;
public class Test {
public static void main(String[] args) throws Exception {
/**
* 1、启动配置+ 扫描
*/
try (ScanResult scanResult = // scanResult 必须使用 try-with-resources
new ClassGraph() // 创建 ClassGraph 实例
//.verbose() // 打印日志(如果你想的话)
.enableAllInfo() // 扫描 classes, methods, fields, annotations
.acceptPackages("com.demo") // 扫描的包
.scan()) { // 开始扫描,返回 ScanResult
// 获取指定类信息
ClassInfo widgetClassInfo = scanResult.getClassInfo("com.demo.springbootdemo.TestController");
// ...
}
}
}
2、查找指定注解的类
java
try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
.scan()) {
ClassInfoList routeClassInfoList = scanResult.getClassesWithAnnotation("com.xyz.Route");
for (ClassInfo routeClassInfo : routeClassInfoList) {
// 获取注解
AnnotationInfo annotationInfo = routeClassInfo.getAnnotationInfo("com.xyz.Route");
AnnotationParameterValueList paramVals = annotationInfo.getParameterValues();
// Route注释有一个名为"path"的参数
String routePath = paramVals.get("path");
//或者,您可以加载并实例化注释,以便注释
//可以直接调用方法来获取注释参数值(这设置
//一个InvocationHandler,用于模拟Route注释实例,因为注释
//如果不加载带注释的类,就不能直接实例化)。
Route route = (Route) annotationInfo.loadClassAndInstantiate();
String routePathDirect = route.path();
// ...
// 1、扫描指定了注解的类
ClassInfoList classInfos = scanResult.getClassesWithAnnotation(TestAnnotation.class.getName());
// getClassesWithMethodAnnotations() --- 来查找所有被目标注解标记了方法的所有类
ClassInfoList classInfos2 = scanResult.getClassesWithMethodAnnotation(TestAnnotation.class.getName());
// 过滤,TestAnnotation注解的value值为web的
ClassInfoList classInfos3 = scanResult.getClassesWithMethodAnnotation(TestAnnotation.class.getName());
ClassInfoList webClassInfos = classInfos3.filter(classInfo -> {
return classInfo.getMethodInfo().stream().anyMatch(methodInfo -> {
AnnotationInfo annotationInfo = methodInfo.getAnnotationInfo(TestAnnotation.class.getName());
if (annotationInfo == null) {
return false;
}
return "web".equals(annotationInfo.getParameterValues().getValue("value"));
});
});
// 查找所有元注解
/**
* 元注解用于注解注解。对于注解类 ClassInfo 的注解 ci ,可以通过调用 ci.getClassesWithAnnotation() 找到它注解的类,
* 返回一个 ClassInfoList 。然后可以通过调用 .getAnnotations() 对该列表进行过滤,仅保留注解类,
* 返回由 ci 注解且本身是注解的类列表。检查该列表是否为空可以测试 ci 是否为元注解:
*/
ClassInfoList metaAnnotations = scanResult.getAllAnnotations()
.filter(ci -> !ci.getClassesWithAnnotation().getAnnotations().isEmpty());
// 使用`getClassesWithFieldAnnotation()`方法根据字段注解来过滤`ClassInfoList`结果
// 查找字段上有TestAnnotation 注解的类
ClassInfoList classInfos4 = scanResult.getClassesWithFieldAnnotation(TestAnnotation.class.getName());
}
}
3、扫描接口、父类的子类
java
try (ScanResult scanResult = new ClassGraph().enableAllInfo()
.whitelistPackages(Test.class.getPackage().getName()).scan()) {
// 获取所有实现了某接口的类
ClassInfoList widgetClasses = scanResult.getClassesImplementing("com.xyz.Widget");
// 获取指定超类所有的子类
/**
* 注意!!!加载的时候一定要用loadClasses方法加载类,而不是Class.forName(className)!!!
*/
ClassInfoList controlClasses = scanResult.getSubclasses("com.xyz.Control");
List<Class<?>> controlClassRefs = controlClasses.loadClasses();
// 找直接子类,而不是子类的子类
ClassInfoList directBoxes = scanResult.getSubclasses("com.xyz.Box").directOnly();
}
4、查找类的方法、注解、字段
java
try (ScanResult scanResult = new ClassGraph().enableAllInfo()
.whitelistPackages(Test.class.getPackage().getName()).scan()) {
/**
* 查找类 com.xyz.Form 的方法、字段和注解
* 从一个 ClassInfo 对象中,你可以获取一个 MethodInfoList 的 MethodInfo 对象、一个 FieldInfoList 的 FieldInfo 对象,
* 以及/或者一个 AnnotationInfoList 的 AnnotationInfo 对象,它们分别提供关于类的方法、字段和注解的信息。
* 同样,这一切都是在不加载或初始化类的情况下完成的。
*
*/
ClassInfo form = scanResult.getClassInfo("com.xyz.Form");
if (form != null) {
MethodInfoList formMethods = form.getMethodInfo();
// 方法
for (MethodInfo mi : formMethods) {
String methodName = mi.getName();
MethodParameterInfo[] mpi = mi.getParameterInfo();
for (int i = 0; i < mpi.length; i++) {
String parameterName = mpi[i].getName();
TypeSignature parameterType =
mpi[i].getTypeSignatureOrTypeDescriptor();
// ...
}
}
// 字段
FieldInfoList formFields = form.getFieldInfo();
for (FieldInfo fi : formFields) {
String fieldName = fi.getName();
TypeSignature fieldType = fi.getTypeSignatureOrTypeDescriptor();
// ...
}
// 注解
AnnotationInfoList formAnnotations = form.getAnnotationInfo();
for (AnnotationInfo ai : formAnnotations) {
String annotationName = ai.getName();
List<AnnotationParameterValue> annotationParamVals =
ai.getParameterValues();
// ...
}
}
}
5、使用过滤器+并交集
java
try (ScanResult scanResult = new ClassGraph().enableAllInfo()
.whitelistPackages(Test.class.getPackage().getName()).scan()) {
/**
* 查找带注解 com.xyz.Checked 的 com.xyz.Box 的子类
* ClassInfoList 提供了并集("and")、交集("or")以及集合差集/排除("and-not")运算符:
*/
ClassInfoList boxes = scanResult.getSubclasses("com.xyz.Box");
ClassInfoList checked = scanResult.getClassesWithAnnotation("com.xyz.Checked");
ClassInfoList checkedBoxes = boxes.intersect(checked); // 交集
// 使用过滤条件同样可以实现,如果是交集的话
ClassInfoList checkedBoxes2 = scanResult.getSubclasses("com.xyz.Box")
.filter(classInfo -> classInfo.hasAnnotation("com.xyz.Checked"));
/**
* 使用复杂过滤条件
*/
ClassInfoList filtered = scanResult.getAllClasses()
.filter(classInfo ->
(classInfo.isInterface() || classInfo.isAbstract())
&& classInfo.hasAnnotation("com.xyz.Widget")
&& classInfo.hasMethod("open"));
// 请注意,某些 ClassInfo 谓词方法不接受参数,因此它们也可以直接作为函数引用来代替 ClassInfoFilter 使用,例如:
ClassInfoList interfaces = filtered.filter(ClassInfo::isInterface);
}
6、读取类型注解
在 Java 中,可以在类型上添加注解(可选带参数)。以下示例打印 100 ,该值是从字段 List<@Size(100) String> values
上的类型参数注解 @Size(100)
中读取的:
java
public class TypeAnnotation {
@Retention(RetentionPolicy.RUNTIME)
public @interface Size {
int value();
}
public class Test {
List<@Size(100) String> values;
}
public static void main(String[] args) {
try (ScanResult scanResult = new ClassGraph()
.acceptPackages(TypeAnnotation.class.getPackage().getName())
.enableAllInfo()
.scan()) {
ClassInfo ci = scanResult.getClassInfo(Test.class.getName());
FieldInfo fi = ci.getFieldInfo().get(0);
ClassRefTypeSignature ts = (ClassRefTypeSignature) fi.getTypeSignature();
List<TypeArgument> taList = ts.getTypeArguments();
TypeArgument ta = taList.get(0);
ReferenceTypeSignature taSig = ta.getTypeSignature();
AnnotationInfoList aiList = taSig.getTypeAnnotationInfo();
AnnotationInfo ai = aiList.get(0);
AnnotationParameterValueList apVals = ai.getParameterValues();
AnnotationParameterValue apVal = apVals.get(0);
int size = (int) apVal.getValue();
System.out.println(size);
}
}
}
7、扫描特定 URL
与其扫描所有检测到的类加载器和模块,您可以通过在 .overrideClassLoaders(new URLClassLoader(urls))
或直接在 .overrideClasspath(urls)
之前调用 .scan()
来扫描特定的 URL:
java
public void scan(URL[] urls) {
try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
.overrideClassLoaders(new URLClassLoader(urls))
.scan()) {
// ...
}
}
或者
java
public void scan(String pathToScan) {
try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz")
.overrideClasspath(pathToScan)
.scan()) {
// ...
}
}
8、查找和读取资源文件
java
import io.github.classgraph.ClassGraph;
import io.github.classgraph.Resource;
import io.github.classgraph.ScanResult;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
public class Test {
public static void main(String[] args) throws Exception {
/**
* 读取所有 XML 资源文件的内容,位于 META-INF/config 。
* 这是一种不同类型的查询,它根据匹配的文件路径查找资源,而不是根据类属性查找类。
* 如果你只需要扫描资源而不需要扫描类,为了提高速度,不应调用 .enableClassInfo() 或 .enableAllInfo() 。
* 此外,如果你不需要扫描类,应通过调用 .acceptPaths() 并使用路径分隔符( / )来指定接受,而不是通过调用 .acceptPackages() 并使用包分隔符( . )来指定。
* 路径和包接受在内部工作方式相同,你可以选择其中一种方式来指定接受/拒绝。
* 然而,调用 .acceptPackages() 也会隐式调用 .enableClassInfo() 。
*
*
* ScanResult 中有几种方法可以获取符合给定条件的资源:
* .getAllResources()
* .getResourcesWithPath(String resourcePath)
* .getResourcesWithLeafName(String leafName)
* .getResourcesWithExtension(String extension)
* .getResourcesMatchingPattern(Pattern pattern)
*/
Map<String, String> pathToFileContent = new HashMap<>();
try (ScanResult scanResult = new ClassGraph().acceptPaths("META-INF/config").scan()) {
scanResult.getResourcesWithExtension("xml")
.forEachByteArray((Resource res, byte[] fileContent) -> {
pathToFileContent.put(
res.getPath(), new String(fileContent, StandardCharsets.UTF_8));
});
}
}
}
9、查找类路径或模块路径中的所有重复类定义
知道同一个类在类路径或模块路径中定义多次时可能很有用。
在 ScanResult
中,ClassGraph
仅对任何给定的完全限定类名返回一个 ClassInfo
对象,该对象对应于类路径或模块路径中遇到的第一个类实例(为了模拟 JRE 的"遮蔽"或"阴影"语义,同一类的后续定义会被忽略)。然而,ScanResult#getAllResources()
返回一个 ResourceList
,其中包含针对非类文件和类文件的 Resource 对象(因为类文件在技术上是一种资源)。
调用 ResourceList#classFilesOnly()
会返回另一个 ResourceList
,其中只包含路径以 ".class"
结尾的 Resource
元素。
调用 ResourceList#findDuplicatePaths()
会返回一个 List<Entry<String, ResourceList>>
,其中条目的键是路径,条目的值是一个 ResourceList
,包含两个或多个 Resource
对象,用于重复的资源。
因此,你可以按照以下方式打印所有重复的 class 文件的类路径/模块路径 URL:
java
for (Entry<String, ResourceList> dup :
new ClassGraph().scan()
.getAllResources()
.classFilesOnly() // Remove this for all resource types
.findDuplicatePaths()) {
System.out.println(dup.getKey()); // Classfile path
for (Resource res : dup.getValue()) {
System.out.println(" -> " + res.getURI()); // Print Resource URI
}
}