65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 GSON 进行 Java/JSON 映射

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (9投票s)

2013年6月10日

CPOL

10分钟阅读

viewsIcon

75685

使用 GSON 进行 Java/JSON 映射。

引言

如今,如果您需要使用或生成**Web API**,很可能需要使用 **JSON**。
JSON 在 Web 世界中如此普及是有充分理由的

  • 首先,JSON 与 **JavaScript**(Web 的世界语,随着 **服务器端 JavaScript** 平台(如 **Node.js**)的兴起,这并非言过其实)结合得非常好:**JavaScript 对象树** 的字符串化会生成 JSON 文档(但并非所有 JSON 文档都是有效的 JavaScript 对象),
  • 其次,JSON 比 **XML** **不那么冗长**(请参阅我关于此主题的另一篇文章),并且可以用于 XML 历史上用于的大多数场景

因此,无论您使用何种语言和平台,都需要一个强大的 **JSON 解析组件**。

在 **Java** 世界中,至少有两个不错的选择:**Gson** 和 **Jackson**。在本文中,我将演示如何使用 **Gson**:我将从一个(不那么)简单的用例开始,它只是工作,然后展示如何处理不那么标准的情况,例如命名差异、以 Gson 不原生支持的格式表示的数据或类型保留。

源代码以及一组 **JUnit 测试**,可在此存档中获取:Gson Post Source Code

简单的 Java-JSON 映射

模型

这是一个**文件系统**的简化表示

    +--------------------+
    |   FileSystemItem   |
    +--------------------+
    | name: string       |
    | creationDate: date |
    | hidden: boolean    |
    +--------------------+
       ^               ^
       |               |
+------------+      +--------+
|    File    |<--*--| Folder |-----+
+------------+      +--------+     |*
| size: long |      |        |<----+
+------------+      +--------+

因此,`File` 和 `Folder` 继承自 `FileSystemItem`,`File` 有一个大小,`Folder` 可以包含任意数量的文件和文件夹。这个模型至少有四个有趣的原因

  • 它使用了 JSON 的所有**原始类型**:**字符串**、**布尔值**和**数字**,以及不是原生 JSON 类型但非常常见的**日期**
  • 它使用了复杂的**对象**和**数组**
  • 它使用了**继承**
  • 它使用了**递归数据结构**:*文件夹*

它很好地说明了**双向 Java-Json 映射**的所有有趣之处。

Java 实现

这是这个模型在 **Java** 中的实现

FileSystemItem:

import java.util.Date;
public class FileSystemItem
{
    private String name;
    private Date creationDate;
    private Boolean hidden;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public Date getCreationDate() {
        return creationDate;
    }
    
    public void setCreationDate(Date creationDate) {
        this.creationDate = creationDate;
    }
    
    public Boolean isHidden() {
        return hidden;
    }
    
    public void setHidden(Boolean hidden) {
        this.hidden = hidden;
    }
    
    public FileSystemItem(String name)
    {
        this(name, null);
    }
    
    public FileSystemItem(String name, Date creationDate)
    {
        this.name = name;
        this.creationDate = creationDate;
    }
}

文件:

import java.util.*;
public class File extends FileSystemItem
{
    private long size;
    public long getSize() {
        return size;
    }
    public void setSize(long size) {
        this.size = size;
    }
    
    public File(String name)
    {
        super(name);
    }
    
    public File(String name, Date creationDate)
    {
        super(name, creationDate);
    }
    
    public File(String name, long size)
    {
        super(name);
        this.size = size;
    }
    
    public File(String name, Date creationDate, long size)
    {
        super(name, creationDate);
        this.size = size;
    }
}

以及 *Folder*

import java.util.*;
public class Folder extends FileSystemItem
{
    private File[] files;
    public File[] getFiles() {
        return files;
    }
    
    private Folder[] folders;
    public Folder[] getFolders() {
        return folders;
    }
    
    public Folder(String name, FileSystemItem...items)
    {
        super(name);
        
        List<File> files = new ArrayList<File>();
        List<Folder> folders = new ArrayList<Folder>();
        
        for(FileSystemItem item : items)
        {
            if (item instanceof File)
            {
                files.add((File)item);
            }
            else
            {
                folders.add((Folder)item);
            }
        }
        
        this.files = files.toArray(new File[files.size()]);
        this.folders = folders.toArray(new Folder[folders.size()]);
    }
}

面向对象设计理论派的各位,在您提问之前:是的,*FileSystemItem* 可以/应该被设置为**抽象**,但这旨在演示类型丢失问题

使用 Gson 进行映射

