«1. Обзор

В этом учебнике мы проанализируем технику перечисления всех классов, загружаемых определенным загрузчиком классов в Java, с использованием API инструментария Java. Мы также увидим, как создать и загрузить агент Java для получения экземпляра Instrumentation и вызвать необходимые методы для выполнения нашей задачи.

2. Загрузчики классов в Java

Загрузчики классов являются неотъемлемой частью JRE (Java Runtime Environment). Их работа заключается в динамической загрузке классов в виртуальную машину Java. Другими словами, они загружают классы в память по требованию, когда этого требует приложение. В статье о загрузчиках классов Java рассказывается об их различных типах и подробно рассказывается, как они работают.

3. Использование Instrumentation API

Интерфейс Instrumentation предоставляет метод getInitiatedClasses(загрузчик загрузчика классов), который можно вызывать для возврата массива, содержащего все классы, загруженные конкретным загрузчиком. Давайте посмотрим, как это работает.

Во-первых, нам нужно создать и загрузить агент для получения экземпляра интерфейса Instrumentation. Агент Java — это инструмент для инструментовки программ, работающих на JVM (виртуальная машина Java).

Другими словами, он может добавлять или изменять байт-код методов для сбора данных. Нам потребуется, чтобы агент получил дескриптор экземпляра Instrumentation и вызвал требуемый метод.

Существует несколько способов создания и загрузки агента. В этом руководстве мы будем использовать метод статической загрузки с использованием метода premain и параметра -javaagent.

3.1. Создание агента Java

Чтобы создать агент Java, нам нужно определить метод premain, которому экземпляр Instrumentation будет передан при загрузке агента. Теперь создадим класс ListLoadedClassesAgent:

public class ListLoadedClassesAgent {

    private static Instrumentation instrumentation;

    public static void premain(String agentArgs, Instrumentation instrumentation) {
        ListLoadedClassesAgent.instrumentation = instrumentation;
    }
}

3.2. Определение методов listLoadedClasses

Помимо определения агента, мы определим и предоставим статический метод для возврата массива загруженных классов для данного загрузчика классов.

Обратите внимание, что если мы передаем загрузчик классов с нулевым значением методу getInitiatedClasses, он возвращает классы, загруженные загрузчиком классов начальной загрузки.

Давайте посмотрим на код в действии:

public static Class<?>[] listLoadedClasses(String classLoaderType) {
    return instrumentation.getInitiatedClasses(
      getClassLoader(classLoaderType));
}

private static ClassLoader getClassLoader(String classLoaderType) {
    ClassLoader classLoader = null;
    switch (classLoaderType) {
        case "SYSTEM":
            classLoader = ClassLoader.getSystemClassLoader();
            break;
        case "EXTENSION":
            classLoader = ClassLoader.getSystemClassLoader().getParent();
            break;
        case "BOOTSTRAP":
            break;
        default:
            break;
    }
    return classLoader;
}

Обратите внимание, что если мы используем Java 9 или более позднюю версию, мы можем использовать метод getPlatformClassLoader. В нем будут перечислены классы, загруженные загрузчиком классов платформы. В этом случае кейс switch также будет содержать:

case "PLATFORM":
    classLoader = ClassLoader.getPlatformClassLoader();
    break;

3.3. Создание файла манифеста агента

Теперь давайте создадим файл манифеста MANIFEST.MF с соответствующими атрибутами для запуска нашего агента, в том числе:

Premain-Class: com.baeldung.loadedclasslisting.ListLoadedClassesAgent

Доступен полный список атрибутов манифеста для JAR-файла агента. в официальной документации пакета java.lang.instrument.

3.4. Загрузка агента и запуск приложения

Давайте теперь загрузим агент и запустим приложение. Во-первых, нам нужен JAR-файл агента с файлом манифеста, содержащим информацию о Premain-Class. Кроме того, нам нужен JAR-файл приложения с файлом манифеста, содержащим информацию об основном классе. Класс Launcher, содержащий метод main, запустит наше приложение. Затем мы сможем распечатать классы, загруженные различными типами загрузчиков классов:

public class Launcher {

    public static void main(String[] args) {
        printClassesLoadedBy("BOOTSTRAP");
        printClassesLoadedBy("SYSTEM");
        printClassesLoadedBy("EXTENSION");
    }

    private static void printClassesLoadedBy(String classLoaderType) {
        System.out.println(classLoaderType + " ClassLoader : ");
        Class<?>[] classes = ListLoadedClassesAgent.listLoadedClasses(classLoaderType);
        Arrays.asList(classes)
            .forEach(clazz -> System.out.println(clazz.getCanonicalName()));
    }
}

Далее давайте статически загрузим агент Java и запустим наше приложение:

java -javaagent:agent.jar -jar app.jar

После выполнения приведенной выше команды мы увидим вывод:

BOOTSTRAP ClassLoader :
java.lang.ClassValue.Entry[]
java.util.concurrent.ConcurrentHashMap.Segment
java.util.concurrent.ConcurrentHashMap.Segment[]
java.util.StringTokenizer
..............
SYSTEM ClassLoader : 
java.lang.Object[]
java.lang.Object[][]
java.lang.Class
java.lang.Class[]
..............
EXTENSION ClassLoader :
byte[]
char[]
int[]
int[][]
short[]

4. Заключение

В этом уроке мы узнали о методе перечисления всех классов, загруженных в определенный загрузчик классов.

Сначала мы создали агент Java. После этого мы определили метод для вывода списка загруженных классов с помощью Java Instrumentation API. Наконец, мы создали файлы манифеста агента, загрузили агент и запустили наше приложение.

Как всегда, полный исходный код примера можно найти на GitHub.