0x00 定義以及相關概念
POJO,Plain Old Java Object,是一種簡單的Java對象,一般就是有一些private屬性及其屬性getter、setter方法的類。這種對象只能用來裝載數據,作爲數據存儲的載體,而不具有業務邏輯處理的能力。
JSON,Javascript Object Notation,是一種輕量級的數據交換格式,易於人閱讀和編寫,同時也易於機器解析和生成。其主要構建於兩種結構:
- 鍵值對的集合:不同的語言中,它被理解爲對象(object),紀錄(record),結構(struct),字典(dictionary),哈希表(hash table),有鍵列表(keyed list),或者關聯數組 (associative array)
- 值的有序列表:在大部分語言中,它被理解爲數組(array)。
之所以會把它們兩者牽扯到一起,是因爲互聯網的產生帶來了機器間通訊的需求,通常通過WebService提交和返回的結果都是JSON格式字符,但是在端(本文爲Android端)中,則都需要以POJO爲基礎單位進行數據的裝載。因此雙方需要採用約定的協議,序列化和反序列化屬於通訊協議的一部分。
- 序列化:數據結構或對象轉爲可存儲或傳輸的形式
- 反序列化:從可存儲或傳輸的形式中解出對應數據結構或對象
0x01 第一次嘗試
假設Webservice返回了一個如下的Json序列化結果,代表用戶排名信息:
{
'name': 'HuldaYu',
'count': 11,
'rank': 1
}
而其對應的POJO如下所示定義:
public class UserRank {
private int rank;
private String name;
private int count;
///setters & getters
}
那麼我們可以通過將JSON序列化結果轉成JsonObject,再寫相應的Parser解析成我們需要的userRank
對象。
public class UserRankJSONParser {
public UserRank parse(JSONObject obj) {
UserRank rtn = new UserRank();
rtn.setRank(obj.optInt("rank", -1);
rtn.setName(obj.optString("name", "");
rtn.setCount(obj,optInt("count", -1));
return rtn;
}
}
同樣的,若我們需要將一個userRank
對象序列化,則我們也需要再寫一個對應的Composer組裝成一個JsonObject,再轉爲Json串。
public class UserRankJSONComposer {
public JSONObject compose(UserRank model) {
if (model == null) return null;
JSONObject obj = new JSONObject();
obj.put("rank", model.getRank());
obj.put("name", model.getName());
obj.put("count", model.getCount());
return obj;
}
}
用這種方式,我們發現,我們需要爲每個POJO實現parse和compose的方法,手工進行POJO與JSON之間的對應關係映射。
0x02 似乎有問題
但是之前的這種手工的方式是否就可以滿足我們了呢?
讓我們這樣來看之前的這種實現:如果需求變更,我們返回的json串中多了一個key需要解析呢?比如多了一個age
:
{
'name': 'HuldaYu',
'count': 11,
'rank': 1,
'age': 17
}
那麼對應的,我們的POJO中也需要增加age
屬性:
public class UserRank {
// other properties
private int age;
}
Parser中也是:
public UserRank parse(JSONObject obj) {
UserRank rtn = new UserRank();
// other property parse
rtn.setAge(obj.optInt("age", -1));
return rtn;
}
同樣的,POJO轉成JSON的composer中也需要相應修改:
public JSONObject compose(UserRank model) {
// other property compose
obj.put("age", model.getAge());
return obj;
}
也就是說,如果有大量POJO改寫變化,那麼POJO本身的property要改,parser要改,composer也要改。只要有一處沒有修改,就可能出現不一致性錯誤。同樣,如果一個POJO有20個屬性,則對應的parser和composer中也需要手工將這麼多的屬性進行手工映射處理。
那麼這個過程是否可以自動化掉,從而減少代碼量,減少出錯可能?
0x03 探索自動化可能性
仔細回想一開始想到的方法中,我們所做的工作,可以發現POJO中property的name就等於json中的key,property的value等於json的value;而json類型也與java類型有着映射關係:
No. | JSON類型 | Java類型 |
---|---|---|
1 | Object | LinkedHashMap<String, Object> |
2 | Array | ArrayList<Object> |
3 | String | String |
4 | Complete number | Integer, Long or BigInteger |
5 | Fractional number | Double/BigDecimal |
6 | True / False | Boolean |
7 | Null | Null |
而我們之前的parser和composer所做的只是純手工進行對象映射。
但其實在Java這種動態語言中,我們是可以在運行時獲取到類的property信息的,這就是Java反射機制。
Java反射機制
Java反射是Java語言的一個很重要特性,它使得Java具體了“動態性”。這個機制使得程序在運行的時候可以獲取任何一個已知名稱的class的內部信息,包括其中的構造方法、聲明的域和定義的方法等。只要有了java.lang.Class
類的對象,就可以通過getConstructor
、getField
和getMethod
方法來獲取該類中的構造方法、域和方法。這三個方法還有相應的getDeclaredXXX
,其只會獲取類本身所聲明的元素,而不考慮繼承下來的。
以前面的UserRank
爲例:UserRank userRank = new UserRank();
- 獲取實例的對象: Class
運用反射機制對POJO進行序列化的實現流程
有了Java反射機制和POJO與JSON中的自動映射關係,我們就可以用以下流程實現POJO的自動序列化過程:
根據POJO實例,遍歷出POJO類的域,並自動轉爲JSON Object中的鍵值對。
因此,我們可以實現一個通用的第三方工具,在運行時環境下對標準POJO實現序列化與反序列化,而不需要在編碼階段確定POJO的對象類。同樣也不需要像之前方案中的做法,在每個POJO中實現parse和compose方法,從而能有效提高開發效率,減少錯誤。此外,這樣可以使得代碼結構能加清晰簡潔,方便日後擴展,也方便對更多的數據格式提供支持(如XML),只需要在工具中進行配置,不需要爲每個POJO增加parseXML和composeXML等方法。
0x04 第三方庫
前面我們已經介紹了POJO序列化與反序列化的優化自動化方案,爲了防止重複造輪子,我們先去看看是否已經有了第三方庫支持。而實際上的確已經有了一些優秀的第三方庫,例如Jackson、GSON、FastJson和Json-lib。
- Gson(項目地址:https://github.com/google/gson)
- Gson當初是爲因應Google公司內部需求而由Google自行研發而來,但自從在2008年五月公開發布第一版後已被許多公司或用戶應用。Gson的應用主要爲toJson與fromJson兩個轉換函數,無依賴,不需要例外額外的jar,能夠直接跑在JDK上。而在使用這種對象轉換之前需先創建好對象的類型以及其成員才能成功的將JSON字符串成功轉換成相對應的對象。類裏面只要有get和set方法,Gson完全可以將複雜類型的json到bean或bean到json的轉換,是JSON解析的神器。
- FastJson(項目地址:https://github.com/alibaba/fastjson)
- Fastjson是一個Java語言編寫的高性能的JSON處理器,由阿里巴巴公司開發。無依賴,不需要例外額外的jar,能夠直接跑在JDK上。FastJson在複雜類型的Bean轉換Json上會出現一些問題,可能會出現引用的類型,導致Json轉換出錯,需要制定引用。FastJson採用獨創的算法,將parse的速度提升到極致,超過所有json庫。
- Json-lib(項目地址:http://json-lib.sourceforge.net/index.html)
- json-lib最開始的也是應用最廣泛的json解析工具,json-lib 不好的地方確實是依賴於很多第三方包,包括commons-beanutils.jar,commons-collections-3.2.jar,commons-lang-2.6.jar,commons-logging-1.1.1.jar,ezmorph-1.0.6.jar,對於複雜類型的轉換,json-lib對於json轉換成bean還有缺陷,比如一個類裏面會出現另一個類的list或者map集合,json-lib從json到bean的轉換就會出現問題。json-lib在功能和性能上面都不能滿足現在互聯網化的需求。
- Jackson(項目地址:https://github.com/FasterXML/jackson)
- 相比json-lib框架,Jackson所依賴的jar包較少,簡單易用並且性能也要相對高些。而且Jackson社區相對比較活躍,更新速度也比較快。支持流式API、樹模型和數據綁定多種模式的JSON解析方式,數據綁定方式使用方便,利用註解包可以爲開發提供很多便利。
關於這些第三方庫的性能對比測試等在網上有很多,大家都可以根據自己的需求去使用最合適的類庫。而這裏,我想簡單的介紹下其中一款類庫:Jackson。
Jackson簡單使用
Jackson主要提供了3個jar包:
- Jackson-core.jar —— 核心包(必須) 提供基於“流數據”解析的API,JsonParser(Json流讀取)、JsonGenerator(json流輸出)
- jackson-databind —— 數據綁定包(可選) 提供基於“數據綁定”和“樹模型”相關API,把JSON和基於屬性訪問器或註釋的POJO之間進行轉換。
- Jackson-annotations —— 註解包(可選) 提供註解功能
還是以我們之前的UserRank
爲例,利用Jackson的數據綁定,現在我們不需要額外寫parse或compose方法,就可以簡單的進行序列化和反序列化:
// 反序列化
ObjectMapper objectMapper = new ObjectMapper();
UserRank userRank = objectMapper.readValue(jsonStr, UserRank.class);
// 序列化
ObjectMapper objectMapper = new ObjectMapper();
String strObj = objectMapper.writeValueAsString(userRank);
使用註解統一不規範性
此外,如果我們的POJO或者JSON並不規範,或者JSON中的key都是下劃線命名,而POJO的屬性都是駝峯命名,那麼Jackson將不能直接解析出兩者對應關係。
這時就可以用Jackson提供的註解功能了。常用的如下:
- 排除屬性
- @JsonIgnore,標記在屬性或方法上
- @JsonIgnoreProperties,可以標記在類聲明上,例如
@JsonIgnoreProperties({"uselessValue"})
,或者直接忽略掉從JSON中獲得的所有“多餘”屬性@JsonIgnoreProperties(ignoreUnknown=true)
屬性別名
- @JsonProperty,可以改變某個成員屬性所使用的json名稱
@JsonProperty("done_page_count") private int donePageCount;
- 屬性格式轉化
- @JsonSerialize,使用自定義序列化來處理
- @JsonDeserialize,使用自定義反序列化來處理
處理多態類型
- @JsonTypeInfo,如果讀取或輸出的對象擁有很多可能的子類型,則需要添加一些類型信息,以方便Jackson在反序列化時正確讀取對象類型。
@JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=As.WRAPPER_OBJECT, property="type") @JsonSubTypes({ @JsonSubTypes.Type(value=Dog.class, name="dog"), @JsonSubTypes.Type(value=Cat.class, name="cat") })
0x05 總結
本文從一開始的POJO和JSON的手工序列化和反序列化的方案中,分析其自動化可行性,給出其可信性方案,並列出了已有的第三方類庫,簡單介紹了其中的Jackson。如此可以大大減少重複代碼量和出錯可能性。
MAY 少寫代碼的信念 BE WITH YOU~