1. 背景
所在公司项目中,涉及金额数字,所以在反序列化数字金额时,选择了BigDecimal作为金额相关JSON字段的对象类型。在项目中使用Kotlin语言,搭配Retrofit框架和Moshi作为序列化工具,但Moshi在使用当中发现,是不支持string、number类型的JSON字段转换成BigDecimal类型的参数的。于是需要自定义一个JsonAdapter.Factory来解决这一问题。
2. 先来介绍为什么选BigDecimal
1. 精确的十进制表示
BigDecimal内部,通过整数+小数位存储数据,无精度损失,例如 321.45 存储为 32145 X 10^-2。
2. 完全控制舍入行为
提供多种舍入模式,如RoundingModel.HALF_UP 四舍五入,符合财务规范。
3. 支持任意精度
不受固定自己安长度限制,可处理超大金额或超高精度
4. 避免隐式转换风险
强制使用显示计算方式 如,add()、mulitply() ,避免意外错误。
3. 适配器工厂 代码如下
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import java.lang.reflect.Type
import java.math.BigDecimal
class BigDecimalAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: Set<Annotation?>, moshi: Moshi): JsonAdapter<*>? {
if (type != BigDecimal::class.java) return null
return object : JsonAdapter<BigDecimal>() {
override fun fromJson(reader: JsonReader): BigDecimal? {
return when (reader.peek()) {
JsonReader.Token.NUMBER -> {
val number = reader.nextDouble()
BigDecimal.valueOf(number)
}
JsonReader.Token.STRING -> {
val string = reader.nextString()
BigDecimal(string)
}
JsonReader.Token.NULL -> {
null
}
else -> {
val intValue = reader.nextInt()
BigDecimal(intValue)
}
}
}
override fun toJson(wirtter: JsonWriter, value: BigDecimal?) {
if (value != null) {
wirtter.value(value)
} else {
wirtter.nullValue()
}
}
}.nullSafe()
}
}
4. 适配器添加到Moshi中
val moshi = Moshi.Builder()
.add(BigDecimalAdapterFactory())
.add(KotlinJsonAdapterFactory())
.build()
如果使用了Retrofit,只需要把添加了BigDecimalAdpterFactory的moshi传入retrofit即可:
Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(MoshiConverterFactory.create(moshi))
.client(okHttpClient)
.build()
5. 举个栗子
JSON
{
"code":"200",
"message": "success",
"data": {
"money": 2000
}
}
Model
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import java.math.BigDecimal
// 封装的通用code、message 响应体
@JsonClass(generateAdapter = true)
data class Result<T>(
@Json(name = "code")
val code: String,
@Json(name = "message")
val message: String,
@Json(name = "data")
val data:T?
)
// data 泛型
@JsonClass(generateAdapter = true)
data class MoneyResp(
@Json(name = "money")
val money: BigDecimal? = null,
)
// retrofit service 接口
@POST("/api/getMoney")
suspend fun getMoney(): BaseResp<MoneyResp>
// 模拟使用flow(真是情况应该还要搭配上协程)请求上述Json的方法逻辑
flow{
try{
emit(getMoney())
}catch(e:Exception){
Log.e("TAG", e)
}
}.flowOn(Dispatchers.IO)
.collect {
// 处理数据的业务逻辑代码
}
以上这种JSON结构,当Model中money参数类型为BigDecimal时,Moshi反序列化会对BigDecimal类型进行匹配,到BigDecimalAdapterFactory当中进行处理,并且按照reader读取到与名称相对应JSON的字段去匹配JsonReader.Token,之后按照我们自己定义的逻辑 加上 Moshi自动生成Adapter(因为generateAdapter = true)的逻辑处理后反序列化BaseResp<MoneyResp>Model类