溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何解決Gson導(dǎo)致的問題

發(fā)布時間:2021-10-19 15:13:24 來源:億速云 閱讀:155 作者:iii 欄目:移動開發(fā)

這篇文章主要介紹“如何解決Gson導(dǎo)致的問題”,在日常操作中,相信很多人在如何解決Gson導(dǎo)致的問題問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何解決Gson導(dǎo)致的問題”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

一、問題的起源

先看一個非常簡單的model類Boy:

public class Boy {      public String boyName;     public Girl girl;      public class Girl {         public String girlName;     } }

項目中一般都會有非常多的model類,比如界面上的每個卡片,都是解析Server返回的數(shù)據(jù),然后解析出一個個卡片model對吧。

對于解析Server數(shù)據(jù),大多數(shù)情況下,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);     }  }

運行結(jié)果是?

我們來看一眼:

boy name is = zhy , girl name is = lmj

非常正常哈,符合我們的預(yù)期。

忽然有一天,有個同學(xué)給girl類中新增了一個方法getBoyName(),想獲取這個女孩心目男孩的名稱,很簡單:

public class Boy {      public String boyName;     public Girl girl;      public class Girl {         public String girlName;          public String getBoyName() {             return boyName;         }     } }

看起來,代碼也沒毛病,要是你讓我在這個基礎(chǔ)上新增getBoyName(),可能代碼也是這么寫的。

但是,這樣的代碼埋下了深深的坑。

什么樣的坑呢?

再回到我們的剛才測試代碼,我們現(xiàn)在嘗試解析完成json字符串,調(diào)用一下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());     }  }

很簡單,加了一行打印。

這次,大家覺得運行結(jié)果是什么樣呢?

還是沒問題?當(dāng)然不是,結(jié)果:

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字段,這個方法更細致一些寫法應(yīng)該是:

public String getBoyName() {  return Boy.this.boyName;  }

所以,現(xiàn)在問題清楚了,確實是Boy.this這個對象是null。

** 那么問題來了,為什么經(jīng)過Gson序列化之后需,這個對象為null呢?**

想搞清楚這個問題,還有個前置問題:

  • 在Girl類里面為什么我們能夠訪問外部類Boy的屬性以及方法?

三、非靜態(tài)內(nèi)部類的一些秘密

探索Java代碼的秘密,最好的手段就是看字節(jié)碼了。

我們下去一看Girl的字節(jié)碼,看看getBodyName()這個“罪魁禍首”到底是怎么寫的?

javap -v Girl.class

看下getBodyName()的字節(jié)碼:

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; }

相當(dāng)于:

public String getBoyName(){     return $this0.boyName; }

那么這個$this0哪來的呢?

我們再看下Girl的字節(jié)碼的成員變量:

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在哪兒能夠進行賦值?

翻了下字節(jié)碼,發(fā)現(xiàn)Girl的構(gòu)造方法是這么寫的:

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;

可以看到這個構(gòu)造方法包含一個形參,即Boy對象,最終這個會賦值給我們的$this0。

而且我們還發(fā)下一件事,我們再整體看下Girl的字節(jié)碼:

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(); }

其只有一個構(gòu)造方法,就是我們剛才說的需要傳入Boy對象的構(gòu)造方法。

這塊有個小知識,并不是所有沒寫構(gòu)造方法的對象,都會有個默認的無參構(gòu)造喲。

也就是說:

如果你想構(gòu)造一個正常的Girl對象,理論上是必須要傳入一個Boy對象的。

所以正常的你想構(gòu)建一個Girl對象,Java代碼你得這么寫:

public static void testGenerateGirl() {     Boy.Girl girl = new Boy().new Girl(); }

先有body才能有g(shù)irl。

這里,我們搞清楚了非靜態(tài)內(nèi)部類調(diào)用外部類的秘密了,我們再來想想Java為什么要這么設(shè)計呢?

因為Java支持非靜態(tài)內(nèi)部類,并且該內(nèi)部類中可以訪問外部類的屬性和變量,但是在編譯后,其實內(nèi)部類會變成獨立的類對象,例如下圖:讓另一個類中可以訪問另一個類里面的成員,那就必須要把被訪問對象傳進入了,想一定能傳入,那么就是唯一的構(gòu)造方法最合適了。

如何解決Gson導(dǎo)致的問題

可以看到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

到這里,我們已經(jīng)完全了解這個過程了,肯定是Gson在反序列化字符串為對象的時候沒有傳入body對象,然后造成$this0其實一直是null,當(dāng)我們調(diào)用任何外部類的成員方法、成員變量是,熬的一聲給你扔個NullPointerException。

四、Gson怎么構(gòu)造的非靜態(tài)匿名內(nèi)部類對象?

現(xiàn)在我就一個好奇點,因為我們已經(jīng)看到Girl是沒有無參構(gòu)造的,只有一個包含Boy參數(shù)的構(gòu)造方法,那么Girl對象Gson是如何創(chuàng)建出來的呢?

