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~