跳到主要内容

JEP 181:基于嵌套的访问控制

概括

引入_Nests_,这是一个与 Java 编程语言中现有的嵌套类型概念一致的访问控制上下文。嵌套允许逻辑上属于同一代码实体但被编译为不同类文件的类访问彼此的私有成员,而无需编译器插入可访问性扩展的桥接方法。

非目标

该 JEP 不涉及大规模的访问控制,例如模块。

动机

许多 JVM 语言支持单个源文件中的多个类(例如 Java 的嵌套类),或者将非类源工件转换为类文件。然而,从用户的角度来看,这些通常被认为都属于“同一类”,因此用户希望它们共享共同的访问控制机制。为了保持这些期望,编译器经常必须通过添加访问桥来扩大private对成员的访问:对私有成员的调用被编译为对目标类中编译器生成的包私有方法的调用,该方法在packageturn 访问预期的私有成员。这些桥颠覆了封装,稍微增加了已部署应用程序的大小,并且可能使用户和工具感到困惑。一组类文件形成_嵌套_的正式概念,其中_嵌套成员_共享公共访问控制机制,允许以更简单、更安全、更透明的方式直接实现所需的结果。

公共访问控制上下文的概念也出现在其他地方,例如 中的主机类机制Unsafe.defineAnonymousClass(),其中动态加载的类可以使用主机的访问控制上下文。巢成员资格的正式概念将使该机制具有更坚实的基础(但实际上提供受支持的替代品defineAnonymousClass()将是一项单独的工作。)

描述

Java 语言规范允许类和接口相互嵌套。在顶级声明 (JLS 7.6) 的范围内,可以嵌套任意数量的类型。这些嵌套类型可以不受限制地相互访问 (JLS 6.6.1),包括私有字段、方法和构造函数。我们可以描述一个顶级类型,加上嵌套在其中的所有类型,形成一个_Nest_,并且一个 Nest 的两个成员被描述为_Nestmates_。

私有访问在包含顶级类型的整个声明中是完整的(无差别的、平坦的)。 (可以将其视为定义一种“迷你包”的顶级类型,在其中授予额外的访问权限,甚至超出向同一 Java 包的其他成员提供的访问权限。)

如今,JVM 访问规则不允许嵌套成员之间的私有访问。为了提供允许的访问,Java 源代码编译器必须引入一定程度的间接访问。例如,对私有成员的调用被编译为对目标类中编译器生成的包私有桥接方法的调用,该调用又调用预期的私有方法。这些访问桥仅根据需要生成,以满足嵌套内请求的成员访问。

JVM 缺乏对嵌套内私有访问的支持的进一步后果是核心反射也拒绝访问。java.lang.reflect.Method.invoke从一个嵌套对象到另一个嵌套对象的反射方法调用(使用)会抛出异常IllegalAccessError(除非已禁用访问控制)。鉴于反射调用的行为应与源级调用相同,这是令人惊讶的。类似地,MethodHandleAPI 拒绝直接“查找”私有 Nestmate 方法,但提供特殊支持以Lookup.in允许表达源级别调用语义。

通过将 Nestmates 的概念和相关的访问规则编入 JVM 中,我们简化了 Java 源代码编译器的工作,加强了现有的访问检查,并从核心反射和MethodHandleAPI 中删除了令人惊讶的行为。我们还允许未来的增强功能利用“嵌套”概念。例如:

  • 泛型专业化中,每个专业类型都可以创建为泛型类型的嵌套对象。
  • 安全且受支持的 API 替代品Unsafe.defineAnonymousClass()可以创建新类作为现有类的嵌套类。
  • “密封类”的概念可以通过仅允许嵌套子类来实现。
  • 真正的私有嵌套类型可能会受到影响(目前私有嵌套类型是通过包访问定义的)。

嵌套类文件属性

现有的类文件格式定义了InnerClassesEnclosingMethod属性(JVMS 4.7.6 和 4.7.7),以允许 Java 源代码编译器(例如javac)具体化源级别嵌套关系。每个嵌套类型都编译为自己的类文件,不同的类文件通过这些属性的值“链接”。虽然这些属性足以让 JVM 确定嵌套关系,但它们并不直接适合访问控制,并且本质上与单个 Java 语言概念相关。

为了允许超越简单的 Java 语言嵌套类型的更广泛、更通用的嵌套概念,并且为了有效的访问控制检查,建议修改类文件格式以定义两个新属性。一个嵌套成员(通常是顶级类)被指定为_嵌套主机_,并包含一个属性 ( NestMembers) 来标识其他静态已知的嵌套成员。其他每个巢成员都有一个属性 ( NestHost) 来标识其巢宿主。

Nestmate 的 JVM 访问控制

我们将通过在 JVMS 5.4.4 中添加类似以下子句来调整 JVM 的访问规则:

当且仅当以下任一条件成立时,类或接口_D_才能访问字段或方法_R :_

  • ...
  • R 是私有的,并且在不同的类或接口 C 中声明,并且 C 和 D 是嵌套对象。

