JSON 的表示在資料傳送時很方便,不過在程式碼裡就不見得了,尤其像是 Java 這樣的強型別語言,要操作 JSON 常常得寫上一堆麻煩的程式碼。這也就是為什麼會有許多 JSON parser 的開源專案的原因~吧。以前我在做這些事情時都是用 GSON 來處理的,不過由於 GSON 到現在似乎還是不支援 setter、getter 形式的轉換,能夠操弄的手法比較少,因此就來做做 Jackson 的簡易實驗。
Jackson 的基本使用
1 2 3 4 |
public class Product { public String name; public String description; } |
首先先只看一個簡單的物件 Product,裡面有兩個字串 name 和 description:
1 2 3 4 5 6 7 8 9 10 11 |
ObjectMapper mapper = new ObjectMapper(); // Build a JSON object through GSON. JsonObject json = new JsonObject(); json.addProperty( "name" , "iPhone" ); json.addProperty( "description" , "expensive mobile phone" ); // Deserialize the JSON using ObjectMapper. Product product = mapper.readValue(json.toString(), Product. class ); LOGGER.trace( "product: {}, {}" , product.name, product.description); |
這邊先用 GSON 組成一個 JSON 物件,然後把組出來的 JSON 餵給 Jackson 去做反序列化。最後印出來的結果如下:
1 |
product: iPhone, expensive mobile phone |
透過 field、getter 或者 setter 進行 JSON 序列化或反序列化
在 Jackson 預設的狀況下,一個 field 會被序列化或反序列化有幾個條件,必須要滿足任一條件才行:
- field 是以 public 被宣告
- field 有對應的 getter
- field 有對應的 setter(只有在反序列化時會生效)
自定義序列化或反序列化的行為
接著要嘗試一點稍微不同的東西,如果我有一個 Instant 欄位想要讓 Jackson 幫我處理,但轉換的方式我希望是用 epoch time 來表示,那麼這時就會需要客製化一個 JsonSerializer 或者 JsonDeserializer 了。這裡實際的作法跟 GSON 非常近似(而且 JsonSerializer/JsonDeserializer 名字也都取一樣 XD,雖然一邊是介面一邊是類別)。略有不同的地方在於,Jackson 是把宣告放在 Bean 上,而不像 GSON 是註冊在 GsonBuilder 裡面,這樣實際上也能帶來比較大的靈活度。
1 2 3 4 5 6 7 8 9 10 |
public class Product { public String name; public String description; @JsonDeserialize (using = InstantDeserialiser. class ) public Instant lastModifiedTime; public Information info; } |
這邊可以看到,在 Product 上我增加了一個 lastModifiedTime 這個屬性,型別是 Instant。同時我在 lastModifiedTime 的宣告上加上了 annotation @JsonDeserialize,這裡是代表當 Jackson 要反序列化時,請它呼叫我告訴它的類別來進行。因此這裡我也需要實作一個這裡宣告的 JsonDeserializer。
1 2 3 4 5 6 7 |
public class InstantDeserialiser extends JsonDeserializer<Instant> { @Override public Instant deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { return Instant.ofEpochMilli(p.getLongValue()); } } |
當反序列化的事件發生時,Jackson 會呼叫我定義的這個 JsonDeserializer,而這裡會做的事情就是取出 JSON 的 long 值,然後轉換成 Instant 物件後回傳。實際測試在 JSON 裡面追加了 lastModifiedTime 這個屬性後:
1 2 3 4 5 6 7 8 |
JsonObject json = new JsonObject(); json.addProperty( "name" , "iPhone" ); json.addProperty( "description" , "expensive mobile phone" ); json.addProperty( "lastModifiedTime" , 123 ); Product product = mapper.readValue(json.toString(), Product. class ); LOGGER.trace( "product: {}, {}, {}" , product.name, product.description, product.lastModifiedTime); |
就會得到這樣的結果:
1 |
product: iPhone, expensive mobile phone, 1970-01-01T00:00:00.123Z |
特殊應用:透過 setter 調整 JSON 的結構
其實到這段這次才是真正想要做的實驗。因為資料從資料庫拿出來時,格式往往跟最後要輸出的結構不太一樣,因此想要有個簡易一點的方法處理這個問題。GSON 雖然有 @SerializedName 可以做一點小技巧,但好像還是很難在 GSON 做到這種行為,因此才會考慮改用 Jackson 來嘗試。
首先一樣先擴充一下上面的範例 Product,主要是追加了巢狀結構,也就是有 info 這個下一階層的屬性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
public class Product { public String name; public String description; public Instant lastModifiedTime; public Information info; public void setName(String name) { this .name = name; } public void setDescription(String description) { this .description = description; } @JsonDeserialize (using=InstantDeserialiser. class ) public void setLastModifiedTime(Instant lastModifiedTime) { this .lastModifiedTime = lastModifiedTime; } public void setPrice(BigDecimal price) { if (info == null ) { info = new Information(); } info.price = price; } public void setOwner(String owner) { if (info == null ) { info = new Information(); } info.owner = owner; } } public class Information { public BigDecimal price; public String owner; } |
接著就是測試的主程式:
1 2 3 4 5 6 7 8 9 10 11 12 |
ObjectMapper mapper = new ObjectMapper() .configure(SerializationFeature.INDENT_OUTPUT, true ); JsonObject json = new JsonObject(); json.addProperty( "name" , "iPhone" ); json.addProperty( "description" , "expensive mobile phone" ); json.addProperty( "lastModifiedTime" , 123 ); json.addProperty( "owner" , "User1" ); json.addProperty( "price" , 312.57 ); Product product = mapper.readValue(json.toString(), Product. class ); LOGGER.trace( "product: {}" , mapper.writeValueAsString(product)); |
可以從組成的 JsonObject 那邊看出來,JSON 的格式是所有欄位都在同一層,但是 Product 物件的宣告卻是要把 owner 和 price 放在下一階層。而這樣的執行結果如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
product: { "name" : "iPhone" , "description" : "expensive mobile phone" , "lastModifiedTime" : { "nano" : 123000000, "epochSecond" : 0 }, "info" : { "price" : 312.57, "owner" : "User1" } } |
從結果來說確實達到了目標,讓 price 和 owner 兩個欄位都輸出在 info 底下變成 inner JSON。此外這裡其實還可以注意到 Instant 的輸出變回 Jackson 的預設方法了,不過這其實只是因為我沒有為 Instant 定義客製化的 Serilizer 才產生的結果。
沒有留言:
張貼留言