1. Обзор
Иногда мы хотим получить информацию о поведении нашего приложения во время выполнения, например найти все классы, доступные во время выполнения.
В этом руководстве мы рассмотрим несколько примеров того, как найти все классы в пакете Java во время выполнения.
2. Загрузчики классов
Во-первых, мы начнем обсуждение с загрузчиков классов Java .
Загрузчик классов Java является частью среды выполнения Java (JRE), которая динамически загружает классы Java в виртуальную машину Java (JVM). Загрузчик классов Java отделяет JRE от знаний о файлах и файловых системах. Не все классы загружаются одним загрузчиком классов .
Давайте разберемся с доступными загрузчиками классов в Java через графическое представление:
В Java 9 были внесены некоторые серьезные изменения в загрузчики классов. С введением модулей у нас есть возможность указать путь к модулю вместе с путем к классам. Загрузчик системных классов загружает классы, присутствующие в пути к модулю.
Загрузчики классов являются динамическими . От них не требуется сообщать JVM, какие классы она может предоставлять во время выполнения. Следовательно, поиск классов в пакете — это, по сути, операция файловой системы, а не операция, выполняемая с помощью Java Reflection .
Однако мы можем написать собственные загрузчики классов или изучить путь к классам, чтобы найти классы внутри пакета.
3. Поиск классов в пакете Java
Для нашей иллюстрации давайте создадим пакет
com.foreach.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.foreach.reflection.access.packages.search");
Assertions.assertEquals(3, classes.size());
}
В пакете всего два файла Java. Однако у нас есть три объявленных класса, включая вложенный класс NestedExample
. В результате наш тест вылился в три класса.
Обратите внимание, что пакет поиска отличается от текущего рабочего пакета.
3.2. Библиотека отражений
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.foreach.reflection.access.packages.search");
Assertions.assertEquals(3, classes.size());
}
Подобно нашему предыдущему тесту, этот тест находит классы, объявленные в данном пакете.
Теперь давайте перейдем к нашему следующему примеру.
3.3. Библиотека Гуавы Google
В этом разделе мы увидим, как находить классы с помощью библиотеки 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.foreach.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.foreach.reflection.access.packages.search;
}
Для обычного модуля отражающий доступ для открытых пакетов обеспечивает доступ ко всем типам и их членам объявленного пакета:
module my.module {
opens com.foreach.reflection.access.packages.search;
}
Точно так же открытый модуль предоставляет отражающий доступ ко всем типам и их членам, как если бы все пакеты были открыты. Давайте теперь откроем весь наш модуль для рефлексивного доступа:
open module my.module{
}
Наконец, убедившись, что для модуля предоставлены надлежащие модульные описания для доступа к пакетам, можно использовать любой из методов из предыдущего раздела для поиска всех доступных классов внутри пакета.
5. Вывод
В заключение мы узнали о загрузчиках классов и различных способах поиска всех классов в пакете. Также мы обсудили доступ к пакетам в модульном приложении.
Как обычно, весь код доступен на GitHub .