FastJson反序列化和構造函數之間的一點小秘密

  • 2020 年 4 月 11 日
  • 筆記

各位看官大家下午好,FastJson想必大家都很熟悉了,很常見的Json序列化工具。今天在下要和大家分享一波FastJson反序列化和構造函數之間的一點小秘密。

 

 

 

下面先進入大家都深惡痛絕的做題環節。哈哈哈…

/**   * @創建人:Raiden   * @Descriotion:   * @Date:Created in 15:53 2020/3/21   * @Modified By:   */  public class User {      private String name;      private String id;      private String student;        public User(String name,String id){          this.name = name;          this.id = id;      }        public String getName() {          return name;      }        public void setName(String name) {          this.name = name;      }        public String getId() {          return id;      }        public void setId(String id) {          this.id = id;      }        public String getStudent() {          return student;      }        public void setStudent(String student) {          this.student = student;      }        @Override      public String toString() {          return "User{" +                  "name='" + name + ''' +                  ", id='" + id + ''' +                  ", student='" + student + ''' +                  '}';      }  }

    @Test      public void testFastJosn() throws Throwable {          String userStr = "{n" +                  "t"name":"張三",n" +                  "t"id":"20200411001",n" +                  "t"student":"高三三班"n" +                  "}";          User user = JSON.parseObject(userStr, User.class);          System.err.println(user);      }

大家看看會打印出什麼?

A:User{name='張三', id='20200411001', student='null'}  B:User{name='張三', id='20200411001', student='高三三班'}  C:User{name='null', id='20200411001', student='高三三班'}  D:User{name='null', id='null', student='高三三班'}  

沒整明白吧,腦袋又嗡嗡的吧!

下面公布答案:A!

是不是有點意外啊,下面就由在下為各位解答一下疑惑。

大家都知道FastJson反序列化的時候,普通類(不包括接口和抽象類)是通過反射獲取構造函數來生成對象的,最後通過反射調用set方法來設置屬性的。

那為什麼上面會產生這樣奇怪的結果呢。想必有些聰明的看官已經猜到了吧,對沒錯,就是構造函數的問題。通常大家在工作中寫的類都是這個樣子:

@Setter  @Getter  public class User {        private String name;      private String id;      private String student;  }

寫好類和屬性以後,通過lombok生成get、set方法。構造函數在編譯期間由編譯器自動生成的一個無參構造函數。在FastJson反序列化的時候這樣的類沒有任何問題。

會依照各位看官的意思反序列化成各位所需的對象。但是,哈哈哈…只要出現轉折詞那下面就是重點了。

但是當我們手動為類添加有參構造函數時候,在編譯器編譯時,就不會再為其添加無參構造函數,也就是說你的類有且只有你添加的這個構造函數。那這樣FastJson是如何反序列化生成實例的呢?

下面請各位看官移步到FastJson反序列化方法調用圖和源碼片段:

 

 

 我們這次要講的重點在JavaBeanInfo的build方法中。從類名中大獎可以知道,這是FastJson內部存儲反序列化對象信息的類。這其中就包含了創建該類的構造方法信息。

我們先看看他的屬性:

public class JavaBeanInfo {        public final Class<?> clazz;      public final Class<?> builderClass;      //默認的構造方法放在這      public final Constructor<?> defaultConstructor;      //手動寫的構造方法放在這      public final Constructor<?> creatorConstructor;      public final Method factoryMethod;      public final Method buildMethod;        public final int defaultConstructorParameterSize;        public final FieldInfo[] fields;      public final FieldInfo[] sortedFields;        public final int parserFeatures;        public final JSONType jsonType;        public final String typeName;      public final String typeKey;        public String[] orders;        public Type[] creatorConstructorParameterTypes;      public String[] creatorConstructorParameters;        public boolean kotlin;      public Constructor<?> kotlinDefaultConstructor;

在其中我們會發現 defaultConstructor 和 creatorConstructor 兩個屬性。從屬性名稱各位看官應該能看出來其中defaultConstructor 是默認的構造函數,那我們來看看獲取他的源碼片段:

這段代碼的含義是先遍歷所有的構造函數,看看其中是否存在無參構造函數,如果存在直接返回。

    static Constructor<?> getDefaultConstructor(Class<?> clazz, final Constructor<?>[] constructors) {          if (Modifier.isAbstract(clazz.getModifiers())) {              return null;          }            Constructor<?> defaultConstructor = null;          //這裡很好理解 先遍歷下所有的構造函數,找到其中無參構造函數          for (Constructor<?> constructor : constructors) {              if (constructor.getParameterTypes().length == 0) {                  defaultConstructor = constructor;                  break;              }          }            if (defaultConstructor == null) {              if (clazz.isMemberClass() && !Modifier.isStatic(clazz.getModifiers())) {                  Class<?>[] types;                  for (Constructor<?> constructor : constructors) {                      if ((types = constructor.getParameterTypes()).length == 1                              && types[0].equals(clazz.getDeclaringClass())) {                          defaultConstructor = constructor;                          break;                      }                  }              }          }            return defaultConstructor;      }

接下來使用無參構造函數進行反序列化,從調試狀態我們可以看到JavaBeanInfo的信息:

 

 

從調試狀態的信息可以看到默認構造函數是無參構造函數,默認構造函數的參數長度為0個。

接下了請各位看官了解一下有參構造函數的獲取方式:(下面的代碼取自JavaBeanInfo 的403行到455行)

                    for (Constructor constructor : constructors) {                          Class<?>[] parameterTypes = constructor.getParameterTypes();                            if (className.equals("org.springframework.security.web.authentication.WebAuthenticationDetails")) {                              if (parameterTypes.length == 2 && parameterTypes[0] == String.class && parameterTypes[1] == String.class) {                                  creatorConstructor = constructor;                                  creatorConstructor.setAccessible(true);                                  paramNames = ASMUtils.lookupParameterNames(constructor);                                  break;                              }                          }                            if (className.equals("org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken")) {                              if (parameterTypes.length == 3                                      && parameterTypes[0] == Object.class                                      && parameterTypes[1] == Object.class                                      && parameterTypes[2] == Collection.class) {                                  creatorConstructor = constructor;                                  creatorConstructor.setAccessible(true);                                  paramNames = new String[] {"principal", "credentials", "authorities"};                                  break;                              }                          }                            if (className.equals("org.springframework.security.core.authority.SimpleGrantedAuthority")) {                              if (parameterTypes.length == 1                                      && parameterTypes[0] == String.class) {                                  creatorConstructor = constructor;                                  paramNames = new String[] {"authority"};                                  break;                              }                          }                            boolean is_public = (constructor.getModifiers() & Modifier.PUBLIC) != 0;                          if (!is_public) {                              continue;                          }                          //前面的方法都是進行一些過濾 下面的才是獲取手動有參構造函數的關鍵。                          //首先會獲取構造函數的參數名稱列表 如果參數列表為空或者長度為0 則放棄該方法,開始下一次循環                          //這裡的獲取參數名稱使用的並不是java8中提供的獲取方法參數名稱的方式                          //而是通過流讀取class文件的的方式來獲取的                          String[] lookupParameterNames = ASMUtils.lookupParameterNames(constructor);                          if (lookupParameterNames == null || lookupParameterNames.length == 0) {                              continue;                          }                          //下面這段方法很顯然就是在比較並交換,如果該有參構造函數的參數個數大於之前的構造方法中                          //參數個數最多的構造方法,則用這個構造方法和參數名稱數組替換之前保存的構造方法和參數名稱數組                          if (creatorConstructor != null                                  && paramNames != null && lookupParameterNames.length <= paramNames.length) {                              continue;                          }                            paramNames = lookupParameterNames;                          creatorConstructor = constructor;                      }

上面的方法的作用是從所有的構造方法中獲取參數最多的一個,並將其放入JaveBeanInfo的creatorConstructor屬性中,供後面實例化對象使用。

要特別注意一下,這裡的獲取參數名稱使用的並不是java8中提供的獲取方法參數名稱的方式,而是通過流讀取class文件的的方式來獲取的。

在賦值的時候,會通過參數名稱去json串中查找對應名稱的字段來賦值,並且在通過構造函數賦值完畢之後,將不再通過set方法賦值(這裡有坑一定要記住,否則會出現賦值不上的莫名其妙的錯誤)。

如果構造函數中的入參命名和JSON串中的屬性名稱對應不上將無法賦值,這裡一定要記牢,否則會出現莫名其妙的問題。舉個例子:

    public User(String a,String i,String s){          this.name = a;          this.id = i;          this.student = s;      }

上面所示的構造函數,在Json串中就必須有對應的屬性a,i,s。否則會導致反序列化後屬性為空。

當然這裡也可以通過JSONField來從定義參數名稱。想詳細了解的同學可以去看看ASMUtils.lookupParameterNames(constructor)這個方法的源碼。因為篇幅問題在這就不在贅述。

下面我們使用如下類進行調試,看看是否如我所說的。

public class User {          private String name;      private String id;      private String student;          public User(String name,String id){          this.name = name;          this.id = id;      }        public User(String name,String id,String student){          this.name = name;          this.id = id;          this.student = student;      }          public String getName() {          return name;      }        public void setName(String name) {          this.name = name;      }        public String getId() {          return id;      }        public void setId(String id) {          this.id = id;      }        public String getStudent() {          return student;      }        public void setStudent(String student) {          this.student = student;      }        @Override      public String toString() {          return "User{" +                  "name='" + name + ''' +                  ", id='" + id + ''' +                  ", student='" + student + ''' +                  '}';      }  }

 

 

