Статьи

Как эффективно использовать Reflection

Эта статья является частью нашего Академического курса под названием Advanced Java .

Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !

1. Введение

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

2. Отражение API

Reflection API , который является частью стандартной библиотеки Java, предоставляет способ изучения внутренних деталей класса во время выполнения, динамического создания новых экземпляров класса (без явного использования оператора new ), динамического вызова методов, интроспективных аннотаций (аннотации были рассмотрено в части 5 урока « Как и когда использовать Enums and Annotations» , и многое, многое другое. Это дает разработчикам Java свободу писать код, который может адаптироваться, проверяться, выполняться и даже изменять себя во время работы.

API Reflection разработан довольно интуитивно понятным способом и размещается в пакете java.lang.reflect . Его структура тесно связана с концепциями языка Java и содержит все элементы для представления классов (включая общие версии), методов, полей (членов), конструкторов, интерфейсов, параметров и аннотаций. Точкой входа для Reflection API является соответствующий экземпляр Class< ? > Class< ? > класс. Например, самый простой способ перечислить все открытые методы класса String — использовать getMethods() метода getMethods() :

1
2
3
4
final Method[] methods = String.class.getMethods();
for( final Method method: methods ) {
    System.out.println( method.getName() );
}

Следуя тому же принципу, мы можем перечислить все открытые поля класса String с помощью getFields() метода getFields() , например:

1
2
3
4
final Field[] fields = String.class.getFields();
for( final Field field: fields ) {
    System.out.println( field.getName() );
}

Продолжая экспериментировать с классом String, используя отражение, давайте попробуем создать новый экземпляр и вызвать для него метод length() , причем только с использованием API отражения .

1
2
3
4
5
final Constructor< String > constructor = String.class.getConstructor( String.class );
final String str = constructor.newInstance( "sample string" );
final Method method = String.class.getMethod( "length" );
final int length = ( int )method.invoke( str );
// The length of the string is 13 characters

Вероятно, наиболее востребованные варианты использования для рефлексии вращаются вокруг обработки аннотаций. Сами по себе аннотации (исключая аннотации из стандартной библиотеки Java) не влияют на код. Тем не менее, Java-приложения могут использовать отражение для проверки аннотаций, представленных на различных интересующих их элементах Java, во время выполнения и применять некоторую логику в зависимости от аннотации и ее атрибутов. Например, давайте взглянем на способ внутреннего анализа, если конкретная аннотация присутствует в определении класса:

01
02
03
04
05
06
07
08
09
10
@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.TYPE )
public @interface ExampleAnnotation {
    // Some attributes here
}
 
@ExampleAnnotation
public class ExampleClass {
    // Some getter and setters here
}

Используя API отражения , это легко сделать с помощью getAnnotation() метода getAnnotation() . Возвращенное ненулевое значение указывает, что аннотация присутствует, например:

1
2
3
4
5
6
final ExampleAnnotation annotation =
    ExampleClass.class.getAnnotation( ExampleAnnotation.class );
 
if( annotation != null ) {
    // Some implementation here
}

