openjdk.ruOpenJDK на русском

JEP 181: Nest-Based Access Control

Управление доступом на основе nest

АвторJohn Rose
ОтветственныйDavid Holmes
ТипFeature
ОбластьSE
СтатусClosed / Delivered
Выпуск11
Компонентhotspot / runtime
Обсуждениеvalhalla dash dev at openjdk dot java dot net
ТрудоёмкостьM
ДлительностьM
РецензентыKaren Kinnear, Mikael Vidstedt, Vladimir Kozlov
ОдобренMikael Vidstedt
Создан2013/03/19 20:00
Обновлён2021/04/24 21:29
Задача8046171

Аннотация

Вводится понятие nest — контекст управления доступом, согласованный с существующим понятием вложенных типов в языке программирования Java. Nest позволяют классам, которые логически являются частью одной сущности кода, но компилируются в отдельные class-файлы, обращаться к private-членам друг друга без того, чтобы компиляторы вставляли мостовые методы, расширяющие доступность.

Что не является целью

Этот JEP не касается управления доступом в крупном масштабе, например на уровне модулей.

Мотивация

Многие языки для JVM поддерживают несколько классов в одном исходном файле (например, вложенные классы Java) или транслируют в class-файлы исходные артефакты, не являющиеся классами. Однако с точки зрения пользователя всё это обычно считается находящимся «в одном и том же классе», поэтому пользователи ожидают, что для них действует общий режим управления доступом. Чтобы оправдать эти ожидания, компиляторам часто приходится расширять доступ к private-членам до уровня package, добавляя мосты доступа: вызов private-члена компилируется в вызов сгенерированного компилятором package-private метода в целевом классе, который, в свою очередь, обращается к нужному private-члену. Эти мосты нарушают инкапсуляцию, немного увеличивают размер развёрнутого приложения и могут сбивать с толку пользователей и инструменты. Формальное понятие группы class-файлов, образующих nest, где члены группы — nest mates — используют общий механизм управления доступом, позволяет достичь желаемого результата напрямую — более простым, более безопасным и более прозрачным способом.

Понятие общего контекста управления доступом возникает и в других местах, например в механизме класса-хоста в Unsafe.defineAnonymousClass(), где динамически загружаемый класс может использовать контекст управления доступом хоста. Формальное понятие членства в nest поставило бы этот механизм на более прочную основу (но собственно предоставление поддерживаемой замены для defineAnonymousClass() было бы отдельной работой).

Описание

The Java Language Specification допускает вложение классов и интерфейсов друг в друга. В области видимости объявления верхнего уровня (JLS 7.6) может находиться любое количество вложенных типов. Эти вложенные типы имеют неограниченный доступ друг к другу (JLS 6.6.1), включая private-поля, методы и конструкторы. Можно описать тип верхнего уровня вместе со всеми вложенными в него типами как образующие nest, а два члена nest называются nestmates.

Private-доступ является полным (недифференцированным, плоским) в пределах всего объявления содержащего типа верхнего уровня. (Можно считать, что тип верхнего уровня определяет своего рода «мини-пакет», внутри которого предоставляется дополнительный доступ — даже сверх того, что предоставлен другим членам того же пакета Java.)

Сегодня private-доступ между nestmates не разрешён правилами доступа JVM. Чтобы обеспечить разрешённый доступ, компилятор исходного кода Java вынужден вводить уровень косвенности. Например, вызов private-члена компилируется в вызов сгенерированного компилятором package-private мостового метода в целевом классе, который, в свою очередь, вызывает нужный private-метод. Эти мосты доступа генерируются только по мере необходимости, чтобы обеспечить обращения к членам, запрошенные внутри nest.

Ещё одно следствие отсутствия в JVM поддержки private-доступа внутри nest состоит в том, что базовая рефлексия (core reflection) также запрещает доступ. Рефлексивный вызов метода (с помощью java.lang.reflect.Method.invoke) из одного nestmate-класса в другой выбрасывает IllegalAccessError (если управление доступом не отключено). Это удивительно, поскольку рефлексивные вызовы должны вести себя так же, как вызовы на уровне исходного кода. Аналогично API MethodHandle отклоняет прямой «поиск» (lookup) private-метода nestmate-класса, но предоставляет специальную поддержку через Lookup.in, позволяющую выразить семантику вызова на уровне исходного кода.

