Gson使用技巧小结

Gson使用技巧小结

背景

json格式在移动端开发中再熟悉不过了,相较xml等格式,json有易读、体积小等优点。在解析json格式时,个人比较习惯使用Google的gson工具包,之前看过gson和阿里fastjson的性能比较,貌似gson在数据量不大时性能更好。目前项目级别使用gson完全能够胜任。
开始的时候使用gson只是简单的新建Gson实例,调用from方法解析成对应model类。随着业务发展和http模块的升级,简单的toJson和from方法已经不能满足需求了。因为接口返回数据的差异性,不同情形下可能使用不同的解析策略,最简单的方法当然是针对每个接口返回的数据使用相对应的model,但这样很容易造成model对象过多过杂难以管理和分辨。另外接口返回数据命名策略也可能因人而异,下划线和驼峰式都有可能,为了保证Application端代码的一致性,就要想办法把下划线风格的转为驼峰式风格。这些需求gson通通能够解决。

Gson基础

from和toJson方法,分别用于json格式字符串转为Model对象、对象转json。很好理解。

public static void baseGson() {
    String json = "{\"name\":\"gson\",\"date\":\"2015/12/29\"}";
    BaseGson fromJson = getSimpleGson().fromJson(json, BaseGson.class);

    String toJson = getSimpleGson().toJson(fromJson);
}

static class BaseGson {
    public String name;
    public String date;

    @Override
    public String toString() {
        return "BaseGson{" +
                "name='" + name + '\'' +
                ", date='" + date + '\'' +
                '}';
    }
}

注意这里的fromJson方法后可以跟type类型或class类型。解析List时多用TypeToken,如下:

new TypeToken<List<BaseGson>>(){}.getType();

Gson注解

介绍下常用几个注解,Expose和SerializedName。

  • SerializedName

举个例子,解决接口和本地model风格差异问题,比如返回的字段mobile_phone要解析为mobilePhone,只需在本地model类中添加注解@SerializedName("mobile_phone")即可,代码如下:

public static void annotataionGson(){
    String json = "{\"name\":\"gson\",\"date\":\"2015/12/29\", \"mobile_phone\":\"13111111111\"}";
    BaseGson fromJson = getSimpleGson().fromJson(json, BaseGson.class);

    Log.d("Gson", fromJson.toString());
}

static class BaseGson {
    public String name;
    public String date;
    @SerializedName("mobile_phone")
    public String mobilePhone;

    @Override
    public String toString() {
        return "BaseGson{" +
                "name='" + name + '\'' +
                ", date='" + date + '\'' +
                ", mobilePhone='" + mobilePhone + '\'' +
                '}';
    }
}

输出为

12-29 05:59:27.490 15245-15245/? D/Gson: BaseGson{name='gson', date='2015/12/29', mobilePhone='13111111111'}

相当方便吧,SerializedName后跟的字段就是转化后/被转化前json字符串时显示的字段。此外,除了注解Gson还提供Builder方式建造不同策略的gson对象,例如

Gson policyGson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
            .create();

使用这样的policyGson,toJson时会按照“小写下划线”格式输出(toJson)。不过对@SerializedName不生效,即优先级小于@SerializedName

  • Expose

用来标示toJson时输出的字段。
比如对name字段使用Expose。同时开启excludeFieldsWithoutExposeAnnotation。代码如下:

public static void annotataionGson(){
    String json = "{\"name\":\"gson\",\"date\":\"2015/12/29\", \"mobile_phone\":\"13111111111\"}";
    Gson g = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();
    BaseGson fromJson = g.fromJson(json, BaseGson.class);

    Log.d("Gson", fromJson.toString());

    Log.d("Gson", g.toJson(fromJson));
}

static class BaseGson {
    @Expose
    public String name;
    public String date;
    @SerializedName("mobile_phone")
    public String mobilePhone;

    @Override
    public String toString() {
        return "BaseGson{" +
                "name='" + name + '\'' +
                ", date='" + date + '\'' +
                ", mobilePhone='" + mobilePhone + '\'' +
                '}';
    }
}

则输出

