目录
吐槽
现如今搜索引擎实在不够智能,想找到一两篇高质量的文章好难,对于我这种新手,知识获取成本极高。关键字搜索到的文章同质化太严重,都是复制粘贴,代码还没有格式,阅读起来头皮发麻。感觉身处海量的信息之中,却又无法获取到信息。
前言
处理 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 的方法,方便使用。举例说明泛型类嵌套时,封装方法的使用细节。