«1. Обзор

Иногда нам нужно получить информацию о поведении нашего приложения во время выполнения, например найти все классы, доступные во время выполнения.

В этом руководстве мы рассмотрим несколько примеров того, как найти все классы в пакете Java во время выполнения.

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

Сначала мы начнем обсуждение с загрузчиков классов Java. Загрузчик классов Java является частью среды выполнения Java (JRE), которая динамически загружает классы Java в виртуальную машину Java (JVM). Загрузчик классов Java отделяет JRE от знаний о файлах и файловых системах. Не все классы загружаются одним загрузчиком классов.

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

В Java 9 в загрузчики классов были внесены некоторые существенные изменения. С введением модулей у нас есть возможность указать путь к модулю вместе с путем к классам. Загрузчик системных классов загружает классы, присутствующие в пути к модулю.

Загрузчики классов являются динамическими. От них не требуется сообщать JVM, какие классы она может предоставлять во время выполнения. Следовательно, поиск классов в пакете — это, по сути, операция файловой системы, а не операция, выполняемая с помощью Java Reflection.

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

3. Поиск классов в пакете Java

Для иллюстрации создадим пакет com.baeldung.reflection.access.packages.search.

Теперь давайте определим пример класса:

public class ClassExample {
    class NestedClass {
    }
}

Затем давайте определим интерфейс:

public interface InterfaceExample {
}

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

3.1. Загрузчик системных классов

Во-первых, мы будем использовать встроенный загрузчик системных классов. Загрузчик системных классов загружает все классы, найденные в пути к классам. Это происходит во время ранней инициализации JVM:

public class AccessingAllClassesInPackage {

    public Set<Class> findAllClassesUsingClassLoader(String packageName) {
        InputStream stream = ClassLoader.getSystemClassLoader()
          .getResourceAsStream(packageName.replaceAll("[.]", "/"));
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        return reader.lines()
          .filter(line -> line.endsWith(".class"))
          .map(line -> getClass(line, packageName))
          .collect(Collectors.toSet());
    }
 
    private Class getClass(String className, String packageName) {
        try {
            return Class.forName(packageName + "."
              + className.substring(0, className.lastIndexOf('.')));
        } catch (ClassNotFoundException e) {
            // handle the exception
        }
        return null;
    }
}

В нашем примере выше мы загружаем загрузчик системных классов с помощью статического метода getSystemClassLoader().

Далее мы найдем ресурсы в данном пакете. Мы будем читать ресурсы как поток URL-адресов, используя метод getResourceAsStream. Чтобы получить ресурсы в пакете, нам нужно преобразовать имя пакета в строку URL. Итак, мы должны заменить все точки (.) разделителем пути (€œ/â€).

После этого мы собираемся ввести наш поток в BufferedReader и отфильтровать все URL-адреса с расширением .class. Получив необходимые ресурсы, мы создадим класс и соберем все результаты в Set. Поскольку Java не позволяет лямбде генерировать исключение, мы должны обрабатывать его в методе getClass.

Теперь протестируем этот метод:

@Test
public void when_findAllClassesUsingClassLoader_thenSuccess() {
    AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
 
    Set<Class> classes = instance.findAllClassesUsingClassLoader(
      "com.baeldung.reflection.access.packages.search");
 
    Assertions.assertEquals(3, classes.size());
}

В пакете всего два файла Java. Однако у нас есть три объявленных класса, включая вложенный класс NestedExample. В результате наш тест вылился в три класса.

Обратите внимание, что пакет поиска отличается от текущего рабочего пакета.

3.2. Библиотека Reflections

Reflections — это популярная библиотека, которая сканирует текущий путь к классам и позволяет запрашивать его во время выполнения.

Давайте начнем с добавления зависимости отражений в наш проект Maven:

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId> 
    <version>0.9.12</version>
</dependency>

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

public Set<Class> findAllClassesUsingReflectionsLibrary(String packageName) {
    Reflections reflections = new Reflections(packageName, new SubTypesScanner(false));
    return reflections.getSubTypesOf(Object.class)
      .stream()
      .collect(Collectors.toSet());
}

