遇到问题
最近得到用户反馈,有些界面请求数据失败,调试接口发现,是后台返回的类型不确定导致的,例如:
这是一段我们需要的正常json:
{
"id":1,
"number":100000000,
"user":{
"name":"aoteman",
"userId":110
}
}
但是后台有时会返回这样一段json:
{
"id":"",
"number":"",
"user":""
}
可以发现,本来是int型的id,long型的number和Object的类型的user都变成了空字符串。只要是返回结果为null的,都有可能返回成空字符串,可以猜到后台可能是使用了PHP这种弱类型语言,并且没有对字段做类型校验。这本来应该是后台的锅,但是部门之间沟通困难,只能先我们客户端解决了。
解决方法
Gson在GsonBuilder提供了registerTypeAdapter这个方法,让我们对特定的模型进行自定义序列化和反序列化。这里提供了俩种序列化和反序列化的方式:
- 第一种是继承自JsonSerializer和JsonDeserializer接口,我们主要看JsonDeserializer,代码如下:
public class Deserializer implements JsonDeserializer<Test> {
@Override
public Test deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context)
throws JsonParseException {
final JsonObject jsonObject = json.getAsJsonObject();
final JsonElement jsonId = jsonObject.get("id");
final JsonElement jsonNumber = jsonObject.get("number");
final Test test = new Test();
try {
test.setId(jsonId.getAsInt());
}catch (Exception e){
//id不是int型的时候,捕获异常,并且设置id的值为0
test.setId(0);
}
try {
test.setNumber(jsonNumber.getAsLong());
}catch (Exception e){
test.setNumber(0);
}
return test;
}
}
这种方式类似于查字典,gson会把json先转换成JsonElement的结构,JsonElement有4种子类,JsonObject(对象结构)、JsonArray(数组结构)、JsonPrimitive(基本类型)、JsonNull。
JsonObject内部其实维护了一个HashMap,jsonObject.get("id");其实就是查字段。这种方法对性能的消耗比较大,因为它需要先把流数据转换成JsonElement的结构对象,这样会产生更大的内存消耗和运行时间。
- 第二种是继承自TypeAdapter,代码如下:
new GsonBuilder().registerTypeAdapter(Test.class,
new TypeAdapter<Test>() {
public Test read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
in.beginObject();
Test test = new Test();
while (in.hasNext()) {
switch (in.nextName()) {
case "id":
try {
test.setId(in.nextInt());
} catch (Exception e) {
in.nextString();
test.setId(0);
}
break;
case "number":
try {
test.setNumber(in.nextLong());
} catch (Exception e) {
in.nextString();
test.setNumber(0);
}
break;
}
}
return test;
}
public void write(JsonWriter out, Test src) throws IOException {
if (src == null) {
out.nullValue();
return;
}
}
})
这种方式相比JsonElement更加高效,因为它直接是用流来解析数据,去掉了JsonElement这个中间件,它流式的API相比于第一种的树形解析API将会更加高效。
那么问题的解决办法可以这样,如下代码:
//int类型的解析器
private static TypeAdapter<Number> INTEGER = new TypeAdapter<Number>() {
@Override
public Number read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
try {
return in.nextInt();
} catch (NumberFormatException e) {
//这里解析int出错,那么捕获异常并且返回默认值,因为nextInt出错中断了方法,没有完成位移,所以调用nextString()方法完成位移。
in.nextString();
return 0;
}
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
out.value(value);
}
};
private static TypeAdapter<Number> LONG = new TypeAdapter<Number>() {
@Override
public Number read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
//这里同
try {
return in.nextLong();
} catch (Exception e) {
in.nextString();
}
return 0;
}
@Override
public void write(JsonWriter out, Number value) throws IOException {
out.value(value);
}
};
Gson gson = new GsonBuilder()
.registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER))
.registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG))
.create();
以上代码可以解决int和long类型的返回空字符串问题。但是自定义类型怎么办,不可能把每种类型都注册进来,我们先看看Gson是怎么做的,查看Gson的构造方法,发现下面一段代码:
factories.add(new ReflectiveTypeAdapterFactory(
constructorConstructor, fieldNamingPolicy, excluder));
查看ReflectiveTypeAdapterFactory内容发现,这个是对所有用户定义类型的解析器。修改问题主要定位在ReflectiveTypeAdapterFactory类中的Adapter的read方法。我们只要修改read方法就可以解决问题:
@Override
public T read(JsonReader in) throws IOException {
if (in.peek() == JsonToken.NULL) {
in.nextNull();
return null;
}
T instance = constructor.construct();
//这里对beginObject进行异常捕获,如果不是object,说明可能是"",直接返回null,不中断解析
try {
in.beginObject();
} catch (Exception e) {
in.nextString();
return null;
}
try {
int count = 0;
while (in.hasNext()) {
count++;
String name = in.nextName();
BoundField field = boundFields.get(name);
if (field == null || !field.deserialized) {
in.skipValue();
} else {
field.read(in, instance);
}
}
if (count == 0) return null;
} catch (IllegalStateException e) {
throw new JsonSyntaxException(e);
} catch (IllegalAccessException e) {
throw new AssertionError(e);
}
in.endObject();
return instance;
}
ReflectiveTypeAdapterFactory是个final类不能继承,并且构造方法有几个重要参数从Gson传入,所以我们只能把ReflectiveTypeAdapterFactory复制一份出来修改,并且利用反射修改Gson,吧修改的类替换掉原来的,代码如下:
public static Gson buildGson() {
Gson gson = new GsonBuilder().registerTypeAdapterFactory(TypeAdapters.newFactory(int.class, Integer.class, INTEGER))
.registerTypeAdapterFactory(TypeAdapters.newFactory(long.class, Long.class, LONG))
.create();
try {
Field field = gson.getClass().getDeclaredField("constructorConstructor");
field.setAccessible(true);
ConstructorConstructor constructorConstructor = (ConstructorConstructor) field.get(gson);
Field factories = gson.getClass().getDeclaredField("factories");
factories.setAccessible(true);
List<TypeAdapterFactory> data = (List<TypeAdapterFactory>) factories.get(gson);
List<TypeAdapterFactory> newData = new ArrayList<>(data);
newData.remove(data.size() - 1);
newData.add(new MyReflectiveTypeAdapterFactory(constructorConstructor, FieldNamingPolicy.IDENTITY, Excluder.DEFAULT));
newData = Collections.unmodifiableList(newData);
factories.set(gson, newData);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return gson;
}
问题到此得到解决,因为项目很少用到float、byte等类型的字段,所以就没有适配,如果有需要也可以通过以上方式解决。
示例代码托管在github上。