«1. Обзор

В этом кратком руководстве мы поговорим о библиотеке Classgraph — в чем она помогает и как мы можем ее использовать.

Classgraph помогает нам находить целевые ресурсы в пути к классам Java, создает метаданные о найденных ресурсах и предоставляет удобные API для работы с метаданными.

Этот вариант использования очень популярен в приложениях на основе Spring, где компоненты, отмеченные стереотипными аннотациями, автоматически регистрируются в контексте приложения. Однако мы можем использовать этот подход и для пользовательских задач. Например, нам может понадобиться найти все классы с определенной аннотацией или все файлы ресурсов с определенным именем.

Самое интересное, что Classgraph работает быстро, так как работает на уровне байт-кода, то есть проверяемые классы не загружаются в JVM и не использует отражение для обработки.

2. Зависимости Maven

Во-первых, давайте добавим библиотеку classgraph в наш pom.xml:

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

В следующих разделах мы рассмотрим несколько практических примеров с API библиотеки.

3. Основное использование

Использование библиотеки состоит из трех основных шагов:

  1. Set up scan options – for example, target package(s)
  2. Perform the scan
  3. Work with the scan results

Давайте создадим следующий домен для нашего примера установки:

@Target({TYPE, METHOD, FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {

    String value() default "";
}
@TestAnnotation
public class ClassWithAnnotation {
}

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());
}

Теперь давайте посмотрим на 3 шага выше на пример поиска классов с помощью @TestAnnotation:

    Давайте разберем приведенный выше пример:

мы начали с настройки параметров сканирования (мы настроили сканер на анализ только информации о классе и аннотации, а также указание анализировать только файлы из целевого пакета) мы выполнили сканирование с помощью метода ClassGraph.scan() мы использовали ScanResult для поиска аннотированных классов, вызвав метод getClassWithAnnotation()

Как мы также увидим в В следующих примерах объект ScanResult может содержать много информации об API, которые мы хотим проверить, например ClassInfoList.

4. Фильтрация по аннотации метода

public class MethodWithAnnotation {

    @TestAnnotation
    public void service() {
    }
}

Расширим наш пример на аннотации методов:

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());
}

Мы можем найти все классы, у которых есть методы, отмеченные целевой аннотацией, с помощью аналогичного метода — getClassesWithMethodAnnotations():

Метод возвращает объект ClassInfoList, содержащий информацию о классах, соответствующих проверке.

5. Фильтрация по параметру аннотации

Давайте также посмотрим, как мы можем найти все классы с методами, помеченными целевой аннотацией и со значением параметра целевой аннотации.

public class MethodWithAnnotationParameterDao {

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

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

Во-первых, давайте определим классы, содержащие методы с @TestAnnotation, с двумя разными значениями параметров:

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());
}

Теперь давайте пройдемся по результату ClassInfoList и проверим аннотации каждого метода:

~~ ~ Здесь мы использовали классы метаданных AnnotationInfo и MethodInfo, чтобы найти метаданные для методов и аннотаций, которые мы хотим проверить.

public class FieldWithAnnotation {

    @TestAnnotation
    private String s;
}
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());
}

6. Фильтрация по аннотации поля

Мы также можем использовать метод getClassesWithFieldAnnotation() для фильтрации результата ClassInfoList на основе аннотации поля:

my data

7. Поиск ресурсов

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");
}

Наконец, мы посмотрим, как мы можем найти информацию о ресурсах пути к классам.

Давайте создадим файл ресурсов в корневом каталоге пути к классам classgraph — например, src/test/resources/classgraph/my.config — и добавим в него некоторое содержимое:

Теперь мы можем найти ресурс и получить его содержимое:

Здесь мы видим, что мы использовали метод getResourcesWithExtension() ScanResult для поиска нашего конкретного файла. У класса есть несколько других полезных методов, связанных с ресурсами, таких как getAllResources(), getResourcesWithPath() и getResourcesMatchingPattern().

Эти методы возвращают объект ResourceList, который в дальнейшем можно использовать для перебора объектов Resource и управления ими.

8. Создание экземпляров

Когда мы хотим создавать экземпляры найденных классов, очень важно делать это не через Class.forName, а с помощью библиотечного метода ClassInfo.loadClass.

«Причина в том, что Classgraph использует собственный загрузчик классов для загрузки классов из некоторых файлов JAR. Итак, если мы используем Class.forName, один и тот же класс может быть загружен более одного раза разными загрузчиками классов, и это может привести к нетривиальным ошибкам.