3.gson-plugin深入源碼分析(三)

一聪富、項目地址

項目地址:github-gson-plugin

二、ReaderTools解析

/**
 * Created by tangfuling on 2018/10/23.
 */

public class ReaderTools {

  private static JsonSyntaxErrorListener mListener;

  public static void setListener(JsonSyntaxErrorListener listener) {
    mListener = listener;
  }

  /**
   * used for array著蟹、collection善涨、map、object
   * skipValue when expected token error
   *
   * @param in input json reader
   * @param expectedToken expected token
   */
  public static boolean checkJsonToken(JsonReader in, JsonToken expectedToken) {
    if (in == null || expectedToken == null) {
      return false;
    }
    JsonToken inToken = null;
    try {
      inToken = in.peek();
    } catch (IOException e) {
      e.printStackTrace();
    }
    if (inToken == expectedToken) {
      return true;
    }
    if (inToken != JsonToken.NULL) {
      String exception = "expected " + expectedToken + " but was " + inToken + " path " + in.getPath();
      notifyJsonSyntaxError(exception);
    }
    skipValue(in);
    return false;
  }

  /**
   * used for basic data type, we only deal type Number and Boolean
   * skipValue when json parse error
   *
   * @param in input json reader
   * @param exception json parse exception
   */
  public static void onJsonTokenParseException(JsonReader in, Exception exception) {
    if (in == null || exception == null) {
      return;
    }
    skipValue(in);
    notifyJsonSyntaxError(exception.getMessage());
  }