Закрепив в JVM понятие nestmates и связанные с ним правила доступа, мы упрощаем работу компиляторов исходного кода Java, ужесточаем существующие проверки доступа и устраняем неожиданное поведение базовой рефлексии и API MethodHandle. Мы также даём возможность будущим улучшениям использовать понятие «nest». Например:

  • В обобщённой специализации (generic specialization) каждый специализированный тип можно было бы создавать как nestmate обобщённого типа.
  • Безопасная и поддерживаемая замена API Unsafe.defineAnonymousClass() могла бы создавать новый класс как nestmate существующего класса.
  • Концепцию Sealed Classes (запечатанные классы) можно было бы реализовать, разрешив только подклассы, являющиеся nestmates.
  • Можно было бы реализовать действительно private вложенные типы (сейчас private вложенные типы определяются с доступом на уровне пакета).

Атрибуты class-файла для nest

Существующий формат class-файла определяет атрибуты InnerClasses и EnclosingMethod (JVMS 4.7.6 и 4.7.7), чтобы компиляторы исходного кода Java, такие как javac, могли материализовать отношение вложенности на уровне исходного кода. Каждый вложенный тип компилируется в собственный class-файл, а разные class-файлы «связываются» значениями этих атрибутов. Хотя этих атрибутов достаточно, чтобы JVM могла определить принадлежность к nestmates, они непосредственно не подходят для управления доступом и по своей природе привязаны к одному понятию языка Java.

Чтобы обеспечить более широкое и более общее понятие nestmates, выходящее за рамки просто вложенных типов языка Java, а также ради эффективной проверки доступа, предлагается изменить формат class-файла, определив два новых атрибута. Один член nest (как правило, класс верхнего уровня) назначается nest host и содержит атрибут (NestMembers), определяющий остальные статически известные члены nest. Каждый из остальных членов nest имеет атрибут (NestHost), определяющий его nest host.

Управление доступом в JVM для nestmates

Мы изменим правила доступа JVM, добавив в JVMS 5.4.4 примерно следующее условие:

Поле или метод R доступны классу или интерфейсу D тогда и только тогда, когда выполняется любое из следующих условий:

  • ...
  • R является private и объявлен в другом классе или интерфейсе C, а C и D являются nestmates.

Чтобы типы C и D были nestmates, у них должен быть один и тот же nest host. Тип C заявляет о своём членстве в nest с хостом D, если указывает D в своём атрибуте NestHost. Членство подтверждается, если D также указывает C в своём атрибуте NestMembers. D неявно является членом nest, хостом которого он служит.

Класс без атрибута NestHost или NestMembers неявно образует nest, в котором сам является nest host и единственным членом.

Ослабленные правила доступа повлияют на проверки доступа при следующих действиях:

  • Разрешение полей и методов (JVMS 5.4.3.2 и др.)
  • Разрешение констант method handle (JVMS 5.4.3.5)
  • Разрешение спецификаторов мест вызова (JVMS 5.4.3.6)
  • Проверка доступа по правилам языка Java экземплярами java.lang.reflect.AccessibleObject
  • Проверка доступа при запросах к java.lang.invoke.MethodHandles.Lookup

Благодаря изменению правил доступа и соответствующим поправкам в правилах байт-кода мы можем ввести упрощённые правила генерации байт-кодов вызова:

  • invokespecial для private-конструкторов nestmate-классов,
  • invokevirtual для private-методов экземпляра nestmate-классов, не являющихся интерфейсами,
  • invokeinterface для private-методов экземпляра nestmate-интерфейсов; и
  • invokestatic для private статических методов nestmate-классов

Это ослабляет существующее ограничение, согласно которому private-методы интерфейсов должны вызываться с помощью invokespecial (JVMS 6.5), и в более общем виде позволяет использовать invokevirtual для вызова private-методов, вместо того чтобы усложнять и без того сложные правила использования invokespecial. Аналогичные изменения можно внести в семантику вызова через MethodHandle (которая повторяет ограничения байт-кодов вызова).

Проверка членства в nest