要使_C_型和_D_型成为同窝伴侣,它们必须具有相同的巢宿主。如果类型_C_在其属性中列出_D ,则它声称是__D_托管的巢的成员。如果_D_也在其属性中列出了_C_ ,则成员资格有效。 _D_隐含地是它所承载的巢的成员。NestHost``NestMembers

没有NestHostorNestMembers属性的类隐式地形成一个嵌套,其自身作为嵌套宿主和唯一的嵌套成员。

放宽的访问规则将影响以下活动期间的访问检查:

  • 解析字段和方法(JVMS 5.4.3.2等)
  • 解析方法句柄常量 (JVMS 5.4.3.5)
  • 解析调用站点说明符 (JVMS 5.4.3.6)
  • 通过实例检查 Java 语言访问java.lang.reflect.AccessibleObject
  • 在查询期间检查访问java.lang.invoke.MethodHandles.Lookup

通过访问规则的更改以及对字节码规则的适当调整,我们可以允许生成调用字节码的简化规则:

  • invokespecial对于私有嵌套构造函数,
  • invokevirtual对于私有非接口、nestmate 实例方法,
  • invokeinterface对于私有接口,nestmate 实例方法;和
  • invokestatic对于私有巢友,静态方法

这放宽了必须使用invokespecial(JVMS 6.5) 调用私有接口方法的现有约束,并且更普遍地允许invokevirtual用于私有方法调用,而不是添加围绕invokespecial.可以对MethodHandle调用的语义进行类似的更改(它反映了调用字节码约束)。

Nest 会员验证

必须先验证 Nest 成员身份,然后才能继续进行依赖 Nestmate 访问的访问检查。这可能晚于成员的访问时间发生,或者早于类的验证时间发生,或者介于两者之间,例如方法的 JIT 编译。嵌套成员身份验证需要加载嵌套主机类(如果尚未加载)。为了避免潜在不必要的类加载,应尽可能晚地(即在访问检查时)执行嵌套成员身份验证。这减轻了由于在依赖嵌套访问时要求嵌套主机类存在而引入的不兼容性的影响。

为了保持嵌套的完整性,建议至少在开始时禁止使用任何形式的类转换或类重新定义来修改嵌套类文件属性。

Nestmate 反射 API

当我们引入新的类文件属性时,通常会提供一种使用核心反射检查/查询这些属性的方法。目前,这被设想为以下三种方法java.lang.ClassgetNestHostgetNestMembersisNestmateOf

受影响的规格和 API

所提议的更改虽然概念上很简单,但会影响所有显式或隐式涉及访问控制或与方法调用模式相关的规范和 API。这些包括:

  • Java 虚拟机规范
    • 类文件属性更改
    • 访问控制规则更改
    • 调用字节码规则更改
  • 核心反射
    • Method调用规则
    • Field访问规则
  • MethodHandle查找规则
  • 类转换/重新定义:JVM TI 和java.lang.instrumentAPI、JDWP 和 JDI ( com.sun.jdi.VirtualMachine)
    • 禁止修改嵌套相关的类文件属性
  • Pack200规格
    • 识别新的类文件属性

对 Java 源代码编译器的影响

提议的更改简化了将 Java 源构造映射到类文件的规则,因此对选择使用它们的 Java 源代码编译器有许多影响:

  • 正确生成嵌套相关的类文件属性
  • 消除先前所需的访问桥接方法并为私有嵌套成员生成直接成员访问指令
  • 发出正确/适当的调用字节码
  • 能够将其他合成方法更改为私有而不是包私有(甚至消除它们,或用共享但私有的方法句柄常量替换它们)

编译javac器将进行更新,以在生成最新版本的类文件时充分利用 Nestmate。 (旧版本将像现在一样使用访问桥等生成。)

对其他工具的影响

任何操作类文件或生成或处理字节码的工具都可能受到这些更改的影响。至少,此类工具必须容忍新类文件属性的存在,并允许字节码规则的更改。例如:

  • javap文件检查工具,
  • Pack200 实施,以及
  • ASM字节码操作框架,也在JDK内部使用。

开放式问题

访问检查的额外复杂性是必须检查的事情。特别是围绕嵌套主机类的解析以及可能出现的错误的问题。我们已经遇到并解决了编译器线程需要加载嵌套主机类的问题 - 这在编译器线程中是不允许的。我们需要确保实现能够处理这些条件,并确保规范不会受到它们的引入的影响。

备择方案

我们可以根据需要继续在 Java 编译器中生成桥接方法。这是一个很难预测的过程。例如,Lambda 项目在存在内部类的情况下难以解析方法句柄常量,从而产生了一种新型的桥接方法。由于编译器生成的桥接方法非常棘手且不可预测,因此它们也存在缺陷,并且难以通过各种工具(包括反编译器和调试器)进行分析。

