TypeAdapter
现在轮到TypeAdapter类上场,但考虑到gson默认行为已足够强大,加上项目实践中应用json时场景不会太复杂,所以一般不需要自定义TypeAdapter。TypeAdapter优点是集成了JsonWriter和JsonReader两个类,定义了一套与gson框架交互的良好接口,同时便于管理编码和解码的实现代码,不至于太零碎。因而在了解JsonReader和JsonWriter的使用方法之后,自定义TypeAdapter类来完成特定类的编码和解码也就不困难了。
TypeToken
Type listType = new TypeToken<List<String>>() {}.getType();//!!!
List<String> target = new LinkedList<String>();
target.add("blah");
Gson gson = new Gson();
String json = gson.toJson(target, listType);
List<String> target2 = gson.fromJson(json, listType);// !!!
上述代码在之前的样例里看到过,这里单独拿出来研究。行尾有!!!的两行是这里要特别关注的,原因是看起来很怪异。初次看到第一行代码的时候感觉非常怪异,很奇怪为什么需要设计这样的API。仔细思索之后,发现设计者真实的目的其实非常简单,只是为了提取泛型的类型,原因是Java的泛型对象的类型不能直接通过类型本身获取到,比如类似List<String>.class的代码是无法通过编译,原因和Java泛型的实现机制有关系。
分析TypeToken类的实现代码,发现GSON的开发者想出了一个比较有意思的实现方法,既然不能直接通过类型本身得到真实的类型对象,那么就从对象本身得到。而TypeToken就是这一想法的通用实现。TypeToken类本身不能直接实例化,使用时需要定义其子类对象,这时即通过子类对象来获取其模板类型参数,也就得到了泛型类型的真实类型。
控制缩进
比如面对类似 [{"Qualified Name":"Jackie","age":30,"contact":{"email":"email","phoneno":"phoneno"}},{"Qualified Name":"Jackie Boy","age":1,"contact":{"email":"email","phoneno":"phoneno"}}] 的json格式字符串,对于程序来说理解这样的字符串毫无压力,但对于人来说困难就比较大了。那怎么办呢?gson有办法,通过修改gson的默认行为,可以在输出成json字符串时,提供缩进使用字符串表现出良好的格式。样例代码和缩进后的输出如下。
final Gson gson = new GsonBuilder().setVersion(1.1).setPrettyPrinting().create();// 调用setPrettyPrinting方法,改变gson对象的默认行为
如下即是输出
[
{
"Qualified Name": "Jackie",
"age": 30,
"contact": {
"email": "email",
"phoneno": "phoneno"
}
},
{
"Qualified Name": "Jackie Boy",
"age": 1,
"contact": {
"email": "email",
"phoneno": "phoneno"
}
}
]
这个特性很有意思,但项目实际开发的时候可能意义不大,毕竟换行、缩进都是占用空间的。日志里输出json格式的字符串时,倒是可以考虑写入调整过格式后的json字符串,在事后分析日志时方便阅读,给运维同事减轻负担。
输出空引用
基于已有的使用经验,如果不做对字段进行特别的处理,对于引用类型的成员,如果取值为null时,编码后将不会出现在json字符串中,这是gson的默认行为。但通过修改gson的默认行为,可以将取值为null的字段也输出到json字符串中。样例代码如下,为了方便查看,还设置了缩进。
final Gson gson = new GsonBuilder().setVersion(1.1).serializeNulls().setPrettyPrinting().create();// 调用serializeNulls方法,改变gson对象的默认行为
如下是输出
[
{
"Qualified Name": "Jackie",
"age": 30,
"contact": {
"email": "email",
"phoneno": "phoneno"
},
"address": null,// address和location被原样输出
"location": null
},
{
"Qualified Name": "Jackie Boy",
"age": 1,
"contact": {
"email": "email",
"phoneno": "phoneno"
},
"address": null,
"location": null
}
]
// 如下是类定义
@Data
class Person {
@Since(1.0)
@SerializedName("Qualified Name")
private String name;
@Since(1.1)
private int age;
@Since(1.0)
private Map<String, Object> contact;
private String address;
private String location;
public Person() {
contact = new HashMap<String, Object>();
}
public void putValue(final String key, final Object value) {
contact.put(key, value);
}
}
Gson判断Java Bean的字段是否需要格式化为json的方法
这是一个好问题。有这么几种可能:
- 从Java Bean类的元数据中提取成员列表,然后根据各个成员的类型来生成对象的json表示;
- 可以提取符合getter/settter方法命名规范的公有方法列表,然后依据这个列表来生成对象的json表示;
设计如下的样例代码,可以检验一下刚才的猜测。
import java.util.HashMap;
import java.util.Map;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import com.google.gson.Gson;
public class JsonTest {
public static void main(final String[] args) {
final Gson gson = new Gson();
final Person jack1 = new Person();
jack1.setAge(30);
jack1.setName("Jackie");
jack1.putValue("email", "email");
jack1.putValue("phoneno", "phoneno");
final String json = gson.toJson(jack1);
System.out.println(json);
final Gson gson2 = new Gson();
final Person p = gson2.fromJson(json, Person.class);
System.out.println(p);
}
}
@Data
class Person {
private String name;
@Getter(AccessLevel.NONE)
// @Setter(AccessLevel.NONE)
private int age;
private Map<String, Object> contact;
private String address;
private String location;
public Person() {
age = 20;
contact = new HashMap<String, Object>();
}
public void putValue(final String key, final Object value) {
contact.put(key, value);
}
/*
public int getAGe() {
return age;
}
public int getAge() {
return 90;
}
*/
}
样例代码的输出如下。
{"name":"Jackie","age":30,"contact":{"email":"email","phoneno":"phoneno"}}
Person(name=Jackie, age=30, contact={email=email, phoneno=phoneno}, address=null, location=null)
前述代码主要是在验证成员的公有方法对gson为对象生成json字符串时的影响。比如通过使用注解,控制lombok不为Person类的成员age生成getter方法,或者写一个命名不合要求的访问方法,或者写一个命名符合要求,但是取值与成员age的值无关的公有getter方法。经验证,这几种修改方法对最终的结果没有任何影响,因而从上面的样例代码可以得出结论,gson在为Bean对象生成json时,并没有依赖类定义中的公有方法。假如gson直接使用Bean元数据中的成员信息来直接生成json的话,暂时没有想到怎样设计样例代码来检验,所以只好单步跟踪gson在生成json时的代码。最终跟踪到了如下的一段代码。
// 如下代码来自于gson 2.2.4,版权归作者所有
Field[] fields = raw.getDeclaredFields();
for (Field field : fields) {
boolean serialize = excludeField(field, true);
boolean deserialize = excludeField(field, false);
if (!serialize && !deserialize) {
continue;
}
field.setAccessible(true);
Type fieldType = $Gson$Types.resolve(type.getType(), raw, field.getGenericType());
BoundField boundField = createBoundField(context, field, getFieldName(field),
TypeToken.get(fieldType), serialize, deserialize);
BoundField previous = result.put(boundField.name, boundField);
if (previous != null) {
throw new IllegalArgumentException(declaredType
+ " declares multiple JSON fields named " + previous.name);
}
}
这段代码在类com.google.gson.internal.bind.ReflectiveTypeAdapterFactory的方法getBoundFields中,不相关的部分没有列出来。从这段代码可以看出gson直接读取JavaBean的成员信息来完成json的生成,并没有校验成员是否定义了get/set方法。