Членство в nest должно быть проверено до того, как сможет выполниться проверка доступа, опирающаяся на доступ между nestmates. Это может происходить как поздно — в момент обращения к члену, — так и рано — во время верификации класса, или где-то посередине, например при JIT-компиляции метода. Для проверки членства в nest требуется загрузить класс nest host, если он ещё не загружен. Чтобы избежать потенциально ненужной загрузки классов, проверку членства в nest следует выполнять как можно позже, т. е. в момент проверки доступа. Это смягчает последствия несовместимости, возникающей из-за требования, чтобы класс nest host присутствовал, если используется доступ между nestmates.

Чтобы сохранить целостность nest, предлагается, по крайней мере на первых порах, запретить изменение атрибутов class-файла, относящихся к nest, с помощью любой формы трансформации или переопределения классов.

API рефлексии для nestmates

Поскольку мы вводим новые атрибуты class-файла, принято предоставлять средства для их просмотра и запроса через базовую рефлексию. Сейчас это предполагается в виде трёх методов в java.lang.Class: getNestHost, getNestMembers и isNestmateOf.

Затрагиваемые спецификации и API

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

  • The Java Virtual Machine Specification
    • Изменения атрибутов class-файла
    • Изменения правил управления доступом
    • Изменения правил байт-кодов вызова
  • Базовая рефлексия
    • Правила вызова Method
    • Правила доступа Field
  • Правила поиска (lookup) MethodHandle
  • Трансформация и переопределение классов: JVM TI и API java.lang.instrument, JDWP и JDI (com.sun.jdi.VirtualMachine)
    • Запрет изменения атрибутов class-файла, относящихся к nest
  • Спецификация Pack200
    • Распознавание новых атрибутов class-файла

Влияние на компиляторы исходного кода Java

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

  • Корректная генерация атрибутов class-файла, относящихся к nest
  • Отказ от ранее необходимых мостовых методов доступа и генерация инструкций прямого доступа к членам для private-членов nestmate-классов
  • Выдача корректных/подходящих байт-кодов вызова
  • Возможность сделать другие синтетические методы private, а не package-private (или даже устранить их либо заменить общими, но private константами method handle)

Компилятор javac будет обновлён так, чтобы в полной мере использовать nestmates при генерации class-файлов последней версии. (Более старые версии будут генерироваться так же, как сейчас, с мостами доступа и т. д.)

Влияние на другие инструменты

Эти изменения потенциально затрагивают любой инструмент, который работает с class-файлами или генерирует либо обрабатывает байт-коды. Как минимум такие инструменты должны допускать наличие новых атрибутов class-файла и учитывать изменение правил байт-кода. Например:

Открытые вопросы

Дополнительную сложность проверки доступа необходимо изучить. В частности, вопросы, связанные с разрешением класса nest host, и ошибки, которые могут при этом возникать. Мы уже столкнулись с проблемой, когда потоку компилятора требовалось загрузить класс nest host, что в потоке компилятора не допускается, и решили её. Нам нужно убедиться, что реализация способна обрабатывать такие ситуации, и что их появление не затрагивает спецификацию.

Альтернативы

Мы можем и дальше генерировать мостовые методы в компиляторе Java по мере необходимости. Этот процесс трудно предсказать. Например, в проекте Lambda возникли трудности с разрешением констант method handle при наличии внутренних классов, что привело к появлению нового типа мостовых методов. Поскольку сгенерированные компилятором мостовые методы запутанны и непредсказуемы, они также содержат ошибки и плохо поддаются анализу различными инструментами, включая декомпиляторы и отладчики.

В первоначальном предложении рассматривалась возможность использовать для установления отношения nestmate существующие атрибуты InnerClasses и EnclosingMethod. Однако введение отдельных атрибутов для nestmates, во-первых, делает nestmates более общим механизмом, не связанным только с вложенными типами на уровне языка, а во-вторых, позволяет реализовать его эффективнее. Кроме того, если бы мы выбрали немедленную проверку принадлежности к nest, это изменило бы семантику существующих атрибутов, что создало бы проблему совместимости. Хотя компилятор javac, скорее всего, будет поддерживать согласованность атрибутов «inner classes» и «nest members», это выбор компилятора, а JVM будет обрабатывать их полностью независимо.

