How to restore fastjson deserialization after the type of generic type is erased?

Original: WeChat official account, welcome to share, reprint, please keep the source.

Hello everyone, I'm Hydra ~ in the previous article, we talked about the type erasure of generics in Java, but a little partner left a message in the background and asked a question, how to realize the deserialization process of entities with generics. Today we'll take a look at this question.

Bedding

We choose fastjson to test the deserialization. Before the test, we define an entity class:

@Data
public class Foo<T> {
    private String val;
    private T obj;
}

If you are familiar with the type erasure of generics, you will know that there is no generics in the class after compilation. Let's decompile the bytecode file with Jad. We can see that T without type restrictions will be directly replaced with Object type:

The following uses fastjson for deserialization without specifying the type of generic type in Foo:

public static void main(String[] args) {
    String jsonStr = "{\"obj\":{\"name\":\"Hydra\",\"age\":\"18\"},\"val\":\"str\"}";
    Foo<?> foo = JSONObject.parseObject(jsonStr, Foo.class);
    System.out.println(foo.toString());
    System.out.println(foo.getObj().getClass());
}

Looking at the execution results, it is obvious that fastjson doesn't know to deserialize the contents in obj into our custom User type, so it resolves it into an object of JSONObject type.

Foo(val=str, obj={"name":"Hydra","age":"18"})
class com.alibaba.fastjson.JSONObject

So, if you want to map the content of obj to User entity object, how should you write it? Let's demonstrate several wrong writing methods first.

Wrong writing 1

When attempting to deserialize, directly specify the generic type in Foo as User:

Foo<User> foo = JSONObject.parseObject(jsonStr, Foo.class);
System.out.println(foo.toString());
System.out.println(foo.getObj().getClass());

The result will report an error of type conversion. JSONObject cannot be converted to our customized User:

Exception in thread "main" java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to com.hydra.json.model.User
	at com.hydra.json.generic.Test1.main(Test1.java:24)

Wrong writing 2

Try casting again:

Foo<?> foo =(Foo<User>) JSONObject.parseObject(jsonStr, Foo.class);
System.out.println(foo.toString());
System.out.println(foo.getObj().getClass());

The execution results are as follows. It can be seen that although the mandatory type conversion of generic type will not report an error, it also does not take effect.

Foo(val=str, obj={"name":"Hydra","age":"18"})
class com.alibaba.fastjson.JSONObject

Well, now please forget the above two wrong methods of use. Don't write that in the code. Let's see the correct way.

Correct writing

When using fastjson, you can use TypeReference to deserialize the specified generic type:

public class TypeRefTest {
    public static void main(String[] args) {
        String jsonStr = "{\"obj\":{\"name\":\"Hydra\",\"age\":\"18\"},\"val\":\"str\"}";
        Foo foo2 = JSONObject.parseObject(jsonStr, new TypeReference<Foo<User>>(){});
        System.out.println(foo2.toString());
        System.out.println(foo2.getObj().getClass());
    }
}

Operation results:

Foo(val=str, obj=User(name=Hydra, age=18))
class com.hydra.json.model.User

The obj type in Foo is User, which meets our expectations. Let's take a look at how fastjson restores generic types after erasure with the help of TypeReference.

TypeReference

Look back at the sentence in the above code:

Foo foo2 = JSONObject.parseObject(jsonStr, new TypeReference<Foo<User>>(){});

The focus is on the second parameter in the parseObject method. Note that there is a pair of braces {} in TypeReference < foo < user > > (). In other words, an anonymous class object inheriting TypeReference is created here. A typereftest $1 can be found in the compiled project target directory Class bytecode file, because the naming rule of anonymous class is the main class name + $+ (1,2,3...).

Decompile this file to see that this subclass inherits TypeReference:

static class TypeRefTest$1 extends TypeReference
{
    TypeRefTest$1()
    {
    }
}

We know that when creating an object of a subclass, the subclass will call the parameterless construction method of the parent class by default, so take a look at the construction method of TypeReference:

protected TypeReference(){
    Type superClass = getClass().getGenericSuperclass();
    Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];

    Type cachedType = classTypeCache.get(type);
    if (cachedType == null) {
        classTypeCache.putIfAbsent(type, type);
        cachedType = classTypeCache.get(type);
    }
    this.type = cachedType;
}

In fact, the key point is the first two lines of code. First look at the first line:

Type superClass = getClass().getGenericSuperclass();

Although this is the code executed in the parent Class, the Class object obtained by getClass() must be the Class object of the child Class. Because the Class obtained by getClass() method is the Class of the currently running instance itself, and the calling position will not change, so the value obtained by getClass() must be TypeRefTest .

After obtaining the Class of the current object, execute the getGenericSuperclass() method. This method is similar to getSuperclass and will return the directly inherited parent Class. The difference is that getSuperclas does not return generic parameters, while getGenericSuperclass returns the parent Class containing generic parameters.

