前言
一、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
毕竟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的使用场景:性能优先的场景。另一种可能的场景是内容结构非常不整齐,更加自动化的方式不起作用(此处仅做声明,后续详细讨论),或者数据结构有很高的阻抗。
使用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,敬请期待!