JEP 181: контроль доступа ко вложенным классам

в 12:23, , рубрики: java, jvm, mvt, nestmates, Блог компании JUG.ru Group

Продолжаем знакомиться с не слишком известными JEPами. На этот раз, у нас еще один подпроект из Valhalla, который так и называется — nestmates. Эта фича позволяет вложенным классам иметь неограниченный доступ друг к другу. Как именно — описано ниже. По сути, эта статья — формальный перевод JEP 181, ибо он весьма внятно описывает суть вопроса для новичка.

(Если нужен не новичковый взгляд, а какое-то экспертное мнение, то с этим лучше обратиться к специалистам — Никите Липскому, Тагиру Валееву, Владимиру Иванову и другим. Скорей всего, их можно будет поймать на Joker 2017. В августе еще действуют ранние цены, кстати).

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

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

Пара слов для тех, кто читал недавнюю статью о Minimum Value Types. (статья о практических аспектах MVT пишется прямо сейчас) Там рассказывалось о байткоде vwithfield, который позволяет взять выбранный тип-значение и подменить в нем поле. Такой метод должен, очевидно, находиться не в самом голом «значении», а в его объектной обертке. По логике вещей, только класс с приватным доступом до объекта языка может менять его поля. Соответственно, между компонентами типа-значения (VCC и DVT) должны возникать особые доверительные отношения, реализацию которых очень хочется иметь напрямую в JVM.

Поэтому предполагается, что некие будущие версии JVM могут реализовать явные nestmates на уровне VM, которые будут иметь доступ к приватным полям и методам друг друга. Это будет не какой-то компиляторный хак на генерации оберток, а вполне нативная возможность. В этих версиях JVM, инструкция vwithfield будет доступна для всех nestmates каждого конкретного типа-значения. Другими словами, vwithfield будет доступна внутри некоей «капсулы», в которой доступны все приватные методы.

Ладно, хватит болтать, переходим к самому JEP!

JEP 181: контроль доступа ко вложенным классам

Краткое описание

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

Цели

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

Добавить возможность точно описывать вложенность классов и интересов внутри классфайлов.

Провести подготовительную работу в виртуальной машине, которая ляжет в основу связанной функциональности, такой как безопасная и хорошо поддерживаемая альтернатива Unsafe.defineAnonymousClass() и sealed классов.

Ограничения

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

Мотивация

Многие JVM-языки умеют объявлять несколько классов в одном файле (например, в Java имеются вложенные классы), или транслировать другие исходники (не классы!) в классфайлы. Между тем, с точки зрения пользователя, все эти вещи кажутся частями «одного и того же класса», и поэтому пользователь интуитивно предполагает, что к ним применяется один и тот же режим безопасности. Пытаясь соответствовать ожиданиям, компиляторам зачастую приходится уменьшать строгость проверок private членов класса до package, используя методы-переходники. К сожалению, подобные переходники разрушают всю инкапсуляцию, могут привести к ошибкам в различных инструментах или просто вызвать непонимание пользователя. Если же мы введем формальное определение для группы классфайлов, формирующих гнездо, в котором соседи (nestmate) объединены общим механизмом контроля доступа, это позволит достичь того же результата более быстро, безопасно и прозрачно для всех.

Описание

Спецификация языка Java позволяет классам и интерфейсам вкладываться друг в друга. JLS 7.6 вводит понятие верхнеуровневого объявления типа. В него можно вложить произвольное количество вложенных типов. Про верхнеуровневый тип и все типы внутри него, можно сказать, что они «формируют гнездо(nest)». Два члена гнезда называются соседями по этому гнезду (nestmates). Соседи имеют неограниченный доступ друг до друга (JLS 6.6.1), включая приватные поля, методы и конструкторы. Такой доступ распространяется совершенно на всё внутри объявления верхнеуровневого типа, содержащего все остальные типы. (Можно думать об этом верхнеуровневом типе как о «мини-пакете», внутри которого всем предоставляется расширенный доступ — более широкий чем то, что предоставляется членам настоящего Java-пакета, где все они лежат).