Большинство API Java в настоящее время включают аннотации для облегчения их использования и интеграции для разработчиков. Последние версии очень популярных спецификаций Java, таких как Java API для веб-служб RESTful ( https://jcp.org/en/jsr/detail?id=339 ), проверка бинов ( https://jcp.org/en/jsr) / detail? id = 349 ), API временного кэширования Java ( https://jcp.org/en/jsr/detail?id=107 ), Служба сообщений Java ( https://jcp.org/en/jsr/detail? id = 343 ), постоянство Java ( https://jcp.org/en/jsr/detail?id=338 ) и многие, многие другие основаны на аннотациях, и их реализации обычно активно используют API Reflection для сбора метаданных о приложение запускается.

3. Доступ к параметрам общего типа

С момента появления обобщений (обобщения описаны в части 4 руководства « Как и когда использовать обобщенные элементы» ) API-интерфейс Reflection был расширен для поддержки самоанализа обобщенных типов. Вариант использования, который часто появляется во многих различных приложениях, заключается в определении типа общих параметров, с которыми был объявлен конкретный класс, метод или другой элемент. Давайте посмотрим на пример объявления класса:

1
2
3
4
5
6
7
public class ParameterizedTypeExample {
    private List< String > strings;
 
    public List< String > getStrings() {
        return strings;
    }
}

Теперь при проверке класса с помощью Reflection было бы очень удобно узнать, что свойство strings объявлено как универсальный тип List с параметром типа String . Фрагмент кода ниже иллюстрирует, как это можно сделать:

1
2
3
4
5
6
7
8
9
final Type type = ParameterizedTypeExample.class
    .getDeclaredField( "strings" ).getGenericType();
 
if( type instanceof ParameterizedType ) {
    final ParameterizedType parameterizedType = ( ParameterizedType )type;
    for( final Type typeArgument: parameterizedType.getActualTypeArguments() ) {
        System.out.println( typeArgument );
    }
}

Следующий консольный параметр типа будет напечатан на консоли:

1
class java.lang.String

4. Отражение API и видимость

В первой части руководства « Как создавать и уничтожать объекты» мы впервые встретились с правилами доступности, которые поддерживаются языком Java. Это может показаться неожиданным, но API отражения может определенным образом изменять правила видимости для данного члена класса.

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

1
2
3
4
5
6
7
public static class PrivateFields {
    private String name;
 
    public String getName() {
        return name;
    }
}

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

1
2
3
4
5
final PrivateFields instance = new PrivateFields();
final Field field = PrivateFields.class.getDeclaredField( "name" );
field.setAccessible( true );
field.set( instance, "sample name" );
System.out.println( instance.getName() );

Следующий вывод будет напечатан на консоли:

1
sample name

Обратите внимание, что без field.setAccessible( true ) во время выполнения будет field.setAccessible( true ) исключение, говорящее о том, что член класса с модификаторами private недоступен.

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

5. Отражение API-ошибок

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

Наконец, API отражения не обеспечивает достаточных гарантий безопасности типов, вынуждая разработчиков использовать экземпляры Object в большинстве мест, и весьма ограничено в преобразовании аргументов конструктора / метода или возвращаемых значений метода.

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

6. Методы

Релиз Java 7 представил новую очень важную функцию в стандартной библиотеке JVM и Java — дескрипторы методов. Дескриптор метода — это типизированная, непосредственно исполняемая ссылка на базовый метод, конструктор или поле (или аналогичную низкоуровневую операцию) с необязательными преобразованиями аргументов или возвращаемых значений. Во многих отношениях они являются лучшей альтернативой вызовам методов, выполняемым с использованием API Reflection . Давайте посмотрим на фрагмент кода, который использует дескрипторы метода для динамического вызова метода length() класса String .

1
2
3
4
5
6
final MethodHandles.Lookup lookup = MethodHandles.lookup();
final MethodType methodType = MethodType.methodType( int.class );
final MethodHandle methodHandle =
    lookup.findVirtual( String.class, "length", methodType );
final int length = ( int )methodHandle.invokeExact( "sample string" );
// The length of the string is 13 characters

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

Описатели методов являются очень мощным инструментом и создают необходимую основу для эффективной реализации динамических (и скриптовых) языков на платформе JVM. В части 12 руководства « Поддержка динамических языков» мы рассмотрим пару из этих языков.

7. Метод Аргумент Имена

Хорошо известная проблема, с которой разработчики Java сталкивались годами, заключается в том, что имена аргументов метода не сохраняются во время выполнения и полностью удаляются. Несколько общественных проектов, таких как, например, Paranamer ( https://github.com/paul-hammant/paranamer ), попытались решить эту проблему, добавив некоторые дополнительные метаданные в сгенерированный байт-код. К счастью, Java 8 изменила это, введя новый аргумент –parameters компилятора, который вставляет точные имена аргументов метода в байт-код. Давайте посмотрим на следующий метод:

1
2
3
public static void performAction( final String action, final Runnable callback ) {
    // Some implementation here
}

На следующем шаге давайте используем API Reflection для проверки имен аргументов метода для этого метода и убедитесь, что они сохранены:

1
2
3
4
final Method method = MethodParameterNamesExample.class
    .getDeclaredMethod( "performAction", String.class, Runnable.class );
Arrays.stream( method.getParameters() )
    .forEach( p -> System.out.println( p.getName() ) );

Если указана опция компилятора -parameters на консоли будут напечатаны следующие имена аргументов:

1
2
action
callback

Эта очень долгожданная функция действительно помогает разработчикам многих библиотек и сред Java. Отныне гораздо больше полезных метаданных можно извлекать, используя только чистый Java Reflection API, без необходимости вводить какие-либо дополнительные обходные пути (или хаки).

8. Что дальше

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

9. Загрузите исходный код

Это был урок Reflection , часть 11 курса Advanced Java . Вы можете скачать исходный код здесь: advanced-java-part-11