Полное руководство по Java Reflection API. Рефлексия на примерах
Аннотации в Java и их обработка в RUNTIME с помощью рефлексии
Аннотации – это своего рода маркеры, которые служат знаком либо компилятору, либо обработчику скомпилированных классов, либо самому приложению во время его выполнения – знаком что-то сделать. Для аннотаций всегда нужен обработчик, сами они ничего не делают.
Пример обработчика – тестовый фреймворк JUnit. Для него мы помечаем метод аннотацией @Test, и он обрабатывает этот метод как тестовый. Вне фреймворка аннотация @Test ничего не значит.
Другой пример – ORM-фреймворк Hibernate. Еще есть Spring Framework, проект Lombook.
Задача
В этой статье мы сконцентрируемся на обработке аннотаций в Runtime. Мы напишем свой собственный обработчик – упрощенный фреймворк для тестирования, который запускает тесты. Обработчик будет искать методы, аннотированные аннотацией @Test, и запускать их.
Наша задача создать статический метод, которому передается объект типа Class, содержащий тестируемые методы:
Метод должен запустить все аннотированные с помощью @Test методы класса testClass и посчитать, сколько тестов прошло, а сколько нет.
Для начала мы создадим аннотацию @Test (свою, а не ту, что есть в JUnit). Хотя теоретически можно было бы переиспользовать и ту, просто написать для нее свой обработчик.
Создание аннотации @Test
Синтаксис аннотаций довольно прост, в нашем случае определение аннотации @Test выглядит так:
@Target и @Retention – это метаанотации, то есть аннотации, которые применяются к аннотации.
Значение ElementType.METHOD в @Target показывает, что наша аннотация @Test применима к методам, то есть с помощью нее можно аннотировать только метод:
А вообще аннотации можно применять практически к любому элементу кода – классу, параметрам, полям…Просто в нашем случае требуется аннотировать только методы, что мы и указываем в определении аннотации. Если теперь в коде применить аннотацию @Test к полю, классу или другому элементу, то компилятор выдаст ошибку.
Виды RetentionPolicy
Сделаем небольшое отступление о метааннотации @Retention(RetentionPolicy.RUNTIME). Она, как и @Target, доступна в JDK по умолчанию и служит для оформления пользовательских аннотаций.
Вообще есть три уровня RetentionPolicy:
- RetentionPolicy.SOURCE
- RetentionPolicy.CLASS (действует по умолчанию, если метааннотация @Retention не указана)
- RetentionPolicy.RUNTIME
“Retention” переводится как “сохранение”, “удержание”, имеется в виду где (на каком этапе) сохраняются/выживают аннотации.
Уровни вы списке выше упорядочены по степени выживания аннотаций (1 – самая короткоживущая).
- Аннотации с RetentionPolicy.SOURCEостаются только в исходниках, в скомпилированных файлах их уже нет. Они интересны либо статическим анализаторам кода, либо обработчику аннотаций на этапе компиляции. Как создать дополнительный обработчик исходного кода подробно рассматривается в статье – в ней мы генерируем новый класс на этапе компиляции. Так работает Lombok – он на основе аннотаций генерирует сеттеры и геттеры в исходниках.
Аннотации с RetentionPolicy.CLASS остаются в скомпилированных файлах, но на этапе выполнения программы в машинном коде их уже нет. Эти аннотации могут быть интересны обфускатору кода (который переименовывает и сокращает скомпилированный код). Или, например, служебной библиотеке Javassist. Как изменить байт-код с помощью этой библиотеки есть в другой статье.
В этой статье мы рассмотрим, как работать с аннотациями на этапе выполнения кода в (runtime), для этого аннотации должны иметь самый сильный уровень RetentionPolicy.RUNTIME. Мы будем использовать Java Reflection – библиотеку, которая может считать и изменить информацию о классе в процессе выполнения программы. А также запустить методы класса.
Класс Class
Теперь пара слов о параметре метода TestRunner.run(Class testClass).
Тестировать мы будем класс Sample1, а конкретно, его аннотированные с помощью @Test методы:
Значит в параметр мы будем передавать объект Class, полученный из класса Sample1:
Библиотека Java Reflection имеет дело с объектом типа Class. Она может получить из этого объекта список методов, полей, конструкторов, аннотаций для данного класса.
Объект типа Class создается для каждого класса приложения при его загрузке. То есть при первом обращении к классу, например при создании объекта этого класса, загружается скомпилированный .class файл, содержащий байт-код класса, Sample1.class. Его содержимое загружается в память VM и на его основе в heap создается объект типа Class , в котором есть вся информация о классе – какие в нем методы, поля, конструкторы, аннотации.
Давайте приступим к реализации логики – получим из этого объекта список тестовых методов и запустим их.
Метод TestRunner.run()
Чтобы запустить методы, надо получить их список. В цикле мы идем по полученным методам класса и выбираем аннотированные:
Далее, в InvocationTargetException оборачивается исключение, выброшенное изнутри тестового метода. То есть попадание в этот блок catch() считается провалом теста.
В блок IllegalAccessException мы попадаем, если некорректно вызвали invoke(): например, передали ему null, когда m не статический. Эта ситуация считается невалидным тестом.
Если же ни в какой блок catch() мы не попали, что тест считается валидным и пройденным.
В принципе все, таким образом подсчитывается и выводится количество пройденных и не пройденных тестов.
В реальном фреймворке типа JUnit все гораздо сложнее, потому что поддерживается много аннотаций – @Before, @After и т.д.
А мы давайте еще сделаем поддержку аннотации @ExceptionTest – она используется, чтобы показать, что тест должен выбрасывать определенное исключение.
Тестирование методов на выбрасывание исключения
Напишем отдельный класс с методами, которые должны выбрасывать исключение, и нам надо протестировать, что они их действительно выбрасывают.
Как маркер выбрасывания исключения у нас создана аннотация @ExceptionTest:
Внутри аннотации указан тип исключения, который должен выбрасывать метод.
Обратите внимание, что метод doublyBad() аннотирован аннотацией @ExceptionTest дважды. Это означает, что надо протестировать, что метод выбрасывает любое из этих исключений.
Здесь мы подошли к понятию repeatable annotation.
Repeatable аннотации
Аннотации, которыми можно аннотировать элемент несколько раз, называются repeatable. @ExceptionTest является repeatable-аннотацией. Синтаксис их не прост, а особенно обработка.
Чтобы определить аннотацию как repeatable, мы должны аннотировать ее дополнительной метааннотацией @Repeatable(ExceptionTestContainer.class):
Здесь внутри @Repeatable задана вторая аннотация ExceptionTestContainer.class, и ее нам тоже надо определить. Она служит контейнером для повторяемых значений:
В ExceptionTestContainer должен быть массив типа ExceptionTest. Вот так они закручены, и с обработкой дела обстоят не лучше.
Обрабатывать повторяемые аннотации надо осторожно.
Итак, допишем наш цикл так, чтобы тесты, аннотированные с помощью @ExceptionTest, тоже запускались и проверялись.
Поддержим @ExceptionTest
Для этого допишем еще один блок if внутри цикла в TestRunner.run():
В соответствии с нашим замечанием выше об особенностях работы метода isAnnotationPresent() с повторяемыми аннотациями, мы проверяем методы на наличие любой из двух аннотаций: ExceptionTest и ExceptionTestContainer.
Далее ситуация противоположная – чтобы тест был пройден, исключение должно быть выброшено, то есть мы должны попасть в блок catch(). Причем исключение должно быть именно одного из тех типов, перечисленных в аннотации. Что мы и проверяем.
Рефлексия кода, reflection
Рефлексия (от reflexio – обращение назад) – это механизм исследования данных о программе во время её выполнения. Рефлексия в Java осуществляется с помощью Java Reflection API, состоящий из классов пакетов java.lang и java.lang.reflect. В информатике рефлексия означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Java Reflection API позволяет получать информацию о конструкторах, методах и полях классов и выполнять следующие операции над полями и методами объекта/класса :
- определение класса объекта;
- получение информации о полях, методах, конструкторах и суперклассах;
- получение информации о модификаторах полей и методов;
- создание экземпляра класса, имя которого неизвестно до момента выполнения программы;
- определение и изменение значений свойств объекта/класса;
- вызов методов объекта/класса.
Примечание : в тексте используется объект/класс. При работе с объектом (реализацией класса) можно обращаться к полям и методам класса напрямую, если они доступны (не private). При работе с классом можно обращаться к методам класса с использованием Java Reflection API. Но класс необходимо получить из объекта.
Определение свойств класса
В работающем приложении для получения класса необходимо использовать метод forName (String className). Следующий код демонстрирует возможность создания класса без использования и с использованием Reflection :
Метод класса forName(className) часто используется для загрузки JDBC-драйвера.
Методом getName() объекта Class можно получить наименование класса, включающего пакет (package) :
Для получения значения модификатора класса используется метод getModifiers(). Класс java.lang.reflect.Modifier содержит статические методы, возвращающие логическое значения проверки модификатора класса :
Для получения суперкласса рефлексированного объекта (класса) необходимо использовать метод getSuperclass() :
Поскольку в Java отсутствует множественное наследование, то для получения всех предков следует рекурсивно вызвать метод getSuperclass() в цикле, пока не будет достигнут Object, являющийся родителем всех классов. Object не имеет родителей, поэтому вызов его метода getSuperclass() вернет null.
Определение интерфейсов и конструкторов класса
Для получения в режиме run-time списка реализующих классом интерфейсов, необходимо получить Class и использовать его метод getInterfaces(). В следующем примере извлекается список интерфейсов класса ArrayList :
Чтобы IDE (Eclipse) не предупреждала о необходимости определения типа класса
Class is a raw type. References to generic type Class should be parameterized
в коде были использованы generic’и. В консоль выводятся следующие интерфейсы, реализуемые классом ArrayList :
Метод класса getConstructors() позволяет получить массив открытых конструкторов типа java.lang.reflect.Constructor. После этого, можно извлекать информацию о типах параметров конструктора и генерируемых исключениях :
Определение полей класса
Метод getFields() объекта Class возвращает массив открытых полей типа java.lang.reflect.Field, которые могут быть определены не только в данном классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Field позволяет получить имя поля, тип и модификаторы :
Если известно наименование поля, то можно получить о нем информацию с помощью метода getField() объекта Class.
Методы getField() и getFields() возвращают только открытые члены данных класса. Чтобы получить все поля класса, включая закрытые и защищенные, необходимо использовать методы getDeclaredField() и getDeclaredFields(). Данные методы работают точно так же, как и их аналоги getField() и getFields().
Определение значений полей класса
Класс Field содержит специализированные методы для получения значений примитивных типов: getInt(), getFloat(), getByte() и др. Для установки значения поля, используется метод set(). Для примитивных типов имеются методы setInt(), setFloat(), setByte() и др.
Ниже приведен пример изменения значения закрытого поля класса в режиме run-time.
Определение методов класса
Метод getMethods() объекта Class возвращает массив открытых методов типа java.lang.reflect.Method. Эти методы могут быть определены не только в классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Method позволяет получить имя метода, тип возвращаемого им значения, типы параметров метода, модификаторы и генерируемые исключения.
Если известно имя метода и типы его параметров, то можно получить отдельный метод класса :
Пример изменения значения закрытого поля класса
Чтобы изменить значение закрытого (private) поля класса необходимо получить это поле методом getDeclaredField () и вызвать метод setAccessible (true) объекта Field, чтобы открыть доступ к полю. После этого значение закрытого поля можно изменять, если оно не final. В следующем примере определен внутренний класс PrivateFinalFields с набором закрытых полей; одно из полей final. При создании объекта класса поля инициализируются. В методе main примера поочередно в закрытые поля вносятся изменения и свойства объекта выводятся в консоль.
В результате выполнения примера в консоль будут выведены следующие сообщения :
Из приведённого примера видно что поля private можно изменять. Для этого необходимо получить объект типа java.lang.reflect.Field с помощью метода getDeclaredField (), вызвать его метод setAccessible (true) и с помощью метода set () установить требуемое значение поля. Необходимо иметь в виду, что наличие модификатора final в закрытом текстовом поле не вызывает исключений при изменении значений, а само значение поля остаётся прежним, т.е. final поля остаются неизменные. Если не вызвать метод открытия доступа к полю setAccessible (true), то будет вызвано исключение java.lang.IllegalAccessException.
Пример вызова метода, invoke
Java Reflection Api позволяет вызвать метод класса. Рассмотрим пример, в котором определим класс Reflect, включающий поля и методы управления ими. В режиме run-time с помощью метода данного класса будем изменять значения полей и распечатывать их.
Листинг класса Reflect
Класс Reflect включает два закрытых поля (id, name) и методы управления их значениями set/get. Дополнительно в класс включим метод setData, который будем вызывать для изменения значений полей, и метод toString для печати их значений.
Для тестирования объекта типа Reflect с помощью Java Reflection Api создадим класс ReflectionTest. В этот класс включим две процедуры getClassFields и getClassMethods, которые в режиме run-time распечатают всю информацию (описание полей и методов) о классе. Методы получают класс в качестве параметра. В процедурах сначала определяются массивы полей и методы; после этого их параметры распечатываются :
В конструкторе класса ReflectionTest сначала вызываются процедуры определения полей и методов объекта/класса Reflect. После этого вызываются методы изменения значений и печати значений с использованием Reflection API. Для определения метода setData используется массив типов параметров. Вызов метода setData выполняется с передачей ему массива новых значений.
В результате выполнения примера в консоль будут выведены представленные ниже сообщения. Методы setData и toString(), вызываемые с помощью Java Reflection API, вносят измнения в закрытые поля класса и распечатываются их значения.
Скачать пример
Исходный код рассмотренного примера вызова метода invoke с использованием Java Reflection API можно скачать здесь (989 байт).
Reflection в Java – java.lang.reflect API туториал
Reflection API в Java используется для просмотра информации о классах, интерфейсах, методах, полях, конструкторах, аннотациях во время выполнения java программ. При этом знать названия исследуемых элементов заранее не обязательно.
Все классы для работы с reflection расположены в пакете java.lang.reflect. Это метод (Method), конструктор (Constructor), массив (Array), поле (Field) и многие другие.
Вместо изучения каждого из этих элементов в отдельности, для знакомства с reflection я предлагаю вам небольшой пример. Мы попытаемся в runtime вывести всю возможную информацию о следующем java-классе:
Способы получения необходимой нам информации в некоторых местах я упростил, чтобы пример не становился слишком большим. Чуть ниже я прокомментирую некоторые фрагменты кода, на которые хотел бы обратить ваше внимание.
В результате выполнения кода мы получим следующее:
Модификаторы в Java по спецификации представлены в виде целых чисел, например:
Для декодирования значений модификаторов используется класс Modifiers.
Заметьте, что в классе Class присутствуют пары методов, как например getFields и getDeclaredFields. Метод getFields возвращает только те поля, которые объявлены как public + public поля родительских классов, в то время как getDeclaredFields возвращает все поля текущего класса не зависимо от их видимости. Аналогично для getMethods и getConstructors.
Типы параметров и типы возвращаемых значений могут быть массивами. В этом случае для получения типа элементов, которые можно хранить в массиве, нужно использовать метод getComponentType.
Для получения аннотаций используется метод getAnnotations. Но метод вернет не сами классы аннотаций, а Proxy. Чтобы получить сам класс аннотации используйте метод annotationType().
Если у вас будут вопросы по приведенному выше фрагменту кода, задавайте их в комментариях к статье. Сейчас же я хочу перейти к некоторым практическим аспектам, которые иногда встречаются в реальной жизни.
1. С введением аннотаций в java 1.5 стало легко добавлять метаинформацию к классам, полям, методам и параметрам. По наличию аннотации (или ее отсутствию) можно во время выполнения программы принимать различного рода решения.
Допутим, мы пишем свой модуль для сериализации объектов. Пусть некоторая описанная нами аннотация @Transient говорит о том, что конкретное поле не нужно сериализовать. Нам поможет следующий фрагмент кода:
2. Работая с сервлетами часто нужно иметь возможность меппить параметры запроса на объекты. Вот небольшая часть функционала для такого меппинга из реального проекта, в котором я принимал участие:
В частности, метод wrapperFor используется для определения класса-обертки для примитивных типов. Это удобно для последующего вызова метода valueOf. К сожалению, класс Character не содержит такого метода и его нужно обрабатывать особенным образом.
3. Продлжая тему о сервлетах. В веб-фреймворке, который писали мои друзья, используется следующий контракт: действие представляет собой вызов некоторого метода someMethod у некоторого класса SomeClass. При этом url имеет вид, например, somedomain.com/SomeAction.someMethod.
Вот небольшой фрагмент кода, который показывает, как можно вызвать метод у класса используя reflection. Пример очень упрощен в обучающих целях:
Для вызова, в метод invoke нужно передать инстанс, у которого и будет вызываться наш метод.
Обратите внимание, что InvocationTargetException оборачивает ошибку, которая может произойти в методе, вызываемом через reflection. Для получения самого исключения необходимо взять причину (cause) у инстанса InvocationTargetException.
4. Часто программистам приходится работать с чужими проприетарными библиотеками. Случается, что где-то в библиотеке значения по-умолчанию некоторых полей вас не устраивают, но изменить их через API нет возможности. Или некоторый метод закрыт модификатором доступа private.
Вместо декомпиляции-исправления-компиляции есть способ, который иногда выручает. Это установка флага acccessible. Давайте установим значение 10 для поля field класса Inaccessible:
Программа выведет сначала 0, а потом 10.
5. При написании своих контейнеров (на подобии IoC), вам может пригодиться способ для инстанциирования объектов с помощью default контруктров. Это можно сделать следующим образом:
Кстати, мой друг не так давно показал мне интересный способ инстанциирования объектов, который он обнаружил в sun’овских интерналсах. Нам даже посчастливилось воспользоваться данным способом на практике в виде такого вот метода:
А дело было вот в чем. Нам дали API бинарного протокола, который был написан мягко говоря плохо. Каждый transfer объект (а их около 40) содержал большое количество параметров (2-6), каждый из которых необходимо было проставлять через конструктор. Ни один из параметров на нашей стороне вообще не использовался – эти параметры заполнялись и использовались уже на стороне сервера.
Собственно метод instance очень упростил нам способ инстанциирования объектов.
Полное руководство по Java Reflection API. Рефлексия на примерах
Пример 1: создание объекта.
Допустим нужно создать копию заданного объекта. Какого конкретно типа объект не ясно, изначально мы знаем только то, что это высший тип в иерархии объектов Java – java.lang.Object. Если возможные типы объекта известны заранее, можно предусмотреть проверку с помощью оператора instanceof. Но предположим – типов много и предусмотреть проверку для каждого из них не представляется возможным, нужно универсальное решение. После создания копии объекта с ним могут выполняться какие-то действия, опять же с помощью рефлексии. Ведь мы не знаем, объект какого типа был передан – в этом и нет необходимости. Пример очень простой:
Метод Class.newInstance() предполагает, что у класса, экземпляром которого является заданный объект, есть конструктор по умолчанию (без аргументов). В противном случае произойдет ошибка. Разумеется есть возможность создать класс и с помощью конструктора с аргументами. Для этого используется метод java.lang.reflect.Constructor.newInstance(). Здесь я пожалуй переадресую вас к документации Oracle – Creating New Class Instances. Итак, после создания экземпляра объекта, нужно вызвать на нем какой-то метод, который не объявлен и не определен в классе java.lang.Object, но мы то знаем – он существует (кроме того, возможно на этапе написания кода имя метода неизвестно, и определяется только во время выполнения).
Пример 2: вызов метода на объекте.
Посмотрим на метод, который позволяет вызвать на объекте любой заданный по имени метод. Если такой метод не существует, будет сгенерировано исключение NoSuchMethodException. Перехват Exception указан потому что вызов метода на объекте может порождать и другие исключения.
Как видно, метод позволяет вызвать любой заданный по имени метод – с аргументами или без. Нужно обратить внимание на метод findMethodAtClass. Казалось бы, мы узнали действительный тип объекта, знаем имя метода, что еще нужно?
Дело в том, что если метод вызывается на объекте с помощью рефлексии – нужно обязательно знать, в каком классе определен этот метод. Допустим – целевой класс является подклассом суперкласса, в котором определен нужный метод, а в самом целевом классе нет такого метода – нам нужно это знать. Получить объект Method из подкласса не получится. В примере ищется определенный метод – выше по иерархии классов, начиная с текущего (тип, которым является заданный объект).
Пример 3: доступ к полям класса.
С помощью рефлексии можно получить доступ к любому полю класса по имени.
Можно прочитать и изменить значения приватных полей, и даже final полей. Если говорить более точно – можно “на лету” сделать подобные поля открытыми для чтения или/и изменения – таким образом можно получить доступ к полю, которое было объявлено как приватное, из любого класса. В качестве примера – класс с приватным final полем name. Считываем значение поля, предварительно разрешив доступ к нему, а потом меняем значение:
Кратко о рефлексии в языках программирования C++, Java и Python
October 24, 2015
Если Вам знаком термин “рефлексия” и Вы пишите программы, которые используют этот механизм – то, возможно, Вас заинтересует как обстоят дела с этим “явлением” в других языках программирования. Если же этот термин Вам ни о чем не говорит, то ниже Вы найдете краткое пояснение, что же такое рефлексия (или как её еще называют – отражение) и несколько простых примеров с рефлексией на языках C++, Java и Python.
Несмотря на то, что рефлексия – это мощный и эффективный механизм, он в то же время достаточно сложный. Рефлексия интересует в основном разработчиков инструментальных средств, тогда как программисты, пишущие прикладные программы, обычно ею не пользуются.
И всё-таки иметь представление о том, что такое рефлексия должен каждый программист, так как многие задачи без неё просто не решаются или решение оказывается менее эффективным.
Что же такое рефлексия? Вот определение, которое дает Википедия:
“Термин отражение или рефлексия означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения…”.
Это определение не совсем точное, оно справедливо для языков Python и Java. Но в С++ кроме времени выполнения, у программы есть еще один важный этап – компиляция. И, несмотря на то, что в С++ рефлексия времени выполнения фактически отсутствует, рефлексия времени компиляции (или как её ещё называют – статическая рефлексия) очень скоро будет иметь широкую поддержку (стандарт С++17).
В Java имеется специальная библиотека рефлексии (java.lang.reflect), которая предоставляет большой набор инструментальных средств для манипуляции кодом в динамическом режиме (во время выполнения).
В языке Python всё является объектами: переменные, функции, классы, экземпляры классов, модули. Внутреннее устройство любого объекта всегда доступно во время выполнения программы. Интерпретатор никогда не скрывает внутреннее устройство объектов, поэтому рефлекия в этом языке наиболее естественный процесс.
Поддержка рефлексии есть во многих других языках программирования, таких как: С#, Objective-C, JavaScript – и всегда рефлексия позволяет программе анализировать собственную структуру. Чаще анализ производится на этапе выполнения – это свойственно тем языкам, в которых код исполняется виртуальной машиной или интерпретатором. Но анализ может проводиться и в момент компиляции, как в случае с С++.
Рассмотрим на примере возможности рефлексии в разных языках программирования. В качестве типовой задачи для рефлексии попытаемся: по имени класса создать экземпляр данного класса, “найти” у него определенный метод (также по имени) и вызвать его.. При этом нужно будет определить количество аргуметов этого метода и их тип, чтобы передать правильные значения.
Исходные данные: имя класса “Foo”, имя метода “bar”. Обязательным условием является то, что данный класс присутствует в нашей программе, другими словами, можно создать экземпляр класса Foo.
* – Если Вы не знаете Python, Java или C++, то всё равно посмотрите примеры для этих языков, так как они примитивны и полностью понятны!
Начнем с Python.
Python и рефлексия
Допустим класс Foo определен в текущем модуле, хотя это совершенно не обязательно. Мы смогли бы отыскать Foo и в любом другом модуле. Но для этого потребовалось бы написать чуть больше кода. Поэтому немного упростим задачу и “поместим” Foo в тот модуль, где и будем его искать. В Python нет перегрузки функций (методов), поэтому метод bar, будет только один (в Java нам придется учитывать еще и перегрузку).
Эта коротка программа выведет примерно следующее:
Это словарь атрибутов текущего модуля. В Python мы можем получить словарь атрибутов любого загруженного модуля (а не только текущего), используя переменную sys.modules.
Как видим в словаре присутствует ключ Foo с объектом class Foo в качестве значения (в Python даже классы являются объектами, и их можно, например, передавать в функции или хранить в списке). Можно обратиться по ключу и вызвать конструктор класса Foo.
Обратите внимание, мы работаем только с именем класса, вместо того чтобы явно написать:
Ведь в этом весь смысл рефлексии – нам нужно проанализировать текущий модуль, найти класс по его имени и создать экземпляр этого класса. Всё это уже сделано. Теперь нужно найти и изучить метод bar.
Чтобы обратиться к методу bar, мы можем воспользоваться встроенным атрибутом __dict__ или встроенной функцией getattr, которая позволяет получать доступ к атрибутам по строкам с их именами.
Доступ к методу получен, изучим метод bar. Функции являются обычными объектами, поэтому мы можем оперировать ими с помощью привычных инструментов, можем получить базовый доступ к атрибутам функции. Механизмы рефлексии позволяют нам также исследовать детали реализации – каждая функция, например, имеет присоединенный к ней объект с программным кодом, из которого можно получить такие сведения, как список локальных переменных и аргументов:
Теперь нам доступна вся необходимая информация и можно осуществить вызов метода bar экземпляра класса Foo, передав точное количество аргументов. Тип аргументов в Python не так важен.
Задача решена. Как видете в Python механизм рефлексии является довольно простым и естественным инструментом. Мы можем легко исследовать состав любого модуля или класса, анализировать функции, узнавать об их аргументах и еще много всего. Интерпретатор не скрывает от нас ничего – во время исполнения программы доступны любые знания о её структуре. Это и есть рефлексия – программа обрабатывает собственный код. Такие программы называют рефлективными.
На очереди Java и её библиотека java.lang.reflect.
Java и рефлексия
Задача та же самая. Имеем имя класса и имя метода класса – “Foo” и “bar”. Нужно сделать так:
Вот только явно написать вызов конструктора и метода нельзя. Допустим, в процессе выполнения программы мы получили имя класса и метода по сети. У нас могут быть десятки тысяч классов, и было бы хорошо написать общий код для всех случаев. Попробуем это сделать.
Задача также усложняется еще и тем, что в языке Java разрешены перегрузки, поэтому методов с именем bar в классе может быть несколько. Нам нужно найти тот, который принимает 3 аргумента с типом char. Как будет видно дальше – рефлексия в Java позволяет провести даже самый сложный анализ. Итак, у нас есть класс Foo:
Во время выполнения программы исполняющая система Java всегда осуществляет динамическую идентификацию типов объектов любых классов. Получаемые в итоге сведения используются виртуальной машиной для выбора подходящего вызываемого метода.
Получить доступ к этой информации можно, используя специальный класс, который так и называется: Class (возможно не лучшее название, но все привыкли). Вызывая статический метод forName(), можно получить объект типа Class соответствующий имени класса в строковом представлении:
Это еще не экземпляр класса Foo, это скорее источник метаинформации о классе Foo. А вот создать экземпляр класса Foo поможет метод newInstance():
Теперь нужно проанализировать функциональные возможности класса Foo с помощью рефлексии. Ниже приводится краткий обзор наиболее важных характеристик механизма рефлексии, позволяющих анализировать структуру класса.
Далее я просто приведу абзац из книги Хорстманна:
Три класса, Field, Method и Constructor, из пакета java.lang.reflect описывают соответственно поля, методы и конструкторы класса. У каждого класса есть свой набор методов: в состав Field входит метод getType(), который описывает тип поля; у классов Method и Constructor имеются методы, определяющие типы параметров, а класс Method позволяет также определить тип возвращаемого значения. На самом деле, в этих классах имеется еще много различных методов, которые позволяют получить такие данные как, например, модификатор доступа (public, private, protected).
Вернемся к классу Class. Как было сказано – это источник информации о классе. В классе Class есть методы getMethods(), getFields() и getConstructors(), возвращающие массивы открытых полей, методов и конструкторов, принадлежащих анализируемому классу. Вот как мы получим метод bar по его имени:
Всё хорошо, но есть одна проблема. Метод bar перегружен, и в блок if мы попадем два раза. А нам нужна конкретная версия с тремя аргументами типа char. Поэтому нужно “усилить” условие и применить дополнительный анализ параметров метода. Когда метод будет выбран точно, мы сможем вызвать его с помощью метода invoke класса Method.
Задача решена. Java обладает сильной библиотекой рефлексии, и её возможности очень обширны. Однако, если Вы в состоянии обойтись без применения рефлексии, то не стоит к ней прибегать. Программа, использующая механизм рефлексии работает медленнее, чем программа, непосредственно вызывающая эти методы.
Кроме того, рефлексия усложняет код программы, а значит появляются потенциальные места для ошибок. Рефлексия – мощный инструмент, но прибегать к нему нужно только по необходимости.
На очереди последний язык в этом обзоре – С++. И с ним всё очень неоднозначно.
С++ и рефлексия
Если Вы не знакомы с С++, то многое в этом разделе будет Вам непонятно. Вы можете запомнить главное – в C++ с рефлексией времени выполнения всё очень плохо. Поддержка крайне слабая, фактически отсутствует. Всё что есть – это класс type_info, который содержит некоторые сведения о типе. Эти сведения доступны во время выполнения программы, но, например, создавать экземпляры классов type_info не умеет. Зато, с его помощью, можно получить символьную строку с именем типа (правда толку от неё почти нет, см. ниже почему).
Создать объект класса type_info напрямую нельзя. Единственный способ построения объекта type_info – использовать оператор typeid. Также невозможно копировать объекты класса type_info.
Стандарт С++11 дополнил класс type_info методом hash_code. Этот метод возвращает некоторое значение, которое будет идентично для объектов type_info, ссылающихся на один и того же тип. Других гарантий нет. Значение, которое возвращает hash_code, при следующем запуске программы, будет уже другим.
В С++11 также добавили класс-обёртку type_index над type_info, который можно использовать, например, как индекс в ассоциативном контейнере.
Но всего этого недостаточно, чтобы решить нашу задачу.
С type_info связано несколько печально известных проблем. Первая – функция-член name() (имя типа) класса type_info возвращает нестандартизованную символьную строку, а значит на неё нельзя полагаться, её даже нельзя достоверно знать. Всё будет зависеть от конкретной реализации. Например, на другой платформе, для одного и того же класса, это значение может уже быть другим. Отсюда следует, что имя класса “Foo” из нашего задания просто бессмысленно.
Я проверил, что возвращает метод name() с разными компиляторами. И всегда значение было одно и тоже. Однако, в исходных кодах gcc явно указан комментарий к методу name():
Кроме того, строка, которую возвращает метод name(), не отмечена как constexpr. А это означает, что её нельзя использовать для вычислений на этапе компиляции (не подходит для метапрограммирования).
Еще одна проблема заключается в том, что type_name “ничего не знает” о typedef. Объявления typedef можно использовать для создания более коротких и значимых имен для типов – но класс type_name не способен возвращать соответствующие символьные имена.
Если бы только мы могли получить стандартизованное, портируемое, человекочитаемое символьное имя типа… но, увы. Такую возможность обещают в С++17.
И хотя, рефлексия времени выполнения в С++ уступает аналогичному механизму в Java и Python, всё не так плохо. В С++ рефлексия существует на стадии компиляции – это статическая рефлексия.
Начиная С++11 расширенная версия type_traits, обеспечила широкий доступ к свойствам типа на этапе компиляции. Мы можем исследовать какой-либо тип, проанализировать его атрибуты, и в зависимости от этого “попросить” компилятор создать тот или иной код. Этот процесс по смыслу похож на то, что мы видели в Java или Python, но происходит он в другое время – не во время работы программы, а во время работы компилятора.
Спросите C++ программиста и Java программиста о том, что такое “метапрограммирование”, и обратите внимание, что они будут рассказывать про разные этапы. Для программиста С++ метапрограммирование – это этап компиляции (compile-time). И рефлексия в этом языке имеет место быть именно на этой стадии.
Когда компилятор С++ обрабатывает единицу трансляции, у него есть доступ к большому количеству полезных метаданных. Но, к сожалению, программисту эти данные практически не доступны. Сейчас комитет по стандартизации С++ рассматривает предложения N4111, которое призывает расширить возможности рефлексии на этапе компиляции уже в следующем обновлении языка (С++17). А до той поры с решением нашей задачи на С++ лучше повременить.
Итог
Рефлексия существует в каждом языке программирования. Это сложный механизм – на каком бы этапе он не работал: будь то время выполнения программы или стадия компиляции. Однако, он очень эффективный для целого ряда задач. С помощью рефлексии, например, можно создавать программы с высоким уровнем абстракции, писать обобщенный код. Метапрограммирование сложно представить без рефлексии. Но рефлексией не стоит увлекаться. Это никак не “молоток” для любых задач (гвоздей). Это гораздо более хрупкий инструмент. Но пусть он всегда будет в Вашем арсенале для “особых случаев”.
Основные принципы программирования: интроспекция и рефлексия
Основные принципы программирования: интроспекция и рефлексия
- Переводы , 15 января 2017 в 22:06
Часто во время работы программы нам бывает нужна информация о данных — например, какой у них тип или являются ли они экземпляром класса (в ООП). Опираясь на эти знания, нам нужно проводить над данными некоторые операции, или даже изменять их — но необходимого вида данных у нас может и не быть! Если вы ничего не поняли, не расстраивайтесь — мы подробно во всём разберёмся. Всё, что я здесь описал — это иллюстрация целей двух возможностей, присутствующих почти в каждом современном языке программирования: интроспекции и рефлексии.
Интроспекция
Интроспекция — это способность программы исследовать тип или свойства объекта во время работы программы. Как мы уже упоминали, вы можете поинтересоваться, каков тип объекта, является ли он экземпляром класса. Некоторые языки даже позволяют узнать иерархию наследования объекта. Возможность интроспекции есть в таких языках, как Ruby, Java, PHP, Python, C++ и других. В целом, инстроспекция — это очень простое и очень мощное явление. Вот несколько примеров использования инстроспекции:
В Python самой распространённой формой интроспекции является использование метода dir для вывода списка атрибутов объекта:
В Ruby интроспекция очень полезна — в частности из-за того, как устроен сам язык. В нём всё является объектами — даже класс — и это приводит к интересным возможностям в плане наследования и рефлексии (об этом ниже). Если вы хотите узнать об этом больше, советую прочитать мини-цикл Metaprogramming in Ruby.
Прим. перев. Также не будет лишним прочитать нашу статью, посвящённую интроспекции в Ruby.
Вот несколько простых примеров интроспекции с использованием IRB (Interactive Ruby Shell):
Вы также можете узнать у объекта, экземпляром какого класса он является, и даже “сравнить” классы.
Однако интроспекция — это не рефлексия; рефлексия позволяет нам использовать ключевые принципы интроспекции и делать действительно мощные вещи с нашим кодом.
Рефлексия
Интроспекция позволяет вам изучать атрибуты объекта во время выполнения программы, а рефлексия — манипулировать ими. Рефлексия — это способность компьютерной программы изучать и модифицировать свою структуру и поведение (значения, мета-данные, свойства и функции) во время выполнения. Простым языком: она позволяет вам вызывать методы объектов, создавать новые объекты, модифицировать их, даже не зная имён интерфейсов, полей, методов во время компиляции. Из-за такой природы рефлексии её труднее реализовать в статически типизированных языках, поскольку ошибки типизации возникают во время компиляции, а не исполнения программы (подробнее об этом здесь). Тем не менее, она возможна, ведь такие языки, как Java, C# и другие допускают использование как интроспекции, так и рефлексии (но не C++, он позволяет использовать лишь интроспекцию).
По той же причине рефлексию проще реализовать в интерпретируемых языках, поскольку когда функции, объекты и другие структуры данных создаются и вызываются во время работы программы, используется какая-то система распределения памяти. Интерпретируемые языки обычно предоставляют такую систему по умолчанию, а для компилируемых понадобится дополнительный компилятор и интерпретатор, который следит за корректностью рефлексии.
31 мая , онлайн, беcплатно
Мне кажется, что мы сказали много об определении рефлексии, но смысла это пока несёт мало. Давайте взглянем на примеры кода ниже (с рефлексией и без), каждый из которых создаёт объект класса Foo и вызывает метод hello.
Этот список отнюдь не исчерпывает возможности рефлексии. Это очень мощный принцип, который к тому же является обычной практикой в метапрограммировании. Тем не менее, при использовании рефлексии нужно быть очень внимательным. Хотя у неё и есть свои преимущества, код, использующий рефлексию, значительно менее читаем, он затрудняет отладку, а также открывает двери по-настоящему плохим вещами, например, инъекции кода через выражения eval.
Eval-выражения
Некоторые рефлективные языки предоставляют возможность использования eval-выражений — выражений, которые распознают значение (обычно строку) как выражение. Такие утверждения — это самый мощный принцип рефлексии и даже метапрограммирования, но также и самый опасный, поскольку они представляют собой угрозу безопасности.
Рассмотрим следующий пример кода на Python, который принимает данные из стороннего источника в Сети (это одна из причин, по которой люди пользуются eval-выражениями):
Защита программы будет нарушена, если кто-то передаст в метод get_data() такую строку:
Для безопасного использования eval-утверждений нужно сильно ограничивать формат входных данных — и обычно это лишь занимает лишнее время.
Заключение
Интроспекция и рефлексия — это очень мощные инструменты современных языков, и их понимание может позволить вам писать по-настоящему крутой код. Ещё раз отметим: интроспекция — это изучение атрибутов объекта, а рефлексия — это манипуляция ими. Будьте внимательны при использовании рефлексии, поскольку она может сделать ваш код нечитаемым и уязвимым. Чем больше сила, тем больше и ответственность — вот девиз всего, что связано с метапрогрммированием.