各位看官你們下午好,FastJson想必你們都很熟悉了,很常見的Json序列化工具。今天在下要和你們分享一波FastJson反序列化和構造函數之間的一點小祕密。java
下面先進入你們都深惡痛絕的作題環節。哈哈哈...web
/** * @建立人: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); }
你們看看會打印出什麼?spring
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='高三三班'}
沒整明白吧,腦殼又嗡嗡的吧!json
下面公佈答案:A!數組
是否是有點意外啊,下面就由在下爲各位解答一下疑惑。ide
你們都知道FastJson反序列化的時候,普通類(不包括接口和抽象類)是經過反射獲取構造函數來生成對象的,最後經過反射調用set方法來設置屬性的。函數
那爲何上面會產生這樣奇怪的結果呢。想必有些聰明的看官已經猜到了吧,對沒錯,就是構造函數的問題。一般你們在工做中寫的類都是這個樣子:工具
@Setter @Getter public class User { private String name; private String id; private String student; }
寫好類和屬性之後,經過lombok生成get、set方法。構造函數在編譯期間由編譯器自動生成的一個無參構造函數。在FastJson反序列化的時候這樣的類沒有任何問題。ui
會依照各位看官的意思反序列化成各位所需的對象。可是,哈哈哈...只要出現轉折詞那下面就是重點了。this
可是當咱們手動爲類添加有參構造函數時候,在編譯器編譯時,就不會再爲其添加無參構造函數,也就是說你的類有且只有你添加的這個構造函數。那這樣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也是能夠序列化和反序列化的。
到這裏本篇分享就要結束了,不知道各位看官大大是否滿意。滿意的話但願給個小小的贊以示鼓勵,感謝你們的收看。