1. 程式人生 > >新一代Json解析庫Moshi原始碼解析

新一代Json解析庫Moshi原始碼解析

概述

Moshi是Square公司在2015年6月開源的一個Json解析庫,相對於Gson,FastJson等老牌解析庫而言,Moshi不僅支援對Kotlin的解析,並且提供了Reflection跟Annotion兩種解析Kotlin的方法,除此之外,Moshi最大的改變在於支援自定義JsonAdapter,能夠將Json的Value轉換成任意你需要的型別。

基本用法之Java

Dependency

implementation 'com.squareup.moshi:moshi:1.7.0'
複製程式碼

Bean

String json = ...;
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<Bean> jsonAdapter = moshi.adapter(Bean.class);
//Deserialize 
Bean bean = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.toJson(bean);
複製程式碼

List

Moshi moshi = new Moshi.Builder().build();
Type listOfCardsType = Types.newParameterizedType(List.class, Bean.class);
JsonAdapter<List<Bean>> jsonAdapter = moshi.adapter(listOfCardsType);
//Deserialize 
List<Bean> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json);
複製程式碼

Map

Moshi moshi = new Moshi.Builder().build();
ParameterizedType newMapType = Types.newParameterizedType(Map.class, String.class, Integer.class);
JsonAdapter<Map<String,Integer>> jsonAdapter = moshi.adapter(newMapType);
//Deserialize 
Map<String,Integer> beans = jsonAdapter.fromJson(json);
//Serialize
String json = jsonAdapter.fromJson(json); 複製程式碼

Others

  • @json:Key轉換
  • transitent:跳過該欄位不解析
public final class Bean {
  @Json(name = "lucky number") int luckyNumber;
  @Json(name = "objec") int data;
  @Json(name = "toatl_price") String totolPrice;
  private transient int total;//jump the field
}
複製程式碼

基本用法之Kotlin

相對於Java只能通過反射進行解析,針對Kotlin,Moshi提供了兩種解析方式,一種是通過Reflection,一種是通過Annotation,你可以採用其中的一種,也可以兩種都使用,下面分別介紹下這兩種解析方式

Dependency

implementation 'com.squareup.moshi:moshi-kotlin:1.7.0'
複製程式碼

Reflection

Data類
data class ConfigBean(
  var isGood: Boolean = false,
  var title: String = "",
  var type: CustomType = CustomType.DEFAULT
)
複製程式碼
開始解析
val moshi = Moshi.Builder()
    .add(KotlinJsonAdapterFactory())
    .build()
複製程式碼

這種方式會引入Kotlin-Reflect的Jar包,大概有2.5M。

Annotation

上面提到了Reflection,會導致APK體積增大,所以Moshi還提供了另外一種解析方式,就是註解,Moshi的官方叫法叫做Codegen,因為是採用註解生成的,所以除了新增Moshi的Kotlin依賴之外,還需要加上kapt

kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.7.0'
複製程式碼
改造Data類

給我們的資料類增加JsonClass註解

@JsonClass(generateAdapter = true)
data class ConfigBean(
  var isGood: Boolean = false,
  var title: String = "",
  var type: CustomType = CustomType.DEFAULT
)
複製程式碼

這樣的話,Moshi會在編譯期生成我們需要的JsonAdapter,然後通過JsonReader遍歷的方式去解析Json資料,這種方式不僅僅不依賴於反射,而且速度快於Kotlin。

高階用法(JsonAdapter)

JsonAdapter是Moshi有別於Gson,FastJson的最大特點,顧名思義,這是一個Json的轉換器,他的主要作用在於將拿到的Json資料轉換成任意你想要的型別,Moshi內建了很多JsonAdapter,有如下這些:

Built-in Type Adapters

  • Map:MapJsonAdapter
  • Enums:EnumJsonAdapter
  • Arrays:ArrayJsonAdapter
  • Object:ObjectJsonAdapter
  • String:位於StandardJsonAdapters,採用匿名內部類實現
  • Primitives (int, float, char,boolean) :基本資料型別的Adapter都在StandardJsonAdapters裡面,採用匿名內部類實現

Custom Type Adapters

對於一些比較簡單規範的資料,使用Moshi內建的JsonAdapter已經完全能夠Cover住,但是由於Json只支援基本資料型別傳輸,所以很多時候不能滿足業務上需要,舉個例子:

{
"type": 2,
"isGood": 1
"title": "TW9zaGkgaXMgZmxleGlibGU="
}
複製程式碼

