Gson TypeAdapter使用技巧几例:数据免判空、解析后校验、预处理

本文介绍几个Gson使用的技巧实例,可以解决一些实际开发容易遇到的、数据解析相关的问题。建议结合示例工程源码阅读。

本文完整示例工程
https://github.com/jzj1993/GsonStudy

有关Gson源码设计相关,可以阅读

Gson源码设计学习
http://www.paincker.com/gson-study

解析的数据免判空

解析完数据,使用时经常需要对数据判空,以免后台返回数据格式有误导致客户端Crash,非常繁琐,且容易遗漏。

从Gson解析的角度,可以考虑让解析出来的数据不为null,从而不需要做判空。具体实现方面,有几种思路。

  1. 修改数据实现类:在数据Model实现类中过滤掉null值。例如设计一个List,只有非null值才能被添加进去,取出Item时就不需要判空了。

  2. 全局替换解析过程:对于特定类型,全局替换Gson解析过程。例如String类型null解析为空串"";数组、List解析为空数组、空List而不是null等。

  3. 定制化解析:全局替换的方式比较粗暴,对于复杂工程可能会引起预料不到的问题,可以结合注解等方式,对指定的元素进行特殊处理。

ItemNonNullList:List中的Item免判空

List中的每个元素的判空,很容易漏掉。

可以封装一个ItemNonNullList,在ArrayList外面包一层,并特殊处理add/addAll相关方法,保证为null的Item不会被添加进去。不需要自定义TypeAdapter修改Gson解析过程。

具体代码详见示例工程。

全局替换TypeAdapter:特定类型免判空

以String为例,可以把TypeAdapters.STRING复制出来,并修改其中代码如下,将null解析为空字符串,然后注册到Gson中,覆盖String默认的TypeAdapter。

类似的,还可以覆盖Gson内置的TypeAdapters.INTEGER、CollectionTypeAdapterFactory、ArrayTypeAdapter等,实现Integer、Collection、数组等类型的免判空。

具体也可以参考这篇文章

Gson:自定义TypeAdapter
https://www.cnblogs.com/linjzong/p/5201565.html

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
/**
* 自定义TypeAdapter ,null对象将被解析成空字符串
*/
public static final TypeAdapter<String> STRING = new TypeAdapter<String>() {
public String read(JsonReader reader) {
try {
if (reader.peek() == JsonToken.NULL) {
reader.nextNull();
return ""; // 原先是返回null,这里改为返回空字符串
}
return reader.nextString();
} catch (Exception e) {
e.printStackTrace();
}
return "";
}

public void write(JsonWriter writer, String value) {
try {
if (value == null) {
writer.nullValue();
return;
}
writer.value(value);
} catch (Exception e) {
e.printStackTrace();
}
}
};
1
2
3
Gson gson = new GsonBuilder()
.registerTypeAdapter(String.class, STRING)
.create();

NonNullField注解:任意字段免判空

设计一个NonNullField字段,注解到自定义类的成员变量上,可以确保解析时该字段不会为null,会使用默认的实例来代替。

NonNullField注解定义如下。支持给字段配置自定义的InstanceCreator来创建实例。

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface NonNullField {
Class<? extends InstanceCreator> value() default NonNullFieldConstructor.class;
}

使用示例如下,NonNullField注解支持继承、泛型参数类型、嵌套class、自定义InstanceCreator。

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
public static class BaseData<T> {
// 泛型参数类型;成员变量继承
@NonNullField
T type;
}
public static class Data extends BaseData<String> {
String nullField;
@NonNullField
String string;
// 自定义InstanceCreator;
// 嵌套class(Extra内部又定义了NonNullField)
@NonNullField(ExtraCreator.class)
Extra extra;
}
public static class Extra {
final String name;
@NonNullField
Integer integer;

public Extra(String name) {
this.name = name;
}
}
public static class ExtraCreator implements InstanceCreator<Extra> {
@Override
public Extra createInstance(Type type) {
return new Extra("extra");
}
}

