classgraph:Java轻量级类和包扫描器
文章目录
一、写在前面
开源地址:https://github.com/classgraph/classgraph
官方文档:https://github.com/classgraph/classgraph/wiki/Code-examples
参考文档:https://www.baeldung.com/classgraph
注意!ScanResult 实现了 AutoCloseable 接口,必须使用 try-with-resources 语法或手动调用 close() 方法释放资源,否则可能导致内存泄漏(尤其是在频繁扫描的场景中)。
// 正确用法
try (ScanResult result = new ClassGraph().scan()) {// 使用 result
}
注意!避免无限制扫描整个类路径(默认行为),这会导致扫描速度慢且消耗大量内存。
<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) 可以控制不初始化)。
try (ScanResult result = new ClassGraph().enableClassInfo().scan()) {ClassInfo classInfo = result.getClassInfo("com.example.MyClass");// 此时类未加载,更未初始化Class<?> clazz = classInfo.loadClass(); // 加载类(但不一定初始化)// 以下操作会触发类初始化Object instance = clazz.newInstance(); // 创建实例// 或访问静态字段/方法
}
二、使用
1、ClassGraph配置参数
import io.github.classgraph.*;public class Test {public static void main(String[] args) throws Exception {/*** 1、启动配置+ 扫描*/try (ScanResult scanResult = // scanResult 必须使用 try-with-resourcesnew ClassGraph() // 创建 ClassGraph 实例//.verbose() // 打印日志(如果你想的话).enableAllInfo() // 扫描 classes, methods, fields, annotations.acceptPackages("com.demo") // 扫描的包.scan()) { // 开始扫描,返回 ScanResult// 获取指定类信息ClassInfo widgetClassInfo = scanResult.getClassInfo("com.demo.springbootdemo.TestController");// ...}}
}
2、查找指定注解的类
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、扫描接口、父类的子类
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、查找类的方法、注解、字段
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、使用过滤器+并交集
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)
中读取的:
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:
public void scan(URL[] urls) {try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz").overrideClassLoaders(new URLClassLoader(urls)).scan()) {// ...}
}
或者
public void scan(String pathToScan) {try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("com.xyz").overrideClasspath(pathToScan).scan()) {// ...}
}
8、查找和读取资源文件
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:
for (Entry<String, ResourceList> dup :new ClassGraph().scan().getAllResources().classFilesOnly() // Remove this for all resource types.findDuplicatePaths()) {System.out.println(dup.getKey()); // Classfile pathfor (Resource res : dup.getValue()) {System.out.println(" -> " + res.getURI()); // Print Resource URI}
}