User Oleksandr Klymenko
Oleksandr Klymenko
13 уровень
Харьков

Reflection API. Рефлексия. Темная сторона Java

Статья из группы Random
Приветствую вас, юный падаван. В этой статье я поведаю о Силе, мощь которой java-программисты используют только в, казалось бы, безвыходной ситуации. Итак, темная сторона Java — Reflection API
Reflection API. Рефлексия. Темная сторона Java - 1
Рефлексия в Java осуществляется с помощью Java Reflection API. Что такое эта рефлексия? Существует короткое и точное, а также популярное на просторах интернета определение. Рефлексия (от позднелат. reflexio — обращение назад) — это механизм исследования данных о программе во время её выполнения. Рефлексия позволяет исследовать информацию о полях, методах и конструкторах классов. Сам же механизм рефлексии позволяет обрабатывать типы, отсутствующие при компиляции, но появившиеся во время выполнения программы. Рефлексия и наличие логически целостной модели выдачи информации об ошибках дает возможность создавать корректный динамический код. Иначе говоря, понимание принципов работы рефлексии в java открывает перед вами ряд удивительных возможностей. Вы буквально можете жонглировать классами и их составляющими.
Reflection API. Рефлексия. Темная сторона Java - 2
Вот основной список того, что позволяет рефлексия:
  • Узнать/определить класс объекта;
  • Получить информацию о модификаторах класса, полях, методах, константах, конструкторах и суперклассах;
  • Выяснить, какие методы принадлежат реализуемому интерфейсу/интерфейсам;
  • Создать экземпляр класса, причем имя класса неизвестно до момента выполнения программы;
  • Получить и установить значение поля объекта по имени;
  • Вызвать метод объекта по имени.
Рефлексия используется практически во всех современных технологиях Java. Сложно себе представить, могла бы Java, как платформа, достигнуть такого огромного распространения без рефлексии. Скорее всего не смогла бы. С общим теоретическим представлением о рефлексии вы ознакомились, теперь приступим к ее практическому применению! Не будем изучать все методы Reflection API, только то, что реально встречается на практике. Так как механизм рефлексии подразумевает работу с классами, то и у нас будет простой класс — MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Как мы видим, это самый обычный класс. Конструктор с параметрами закомментирован не просто так, мы к этому еще вернемся. Если вы внимательно просмотрели содержимое класса, то наверняка увидели отсутствие getter’a для поля name. Само поле name помечено модификатором доступа private, обратиться к нему вне самого класса у нас не выйдет => мы не можем получить его значение. “Так в чем проблема? — скажете вы. — Допиши getter или измени модификатор доступа”. И вы будете правы, но, что если MyClass находится в скомпилированной aar библиотеке или в другом закрытом модуле без доступа к редактированию, а на практике такое случается крайне часто. И какой-то невнимательный программист просто забыл написать getter. Самое время вспомнить о рефлексии! Попробуем добраться до private поля name класса MyClass:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //no getter =(
   System.out.println(number + name);//output 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name);//output 0default
}
Разберем что тут сейчас произошло. В java есть замечательный класс Class. Он представляет классы и интерфейсы в исполняемом приложении Java. Связь между Class и ClassLoader мы затрагивать не будем, т.к. это не есть тема статьи. Далее, чтобы получить поля этого класса нужно вызвать метод getFields(), этот метод вернет нам все доступные поля класса. Нам это не подходит, так как наше поле private, поэтому используем метод getDeclaredFields(), этот метод также возвращает массив полей класса, но теперь и private и protected. В нашей ситуации мы знаем имя поля, которое нас интересует, и можем использовать метод getDeclaredField(String), где String — имя нужного поля. Примечание: getFields() и getDeclaredFields() не возвращают поля класса-родителя! Отлично, мы получили объект Field с ссылкой на наш name. Т.к. поле не было публичным (public) следует дать доступ для работы с ним. Метод setAccessible(true) разрешает нам дальнейшую работу. Теперь поле name полностью под нашим контролем! Получить его значение можно вызовом get(Object) у объекта Field, где Object — экземпляр нашего класса MyClass. Приводим к типу String и присваиваем нашей переменной name. На тот случай если у нас вдруг не оказалось setter’a, для установки нового значения полю name можно использовать метод set:

field.set(myClass, (String) "new value");
Поздравляю! Вы только что овладели базовым механизмом рефлексии и смогли получить доступ к private полю! Обратите внимание на блок try/catch и типы обрабатываемых исключений. IDE сама укажет на их обязательное присутствие, но по их названию итак ясно зачем они здесь. Идем дальше! Как вы могли заметить, наш MyClass уже имеет метод для вывода информации о данных класса:

private void printData(){
       System.out.println(number + name);
   }
Но этот программист и тут наследил. Метод находится под модификатором доступа private, и нам пришлось самим каждый раз писать код вывода. Не порядок, где там наша рефлексия?… Напишем вот такую функцию:

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
Здесь примерно такая же процедура как и с получением поля — получаем нужный метод по имени и даем доступ к нему. И для вызова объекта Method используем invoke(Оbject, Args), где Оbject — все также экземпляр класса MyClass. Args — аргументы метода — наш таковых не имеет. Теперь для вывода информации мы используем функцию printData:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // outout 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// output 0new value
}
Ура, теперь у нас есть доступ к приватному методу класса. Но что делать если у метода все таки будут аргументы, и зачем тот закомментированный конструктор? Всему свое время. Из определения в начале ясно, что рефлексия позволяет создавать экземпляры класса в режиме runtime (во время выполнения программы)! Мы можем создать объект класса по полному имени этого класса. Полное имя класса — это имя класса, учитывая путь к нему в package.
Reflection API. Рефлексия. Темная сторона Java - 3
В моей иерархии package полным именем MyClass будет “reflection.MyClass”. Также узнать имя класса можно простым способом (вернет имя класса в виде строки):

MyClass.class.getName()
Создадим экземпляр класса с помощью рефлексии:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
На момент старта java приложения далеко не все классы оказываются загруженными в JVM. Если в вашем коде нет обращения к классу MyClass, то тот, кто отвечает за загрузку классов в JVM, а им является ClassLoader, никогда его туда и не загрузит. Поэтому нужно заставить ClassLoader загрузить его и получить описание нашего класса в виде переменной типа Class. Для этой задачи существует метод forName(String), где String — имя класса, описание которого нам требуется. Получив Сlass, вызов метода newInstance() вернет Object, который будет создан по тому самому описанию. Остается привести этот объект к нашему классу MyClass. Круто! Было сложно, но, надеюсь, понятно. Теперь мы умеем создавать экземпляр класса буквально из одной строки! К сожалению описанный способ будет работать только с конструктором по умолчанию (без параметров). Как же вызывать методы с аргументами и конструкторы с параметрами? Самое время раскомментировать наш конструктор. Как и ожидалось, newInstance() не находит конструктор по умолчанию и больше не работает. Перепишем создание экземпляра класса:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);//output created object reflection.MyClass@60e53b93
}
Для получения конструкторов класса следует у описания класса вызвать метод getConstructors(), а для получения параметров конструктора - getParameterTypes():

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Таким образом получаем все конструкторы и все параметры к ним. В моем примере идет обращение к конкретному конструктору с конкретными уже известными параметрами. И для вызова этого конструктора используем метод newInstance, в котором указываем значения этим параметрам. Точно так же будет и с invoke для вызова методов. Возникает вопрос: где может пригодится рефлексивный вызов конструкторов? Современные технологии java, как уже говорилось в начале, не обходятся без Reflection API. Например, DI (Dependency Injection), где аннотации в сочетании с рефлексией методов и конструкторов образуют популярную в Android разработке библиотеку Darer. После прочтения этой статьи вы с уверенностью можете считать себя просвещенным в механизмы Reflection API. Темной стороной java рефлексия называется не зря. Она напрочь ломает парадигму ООП. В java инкапсуляция служит для сокрытия и ограничения доступа одних компонентов программы к другим. Используя модификатор private мы подразумеваем, что доступ к этому полю будет только в пределах класса, где это поле существует, основываясь на этом мы строим дальнейшую архитектуру программы. В этой статье мы увидели, как с помощью рефлексии можно пробираться куда угодно. Хорошим примером в виде архитектурного решения является порождающий шаблон проектирования — Singleton. Основная его идея в том, чтобы на протяжении всей работы программы класс, реализующий этот шаблон был только в одном экземпляре. Осуществляется это при помощи установки конструктору по умолчанию private модификатор доступа. И будет очень нехорошо, если какой-то программист со своей рефлексией будет плодить такие классы. Кстати, есть очень интересный вопрос, который я недавно услышал от своего сотрудника: может ли быть у класса, реализующий шаблон Singleton, наследники? Неужели в этом случае бессильна даже рефлексия? Пишите ваши feedback’и по статье и ответ в коментарии, а также задавайте свои вопросы! Истинная Сила Reflection API раскрывается в комбинации c Runtime Annotations, о чем мы, возможно, поговорим в следующей статье про темную сторону Java. Спасибо за внимание!
Комментарии (100)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Oleg Уровень 51
11 октября 2021
newInstance() с 9 версии Java @Deprecated
SerVer4675@gmail.com Уровень 32, Киев
2 июня 2021
Вот такой ваш код:

