Gson反序列化与InaccessibleObjectException异常处理指南
[LOADING...]
1. 概述
从版本 9 开始,Java 非常强调封装。其背后的主要特性是 Java 平台模块系统 (JPMS)。它控制我们自己的包如何暴露,同时限制对 Java 内部类的访问。然而,许多库严重依赖反射来访问私有成员。因此,我们需要以受控方式放宽这些限制的方法。
在本教程中,我们将学习如何在使用 Gson 反序列化 JSON 时避免 InaccessibleObjectException。我们将重点关注模块化项目和日期处理。
2. Gson 的现状
Gson 提供了便捷的工具,用于将对象序列化为 JSON 并反序列化回来,它依赖通过反射访问私有字段。在强封装下,这不再能无缝工作,无论是对我们自己的对象还是 Java 的内部类。最新的 Gson 版本已向公共 API 迈出一小步,特别是针对 record 和 日期。
Gson 目前处于维护模式。因此,我们不应期待任何新功能,只有错误修复和安全修复。
3. 项目设置
让我们检查将要使用的 pom.xml。我们需要 Java 17 来演示强封装,并完整支持 record。因此,我们在 pom.xml 的 properties 部分中进行设置:
接下来,让我们检查 Gson 版本。我们应该使用最新版本:
4. Gson 与 Java 模块
让我们创建一个名为 gson-module 的简单 Java 项目。我们在 module-info.java 文件中声明 gson.exception 模块:
通过 requires 语句,我们声明模块需要 Gson(以及用于日志记录的 SLF4J)。然后,我们通过 exports 语句让我们自己的包 gson.exception 可供其他模块使用。
现在让我们添加一些描述会议的类。第一个是经典的 POJO:
这个对象存储会议的名称和参会人数。它有私有字段以及公共的 getter 和 setter。
接下来,我们准备一个等价的 Java record:
使用 record 时,我们不需要声明字段,也不需要编写 getter 和 setter。
4.1. 使用 POJO 时 Gson 失败
Gson 可以轻松反序列化像 ConferencePojo 这样的 POJO:在类路径项目中可以,但在模块路径项目中不行。为了证明这一点,我们将在模块化项目中运行 GsonModuleMain 应用程序。让我们看看它的 main 方法:
一开始,我们记录 JVM 是否确实从模块路径加载了我们的类。然后我们尝试反序列化 POJO。让我们检查结果:
Gson 抛出了一个由 InaccessibleObjectException 引起的 JsonIOException。 从这两条消息中,我们了解了发生了什么,并获得如何解决问题的提示。简而言之,在模块化项目中,除非我们允许,否则 Gson 无法访问我们类的私有字段。
4.2. 使用 record 成功
使用 record 时,我们会得到完全不同的结果。让我们使用一个 JUnit 5 测试来演示:
这一次,Gson 成功反序列化 JSON 并创建 record。从 2.10 版本开始,Gson 在实例化对象时使用 record 的规范构造器,这与类不同,对于类,Gson 需要访问私有字段。
5. 如何反序列化 POJO
如果我们仍然想使用 POJO,让我们遵循异常消息中的提示。我们有四种方法来解决这个问题。
5.1. 将字段设为 public
让我们创建一个带有 public 字段的 ConferencePojo 副本:
现在 Java 允许 Gson 通过反射访问这些字段,反序列化成功。然而,这样我们会失去封装。
5.2. 使用 opens 暴露私有字段
为了让 Gson 能够处理我们的私有字段,我们可以向其开放我们的包。让我们创建一个新项目 gson-module-opens,并查看其 module-info.java 文件:
通过语句 opens gson.exception to com.google.gson,我们允许 Gson 访问 gson.exception 包中的私有字段。 让我们再次运行 GsonModuleMain 应用程序:
这一次,程序没有抛出异常,并且即使在模块化项目中也能成功反序列化 JSON。
5.3. 使用 TypeAdapter 抽象类
如果我们不能使用 opens,我们可以为我们的 POJO 扩展 Gson 的 TypeAdapter 抽象类。我们需要做的就是实现它的 write 和 read 方法。由于我们关注反序列化,让我们只看 read 方法:
read 方法返回 ConferencePojo 的实例。它遍历 JsonReader 的所有条目,并根据 JSON 名称挑选字段。
请注意,我们使用默认构造器创建 ConferencePojo 对象,并用 setter 填充它。这是关键点:要使用 TypeAdapter,我们需要一个用于创建对象的公共 API。 在我们的例子中,就是 setter,但构造器、public 字段、工厂或构建器也同样可行。
现在让我们测试适配器:
请注意,我们需要使用 GsonBuilder 注册适配器。
5.4. 最后手段:--add-opens 参数
当我们无法修改模块的源代码时,我们还有一个选择。我们可以在程序启动时通过向 JVM 传递 --add-opens 参数来打开包。 让我们看看我们的 gson-module 项目的参数,该项目没有打开其包:
从左开始,我们有模块名 gson.exception,如 module-info.java 文件中所声明。接下来是我们想要打开的包 gson.exception。最后,我们指定授予访问权限的模块 com.google.gson。
这是 opens 语句在启动时的等价形式,有了它,来自 gson-module 的 GsonModuleMain 可以成功反序列化 POJO。然而,我们应该将其视为最后手段,因为该参数存在于代码之外,而且我们必须在每个启动配置中重复它。我们关于非法反射访问的文章更详细地介绍了背景。
6. Gson 与时间
除了模块化项目的问题外,在将 JSON 反序列化为 java.time API 中的日期和时间类(例如 LocalDate、Instant 或 Duration)时,我们也可能遇到相同的异常。 在强封装和较旧的 Gson 版本下,我们会得到一个 JsonIOException,消息为 "Failed making field 'java.time.LocalDate#year' accessible",同样由 InaccessibleObjectException 引起。这一次,Gson 尝试访问 Java 内部类的私有字段。此外,我们无法用 opens 语句解决这个问题,因为它只能打开我们自己的包,而不能打开 Java 的内部包。然而,Gson 2.14.0 已恢复对这些类的支持。
6.1. LocalDate 与结构化 JSON
让我们在一个新的 ConferencePojoWithDate 类中添加一个 conferenceStart 字段。我们继续在 gson-module-opens 项目中进行,因为其他字段是私有的:
现在让我们准备 JSON。我们需要为 conferenceStart 使用嵌套结构:
请注意,键 year、month 和 day 与 LocalDate 的私有字段名完全匹配。 其他时间类也是如此。例如,对于 Duration,嵌套 JSON 提供秒和纳秒,如下所示:
Gson 为了向后兼容而保留了这种命名约定。它不再(也不被允许)访问 Java 内部类的私有字段。相反,作者实现了内置的 TypeAdapter,使用公共的 java.time API 来传输数据。
我们应该特别小心使用正确的字段名。如果 Gson 在 JSON 中找不到匹配的键,它会静默回退到默认值零。对于 LocalDate,零月份会触发一个完全不同的异常,即来自 java.time 中日期验证的 DateTimeException。
接下来,我们无法开箱即用地反序列化 ISO 格式的日期。 因此,像下面这样的普通字符串值会失败并抛出 JsonSyntaxException:
要处理编码为字符串的日期,我们需要实现自定义的 TypeAdapter。
最后,让我们强调同样的规则也适用于 record:我们可以开箱即用地反序列化嵌套时间结构,否则就需要 TypeAdapter。## 7. 结论
在本文中,我们探讨了在现代模块化 Java 中使用 Gson。我们发现,它不再像在 Java 8 和类路径下那样容易使用。
我们在模块化项目中分析了这个问题,并研究了几种让 Gson 符合 JPMS 规则的方法。第一步,我们将类字段设为 public。接下来,我们通过 opens 语句或等效的 --add-opens JVM 参数,让 Gson 能够访问私有字段。
然后,我们实现了一个 TypeAdapter,通过对象的公共 API 来创建对象。我们还看到,Gson 开箱即用地支持 record,并使用其规范构造函数。
最后,我们检查了 Gson 如何处理来自 java.time 包的类。我们注意到,最新版本可以再次按预期反序列化嵌套时间对象。
归根结底,我们有两种主要方式来避免 InaccessibleObjectException。 第一种是向 Gson 的反射开放我们的代码,而通过 opens 语句,我们可以仅将该访问权限授予 Gson。第二种是改用 record 并遵守 JPMS 规则。此外,如果我们有用于创建对象的公共 API,则可以实现一个 TypeAdapter。
一如既往,代码可在 GitHub 上获取。