最初的提案考虑使用现有的InnerClassesEnclosingMethod属性来建立巢友关系。但是引入特定的 Nestmate 相关属性不仅使 Nestmate 比仅与语言级嵌套类型相关更通用,而且允许更有效的实现。此外,如果我们选择急切的嵌套成员身份验证检查,它将改变现有属性的语义,这将是一个兼容性问题。虽然javac编译器可能会保持“内部类”和“嵌套成员”属性对齐,但这是编译器的选择,JVM 将完全独立地对待它们。

在确定当前方法之前,讨论了如何通过类文件属性最好地表达嵌套关系。对于分散式方法的一项建议是通过 UUID 来识别每个巢。讨论结论如下

该提案分为两个部分:

  1. 基于 UUID 的新嵌套命名约定。这是 JVM 中的一个新概念,需要新的基础设施来管理(生成、转码、验证、反映、调试)。这意味着新的错误和新的攻击面。如果没有决定性的好处,最好重用现有的名称空间,特别是 JVM 的类型名称字典。

  2. 单向链接。 UUID 是一个没有内容的纯粹身份,不包含其嵌套成员的列表。嵌套成员指向嵌套(通过 UUID)。任何类都可以通过提及适当的 UUID 将自身注入嵌套(在同一包中)。单向链接意味着无法枚举嵌套。这使一些优化变得复杂(基于密封类型)。巢的安全性和密封性降低到包装的安全性和密封性。 PRIVATE 只是默认范围访问控制的别名。

抱歉,与当前的提案相比,这两个部分都不吸引我。

测试

我们需要一组广泛的 JVM 测试来验证新的访问规则和对字节码语义的调整以支持嵌套。

同样,我们需要对核心反射、方法句柄、var 句柄和外部访问 API(如 JDWP、JVM TI 和 JNI)进行额外的测试。

由于此处没有提出语言更改建议,因此不需要新的语言合规性测试。

javac在修改编译器以利用 Nestmate 访问之后,语言合规性测试自然会产生对 Nestmate 的充分功能测试。

风险和假设

新规则必须与新的类文件版本号相关联,因为我们需要确保 Java 源编译器在针对理解它们的 JVM 时仅生成依赖于新属性和规则的字节码。其推论是,只有当新属性出现在具有合适版本号的类文件中时,JVM 才会识别新属性并对其执行操作。新的类文件版本给更广泛的 Java 生态系统中的工具带来了负担,但我们不希望 Nestmates 成为目标 JDK 版本中唯一依赖新类文件版本号的技术。

放宽访问权限几乎不会带来合规风险。今天编译和运行的所有 Java 语言访问都将在嵌套更改的情况下编译和运行,而不更改源代码。今天禁用对 Nestmate 反射访问的访问检查(通过 )的代码setAccessible将继续随着 Nestmate 更改正确运行 - 但可以更新为不禁用访问检查。

在某些情况下,检查禁止行为的合规性测试可能会失败。例如:

  • 对私有嵌套方法的直接反射访问当前失败(除非禁用访问检查),但应用这些更改后将“意外”成功。
  • invokeinterface不能用于私有接口方法的测试现在将失败,因为通过这些更改可以使用它。

由于该提案放宽了访问权限,因此用户兼容性几乎没有风险。然而,如果用户“发现”并利用了访问桥接方法,则在桥接被删除后他们将无法这样做。这种风险非常小,因为桥接方法一开始就没有稳定的名称。

由于所提议的规则仅在单个运行时包内授予新的访问权限,所以系统完整性几乎没有风险或没有风险。通过消除对桥接方法的需求,不同顶级类之间的潜在访问将系统地减少。

嵌套成员身份验证需要存在嵌套主机类,即使该类本身未使用(除了作为嵌套成员的容器)。这可能会在三个方面产生影响:

  1. 类加载的顺序可能会发生变化,因为可能比直接使用嵌套主机时更早需要嵌套主机进行访问检查。这预计不会成为问题,因为类仅被加载,而不被初始化,并且对类加载顺序(与类初始化顺序不同)的依赖关系非常罕见。

  2. 这可能会影响从分布式形式中删除未使用的类的测试/应用程序,并且嵌套主机未使用。通过将 Nest 成员身份验证保留到需要 Nestmate 访问检查时,我们的目标是最大限度地减少此问题的影响,但在某些情况下,最终用户将不得不更改他们分发代码的方式。我们认为这是一个非常小的风险,因为纯粹使用顶级类作为无状态容器并不常见,仅包含静态嵌套类型,其中嵌套类型将依赖于彼此的私有访问。

  3. 嵌套主机的解析还将类加载(以及相关异常的可能性)引入到 JVM 的访问检查逻辑中。这主要是 JVM 实现者关心的问题。必须小心确保所有可能导致虚拟机访问检查的路径要么排除加载嵌套主机的可能性,要么能够应对它。对于可能发生的潜在异常也是如此。从用户的角度来看,由此产生的风险非常小,因为 Java 代码很少假设类加载可能发生的时间和地点,并且只有在存在格式错误的类文件时才会发生异常。