  private static void skipValue(JsonReader in) {
    if (in == null) {
      return;
    }
    try {
      in.skipValue();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  private static void notifyJsonSyntaxError(String exception) {
    if (mListener == null) {
      return;
    }
    String invokeStack = Log.getStackTraceString(new Exception("syntax error exception"));
    mListener.onJsonSyntaxError(exception, invokeStack);
  }

  public interface JsonSyntaxErrorListener {
    public void onJsonSyntaxError(String exception, String invokeStack);
  }
}

1.對外暴露setListener()接口草则,用戶可以監(jiān)聽到Json解析異常。
2.checkJsonToken()方法蟹漓,用于判斷輸入字段的數據類型是否與預期的數據類型一致炕横,如果數據類型不一致,則跳過解析葡粒,同時通知listener解析失敗份殿。該方法用于判斷array膜钓、collection、map卿嘲、object是否合法颂斜。
3.onJsonTokenParseException()方法,會利用javassist對Gson拋出的Exception進行捕獲拾枣,然后調用該方法沃疮,同時通知listener解析失敗。該方法用于判斷Integer梅肤、Boolean等基本數據類型司蔬。

三、GsonPlugin插件編寫

1.ReaderTools.java的setListener()方法需要暴露給用戶使用姨蝴,但Plugin僅僅是一個插件俊啼,無法將java語言的接口暴露出去給用戶使用,所以需要建立2個工程左医。
2.gson-plugin-sdk:主要包含ReaderTools.java授帕,與用戶交互的類及方法需要在這個sdk中定義并實現。
3.gson-plugin:主要是侵入編譯流程浮梢,并修改Gson的字節(jié)碼跛十,同時在特定的地方調用ReaderTools.java中的方法,如checkJsonToken()方法黔寇,onJsonTokenParseException()方法等偶器。
4.這樣用戶接入需要引入兩個庫,gson-plugin-sdk和gson-plugin缝裤。
5.為了方便用戶接入屏轰,可以在gson-plugin中幫助用戶引入gson-plugin-sdk,這樣用戶就只需要引入gson-plugin即可憋飞。
6.在gson-plugin中幫助用戶引入gson-plugin-sdk

project.dependencies.add("compile", "com.ke.gson.sdk:gson_sdk:1.3.0")

7.GsonPlugin為插件入口類霎苗,在此注冊自定義的GsonJarTransform

/**
 * Created by tangfuling on 2018/10/25.
 */

class GsonPlugin implements Plugin<Project> {

  @Override
  void apply(Project project) {
    //add dependencies
    project.dependencies.add("compile",
        "com.ke.gson.sdk:gson_sdk:1.3.0")
    //add transform
    project.android.registerTransform(new GsonJarTransform(project))
  }
}

四、GsonJarTransform編譯流程

 @Override
  String getName() {
    return "GsonJarTransform"
  }

  @Override
  void transform(TransformInvocation transformInvocation)
      throws TransformException, InterruptedException, IOException {
    //初始化ClassPool
    MyClassPool.resetClassPool(mProject, transformInvocation)

    //處理jar和file
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
    for (TransformInput input : transformInvocation.getInputs()) {
      for (JarInput jarInput : input.getJarInputs()) {
        // name must be unique榛做,or throw exception "multiple dex files define"
        def jarName = jarInput.name
        if (jarName.endsWith('.jar')) {
          jarName = jarName.substring(0, jarName.length() - 4)
        }
        def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
        //source file
        File file = InjectGsonJar.inject(jarInput.file, transformInvocation.context, mProject)
        if (file == null) {
          file = jarInput.file
        }
        //dest file
        File dest = outputProvider.getContentLocation(jarName + md5Name,
            jarInput.contentTypes, jarInput.scopes, Format.JAR)
        FileUtils.copyFile(file, dest)
      }

      for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
        File dest = outputProvider.getContentLocation(directoryInput.name,
          directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(directoryInput.file, dest)
      }
    }
  }

1.初始化ClassPool唁盏,javassist中用到的類都需要先加入ClassPath。

/**
 * Created by tangfuling on 2018/10/31.
 */

public class MyClassPool {

  private static ClassPool sClassPool

  public static ClassPool getClassPool() {
    return sClassPool
  }

  public static void resetClassPool(Project project, TransformInvocation transformInvocation) {

    // ClassPool.getDefault() 有可能被其他使用 Javassist 的插件污染(如 nuwa)检眯,
    // 導致ClassPool中出現重復的類厘擂,Javassist拋出異常,所以不能使用默認的
    sClassPool = new ClassPool()
    sClassPool.appendSystemPath()

    // bootClasspath 包括 android.jar 和 useLibrary 指定的library 的路徑(如 org.apache.http.legacy )
    project.android.bootClasspath.each {
      sClassPool.appendClassPath(it.absolutePath)
    }

    // 其它class
    for (TransformInput input : transformInvocation.getInputs()) {
      for (JarInput jarInput : input.getJarInputs()) {
        sClassPool.appendClassPath(jarInput.file.getAbsolutePath())
      }
      for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
        sClassPool.appendClassPath(directoryInput.file.getAbsolutePath())
      }
    }
  }
}

2.transform處理過程
2.1.在編譯過程中锰瘸,transform會對項目中所有依賴的jar文件和項目本身的class文件進行處理刽严,將處理結果交給下一個步驟,繼續(xù)處理避凝。
2.2.如果不做任何處理舞萄,那么transform至少會做一件事情眨补,將輸入的jar文件和class文件,拷貝到build/intermediates/transforms/GsonJarTransform目錄倒脓。
2.3.gson-plugin需要對gson.jar做處理撑螺。

File file = InjectGsonJar.inject(jarInput.file, transformInvocation.context, mProject)

五、處理gson.jar包

/**
 * Created by tangfuling on 2018/10/25.
 */

class InjectGsonJar {