12-29 08:02:08.922 26266-26266/? D/Gson: BaseGson{name='gson', date='null', mobilePhone='null'}
12-29 08:02:08.926 26266-26266/? D/Gson: {"name":"gson"}

除此,还有根据关键字来筛选序列化、反序列化时要忽略的字段,new GsonBuilder().excludeFieldsWithModifiers(Modifier.TRANSIENT).create();这样transient修饰的字段就会被忽略啦。

Gson的TypeAdapter

typeAdapter是灵活使用Gson的一大利器,使用场景:服务器的小伙伴传来的json为(这里使用了测试站Get数据

{
  "args": {
    "intValue": "123", 
    "name": "test"
  }, 
  "headers": {
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", 
    "Accept-Encoding": "gzip, deflate, sdch", 
    "Accept-Language": "zh-CN,zh;q=0.8", 
    "Cookie": "_ga=GA1.2.867290806.1451279087", 
    "Host": "httpbin.org", 
    "Upgrade-Insecure-Requests": "1", 
    "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/47.0.2526.106 Safari/537.36"
  }, 
  "origin": "123.126.22.222", 
  "url": "http://httpbin.org/get?name=test&intValue=123"
}

很不幸本地model类没有考虑这么复杂,所有字段均为String类型,且并不需要嵌套过深过细的数据结构。要是能转化为以下数据结构对应的model就好了。

{
  "args": "xx", 
  "headers": "xx", 
  "origin": "123.126.22.222", 
  "url": "http://httpbin.org/get?name=test&intValue=123"
}

使用typeAdapter就可以自定义解析格式。首先要自己新建TypeAdapter的子类,有点类似于实现Parcelable接口,需要实现write和read方法。

/**
 * Created by opticalix@gmail.com on 15/12/28.
 */
public class HttpBinTypeAdapter extends TypeAdapter<HttpBinModel> {
    @Override
    public void write(JsonWriter out, HttpBinModel value) throws IOException {
        //value write to json
        if (value == null) {
            out.nullValue();
        } else {
            out.beginObject();
            out.name("args").value(value.args);
            out.name("headers").value(value.headers);
            out.name("origin").value(value.origin);
            out.name("url").value(value.url);
            out.endObject();
        }
    }
    @Override
    public HttpBinModel read(JsonReader in) throws IOException {
        //read json, return model
        if (in.peek() == JsonToken.NULL) {
            return null;
        } else {
            in.beginObject();
            HttpBinModel model = new HttpBinModel();
            if (in.nextName().equals("args")) {
                in.beginObject();
                String args = "";
                while (in.hasNext()) {
                    if (!TextUtils.isEmpty(args)) args += ", ";
                    args += in.nextName() + "=" + in.nextString();
                }
                model.args = args;
                in.endObject();
            }
            if (in.nextName().equals("headers")) {
                in.beginObject();
                String headers = "";
                while (in.hasNext()) {
                    if (!TextUtils.isEmpty(headers)) headers += ", ";
                    headers += in.nextName() + "=" + in.nextString();
                }
                model.headers = headers;
                in.endObject();
            }
            if (in.nextName().equals("origin")) {
                model.origin = in.nextString();
            }
            if (in.nextName().equals("url")) {
                model.url = in.nextString();
            }
            in.endObject();
            return model;
        }
    }
}

write比较简单,如果model不为空,则输出model中各个字段到JsonWriter。注意out.beginObject();out.endObject();,它处理的是json中的大小括号。
read方法,实现的是从reader中恢复model的过程,关键代码在于

while (in.hasNext()) {
    if (!TextUtils.isEmpty(args)) args += ", ";
    args += in.nextName() + "=" + in.nextString();
}

由于我们不希望Gson死板地解析args为一个jsonObject,这就需要手动空读‘{’即in.beginObject();,循环读args中子字段但都只做拼接操作,最终将拼接好的String赋值给本地model:

model.args = args;
in.endObject();

最后别忘了返回model。

总结

Gson用法远比我之前想象的灵活(才学疏浅 - -),Gson建造者模式能够创建出各式各样策略的gson对象,本文也只是总结自己项目中用到的一些点,像复杂map的解析什么的都没有涉及,日后可以补充。

参考

http://www.javacreed.com/gson-typeadapter-example/