Android Jackson 概述

weixin_34120274發表於2014-08-07

原文地址

本文內容

  • JSON 的三種方式
  • 示例
    • 完全資料繫結(POJO)示例
    • “Raw”資料繫結示例
    • 用泛型資料繫結
    • 樹模型(Tree Model)示例
    • 流(Streaming)API 示例
    • 流(Streaming)API 示例 2:陣列
  • 演示程式碼
  • 參考資料
  • 術語

最近寫 Android APP,需要序列化和反序列化。但是遇到一些問題,於是就順便研究了一下 Jackson。因為,我是搞 .NET 的,怎麼感覺比 Newtonsoft 要麻煩呢。如果不是因為麻煩,那就是因為 Jackson 實在太靈活了。

Jackson 是基於各種各樣的 Java 平臺(StAX,JAXB,  等等)上 XML 工具,一個用於處理 JSON 多用途的 Java 庫。Jackson 的目標是對 Java 開發者來說最大可能的結合快速的、準確的、輕量級的和符合人體工程學。

本文使用 jackson-all-1.9.11 簡單描述 Jackson 的功能。

下載 Demo

下載 jackson-all-1.9.11

JSON 的三種方式


Jackson 為處理 JSON 提供三種可選的方法(其中的一個有兩個變體):

  • Streaming API(又稱“增量解析/生成”)把讀寫 JSON 內容作為離散事件。
    • org.codehaus.jackson.JsonParser 讀,用 org.codehaus.jackson.JsonGenerator 寫。
    • 基於 StAX API。
  • 樹模型(Tree Model)提供了一個表示 JSON 文件的可變記憶體樹。
    • org.codehaus.jackson.map.ObjectMapper 可以生成樹;樹由 JsonNode 節點組成。
    • 樹模型類似 XML DOM。
  • 資料繫結(Data Binding)把 JSON 和 基於屬性訪問器(property accessor)或註釋的 POJO 之間進行轉換。
    • 有兩個變體:簡單資料繫結和完全資料繫結
      • 簡單資料繫結,意思是從或到 Java Map、List、String、Number、Boolean 和 null 的轉換
      • 完整資料繫結,意思是從或到任何 Java bean 型別(以及上面提到的“簡單”型別)的轉換
    • org.codehaus.jackson.map.ObjectMapper 完成重排(marshalling/unmarshalling,對資料儲存結構的重新編排轉換,而不是資料結構),包括把物件寫成 JSON,或讀取 JSON 轉換成物件
    • 基於基於註解(程式碼優先)變種的 JAXB。

從使用的角度,這三種方式:

  • 流 API,具有最好的效能(最低的開銷,最快的讀寫;其他兩個都是基於它的)
  • 資料繫結,通常最方便
  • 樹模型,最靈活

對於這些特性,考慮倒著介紹,先從平時最自然、最方便的方法開始:Jackson 資料繫結 API。

 

示例


完全資料繫結(POJO)示例

org.codehaus.jackson.map.ObjectMapper 用於將 JSON 資料對映成普通的 Java 物件(plain old Java objects,POJOs)。因此,“完全資料繫結(POJO)”就是將 JSON 內容完整地對映成 Java 物件,它們一一對應。這是最簡單的情況。例如,對於給定的 JSON 資料:

{
    "name": {
        "first": "Joe",
        "last": "Sixpack"
    },
    "gender": "MALE",
    "verified": false,
    "userImage": "Rm9vYmFyIQ=="
}

用兩行 Java 程式碼就可以把它轉換成一個 User 例項:

        ObjectMapper mapper = new ObjectMapper();  // can reuse, share globally
        User user = mapper.readValue(new File("user.json"), User.class);

User 類的定義如下所示:

public class User {
    public enum Gender {
        MALE, FEMALE
    };
 
    public static class Name {
        private String _first, _last;
 
        public String getFirst() {
            return _first;
        }
 
        public String getLast() {
            return _last;
        }
 
        public void setFirst(String s) {
            _first = s;
        }
 
        public void setLast(String s) {
            _last = s;
        }
    }
 
    private Gender _gender;
    private Name _name;
    private boolean _isVerified;
    private byte[] _userImage;
 
    public Name getName() {
        return _name;
    }
 
    public boolean isVerified() {
        return _isVerified;
    }
 
    public Gender getGender() {
        return _gender;
    }
 
    public byte[] getUserImage() {
        return _userImage;
    }
 