Java-компилятор компилирует группу вложенных типов в соответствующую ей группу классфайлов. Чтобы материализовать эту вложенность (JVMS 4.7.6, JVMS 4.7.7), он использует атрибуты InnerClasses и EnclosingMethod. Этих атрибутов достаточно, чтобы JVM смогла выявить соседство, и чтобы мы могли дать соседям более широкое и общее определение, чем если бы мы считали их просто вложенными типами. В целях повышения эффективности, предлагается поменять формат классфайла, добавив туда пару новых атрибутов, используемых и соседями, и верхнеуровневым классом (который называется вершиной гнезда, nest top). Каждый сосед будет иметь атрибут, указывающий на вершину, а каждая вершина — атрибут, указывающий на известных соседей.

Мы немного поменяем правила доступа в JVM, добавив что-то такого в JVMS 5.4.4:

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

  • R имеет приватный доступ, определен в другом классе или интерфейсе C, таких что C и D являются соседями

Чтобы C и D стали соседями, у них должна быть одна и та же вершина. Тип C заявляет себя членом гнезда D, добавляя D в свой атрибут MemberOfNest. Это соседство проверяется, когда D тоже добавил C свой атрибут NestMembers. Проверка запускается только при попытке доступа к приватному члену соседа. Из-за этого, некоторые типы придется загрузить раньше, чем это могло бы произойти в других случаях.

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

  • invokespecial для приватных конструкторов
  • invokevirtual для приватных (не интерфейсных) методов экземпляра
  • invokeinterface для приватных интерфейсных методов
  • invokestatic для приватных статических методов

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

  • При специализации дженериков, любой специализированный тип можно будет создать как соседа для дженерика
  • Безопасная и хорошо поддерживаемая замена для API Unsafe.defineAnonymousClass() сможет создавать новые классы, делая их соседями уже существующих
  • Концепцию «sealed classes» можно будет реализовать как возможность создавать только такие подклассы, которые являются соседями

Более мягкие правила доступа повлияют на проверки в следующих вещах:

  • раскрытие полей и методов (JVMS 5.4.3.2, и т.п.)
  • раскрытие method handle constants (JVMS 5.4.3.5)
  • раскрытие call site specifiers (JVMS 5.4.3.6)
  • проверка доступа на уровне языка Java с помощью экземпляров java.lang.reflect.AccessibleObject
  • проверка доступа при выполнении запросов к java.lang.invoke.MethodHandles.Lookup

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

  • приватные методы интерфейса должны вызываться через invokespecial (JVMS 6.5)

и на семантику вызова MethodHandle (которая зеркально отражает ограничения байткодов). Кое-что придется поменять в общих описаниях, таких как JVMS 3.7.

Начав контролировать доступ до соседей на уровне виртуальной машины, мы можем ограничить и все остальные методы-переходники, генерируемые с помощью javac, уровнем доступа private, а не package-private, как это делается сейчас. Много где они не потребуются вовсе.

Актуальные проблемы:

  1. Высказанные здесь предложения расширяют границы доступа до соседей, прямо на уровне VM. Если уж мы уже занялись этим, не должны ли мы сузить доступ до классов, объявленных как protected и private, тем самым более точно следуя правилам языка Java? Это потребует от JVM выполнения дополнительных проверок на основе значения Class.getModifiers. (Скорей всего, не должны, поскольку это может поломать такой код с использованием рефлекшена, который предполагает, что приватный доступ был расширен до package-private. Кроме того, новые проверки на protected-классах могут привести к глобальным эффектам, поскольку представляются в JVM как публичные).
  2. Должно ли отношение «соседства» быть видно с помощью рефлекшена? Базовые методы рефлекшена, конечно, придется поправить, чтобы они учитывали правила доступа до соседей. Но вот стоит ли выносить проверку на соседство в публичное API — с этим придется разбираться. Самый простой запрос, который можно придумать: Class#getNestTop, который вернет либо null, если класс не является частью гнезда, либо вершину гнезда. (Если класс сам по себе является вершиной, запрос вернет самого себя).

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