String name = (String) field.get(myClass);
не рабоатет, получаю ошибку А вот так вот:

String name = field.get(myClass).toString();
сработало без ошибок Полчаса мучился, пока получил нужный результат :)
Бац Андрей Уровень 20
11 мая 2021
Тут попроще описано https://vertex-academy.com/tutorials/ru/reflection-api-v-java-chast1/ https://vertex-academy.com/tutorials/ru/reflection-api-v-java-class-field/
Ivan Уровень 22, Санкт-Петербург
5 апреля 2021
Очень тяжелая для меня статья... Ну, или тема сложная. Полдня просидел. Меня сбивали с толку следующие формулировки: "нужно вызвать метод getFields(), этот метод вернет нам все доступные поля класса"; "используем метод getDeclaredFields(), этот метод также возвращает массив полей класса"; "Здесь примерно такая же процедура как и с получением поля — получаем нужный метод по имени и даем доступ к нему" и т.д. На мой взгляд, правильнее сказать, что возвращаются здесь не поля или методы, а объекты типа Field или Method, которые содержат в себе всего лишь описания полей и методов. В свою очередь, значение поля мы получаем при помощи метода класса Field: field.get(объект), а метод вызываем при помощи метода класса Method: method.invoke(объект). В скобках передаем объект, у которого получаем значение поля или вызываем метод. То же самое с классами и конструкторами - возвращаются объекты типа Class и Constructor, содержащие описания классов и конструкторов. Метод newInstance() можно вызвать и у объекта типа Class, и у объекта типа Constructor. В первом случае объект создается с помощью конструктора без параметров, а во втором можно передать параметры конструктору.
Alexandr Уровень 23, Минск, Беларусь
1 апреля 2021
Отличная подача информации. Всё хорошо разложено по полочкам. Спасибо большое.
barracuda Уровень 41, Санкт-Петербург, Россия Expert
2 ноября 2020
Еще раз перечитал спустя время. Спасибо!
Кунг-Фу-Панда Уровень 36, Кунг-Фу-Сити
6 октября 2020
Отличная статья. Спасибо. До этого читал другой источник, чуть мозг не сломал, думал не моё! А оказывается все просто после Вашей подачи материала!
Alexander Iljushkin Уровень 16, Санкт-Петербург
22 сентября 2020
> И какой-то невнимательный программист просто забыл написать getter. Самое время вспомнить о рефлексии! Не советовал бы разработчикам такое говорить. Это же monkey patching. Самое время взглянуть на эту практику в перспективе и умножить это изменение на 1000. В результате мы видим лапшекод, где все перемешано с рефлексией и самодополняется динамически и вообще невозможно сказать, что в итоге класс делает. Предлагаю вместо этого почитать про доменно-ориентированное проектирование, где описываются такие ситуации работы с общим ядром. В случае когда это внешний апи и повлиять на него сложно, вам надо этот кусок апи забрать к себе и править у себя, а не патчить код в рантайме через рефлексию.
Павел Уровень 31, Санкт-Петербург, Россия
28 августа 2020
А можно привести весь код целиком вместе с импортируемыми пактами
Vhodnoylogin Уровень 2, Москва, Россия
15 мая 2020
Скажите, пожалуйста, кто-нибудь уже реализовал билдер классов на аннотациях?