A little secret between FastJson deserialization and constructor

Good afternoon, everyone. FastJson must be familiar to everyone. Very common Json serialization tools. Today I want to share with you a little secret between FastJson deserialization and the constructor.

 

 

 


Let's first enter the question-making session that everyone hates. Hahaha ...

/**
 * @创建人: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);
    }

Let's see what will be printed?

A: User {name = '张三', id = '20200411001', student = 'null'} 
B: User {name = '张三', id = '20200411001', student = '高三 三班'} 
C : User {name = 'null', id = '20200411001', student = 'High three and three classes'} 
D: User {name = 'null', id = 'null', student = 'High three and three classes'}

I do n’t get it. I ’m buzzing again!

The answer is announced below: A!

Isn't it a bit unexpected? Let me answer your doubts below.

Everyone knows that when FastJson deserializes, ordinary classes (excluding interfaces and abstract classes) generate objects through reflection to obtain constructors, and finally set the properties by calling the set method through reflection.

So why does this produce such strange results? It must have been guessed by some clever observers, yes, it is the constructor. Usually the class that everyone writes at work looks like this:

@Setter
@Getter
public class User {

    private String name;
    private String id;
    private String student;
}

After writing the classes and properties, generate get and set methods through lombok. Constructor A parameterless constructor automatically generated by the compiler during compilation. There is no problem with this class when FastJson deserializes.

It will be deserialized into the objects you need according to the opinions of the officials. However, hahaha ... as long as there is a turning point then the following is the point.

But when we manually add a parameterized constructor to the class, when the compiler compiles, we will not add a parameterless constructor to it, that is to say, your class has only the constructor you added. So how does FastJson deserialize to generate instances?

Below, please move to the FastJson deserialization method call graph and source code fragment:

 

 

 Our focus this time is on the build method of JavaBeanInfo. From the grand prize in the class name, we can know that this is the class where FastJson internally stores the deserialized object information. This contains information about the method of creating the class.

Let's first look at his attributes:

public  class JavaBeanInfo { 

    public  final Class <?> clazz;
     public  final Class <?> builderClass;
     // The default constructor is placed here 
    public  final Constructor <?> defaultConstructor;
     // The manually written constructor is placed here 
    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;

In it we will find two properties, defaultConstructor and creatorConstructor. You should be able to see from the property name that the defaultConstructor is the default constructor, so let's take a look at the source code fragment:

The meaning of this code is to traverse all the constructors first to see if there are no parameter constructors, and return directly if they exist.

    static Constructor <?> getDefaultConstructor (Class <?> clazz, final Constructor <?> [] constructors) {
         if (Modifier.isAbstract (clazz.getModifiers ())) {
             return  null ; 
        } 

        Constructor <?> defaultConstructor = null ;
         / / Here is a good understanding of traversing all the constructors first, and find the parameterless constructor 
        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;
    }

Next, use the parameterless constructor to deserialize. From the debugging state, we can see the information of JavaBeanInfo:

 

 

From the information of the debugging state, we can see that the default constructor is a parameterless constructor, and the parameter length of the default constructor is 0.

Next, please see the officer to know how to get the parameterized constructor: (The following code is taken from lines 403 to 455 of JavaBeanInfo)

                    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 ; 
                        } 
                        // The previous The method is to perform some filtering. The following is the key to obtaining the manual parameterized constructor.
                        // First, the parameter name list of the constructor will be obtained. If the parameter list is empty or the length is 0, the method will be abandoned and the next cycle will be started.
                         //The parameter name obtained here is not the method of obtaining the method parameter name provided in java8
                         // but the 
                        String [] lookupParameterNames = ASMUtils.lookupParameterNames (constructor); if (lookupParameterNames )
                         obtained by streaming the class file == null || lookupParameterNames.length == 0 ) {
                             continue ; 
                        } 
                        // The following method is obviously comparing and swapping, if the number of parameters of the parameterized constructor is greater than the previous constructor
                         // parameters The most numbered constructor, replace the previously saved constructor and parameter name array with this constructor and parameter name array 
                        if (creatorConstructor! = Null 
                                && paramNames! = Null && lookupParameterNames.length <= paramNames.length) {
                            continue;
                        }

                        paramNames = lookupParameterNames;
                        creatorConstructor = constructor;
                    }

The function of the above method is to get the one with the most parameters from all the construction methods, and put it in the creatorConstructor property of JaveBeanInfo for later instantiated objects.

Pay particular attention to the fact that the parameter name obtained here is not the method of obtaining the method parameter name provided in java8, but is obtained by streaming the class file.

During the assignment, the parameter name will be used to find the field of the corresponding name in the json string to assign the value, and after the assignment through the constructor is completed, it will no longer be assigned through the set method (here there are pits must be remembered, otherwise assignment Inexplicable mistakes).

If the input parameter name in the constructor does not correspond to the attribute name in the JSON string, the value cannot be assigned. Here, we must remember it, otherwise there will be some inexplicable problems. for example:

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

The constructor shown above must have the corresponding attributes a, i, s in the Json string. Otherwise, the attribute will be empty after deserialization.

Of course, the parameter name can also be defined through JSONField. Students who want to know more can go to the source code of ASMUtils.lookupParameterNames (constructor). I will not repeat them here because of space issues.

Below we use the following classes for debugging to see if it is as I said.

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 + '\'' +
                '}';
    }
}

 

 

It can be clearly seen from the debugging screenshots that the creatorConstructor property in JavaBeanInfo stores the construction method with three parameters, and the types of the three parameters are all String. This just confirms our conclusion above.

From the source code snippets of lines 969 to 1026 of the JavaBeanDeserializer class, you can see that the parameterized constructor is directly invoked through reflection to generate the class to be deserialized. And because the buildMethod attribute in JavaBeanInfo is empty here, the result is returned directly at 1025 lines of code.

At this point, the method does not perform set assignment.

                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 {
                             // Call the parameterized constructor directly through reflection and initialize the input parameters 
                            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; 
                } 
            } 
            // Here, because the buildMethod attribute in JavaBeanInfo is empty, the result method is returned directly, and the set is not assigned. 
            Method buildMethod = beanInfo.buildMethod;
             if (buildMethod == null ) {
                 return (T) object; 
            }

The flow of the parameterized constructor here is basically over.

The following is a logic diagram of the deserialization process:

 

 

 In the final introduction, com.alibaba.fastjson.util.FieldInfo is a class. Fastjson uses this class to assign values ​​to instances generated by parameterless constructors.

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

As can be seen from the source code snippets, whether assigning or obtaining values ​​is first implemented by reflection by calling the get and set methods, but if there is no get, the set method will be implemented by reflecting the field. In other words, without get, set can also be serialized and deserialized.

 

 

At this point, the sharing of this article is coming to an end. I don't know if you are satisfied with the official. If you are satisfied, I hope to give a little praise as encouragement, thank you for watching.

 

Guess you like

Origin www.cnblogs.com/Raiden-xin/p/12681577.html