Рефлексия кода, reflection
Рефлексия (от reflexio — обращение назад) — это механизм исследования данных о программе во время её выполнения. Рефлексия в Java осуществляется с помощью Java Reflection API, состоящий из классов пакетов java.lang и java.lang.reflect. В информатике рефлексия означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения.
Java Reflection API позволяет получать информацию о конструкторах, методах и полях классов и выполнять следующие операции над полями и методами объекта/класса :
- определение класса объекта;
- получение информации о полях, методах, конструкторах и суперклассах;
- получение информации о модификаторах полей и методов;
- создание экземпляра класса, имя которого неизвестно до момента выполнения программы;
- определение и изменение значений свойств объекта/класса;
- вызов методов объекта/класса.
Примечание : в тексте используется объект/класс. При работе с объектом (реализацией класса) можно обращаться к полям и методам класса напрямую, если они доступны (не private). При работе с классом можно обращаться к методам класса с использованием Java Reflection API. Но класс необходимо получить из объекта.
Определение свойств класса
В работающем приложении для получения класса необходимо использовать метод forName (String className). Следующий код демонстрирует возможность создания класса без использования и с использованием Reflection :
// Без использования Reflection Foo foo = new Foo(); // С использованием Reflection Class foo = Class.forName("Foo"); // Загрузка JDBC-драйвера Class.forName("com.mysql.jdbc.Driver");
Метод класса forName(className) часто используется для загрузки JDBC-драйвера.
Методом getName() объекта Class можно получить наименование класса, включающего пакет (package) :
Class aclass = foo.getClass(); System.out.println (aclass.getName());
Для получения значения модификатора класса используется метод getModifiers(). Класс java.lang.reflect.Modifier содержит статические методы, возвращающие логическое значения проверки модификатора класса :
Class cls = foo.getClass(); int mods = cls.getModifiers(); if (Modifier.isPublic (mods)) < System.out.println("public"); >if (Modifier.isAbstract(mods)) < System.out.println("abstract");>if (Modifier.isFinal (mods))
Для получения суперкласса рефлексированного объекта (класса) необходимо использовать метод getSuperclass() :
Class cls = foo.getClass(); Class superCls = cls.getSuperClass();
Поскольку в Java отсутствует множественное наследование, то для получения всех предков следует рекурсивно вызвать метод getSuperclass() в цикле, пока не будет достигнут Object, являющийся родителем всех классов. Object не имеет родителей, поэтому вызов его метода getSuperclass() вернет null.
Определение интерфейсов и конструкторов класса
Для получения в режиме run-time списка реализующих классом интерфейсов, необходимо получить Class и использовать его метод getInterfaces(). В следующем примере извлекается список интерфейсов класса ArrayList :
Class cls = ArrayList.class; Class[] ifs = cls.getInterfaces(); System.out.println(«List of interfaces\n»); for(Class ifc : ifs)
Чтобы IDE (Eclipse) не предупреждала о необходимости определения типа класса
Class is a raw type. References to generic type Class should be parameterized
в коде были использованы generic’и. В консоль выводятся следующие интерфейсы, реализуемые классом ArrayList :
List of interfaces java.util.List java.util.RandomAccess java.lang.Cloneable java.io.Serializable
Метод класса getConstructors() позволяет получить массив открытых конструкторов типа java.lang.reflect.Constructor. После этого, можно извлекать информацию о типах параметров конструктора и генерируемых исключениях :
Class cls = obj.getClass(); Constructor[] constructors = cls.getConstructors(); for (Constructor constructor : constructors) < Class[] params = constructor.getParameterTypes(); for (Class param : params) < System.out.println(param.getName()); >>
Определение полей класса
Метод getFields() объекта Class возвращает массив открытых полей типа java.lang.reflect.Field, которые могут быть определены не только в данном классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Field позволяет получить имя поля, тип и модификаторы :
Class cls = obj.getClass(); Field[] fields = cls.getFields(); for (Field field : fields) < Classfld = field.getType(); System.out.println("Class name : " + field.getName()); System.out.println("Class type : " + fld.getName()); >
Если известно наименование поля, то можно получить о нем информацию с помощью метода getField() объекта Class.
Class cls = obj.getClass(); Field fld = cls.getField("fieldName");
Методы getField() и getFields() возвращают только открытые члены данных класса. Чтобы получить все поля класса, включая закрытые и защищенные, необходимо использовать методы getDeclaredField() и getDeclaredFields(). Данные методы работают точно так же, как и их аналоги getField() и getFields().
Определение значений полей класса
Класс Field содержит специализированные методы для получения значений примитивных типов: getInt(), getFloat(), getByte() и др. Для установки значения поля, используется метод set(). Для примитивных типов имеются методы setInt(), setFloat(), setByte() и др.
Class cls = obj.getClass(); Field field = cls.getField("fieldName"); String value = (String) field.get(obj); field.set(obj, "New value");
Ниже приведен пример изменения значения закрытого поля класса в режиме run-time.
Определение методов класса
Метод getMethods() объекта Class возвращает массив открытых методов типа java.lang.reflect.Method. Эти методы могут быть определены не только в классе, но также и в его родителях (суперклассе), либо интерфейсах, реализованных классом или его родителями. Класс Method позволяет получить имя метода, тип возвращаемого им значения, типы параметров метода, модификаторы и генерируемые исключения.
Class cls = obj.getClass(); Method[] methods = cls.getMethods(); for (Method method : methods) < System.out.println("Method name : " + method.getName()); System.out.println("Return type : " + method.getReturnType().getName()); Class[] params = method.getParameterTypes(); System.out.print("Parameters : "); for (Class paramType : params) < System.out.print(" " + paramType.getName()); >System.out.println(); >
Если известно имя метода и типы его параметров, то можно получить отдельный метод класса :
Class cls = obj.getClass(); Class[] params = new Class[] ; Method method = cls.getMethod("methodName", params);
Пример изменения значения закрытого поля класса
Чтобы изменить значение закрытого (private) поля класса необходимо получить это поле методом getDeclaredField () и вызвать метод setAccessible (true) объекта Field, чтобы открыть доступ к полю. После этого значение закрытого поля можно изменять, если оно не final. В следующем примере определен внутренний класс PrivateFinalFields с набором закрытых полей; одно из полей final. При создании объекта класса поля инициализируются. В методе main примера поочередно в закрытые поля вносятся изменения и свойства объекта выводятся в консоль.
import java.lang.reflect.Field; class PrivateFinalFields < private int i = 1; private final String s = "String S"; private String s2 = "String S2"; public String toString() < return "i = " + i + ", " + s + ", " + s2; >> public class ModifyngPrivateFields < public static void main(String[] args) throws Exception < PrivateFinalFields pf = new PrivateFinalFields(); Field f = pf.getClass().getDeclaredField("i"); f.setAccessible(true); f.setInt(pf, 47); System.out.println("1. " + pf); f = pf.getClass().getDeclaredField("s"); f.setAccessible(true); f.set(pf, "MODIFY S"); System.out.println("2. " + pf); f = pf.getClass().getDeclaredField("s2"); f.setAccessible(true); f.set(pf, "MODIFY S2"); f = pf.getClass().getDeclaredField("i"); f.setAccessible(true); f.setInt(pf, 35); System.out.println("3. " + pf); >>
В результате выполнения примера в консоль будут выведены следующие сообщения :
1. i = 47, String S, String S2 2. i = 47, String S, String S2 3. i = 35, String S, MODIFY S2
Из приведённого примера видно что поля 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 для печати их значений.
class Reflect < private String name; private int id; Reflect() < name = "Test"; >public int getId() < return id; >public void setId(int id) < this.id = id; >String getName() < return name; >public void setName(String name) < this.name = name; >public void setData(final int id, String name) < this.id = id; this.name = name; >@Override public String toString() < return "Reflect [ id : " + id + ", name : " + name + "]"; >>
Для тестирования объекта типа Reflect с помощью Java Reflection Api создадим класс ReflectionTest. В этот класс включим две процедуры getClassFields и getClassMethods, которые в режиме run-time распечатают всю информацию (описание полей и методов) о классе. Методы получают класс в качестве параметра. В процедурах сначала определяются массивы полей и методы; после этого их параметры распечатываются :
private void getClassFields(Class cls) < Field[] fields = cls.getDeclaredFields(); System.out.println("Class fields"); for (Field field : fields) < Classfld = field.getType(); System.out.println("Class name : " + field.getName()); System.out.println("Class type : " + fld.getName()); > > private void getClassMethods(Class cls) < Method[] methods = cls.getDeclaredMethods(); System.out.println("Class methods"); for (Method method : methods) < System.out.println("Method name : " + method.getName()); System.out.println("Return type : " + method.getReturnType().getName()); Class[] params = method.getParameterTypes(); System.out.print("Parameters : "); for (Class param : params) System.out.print(" " + param.getName()); System.out.println(); > >
В конструкторе класса ReflectionTest сначала вызываются процедуры определения полей и методов объекта/класса Reflect. После этого вызываются методы изменения значений и печати значений с использованием Reflection API. Для определения метода setData используется массив типов параметров. Вызов метода setData выполняется с передачей ему массива новых значений.
public class ReflectionTest < static Reflect reflect; public ReflectionTest() < getClassFields (reflect.getClass()); getClassMethods(reflect.getClass()); Classcls = reflect.getClass(); try < System.out.println("\n1. invoke method toString()\n"); Method method = cls.getMethod("toString"); System.out.println(method.invoke(reflect)); Class[] paramTypes; Object [] args; paramTypes = new Class[] ; method = cls.getMethod("setData", paramTypes); args = new Object[]; method.invoke(reflect, args); System.out.println("\n2. invoke method toString()\n"); method = cls.getMethod("toString"); System.out.println(method.invoke(reflect)); > catch (NoSuchMethodException e) < >catch (SecurityException e) < >catch (IllegalAccessException e) < >catch (IllegalArgumentException e) < >catch (InvocationTargetException e) < >> private void getClassFields(Class cls) < // код метода представлен выше >private void getClassMethods(Class cls) < // код метода представлен выше >public static void main(String[] args) < reflect = new Reflect(); new ReflectionTest(); System.exit(0); >>
В результате выполнения примера в консоль будут выведены представленные ниже сообщения. Методы setData и toString(), вызываемые с помощью Java Reflection API, вносят измнения в закрытые поля класса и распечатываются их значения.
Class fields Class name : name Class type : java.lang.String Class name : id Class type : int Class methods Method name : toString Return type : java.lang.String Parameters : Method name : getId Return type : int Parameters : Method name : setId Return type : void Parameters : int Method name : getName Return type : java.lang.String Parameters : Method name : setName Return type : void Parameters : java.lang.String Method name : setData Return type : void Parameters : int java.lang.String 1. invoke method toString() Reflect [ id : 999, name : Test] 2. invoke method toString() Reflect [ id : 123, name : New value]
Скачать пример
Исходный код рассмотренного примера вызова метода invoke с использованием Java Reflection API можно скачать здесь (989 байт).
Java Reflection API
Java Reflection API — это программный интерфейс в языке Java, который позволяет приложениям анализировать свои компоненты и программное окружение, изменять собственное поведение и структуру. Позволяет исследовать информацию о полях, методах и конструкторах классов.
Освойте профессию «Java-разработчик»
С помощью механизма рефлексии можно обрабатывать типы, которые отсутствовали при компиляции, но появились во время выполнения программы. Рефлексия и наличие логически целостной модели выдачи информации об ошибках позволяют создавать корректный динамический код.
Возможности
Помимо самомодификации, API способен проводить самопроверку и самоклонирование. Чаще всего рефлексию Java используют:
- для получения информации о классах, интерфейсах, функциях, конструкторах, методах и модулях;
- изменения имен функций и классов во время выполнения программы;
- создания новых экземпляров классов;
- анализа и исполнения кода, поступающего из программного окружения;
- преобразования классов из одного типа в другой;
- создания массивов данных и манипуляций с ними;
- установления значений полей объектов по именам;
- получения доступа к переменным и методам, включая приватные, и к внешним классам;
- вызова методов объектов по именам.
Профессия / 14 месяцев
Java-разработчик
Освойте востребованный язык
Особенности рефлексии в Java
Снижение производительности программы
Рефлексия работает медленнее, чем обычные приемы по обработке классов, методов и переменных. Это связано с тем, что во время динамического определения многих типов оптимизация производительности становится недоступной. Поэтому не следует применять Reflection API во фрагментах кода, которые часто используются приложением, в особенности если скорость выполнения программы — приоритет разработчика.
Блокировка диспетчером безопасности
Для запуска рефлексии в программировании требуется разрешение на выполнение, которое, как правило, не выдается при работе программного компонента под управлением Security Manager (диспетчера безопасности).
Станьте Java-разработчиком
и создавайте сложные сервисы
на востребованном языке
Уязвимость
При неверном использовании API способен нарушать один из главных принципов объектно-ориентированного программирования — инкапсуляцию данных. Это может привести к появлению потенциальных уязвимостей в веб-приложениях. В период с 2013 по 2016 год в библиотеке Reflection существовала брешь, которая позволяла хакерам обходить «песочницу» (изолированную зону для выполнения программ).
Нарушение переносимости программы
Поскольку Reflection API позволяет коду выполнять операции, которые обычно находятся под запретом, например получать доступ к закрытым полям и методам, использование рефлексии может сделать код неработоспособным и нарушить переносимость с одной операционной системы на другую. Кроме того, рефлексивный код нарушает абстракции, поэтому может изменить поведение программы при обновлении платформы.
Пример работы Reflection API в Java
Чтобы использовать Java Reflection API, не нужно подключать сторонние библиотеки. Все расположено в пакете java.lang.reflect.
Продемонстрируем некоторые методы рефлексии в программировании на конкретных примерах.
// Демонстрация работы Рефлексии в Java
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Constructor;
// Создание объектов для класса Sample
// Создаем приватное поле private
// Создаем публичный конструктор
// Создаем публичный метод без параметров
public void method1() System.out.println(«Информация в строке — » + s); >
// Создаем публичный метод с целым числом в качестве параметра
public void method2(int x) System.out.println(«Целое число — » + x);
>
// Создаем приватный метод
private void method3() System.out.println(«Вызов приватного метода»);
>
>
class Exercise
public static void main(String args[]) throws Exception
// Создаем объект для последующей проверки свойств
Sample obj = new Sample();
// Создаем новый объект класса из другого объекта
Class cls = obj.getClass();
System.out.println(«Имя класса — » +
cls.getName());
// Получаем имя конструктора класса с помощью объекта
Constructor constructor = cls.getConstructor();
System.out.println(«Имя конструктора — » +
constructor.getName());
System.out.println(«Это публичные методы классов: «);
// Получаем методы классов с помощью объектов
Method[] methods = cls.getMethods();
// Выводим имена методов
for (Method method:methods)
System.out.println(method.getName());
// Создаем объект нужного метода с помощью имени метода и параметра класса
Method methodcall1 = cls.getDeclaredMethod(«method2», int.class);
// Вызов метода во время исполнения
// Создаем объект нужного поля с помощью имени поля
Field field = cls.getDeclaredField(«s»);
// Открываем доступ к полю независимо от используемого в нем спецификатора доступа
// Устанавливаем новое значение поля
// Создаем объект метода с помощью имени метода
Method methodcall2 = cls.getDeclaredMethod(«method1»);
// Вызов метода во время исполнения
// Создаем третий объект метода с помощью имени метода
Method methodcall3 = cls.getDeclaredMethod(«method3»);
// Изменяем настройки доступа
// Вызов метода во время исполнения
Рефлексия — мощный инструмент, для правильного использования которого требуются высокая квалификация и взвешенный подход.
Java-разработчик
Java уже 20 лет в мировом топе языков программирования. На нем создают сложные финансовые сервисы, стриминги и маркетплейсы. Освойте технологии, которые нужны для backend-разработки, за 14 месяцев.
Заметки на полях о Java Reflection API
Всем привет, меня зовут Евгений Кузьменко, я Android-разработчик и сегодня хочу рассказать о некоторых интересных моментах, с которыми можно столкнуться при работе с Java Reflection (далее просто рефлексия). Хочу обратить ваше внимание, что это не вводная статья, а скорее набор заметок из личного опыта, о которых будет интересно узнать, а еще это полезно для чуточку большего понимания, что же там происходит «под капотом».
Стоит уточнить для молодых специалистов (а может и не только), чьи умы будоражит возможность доминировать, властвовать и унижать использовать рефлексию, что ее применение часто несет за собой двойной расход кармы, но бывают случаи, когда без всего этого не обойтись и просто необходимо ворваться в мир рантайма.
Теперь по традиции, несколько слов, что же это такое рефлексия и зачем это все вообще надо. Итак, рефлексия — это средство языка программирования Java, необходимое для получения информации о загруженных в память классах, объектах, интерфейсах и последующей работе с ними на этапе выполнения программы. Зачем это надо? Обработка метаинформации о классах, свойствах, методах, параметрах, посредством обработки аннотаций (привет Retrofit); создание прокси-объектов, например для модульного-тестирования; изменение состояния и/или поведения системы посредством модификации свойств объектов; создание экземпляров классов по заданному типу и многое другое.
Работа с классами через Reflection API
Основным классом для работы с Reflection API является java.lang.Class , экземпляр которого можно получить, например для java.lang.String , несколькими способами:
- посредством вызова метода на строковом литерале “abc”.getClass() ,
- используя конструкцию Class.forName(“java.lang.String”) ,
- через загрузчик классов,
- просто указав String.class .
Все это и можно условно считать отражением (рефлексией) класса String на класс java.lang.Class . Именно с его помощью мы можем получить всю информацию о загруженном классе такую как: методы класса и всей иерархии классов, реализованные интерфейсы, данные о полях класса, аннотации для которых указан @Retention(value= RetentionPolicy.RUNTIME) . Ну вроде бы все понятно и легко, класс мы получили дальше делай все, что душе пожелается, но тут закрался один хитрый момент. При попытке получить класс с помощью вызова метода Class.forName(“com.example.СlassName”) мы можем получить исключение ClassNotFoundException . Хотя мы на 100% уверены, что он присутствует в системе. Как такое может быть? Чтобы ответить на этот вопрос надо немного разобраться с процессом загрузки классов. Конечно подробное обсуждение выходит за рамки данной статьи, но вот основная и упрощенная идея. Есть три основных загрузчика классов, они вызываются иерархически в следующем порядке: системный загрузчик, загрузчик расширений, базовый загрузчик. При загрузке класса происходит поиск данного класса в кэше системного загрузчика, и в случае успешного поиска он возвращает искомый класс, в противном случае — делегирует вышестоящему в иерархии загрузчику. Если мы дошли до базового загрузчика, но в кэше так и не оказалось искомого класса, то в обратном порядке загрузчики пытаются загрузить его, передавая управление уже вниз по иерархии, пока класс не будет загружен, если класс не удалось найти и загрузить будет выброшено исключение ClassNotFoundException .
Теперь важно понять два момента:
- каждый загрузчик классов определяет свое пространство имен,
- может быть определен пользовательский загрузчик.
Логично, что пользовательский загрузчик тоже определяет собственное пространство имен для загружаемых классов. И вот тут и кроется ответ на наш вопрос, откуда же берется этот ClassNotFoundException , если класс загружен в память. Данный класс существует в другом пространстве имен, т.к. был загружен другим загрузчиком и возможно даже в другом процессе (привет WebViewChromium ). Так вот метод Class.forName(“com.example.ClassName”) всегда использует загрузчик, с помощью которого он был загружен и выполняет поиск по своему пространству имен. Строго говоря, если пользовательские загрузчики следуют модели делегирования, то через них могут загружаться и классы вышестоящих загрузчиков путем делегирования загрузки, ну а если они не следуют этой модели, то нам необходимо явно указывать загрузчик классов, используя перегруженный метод Class.forName(“com.example.className”, true, classLoader) .
Конкретно для Android-платформы мы также можем получить загрузчик классов другого приложения, используя следующий код:
Context someAppContext = context.createPackageContext( "com.package.SomeClass", Context.CONTEXT_INCLUDE_CODE|Context.CONTEXT_IGNORE_SECURITY); Class cl = Class.forName("com.package.SomeClass", true, someAppContext.getClassLoader());
или создать экземпляр загрузчика классов из файлов *.apk или *.jar, используя PathClassLoader, DexClassLoader . Пример приведен ниже:
String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "someName.jar"; PathClassLoader pathClassLoader = new PathClassLoader(dexPath, getClassLoader()); Class loadedClass1 = pathClassLoader.loadClass("com.example.loader.Class"); DexClassLoader dexClassLoader = new DexClassLoader(dexPath, getDir("dex", 0).getAbsolutePath(), null, getClassLoader()); Class loadedClass2 = dexClassLoader.loadClass("com.example.loader.Class");
Следует также вспомнить о вложенных классах и как такие классы загружать. Конечно, первое, что может прийти в голову — написать что-то вроде:
Class.forName(“com.example.OuterClass.NestedClass”);
Но правильно указать имя класса не получится, если не знать, как после компиляции будет выглядеть вложенный класс, а будет он иметь следующий вид com.example.OuterClass$NestedClass , а значит и загружен он будет точно также, т.е. чтоб такой класс загрузить нам нужно будет вызвать:
Class.forName(“com.example.OuterClass$NestedClass”)
Итак, мы загрузили класс, теперь проясним несколько моментов. Здесь главное понять вот что — getDeclaredMethod возвращает нам методы с любым спецификатором доступа и только для данного класса или интерфейса, а getMethod в свою очередь возвращает только публичные методы, но зато умеет искать методы в родительском классе. Вот и выходит, что универсальным решением выходит использование getDeclaredMethod , но с щепоткой рекурсии:
@Nullable public static Method getMethod(Class clazz, String methodName, Class. params) < if (clazz != null) < try < return clazz.getDeclaredMethod(methodName, params); >catch (NoSuchMethodException e) < return getMethod(clazz.getSuperclass(), methodName, params); >> return null; >
Этот же подход можно применить и к методам getField(. ) и getDeclaredField(. ) , т.к. они ведут себя точно также, только возвращают поля класса или интерфейса. Кстати о полях! Всем нам известно, что final поле не может быть изменено. Но мы можем это сделать с помощью рефлексии и вот пример кода:
void setStaticFinalField(Field field, Object newValue) throws Exception < field.setAccessible(true); // set private field as public Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(null, newValue); >
Для статической переменной мы можем передать null в качестве первого аргумента методу field.set(. ) , принимающего объект, в котором мы хотим провести изменения. Но вот незадача, если запустить этот код в приложении под Android, то он не будет работать. Но это легко исправить, достаточно заменить имя поля modifiers на accessFlags и final поля поддадутся даже на Андроиде. Ладно, должен признаться, что с final полями на самом деле все немного сложнее. Рассмотрим простой пример:
public class TestClass < public final int a; public final int b; public static final int c = 10; public TestClass(int a) < this.a = a; this.b = 5; >public void printA() < System.out.println("a = " + a); >public void printB() < System.out.println("b = " + b); >public void printC() < System.out.println("c = " + c); >> public class ReflectionTest < public static void main(String[] args) < try < TestClass test = new TestClass(1); System.out.println("before"); test.printA(); test.printB(); test.printC(); System.out.println("after"); setFinalField(TestClass.class.getField("a"), 2, test); test.printA(); setFinalField(TestClass.class.getField("b"), 7, test); test.printB(); setFinalField(TestClass.class.getField("c"), 100, null); test.printC(); >catch (Exception e) < e.printStackTrace(); >> static void setFinalField(Field field, Object newValue, Object receiver) throws Exception < Field modifiersField = Field.class.getDeclaredField("modifiers"); modifiersField.setAccessible(true); modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL); field.set(receiver, newValue); >>
Так вот после выполнения данного кода, в консоль будет выведено следующее:
before
a = 1
b = 5
c = 10
after
a = 2
b = 7
c = 10
И внимательный читатель заметит, что мы-то присвоили константе с значение 100 , но в выводе консоли значение как было 10 , так и осталось. Дело в том, что мы имеем дело с оптимизирующим компилятором javac, который с целью ускорения наших с вами программ, производит некие улучшения нашего кода. В данном случае компилятор пытается провести встраивание констант, которое работает для примитивных типов и java.lang.String . Что это значит? Если на этапе компиляции компилятор уверен, что это константа, и он точно знает ее значение (как в нашем случае с константой с ), то просто происходит замена обращения к этой константе на ее значение. Более наглядно это можно увидеть в байткоде. Смотрим, как выглядят методы printB() и printC() :
public printB()V L0 LINENUMBER 20 L0 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder. ()V LDC "b = " … public printC()V L0 LINENUMBER 24 L0 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; LDC "c = 10" …
Нас интересует инструкция LDC, вот здесь и тут можно о ней почитать. Как видим, в приведенном выше примере, в первом случае в пул констант помещается просто строка, а во втором случае уже строка со встроенным значением 10 , поэтому наши изменения с помощью рефлексии и не дают видимого результата. А что в Андроиде? А там все аналогично, ведь мы знаем, что сначала java классы компилируются с помощью javac и только потом в DEX байткод. JIT компилятор тоже может производить свои оптимизации на этапе выполнения программы, поэтому это тоже нужно держать в уме. Ну ладно, а что там с остальными final ссылочными типами, которые мы меняем с помощью рефлексии? Строго говоря, изменить final поле можно сразу после создания объекта и до того, как другие потоки получат на него ссылочку, в таком случае все будет гарантированно работать. Но ведь нам-то надо менять когда-то потом, и мы можем это сделать, и оно по идее будет работать, благодаря memory barrier. Ну и что касается Андроида, то, начиная с версии 4.0 (Ice Cream Sandwich), он должен следовать JSR-133(Java Memory Model).
Конечно, менять закрытые свойства объекта с помощью рефлексии это плохая идея, также как и вызывать его приватные методы, т.к. это с большой вероятностью повлияет на поведение всей системы и стабильность ее работы будет нарушена.
Proxy и InvocationHandler
Итак, мы подошли к еще одной интересной теме, а именно — генерации прокси-объектов. Начну с плохой новости — мы можем создать прокси только для интерфейса или набора интерфейсов. Вот простой пример кода:
Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader, new Class[] < Foo.class >, handler);
Хорошая новость — мы можем перехватывать вызовы методов данного прокси. А зачем это нужно, ведь мы же можем создать свой экземпляр интерфейса и добавить туда необходимую логику, допустим трассировку вызова методов! Да, разумеется, мы можем, но представьте, что нужно взять некий интерфейс, который существует только в рантайме и в исходном коде нет к нему доступа, да еще этот интерфейс содержит метод обратного вызова, и нам надо знать, когда он вызывается. Вот тут и пригодится нам Proxy с InvocationHandler . Вот пример создания InvocationHandler :
public class SampleInvocationHandler implements InvocationHandler < private Object obj; public SampleInvocationHandler(Object obj) < this.obj = obj; >public Object invoke(Object proxy, Method m, Object[] args). < if(m.getName().startsWith("get"))< System.out.println(". get Method Executing. "); >return m.invoke(obj, args); //return null; //bad idea > >
В данном примере метод invoke(. ) будет вызываться всякий раз при вызове любого метода нашего прокси-объекта. Здесь нужно обратить внимание на возвращаемое значение метода invoke(. ) . Мы не всегда можем располагать объектом obj , а если в интерфейсе, для которого мы сгенерировали прокси, всего один метод, который возвращает void , то может показаться хорошей идеей возвращать null в методе invoke(. ) . Но тут кроется ошибка, которая может проявить себя намного позже. Просто для сгенерированного прокси добавляются еще стандартные методы класса Object , т.к. все классы от него наследуются по умолчанию. И выходит, что допустим при вызове метода equals(. ) или toString() будет возвращаться null , и это приведет к ошибке времени выполнения!
Kotlin и рефлексия
Я думаю многие уже так или иначе присматривались к Kotlin, может даже уже и успели написать несколько приложений, используя его как основной язык программирования. Конечно компания JetBrains позаботилась о совместимости своего детища с Java, но что там с рефлексией? Ведь базовые типы отличаются у этих двух языков, у Kotlin базовый тип Any, а не Object. Да и если мы попытаемся выудить класс с помощью Int::class , то получим KClass… Но мы же только подключили Jackson(Gson. ) и хотим получать Class , а не KClass ! Успокойтесь, выход есть и даже несколько! Смотрим на пример:
val a = 1::class.java //int val b = 1::class.javaObjectType //class java.lang.Integer val c = 1::class.javaPrimitiveType //int val d = 1.javaClass //int
Так, давайте разбираться. В Kotlin все является объектом, а значит мы можем себе легко позволить написать что-то вроде 1::class, 1.2.compareTo(1) и т.д., и с этим все понятно. Теперь у нас с вами в распоряжении есть четыре способа получить класс, но в чем сила брат различие, спросите вы? Подробно разбирать, как происходит процесс маппинга классов Java в Kotlin и обратно мы не будем, т.к. на эту тему можно написать отдельную статью (кстати, может стоит ее написать?) просто рассмотрим вкратце отличия, чтоб было общее понимание. Итак 1::class.java всегда возвращает нам Class , который ассоциирован с данным типом/объектом на уровне стандартной библиотеки языка. Второй пример 1::class.javaObjectType вернет уже объектный/ссылочный тип, а не примитив. Ведь всем нам известно, что в языке Java есть примитивный тип int и ссылочный тип Integer, который так нам необходим для полноценной работы с коллекциями. Т.е. это свойство как раз и возвращает нам именно обертки для примитивных типов в Java. Третий вариант 1::class.javaPrimitiveType вернет снова int , тут важно понять вот что — Kotlin уже внутри содержит маппинг на примитивные типы Java и возвращает их. Если попытаться получить примитивный тип от String , то данное свойство вернет нам null . Четвертый способ быстро получить тип — это использовать 1.javaClass , он будет работать аналогично 1::class.java и, если посмотреть на исходный код данного свойства, то там просто происходит приведение текущего типа в java.lang.Object и взятие его класса с помощью метода getClass() .
Более детальную информацию можно получить в официальной документации, а также обратить внимание на описание содержимого пакета kotlin.reflect
Java 7 и новое API для непрямого вызова методов
Теперь две новости — хорошая и плохая. Начну с хорошей — есть альтернативный путь для непрямого вызова методов, не используя рефлексию, а плохая — разработчикам под платформу Андроид этот путь закрыт. Да, конечно, мы можем в проекте использовать switch со строками, ромбовидный оператор и это как бы Java 7, но все мы в душе понимаем, что это лишь «синтаксический обман», а что-то большее спрятано от нас. Вот это именно такой случай с пакетом java.lang.invoke. Android Studio даже будет специально игнорировать этот пакет, чтоб у нас не было соблазна его использовать. Если покопаться в исходниках Android, то можно наткнуться вот на это, а активность по коммитам показывает что работа идет. Вывод — Google работает над этим, ну а время покажет. Ладно, хватит об Андроиде, давайте попробуем разобраться, в чем же основная идея данного механизма вызова методов. Идея в том, что теперь можно получить типизированную ссылку на метод (конструктор, поле) — дескриптор метода. Чтоб было понятнее перейдем к примеру:
MethodHandles.Lookup lookup = MethodHandles.lookup(); MethodHandle toStrMH = lookup.findVirtual(Object.class,"toString", MethodType.methodType(String.class)); //String str = (String) toStrMH.invokeExact((Object) this); String str = (String) toStrMH.invoke(this);
MethodHandles.lookup() определяет контекст поиска метода. Т.е. определив его в своем классе, мы получаем доступ ко всем методам своего класса и к другим методам, к которым мы можем получить доступ непосредственно из нашего класса. Из этого выходит, что мы не можем получить доступ к закрытым методам системных классов, к которым могли бы достучаться через рефлексию. MethodHandle — это и есть дескриптор метода, который включает в себя неизменяемый экземпляр типа MethodType , содержащий возвращаемый тип и набор параметров данного метода. Ну и собственно с помощью методов invokeExact() и invoke() мы можем вызвать метод, на который и указывает MethodHandle . Отличаются они тем, что invokeExact() принимает в качестве аргумента объект именно того типа, который ожидает получить базовый метод, а в нашем случае это тип java.lang.Object . Метод invoke() менее строгий и может проводить дополнительные преобразования над аргументом, с целью подогнать его под необходимый тип. Конечно, нельзя не упомянуть о том, что это все стало возможным благодаря введению новой инструкции invokedynamic и для любознательных рекомендую посмотреть данный доклад.
Java 9
Как подсказали в комментариях к данной статье, в Java 9 появились модули. Что это и чем чревато для нашего кода, использующего рефлексию? Модуль — это именованный, самоописываемый набор кода и данных. С введением модулей, также расширяются правила организации доступа к исходному коду. Каждый модуль содержит файл module-info.java , в котором указаны имя модуля, список всех пакетов, которые считаются публичным API этого модуля и список модулей, от которых зависит данный модуль. Так вот важный момент в том, что публичные классы, которые содержатся в модуле, но не входят в публичный API этого модуля, т.е. находятся в других пакетах, которые не были объявлены в файле module-info.java как экспортируемые — не будут доступны за пределами этого модуля. И вот тут нам не поможет рефлексия. Но зато мы сможем в рантайме получать информацию о модуле, вызвав метод getModule() на экземпляре класса java.lang.Class , который соответствует необходимому нам классу. Здесь можно ознакомиться с так называемым базовым модулем, который будет доступен по умолчанию всем модулям, а значит и будет подвластен рефлексии.
Выводы
Конечно, была показана лишь часть того, что можно сделать с помощью рефлексии, но я постарался показать одни из самых интересных и не всегда очевидных моментов. Конечно, если Вы думаете, а не добавить ли себе в проект немного подобного кода, то скорее всего не стоит этого делать, т.к. в данном случае проект будет сильно зависим от закрытой части чужих библиотек, а скрытое API может часто меняться и с каждым таким изменением надо будет подпирать приложение очередным костылем. Также рефлексия гораздо медленнее прямых вызовов, а это значит, что производительности это точно не добавит в приложение. Ну и наконец это очень простой способ сломать логику работы сторонней библиотеки или Android-фреймворка, что может привести к трудно отслеживаемым ошибкам.
Почитать по теме
- docs.oracle.com/. jvms/se7/html/jvms-5.html
- developer.android.com/. stem/package-summary.html
- docs.oracle.com/. ml/jls-17.html#jls-17.5.3
- habrahabr.ru/post/133981
- openjdk.java.net/. c/jdk-modularization-tips
- openjdk.java.net/. ojects/jigsaw/spec/sotms
Все про українське ІТ в телеграмі — підписуйтеся на канал DOU
Reflection API в Java — Часть 1
В этой статье мы узнаем, что такое Рефлексия (Reflection) в Java, зачем она нам нужна, каковы её минусы, а также научимся базовой работе с ней.
Для начала нам необходимо будет разобрать немного теории.
Что такое Рефлексия?
Рефлексия — это API, который позволяет:
- получать информацию о переменных, методах внутри класса, о самом классе, его конструкторах, реализованных интерфейсах и т.д.;
- получать новый экземпляр класса;
- получать доступ ко всем переменным и методам, в том числе приватным;
- преобразовывать классы одного типа в другой (cast);
- делать все это во время исполнения программы (динамически, в Runtime).
Минусы Рефлексии
Как и у всего в этом мире, у Рефлексии есть свои недостатки:
- Худшая производительность в сравнении с классической работой с классами, методами и переменными;
- Ограничения безопасности. Если мы захотим использовать рефлексию на классе, который защищен с помощью специального класса SecurityManager, то ничего у не выйдет т.к. этот класс будет выбрасывать исключения каждый раз, как мы попытаемся получить доступ к закрытым членам класса. Такая защита может применяться, например, в Апплетах (Applets);
- Получение доступа к внутренностям класса, что нарушает принцип инкапсуляции. Фактически, мы получаем доступ туда, куда обычному человеку лезть не желательно. Это как с розеткой, ребёнку лучше к ней не лезть, тогда как опытный электрик запросто с ней поладит.
Что такое Класс класса, у кого он есть?
В Java есть специальный класс по имени Class, да-да, именно Class. Поэтому его и называют классом класса. С помощью него осуществляется работа с рефлексией, он и является входной точкой в мир рефлексии.
Class есть у :
- классов, интерфейсов, перечислений;
- примитивов и обёрток над ними;
- массивов;
- void. Да, ключевое слово void также имеет Class.
В общем, Class есть у всех объектов в Java.
А теперь перейдем к практике, для этого нам понадобится класс Car
package com . vertex . reflection ;
private int horsepower ;
public final String serialNumber ;
public Car ( ) <
serialNumber = "" ;
public Car ( int horsepower , String serialNumber ) <
this . horsepower = horsepower ;
this . serialNumber = serialNumber ;
public int getHorsepower ( ) <
return horsepower ;
void setHorsepower ( int horsepower ) <
this . horsepower = horsepower ;
protected void printSerialNumber ( ) <
System . out . println ( serialNumber ) ;
Как получить Класс класса?
Способ 1 — Сlass.forName(“имя.пакета.ИмяКласса”)
Class < ? >carClass = Class . forName ( «com.vertex.reflection.Car» ) ;
> catch ( ClassNotFoundException e ) <
e . printStackTrace ( ) ;
Вызов метода forName() необходимо обернуть в блок try-catch т.к. метод может бросить ClassNotFoundException, в случае если он не найдет класс с таким именем.
Способ 2 — метод getClass() у экземпляра класса
Car car = new Car ( ) ;
Class < ? extends Car >carClass = car . getClass ( ) ;
В этом случае оборачивать метод getClass() в блок try-catch нет необходимости т.к. мы вызываем этот метод у существующего класса, который видит компилятор. Но, к сожалению, компилятор не может знать тип переменной до конца, поэтому мы и имеем «? extends Car», как дженерик тип.
Способ 3 — ИмяКласса.class
Class < Car >carClass = Car . class ;
Здесь по той же причине не нужно использовать блок try-catch.
Если вы заметили, то чем больше мы знаем о классе, тем точнее будет тип класса при получении Класса класса.
В первом случае мы знаем только относительный путь к классу, это самый ненадёжный способ, поэтому и тип дженерика был Class. Ведь тип будет известен только после того, как мы запустим программу, компилятор этого знать не может наперёд.
Во втором случае мы получили Класс класса прямиком из экземпляра класса. Такой способ надёжнее, но он является не самым оптимальным. В этом случае компилятор не знает, экземпляр этого класса перед нами или его наследник, поэтому он подставляет тип Class.
В третьем случае мы прямо указываем из какого класса мы хотим получить Класс. Тут нет возможности ошибится в имени, нет возможности получить класс наследника класса Car, как это было возможно во втором случае. Компилятор точно знает, что это за тип класса. Этот способ является самым надёжным, поэтому и тип здесь Class.
Как получить информацию о переменных класса с помощью Рефлексии?
Получить информацию о переменных класса можно с помощью методов getDeclaredFields(), getDeclaredField() и getFields(), getField().
Пример 1 getDeclaredFields()
Метод возвращает все объявленные переменные в классе