测试代码如下,正常情况下应该解析出Data实例且所有Field均为null。但使用了NonNullField后,相应字段都会赋值为默认实例而不是null。

1
2
3
4
5
6
7
8
9
10
Gson gson = new GsonBuilder().registerTypeAdapterFactory(new NonNullFieldFactory()).create();
Data data = gson.fromJson("{}", Data.class);

Assert.assertNull(data.nullField);
Assert.assertEquals("", data.type);

Assert.assertNotNull(data.extra);

Assert.assertEquals("extra", data.extra.name);
Assert.assertEquals((Integer) 0, data.extra.integer);

NonNullField注解通过NonNullFieldFactory实现如下。此Factory在创建TypeAdapter时,先搜索Class和父类中包含NonNullField注解的成员变量:

  • 如果没找到,则返回null,由其他Factory创建TypeAdapter;
  • 如果找到了,则在DelegateAdapter外包裹一层,DelegateAdapter解析完成后调用replaceNonNullFields方法,将NonNullField的null值替换为默认实例。
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
public class NonNullFieldFactory implements TypeAdapterFactory {

@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> typeToken) {

List<Field> fields = findMatchedFields(typeToken);
final Type type = typeToken.getType();

// 如果找到了,则包裹一层Adapter
if (fields != null && !fields.isEmpty()) {
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, typeToken);
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}

@Override
public T read(JsonReader in) throws IOException {
T t = delegate.read(in);
replaceNonNullFields(t, typeToken);
return t;
}
};
}
return null;
}
// ...
}

在replaceNonNullFields方法中,调用InstanceCreator创建实例,然后通过反射设置给Java对象。创建默认实例时,还会递归替换默认实例中嵌套的NonNullField,从而支持嵌套class。

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
private static void replaceNonNullFields(Object o, TypeToken typeToken) {
// ...
List<Field> fields = fieldMap.get(typeToken.getType());
// ...
for (Field field : fields) {
try {
Object fieldValue = field.get(o);
if (fieldValue == null) {
Object value = constructField(field, resolveFieldType(typeToken, field));
// ...
field.set(o, value);
}
} catch (IllegalArgumentException IllegalAccessException e) {
L.e(e);
}
}
}

private static Object constructField(Field field, Type type) {
NonNullField annotation = field.getAnnotation(NonNullField.class);
Class<? extends InstanceCreator> creatorClass = annotation.value();
InstanceCreator creator = getCreator(creatorClass);
Object instance = creator.createInstance(type);
replaceNonNullFields(instance, TypeToken.get(type));
return instance;
}

InstanceCreator可由NonNullField注解指定,默认值为NonNullFieldConstructor。NonNullFieldConstructor中先判断如果是基本类型或数组,则直接创建,否则调用Gson内部的ConstructorConstructor工具类创建实例。

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
35
36
37
38
39
public class NonNullFieldConstructor implements InstanceCreator<Object> {
/**
* 保存基本类型及其默认值。基本类型默认值的内容不能被修改,因此可以重复利用,赋值给多个Field。
*/
private static final Map<Class, Object> basicMap = new HashMap<>();
/**
* Gson的Constructor
*/
private static final ConstructorConstructor constructor = new ConstructorConstructor(new HashMap<>());

static {
basicMap.put(Boolean.class, false);
basicMap.put(Byte.class, (byte) 0);
basicMap.put(Character.class, (char) 0);
basicMap.put(Short.class, (short) 0);
basicMap.put(Integer.class, 0);
basicMap.put(Long.class, 0L);
basicMap.put(Float.class, 0F);
basicMap.put(Double.class, (double) 0);
basicMap.put(String.class, "");
}

@Override
public Object createInstance(Type type) {
if (type instanceof Class) {
Object o = basicMap.get(type);
if (o != null) { // Integer.class
return o;
} else if (((Class) type).isArray()) { // String[].class
return Array.newInstance($Gson$Types.getRawType(((Class) type).getComponentType()), 0);
}
} else if (type instanceof GenericArrayType) { // String[]
return Array.newInstance($Gson$Types.getRawType(((GenericArrayType) type).getGenericComponentType()), 0);
}
// 其他类型使用constructor创建
TypeToken<?> typeToken = TypeToken.get(type);
return constructor.get(typeToken).construct();
}
}