    public void setName(Name n) {
        _name = n;
    }
 
    public void setVerified(boolean b) {
        _isVerified = b;
    }
 
    public void setGender(Gender g) {
        _gender = g;
    }
 
    public void setUserImage(byte[] b) {
        _userImage = b;
    }
 
    public String toString() {
        return "name=" + this._name._last + " " + this._name._first
                + ",gender=" + this._gender;
    }
}

User 物件再轉換成 JSON,並儲存名為 user-modified.json 檔案,如下所示:

        mapper.writeValue(new File("user-modified.json"), user);

對於某些資料繫結(例如,把格式化的日期編排成 java.util.Date),Jackson 提供註解自定義重排的處理。

完全資料繫結是最簡單的情況。我們很容易想到,一個是 Java 系統,另一個是 .net 系統,顯然它們的類結構會不同(除非當初設計好了),即便不是兩個不同框架,就算是一個框架下的不同子系統,要讓這它們通訊,一個系統序列化的 JSON,另一個系統反序列化 JSON 時完全不知道是什麼。最好的辦法當然是,在序列化時做點手腳,直接序列化成另一個系統能識別的 JSON。千萬不能將序列化/反序列化理解得太狹隘,不是會呼叫序列化/反序列化兩個函式就完事啦~

“Raw”資料繫結示例

(也稱為“非型別”,或有時稱為“簡單”資料繫結)

在一些情況,我們沒有明確的 Java 類(也不想這麼做)去繫結 JSON,那麼“非型別的資料繫結”是最好的方法。它的使用與完全資料繫結一樣,只是簡單地規定把 Object.class(或是 Map.classList.classString[].class 等)作為繫結型別。因此,User 的 JSON 繫結如下所示:

        Map<String, Object> userData = mapper.readValue(new File("user.json"),
                Map.class);

userData 可以如下面程式碼顯示構造:

        Map<String, Object> userData = new HashMap<String, Object>();
        Map<String, String> nameStruct = new HashMap<String, String>();
        nameStruct.put("first", "Joe");
        nameStruct.put("last", "Sixpack");
        userData.put("name", nameStruct);
        userData.put("gender", "MALE");
        userData.put("verified", Boolean.FALSE);
        userData.put("userImage", "Rm9vYmFyIQ==");

如果你構造一個 Map,如上所示,或從 JSON 構造,並可能進行修改,那麼你可以跟之前一樣寫成 JSON 檔案:

        mapper.writeValue(new File("user-modified-map.json"), userData);

Jackson 用於簡單資料繫結的具體 Java 型別:

JSON Type Java Type
object LinkedHashMap<String,Object>
array ArrayList
string String
number(非小數) Integer, Long 或 BigInteger (smallest applicable)
number(小數) Double (configurable to use BigDecimal)
true|false Boolean
null null

用泛型資料繫結

除了繫結 POJOs 和“簡單”型別外,還可以繫結泛型。

這種情況需要特殊處理,這是由於所謂的型別擦除(Type Erasure)(Java 以向後相容的方式實現泛型),這會阻止你使用類似 Collection<String>.class (它不會被編譯)。

因此,如果你想繫結資料到 Map<String,User>,你需要使用:

        Map<String, User> userData = mapper.readValue(new File(
                "user-modified-generic.json"),
                new TypeReference<Map<String, User>>() {
                });

其中,只能通過 TypeReference 傳遞泛型定義(在這種情況下,是通過 anynomous 內部類):最重要的部分是 <Map<String,User>>,它定義繫結到的型別。

如果你不這樣做,而只是使用 Map.class,那麼呼叫等價於繫結到 Map<?,?>(例如,"untyped" Map),正如前面說明的。

注意:作為替代方案,1.3 版本還允許通過 TypeFactory 構造。 

樹模型(Tree Model)示例

另一種從 JSON 獲得物件的方式是建立一棵樹。這類似 XML 的 DOM 樹。Jackson 生成樹的方式是使用基本的 JsonNode 基類,它公開了通常需要的讀取訪問。實際使用的節點型別是其子類;但子類只是在需要修改樹時使用。

可以用 Streaming API (如下面小節)或是 ObjectMapper 來讀/寫樹。

