为什么当我将 JSON 从 Firebase 转换为 Java 对象时,会出现“无法跳转到键入”?加载完整用户部分加载用户部分节省用户数据结构加载完整用户部分加载用户部分节省用户在 JSON 中使用与在 Java 代码中使用的属性名称不同的属性名称

[披露:我是Firebase的工程师。这个问题旨在作为一个参考问题,一次回答许多问题。

我的 Firebase 数据库中有以下 JSON 结构:

{  
  "users": {
    "-Jx5vuRqItEF-7kAgVWy": {
        "handle": "puf",
        "name": "Frank van Puffelen",
        "soId": 209103
    },
    "-Jx5w3IOHD2kRFFgkMbh": {
        "handle": "kato",
        "name": "Kato Wulf",
        "soId": 394010
    },
    "-Jx5x1VWs08Zc5S-0U4p": {
        "handle": "mimming",
        "name": "Jenny Tong",
        "soId": 839465
    }
  }
}

我正在使用以下代码阅读此内容:

private static class User {
    String handle;
    String name;

    public String getHandle() { return handle; }
    public String getName() { return name; }
}

Firebase ref = new Firebase("https://stackoverflow.firebaseio.com/32108969/users");

ref.addListenerForSingleValueEvent(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot usersSnapshot) {
        for (DataSnapshot userSnapshot : usersSnapshot.getChildren()) {
          User user = userSnapshot.getValue(User.class);
          System.out.println(user.toString());
        }
    }

    @Override
    public void onCancelled(FirebaseError firebaseError) { }
});

但是我得到这个错误:

线程“FirebaseEventTarget” com.firebase.client.FirebaseException 中的异常:无法跳转到类型

如何将我的用户读入 Java 对象?


答案 1

Firebase 使用 Jackson 允许将 Java 对象序列化为 JSON,并将 JSON 反序列化回 Java 对象。您可以在杰克逊网站上找到有关杰克逊的更多信息,也可以在有关杰克逊注释的此页面上找到。

在这个答案的其余部分,我们将展示一些将Jackson与Firebase一起使用的常见方法。

加载完整用户

将用户从Firebase加载到Android的最简单方法是创建一个完全模仿JSON中属性的Java类:

private static class User {
  String handle;
  String name;
  long stackId;

  public String getHandle() { return handle; }
  public String getName() { return name; }
  public long getStackId() { return stackId; }

  @Override
  public String toString() { return "User{handle='"+handle+“', name='"+name+"', stackId="+stackId+"\’}”; }
}

我们可以在侦听器中使用此类:

Firebase ref = new Firebase("https://stackoverflow.firebaseio.com/32108969/users");

ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot usersSnapshot) {
    for (DataSnapshot userSnapshot : usersSnapshot.getChildren()) {
      User user = userSnapshot.getValue(User.class);
      System.out.println(user.toString());
    }
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) { }
});

您可能会注意到,User 类遵循 JavaBean 属性模式。每个 JSON 属性都由 User 类中的一个字段映射,我们为每个字段都有一个公共 getter 方法。通过确保所有属性都以完全相同的名称进行映射,我们确保 Jackson 可以自动映射它们。

您还可以通过将 Jackson 注释放在 Java 类及其字段和方法上来手动控制映射。我们将在下面介绍两个最常见的注释( 和 )。@JsonIgnore@JsonIgnoreProperties

部分加载用户

假设您在 Java 代码中只关心用户名和句柄。让我们删除 并查看会发生什么:stackId

private static class User {
  String handle;
  String name;

  public String getHandle() { return handle; }
  public String getName() { return name; }

  @Override
  public String toString() { 
    return "User{handle='" + handle + “\', name='" + name + "\’}”; 
  }
}

如果我们现在附加与以前相同的侦听器并运行程序,它将引发异常:

Exception in thread "FirebaseEventTarget" com.firebase.client.FirebaseException: Failed to bounce to type

at com.firebase.client.DataSnapshot.getValue(DataSnapshot.java:187)

