一聪富、項目地址
項目地址: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,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.其中e表示捕獲的Exception
八鞋屈、目錄
1.gson-plugin告別Json數據類型不一致(一)
2.gson-plugin基礎源碼分析(二)
3.gson-plugin深入源碼分析(三)
4.gson-plugin如何在JitPack發(fā)布(四)