DeserializeAction

IDataValidateAction:解析后校验数据

解析完数据除了判空,有时还要对一些字段有效性做判断(例如id有效)。能否在解析过程中直接过滤掉无效异常数据,用的时候不需要再判断呢?

可以设计一个IDataValidateAction接口定义如下。自动解析完成后,如果对象实现了这个接口,Gson就会调用isDataValid校验数据,如果数据无效,则直接过滤掉这个对象,返回null。

1
2
3
public interface IDataValidateAction {
boolean isDataValid();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class ValidUser implements IDataValidateAction {

private long id;
private String name;

public long getId() {
return id;
}

public String getName() {
return name;
}

@Override
public boolean isDataValid() {
// 如果id为0或负值,说明接口异常,视为无效数据,丢弃不用
return id > 0;
}
}

IAfterDeserializeAction:解析后预处理数据

接口返回的数据自动解析后,经常和最终需要的数据格式不完全一致,需要进行预处理。例如User的名称返回空串,则需要在客户端显示成“匿名”;再例如接口返回二维数组,而UI组件需要的是带有Type信息的一维数组来实现分组列表。

解决上述问题的常规思路有:

  1. 在网络框架onResponse方法中处理整个Response。onResponse方法在UI线程执行,容易引起卡顿;如果数据Model嵌套层级深,还需要逐层访问;Model与其预处理的代码分散在不同地方,设计不够合理。

  2. 在Model中添加Getter方法,调用时对数据进行处理。Getter中要用标志位判断以避免重复处理;有UI线程卡顿问题。

  3. 包裹一层UI Model,对数据Model做转换。有些繁琐;有UI线程卡顿问题。

  4. 直接注册Deserializer手动解析整个Model。有些繁琐。

结合Gson强大的自定义功能,我们可以通过定义IAfterDeserializeAction接口更好的解决问题。自动解析完成后,如果对象实现了该接口,就会在数据解析线程继续调用doAfterDeserialize方法处理数据。

1
2
3
public interface IAfterDeserializeAction {
void doAfterDeserialize();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static class User implements IAfterDeserializeAction {

private long id;
private String name;

public long getId() {
return id;
}

public String getName() {
return name;
}

@Override
public void doAfterDeserialize() {
if (TextUtils.isEmpty(name)) {
name = "匿名";
}
}
}

实现

DeserializeAction的实现很简单,只需要给Gson注册一个DeserializeActionAdapterFactory即可。这个Factory会判断如果Type实现了DeserializeAction相关接口,则在DelegateAdapter外包裹一层进行相应的处理;否则直接返回DelegateAdapter。

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
35
36
37
public class DeserializeActionAdapterFactory implements TypeAdapterFactory {

public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {

// 获取其他低优先级Factory创建的DelegateAdapter
final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);

// 如果type实现了DeserializeAction,则返回包裹后的TypeAdapter
if (shouldWrap(type.getRawType())) {
return new TypeAdapter<T>() {
public void write(JsonWriter out, T value) throws IOException {
delegate.write(out, value);
}

public T read(JsonReader in) throws IOException {
T t = delegate.read(in);
if (t instanceof IDataValidateAction) {
if (!((IDataValidateAction) t).isDataValid()) {
return null;
}
}
if (t instanceof IAfterDeserializeAction) {
((IAfterDeserializeAction) t).doAfterDeserialize();
}
return t;
}
};
} else {
return delegate;
}
}

private boolean shouldWrap(Class clazz) {
return IAfterDeserializeAction.class.isAssignableFrom(clazz)
IDataValidateAction.class.isAssignableFrom(clazz);
}
}

参考资料与扩展阅读

本文完整示例工程
https://github.com/jzj1993/GsonStudy

Gson源码设计学习
http://www.paincker.com/gson-study

Gson:自定义TypeAdapter
https://www.cnblogs.com/linjzong/p/5201565.html