JavaRush/Java блог/Random UA/Reflection API. Рефлексія. Темна сторона Java
Oleksandr Klymenko
13 рівень

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

Стаття з групи Random UA
учасників
Вітаю вас, молодий падан. У цій статті я розповім про Сил, міць якої 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 розробці бібліотеку Dagger. Після прочитання цієї статті ви з упевненістю можете вважати себе освіченим механізмами Reflection API. Темною стороною java рефлексія називається не дарма. Вона геть-чисто ламає парадигму ОВП. У Java інкапсуляція служить для приховування та обмеження доступу одних компонентів програми до інших. Використовуючи модифікатор private ми маємо на увазі, що доступ до цього поля буде тільки в межах класу, де це поле існує, ґрунтуючись на цьому, ми будуємо подальшу архітектуру програми. У цій статті ми побачабо, як за допомогою рефлексії можна пробиратися будь-куди. Хорошим прикладом у вигляді архітектурного рішення є шаблон проектування, що породжує.Singleton. Основна його ідея в тому, щоб протягом всієї роботи програми клас, що реалізує цей шаблон, був тільки в одному примірнику. Здійснюється це за допомогою установки конструктору за замовчуванням модифікатор private доступу. І буде дуже погано, якщо якийсь програміст зі своєю рефлексією плодитиме такі класи. До речі, є дуже цікаве питання, яке я нещодавно почув від свого співробітника: чи може бути у класу Singletonспадкоємці, що реалізує шаблон? Невже в цьому випадку неспроможна навіть рефлексія? Пишіть ваші feedback'и за статтею та відповідь у коментарі, а також ставте свої запитання! Справжня Сила Reflection API розкривається в комбінації з Runtime Annotations, про що ми, можливо, поговоримо в наступній статті про темну сторону Java. Дякую за увагу!
Коментарі
  • популярні
  • нові
  • старі
Щоб залишити коментар, потрібно ввійти в систему
Для цієї сторінки немає коментарів.