  public static File inject(File jarFile, Context context, Project project) throws NotFoundException {
    if (!jarFile.name.contains("gson")) {
      return null
    }
    println("GsonPlugin: inject gson jar start")
    //原始jar path
    String srcPath = jarFile.getAbsolutePath()

    //原始jar解壓后的tmpDir
    String tmpDirName = jarFile.name.substring(0, jarFile.name.length() - 4)
    String tmpDirPath = context.temporaryDir.getAbsolutePath() + File.separator + tmpDirName

    //目標jar path
    String targetPath = context.temporaryDir.getAbsolutePath() + File.separator + jarFile.name

    //解壓
    Decompression.uncompress(srcPath, tmpDirPath)

    //修改
    InjectReflectiveTypeAdapterFactory.inject(tmpDirPath)
    InjectMapTypeAdapterFactory.inject(tmpDirPath)
    InjectArrayTypeAdapter.inject(tmpDirPath)
    InjectCollectionTypeAdapterFactory.inject(tmpDirPath)
    InjectTypeAdapters.inject(tmpDirPath)

    //重新壓縮
    Compressor.compress(tmpDirPath, targetPath)

    //刪除臨時目錄
    StrongFileUtil.deleteDirPath(tmpDirPath)

    println("GsonPlugin: inject gson jar success")

    //返回目標jar
    File targetFile = new File(targetPath)
    if (targetFile.exists()) {
      return targetFile
    }
    return null
  }
}

1.輸入的gson.jar位置:.gradle/caches/modules-2/files-2.1/com.google.code.gson/gson
2.對輸入的jar包解壓到一個臨時目錄崎弃,并對解壓后的class文件進行修改:build/tmp/transformClassesWithGsonJarTransformForDebug甘晤,會生成一個文件夾gson-2.8.5
3.將修改后的文件重新壓縮到當前目錄:build/tmp/transformClassesWithGsonJarTransformForDebug,會重新生成一個jar包gson-2.8.5.jar
4.刪除步驟2中生成的文件夾gson-2.8.5
5.將tmp目錄下的gson-2.8.5.jar返回
6.transform會將tmp目錄下gson-2.8.5.jar拷貝到build/intermediates/transforms/GsonJarTransform目錄供下一個步驟使用吊履。

六安皱、修改內部類的方法

1.這個Adapter.class的read()方法是對Object類型的數據進行解析,我們判斷輸入的數據類型不是Object類型艇炎,就直接跳過解析酌伊,核心是在read()方法中插入ReaderTools.checkJsonToken()方法。
2.每一個類缀踪、每一個內部類居砖、每一個匿名內部類,都會生成一個獨立的.class文件驴娃,如ReflectiveTypeAdapterFactory.class,ReflectiveTypeAdapterFactoryAdapter.class,ReflectiveTypeAdapterFactoryBoundField.class,ReflectiveTypeAdapterFactory$1.class奏候。
3.遍歷文件夾找到對應的class,通過javassist在read()方法前面插入判斷代碼唇敞。

/**
 * Created by tangfuling on 2018/10/30.
 */

public class InjectReflectiveTypeAdapterFactory {

  public static void inject(String dirPath) {

    ClassPool classPool = MyClassPool.getClassPool()

    File dir = new File(dirPath)
    if (dir.isDirectory()) {
      dir.eachFileRecurse { File file ->
        if ("ReflectiveTypeAdapterFactory.class".equals(file.name)) {
          CtClass ctClass = classPool.getCtClass("com.google.gson.internal.bind.ReflectiveTypeAdapterFactory\$Adapter")
          CtMethod ctMethod = ctClass.getDeclaredMethod("read")
          ctMethod.insertBefore("     if (!com.ke.gson.sdk.ReaderTools.checkJsonToken(\$1, com.google.gson.stream.JsonToken.BEGIN_OBJECT)) {\n" +
              "        return null;\n" +
              "      }")
          ctClass.writeFile(dirPath)
          ctClass.detach()
          println("GsonPlugin: inject ReflectiveTypeAdapterFactory success")
        }
      }
    }
  }
}

七蔗草、字節(jié)碼加 try-catch

1.TypeAdapters.class處理基本數據類型,每個基本數據類型都對應一個匿名內部類

    public static final TypeAdapter<Boolean> BOOLEAN = new TypeAdapter<Boolean>() {
      public Boolean read(JsonReader in) throws IOException {
        if(in.peek() == JsonToken.NULL) {
          in.nextNull();
          return null;
        } else {
          return in.peek() == JsonToken.STRING?Boolean.valueOf(Boolean.parseBoolean(in.nextString())):Boolean.valueOf(in.nextBoolean());
        }
      }

      public void write(JsonWriter out, Boolean value) throws IOException {
        if(value == null) {
          out.nullValue();
        } else {
          out.value(value.booleanValue());
        }
      }
    };

2.找到TypeAdapters的所有內部類疆柔,獲取內部類的read()方法的返回值咒精,如果是Number或Boolean類型,添加try-catch代碼塊旷档,并回調ReaderTools.onJsonTokenParseException()方法模叙。

/**
 * Created by tangfuling on 2018/10/30.
 */

public class InjectTypeAdapters {