Прежде чем остановиться на текущем подходе, обсуждалось, как лучше всего выразить отношение вложенности через атрибуты class-файла. Одно из предложений, предполагавшее децентрализованный подход, состояло в том, чтобы идентифицировать каждый nest с помощью UUID. Это обсуждение завершилось так:

У такого предложения две части:

  1. Новое соглашение об именовании nest на основе UUID. Это новое понятие для JVM, и для работы с ним (генерации, перекодирования, проверки, рефлексии, отладки) потребуется новая инфраструктура. Это означает новые ошибки и новые поверхности атаки. При отсутствии решающего преимущества лучше повторно использовать существующие пространства имён и (в частности) словарь имён типов JVM.

  2. Однонаправленные связи. UUID, будучи чистой идентичностью без содержимого, не содержит списка членов своего nest. Члены nest указывают на nest (через UUID). Любой класс может внедрить себя в nest (в том же пакете), просто указав соответствующий UUID. Однонаправленная связь означает, что перечислить члены nest невозможно. Это усложняет некоторые оптимизации (основанные на sealed-типах). Безопасность и возможность запечатывания nest сводятся к уровню пакетов. PRIVATE становится просто синонимом управления доступом с областью видимости по умолчанию.

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

Тестирование

Нам понадобится обширный набор тестов JVM для проверки новых правил доступа и изменений в семантике байт-кода, обеспечивающих поддержку nestmates.

Аналогично нам понадобятся дополнительные тесты для core reflection, method handles, var handles и API внешнего доступа, таких как JDWP, JVM TI и JNI.

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

Достаточные функциональные тесты для nestmates естественным образом появятся из тестов на соответствие языку после того, как компилятор javac будет изменён так, чтобы использовать доступ между nestmates.

Риски и допущения

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

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

Тесты на соответствие, проверяющие запрещённое поведение, в некоторых случаях могут не проходить. Например:

  • Прямой рефлективный доступ к private-методам nest сейчас завершается ошибкой (если проверка доступа не отключена), но после применения этих изменений будет «неожиданно» завершаться успешно.
  • Тест, проверяющий, что invokeinterface нельзя использовать для private-метода интерфейса, теперь не пройдёт, потому что с этими изменениями его использовать можно.

Риск для совместимости с пользовательским кодом невелик или отсутствует, поскольку предложение ослабляет ограничения доступа. Однако если пользователи «обнаружили» мостовые методы доступа и воспользовались ими, после удаления мостов они больше не смогут этого делать. Этот риск очень мал, поскольку у мостовых методов изначально нет стабильных имён.

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

Проверка принадлежности к nest требует наличия класса nest host, даже если сам этот класс не используется (кроме как в качестве контейнера для членов nest). Это может повлиять на три области:

  1. Порядок загрузки классов может измениться, поскольку nest host может понадобиться для проверки доступа раньше, чем произойдёт какое-либо прямое использование nest host. Не ожидается, что это станет проблемой, поскольку класс только загружается, но не инициализируется, а зависимости от порядка загрузки классов (в отличие от порядка инициализации классов) встречаются очень редко.

  2. Это может затронуть тесты и приложения, которые удаляют неиспользуемые классы из своих дистрибутивов, если nest host не используется. Откладывая проверку принадлежности к nest до момента, когда понадобится проверка доступа между nestmates, мы стремимся минимизировать влияние этой проблемы, но в некоторых случаях конечному пользователю придётся изменить способ распространения своего кода. Мы считаем этот риск очень малым, поскольку класс верхнего уровня редко используется исключительно как контейнер без состояния, содержащий только статические вложенные типы, которые опираются на private-доступ друг к другу.

  3. Разрешение nest host также привносит загрузку классов (и возможность связанных с ней исключений) в логику проверки доступа JVM. Это касается прежде всего разработчиков реализаций JVM. Необходимо следить за тем, чтобы все пути, которые могут привести к проверке доступа в VM, либо исключали возможность загрузки nest host, либо могли с ней справиться. То же относится к возможным исключениям. С точки зрения пользователя риск здесь очень мал, поскольку код на Java редко делает предположения о том, когда и где может происходить загрузка классов, а исключения будут возникать только при наличии некорректно сформированных class-файлов.