Look at the second line of code:

Type type = ((ParameterizedType) superClass).getActualTypeArguments()[0];

First, convert the Type obtained in the previous step to ParameterizedType parameterized Type, which is an interface of generic Type, and the instance is an object that inherits its ParameterizedTypeImpl class.

In ParameterizedType, three methods are defined. The getActualTypeArguments() method called in the above code is used to return to the generic type array. It may return multiple generics, where [0] takes out the first element in the array.

verification

Well, after understanding the function of the above code, let's verify the above process through debug, execute the above TypeRefTest code, and view the data in the breakpoint:

A problem is found here. According to our analysis above, it is reasonable that the generic type of the parent TypeReference here should be foo < user >. Why is there a list < string >?

Don't worry. Let's move on. If you add a breakpoint to the parameterless constructor of TypeReference, you will find that the constructor will be called again during code execution.

Well, the result this time is the same as we expected. Foo < user > is stored in the generic array of the parent class, that is, the parent class inherited by TypeRefTest should be typereference < foo < user > >, but the reason for erasure is not displayed in our decompiled file above.

Then there is another question. Why is this constructor called twice?

After reading the code of TypeReference, I finally found the reason in the last line of the code. It turned out that a TypeReference anonymous class object was created here first!

public final static Type LIST_STRING 
    = new TypeReference<List<String>>() {}.getType();

Therefore, the order of execution of the whole code is as follows:

  • First implement the definition of static member variables in the parent class, and declare and instantiate the list here_ String, so the TypeReference() construction method will be executed once. This process corresponds to the first figure above
  • Then, when instantiating the object of the subclass, the construction method TypeReference() of the parent class will be executed again, corresponding to the second figure above
  • Finally, the empty construction method of the subclass is executed, and nothing is done

As for the list declared here_ I don't know if other partners have left messages in the background. I don't know the meaning of this code. I don't know if it's used by Hydra in the background.

After you get the generic User in Foo, you can deserialize it according to this type. Small partners interested in the follow-up process can chew the source code by themselves, which will not be expanded here.

extend

After understanding the above process, let's deepen our understanding through an example, taking the commonly used HashMap as an example:

public static void main(String[] args) {
    HashMap<String,Integer> map=new HashMap<String,Integer>();
    System.out.println(map.getClass().getSuperclass());
    System.out.println(map.getClass().getGenericSuperclass());
    Type[] types = ((ParameterizedType) map.getClass().getGenericSuperclass())
            .getActualTypeArguments();
    for (Type t : types) {
        System.out.println(t);
    }
}

The execution results are as follows. You can see that the parent class obtained here is AbstractMap, the parent class of HashMap, and the actual generic type cannot be obtained.

class java.util.AbstractMap
java.util.AbstractMap<K, V>
K
V

Modify the above code with only a small change:

public static void main(String[] args) {
    HashMap<String,Integer> map=new HashMap<String,Integer>(){};
    System.out.println(map.getClass().getSuperclass());
    System.out.println(map.getClass().getGenericSuperclass());
    Type[] types = ((ParameterizedType) map.getClass().getGenericSuperclass())
            .getActualTypeArguments();
    for (Type t : types) {
        System.out.println(t);
    }
}

The execution results are quite different. You can see that the generic type can be obtained by adding a pair of braces {} after new HashMap < string, integer > ():

class java.util.HashMap
java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer

Because the instantiation here is an object that inherits the anonymous inner class of HashMap, the obtained parent class is HashMap, and the generic type of the parent class can be obtained.

In fact, you can also change the writing method to replace this anonymous inner class with a non anonymous inner class that displays the declaration, and then modify the above code:

public class MapTest3 {
    static class MyMap extends HashMap<String,Integer>{}

    public static void main(String[] args) {
        MyMap myMap=new MyMap();
        System.out.println(myMap.getClass().getSuperclass());
        System.out.println(myMap.getClass().getGenericSuperclass());
        Type[] types = ((ParameterizedType) myMap.getClass().getGenericSuperclass())
                .getActualTypeArguments();
        for (Type t : types) {
            System.out.println(t);
        }
    }
}

The operation result is exactly the same as the above:

class java.util.HashMap
java.util.HashMap<java.lang.String, java.lang.Integer>
class java.lang.String
class java.lang.Integer

The only difference is that the naming rules of the explicitly generated internal class are different from those of the anonymous class. The bytecode file generated here is not maptest3 $1 Class, but maptest3 $mymap Class, the class name we defined is used after the $character.

Well, that's all for the pit filling trip. I'm Hydra. See you next time.

The author introduces a public official account for code sharing, which is interesting, deep and direct, and talks with you about technology. Personal wechat DrHydra9, welcome to add friends for further communication.

Added by GetReady on Wed, 09 Mar 2022 07:43:13 +0200