Jackson 泛型探讨

  • Post author:
  • Post category:其他




吐槽

现如今搜索引擎实在不够智能,想找到一两篇高质量的文章好难,对于我这种新手,知识获取成本极高。关键字搜索到的文章同质化太严重,都是复制粘贴,代码还没有格式,阅读起来头皮发麻。感觉身处海量的信息之中,却又无法获取到信息。



前言

处理 JSON 格式数据,Jackson 是称手的工具。Spring Boot 自带的 JSON 解析工具也是 Jackson。感觉上,Jackson 相对于 fastjson 和 Gson 在函数名称和方法使用上有些对新手不友好。使用一段时间后还是比较顺手的,也逐渐将项目中的 JSON 解析都换上了 Jackson,保证前后端的一致性。

JSON 解析相对困难的地方在于处理泛型。本篇文章正欲将这一问题解释清楚,希望对你有所帮助。

本文示例代码使用 Kotlin 书写



基础知识

通常使用

JSON

数据格式的目的很简单:解决 Java 对象的序列化问题。无论是持久化保存数据,还是远程传输数据,都需要将 Java 对象转换为二进制数据,然后再转换回来。JSON是这一过程中需要的可阅读可解析的中间件,是一种标准。


泛型

是一种语法糖,用来定义有相同模板的 Java 对象。可以显著减少重复定义的工作量。

Java 泛型中一个重要的概念是

泛型擦除

,编辑器编译带类型说明的泛型时会去掉类型信息,因为对运行时而言这些类型信息没有意义,都是占用一块内存而已。

那么泛型擦除是如何影响 JSON 解析的呢?

泛型擦除对 JSON 的序列化没有影响,因为序列化时是将 Object 转换为 JSON,这时 Object 属性已经指定了泛型类型。

JSON 的反序列化过程,是将 JSON 转换为 Object,这时的输入条件是 JSON 和 Java类,输出结果是 Java类的实例化 Object。而 Java 类是使用泛型定义的,没有指定泛型参数类型,所以需要另外处理。



Jackson 反序列化

Jackson 常用的反序列化函数是这样的:

// ObjectMapper.readValue
public <T> T readValue(String content, Class<T> valueType)

ObjectMappter 的这个函数用于将 JSON 转换为 Object,valueType 参数用于指定 Object 对应类的类型。该方法只能用于反序列化非泛型类型,比如指定valueType 为 String::class.java (Kotlin)。

拿常见泛型 List<T> 举例。如果仍使用以上函数反序列化 List:

objectMapper.readValue(content, List::class.java) // Kotlin

因为 List.class 不包含泛型参数类型信息,Jackson 无法得知将要反序列化的目标类型。但是 Jackson 也不是什么都不做,它会猜测可能需要的类型。比如基础类型或者 LinkedHashMap。这时就无法达到我们预期的结果,类型转换时会出错。



异常示例

类:

data class User(var id: Long = 0, var name: String = "") {
	constructor() : this(0)
}

JSON:

[{"id":1,"name":"LiNing"},{"id":2,"name":"SunYang"}]

反序列化:

// Kotlin
val users = mapper.readValue(json, List::class.java) 
println(users)
println(users?.get(0)?.javaClass)

结果:

[{id=1, name=LiNing}, {id=2, name=SunYang}]
class java.util.LinkedHashMap

可以看到,得到的 users 类型为:List<LinkedHashMap>。这是当然的,因为反序列化代码中根本没有出现 User,Jackson 是无从知晓的。如果这时候,我们依然按照原本的设计进行类型强转,则会出现错误。

反序列化:

val users = mapper.readValue(json, List::class.java) as List<User>

结果:

java.lang.ClassCastException: class java.util.LinkedHashMap cannot be cast to class User



JavaType

由于泛型擦除的限制,泛型类不能通过Class<T>来获取类型全部信息,比如:List<User>::class.java 语法不成立。Jackson 为我们提供了两种方法将原始类型 List 和 参数类型 User 包装起来使用。一种是使用 TypeReference,另一种是 JavaType。两种方法效果一样,这里介绍代码稍微好看点的 JavaType。

使用到的核心构造 JavaType 的方法:

