如何使用反射递归序列化对象?

Posted

技术标签:

【中文标题】如何使用反射递归序列化对象?【英文标题】:How to recursively serialize an object using reflection? 【发布时间】:2011-02-07 01:29:32 【问题描述】:

我想导航到对象的第 N 级,并以字符串格式序列化它的属性。 例如:

class Animal 
   public String name;
   public int weight;
   public Animal friend;
   public Set<Animal> children = new HashSet<Animal>() ;

应该这样序列化:

name:"Monkey",
 weight:200,
 friend:name:"Monkey Friend",weight:300 ,children:...if has children,
 children:name:"MonkeyChild1",weight:100,children:... recursively nested

您可能会注意到它类似于将对象序列化为 json。我知道有很多库(Gson,Jackson...)可以做到这一点,你能给我一些关于如何自己写这个的指导性想法吗?

【问题讨论】:

这是否有特定的部分被证明是困难的? 如果你知道那里有很多库,为什么不直接使用一个? 您能告诉我们到目前为止您尝试了什么,以便我们有一个起点吗? @Justin Niessner :我不知道如何检查应该递归序列化的字段。 这只是简单的递归:内省一种类型(具有超类型等)的字段、方法(参见 JDK javadocs for Class);使用 java.lang.reflect.Field 查找字段的实际值,冲洗重复。如果你想变得花哨,跟踪已经解析的对象来处理循环(或共享引用)。 【参考方案1】:

序列化基本上是深度克隆。

您需要跟踪每个对象引用是否重复出现(例如,使用IdentityHashMap)。你的最终实现方法是什么(如果不是外部库)记得检查对象重复出现,否则你可能会陷入无限循环(当对象 A 引用对象 B 时又引用对象 A 或更复杂的循环对象图)。

一种方法是使用类似 DFS 的算法遍历对象图并从那里构建克隆(序列化字符串)。

这个伪代码有望解释如何:

visited = 
function visit(node) 
  if node in visited 
    doStuffOnReoccurence(node)
    return
  
  visited.add(node)
  doStuffBeforeOthers(node)
  for each otherNode in node.expand() visit(otherNode)
  doStuffAfterOthers(node)

示例中的访问集是我将使用身份集(如果有的话)或 IdentityHashMap 的地方。

当反射性地查找字段时(即 node.expand() 部分),请记住也要遍历超类字段。

反射不应该用在“正常”的开发案例中。反射将代码作为数据处理,您可以忽略所有正常的对象访问限制。我仅将这种反射式深拷贝用于测试:

    在检查不同类型对象的深度对象图相等性的测试中

    在分析对象图大小和其他属性的测试中

【讨论】:

是的,你提到了IdentityHashMap,我注意到的作者在他的示例代码中也使用了IdentityHashMap。但是我不懂IdentityHashMap,即使我读过java文档关于它。 与法线贴图的不同之处在于它使用标识(==)作为键而不是等号。当你进行深度克隆时,你需要知道什么时候真的是同一个对象..【参考方案2】:

Google Gson 可以在一行中完成此特定任务:

String json = new Gson().toJson(animal);

顺便说一句,反过来也一样简单:

Animal animal = new Gson().fromJson(json, Animal.class);

我还没有看到另一个对泛型、集合/映射和(嵌套)javabean 提供更好支持的 JSON 序列化程序。

更新: 言归正传,您只需要了解反射 API。我建议先让自己通过Sun tutorial on the subject。简而言之,您可以使用Object#getClass()java.lang.Classjava.lang.reflect.Method等提供的所有方法来确定一个和另一个。 Google Gson 是开源的,您也可以从中受益。

【讨论】:

谢谢,我经常使用 Gson 和 jackson、jsonlib。我想知道如何使用反射来编写它。 嗯?为什么要重新发明***?无论如何:Gson 是开源的。受益匪浅。 lol,json之父说,reinvent Wheels的好处是,你可以找到一个round one。是的,使用 Gson 非常简单,但是我仍然不知道如何自己编写,如果有一天我遇到类似的问题但没有可以使用的库怎么办? 我不确定这是否有意义,但没关系。 :) 但即便如此,也许看看其他库是如何做到的?还有更简单的包;例如,Hessian 具有相当简单的基于反射的序列化器/反序列化器。 JDK 有 bean 序列化器(忘记了它的名字),等等。去看看那些库是怎么做的。但这一切都归结为使用 java.lang.Class 中的方法 - getDeclaredFields / Methods 等(并为超类型递归地执行此操作)。然后动态访问字段/方法。没有什么特别花哨的。 在问这个问题之前,我已经检查了 Gson 源。 Gson 是一个成熟的序列化器,可以处理大多数情况。它的源代码有点过度封装,我认为对于简单的问题,这可以在 100 行代码内完成。【参考方案3】:

解决此问题的一种简洁方法是使用 visitor 模式来将编码实现与业务对象分开。有些人会争辩说,您可以简单地实现Externalizable 接口以及readExternal / writeExternal 但这有以下问题:

您的协议嵌入在您的业务对象中,这意味着它分布在您的代码库中,而不是在一个地方。 您的应用程序不能支持多种协议(由 Google Protocol Buffers 提供)。

示例

/**
 * Our visitor definition.  Includes a visit method for each
 * object it is capable of encoding.
 */
public interface Encoder 
  void visitAnimal(Animal a);
  void visitVegetable(Vegetable v);
  void visitMineral(Mineral m);


/**
 * Interface to be implemented by each class that can be encoded.
 */
public interface Encodable 
  void applyEncoder(Encoder e);


public class Animal implements Encodable 
  public void applyEncoder(Encoder e) 
    // Make call back to encoder to encode this particular Animal.
    // Different encoder implementations can be passed to an Animal
    // *without* it caring.
    e.visitAnimal(this);
  

通常会定义一个有状态的Encoder 实现,当调用visitXXX 方法时,它会将每个对象“推送”到OutputStream;例如

public class EncoderImpl implements Encoder 
  private final DataOutputStream daos;

  public EncoderImpl(File file) throws IOException 
    this.daos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
  

  public void visitAnimal(Animal a)         
    daos.writeInt(a.getWeight());
    daos.writeUTF(a.getName());

    // Write the number of children followed by an encoding of each child animal.
    // This allows for easy decoding.
    daos.writeInt(a.getChildren().size());

    for (Animal child : a.getChildren()) 
      visitAnimal(child);
    
  

  // TODO: Implement other visitXXX methods.

  /**
   * Called after visiting each object that requires serializing.
   */
  public void done() 
    daos.close();
  

【讨论】:

以上是关于如何使用反射递归序列化对象?的主要内容,如果未能解决你的问题,请参考以下文章

单例模式--反射--防止序列化破坏单例模式

当对象只有最终字段时,如何编写递归方法来生成算术序列?

单例模式防反射及性能

如何递归调用 WriteJson?

Java inputstream对象如何转入service方法中怎么传入service方法中

使用反射递归获取私有字段值