這是一個很普通的Json,包含了5個欄位,我們如果按照服務端返回的欄位來定義解析的Bean,顯然是可以完全解析的,但是我們在實際呼叫的時候,這些資料並不是很乾淨,我們還需要處理一下:

  • type:Int型別,我需要Enum,我得定義一個Enum的轉換類,去將Int轉換成Enum
  • isGood:Int型別,我需要Boolean,所以我用的時候還得將Int轉成Boolean
  • title:String型別,這個欄位是加密過的,可能是通過AES或者RSA加密,這裡我們為了方便測試,只是用Base64對Moshi is flexible對進行encode。

對於客戶端的同學來說,好像沒毛病,以前都是這麼幹的,如果這種不乾淨的Json少點還好,多了之後就很頭疼,每個在用的時候都需要轉一遍,很多時候我這麼幹的時候都覺得浪費時間,而今天有了Moshi之後,我們只需要針對需要轉換的型別定義對應的JsonAdapter,達到一次定義,一勞永逸的效果,Moshi針對常見的資料型別已經定義了Adapter,但是內建的Adapter現在已經不能滿足我們的需求了,所以我們需要自定義JsonAdapter。

實體定義

class ConfigBean {
  public CustomType type;
  public Boolean isGood;
  public String title;
}
複製程式碼

此處我們定義的資料型別不是根據伺服器返回的Json資料,而是定義的我們業務需要的格式,那麼最終是通過JsonAdapter轉換器來完成這個轉換,下面開始自定義JsonAdapter。

Int->Enum

CustomType
enum CustomType {
  DEFAULT(0, "DEFAULT"), BAD(1, "BAD"), NORMAL(2, "NORMAL"), GOOD(3, "NORMAL");
  public int type;
  public String content;
  CustomType(int type, String content) {
    this.type = type;
    this.content = content;
  }
}
複製程式碼
TypeAdapter

定義一個TypeAdapter繼承自JsonAdapter,傳入對應的泛型,會自動幫我們複寫fromJson跟toJson兩個方法

public class TypeAdapter  {
  @FromJson
  public CustomType fromJson(int value) throws IOException {
    CustomType type = CustomType.DEFAULT;
    switch (value) {
      case 1:
        type = CustomType.BAD;
        break;
      case 2:
        type = CustomType.NORMAL;
        break;
      case 3:
        type = CustomType.GOOD;
        break;
    }
    return type;
  }
  @ToJson
  public Integer toJson(CustomType value)  {
    return value != null ? value.type : 0;
  }
}

複製程式碼

至此已經完成Type的轉換,接下來我們再以title舉個例子,別的基本上都是照葫蘆畫瓢,沒什麼難度

StringDecode

TitleAdapter
public class TitleAdapter {
  @FromJson
  public String fromJson(String value) {
    byte[] decode = Base64.getDecoder().decode(value);
    return new String(decode);
  }
  @ToJson
  public String toJson(String value) {
   return new String(Base64.getEncoder().encode(value.getBytes()));
  }
}
複製程式碼

Int->Boolean

BooleanAdapter
public class BooleanAdapter {
  @FromJson
  public Boolean fromJson(int value) {
    return value == 1;
  }
  @ToJson
  public Integer toJson(Boolean value) {
    return value ? 1 : 0;
  }
}

複製程式碼

Adapter測試

下面我們來測試一下

  String json = "{\n" + "\"type\": 2,\n" + "\"isGood\": 1,\n"
      + "\"title\": \"TW9zaGkgaXMgZmxleGlibGU=\"\n"+ "}";
    Moshi moshi = new Moshi.Builder()
        .add(new TypeAdapter())
        .add(new TitleAdapter())
        .add(new BooleanAdapter())
        .build();
    JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
    ConfigBean cofig = jsonAdapter.fromJson(json);
    System.out.println("=========Deserialize ========");
    System.out.println(cofig);
    String cofigJson = jsonAdapter.toJson(cofig);
    System.out.println("=========serialize ========");
    System.out.println(cofigJson);
複製程式碼

列印Log

=========Deserialize ========
ConfigBean{type=CustomType{type=2, content='NORMAL'}, isGood=true, title='Moshi is flexible'}
=========serialize ========
{"isGood":1,"title":"TW9zaGkgaXMgZmxleGlibGU=","type":2}
複製程式碼

符合我們預期的結果,並且我們在開發的時候,只需要將Moshi設定成單例的,一次性將所有的Adapter全部add進去,就可以一勞永逸,然後愉快地進行開發了。

