Serialization exploration 5 - Gson

Keywords: Back-end gson

Gson is a relatively simple library. It doesn't have so many functions. In terms of design, it doesn't want others to expand it. It just wants to make a Json serialization library quietly, which is simple and practical.

in brief

Gson provides two ways to create gson instances

  • new Gson(): quick creation, default configuration, quick use
  • new GsonBuilder().setxxxxx().create(): it is created in a complete way and supports some customized configurations

Gson highlights a simple API, especially the functions. Let's outline the functions it supports

  • field based serialization and deserialization: basic features
  • Support custom attribute name: @ SerializedName
  • Specifying generic information when deserialization is supported: TypeToken
  • It supports the exclusion of a field: the transient keyword excludes a single field, the visibility modifier excludes it, and @ Expose actively selects it
  • Support custom serialization and deserialization logic: JsonSerializer, JsonDeserializer, or their aggregate: TypeAdapter

Basic ability

You don't need to add any notes or do anything. You can use it directly

class Resource1<T> {
    var id: Int = -1
    var type: ResourceType? = null

    @Transient
    var secret: String = ""

    @SerializedName("I'm the actual data")
    var data: T? = null

    override fun toString(): String {
        return "Resource1(id=$id, type=$type, secret='$secret', data=$data)"
    }

}

fun main() {
    val gson = Gson()
    val resource = Resource1<JsonObject>().apply {
        this.id = 1
        this.type = null
        this.secret = "I'm the password"
        this.data = JsonObject().apply {
            addProperty("key", "value")
        }
    }
    val jsonString = gson.toJson(resource)
    println(jsonString)
    println(gson.fromJson<Resource1<JsonObject>>(jsonString, object : TypeToken<Resource1<JsonObject>>() {}.type))
}

main points

  • gson.toJson(xxx) for serialization and gson.fromjason (jsonstring, type information) for deserialization
  • Ignore the field and use the transient keyword
  • The custom field name is annotated with @ SerializedName, which is actually one of the only four fields that Gson can personalize
  • In the case of generic erasure, specify: object: TypeToken < resource1 < jsonobject > > () {}. Type through TypeToken during deserialization. This is similar to the TypeReference of Jackson and Fastjson

Exclude field

class Resource4 {
    @Expose
    var id: Int = -1
    var type: ResourceType? = null
}

fun main() {
    val gson = GsonBuilder().excludeFieldsWithoutExposeAnnotation().create()
    println(gson.toJson(Resource4().apply { id = 10; type = ResourceType.CHARACTER }))
}
  • GsonBuilder().excludeFieldsWithoutExposeAnnotation() sets that only @ Expose data is exposed
  • @Expose tag field

Custom instance Creator

Logic for creating objects when Gson deserializes

  • First, find out whether there is a parameterless constructor of the target class. If so, use it to create an instance
  • Secondly, find out whether there is a user-defined instance creator
  • Then, if the target class is a native type, directly find the constructor of the corresponding type and create an instance
  • If none, create an instance with sun.misc.Unsafe

It is generally not recommended to use Unsafe to create instances. Either a parameterless construction method or an instance creator are provided. Here is an example of the latter

class Resource3(var id: Int, var type: ResourceType?) {

    init {
        println("A parameter constructor was executed")
    }

    override fun toString(): String {
        return "Resource3(id=$id, type=$type)"
    }

}

class Resource3Creator : InstanceCreator<Resource3> {

    override fun createInstance(type: Type): Resource3 {
        return Resource3(-1, null)
    }

}

fun main() {
    val gson = GsonBuilder().registerTypeAdapter(Resource3::class.java, Resource3Creator()).create()
    val resource = Resource3(1, null)
    println("serialize")
    val jsonString = gson.toJson(resource)
    println(jsonString)
    println("Deserialization")
    println(gson.fromJson(jsonString, Resource3::class.java))
}

output

A parameter constructor was executed
 serialize
{"id":1}
Deserialization
 A parameter constructor was executed
Resource3(id=1, type=null)

PS: Gson ignores the inner class by default because it does not have a parameterless constructor

Custom serializer

The old rule is to customize the serialization and deserialization logic of LocalDateTime. For this reason, Gson provides three types to define, either a serializer, a deserializer, or both.

class Resource5 {
    var id: Int = -1
    var type: ResourceType = ResourceType.CHARACTER

    @JsonAdapter(Resource5TypeAdapter::class)
    var updatedTime: LocalDateTime? = null

    override fun toString(): String {
        return "Resource5(id=$id, type=$type, updatedTime=$updatedTime)"
    }

}

class Resource5TypeAdapter : TypeAdapter<LocalDateTime>() {