// TypeFactory.constructParametricType
public JavaType constructParametricType(Class<?> parametrized, Class<?>... parameterClasses)

parametrized 用于指定原始类型,比如 List::class.java,parameterClasses用于指定泛型参数类型,比如:User::class.java。

JavaType 使用示例:

val type = mapper.typeFactory.constructParametricType(List::class.java, User::class.java)
val users = mapper.readValue(json, type)
println(users?.get(0)?.javaClass)

如此便可以得到我们想要的 List<User>类型。



泛型嵌套

实际开发过程中,我们可能遇到更为复杂的 JSON 反序列化需求。比如 User 也是泛型 User<T>,需要解析 List<User>。解决这种问题的方法也很简单:由内到外使用 JavaType。举例如下:

User类:

data class User<T>(var id: Long = 0, var name: String = "", var info: T? = null) {
    constructor() : this(0)
}

JSON:

[{"id":1,"name":"LiNing","info":23},{"id":2,"name":"SunYang","info":27}]

反序列化:

val userType = mapper.typeFactory.constructParametricType(User::class.java, Int::class.java)
val type = mapper.typeFactory.constructParametricType(List::class.java, userType)
val users = mapper.readValue(json, type)

这里用到了 constructParametricType 参数为 JavaType 的重载函数。



泛型变量

刚开始使用 Jackson 时一直但心泛型的解析,以为 Java 类只能定义基础类型,才能正常解析。其实,当变量类型是指定参数类型的泛型时,和基础类型没有区别。

比如上例 User 类修改为:

data class User(var id: Long = 0, var name: String = "", var address: List<String> = listOf()) {
    constructor() : this(0)
}

这种情况不需要特殊处理,因为 address 在定义时已经指定了 List 的泛型参数类型为 String,反序列化时,Jackson 会使用反射方法获取到 String 类型。

也就是说,只有在反序列化泛型类时需要考虑使用 JavaType,以上 User 类并不是泛型类。

反过来,在反序列化泛型类时,即时泛型参数类型是基础类,也应当使用 JavaType 指定基础类型。比如:List<Long> 的反序列化,应当使用:

val type = mapper.typeFactory.constructParametricType(List::class.java, Long::class.java)
val obj = mapper.readValue(json, type)

而不是:

val obj = mapper.readValue(json, List::class.java)

不然也可能出来类型转换错误。



方法封装

有些常用配置可以根据项目需要设置好并封装起来方便使用,常用的方法也可以封装起来起个好听的名字。这里使用 Kotlin 的扩展方法书写,Java 方法请自行转换。



定义 ObjectMapper

ObjectMapper 类用来配置 Jackson 对 JSON 特殊格式的处理方法。比如,如何处理单引号,如何处理属性个数不同等情况。

示例:

/**
 * 定义 mapper
 */
private val mapper = ObjectMapper().apply {
    // 忽略对象空,遇到未知对象无法序列化时不报错
    // disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
    // 允许key没有引号
    configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
    // 允许key是单引号
    configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true)
    // 忽略未知属性,属性个数不对等或都不一样时不报错
    disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
    // 包含属性风格
    // setSerializationInclusion(JsonInclude.Include.NON_NULL)
}



Object -> JSON

该方法比较简单,直接使用 mapper.writeValueAsString,没有泛型问题。

fun Any?.toJsonString(): String {
    val empty = "{}"
    return this?.let {
        try {
            mapper.writeValueAsString(it)
        } catch (e: Exception) {
            e.printStackTrace()
            empty
        }
    } ?: empty
}



构造 JavaType

根据泛型原始类型和参数类型构造 JavaType。

fun parametricType(rawClass: Class<*>, vararg paramClasses: Class<*>): JavaType {
    return mapper.typeFactory.constructParametricType(rawClass, *paramClasses)
}

fun parametricType(rawClass: Class<*>, vararg paramTypes: JavaType): JavaType {
    return mapper.typeFactory.constructParametricType(rawClass, *paramTypes)
}

这里有两个方法,当参数类型不是泛型类时,使用第一个。当参数类型是泛型类时,使用第二个,先生成参数类型的 JavaType,再生成整个泛型的 JavaType。如果是泛型多层嵌套,需要循环调用第二个方法。



