访问Java 9模块的私有状态

发布者:    |       讨论

数据为中心的库通常需要访问库用户提供的类的私有状态。

例如,Hibernate ORM。当在实体的字段上给定@Id注解时,Hibernate默认会直接访问字段 - 而不是调用属性获取器和设置器 - 来读取和写入实体的状态。

通常,这些字段是私有的。尽管从外部代码访问它们从来不是问题,但随着Java 9模块系统的出现,这些规则将略有变化。

以下我们将探讨提供Java 9模块的库的作者如何访问其他模块中定义的类的私有状态。

示例

作为一个例子,让我们考虑一个简单的方法,该方法接受一个对象 - 例如用户定义的实体类型的一个实例 - 和一个字段名,并返回该名称的对象的字段值。使用反射,此方法可以如下实现(为了简化,我们忽略了可能存在安全管理器的情况)

package com.example.library;

public class FieldValueAccessor {

    public Object getFieldValue(Object object, String fieldName) {
        try {
            Class<?> clazz = object.getClass();
            Field field = clazz.getDeclaredField( fieldName );
            field.setAccessible( true );
            return field.get( object );
        }
        catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            throw new RuntimeException( e );
        }
    }
}

通过调用Field#setAccessible(),我们可以在以下情况下获得字段的值,即使它被声明为private。库模块的模块描述符很简单,它仅导出访问器类的包

module com.example.library {
    exports com.example.library;
}

在第二个模块中,代表我们的应用程序,让我们定义一个简单的“实体”

package com.example.entities;

public class MyEntity {

    private String name;

    public MyEntity(String name) {
        this.name = name;
    }

    // ...
}

还有一个简单的main方法,它使用访问器从实体中读取一个字段

package com.example.entities;

public class Main {
    public static void main(String... args) {
        FieldValueAccessor accessor = new FieldValueAccessor();
        Object fieldValue = accessor.getFieldValue( new MyEntity( "hey there" ), "name" );
        assert "hey there".equals( fieldValue );
    }
}

由于此模块使用了库模块,我们需要在实体模块的描述符中声明此依赖关系

module com.example.myapp {
    requires com.example.library;
}

放置示例类后,让我们运行代码并看看会发生什么。在Java 8上会正常工作,但自从Java 9以来,我们将看到这个异常

java.lang.reflect.InaccessibleObjectException:
Unable to make field private final java.lang.String com.example.entities.MyEntity.name accessible:
module com.example.myapp does not "opens com.example.entities" to module com.example.library

调用 setAccessible() 失败,因为默认情况下,一个模块中的代码不允许对另一个(命名)模块中的代码进行所谓的“深度反射”。

打开此模块!

现在我们如何克服这个问题?错误信息已经给出了正确的提示:要反射的类型所在的包必须对调用 setAccessible() 的代码所在的模块 打开

如果某个包已被打开到另一个模块,则该模块可以在运行时反射性地访问该包的类型。请注意,打开一个包不会使其在编译时对其他模块可用;这需要将包 导出(如上面的库模块所示)。

打开包有几种选项。首先是将模块变成一个 打开模块

open module com.example.myapp {
    requires com.example.library;
}

这将使本模块中所有包对所有其他模块都可用于反射(即,这将像在其他模块系统(如OSGi)中看到的行为一样)。如果您想要更细粒度的控制,您可以仅打开特定的包

module com.example.myapp {
    opens com.example.entities;
    requires com.example.library;
}

这将允许对 entities 包进行深度反射,但不会对应用程序模块中的其他包进行深度反射。最后,可以将 opens 子句限制在一个或多个特定目标模块

module com.example.myapp {
    opens com.example.entities to com.example.library;
    requires com.example.library;
}

这样,库模块可以执行深度反射,但其他模块则不能。

无论我们使用哪种选项,库模块现在都可以使实体模块的 entities 包中的类型的私有字段可访问,并随后读取或写入它们的值。

