资讯专栏INFORMATION COLUMN

gson-plugin深入源码分析(三)

pkwenda / 2986人阅读

摘要:六修改内部类的方法这个的方法是对类型的数据进行解析,我们判断输入的数据类型不是类型,就直接跳过解析,核心是在方法中插入方法。每一个类每一个内部类每一个匿名内部类,都会生成一个独立的文件,如。

一、项目地址

项目地址: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()接口,用户可以监听到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的字节码,同时在特定的地方调用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 {

  @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文件进行处理,将处理结果交给下一个步骤,继续处理。
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,ReflectiveTypeAdapterFactory$Adapter.class,ReflectiveTypeAdapterFactory$BoundField.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)) {
" +
              "        return null;
" +
              "      }")
          ctClass.writeFile(dirPath)
          ctClass.detach()
          println("GsonPlugin: inject ReflectiveTypeAdapterFactory success")
        }
      }
    }
  }
}
七、字节码加 try-catch

1.TypeAdapters.class处理基本数据类型,每个基本数据类型都对应一个匿名内部类

    public static final TypeAdapter BOOLEAN = new TypeAdapter() {
      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发布(四)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/14564.html

相关文章

  • gson-plugin深入源码分析

    摘要:六修改内部类的方法这个的方法是对类型的数据进行解析,我们判断输入的数据类型不是类型,就直接跳过解析,核心是在方法中插入方法。每一个类每一个内部类每一个匿名内部类,都会生成一个独立的文件,如。 一、项目地址 项目地址:github-gson-plugin 二、ReaderTools解析 /** * Created by tangfuling on 2018/10/23. */ pu...

    oujie 评论0 收藏0
  • gson-plugin如何在JitPack发布(四)

    摘要:一项目地址项目地址二与关系普通的库可以通过源码的方式直接引入并使用,是一个插件,无法通过源码的方式使用,只能编译并发布以后,才能被正常使用。是一个代码仓库,我们可以将源代码托管在这个平台上。 一、项目地址 项目地址:github-gson-plugin 二、github与JitPack关系 1.普通的java库可以通过源码的方式直接引入并使用,gson-plugin是一个插件,无法通过...

    hyuan 评论0 收藏0
  • gson-plugin如何在JitPack发布(四)

    摘要:一项目地址项目地址二与关系普通的库可以通过源码的方式直接引入并使用,是一个插件,无法通过源码的方式使用,只能编译并发布以后,才能被正常使用。是一个代码仓库,我们可以将源代码托管在这个平台上。 一、项目地址 项目地址:github-gson-plugin 二、github与JitPack关系 1.普通的java库可以通过源码的方式直接引入并使用,gson-plugin是一个插件,无法通过...

    StonePanda 评论0 收藏0
  • gson-plugin告别Json数据类型不一致(一)

    摘要:六原理说明侵入编译流程,在编译过程中,修改库的字节码,修改解析相关的方法,在数据类型不一致的时候,跳过当前字段的解析。 一、目录 1.gson-plugin告别Json数据类型不一致(一)2.gson-plugin基础源码分析(二)3.gson-plugin深入源码分析(三)4.gson-plugin如何在JitPack发布(四) 看完这4篇文章,对Gson解析会有更加深刻的认识,对A...

    LucasTwilight 评论0 收藏0
  • gson-plugin告别Json数据类型不一致(一)

    摘要:六原理说明侵入编译流程,在编译过程中,修改库的字节码,修改解析相关的方法,在数据类型不一致的时候,跳过当前字段的解析。 一、目录 1.gson-plugin告别Json数据类型不一致(一)2.gson-plugin基础源码分析(二)3.gson-plugin深入源码分析(三)4.gson-plugin如何在JitPack发布(四) 看完这4篇文章,对Gson解析会有更加深刻的认识,对A...

    canopus4u 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<