從調試截圖中可以清晰看到,在JavaBeanInfo中creatorConstructor屬性存放的是有三個參數的構造方法,而且三個參數的類型都是String。這正好印證了我們上面的結論。

從JavaBeanDeserializer類的969行到1026行源代碼片段可以看到,這裡直接通過反射調用有參構造函數生成了要反序列化的類。並且因為這裡因為JavaBeanInfo中 buildMethod 屬性為空,所以在1025行代碼處直接返回結果。

至此方法結束,不在進行set賦值。

                if (beanInfo.creatorConstructor != null) {                      boolean hasNull = false;                      if (beanInfo.kotlin) {                          for (int i = 0; i < params.length; i++) {                              if (params[i] == null && beanInfo.fields != null && i < beanInfo.fields.length) {                                  FieldInfo fieldInfo = beanInfo.fields[i];                                  if (fieldInfo.fieldClass == String.class) {                                      hasNull = true;                                  }                                  break;                              }                          }                      }                        try {                          if (hasNull && beanInfo.kotlinDefaultConstructor != null) {                              object = beanInfo.kotlinDefaultConstructor.newInstance(new Object[0]);                                for (int i = 0; i < params.length; i++) {                                  final Object param = params[i];                                  if (param != null && beanInfo.fields != null && i < beanInfo.fields.length) {                                      FieldInfo fieldInfo = beanInfo.fields[i];                                      fieldInfo.set(object, param);                                  }                              }                          } else {                              //在這通過反射直接調用有參構造函數 並且輸入參數進行初始化                              object = beanInfo.creatorConstructor.newInstance(params);                          }                      } catch (Exception e) {                          throw new JSONException("create instance error, " + paramNames + ", "                                                  + beanInfo.creatorConstructor.toGenericString(), e);                      }                        if (paramNames != null) {                          for (Map.Entry<String, Object> entry : fieldValues.entrySet()) {                              FieldDeserializer fieldDeserializer = getFieldDeserializer(entry.getKey());                              if (fieldDeserializer != null) {                                  fieldDeserializer.setValue(object, entry.getValue());                              }                          }                      }                  } else if (beanInfo.factoryMethod != null) {                      try {                          object = beanInfo.factoryMethod.invoke(null, params);                      } catch (Exception e) {                          throw new JSONException("create factory method error, " + beanInfo.factoryMethod.toString(), e);                      }                  }                    if (childContext != null) {                      childContext.object = object;                  }              }              //這裡因為JavaBeanInfo中 buildMethod 屬性為空,所以直接返回結果方法結束,不在進行set賦值              Method buildMethod = beanInfo.buildMethod;              if (buildMethod == null) {                  return (T) object;              }

到這裡有參構造函數的流程基本也就結束了。

下面是反序列化流程的邏輯圖:

 

 

 在最後特別介紹下com.alibaba.fastjson.util.FieldInfo 這個類。Fastjson就是通過這個類給無參構造函生成的實例賦值的。

public class FieldInfo implements Comparable<FieldInfo> {        public Object get(Object javaObject) throws IllegalAccessException, InvocationTargetException {          return method != null                  ? method.invoke(javaObject)                  : field.get(javaObject);      }        public void set(Object javaObject, Object value) throws IllegalAccessException, InvocationTargetException {          if (method != null) {              method.invoke(javaObject, new Object[] { value });              return;          }            field.set(javaObject, value);      }  }

從源代碼片段中可以看出,不管是賦值還是獲取值,都是先通過反射調用get,set方法來實現的,但是如果沒有get,set方法會通過反射調用field來實現。也就是說沒有get,set也是可以序列化和反序列化的。

 

 

到這裡本篇分享就要結束了,不知道各位看官大大是否滿意。滿意的話希望給個小小的贊以示鼓勵,感謝大家的收看。