您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何解決Gson導致的問題”,在日常操作中,相信很多人在如何解決Gson導致的問題問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何解決Gson導致的問題”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
一、問題的起源
先看一個非常簡單的model類Boy:
public class Boy { public String boyName; public Girl girl; public class Girl { public String girlName; } }
項目中一般都會有非常多的model類,比如界面上的每個卡片,都是解析Server返回的數據,然后解析出一個個卡片model對吧。
對于解析Server數據,大多數情況下,Server返回的是json字符串,而我們客戶端會使用Gson進行解析。
那我們看下上例這個Boy類,通過Gson解析的代碼:
public class Test01 { public static void main(String[] args) { Gson gson = new Gson(); String boyJsonStr = "{\"boyName\":\"zhy\",\"girl\":{\"girlName\":\"lmj\"}}"; Boy boy = gson.fromJson(boyJsonStr, Boy.class); System.out.println("boy name is = " + boy.boyName + " , girl name is = " + boy.girl.girlName); } }
運行結果是?
我們來看一眼:
boy name is = zhy , girl name is = lmj
非常正常哈,符合我們的預期。
忽然有一天,有個同學給girl類中新增了一個方法getBoyName(),想獲取這個女孩心目男孩的名稱,很簡單:
public class Boy { public String boyName; public Girl girl; public class Girl { public String girlName; public String getBoyName() { return boyName; } } }
看起來,代碼也沒毛病,要是你讓我在這個基礎上新增getBoyName(),可能代碼也是這么寫的。
但是,這樣的代碼埋下了深深的坑。
什么樣的坑呢?
再回到我們的剛才測試代碼,我們現在嘗試解析完成json字符串,調用一下girl.getBoyName():
public class Test01 { public static void main(String[] args) { Gson gson = new Gson(); String boyJsonStr = "{\"boyName\":\"zhy\",\"girl\":{\"girlName\":\"lmj\"}}"; Boy boy = gson.fromJson(boyJsonStr, Boy.class); System.out.println("boy name is = " + boy.boyName + " , girl name is = " + boy.girl.girlName); // 新增 System.out.println(boy.girl.getBoyName()); } }
很簡單,加了一行打印。
這次,大家覺得運行結果是什么樣呢?
還是沒問題?當然不是,結果:
boy name is = zhy , girl name is = lmj Exception in thread "main" java.lang.NullPointerException at com.example.zhanghongyang.blog01.model.Boy$Girl.getBoyName(Boy.java:12) at com.example.zhanghongyang.blog01.Test01.main(Test01.java:15)
Boy$Girl.getBoyName報出了npe,是girl為null?明顯不是,我們上面打印了girl.name,那更不可能是boy為null了。
那就奇怪了,getBoyName里面就一行代碼:
public String getBoyName() { return boyName; // npe }
到底是誰為null呢?
二、令人不解的空指針
return boyName; 只能猜測是某對象.boyName,這個某對象是null了。
這個某對象是誰呢?
我們重新看下getBoyName()返回的是boy對象的boyName字段,這個方法更細致一些寫法應該是:
public String getBoyName() { return Boy.this.boyName; }
所以,現在問題清楚了,確實是Boy.this這個對象是null。
** 那么問題來了,為什么經過Gson序列化之后需,這個對象為null呢?**
想搞清楚這個問題,還有個前置問題:
在Girl類里面為什么我們能夠訪問外部類Boy的屬性以及方法?
三、非靜態內部類的一些秘密
探索Java代碼的秘密,最好的手段就是看字節碼了。
我們下去一看Girl的字節碼,看看getBodyName()這個“罪魁禍首”到底是怎么寫的?
javap -v Girl.class
看下getBodyName()的字節碼:
public java.lang.String getBoyName(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #1 // Field this$0:Lcom/example/zhanghongyang/blog01/model/Boy; 4: getfield #3 // Field com/example/zhanghongyang/blog01/model/Boy.boyName:Ljava/lang/String; 7: areturn
可以看到aload_0,肯定是this對象了,然后是getfield獲取this0字段,再通過this0字段,再通過this0字段,再通過this0再去getfield獲取boyName字段,也就是說:
public String getBoyName() { return boyName; }
相當于:
public String getBoyName(){ return $this0.boyName; }
那么這個$this0哪來的呢?
我們再看下Girl的字節碼的成員變量:
final com.example.zhanghongyang.blog01.model.Boy this$0; descriptor: Lcom/example/zhanghongyang/blog01/model/Boy; flags: ACC_FINAL, ACC_SYNTHETIC
其中果然有個this$0字段,這個時候你獲取困惑,我的代碼里面沒有呀?
我們稍后解釋。
再看下這個this$0在哪兒能夠進行賦值?
翻了下字節碼,發現Girl的構造方法是這么寫的:
public com.example.zhanghongyang.blog01.model.Boy$Girl(com.example.zhanghongyang.blog01.model.Boy); descriptor: (Lcom/example/zhanghongyang/blog01/model/Boy;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #1 // Field this$0:Lcom/example/zhanghongyang/blog01/model/Boy; 5: aload_0 6: invokespecial #2 // Method java/lang/Object."<init>":()V 9: return LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/example/zhanghongyang/blog01/model/Boy$Girl; 0 10 1 this$0 Lcom/example/zhanghongyang/blog01/model/Boy;
可以看到這個構造方法包含一個形參,即Boy對象,最終這個會賦值給我們的$this0。
而且我們還發下一件事,我們再整體看下Girl的字節碼:
public class com.example.zhanghongyang.blog01.model.Boy$Girl { public java.lang.String girlName; final com.example.zhanghongyang.blog01.model.Boy this$0; public com.example.zhanghongyang.blog01.model.Boy$Girl(com.example.zhanghongyang.blog01.model.Boy); public java.lang.String getBoyName(); }
其只有一個構造方法,就是我們剛才說的需要傳入Boy對象的構造方法。
這塊有個小知識,并不是所有沒寫構造方法的對象,都會有個默認的無參構造喲。
也就是說:
如果你想構造一個正常的Girl對象,理論上是必須要傳入一個Boy對象的。
所以正常的你想構建一個Girl對象,Java代碼你得這么寫:
public static void testGenerateGirl() { Boy.Girl girl = new Boy().new Girl(); }
先有body才能有girl。
這里,我們搞清楚了非靜態內部類調用外部類的秘密了,我們再來想想Java為什么要這么設計呢?
因為Java支持非靜態內部類,并且該內部類中可以訪問外部類的屬性和變量,但是在編譯后,其實內部類會變成獨立的類對象,例如下圖:讓另一個類中可以訪問另一個類里面的成員,那就必須要把被訪問對象傳進入了,想一定能傳入,那么就是唯一的構造方法最合適了。
可以看到Java編譯器為了支持一些特性,背后默默的提供支持,其實這種支持不僅于此,非常多的地方都能看到,而且一些在編譯期間新增的這些變量和方法,都會有個修飾符去修飾:ACC_SYNTHETIC。
不信,你再仔細看下$this0的聲明。
final com.example.zhanghongyang.blog01.model.Boy this$0; descriptor: Lcom/example/zhanghongyang/blog01/model/Boy; flags: ACC_FINAL, ACC_SYNTHETIC
到這里,我們已經完全了解這個過程了,肯定是Gson在反序列化字符串為對象的時候沒有傳入body對象,然后造成$this0其實一直是null,當我們調用任何外部類的成員方法、成員變量是,熬的一聲給你扔個NullPointerException。
四、Gson怎么構造的非靜態匿名內部類對象?
現在我就一個好奇點,因為我們已經看到Girl是沒有無參構造的,只有一個包含Boy參數的構造方法,那么Girl對象Gson是如何創建出來的呢?
是找到帶Body參數的構造方法,然后反射newInstance,只不過Body對象傳入的是null?
好像也能講的通,下面看代碼看看是不是這樣吧:
我就長話短說了:
Gson里面去構建對象,一把都是通過找到對象的類型,然后找對應的TypeAdapter去處理,本例我們的Girl對象,最終會走走到ReflectiveTypeAdapterFactory.create然后返回一個TypeAdapter。
我只能再搬運一次了:
# ReflectiveTypeAdapterFactory.create @Override public <T> TypeAdapter<T> create(Gson gson, final TypeToken<T> type) { Class<? super T> raw = type.getRawType(); if (!Object.class.isAssignableFrom(raw)) { return null; // it's a primitive! } ObjectConstructor<T> constructor = constructorConstructor.get(type); return new Adapter<T>(constructor, getBoundFields(gson, type, raw)); }
重點看constructor這個對象的賦值,它一眼就知道跟構造對象相關。
# ConstructorConstructor.get public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) { final Type type = typeToken.getType(); final Class<? super T> rawType = typeToken.getRawType(); // ...省略一些緩存容器相關代碼 ObjectConstructor<T> defaultConstructor = newDefaultConstructor(rawType); if (defaultConstructor != null) { return defaultConstructor; } ObjectConstructor<T> defaultImplementation = newDefaultImplementationConstructor(type, rawType); if (defaultImplementation != null) { return defaultImplementation; } // finally try unsafe return newUnsafeAllocator(type, rawType); }
可以看到該方法的返回值有3個流程:
newDefaultConstructor newDefaultImplementationConstructor newUnsafeAllocator
我們先看第一個newDefaultConstructor
private <T> ObjectConstructor<T> newDefaultConstructor(Class<? super T> rawType) { try { final Constructor<? super T> constructor = rawType.getDeclaredConstructor(); if (!constructor.isAccessible()) { constructor.setAccessible(true); } return new ObjectConstructor<T>() { @SuppressWarnings("unchecked") // T is the same raw type as is requested @Override public T construct() { Object[] args = null; return (T) constructor.newInstance(args); // 省略了一些異常處理 }; } catch (NoSuchMethodException e) { return null; } }
可以看到,很簡單,嘗試獲取了無參的構造函數,如果能夠找到,則通過newInstance反射的方式構建對象。
追隨到我們的Girl的代碼,并沒有無參構造,從而會命中NoSuchMethodException,返回null。
返回null會走newDefaultImplementationConstructor,這個方法里面都是一些集合類相關對象的邏輯,直接跳過。
那么,最后只能走:newUnsafeAllocator 方法了。
從命名上面就能看出來,這是個不安全的操作。
newUnsafeAllocator最終是怎么不安全的構建出一個對象呢?
往下看,最終執行的是:
public static UnsafeAllocator create() { // try JVM // public class Unsafe { // public Object allocateInstance(Class<?> type); // } try { Class<?> unsafeClass = Class.forName("sun.misc.Unsafe"); Field f = unsafeClass.getDeclaredField("theUnsafe"); f.setAccessible(true); final Object unsafe = f.get(null); final Method allocateInstance = unsafeClass.getMethod("allocateInstance", Class.class); return new UnsafeAllocator() { @Override @SuppressWarnings("unchecked") public <T> T newInstance(Class<T> c) throws Exception { assertInstantiable(c); return (T) allocateInstance.invoke(unsafe, c); } }; } catch (Exception ignored) { } // try dalvikvm, post-gingerbread use ObjectStreamClass // try dalvikvm, pre-gingerbread , ObjectInputStream }
嗯...我們上面猜測錯了,Gson實際上內部在沒有找到它認為合適的構造方法后,通過一種非常不安全的方式構建了一個對象。
關于更多UnSafe的知識,可以參考:
每日一問 | Java里面還能這么創建對象?
五、如何避免這個問題?
其實最好的方式,會被Gson去做反序列化的這個model對象,盡可能不要去寫非靜態內部類。
在Gson的用戶指南中,其實有寫到:
github.com/google/gson…
大概意思是如果你有要寫非靜態內部類的case,你有兩個選擇保證其正確:
內部類寫成靜態內部類;
自定義InstanceCreator
2的示例代碼在這,但是我們不建議你使用。
嗯...所以,我簡化的翻譯一下,就是:
別問,問就是加static
不要使用這種口頭的要求,怎么能讓團隊的同學都自覺遵守呢,誰不注意就會寫錯,所以一般遇到這類約定性的寫法,最好的方式就是加監控糾錯,不這么寫,編譯報錯。
六、那就來監控一下?
我在腦子里面大概想了下,有4種方法可能可行。
嗯...你也可以選擇自己想下,然后再往下看。
最簡單、最暴力,編譯的時候,掃描model所在目錄,直接讀java源文件,做正則匹配去發現非靜態內部類,然后然后隨便找個編譯時的task,綁在它前面,就能做到每次編譯時都運行了。
Gradle Transform,這個不要說了,掃描model所在包下的class類,然后看類名如果包含AB的形式,且構造方法中只有一個需要A的構造且成員變量包含B的形式,且構造方法中只有一個需要A的構造且成員變量包含B的形式,且構造方法中只有一個需要A的構造且成員變量包含this0拿下。
AST 或者lint做語法樹分析;
運行時去匹配,也是一樣的,運行時去拿到model對象的包路徑下所有的class對象,然后做規則匹配。
好了,以上四個方案是我臨時想的,理論上應該都可行,實際上不一定可行,歡迎大家嘗試,或者提出新方案。
有新的方案,求留言補充下知識面
鑒于篇幅...
不,其實我一個都沒寫過,不太想都寫一篇了,這樣博客太長了。
方案1,大家拍大腿都能寫出來,過,不過我感覺1最實在了,而且觸發速度極快,不怎么影響研發體驗;
方案2,大家查一下Transform基本寫法,利用javassist,或者ASM,估計也問題不大,過;
方案3,AST的語法我也要去查,我寫起來也費勁,過;
方案4,是我最后一個想出來的,寫一下吧。
其實方案4,如果你看到ARouter的早期版本的初始化,你就明白了。
其實就是遍歷dex中所有的類,根據包+類名規則去匹配,然后就是發射API了。
我們一起寫下。
運行時,我們要遍歷類,就是拿到dex,怎么拿到dex呢?
可以通過apk獲取,apk怎么拿呢?其實通過cotext就能拿到apk路徑。
public class PureInnerClassDetector { private static final String sPackageNeedDetect = "com.example.zhanghongyang.blog01.model"; public static void startDetect(Application context) { try { final Set<String> classNames = new HashSet<>(); ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0); File sourceApk = new File(applicationInfo.sourceDir); DexFile dexfile = new DexFile(sourceApk); Enumeration<String> dexEntries = dexfile.entries(); while (dexEntries.hasMoreElements()) { String className = dexEntries.nextElement(); Log.d("zhy-blog", "detect " + className); if (className.startsWith(sPackageNeedDetect)) { if (isPureInnerClass(className)) { classNames.add(className); } } } if (!classNames.isEmpty()) { for (String className : classNames) { // crash ? Log.e("zhy-blog", "編寫非靜態內部類被發現:" + className); } } } catch (Exception e) { e.printStackTrace(); } } private static boolean isPureInnerClass(String className) { if (!className.contains("$")) { return false; } try { Class<?> aClass = Class.forName(className); Field $this0 = aClass.getDeclaredField("this$0"); if (!$this0.isSynthetic()) { return false; } // 其他匹配條件 return true; } catch (Exception e) { e.printStackTrace(); return false; } } }
啟動app:
以上僅為demo代碼,并不嚴謹,需要自行完善。
就幾十行代碼,首先通過cotext拿ApplicationInfo,那么apk的path,然后構建DexFile對象,遍歷其中的類即可,找到類,就可以做匹配了。
到此,關于“如何解決Gson導致的問題”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。