原始碼解析

Moshi底層採用了Okio進行優化,但是上層的JsonReader,JsonWriter等程式碼是直接從Gson借鑑過來的,所以不再過多分析,主要是就Moshi的兩大創新點JsonAdapter以及Kotlin的Codegen解析重點分析一下。

Builder

   Moshi moshi = new Moshi.Builder().add(new BooleanAdapter()).build();
複製程式碼

Moshi是通過Builder模式進行構建的,支援新增多個JsonAdapter,下面先看看Builder原始碼

public static final class Builder {
//儲存所有Adapter的建立方式,如果沒有新增自定義Adapter,則為空
final List<JsonAdapter.Factory> factories = new ArrayList<>();
//新增自定義Adapter,並返回自身
public Builder add(Object adapter) {
     return add(AdapterMethodsFactory.get(adapter));
    }
//新增JsonAdapter的建立方法到factories裡,並返回自身
public Builder add(JsonAdapter.Factory factory) {
      factories.add(factory);
      return this;
    }
//新增JsonAdapter的建立方法集合到factories裡,並返回自身
public Builder addAll(List<JsonAdapter.Factory> factories) {
      this.factories.addAll(factories);
      return this;
    }
 //通過Type新增Adapter的建立方法,並返回自身
public <T> Builder add(final Type type, final JsonAdapter<T> jsonAdapter) {
      return add(new JsonAdapter.Factory() {
  @Override 
  public @Nullable JsonAdapter<?> create(
     Type targetType, Set<? extends Annotation> annotations, Moshi moshi) { return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
        }
      });
    }
//建立一個Moshi的例項
public Moshi build() {
      return new Moshi(this);
    }
  }
複製程式碼

通過原始碼發現Builder儲存了所有自定義Adapter的建立方式,然後呼叫Builder的build方式建立了一個Moshi的例項,下面看一下Moshi的原始碼。

Moshi

構造方法
  Moshi(Builder builder) {
    List<JsonAdapter.Factory> factories = new ArrayList<>(
      builder.factories.size() + BUILT_IN_FACTORIES.size());
    factories.addAll(builder.factories);
    factories.addAll(BUILT_IN_FACTORIES);
    this.factories = Collections.unmodifiableList(factories);
  }
複製程式碼

構造方法裡面建立了factories,然後加入了Builder中的factories,然後又增加了一個BUILT_IN_FACTORIES,我們應該也能猜到這個就是Moshi內建的JsonAdapter,點進去看一下