使用 **Gson**,负责将 Java 序列化为 JSON 并将 JSON 反序列化为 Java 的类名为 *Gson*,这很合乎逻辑。更具体地说,我们分别使用“*toJson*”和“*fromJson*”方法。

让我们尝试将此文件系统树 JSON 序列化

+ /
|
+----+ /tmp
|    |
|    +--+ cache.tmp
|
+----+ /opt
|    |
|    +--+ /opt/Chrome
|       |
|       +--+ /opt/Chrome/chrome
|
+----+ /home
     |
     +--+ /home/john
     |
     +--+ /home/kate

这是一段 **Java** 代码,它创建树,将其序列化为 JSON,然后反序列化回 Java

Date time = Calendar.getInstance().getTime();
    
Folder root = new Folder
("/",
        new Folder("tmp",
                new File("cache.tmp", time)),
        new Folder("opt",
                new Folder("Chrome",
                        new File("chrome", 123456))),
        new Folder("home",
                new Folder("john"),
                new Folder("kate"))                
);
    
Gson gson = new Gson();
String json = gson.toJson(root);
    
Folder outputRoot = gson.fromJson(json, Folder.class);

存储在“*json*”变量中的树的 JSON 表示是

{
  "files" : [  ],
  "folders" : [ { "files" : [ { "creationDate" : "Jun 8, 2013 4:41:29 PM",
              "name" : "cache.tmp",
              "size" : 0
            } ],
        "folders" : [  ],
        "name" : "tmp"
      },
      { "files" : [  ],
        "folders" : [ { "files" : [ { "name" : "chrome",
                    "size" : 123456
                  } ],
              "folders" : [  ],
              "name" : "Chrome"
            } ],
        "name" : "opt"
      },
      { "files" : [  ],
        "folders" : [ { "files" : [  ],
              "folders" : [  ],
              "name" : "john"
            },
            { "files" : [  ],
              "folders" : [  ],
              "name" : "kate"
            }
          ],
        "name" : "home"
      }
    ],
  "name" : "/"
}

您可以运行名为“*canSerializeATree*”的测试来确信它按预期工作。

是的,就这么简单:3 行代码即可序列化和反序列化……至少在您没有太特殊要求的情况下。

通过自定义 JSON 映射来帮助 Gson

正如您在第一部分中看到的那样,映射相对复杂的数据结构非常简单,因为 Gson 处理了大部分繁重的工作。但它不能做所有事情,有时它需要一些帮助才能正确序列化和反序列化。

在第二部分中,我将介绍一些需要**自定义映射**的常见用例。

命名差异

第一个用例可能是最常见的,如果您曾经进行过**对象 XML 映射**,很可能您遇到过它:JSON 文档和类中的属性名称不相同。

这种情况非常普遍,解决它就像在字段上添加一个注解一样简单:**@SerializedName**

这是一个使用此注解的基本 *Mail* 类

import com.google.gson.annotations.SerializedName;
public class Mail
{
    @SerializedName("time")
    private int timestamp;
    
    public int getTimestamp()
    {
        return timestamp;
    }
    
    public Mail(int timestamp)
    {
        this.timestamp = timestamp;
    }
}

当我们将其序列化为 JSON 时

int timestamp = (int)new Date().getTime() / 1000;
Mail mail = new Mail(timestamp);
        
Gson gson = new Gson();
String json = gson.toJson(mail);

我们得到

{"time":629992}

而不是

{"timestamp":629992}

您可以运行“*canCustomizeTheNames*”单元测试亲自验证。

映射布尔整数

最近我使用了一个 **第三方 API**,它使用 JSON 但用整数表示布尔值,1 表示真,0 表示假;这对于没有真正布尔类型的语言来说是一种常见模式(或至少曾经是)。

从第三方 API 映射

如果您打算将这些值映射到使用“真”布尔值的 Java 对象表示中,您需要稍微帮助 Gson。
例如,这是一个简单的尝试

Gson gson = new Gson();
File file = gson.fromJson("{hidden: 1}", File.class); 

当您运行它时,Gson 会抛出此异常

com.google.gson.JsonSyntaxException:
java.lang.IllegalStateException:
Expected a boolean but was NUMBER at line 1 column 11

因为 1 确实不是有效的布尔值。

因此,我们将为 Gson 提供一个**钩子**,一个用于布尔值的**自定义反序列化器**,即实现 **_JsonDeserializer_** **接口**的类

