1. 概述

在本教程中,我们将介绍 Classgraph 这个库的作用及其使用方式。

一句话总结:Classgraph 是一个用于扫描 Java classpath 中资源的高性能库,它能构建资源的元数据,并提供友好的 API 供操作这些元数据。

这种能力在 Spring 系列框架中非常常见,比如自动扫描并注册带有特定注解的类到 Spring 容器中。但 Classgraph 的用途远不止于此。你可以用它来:

  • 查找所有带有某个注解的类
  • 查找特定名称的资源文件(如配置文件、JSON 文件等)
  • 获取类的方法、字段、注解等元信息

⚠️ 特别提醒:Classgraph 直接操作字节码,不会加载类到 JVM,也不依赖反射机制,因此性能非常优秀。

2. Maven 依赖

首先,我们需要在 pom.xml 中添加 Classgraph 的依赖:

<dependency>
    <groupId>io.github.classgraph</groupId>
    <artifactId>classgraph</artifactId>
    <version>4.8.153</version>
</dependency>

添加完成后,我们就可以开始使用其 API 实现各种扫描任务了。

3. 基本用法

使用 Classgraph 的基本流程分为三步:

  1. 配置扫描参数(如包路径)
  2. 执行扫描
  3. 处理扫描结果

我们先定义一个测试注解和一个使用该注解的类:

@Target({TYPE, METHOD, FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
    String value() default "";
}
@TestAnnotation
public class ClassWithAnnotation {
}

接下来,我们使用 Classgraph 扫描所有带有 @TestAnnotation 注解的类:

try (ScanResult result = new ClassGraph()
        .enableClassInfo()
        .enableAnnotationInfo()
        .whitelistPackages(getClass().getPackage().getName())
        .scan()) {

    ClassInfoList classInfos = result.getClassesWithAnnotation(TestAnnotation.class.getName());

    assertThat(classInfos)
        .extracting(ClassInfo::getName)
        .contains(ClassWithAnnotation.class.getName());
}

这段代码的逻辑很清晰:

  • 配置扫描器:启用类和注解信息,限定扫描包范围
  • 执行扫描:调用 scan() 方法
  • 获取结果:通过 getClassesWithAnnotation() 获取目标类列表

扫描结果 ScanResult 包含了丰富的信息,后续我们还会看到它支持的其他方法。

4. 按方法注解过滤

接下来我们看看如何查找带有特定方法注解的类。

先定义一个类:

public class MethodWithAnnotation {

    @TestAnnotation
    public void service() {
    }
}

然后使用 getClassesWithMethodAnnotation() 方法进行查找:

try (ScanResult result = new ClassGraph()
        .enableAllInfo()
        .whitelistPackages(getClass().getPackage().getName())
        .scan()) {

    ClassInfoList classInfos = result.getClassesWithMethodAnnotation(TestAnnotation.class.getName());

    assertThat(classInfos)
        .extracting(ClassInfo::getName)
        .contains(MethodWithAnnotation.class.getName());
}

这个方法返回的 ClassInfoList 包含了所有方法上带有目标注解的类。

5. 按注解参数过滤

我们还可以根据注解中的参数值进一步过滤目标类。

先定义两个类,分别使用不同参数值的 @TestAnnotation

public class MethodWithAnnotationParameterDao {

    @TestAnnotation("dao")
    public void service() {
    }
}
public class MethodWithAnnotationParameterWeb {

    @TestAnnotation("web")
    public void service() {
    }
}

然后在扫描结果中,通过 filter() 方法筛选出参数值为 "web" 的类:

try (ScanResult result = new ClassGraph()
        .enableAllInfo()
        .whitelistPackages(getClass().getPackage().getName())
        .scan()) {

    ClassInfoList classInfos = result.getClassesWithMethodAnnotation(TestAnnotation.class.getName());

    ClassInfoList webClassInfos = classInfos.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"));
        });
    });

    assertThat(webClassInfos)
        .extracting(ClassInfo::getName)
        .contains(MethodWithAnnotationParameterWeb.class.getName());
}

这里我们用到了 AnnotationInfoMethodInfo 来访问注解参数值,从而实现更精确的筛选。

6. 按字段注解过滤

Classgraph 也支持查找字段上带有指定注解的类。

先定义一个类:

public class FieldWithAnnotation {

    @TestAnnotation
    private String s;
}

然后使用 getClassesWithFieldAnnotation() 方法进行查找:

try (ScanResult result = new ClassGraph()
        .enableAllInfo()
        .whitelistPackages(getClass().getPackage().getName())
        .scan()) {

    ClassInfoList classInfos = result.getClassesWithFieldAnnotation(TestAnnotation.class.getName());

    assertThat(classInfos)
        .extracting(ClassInfo::getName)
        .contains(FieldWithAnnotation.class.getName());
}

非常简单直接,和方法注解类似,只是换了个 API。

7. 查找资源文件

除了扫描类,Classgraph 还能扫描 classpath 中的资源文件。

比如我们在 src/test/resources/classgraph/ 下创建一个配置文件:

my.config

文件内容为:

my data

然后使用如下代码查找并读取该文件内容:

try (ScanResult result = new ClassGraph()
        .whitelistPaths("classgraph")
        .scan()) {

    ResourceList resources = result.getResourcesWithExtension("config");

    assertThat(resources)
        .extracting(Resource::getPath)
        .containsOnly("classgraph/my.config");

    assertThat(resources.get(0).getContentAsString())
        .isEqualTo("my data");
}

这里的 getResourcesWithExtension() 方法可以按扩展名查找资源,除此之外还有:

  • getAllResources():获取所有资源
  • getResourcesWithPath():按路径查找
  • getResourcesMatchingPattern():按正则表达式查找

这些方法返回的是 ResourceList,我们可以从中获取 Resource 对象并操作其内容。

8. 实例化类

当我们需要实例化扫描到的类时,强烈建议使用 ClassInfo.loadClass() 方法,而不是 Class.forName()

原因很简单:Classgraph 使用自己的类加载器加载某些类(尤其是 JAR 中的类),如果使用 Class.forName(),可能会导致同一个类被不同类加载器重复加载,进而引发各种奇怪的问题。

✅ 正确做法:

MyClass clazz = (MyClass) classInfo.loadClass().newInstance();

❌ 踩坑写法:

MyClass clazz = (MyClass) Class.forName(classInfo.getName()).newInstance();

9. 总结

在本文中,我们学习了如何使用 Classgraph 高效扫描 Java classpath 中的类和资源文件,并获取它们的元信息。

✅ Classgraph 的主要优势包括:

  • 支持类、方法、字段、注解、资源的扫描
  • 不依赖反射,不加载类到 JVM,性能高
  • 提供丰富的 API,使用简单直观
  • 支持多种过滤方式,满足复杂业务需求

如果你正在做自动注册、插件系统、代码分析、注解处理等相关功能,Classgraph 是一个非常值得尝试的工具。


原始标题:Guide to Classgraph Library | Baeldung

« 上一篇: Quarkus 入门指南
» 下一篇: Java 中的 Void 类型