以某种方式打开包允许在Java 9之前编写的库代码继续像以前一样工作。但这需要一些隐式知识。即,应用程序开发人员需要知道哪些库需要访问哪些类型的反射,以便他们可以打开正确的包。在具有多个库执行反射的更复杂的应用程序中,这可能很难管理。

幸运的是,存在一种更明确的做法,即 变量句柄

你能处理变量吗?

变量句柄——由 JEP 193 定义——是Java 9 API的一个强大补充,提供了“在多种访问模式下对[变量]的读写访问”。详细描述它们会远远超出本文的范围(如果您想了解更多,请参阅JEP和这篇文章)。为了我们的目的,让我们关注它们访问字段的特性,这为传统的基于反射的方法提供了一个替代方案。

那么,我们的 FieldValueAccessor 类如何使用变量句柄来实现呢?

通过 MethodHandles.Lookup 类来获取变量句柄。如果此类查找对实体模块具有“私有访问”权限,则允许我们访问该模块类型的私有字段。要获取此类查找,我们在启动库代码时让客户端代码传递一个查找器。

Lookup lookup = MethodHandles.lookup();
FieldValueAccessor accessor = new FieldValueAccessor( lookup );

FieldValueAccessor#getFieldValue() 中,我们现在可以使用 MethodHandles#privateLookupIn() 方法,它将返回一个新的查找器,授予对给定实体实例的私有访问权限。从该查找器,我们可以最终获得一个 VarHandle,它允许我们获取对象的字段值

public class FieldValueAccessor {

    private final Lookup lookup;

    public FieldValueAccessor(Lookup lookup) {
        this.lookup = lookup;
    }

    public Object getFieldValue(Object object, String fieldName) {
        try {
            Class<?> clazz = object.getClass();
            Field field = clazz.getDeclaredField( fieldName );

            MethodHandles.Lookup privateLookup = MethodHandles.privateLookupIn( clazz, lookup );
            VarHandle handle = privateLookup.unreflectVarHandle( field );

            return handle.get( object );
        }
        catch (NoSuchFieldException | IllegalAccessException e) {
            throw new RuntimeException( e );
        }
    }
}

请注意,这只适用于原始查找器是由实体模块中的代码创建的情况。

这是因为 MethodHandles#lookup() 是一个 调用者敏感 的方法,也就是说返回值将取决于该方法的直接调用者。privateLookupIn() 检查给定的查找是否允许对给定类进行深度反射。因此,在实体模块中获得的查找将起作用,而在库模块中检索的查找则毫无用处。

选择哪条路线?

两种讨论的方法都允许库访问 Java 9 模块的私有状态。

var handle 方法使得库模块的要求更加明确,这是我喜欢的。在引导过程中期望获得查找实例应该比要求打开包或模块的相对隐含要求错误更少。

OpenJDK 团队的邮件也 似乎暗示 - 与方法句柄一起,var handle 是长期以来的可行方案。当然,这要求应用程序模块是合作的并传递所需的查找。在容器/应用程序服务器场景中,这还剩下如何看,在这些场景中,库通常不是由应用程序代码而是由服务器运行时引导。在部署时注入一些帮助代码以获取查找对象可能是一个可能的解决方案。

由于 var handle 仅在 Java 9 中引入,如果您希望您的库也能在旧版本的 Java 上运行,您可能想避免使用它们(实际上,您可以通过 构建多版本 JAR 来同时做到这一点)。在早期的 Java 版本中,可以使用方法句柄实现一个非常类似的方法(参见 MethodHandles.Lookup#findGetter())。不幸的是,尽管如此,在 Java 9 和 privateLookupIn​() 引入之前,没有官方的方法来获取具有私有访问权限的方法句柄。讽刺的是,获取此类句柄的唯一方法是通过 一些反射

最后要注意的是,使用 var 和方法句柄可能会有性能优势,因为获取它们时只进行一次访问检查(与每次调用时都进行访问检查相比)。不过,为了了解特定用例中的差异,需要进行适当的基准测试。

一如既往,您的反馈非常受欢迎。您更喜欢哪种方法?或者您可能已经发现了我们以前没有注意到的其他解决方案?请在下面的评论中告诉我们。


返回顶部