    override fun write(out: JsonWriter, value: LocalDateTime?) {
        println("Write method executed")
        if (value == null) out.nullValue()
        else out.value(value.toInstant(ZoneOffset.UTC).toEpochMilli())
    }

    override fun read(`in`: JsonReader): LocalDateTime {
        println("Read method executed")
        return LocalDateTime.ofInstant(Instant.ofEpochMilli(`in`.nextLong()), ZoneOffset.UTC)
    }

}

fun main() {
    val resource = Resource5().apply {
        this.id = 1
        this.updatedTime = LocalDateTime.now()
    }
    val gson = Gson()
    val jsonString = gson.toJson(resource)
    println(jsonString)
    println(gson.fromJson(jsonString, Resource5::class.java))
}
  • It can be specified locally through @ JsonAdapter
  • You can also register globally through GsonBuilder().registerTypeAdapter

polymorphic

Gson native does not support polymorphism, but it can be implemented in some other ways. The following is the officially recommended way (although it's silly)

fun main() {
    val gson = Gson()
    val list = listOf(
        1,
        "",
        mapOf(
            "key" to "value"
        )
    )
    val jsonString = gson.toJson(list)
    println(jsonString)
    gson.fromJson(jsonString, JsonArray::class.java).mapIndexed { index, jsonElement ->
        when (index) {
            0 -> gson.fromJson(jsonElement, Int::class.java)
            1 -> gson.fromJson(jsonElement, String::class.java)
            2 -> gson.fromJson(jsonElement, object : TypeToken<Map<String, String>>() {}.type)
            else -> throw Exception()
        }
    }.toList().also { println(it) }
}
  • If there are multiple types in a collection, when deserializing, get JsonArray first, and then apply specific types to specific elements
  • This is not very scientific. Another way is the RuntimeTypeAdapterFactory, which is not officially recommended, so you should use polymorphism or don't use Gson

Tree model

Gson's tree model is still simple. There are only three classes: JsonArray, JsonObject and JsonElement, but the API is not friendly and the restrictions are relatively rigid

  • To add a general property, you need to call the addProperty method, and only support String, Boolean, Number and Character
  • To add an object or array property, you have to use the add method
  • The fluent API is not supported
fun main() {
    val resource = JsonObject().apply {
        addProperty("id", 1)
        addProperty("type", ResourceType.CHARACTER.name)
        addProperty("usn", null as String?)
        add("data", JsonArray().apply {
            add(false)
            add(123)
        })
    }
    println(GsonBuilder().setPrettyPrinting().create().toJson(resource))
}

What capabilities are supported

Give an example of the capabilities of GsonBuilder. You can see that it has no capabilities.

[external chain picture transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-na27ydhi-1635758683237)( https://gdz.oss-cn-shenzhen.aliyuncs.com/local/image-20211016115638338.png )]

  • Set the exclusion policy for serialization and deserialization
  • Set various type adapters to control the behavior of type serialization and deserialization
  • Turn off serialization of inner classes
  • Html format escape
  • Serialization name control
  • output formats
  • Version control (@ Since and @ Until annotations can set the version of POJO, which is somewhat similar to the function of @ JsonView, but feels very weak)
  • Format date
  • Set field name naming policy

Basic principles

In addition, Gson looked at the five serialization libraries before and after. Except for Java, they are similar in principle and structure, but there are differences in the specific algorithms of serialization and deserialization. They are buckled in detail. In particular, Fastjson uses a lot of tricks to improve the speed.

  • For serialization, the serializer is obtained first, and then the serializer writes the actual object to the stream
  • For deserialization, first get the deserializer, then get the instance of the target class, and then use the deserializer to read the content from the stream and insert it into the target instance

As for Gson, what makes it different is

  • During serialization, the StringWriter is directly used for write operations instead of maintaining the output stream and buffer
  • The actual writing of Json format is in com.google.gson.stream.JsonWriter, which is internally set with StringWriter
    • Unlike Jackson's tree structure, write state control is maintained by stack (a one-dimensional array, the size of the array is the depth of the current hierarchy, and the value of the array is the type of the current structure). See com.google.gson.stream.JsonWriter#stack
  • For types without a custom serializer, use com.google.gson.internal.bind.ReflectiveTypeAdapterFactory.Adapter
    • For serialization, it directly uses reflection to get the fields that meet the requirements, and then writes to the writer
    • For deserialization, it creates the object in the way described in the previous "custom instance creator", and then writes to the target object through reflection

summary

Throughout, it seems that Gson has clear positioning, clear objectives, standardized documents and code, and it is easy to use. The function is simple and the principle is simple. Using StringWriter and reflection directly is a functional Json library. It is not obvious that there is any performance optimization. Therefore, it can be inferred that the performance of Gson will not be too excellent.

Posted by kenwvs on Mon, 01 Nov 2021 01:33:29 -0700