import java.lang.reflect.Type;
import com.google.gson.*;
class BooleanTypeAdapter implements JsonDeserializer<Boolean>
{
      public Boolean deserialize(JsonElement json, Type typeOfT, 
             JsonDeserializationContext context) throws JsonParseException
      {
          int code = json.getAsInt();
          return code == 0 ? false :
                   code == 1 ? true :
                 null;
      }
}

要使用它,我们需要稍微改变获取 Gson 映射器实例的方式,使用一个**工厂对象**,即 **_GsonBuilder_**,这是 Java 中的常见模式

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Boolean.class, new BooleanTypeAdapter());
Gson gson = builder.create();
File file = gson.fromJson("{hidden: 1}", File.class); 

这一次不再有异常,如果您调试“**_BooleanTypeAdapter_**”类,您会看到 **Gson** 像预期一样调用其“**_deserialize_**”方法;因此,我们现在能够读取第三方发送的值。

映射到第三方 API

但是,如果我们需要进行双向映射呢:我们不仅需要反序列化,还需要将一些数据序列化回去?

为了说明这一点,假设第三方 API 向我们发送了这种文件表示

public class ThirdPartyFile
{
    private String name;
    private int hidden;
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
    
    public int isHidden() {
        return hidden;
    }
    
    public void setHidden(int hidden) {
        this.hidden = hidden;
    }    
}

如果客户端使用其本地 *File* 表示发送文件,我们得到这个 JSON

{name: "test.txt", hidden: true}

但是如果第三方尝试解析此 JSON,我们用此代码模拟

Gson gson = new Gson();
ThirdPartyFile file = 
  gson.fromJson("{name: \"test.txt\", hidden: true}", ThirdPartyFile.class);

它会感到惊讶

com.google.gson.JsonSyntaxException:
java.lang.IllegalStateException:
Expected an int but was BOOLEAN at line 1 column 14

这与我们尝试反序列化第三方 API JSON 时遇到的错误完全相反。

为了确保我们生成对 API 有效的 JSON,我们还需要**自定义序列化**,这就像实现 **_JsonSerializer_** 接口一样简单。

我们不需要创建一个专门的类,而是可以简单地用这个新功能来增强我们之前的实现

import java.lang.reflect.Type;
import com.google.gson.*;
class BooleanTypeAdapter implements JsonSerializer<Boolean>, JsonDeserializer<Boolean>
{
      public JsonElement serialize(Boolean value, Type typeOfT, JsonSerializationContext context)
      {
          return new JsonPrimitive(value ? 1 : 0);
      }
    
      public Boolean deserialize(JsonElement json, Type typeOfT, 
             JsonDeserializationContext context) throws JsonParseException
      {
          int code = json.getAsInt();
          return code == 0 ? false :
                   code == 1 ? true :
                 null;
      }
}

这是一个模拟客户端和第三方之间交互的程序

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Boolean.class, new BooleanTypeAdapter());
Gson gson = builder.create();
// We generate some JSON for the third-party API...
String json = gson.toJson(file);
// we send the JSON to the API...
// it parses the file in its own representation...
ThirdPartyFile otherFile = gson.fromJson(json, ThirdPartyFile.class);
// then it returns us the same file as its JSON representation...
String otherJson = gson.toJson(otherFile);
// we finally try to read it and ensure all is fine.
File outFile = gson.fromJson(otherJson, File.class);

此场景已在“_canMapToAndFromThirdParty_”**JUnit 测试**中实现。

映射状态码

我使用 API 遇到的一个类似用例是将**数字错误代码**(例如 0 和 1)映射到更有意义的**枚举值**(例如“_OK_”和“_KO_”)。

这是一个简化的情况

enum Status
{
    OK,
    KO
}
public class Response
{
    private Status status;
    
    public Status getStatus()
    {
        return status;
    }
    
    public Response(Status status)
    {
        this.status = status;
    }
}

如果您天真地尝试解析像“{status:1}”这样的 JSON,目标 *status* 属性将绝望地保持为 *null*。

再一次,**自定义适配器**来救援

import java.lang.reflect.Type;
import com.google.gson.*;
class StatusTypeAdapter implements JsonSerializer<Status>, JsonDeserializer<Status>
{
      public JsonElement serialize(Status value, Type typeOfT, JsonSerializationContext context)
      {
          return new JsonPrimitive(value.ordinal());
      }
    
      Status[] statuses = Status.values();
      
      public Status deserialize(JsonElement json, Type typeOfT, 
              JsonDeserializationContext context) throws JsonParseException
      {
          int code = json.getAsInt();
          
          return code < statuses.length ? statuses1 : null;
      }
}

这是一个示例

GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Status.class, new StatusTypeAdapter());
Gson gson = builder.create();
Response responseOK = gson.fromJson("{status:0}", Response.class);
Response responseKO = gson.fromJson("{status:1}", Response.class);

正如预期的那样,这次“_responseOK_”的状态是“_Status.OK_”,“_responseKO_”的状态是“_Status.KO_”。
如果您想实时检查它,请运行“_canMapStatusCodes_”JUnit 测试。

修复日期格式

处理 JSON 时另一个常见的要求是**将日期表示固定为约定格式**。例如,如果您正在与使用 **ISO 8601** 格式(例如“**yyyy-MM-ddTHH:mm:ss**”,即 2013-06-08T18:05:23)的 API 进行通信,那么您需要通知 Gson 它应该使用另一种日期格式来解释它读取的日期并序列化它写入的日期。这可以通过使用 *GsonBuilder* 类的“**_setDateFormat_**”**方法**来完成,这是一个示例

Calendar calendar = Calendar.getInstance();
calendar.set(2013, 05, 8, 18, 05, 23);
Date time = calendar.getTime();
GsonBuilder builder = new GsonBuilder();
builder.setDateFormat("yyyy-MM-dd'T'HH:mm:ss");
Gson gson = builder.create();
String json = gson.toJson(time); 

json 将是

"2013-06-08T18:05:23"

而不是默认值

"Jun 8, 2013 6:05:23 PM"

相应的单元测试名为“_canCustomizeTheDateFormatting_”。

映射复合字符串

有时,数据的 JSON 表示是紧凑的,例如,我最近使用了一个 API,它将文件格式 A 到另一种格式 B 的转换表示为“A2B”,例如“PDF2XLS”,而不是使用更冗长的表示,如“{ from: "PDF", to: "XLS" }”。

为了展示如何使用 Gson 映射此类表示,我将使用一个类似的用例,**货币对**,这是一个完美的候选者,因为紧凑版本(例如 **EUR/USD**)几乎总是被使用。

这是我们的 *CurrencyPair* 类

public class CurrencyPair
{
    private String baseCurrency;
    private String counterCurrency;
    
    public String getBaseCurrency()
    {
        return baseCurrency;
    }
    
    public String getCounterCurrency()
    {
        return counterCurrency;
    }
    
    public CurrencyPair(String baseCurrency, String counterCurrency)
    {
        this.baseCurrency = baseCurrency;
        this.counterCurrency = counterCurrency;
    }
}

但我们不希望其 JSON 表示为

{
    baseCurrency: "EUR",
    counterCurrency: "USD",
}

而是简单地

"EUR/USD"

因此,我们再次编写了一个**专门的类型适配器**,它将负责序列化紧凑表示并反序列化扩展表示

import java.lang.reflect.Type;
import com.google.gson.*;
class CurrencyPairTypeAdapter implements 
      JsonSerializer<CurrencyPair>, JsonDeserializer<CurrencyPair>
{
      public JsonElement serialize(CurrencyPair value, Type typeOfT, JsonSerializationContext context)
      {
          return new JsonPrimitive(value.getBaseCurrency() + 
                     "/" + value.getCounterCurrency());
      }
    
      public CurrencyPair deserialize(JsonElement json, Type typeOfT, 
             JsonDeserializationContext context) throws JsonParseException
      {
          String currencyPairStr = json.getAsString();
          
          String[] tokens = currencyPairStr.split("/");
          
          return new CurrencyPair(tokens[0], tokens[1]);
      }
}

这是一个往返示例

CurrencyPair EURUSD = new CurrencyPair("EUR", "USD");
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(CurrencyPair.class, new CurrencyPairTypeAdapter());
Gson gson = builder.create();
String json = gson.toJson(EURUSD);
CurrencyPair outEURUSD = gson.fromJson(json, CurrencyPair.class);

其单元测试名为“_canWriteAndReadCurrencyPairs_”。

保留类型信息

**JSON 冗余减少**的“缺点”之一是您会丢失任何序列化对象的类型,而 XML,无论您是否愿意,都会保留它,这也是 XML 冗余的原因之一。

一个非问题?

让我们看一个具体的例子,一个名为“test”的 *File* 的表示之间的区别
在 XML 中你会得到

<file name="test" />

在 JSON 中你会得到

{ name: "test" }

所以你丢失了类型信息,它是文件、文件夹、用户……?你不能仅仅通过查看 JSON 文档来判断。你必须依赖对应于该对象的属性类型,例如,如果文件实例是 Database 类的一部分

class Database
{
    File file;
}

它将被序列化为