JSON -> Object

基础方法:

fun <T> String.parseObject(clazz: Class<T>): T? {
    return try {
        mapper.readValue(this, clazz)
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

该方法仅用于对非泛型类的转换。

处理泛型:

fun <T> String.parseObject(type: JavaType): T? {
    return try {
        mapper.readValue(this, type)
    } catch (e: Exception) {
        e.printStackTrace()
        null
    }
}

该方法用于处理泛型类,参数是上面构造的 JavaType。



常用泛型

有了以上基础,我们可以将常用泛型也封装起来,方便使用。

// 常用泛型 JavaType
fun listType(paramClass: Class<*>): JavaType {
    return parametricType(List::class.java, paramClass)
}

fun listType(paramType: JavaType): JavaType {
    return parametricType(List::class.java, paramType)
}

fun arrayType(paramClass: Class<*>): JavaType {
    return parametricType(Array::class.java, paramClass)
}

fun arrayType(paramType: JavaType): JavaType {
    return parametricType(Array::class.java, paramType)
}

fun mapType(keyClass: Class<*>, valueClass: Class<*>): JavaType {
    return parametricType(Map::class.java, keyClass, valueClass)
}

fun mapType(keyType: JavaType, valueType: JavaType): JavaType {
    return parametricType(Map::class.java, keyType, valueType)
}

// 常用泛型 JSON -> Object
fun <T> String.parseList(clazz: Class<T>): List<T>? {
    return parseObject(listType(clazz))
}

fun <T> String.parseList(type: JavaType): List<T>? {
    return parseObject(listType(type))
}

fun <T> String.parseArray(clazz: Class<T>): Array<T>? {
    return parseObject(arrayType(clazz))
}

fun <T> String.parseArray(type: JavaType): Array<T>? {
    return parseObject(arrayType(type))
}

fun <K, V> String.parseMap(keyClass: Class<K>, valueClass: Class<V>): Map<K, V>? {
    return parseObject(mapType(keyClass, valueClass))
}

fun <K, V> String.parseMap(keyType: JavaType, valueType: JavaType): Map<K, V>? {
    return parseObject(mapType(keyType, valueType))
}

以上方法均分别处理非泛型类与泛型类。方便在泛型嵌套时使用。



举例

后端返回的数据结构如下:

data class Result(
        /**
         * 错误码
         */
        var code: Int = 0,
        /**
         * 信息
         */
        var message: String = "",
        /**
         * 数据
         */
        var data: Any? = null
)

不同的请求返回不同数据类型的 data,前端接收的时候应当使用泛型类,比如:

data class NetResult<T>(
    var code: Int = 0,
    var message: String = "",
    var data: T? = null
){
	 constructor() : this(0)
}

借用上面的 User 类:

data class User<T>(var id: Long = 0, var name: String = "", var info: T? = null) {
    constructor() : this(0)
}

假如后端返回的 data 是 List<User<Int>>。JSON 内容为:

{
  "code": 0,
  "message": "success",
  "data": [
    {
      "id": 1,
      "name": "LiNing",
      "info": 23
    },
    {
      "id": 2,
      "name": "SunYang",
      "info": 27
    }
  ]
}

前端如何将 JSON 解析为 NetResult<List<User<Int>>> 对象呢?示例如下:

val userType = parametricType(User::class.java, Int::class.java) // User<Int>
val listType = listType(userType)  // List<User>
val resultType = parametricType(NetResult::class.java, listType) // NetResult<List>
val result = json.parseObject<NetResult<User<Int>>>(resultType)
println(result)

可见解析方法是从内到外一层层构造 JavaType,然后调用 String.parseObject 转换为 Object。

运行结果:

NetResult(code=0, message=success, data=[User(id=1, name=LiNing, info=23), User(id=2, name=SunYang, info=27)])



总结

本文简述了 JSON、泛型、泛型擦除、Jackson使用的基础知识。介绍了使用 JavaType 进行反序列化泛型类的方法。使用 Kotlin 封装了部分 Jackson 的方法,方便使用。举例说明泛型类嵌套时,封装方法的使用细节。



版权声明:本文为yaopochundexia原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。