Пару лет назад я присутствовал на беседе бывшего коллеги (но все еще друга) Volker Simonis . Это натолкнуло меня на мысль немного углубиться в тему того, как обезопасить JVM. На основе этого материала я создал серию постов в блоге , а также беседу .
С этого момента я представлял доклад на встречах и конференциях, где он был хорошо принят. Поскольку мне нравится исследовать разные области, я остановился, чтобы представить другие предложения. Тем не менее, этот доклад есть в моем портфолио, и он был запрошен снова в 2021 году. Я уже представлял его дважды с начала года на момент написания этой статьи.
Это позволило мне обновить демонстрационную версию до версии 16 JDK. В этом сообщении в блоге я хочу поделиться некоторыми выводами относительно изменений безопасности, касающихся изменения типа поля в разных версиях JDK.
Весело провести время с JDK 8
Давайте начнем с JDK. Вот тест, насколько рано я выступаю:
Foo foo = new Foo(); Classclazz = 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 // ... }
- Интересно, что там есть тип
поля
.
Если поле присутствует, почему мы получаем исключение? Нам нужно немного углубиться в код, чтобы понять причину.
Вот схема последовательности Class.getDeclaredField()
:
Диаграмма показывает два интересных момента:
- Класс
Отражение
управляет кэшем для повышения производительности. - Поле с именем
Карта фильтра полей
отфильтровывает поля, возвращаемые отражающим доступом.
Давайте исследуем класс Отражение
, чтобы понять, что среда выполнения не находит атрибут тип
:
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(); }
- Все атрибуты
Поля
отфильтрованы!
По этой причине ни один из атрибутов поля
не доступен через отражение!
Альтернативный способ изменить тип
Начиная с версии 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 }
- Возвращает копию объекта
Поле
, а не самоПоле
.
Поскольку код JDK возвращает копию поля, изменение происходит в этой копии, и мы не можем изменить тип исходного поля.
Вывод
Хотя Java позиционирует себя как язык со статической типизацией, версия 8 JVM позволяет нам динамически изменять тип во время выполнения. Одна из моих любимых шуток во время упомянутого выше выступления заключается в том, что, хотя мы узнали, что Java статически типизирована, на самом деле она динамически типизирована.
Мы можем точно отследить изменения в Java 12: версия 11 класса Отражение
показывает базовую карта фильтров полей
; версия 12 показывает полностью настроенную версию. Следовательно, если вы хотите избежать неприятных сюрпризов, вам следует перейти на последнюю, если не последнюю версию.
Идти дальше:
- Сосредоточьтесь на безопасности JVM
- Что отражает солнце. Чувствительная к звонкам аннотация означает?
- Рекомендации по безопасному кодированию для Java SE
Первоначально опубликовано на Фанат Java 4 апреля th , 2021
Оригинал: “https://dev.to/nfrankel/changing-a-field-s-type-in-recent-jdks-4ef9”