Рубрики
Без рубрики

Изменение типа поля в последних JDK

Пару лет назад я присутствовал на беседе бывшего коллеги (но все еще друга) Фолькера Симониса. Это га… Помечено как java, безопасность, взлом, jvm.

Пару лет назад я присутствовал на беседе бывшего коллеги (но все еще друга) Volker Simonis . Это натолкнуло меня на мысль немного углубиться в тему того, как обезопасить JVM. На основе этого материала я создал серию постов в блоге , а также беседу .

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

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

Весело провести время с JDK 8

Давайте начнем с JDK. Вот тест, насколько рано я выступаю:

Foo foo = new Foo();
Class clazz = foo.getClass();
Field field = clazz.getDeclaredField("hidden");
Field type = Field.class.getDeclaredField("type");
AccessibleObject.setAccessible(
        new AccessibleObject[]{field, type}, true);
type.set(field, String.class);
field.set(foo, "This should print 5!");
Object hidden = field.get(foo);
System.out.println(hidden);

class Foo {
    private int hidden = 5;
}

Потратьте некоторое время, чтобы угадать результат выполнения этой программы при ее запуске с JDK 8.

Вот соответствующая диаграмма классов, которая поможет вам:

Как видно, Поле имеет атрибут типа , который содержит… его тип. С помощью приведенного выше кода можно изменить тип скрытого с int на Строку так что приведенный выше код выполняется и печатает "Это должно напечатать 5!" .

С JDK 16 фрагмент больше не работает. Вместо этого он создает исключение во время выполнения:

Exception in thread "main" java.lang.NoSuchFieldException: type
    at java.base/java.lang.Class.getDeclaredField(Class.java:2549)
    at ch.frankel.blog.FirstAttempt.main(FirstAttempt.java:12)

В исключении явно упоминается строка 12: Поле.класс.getDeclaredField("тип") . Похоже, что реализация класса Field изменилась.

Глядя на исходный код JDK 16

Давайте посмотрим на исходный код в JDK 16:

public final class Field extends AccessibleObject implements Member {

    private Class            clazz;
    private int                 slot;
    // This is guaranteed to be interned by the VM in the 1.4
    // reflection implementation
    private String              name;
    private Class            type;     // 1

    // ...
}
  1. Интересно, что там есть тип поля .

Если поле присутствует, почему мы получаем исключение? Нам нужно немного углубиться в код, чтобы понять причину.

Вот схема последовательности Class.getDeclaredField() :

Диаграмма показывает два интересных момента:

  1. Класс Отражение управляет кэшем для повышения производительности.
  2. Поле с именем Карта фильтра полей отфильтровывает поля, возвращаемые отражающим доступом.

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

static {
    fieldFilterMap = Map.of(
        Reflection.class, ALL_MEMBERS,
        AccessibleObject.class, ALL_MEMBERS,
        Class.class, Set.of("classLoader", "classData"),
        ClassLoader.class, ALL_MEMBERS,
        Constructor.class, ALL_MEMBERS,
        Field.class, ALL_MEMBERS,           // 1
        Method.class, ALL_MEMBERS,
        Module.class, ALL_MEMBERS,
        System.class, Set.of("security")
    );
    methodFilterMap = Map.of();
}
  1. Все атрибуты Поля отфильтрованы!

По этой причине ни один из атрибутов поля не доступен через отражение!

Альтернативный способ изменить тип

Начиная с версии 9, JDK предлагает новый API для доступа к полям как часть пакета java.lang.invoke .

Вот довольно упрощенная диаграмма классов, посвященная нашему использованию:

Можно использовать API для доступа к атрибуту type , как указано выше. Код выглядит следующим образом:

var foo = new Foo();
var clazz = foo.getClass();
var lookup = MethodHandles.privateLookupIn(Field.class, MethodHandles.lookup());
var type = lookup.findVarHandle(Field.class, "type", Class.class);
var field = clazz.getDeclaredField("hidden");
type.set(field, String.class);
field.setAccessible(true);
field.set(foo, "This should print 5!");
var hidden = field.get(foo);
System.out.println(hidden);

Но выполнение кода приводит к следующему:

Exception in thread "main" java.lang.IllegalArgumentException: Can not set int field ch.frankel.blog.Foo.hidden to java.lang.String
    at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:167)
    at java.base/jdk.internal.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException(UnsafeFieldAccessorImpl.java:171)
    at java.base/jdk.internal.reflect.UnsafeIntegerFieldAccessorImpl.set(UnsafeIntegerFieldAccessorImpl.java:98)
    at java.base/java.lang.reflect.Field.set(Field.java:793)
    at ch.frankel.blog.FinalAttempt.main(FinalAttempt.java:16)

Хотя код компилируется и запускается, он выдает field.set(foo, "Это должно напечатать 5!") . Мы ссылаемся на поле тип и можем изменить его без каких-либо проблем, но оно все равно жалуется.

Причина кроется в последней строке метода getDeclaredField() :

public Field getDeclaredField(String name)
    throws NoSuchFieldException, SecurityException {
    Objects.requireNonNull(name);
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
        checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
    }
    Field field = searchFields(privateGetDeclaredFields(false), name);
    if (field ## null) {
        throw new NoSuchFieldException(name);
    }
    return getReflectionFactory().copyField(field);      // 1
}
  1. Возвращает копию объекта Поле , а не само Поле .

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

Вывод

Хотя Java позиционирует себя как язык со статической типизацией, версия 8 JVM позволяет нам динамически изменять тип во время выполнения. Одна из моих любимых шуток во время упомянутого выше выступления заключается в том, что, хотя мы узнали, что Java статически типизирована, на самом деле она динамически типизирована.

Мы можем точно отследить изменения в Java 12: версия 11 класса Отражение показывает базовую карта фильтров полей ; версия 12 показывает полностью настроенную версию. Следовательно, если вы хотите избежать неприятных сюрпризов, вам следует перейти на последнюю, если не последнюю версию.

Идти дальше:

Первоначально опубликовано на Фанат Java 4 апреля th , 2021

Оригинал: “https://dev.to/nfrankel/changing-a-field-s-type-in-recent-jdks-4ef9”