Мы можем продолжать генерировать обертки в компиляторе Java. Тут сложно что-то предсказать. Например, в Project Lambda было сложно раскрыть ссылки на методы, если в этом раскрытии участвовали внутренние классы, и это привело к созданию нового метода-переходника. Поскольку сгенерированные компиляторами обертки получаются хитрыми и непредсказуемыми, в них множество багов, и их очень сложно анализировать с помощью инструментария, включая декомпиляторы и отладчики.

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

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

Нам придется разработать большой набор тестов для JVM, которые будут проверять правила доступа и изменения в семантике байткодов, которые были введены специально для реализации соседства.

Точно так же, придется написать дополнительные тесты для рефлекшена, ссылок на методы, var-handles, и на внешний доступ к стандартным API типа JDWP, JVM TI и JNI.

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

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

Риски и предположения

Все эти новшества придется связать с новым номером версии классфайла, поскольку меняются правила, по которым работает компилятор Java.

Чтобы сохранить обратную совместимость со старыми версиями JVM, компилятору Java потребуется поддерживать устаревшую логику генерации оберток.

Более мягкие права доступа не должны что-либо поломать. В качестве исключения, теоретически, могут сломаться негативные compliance тесты.

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

Риск поломать целостность платформы — очень мал или отсутствует, поскольку предложенные правила присваивают новые права доступа только внутри какого-то конкретного рантайм пакета. Убирая необходимость в методах-переходниках, мы системно понижаем шанс доступа между отдельными верхнеуровневыми классами.

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

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

Потребуются новые описания в спецификации JVM, а также изменения в реализации JVM. Кроме того, потребуются изменения в спецификации и реализации рефлекшена, ссылок на методы, var-handles, и возможно, JVM TI, JDWP и JNI (впрочем, нативные интерфейсы обычно игнорируют права доступа — может оказаться, что делать здесь почти ничего не придется).

Необходимо исследовать, как изменяется производительность при осуществлении дополнительных проверок доступа.

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

Правила соответствия между исходником на Java и классфайлом упростятся. И это очень вовремя, поскольку Project Lambda усложняет те же самые правила. Впрочем, некоторые эффекты возникают и на пересечении продуктов (например, JDK-8005122), поэтому недавнее увеличение сложности нельзя считать обычным ростом.

Выбрасывание переходников может слегка уменьшить размер некоторых приложений.

Возможно, придется поправить спецификацию Pack200.

Авторы

Джон Роуз — инженер и архитектор JVM в Oracle. Ведущий инженер Da Vinci Machine Project (часть OpenJDK). Ведущий инженер JSR 292 (Supporting Dynamically Typed Languages on the Java Platform), занимается спецификацией динамических вызовов и связанных вопросов, таких как профилирование типов и улучшенные компиляторные оптимизации. Раньше работал над inner classes, делал изначальный порт HotSpot на SPARC, Unsafe API, а также разрабатывал множество динамических, параллельных и гибридных языков, включая Common Lisp, Scheme («esh»), динамические биндинги для C++.

Переводчики

Олег Чирухин — на момент написания этого текста, работает архитектором в компании «Сбербанк-Технологии», занимается разработкой архитектуры систем автоматизированного управления бизнес-процессами. До перехода в Сбербанк-Технологии, принимал участие в разработке нескольких государственных информационных систем, включая Госуслуги и Электронную Медицинскую Карту, а также в разработке онлайн-игр. Спикер на конференциях JUG.ru (JPoint, JBreak). Текущие исследовательские интересы включают виртуальные машины, компиляторы и языки программирования.

Автор: Олег Чирухин

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js