BUILT_IN_FACTORIES
 static final List<JsonAdapter.Factory> BUILT_IN_FACTORIES = new ArrayList<>(5);
  static {
    BUILT_IN_FACTORIES.add(StandardJsonAdapters.FACTORY);
    BUILT_IN_FACTORIES.add(CollectionJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES.add(MapJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES.add(ArrayJsonAdapter.FACTORY);
    BUILT_IN_FACTORIES.add(ClassJsonAdapter.FACTORY);
  }
複製程式碼

BUILT_IN_FACTORIES這裡面提前用一個靜態程式碼塊加入了所有內建的JsonAdapter

JsonAdapter

JsonAdapter<ConfigBean> jsonAdapter = moshi.adapter(ConfigBean.class);
複製程式碼

不管是我們自定義的JsonAdapter還是Moshi內建的JsonAdapter,最終都是為我們的解析服務的,所以最終所有的JsonAdapter最終匯聚成JsonAdapter,我們看看是怎麼生成的,跟一下Moshi的adapter方法,發現最終呼叫的是下面的方法

public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations,
      @Nullable String fieldName) {
    type = canonicalize(type);
    // 如果有對應的快取,那麼直接返回快取
    Object cacheKey = cacheKey(type, annotations);
    synchronized (adapterCache) {
      JsonAdapter<?> result = adapterCache.get(cacheKey);
      if (result != null) return (JsonAdapter<T>) result;
    }
  
    boolean success = false;
    JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
    try {
      if (adapterFromCall != null)
          return adapterFromCall;
      // 遍歷Factories,直到命中泛型T的Adapter
     for (int i = 0, size = factories.size(); i < size; i++) {
 JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
        if (result == null) continue;
        lookupChain.adapterFound(result);
        success = true;
        return result;
      }
    } 
  }
複製程式碼

最開始看到這裡,我比較奇怪,不太確定我的Config命中了哪一個JsonAdapter,最終通過斷點追蹤,發現了是命中了ClassJsonAdapter,既然命中了他,那麼我們就看一下他的具體實現

ClassJsonAdapter

構造方法

final class ClassJsonAdapter<T> extends JsonAdapter<T> {
  public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
    @Override public @Nullable JsonAdapter<?> create(
        Type type, Set<? extends Annotation> annotations, Moshi moshi) {
        //省略了很多異常判斷程式碼
      Class<?> rawType = Types.getRawType(type);
      //獲取Class的所有型別
      ClassFactory<Object> classFactory = ClassFactory.get(rawType);
      Map<String, FieldBinding<?>> fields = new TreeMap<>();
      for (Type t = type; t != Object.class; t = Types.getGenericSuperclass(t)) {
        //建立Moshi跟Filed的繫結關係,便於解析後賦值
        createFieldBindings(moshi, t, fields);
      }
      return new ClassJsonAdapter<>(classFactory, fields).nullSafe();
    }
}
複製程式碼

當我們拿到一個JsonAdapter的時候,基本上所有的構建都已經完成,此時可以進行Deserialize 或者Serialize 操作,先看下Deserialize 也就是fromjson方法

JsonReader&JsonWriter

對於Java的解析,Moshi並沒有在傳輸效率上進行顯著的提升,只是底層的IO操作採用的是Okio,Moshi的創新在於靈活性上面,也就是JsonAdapter,而且Moshi的官方文件上面也提到了

Moshi uses the same streaming and binding mechanisms as Gson. If you’re a Gson user you’ll find Moshi works similarly. If you try Moshi and don’t love it, you can even migrate to Gson without much violence!

所以這裡的JsonReader跟JsonWriter說白了都是從Gson那裡直接借鑑過來的,就是這麼坦誠。

fromjson
ConfigBean cofig = jsonAdapter.fromJson(json);
複製程式碼

這個方法先是呼叫了父類JsonAdapter的fromJson方法

 public abstract  T fromJson(JsonReader reader) throws IOException;
 public final  T fromJson(BufferedSource source) throws IOException {
    return fromJson(JsonReader.of(source));
  }
 public final  T fromJson(String string) throws IOException {
    JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
    T result = fromJson(reader);
    return result;
  
複製程式碼

我們發現fromJson是個過載方法,既可以傳String也可以傳BufferedSource,不過最終呼叫的都是fromJson(JsonReader reader)這個方法,BufferedSource是Okio的一個類,因為Moshi底層的IO採用的是Okio,但是我們發現引數為JsonReader的這個方法是抽象方法,所以具體的實現是是在ClassJsonAdapter裡面,。

 @Override public T fromJson(JsonReader reader) throws IOException {
    T  result = classFactory.newInstance();
    try {
      reader.beginObject();
      while (reader.hasNext()) {
        int index = reader.selectName(options);
        //如果不是Key,直接跳過
        if (index == -1) {
          reader.skipName();
          reader.skipValue();
          continue;
        }
        //解析賦值
        fieldsArray[index].read(reader, result);
      }
      reader.endObject();
      return result;
    } catch (IllegalAccessException e) {
      throw new AssertionError();
    }
  }
  
//遞迴呼叫,直到最後 
void read(JsonReader reader, Object value) throws IOException, IllegalAccessException {
      T fieldValue = adapter.fromJson(reader);
      field.set(value, fieldValue);
    }
複製程式碼
toJson
String cofigJson = jsonAdapter.toJson(cofig);
複製程式碼

跟fromJson一樣,先是呼叫的JsonAdapter的toJson方法

 public abstract void toJson(JsonWriter writer,  T value) throws IOException;
 public final void toJson(BufferedSink sink, T value) throws IOException {
    JsonWriter writer = JsonWriter.of(sink);
    toJson(writer, value);
  }
 public final String toJson( T value) {
    Buffer buffer = new Buffer();
    try {
      toJson(buffer, value);
    } catch (IOException e) {
      throw new AssertionError(e); // No I/O writing to a Buffer.
    }
    return buffer.readUtf8();
  }
複製程式碼

不管傳入的是泛型T還是BufferedSink,最終呼叫的toJson(JsonWriter writer),然後返回了buffer.readUtf8()。我們繼續看一下子類的具體實現


  @Override public void toJson(JsonWriter writer, T value) throws IOException {
    try {
      writer.beginObject();
      for (FieldBinding<?> fieldBinding : fieldsArray) {
        writer.name(fieldBinding.name);
        //將fieldsArray的值依次寫入writer裡面
        fieldBinding.write(writer, value);
      }
      writer.endObject();
    } catch (IllegalAccessException e) {
      throw new AssertionError();
    }
  }
複製程式碼

Codegen

Moshi’s Kotlin codegen support is an annotation processor. It generates a small and fast adapter for each of your Kotlin classes at compile time. Enable it by annotating each class that you want to encode as JSON:

所謂Codegen,也就是我們上文提到的Annotation,在編譯期間生成對應的JsonAdapter,我們看一下先加一下註解,看看Kotlin幫我們自動生成的註解跟我們自定義的註解有什麼區別,rebuild一下專案:

CustomType

@JsonClass(generateAdapter = true)
data class CustomType(var type: Int, var content: String)
複製程式碼

我們來看一下對應生成的JsonAdapter

CustomTypeJsonAdapter

這個類方法很多,我們重點看一下formJson跟toJson

 override fun fromJson(reader: JsonReader): CustomType {
        var type: Int? = null
        var content: String? = null
        reader.beginObject()
        while (reader.hasNext()) {
            when (reader.selectName(options)) {
            //按照變數的定義順序依次賦值
                0 -> type = intAdapter.fromJson(reader) 
                1 -> content = stringAdapter.fromJson(reader) 
                -1 -> {
                    reader.skipName()
                    reader.skipValue()
                }
            }
        }
        reader.endObject()
        //不通過反射,直接建立物件,傳入解析的Value
        var result = CustomType(type = type ,content = content )
        return result
    }

    override fun toJson(writer: JsonWriter, value: CustomType?) {
        writer.beginObject()
        writer.name("type")//寫入type
        intAdapter.toJson(writer, value.type)
        writer.name("content")//寫入content
        stringAdapter.toJson(writer, value.content)
        writer.endObject()
    }
複製程式碼

ConfigBean

@JsonClass(generateAdapter = true)
data class ConfigBean(var isGood: Boolean ,var title: String ,var type: CustomType)
複製程式碼

ConfigBeanJsonAdapter

override fun fromJson(reader: JsonReader): ConfigBean {
    var isGood: Boolean? = null
    var title: String? = null
    var type: CustomType? = null
    reader.beginObject()
    while (reader.hasNext()) {
        when (reader.selectName(options)) {
            0 -> isGood = booleanAdapter.fromJson(reader) 
            1 -> title = stringAdapter.fromJson(reader) 
            2 -> type = customTypeAdapter.fromJson(reader)
            -1 -> {
                reader.skipName()
                reader.skipValue()
            }
        }
    }
    reader.endObject()
    var result = ConfigBean(isGood = isGood ,title = title ,type = type
    return result
}

override fun toJson(writer: JsonWriter, value: ConfigBean?) {
    writer.beginObject()
    writer.name("isGood")
    booleanAdapter.toJson(writer, value.isGood)
    writer.name("title")
    stringAdapter.toJson(writer, value.title)
    writer.name("type")
    customTypeAdapter.toJson(writer, value.type)
    writer.endObject()
}
複製程式碼

通過檢視生成的CustomTypeJsonAdapter以及ConfigBeanJsonAdapter,我們發現通過Codegen生成也就是註解的方式,跟反射對比一下,會發現有如下優點:

  • 效率高:直接建立物件,無需反射
  • APK體積小:無需引入Kotlin-reflect的Jar包

注意事項

在進行kotlin解析的時候不管是採用Reflect還是Codegen,都必須保證型別一致,也就是父類跟子類必須是Java或者kotlin,因為兩種解析方式,最終都是通過ClassType來進行解析的,同時在使用Codegen解析的時候必須保證Koltin的型別是internal或者public的。

總結

Moshi整個用法跟原始碼看下來,其實並不是很複雜,但是針對Java跟Kotlin的解析增加了JsonAdapter的轉換,以及針對Kotlin的Data類的解析提供了Codegen這種方式,真的讓人耳目一新,以前遇到這種業務呼叫的時候需要二次轉換的時候,都是去寫工具類或者用的時候直接轉換。不過Moshi也有些缺點,對於Kotlin的Null型別的支援並不友好,這樣會在Kotlin解析的時候如果對於一個不可空的欄位變成了Null就會直接拋異常,感覺不太友好,應該給個預設值比較好一些,還有就是對預設值的支援,如果Json出現了Null型別,那麼解析到對應的欄位依然會被賦值成Null,跟之前的Gson一樣,希望以後可以進行完善,畢竟瑕不掩瑜。