В этом методе мы инициируем класс SubTypesScanner и извлекаем все подтипы класса Object. Благодаря этому подходу мы получаем больше детализации при выборке классов.

Опять же, давайте проверим это:

@Test
public void when_findAllClassesUsingReflectionsLibrary_thenSuccess() {
    AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
 
    Set<Class> classes = instance.findAllClassesUsingReflectionsLibrary(
      "com.baeldung.reflection.access.packages.search");
 
    Assertions.assertEquals(3, classes.size());
}

Подобно нашему предыдущему тесту, этот тест находит классы, объявленные в данном пакете.

Теперь давайте перейдем к нашему следующему примеру.

3.3. Библиотека Google Guava

В этом разделе мы увидим, как находить классы с помощью библиотеки Google Guava. Google Guava предоставляет служебный класс ClassPath, который сканирует источник загрузчика классов и находит все загружаемые классы и ресурсы.

Во-первых, давайте добавим в наш проект зависимость от guava:

<dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>31.0.1-jre</version>
</dependency>

Давайте углубимся в код:

public Set<Class> findAllClassesUsingGoogleGuice(String packageName) throws IOException {
    return ClassPath.from(ClassLoader.getSystemClassLoader())
      .getAllClasses()
      .stream()
      .filter(clazz -> clazz.getPackageName()
        .equalsIgnoreCase(packageName))
      .map(clazz -> clazz.load())
      .collect(Collectors.toSet());
}

«

«В приведенном выше методе мы предоставляем загрузчик системных классов в качестве входных данных для метода ClassPath#from. Все классы, сканируемые ClassPath, фильтруются на основе имени пакета. Затем отфильтрованные классы загружаются (но не связываются и не инициализируются) и собираются в набор.

@Test
public void when_findAllClassesUsingGoogleGuice_thenSuccess() throws IOException {
    AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
 
    Set<Class> classes = instance.findAllClassesUsingGoogleGuice(
      "com.baeldung.reflection.access.packages.search");
 
    Assertions.assertEquals(3, classes.size());
}

Теперь протестируем этот метод:

Кроме того, библиотека Google Guava предоставляет методы getTopLevelClasses() и getTopLevelClassesRecursive().

Важно отметить, что во всех приведенных выше примерах информация о пакете включена в список доступных классов, если она присутствует в пакете и снабжена одной или несколькими аннотациями на уровне пакета.

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

4. Поиск классов в модульном приложении

Система Java Platform Module System (JPMS) познакомила нас с новым уровнем управления доступом с помощью модулей. Каждый пакет должен быть явно экспортирован, чтобы к нему можно было получить доступ за пределами модуля.

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

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

Для безымянного модуля в качестве пути к классам будет указан текущий рабочий каталог.

4.1. Внутри модуля

Все пакеты в модуле видны другим пакетам в модуле. Код внутри модуля имеет рефлексивный доступ ко всем типам и всем их членам.

4.2. Вне модуля

Так как Java обеспечивает наиболее ограниченный доступ, мы должны явно объявлять пакеты, используя объявление экспорта или открытия модуля, чтобы получить отражающий доступ к классам внутри модуля.

Для обычного модуля отражающий доступ к экспортированным пакетам (но не к открытым) предоставляет доступ только к общедоступным и защищенным типам и всем их членам объявленного пакета.

module my.module {
    exports com.baeldung.reflection.access.packages.search;
}

Мы можем создать модуль, который экспортирует пакет, который необходимо найти:

module my.module {
    opens com.baeldung.reflection.access.packages.search;
}

Для обычного модуля рефлексивный доступ для открытых пакетов предоставляет доступ ко всем типам и их членам объявленного пакета: ~~ ~

open module my.module{
}

Аналогично, открытый модуль предоставляет рефлективный доступ ко всем типам и их членам, как если бы все пакеты были открыты. Давайте теперь откроем весь наш модуль для рефлексивного доступа:

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

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

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