at com.firebase.LoadPartialUsers$1.onDataChange(LoadPartialUsers.java:16)

“未能反去抖类型”表示 Jackson 无法将 JSON 反序列化为 User 对象。在嵌套异常中,它告诉我们为什么:

Caused by: com.shaded.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "stackId" (class com.firebase.LoadPartialUsers$User), not marked as ignorable (2 known properties: , "handle", "name"])

 at [Source: java.io.StringReader@43079089; line: 1, column: 15] (through reference chain: com.firebase.User["stackId"])

at com.shaded.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.from(UnrecognizedPropertyException.java:79)

Jackson 在 JSON 中发现了一个属性,但不知道该如何处理它,因此它引发了一个异常。幸运的是,有一个注释,我们可以用它来告诉它在将JSON映射到我们的类时忽略JSON中的特定属性:stackIdUser

@JsonIgnoreProperties({ "stackId" })
private static class User {
  ...
}

如果我们不再使用侦听器运行代码,Jackson 将知道它可以在 JSON 中忽略,并且能够再次将 JSON 反序列化为 User 对象。stackId

由于在 Firebase 应用程序中向 JSON 添加属性是一种常见的做法,因此您可能会发现,简单地告诉 Jackson 忽略 Java 类中没有映射的所有属性会更方便:

@JsonIgnoreProperties(ignoreUnknown=true)
private static class User {
  ...
}

现在,如果我们稍后将属性添加到 JSON,Java 代码仍将能够加载 s。请记住,用户对象不会包含 JSON 中存在的所有信息,因此在再次将其写回 Firebase 时要小心。User

部分节省用户

拥有自定义Java类很好的一个原因是我们可以为其添加方便的方法。假设我们添加了一个方便的方法,用于获取要为用户显示的名称:

private static class User {
  String handle;
  String name;

  public String getHandle() { return handle; }
  public String getName() { return name; }

  @JsonIgnore
  public String getDisplayName() {
    return getName() + " (" + getHandle() + ")";
  }

  @Override
  public String toString() { 
    return "User{handle='" + handle + "\', name='" + name + "\', displayName='" + getDisplayName() + "'}"; 
  }
}

现在,让我们从 Firebase 读取用户,并将其写回新位置:

Firebase srcRef = new Firebase("https://stackoverflow.firebaseio.com/32108969/users");
final Firebase copyRef = new Firebase("https://stackoverflow.firebaseio.com/32108969/copiedusers");

srcRef.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot usersSnapshot) {
    for (DataSnapshot userSnapshot : usersSnapshot.getChildren()) {
      User user = userSnapshot.getValue(User.class);
      copyRef.child(userSnapshot.getKey()).setValue(user);
    }
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) { }
});

节点中的 JSON 如下所示:copiedusers

"copiedusers": {
    "-Jx5vuRqItEF-7kAgVWy": {
        "displayName": "Frank van Puffelen (puf)",
        "handle": "puf",
        "name": "Frank van Puffelen"
    },
    "-Jx5w3IOHD2kRFFgkMbh": {
        "displayName": "Kato Wulf (kato)",
        "handle": "kato",
        "name": "Kato Wulf"
    },
    "-Jx5x1VWs08Zc5S-0U4p": {
        "displayName": "Jenny Tong (mimming)",
        "handle": "mimming",
        "name": "Jenny Tong"
    }
}

这与源 JSON 不同,因为 Jackson 将新方法识别为 JavaBean getter,因此向它输出的 JSON 添加了一个属性。我们通过向 中添加注释来解决此问题。getDisplayName()displayNameJsonIgnoregetDisplayName()

    @JsonIgnore
    public String getDisplayName() {
        return getName() + "(" + getHandle() + ")";
    }

序列化 User 对象时,Jackson 现在将忽略该方法,我们写出的 JSON 将与我们获取的 JSON 相同。getDisplayName()


答案 2

Firebase SDK for Android/Java 的 9.x(及更高版本)版本已停止,包括用于序列化/反序列化 Java<->JSON 的 Jackson。相反,较新的 SDK 提供一组最少的自定义注释,以允许控制最常见的自定义需求,同时对生成的 JAR/APK 大小的影响最小。


