Статьи

API отражения Java

Если вы когда-нибудь задавали себе такие вопросы:
— «Как вызвать метод, имеющий только имя в строке?»
— «Как мне динамически перечислить все атрибуты в классе?»
— «Как мне написать метод, который сбрасывает состояние любого данного объекта до значений по умолчанию?»

Тогда вы, вероятно, уже слышали о API рефлексии Java, и, если вы этого не сделали, это хорошая возможность увидеть, что это такое и для чего его можно использовать. Эта функция действительно мощная, но, как всегда, ее следует использовать с хорошим здравым смыслом.

Преимущество, которое она приносит в таблицу, заключается в возможности анализировать информацию, касающуюся класса, его атрибутов, методов, аннотаций или даже состояния экземпляра, и все это во время выполнения. Динамизм, полученный этим, звучит действительно полезно, не так ли?

В этом уроке я собираюсь показать базовое использование Reflection API. Что можно сделать, что не следует делать, преимущества и недостатки.

Итак … Должны ли мы?

Начальная точка, класс Class <T>

При работе с Reflection одной из самых важных вещей является знание того, с чего начать, знание того, какой класс дает нам доступ ко всей этой информации. И ответ на это: java.lang.Class <T>

Предположим, у нас есть следующий класс:

1
2
3
4
5
6
7
8
package com.pkg;
 
public class MyClass{
 
   private int number;
   private String text;
 
}

Мы можем получить ссылку на класс 3 разными способами. Непосредственно из класса, из его имени или из экземпляра:

1
2
3
4
5
Class<?> clazz1 = MyClass.class;
Class<?> clazz2 = Class.forName("com.pkg.MyClass");
 
MyClass instance = new MyClass();
Class<?> clazz3 = instance.getClass();

Совет : здесь мы видим важную деталь. Обычно называют идентификаторы экземпляра Class как-то вроде clazz или clz, это может показаться странным, но это только потому, что class уже является зарезервированным словом в языке Java.

Из справочника класса мы можем перемещаться по всему, выяснить, что это за члены, аннотации, даже пакет или ClassLoader, но об этом мы подробнее поговорим позже, поэтому сейчас давайте сосредоточимся на методах которые дают нам информацию о самом классе:

int getModifiers () Возвращает модификаторы класса или интерфейса внутри int, чтобы узнать, какие именно модификаторы применяются, мы должны использовать статические методы, предоставляемые классом Modifier.
логическое значение isArray () Определяет, представляет ли класс массив
логический isEnum () Определяет, был ли класс объявлен как enum в исходном коде
логическое значение isInstance (Object obj) Возвращает true, если информированный объект может быть назначен объекту типа, представленного этим классом
логическое isInterface () Определяет, представляет ли класс интерфейс
логическое значение isPrimitive () Определяет, представляет ли класс примитивный тип
T newInstance () Создает новый экземпляр этого класса
Класс <? супер T> getSuperclass () Возвращает ссылку на суперкласс, в случае, если она вызывается в классе Object, она возвращает нуль

Вы можете увидеть полное определение этих методов и многих других непосредственно в документации класса.

Совет : чтобы успешно использовать методы в классе Modifier , необходимо иметь базовые знания о побитовых операциях. Будучи самой распространенной в этом случае операцией AND .

Атрибуты, класс Field

Поля представляют атрибуты класса, это так просто. Именно через этот класс мы можем получить информацию о них, но до этого, как мы получаем ссылку на Поле ?

Внутри класса Class у нас есть несколько различных методов, которые могут возвращать нам поля класса, в качестве спойлера. Я уже говорил, что существуют эквивалентные методы для методов и конструкторов , но сначала давайте сосредоточимся на атрибутах .

У нас есть несколько «важных для запоминания» вещей, связанных с тем, как узнать членов класса, я сначала представлю методы, а затем более подробно остановлюсь на этих деталях.

Поле getField (String name) Возвращает поле, которое соответствует общедоступному атрибуту класса с заданным именем.
Field [] getFields () Возвращает массив полей , отражающих открытые атрибуты класса.
Поле getDeclaredField (String name) Возвращает поле, которое соответствует объявленному атрибуту класса с заданным именем.
Field [] getDeclaredFields () Возвращает массив полей, которые отражают объявленные атрибуты класса.

Хорошо, похоже, у нас есть 2 группы очень похожих методов, и мы делаем. Различия между ними тонкие, и не многие их знают.

