基于Event Stream操作JSON

文章目录

前言

一、Reading from Stream-of-Events

二、Writing to Stream-of-Events

三、结论


前言

前面聊到了处理JSON的三种方法,接下来聊聊第一种Event Stream,看看Jackson是如何做相关抽象的。内容大部分翻译自

Json processing with Jackson: Method #1/3: Reading and Writing Event Streams

一、Reading from Stream-of-Events

毕竟Stream-of-Events只是一个逻辑抽象,不是个具体的API,首先需要考虑的是怎么把它暴露出来。此处有3个常用选择:

1. Stax Event API

作为流迭代事件。这么做的好处有几个:简化访问方式,允许在处理过程中持有封装对象;

2. SAX  API

作为回调处理事件,将所有事件相关数据作为回调参数。这就是SAX  API使用的方式。性能非常好并且类型安全(每个回调方法,每个事件类型,都可以有独立的参数)。但站在应用角度,这种方式非常麻烦。

3. Stax Cursor API

作为逻辑游标,每次访问一个事件有关的具体数据:Stax Cursor API。相比事件对象方式,该方式性能比较好(和回调方式接近),毕竟框架没有额外创建对象。如果应用需要对象,需要应用自己创建。相比回调方式,这种方式使用起来比较简单,不需要注册回调的handler,不涉及"Hollywood principle"(Don't call us , we call you)仅仅通过游标遍历事件即可。

Jackson 使用第三种方式,通过"JsonParser"对象暴露逻辑游标。这种方式很好地兼顾便利性和效率(其他方式并未兼顾)。作为Cursor的实体被命名为Parser(替代了 Reader),紧扣Json规范。其他API也遵守类似的规范(结构化的KV集合被称为 Object,Array用来标识一个值序列-- 其他的名字可能也很易于理解,但首先和数据规范直接兼容是个更妙的做法)

为了遍历stream,应用需要调用"JsonParser.nevToken()"来推进游标(Jackson更倾向token而不是event)。如果需要访问当前游标指向token的数据和属性,通过accesor就可以完成。这个设计灵感来自Stax API,但是调整得更加符合Json数据特点。

所以,底层设计其实非常简单。不过,为了更好理解细节咱们搞个栗子看看。这个基于http://apiwiki.twitter.com/Search+API+Documentation 描述的Json数据格式。

{
  "id":1125687077,
  "text":"@stroughtonsmith You need to add a \"Favourites\" tab to TC/iPhone. Like what TwitterFon did. I can't WAIT for your Twitter App!! :) Any ETA?",
  "fromUserId":855523, 
  "toUserId":815309,
  "languageCode":"en"
}

然后我们使用这样一个Bean来承载数据。

public class TwitterEntry
{
  long _id;  
  String _text;
  int _fromUserId, _toUserId;
  String _languageCode;

  public TwitterEntry() { }

  public void setId(long id) { _id = id; }
  public void setText(String text) { _text = text; }
  public void setFromUserId(int id) { _fromUserId = id; }
  public void setToUserId(int id) { _toUserId = id; }
  public void setLanguageCode(String languageCode) { _languageCode = languageCode; }

  public int getId() { return _id; }
  public String getText() { return _text; }
  public int getFromUserId() { return _fromUserId; }
  public int getToUserId() { return _toUserId; }
  public String getLanguageCode() { return _languageCode; }

  public String toString() {
    return "[Tweet, id: "+_id+", text='";+_text+"', from: "+_fromUserId+", to: "+_toUserId+", lang: "+_languageCode+"]";
  }
}

接下来基于样例数据构造对象,首先,写一个方法通过EventStream读取Json内容并填充Bean。

TwitterEntry read(JsonParser jp) throws IOException
 {
  // Sanity check: verify that we got "Json Object":
  if (jp.nextToken() != JsonToken.START_OBJECT) {
    throw new IOException("Expected data to start with an Object");
  }
  TwitterEntry result = new TwitterEntry();
  // Iterate over object fields:
  while (jp.nextToken() != JsonToken.END_OBJECT) {
   String fieldName = jp.getCurrentName();
   // Let's move to value
   jp.nextToken();
   if (fieldName.equals("id")) {
    result.setId(jp.getLongValue());
   } else if (fieldName.equals("text")) {
    result.setText(jp.getText());
   } else if (fieldName.equals("fromUserId")) {
    result.setFromUserId(jp.getIntValue());
   } else if (fieldName.equals("toUserId")) {
    result.setToUserId(jp.getIntValue());
   } else if (fieldName.equals("languageCode")) {
    result.setLanguageCode(jp.getText());
   } else { // ignore, or signal error?
    throw new IOException("Unrecognized field '"+fieldName+"'");
   }
  }
  jp.close(); // important to close both parser and underlying File reader
  return result;
 }

调用过程长这个样子

JsonFactory jsonF = new JsonFactory();
  JsonParser jp = jsonF.createJsonParser(new File("input.json"));
  TwitterEntry entry = read(jp);

到这里,我们用很少的代码完成了相对简单的操作。另一方面,这也很容易理解: 即使你从来没用过Jackson或者Json格式(甚至是Java),你能很快明白其中的思路,并按需修改代码。基本上来说这是"mongkey code" -- 容易读、写、修改但是枯燥乏味易出错(因为本身就乏味)。

另一个可能的好处是非常快,这种方式开销很小并且如果你愿意做基准测试的话,它确实运行得非常快。最后,处理过程是完全流式的:parser(and generator)只跟踪了逻辑游标当前指向的数据(也就是上下文相关的信息,诸如嵌套,当前读取的行号等诸如此类的内容)。

这个栗子简单揭示了使用原始流访问Json的使用场景:性能优先的场景。另一种可能的场景是内容结构非常不整齐,更加自动化的方式不起作用(此处仅做声明,后续详细讨论),或者数据结构有很高的阻抗。

二、Writing to Stream-of-Events

使用Stream-of-Events读取内容是个简单重复的过程,所以没必要惊奇写入过程也是如此,只是少了些不必要的工作。假设我们现在有个基于Json content构建的Bean,不妨尝试将其写回。这里是将Bean转换为Json的方法:

private void write(JsonGenerator jg, TwitterEntry entry) throws IOException
  {
    jg.writeStartObject();
    // can either do "jg.writeFieldName(...) + jg.writeNumber()", or this:
   jg.writeNumberField("id", entry.getId());
   jg.writeStringField("text", entry.getText());
   jg.writeNumberField("fromUserId", entry.getFromUserId());
   jg.writeNumberField("toUserId", entry.getToUserId());
   jg.writeStringField("langugeCode", entry.getLanguageCode());
   jg.writeEndObject();
   jg.close();
  }

接下来调用该方法 

// let's write to a file, using UTF-8 encoding (only sensible one)
  JsonGenerator jg = jsonF.createJsonGenerator(new File("result.json"), JsonEncoding.UTF8);
  jg.useDefaultPrettyPrinter(); // enable indentation just to make debug/testing easier
  TwitterEntry entry = write(jg, entry);

是不是非常简单?写起来毫无挑战也没啥特别。

三、结论

从上述内容,我们可以看到使用Steam-of-Events是一个首先的处理Json的方式。结果包含两个方面收益(非常快,可以清楚地看到推进过程)和损失(细节代码,重复)。

但是无论你是否使用这个API,你至少得明白它是怎么工作的,因为其他的接口都是基于此构建起来的。Data Mapping和Tree Building内部都是基于原始的Stream API来读写Json内容的。下一次,我们一起看看处理Json更精炼的方式:Data Binding,敬请期待!

你可能感兴趣的:(Jackson,后端,json)