如果您符合以下条件,我的原始答案仍然有效:

本答案的其余部分介绍如何在 Firebase SDK 9.0 或更高版本中处理序列化/反序列化方案。


数据结构

我们将在 Firebase 数据库中从此 JSON 结构开始:

{
  "-Jx86I5e8JBMZ9tH6W3Q" : {
    "handle" : "puf",
    "name" : "Frank van Puffelen",
    "stackId" : 209103,
    "stackOverflowId" : 209103
  },
  "-Jx86Ke_fk44EMl8hRnP" : {
    "handle" : "mimming",
    "name" : "Jenny Tong",
    "stackId" : 839465
  },
  "-Jx86N4qeUNzThqlSMer" : {
    "handle" : "kato",
    "name" : "Kato Wulf",
    "stackId" : 394010
  }
}

加载完整用户

最基本的是,我们可以将每个用户从此 JSON 加载到以下 Java 类中:

private static class CompleteUser {
    String handle;
    String name;
    long stackId;

    public String getHandle() { return handle; }
    public String getName() { return name; }
    public long getStackId() { return stackId; }

    @Override
    public String toString() { return "User{handle='"+handle+"', name='"+name+"', stackId="+stackId+ "'}"; }
}

如果我们将字段声明为公共字段,我们甚至不需要 getter:

private static class CompleteUser {
    public String handle;
    public String name;
    public long stackId;
}

部分加载用户

我们还可以部分加载用户,例如:

private static class PartialUser {
    String handle;
    String name;

    public String getHandle() {
        return handle;
    }
    public String getName() { return name; }

    @Override
    public String toString() {
        return "User{handle='" + handle + "', NAME='" + name + "''}";
    }
}

当我们使用此类从同一 JSON 加载用户时,代码将运行(与我的其他答案中提到的 Jackson 变体不同)。但是,您将在日志记录输出中看到一条警告:

警告:在类 Annotations$PartialUser 上找不到 stackId 的 setter/字段

因此,去掉它,我们可以用:@IgnoreExtraProperties

@IgnoreExtraProperties
private static class PartialUser {
    String handle;
    String name;

    public String getHandle() {
        return handle;
    }
    public String getName() { return name; }

    @Override
    public String toString() {
        return "User{handle='" + handle + "', NAME='" + name + "''}";
    }
}

部分节省用户

与以前一样,您可能希望向用户添加计算属性。在将数据保存回数据库时,您需要忽略此类属性。为此,您可以使用以下内容对属性/getter/setter/field 进行批注:@Exclude

private static class OvercompleteUser {
    String handle;
    String name;
    long stackId;

    public String getHandle() { return handle; }
    public String getName() { return name; }
    public long getStackId() { return stackId; }

    @Exclude
    public String getTag() { return getName() + " ("+getHandle()+")"; }

    @Override
    public String toString() { return "User{handle='"+handle+"', name='"+name+"', stackId="+stackId+ "'}"; }
}

现在,将用户写入数据库时,将忽略 的值。getTag()

在 JSON 中使用与在 Java 代码中使用的属性名称不同的属性名称

您还可以指定 Java 代码中的字段/getter/setter 在数据库的 JSON 中应获取的名称。为此,请使用 注释字段/getter/setter。@PropertyName()

private static class UserWithRenamedProperty {
    String handle;
    String name;
    @PropertyName("stackId")
    long stackOverflowId;

    public String getHandle() { return handle; }
    public String getName() { return name; }
    @PropertyName("stackId")
    public long getStackOverflowId() { return stackOverflowId; }

    @Override
    public String toString() { return "User{handle='"+handle+"', name='"+name+"', stackId="+stackOverflowId+ "'}"; }
}

通常,最好使用 Firebase SDK 使用的 Java<-> JSON 之间的默认映射。但是,当您有一个预先存在的JSON结构时,可能需要它,否则您无法将其映射到Java类。@PropertyName


推荐