Методы getField и getFields возвращают только открытые атрибуты класса. Это как-то чище, потому что мы не всегда хотим (и часто не должны) вмешиваться во внутренние атрибуты. Преимущество здесь в том, что он все еще ищет атрибуты в суперклассе, поднимаясь по иерархии , что удобно для нас, но опять же, атрибуты в суперклассах должны быть общедоступными, чтобы их можно было найти.

Теперь методы getDeclaredField и getDeclaredFields не так чисты, для них любой объявленный атрибут является допустимым, может ли он быть закрытым, защищенным или как угодно. Таким образом, вопреки распространенному мнению, доступ к частным пользователям через Reflection не так сложен. И, как еще одно отличие, они не ищут атрибуты в суперклассах, поэтому они полностью игнорируют иерархию наследования .

Давайте проиллюстрируем попытку доступа к атрибуту внутри класса, демонстрируя использование вышеупомянутых методов и других, предоставляемых классом Field . Используя в качестве примера класс MyClass, который мы определили ранее, давайте предположим, что мы хотим знать значение, содержащееся в текстовом атрибуте некоторого экземпляра.

1
2
3
4
5
6
MyClass instance = new MyClass();
instance.setText("Reflective Extraction");
Class<?> clazz = instance.getClass();
Field field = clazz.getDeclaredField("text"); // Accessing private attribute
Object value = field.get(instance);
System.out.println(value);

Подсказка : использование метода get может сначала сбить с толку, но это довольно просто. Имея поле в ваших руках, вы отправляете целевой экземпляр, из которого вы хотите извлечь значение в качестве аргумента. Необходимо иметь в виду, что Поле относится к классу, а не к экземпляру, поэтому, если атрибут не является статическим, требуется конкретный экземпляр, чтобы можно было извлечь значение. В случае, если мы говорим о статическом атрибуте, вы можете передать любой экземпляр любого типа или даже ноль в качестве аргумента.

Итак, мы получили доступ к нашему атрибуту с помощью метода getDeclaredField , передав имя поля и получив его текущее значение с помощью метода get . Все сделано, верно?

Неправильно. Когда мы запускаем наш код, мы видим, что было сгенерировано исключение, точнее, IllegalAccessException , не только потому, что у нас есть ссылка на поле, мы можем манипулировать им по своей воле, но есть и способ обойти это.

Суперклассом Field является класс AccessibleObject, который также является суперклассом метода и конструктора , поэтому это объяснение действительно для любого из 3.

Класс AccessibleObject определяет 2 основных метода: setAccessible ( boolean ) и isAccessible () , которые в основном определяют, доступен ли атрибут или нет. В нашем случае закрытые атрибуты никогда не будут доступны, если вы не находитесь в одном классе, в этом случае вы можете получить к ним доступ без каких-либо проблем. Поэтому все, что нам нужно сделать, это настроить наш текст как доступный.

1
2
3
4
5
6
7
MyClass instance = new MyClass();
instance.setText("Reflective Extraction");
Class<?> clazz = instance.getClass();
Field field = clazz.getDeclaredField("text"); // Accessing private attribute
field.setAccessible(true); // Setting as accessible
Object value = field.get(instance);
System.out.println(value);

И там мы идем, мы можем напечатать нашу ценность без хлопот

Совет : класс AccessibleObject имеет удобный статический метод, также называемый setAccessible, но он получает массив AccessibleObject и логический флаг. Этот метод может быть использован для установки нескольких полей, методов или конструкторов как доступных одновременно. Если вы хотите установить все атрибуты в классе как доступные, вы можете просто сделать это:

1
2
3
4
MyClass instance = new MyClass();
Class<?> clazz = instance.getClass();
Field[] fields = clazz.getDeclaredFields();
AccessibleObject.setAccessible(fields, true);

И таким же образом мы извлекаем значение из атрибута, мы также можем присвоить его. Мы делаем это, используя метод set. Когда мы вызываем его, нам нужно сообщить экземпляру, который мы хотим изменить, и значению, которое мы хотим установить для соответствующего атрибута.

1
2
3
4
5
6
MyClass instance = new MyClass();
Class<?> clazz = instance.getClass();
Field field = clazz.getDeclaredField("text");
field.setAccessible(true);
field.set(instance, "Reflective attribution");
System.out.println(instance.getText());

Действия, класс Method

Прежде чем мы начнем с реальных методов, я хочу подчеркнуть, как я уже говорил, что он очень похож на классы Field и Constructor , поэтому эти 3 могут быть получены таким же образом из нашей ссылки на класс , что означает, что если у нас есть метод getDeclaredField, у нас также есть getDeclaredMethod и getDeclaredConstructor (то же самое верно и для других методов). Таким образом, они работают точно так же, и поэтому объяснять их снова было бы бессмысленно и трата времени.