{ file:{
       name: "test"
   }
}

所以解析器查看 `Database` 类中的目标属性“file”,并看到它是 `File` 类型,所以它知道“{ name: "test" }”应该表示一个 `File` 实例,并将正确地反序列化它;所以这似乎不是问题……

嗯,并非总是如此

但是如果目标属性可以有多种可能的类型呢,也就是说,当它使用某种形式的**多态性**时?

例如,我们将创建另一个文件夹类,比如说 *Folder2*(很原始,不是吗?),但这次我们不将文件和子文件夹存储在两个不同的集合中,而是希望只使用一个集合,一个它们的共同超类 *FileSystemItem* 的数组。

public class Folder2 extends FileSystemItem
{
    private FileSystemItem[] items;
    public FileSystemItem[] getItems() {
        return items;
    }
    
    public Folder2(String name, FileSystemItem...items)
    {
        super(name, null, null);
        
        this.items = items;
    }
}

如果您还记得之前的_Folder_类,您会同意这是一个更简洁的实现,代码更少,也不再需要繁琐的管道。

使用它,我们将重现这个简单的文件系统

+ /
|
+----+ /tmp
|    
+----+ test.txt

所以我们首先直接天真地使用 Gson 解析器

Folder2 folder = new Folder2("/", 
          new Folder2("tmp"), new File("test.txt"));
Gson gson = new Gson();
String json = gson.toJson(folder);

以下是生成的 JSON

{ "items" : [ { "items" : [  ],
        "name" : "tmp"
      },
      { "name" : "test.txt",
        "size" : 0
      }
    ],
  "name" : "/"
}

如你所见,我们丢失了所有类型信息,如果我们反序列化它,我们将得到两个 `FileSystemItem` 实例,而不是一个 `Folder2` 和一个 `File`。

确实,当解析器查看 `items` 属性时,它所拥有的所有信息就是它是一个 `FileSystemItem` 数组。您可能会反驳说这并不完全正确:第一个对象有一个“items”属性,这意味着它不可能是 `File`,只能是 `Folder`,而第二个对象有一个“size”属性,所以它只能是 `File`,您说得对;但是,首先,在一般情况下,这并非总是如此,并且可能会出现真正的歧义,其次,想象一下 JSON 解析器的猜测是错误的(因为它无法知道所有可能存在于其他 jar 中的现有类型),并且您反序列化了错误的类型,这可能会产生非常令人烦恼的后果。

请注意,如果 `FileSystemItem` 是一个**抽象**类,这个问题会更加明显,因为在这种情况下,甚至实例化 `FileSystemItem` 都是不可能的。

解决方案

因此,我们需要通过将类型信息与数据一起序列化来保留它。像往常一样,这通过拦截正常的序列化过程来完成,但这次这并非易事。幸运的是,一个聪明的人已经在 **_RuntimeTypeAdapterFactory_ 类**中为我们实现了所有必要的管道

当您将此类的实例添加到映射过程中时,生成的 JSON 中的每个对象都将通过一个新的属性进行丰富,该属性将保存原始类型名称;我选择将其命名为“**$type**”,因为它很明显,几乎不可能与“正常”属性发生**冲突**,并且此约定被另一个出色的 JSON 库 **Json.NET** 使用。

以下是使用方法

Folder2 folder = new Folder2("/", 
         new Folder2("tmp"), new File("test.txt"));
RuntimeTypeAdapterFactory<FileSystemItem> factory = 
         RuntimeTypeAdapterFactory.of(FileSystemItem.class, "$type")
         .registerSubtype(File.class)
         .registerSubtype(Folder2.class);
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapterFactory(factory);
Gson gson = builder.create();
String json = gson.toJson(folder);

这次生成的 JSON 如下

{ "items" : [ { "$type" : "Folder2",
        "items" : [  ],
        "name" : "tmp"
      },
      { "$type" : "File",
        "name" : "test.txt",
        "size" : 0
      }
    ],
  "name" : "/"
}

如果我们反序列化它,我们将检索原始类型:一个 *Folder* 和一个 *File*。

用于检查此内容的单元测试名为“`canPreserveTypeInformationForASimpleTree`”。

结论

从现在开始,您知道如何使用 Gson 映射对象树以及在有特殊需求时自定义映射过程。

如果您使用过其他库,例如 **Jackson**,来完成类似的工作,我很乐意听取您的意见,所以请留下您的**反馈**。

如果您在代码、拼写错误或问题方面有任何疑问,请随时在评论中提出。


要关注此博客,请订阅 RSS 订阅 RSS Feed

© . All rights reserved.