是找到帶Body參數(shù)的構(gòu)造方法,然后反射newInstance,只不過Body對象傳入的是null?

好像也能講的通,下面看代碼看看是不是這樣吧:

我就長話短說了:

Gson里面去構(gòu)建對象,一把都是通過找到對象的類型,然后找對應(yīng)的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這個對象的賦值,它一眼就知道跟構(gòu)造對象相關(guān)。

# ConstructorConstructor.get public <T> ObjectConstructor<T> get(TypeToken<T> typeToken) {     final Type type = typeToken.getType();     final Class<? super T> rawType = typeToken.getRawType();          // ...省略一些緩存容器相關(guān)代碼      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;     }   }

可以看到,很簡單,嘗試獲取了無參的構(gòu)造函數(shù),如果能夠找到,則通過newInstance反射的方式構(gòu)建對象。

追隨到我們的Girl的代碼,并沒有無參構(gòu)造,從而會命中NoSuchMethodException,返回null。

返回null會走newDefaultImplementationConstructor,這個方法里面都是一些集合類相關(guān)對象的邏輯,直接跳過。

那么,最后只能走:newUnsafeAllocator 方法了。

從命名上面就能看出來,這是個不安全的操作。

newUnsafeAllocator最終是怎么不安全的構(gòu)建出一個對象呢?

往下看,最終執(zhí)行的是:

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實際上內(nèi)部在沒有找到它認為合適的構(gòu)造方法后,通過一種非常不安全的方式構(gòu)建了一個對象。

關(guān)于更多UnSafe的知識,可以參考:

每日一問 | Java里面還能這么創(chuàng)建對象?

五、如何避免這個問題?

其實最好的方式,會被Gson去做反序列化的這個model對象,盡可能不要去寫非靜態(tài)內(nèi)部類。

在Gson的用戶指南中,其實有寫到:

github.com/google/gson&hellip;

如何解決Gson導(dǎo)致的問題

大概意思是如果你有要寫非靜態(tài)內(nèi)部類的case,你有兩個選擇保證其正確:

  • 內(nèi)部類寫成靜態(tài)內(nèi)部類;

  • 自定義InstanceCreator

2的示例代碼在這,但是我們不建議你使用。

嗯...所以,我簡化的翻譯一下,就是:

別問,問就是加static

不要使用這種口頭的要求,怎么能讓團隊的同學(xué)都自覺遵守呢,誰不注意就會寫錯,所以一般遇到這類約定性的寫法,最好的方式就是加監(jiān)控糾錯,不這么寫,編譯報錯。

六、那就來監(jiān)控一下?

我在腦子里面大概想了下,有4種方法可能可行。

嗯...你也可以選擇自己想下,然后再往下看。

  1. 最簡單、最暴力,編譯的時候,掃描model所在目錄,直接讀java源文件,做正則匹配去發(fā)現(xiàn)非靜態(tài)內(nèi)部類,然后然后隨便找個編譯時的task,綁在它前面,就能做到每次編譯時都運行了。

  2. Gradle  Transform,這個不要說了,掃描model所在包下的class類,然后看類名如果包含AB的形式,且構(gòu)造方法中只有一個需要A的構(gòu)造且成員變量包含B的形式,且構(gòu)造方法中只有一個需要A的構(gòu)造且成員變量包含B的形式,且構(gòu)造方法中只有一個需要A的構(gòu)造且成員變量包含this0拿下。

  3. AST 或者lint做語法樹分析;

  4. 運行時去匹配,也是一樣的,運行時去拿到model對象的包路徑下所有的class對象,然后做規(guī)則匹配。

好了,以上四個方案是我臨時想的,理論上應(yīng)該都可行,實際上不一定可行,歡迎大家嘗試,或者提出新方案。

有新的方案,求留言補充下知識面

鑒于篇幅...

不,其實我一個都沒寫過,不太想都寫一篇了,這樣博客太長了。

  • 方案1,大家拍大腿都能寫出來,過,不過我感覺1最實在了,而且觸發(fā)速度極快,不怎么影響研發(fā)體驗;

  • 方案2,大家查一下Transform基本寫法,利用javassist,或者ASM,估計也問題不大,過;

  • 方案3,AST的語法我也要去查,我寫起來也費勁,過;

  • 方案4,是我最后一個想出來的,寫一下吧。

其實方案4,如果你看到ARouter的早期版本的初始化,你就明白了。

其實就是遍歷dex中所有的類,根據(jù)包+類名規(guī)則去匹配,然后就是發(fā)射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", "編寫非靜態(tài)內(nèi)部類被發(fā)現(xiàn):" + 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:

如何解決Gson導(dǎo)致的問題

以上僅為demo代碼,并不嚴謹,需要自行完善。

就幾十行代碼,首先通過cotext拿ApplicationInfo,那么apk的path,然后構(gòu)建DexFile對象,遍歷其中的類即可,找到類,就可以做匹配了。

到此,關(guān)于“如何解決Gson導(dǎo)致的問題”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI