中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

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

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

如何解決Gson導致的問題

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

這篇文章主要介紹“如何解決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支持非靜態內部類,并且該內部類中可以訪問外部類的屬性和變量,但是在編譯后,其實內部類會變成獨立的類對象,例如下圖:讓另一個類中可以訪問另一個類里面的成員,那就必須要把被訪問對象傳進入了,想一定能傳入,那么就是唯一的構造方法最合適了。

如何解決Gson導致的問題

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

如何解決Gson導致的問題

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

  • 內部類寫成靜態內部類;

  • 自定義InstanceCreator

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

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

別問,問就是加static

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

六、那就來監控一下?

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

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

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

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

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

  4. 運行時去匹配,也是一樣的,運行時去拿到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:

如何解決Gson導致的問題

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

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

到此,關于“如何解決Gson導致的問題”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

大丰市| 平罗县| 浮梁县| 吴桥县| 桃园县| 阜新市| 鹤峰县| 武穴市| 绵竹市| 宜宾市| 桃园市| 玉门市| 汝城县| 洪泽县| 遵义市| 炉霍县| 吉林省| 佛坪县| 武夷山市| 临西县| 普兰县| 资溪县| 礼泉县| 浙江省| 剑阁县| 宜兰县| 交城县| 安福县| 三门峡市| 平塘县| 永靖县| 通榆县| 岳阳县| 富民县| 秭归县| 绵竹市| 封开县| 阜宁县| 宽甸| 改则县| 华安县|