若用 ObjectMapper,則可以像下面的程式碼那樣:

        ObjectMapper mapper = new ObjectMapper();
        // can either use mapper.readTree(source), or mapper.readValue(source,
        // JsonNode.class);
        JsonNode rootNode = mapper.readTree(new File("user.json"));
        // ensure that "last name" isn't "Xmler"; if is, change to "Jsoner"
        JsonNode nameNode = rootNode.path("name");
        String lastName = nameNode.path("last").getTextValue();
        if ("xmler".equalsIgnoreCase(lastName)) {
            ((ObjectNode) nameNode).put("last", "Jsoner");
        }
        // and write it out:
        mapper.writeValue(new File("user-modified-tree.json"), rootNode);

或者,你想用 User 示例從頭構造一個樹,你可以這樣:

        TreeMapper treeMapper = new TreeMapper();
        ObjectNode userOb = treeMapper.objectNode();
        Object nameOb = userRoot.putObject("name");
        nameOb.put("first", "Joe");
        nameOb.put("last", "Sixpack");
        userOb.put("gender", User.Gender.MALE.toString());
        userOb.put("verified", false);
        byte[] imageData = getImageData(); // or wherever it comes from
        userOb.put("userImage", imageData);

注意:對於 Jackson 1.2,你可以直接使用 ObjectMapper,通過使用 ObjectMapper.createObjectNode() 來建立 userOb——以上程式碼是針對 Jackson 1.0 和 1.1)。

Streaming API 示例

最後,第三種方式:高效能的流 API(又稱“增量模式”,因為可以增量地讀寫內容)。

        JsonFactory f = new JsonFactory();
        JsonGenerator g = f.createJsonGenerator(new File("user-stream.json"),
                JsonEncoding.UTF8);
        g.writeStartObject();
        g.writeObjectFieldStart("name");
        g.writeStringField("first", "Joe");
        g.writeStringField("last", "Sixpack");
        g.writeEndObject(); // for field 'name'
        g.writeStringField("gender", Gender.MALE.toString());
        g.writeBooleanField("verified", false);
        g.writeFieldName("userImage"); // no 'writeBinaryField' (yet?)
        byte[] binaryData = "Rm9vYmFyIQ==".getBytes();
        g.writeBinary(binaryData);
        g.writeEndObject();
        // important: will force flushing of output, close underlying output
        // stream
        g.close();

看上去也不是特別糟糕(特別是,與需要寫的工作量相比,可以說,相當於 XML 內容),但肯定比基本物件對映費勁點。

在另一方面,你也可以完全控制每個細節。開銷最小:這比使用 ObjectMapper 要快一點;通常情況快 20-30%。但或許最重要的是,已流的方式輸出:除了一些緩衝,所有內容會將立刻寫出來。這意味著,記憶體的使用也是最小的。

那麼如何解析?如下程式碼所示:

        JsonFactory f = new JsonFactory();
        JsonParser jp = f.createJsonParser(new File("user.json"));
        User user = new User();
        jp.nextToken(); // will return JsonToken.START_OBJECT (verify?)
        while (jp.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jp.getCurrentName();
            jp.nextToken(); // move to value, or START_OBJECT/START_ARRAY
            if ("name".equals(fieldname)) { // contains an object
                Name name = new Name();
                while (jp.nextToken() != JsonToken.END_OBJECT) {
                    String namefield = jp.getCurrentName();
                    jp.nextToken(); // move to value
                    if ("first".equals(namefield)) {
                        name.setFirst(jp.getText());
                    } else if ("last".equals(namefield)) {
                        name.setLast(jp.getText());
                    } else {
                        throw new IllegalStateException("Unrecognized field '"
                                + fieldname + "'!");
                    }
                }
                user.setName(name);
            } else if ("gender".equals(fieldname)) {
                user.setGender(User.Gender.valueOf(jp.getText()));
            } else if ("verified".equals(fieldname)) {
                user.setVerified(jp.getCurrentToken() == JsonToken.VALUE_TRUE);
            } else if ("userImage".equals(fieldname)) {
                user.setUserImage(jp.getBinaryValue());
            } else {
                throw new IllegalStateException("Unrecognized field '"
                        + fieldname + "'!");
            }
        }
        // ensure resources get cleaned up timely and properly
        jp.close();

最後,可能直接利用 JsonParser 和 JsonGenerator 進行資料繫結和樹模型。你可以看下下面方法:

  • JsonParser.readValueAs()
  • JsonParser.readValueAsTree()
  • JsonGenerator.writeObject()
  • JsonGenerator.writeTree()

你必須使用 org.codehaus.jackson.map.MappingJsonFactory 來構造解析器和生成器,而不是 org.codehaus.jackson.JsonFactory

Streaming API 示例 2:陣列

考慮下面的 POJO:

    public class Foo {
        public String foo;
    }

以及 JSON 流:

        String json = [{\"foo\": \"bar\"},{\"foo\": \"biz\"}]";

有一個便利的方式進行這種資料繫結(參看 ObjectReader.readValues()),你可以很容易地使用流來迭代,以及繫結單個元素:

        JsonFactory f = new JsonFactory();
          JsonParser jp = f.createJsonParser(json);
          // advance stream to START_ARRAY first:
          jp.nextToken();
          // and then each time, advance to opening START_OBJECT
          while (jp.nextToken() == JsonToken.START_OBJECT)) {
            Foo foobar = mapper.readValue(jp, Foo.class);
            // process
            // after binding, stream points to closing END_OBJECT
          }

 

演示程式碼


  • Windows 7 64 位
  • Eclipse ADT V22.6.2

2014-08-10_113607_副本

圖 1 專案結構

  • com.example.jacksondemo 包,是主程式;
  • com.example.jacksondemo.studemo 包,是針對 Student 實體的定義及其測試類;
  • com.example.jacksondemo.userdemo 包,是針對 User 實體的定義及其測試類,也是本文的演示程式碼;
  • com.example.jacksondemo.catedemo 包,是針對網上一個大的 JSON 檔案的定義及其測試類;
  • com.example.jacksondemo.advanced 和 com.example.jacksondemo.junit 包,提了一下,Jackson 的高階用法及其單元測試類。具體請看“參考資料”小節;
  • *.json 檔案是本演示所生成,用於序列化和反序列化。

 

參考資料


 

術語


JAXB

JAXB(Java Architecture for XML Binding)是一個業界的標準,是一項可以根據 XML Schema 產生 Java 類的技術。JAXB 2.0 是 JDK 1.6 的組成部分。JAXB 2.2.3 是JDK 1.7 的組成部分。JAXB 能夠使用 Jackson 對 JAXB 註解的支援實現(jackson-module-jaxb-annotations),既方便生成 XML,也方便生成 JSON。常用的註解包括:@XmlRootElement、@XmlElement 等等。

JAXB 提供了將XML 例項文件反向生成 Java 物件樹的方法,並能將 Java 物件樹的內容重新寫到 XML 例項文件。另一方面,JAXB 提供了快速而簡便的方法將 XML 模式繫結到 Java 表示,從而使 Java 開發者在 Java 應用程式中能方便地結合 XML 資料和處理函式。

StAX

StAX 是 Streaming API for XML 的縮寫,是一種針對 XML 的流式拉分析 API。StAX 是一種面向流的新方法,最終版本於 2004 年 3 月釋出,併成為 JAXP 1.4(Java 6.0 中)的一部分。StAX 的實現使用了 JWSDP(Java Web Services Development Pack)1.6,並結合了 SJSXP(Sun Java System XML Streaming Parser)。

XML 進行解析技術,在 Java 6.0 之前,就已經有四種:

  • DOM - Document Object Model
  • SAX - Simple API for XML
  • JDOM - Java-based Document Object Model
  • DOM4J - Document Object Model for Java

在程式中訪問和操作 XML 檔案一般有兩種模型:DOM 和流模型。

  • DOM 的優點是,允許編輯和更新 XML 文件,隨機訪問文件中的資料,使用 XPath(XML Path Language)查詢;而缺點是,需要一次性把整個文件載入到記憶體中,對於大型文件,會造成效能問題。
  • 流模型的優點是,對訪問 XML 檔案採用流的概念,在任何時候記憶體中只有當前節點,解決了 DOM 效能問題;但缺點是,只讀且只能向前,不能在文件中執行向後操作。

流模型每次迭代 XML 文件中的一個節點,適合於處理較大的文件,記憶體消耗小。它有兩種變體:“推”模型和“拉”模型。

  • 推模型,就是我們常說的 SAX,它是一種事件驅動模型。每發現一個節點就觸發一個事件,而我們需要編寫這些事件的處理程式。這樣的做法很麻煩,且不靈活。
  • 拉模型,在遍歷文件時,會把感興趣的部分從讀取器中拉出,不需要引發事件,允許我們選擇性地處理節點。這大大提高了靈活性,以及整體效率。

 

下載 Demo

下載 jackson-all-1.9.11

相關文章