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() 方法释放资源,否则可能导致内存泄漏(尤其是在频繁扫描的场景中)。

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
    }
}