Итак, начнем, как мы можем вызывать методы, используя Reflection?

Метод не похож на атрибут, поэтому только его имени недостаточно, потому что у нас может быть много разных методов с одним и тем же именем, что называется перегрузкой. Таким образом, чтобы получить конкретный метод, нам нужно сообщить его имя и список аргументов, которые он получает. Давайте предположим, что наш класс имеет методы, описанные ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public void print(String text){
    System.out.println(text);
}
 
public void printHelloWorld(){
    System.out.println("Hello World");
}
 
public int sum(int[] values){
    int sum = 0;
    for(int n : values){
        sum += n;
    }
    return sum;
}

Чтобы получить ссылку на эти методы, в порядке их объявления в примере, мы можем просто сделать это:

1
2
3
4
5
MyClass instance = new MyClass();
Class<?> clazz = instance.getClass();
Method print = clazz.getMethod("print", String.class);
Method printHelloWorld = clazz.getMethod("printHelloWorld");
Method sum = clazz.getMethod("sum", int[].class);

И чтобы вызвать их, мы используем метод invoke (совпадение? )

Подпись метода такова:

1
Object invoke(Object obj, Object... args)

Это означает, что мы должны сообщить экземпляру (цели), для которого будет вызван метод, а также аргументы, которые нам нужно передать ему, если таковые имеются. Кроме того, он возвращает объект , который будет возвращать метод, каким бы он ни был. Если метод ничего не возвращает ( void ), вызов invoke вернет null .

Для наших трех ссылок на методы, приведенных выше, вызовы будут такими:

1
2
3
4
print.invoke(instance, "I'm Mr. Reflection by now");
printHelloWorld.invoke(instance);
int sumValue = (int) sum.invoke(instance, new int[]{1, 4, 10});
System.out.println(sumValue);

Совет: опять же, если мы работаем со статическими методами, нет необходимости передавать действительный экземпляр, мы можем просто сделать это:

1
staticMethod.invoke(null);

Как насчет общих аргументов?

Что ж, в этом случае нам нужно немного больше понять язык, прежде чем мы сможем их вызвать. Необходимо знать, что универсальные типы существуют только во время компиляции, и, поскольку Reflection выполняется во время выполнения, универсальные типы больше не существуют, и все это из-за функции, называемой стиранием типов , которая была создана для обеспечения обратной совместимости с предыдущими версиями Java.

Скорее всего, метод, который получает универсальный аргумент, получит Object , но есть и другая возможность. Если вы объявляете универсальный тип, подобный следующему: <T расширяет CharSequence> , возможно, что метод во время выполнения получит CharSequence , поскольку это наиболее конкретный возможный тип, все еще оставаясь универсальным, поэтому для этого метода:

1
2
3
public  print(T sequence){
    System.out.println(sequence)
}

Вызов Reflection может выглядеть так:

1
2
Method print = clazz.getMethod(print, CharSequence.class);
print.invoke(instance);

Создание экземпляров класса Constructor <T>

Как все мы знаем, конструктор не является методом, хотя его использование очень похоже. Это похоже на то, что мы вызываем метод, но за ним стоит ключевое слово new , а также мы вызываем его только тогда, когда мы хотим создать новый экземпляр класса. Это сходство в использовании вызывает сходство в манипуляциях с Reflection. Так получилось, что вместо использования invoke мы собираемся использовать метод newInstance с некоторыми отличиями.

Мы создаем экземпляр, поэтому нет экземпляра, который можно связать с конструктором, поэтому мы не передаем его в качестве аргумента. Но, как и в случае с нашими методами , passamos somente a lista de argumentsmentos.

Класс <T> также имеет метод newInstance , но он был бы полезен только в том случае, если бы у нашего класса был доступный конструктор без аргументов, в случае его отсутствия нам нужна прямая ссылка на наш конструктор <T> . Давайте определим несколько конструкторов в нашем примере класса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class MyClass{
 
    private String text;
    private int number;
 
    public MyClass(){
 
    }
 
    public MyClass(String text){
        this.text = text;
    }
 
    public MyClass(String text, int number){
        this.text = text;
        this.number = number;
    }
 
}

Теперь давайте получим каждый из них, используя Reflection:

1
2
3
4
Class clazz = MyClass.class;
Constructor c1 = clazz.getConstructor();
Constructor c2 = clazz.getConstructor(String.class);
Constructor c3 = clazz.getConstructor(String.class, int.class);

Теперь давайте создадим 3 экземпляра, по одному из каждой ссылки на конструктор:

1
2
3
MyClass instance1 = c1.newInstance();
MyClass instance2 = c2.newInstance("text");
MyClass instance3 = c3.newInstance("other text", 1);

Продвинувшись так далеко, мы можем видеть, что мы можем манипулировать классом практически любым способом, перечисляя его атрибуты, получая ссылки на методы, изменяя его состояние, читая информацию, но мы все еще упускаем одну вещь, которая, на мой взгляд, слишком круто не говоря уже

Метаданные, интерфейс AnnotatedElement

Когда нам нужно проверить информацию, связанную с аннотациями, мы используем методы, предоставляемые интерфейсом AnnotatedElement . К сведению : первым классом в иерархии, который реализует эти методы, является AccessibleObject , поэтому все его подклассы будут иметь к ним доступ.

С помощью аннотаций мы можем определять информацию о наших классах, их атрибутах и ​​/ или методах, без написания кода выполнения. Аннотация будет обработана в другое время, что облегчит разработку. Итак, давайте вернемся к этому и напишем небольшой пример:

Мы создаем 1 аннотацию с именем @NotNull , и всякий раз, когда атрибут помечается им, он не может содержать значение null », — сказал nuff. Давайте перейдем к коду ::

1
2
3
4
5
6
public class MyClass{
 
    @NotNull
    private String text;
 
}

Итак, мы определили эту характеристику для нашего текстового атрибута, но как мы будем эффективно проверять это правило? С нашим классом Validator , вот так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class Validator{
 
    public static void validate(Object target) throws IllegalArgumentException, IllegalAccessException{
        Class<?> clazz = target.getClass();
        Field[] fields = clazz.getDeclaredFields();
 
        for (Field field : fields){
            validateField(field, target);
        }
    }
 
    private static void validateField(Field field, Object target) throws IllegalArgumentException, IllegalAccessException{
        field.setAccessible(true);
        Object value = field.get(target);
        if (field.isAnnotationPresent(NotNull.class)){
            if (value == null)
                throw new IllegalArgumentException("This attribute cannot be null");
        }
    }
}

Поэтому, когда мы проверяем наши объекты, мы можем использовать эту аннотацию, чтобы она работала на нас, и наш валидатор проверит ее, сохранив код в одном месте, сделав его намного более удобным в обслуживании. В какой-то момент нашего кода у нас будет такой вызов:

1
Validator.validate(myClassInstance);

И мы сделали. Этот метод широко используется при разработке фреймворков, и вы можете просто проверить документацию, чтобы увидеть, что будет делать каждая аннотация, и использовать их соответствующим образом.

Недостатки, почему я не должен использовать Reflection?

Я считаю, что было ясно, что использование рефлексии в некоторых ситуациях приносит нам много преимуществ и удобств, но, как мы все знаем, когда все, что у нас есть — молоток, любая проблема выглядит как гвоздь, так что не стоит волноваться, думая о как решить все с помощью Reflection, потому что у него есть свои недостатки:

  • Повышение производительности : при использовании рефлексии JVM необходимо проделать большую работу, чтобы получить всю необходимую нам информацию, выполнить динамические вызовы и все такое, так что это требует определенных затрат времени на обработку.
  • Безопасность во время выполнения : для запуска кода Reflection необходимо иметь определенный уровень прозрачности внутри виртуальной машины, и у вас может не быть этого все время, так что имейте это в виду, думая об его использовании.
  • Модель безопасности : наши атрибуты имеют различную видимость по причине, верно? И отражение может полностью игнорировать их, делая все, что захочет, так что это может вызвать некоторые предупреждения в вашей инкапсуляции.

Основное правило: используйте Reflection только тогда, когда нет более легкой альтернативы. Если вы можете сделать что-то без размышлений, вы, вероятно, должны. Вы должны анализировать в каждом конкретном случае.

Более подробную информацию можно получить в руководстве по Oracle .

Я знаю, что в Reflection есть и другие особенности, такие как Прокси , но они гораздо более сложные и сложные. Я могу написать о них в будущем посте, но он выходит за рамки этого, поскольку он предназначен только для ознакомления с предметом или повышения квалификации для тех, кто уже знал это, поэтому поднимая сложные темы принесет больше вреда, чем пользы.

Надеюсь, вам всем понравилось, увидимся в следующий раз!

Ссылка: Java Reflection API от нашего партнера JCG