  public static void inject(String dirPath) {

    ClassPool classPool = MyClassPool.getClassPool()

    File dir = new File(dirPath)
    if (dir.isDirectory()) {
      dir.eachFileRecurse { File file ->
        if (file.name.contains("TypeAdapters\$")) {
          String innerClassName = file.name.substring(13, file.name.length() - 6)
          CtClass ctClass = classPool.getCtClass("com.google.gson.internal.bind.TypeAdapters\$" + innerClassName)
          //only deal type Boolean and Number
          CtMethod[] methods = ctClass.declaredMethods
          boolean isModified = false
          for (CtMethod ctMethod : methods) {
            if ("read".equals(ctMethod.name)) {
              String returnTypeName = ctMethod.getReturnType().name
              if ("java.lang.Number".equals(returnTypeName)
                  || "java.lang.Boolean".equals(returnTypeName)) {
                CtClass etype = classPool.get("java.lang.Exception")
                ctMethod.addCatch("{com.ke.gson.sdk.ReaderTools.onJsonTokenParseException(\$1, \$e); return null;}", etype)
                isModified = true
              }
            }
          }
          if (isModified) {
            ctClass.writeFile(dirPath)
            println("GsonPlugin: inject TypeAdapters success")
          }
          ctClass.detach()
        }
      }
    }
  }
}

3.其中1表示read()方法的第1個參數JsonReader,e表示捕獲的Exception

八鞋屈、目錄

1.gson-plugin告別Json數據類型不一致(一)
2.gson-plugin基礎源碼分析(二)
3.gson-plugin深入源碼分析(三)
4.gson-plugin如何在JitPack發(fā)布(四)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末范咨,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子厂庇,更是在濱河造成了極大的恐慌渠啊,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件权旷,死亡現場離奇詭異替蛉,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門灭返,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坤邪,你說我怎么就攤上這事熙含。” “怎么了艇纺?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵怎静,是天一觀的道長。 經常有香客問我黔衡,道長蚓聘,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任盟劫,我火速辦了婚禮夜牡,結果婚禮上,老公的妹妹穿的比我還像新娘侣签。我一直安慰自己塘装,他們只是感情好,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布影所。 她就那樣靜靜地躺著蹦肴,像睡著了一般。 火紅的嫁衣襯著肌膚如雪猴娩。 梳的紋絲不亂的頭發(fā)上阴幌,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天,我揣著相機與錄音卷中,去河邊找鬼矛双。 笑死,一個胖子當著我的面吹牛仓坞,可吹牛的內容都是我干的背零。 我是一名探鬼主播,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼无埃,長吁一口氣:“原來是場噩夢啊……” “哼徙瓶!你這毒婦竟也來了?” 一聲冷哼從身側響起嫉称,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤侦镇,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后织阅,有當地人在樹林里發(fā)現了一具尸體壳繁,經...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了闹炉。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蒿赢。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖渣触,靈堂內的尸體忽然破棺而出羡棵,到底是詐尸還是另有隱情,我是刑警寧澤嗅钻,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布皂冰,位于F島的核電站,受9級特大地震影響养篓,放射性物質發(fā)生泄漏秃流。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一柳弄、第九天 我趴在偏房一處隱蔽的房頂上張望舶胀。 院中可真熱鬧,春花似錦语御、人聲如沸峻贮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽纤控。三九已至,卻和暖如春碉纺,著一層夾襖步出監(jiān)牢的瞬間船万,已是汗流浹背擦秽。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工腾降, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留峦椰,地道東北人锋叨。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像弃秆,于是被迫代替她去往敵國和親梁剔。 傳聞我的和親對象是個殘疾皇子怯晕,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內容