Kotlin Json库问题总结(Gson以及Moshi的坑)

// $FF: synthetic method
public DefaultPart(String var1, String var2, int var3, int var4, DefaultConstructorMarker var5) {
// 最低不为0表示第一个默认值字段在json中无值,需要默认值
if ((var4 & 1) != 0) {
var1 = “me”;
}

if ((var4 & 2) != 0) {
var2 = “male”;
}

this(var1, var2, var3);
}
}

这种情况下该类并没有生成空参构造函数,所以gson实例化时使用了Unsafe,自然默认值不生效。实际上只有所有字段都有默认值时才会生成空参构造函数。

moshi正常工作原因

Moshi库属于square公司,最初由Jake Wharton主导,他是kotlin的拥趸,不难推测moshi对Kotlin做了兼容,实际上也是这样。

Moshi序列化/反序列化时根据每个类反射创建对应的JsonAdapter,用它来进行具体操作,同时支持使用annotationProcessor编译时预先生成各个类的JsonAdapter,空间换时间提升性能。我们从注解器生成的代码入手,看看它做了什么处理,这里选取DefaultPart类生成的DefaultPartJsonAdapter。

public class DefaultPartJsonAdapter(
moshi: Moshi
) : JsonAdapter() {
private val options: JsonReader.Options = JsonReader.Options.of(“name”, “gender”, “age”)

private val stringAdapter: JsonAdapter = moshi.adapter(String::class.java, emptySet(),
“name”)

private val intAdapter: JsonAdapter = moshi.adapter(Int::class.java, emptySet(), “age”)

@Volatile
private var constructorRef: Constructor? = null

public override fun toString(): String = buildString(33) {
append(“GeneratedJsonAdapter(”).append(“DefaultPart”).append(‘)’) }

public override fun fromJson(reader: JsonReader): DefaultPart {
var name: String? = null
var gender: String? = null
var age: Int? = null
// 补码32个1
var mask0 = -1
reader.beginObject()
while (reader.hasNext()) {
when (reader.selectName(options)) {
0 -> {
// name字段非空类型,如果json中value为null则抛异常
name = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull(“name”, “name”, reader)
// …1111 & …1110 = …1110,最低位0表示第一个默认字段在json中存在,不需要赋默认
mask0 = mask0 and 0xfffffffe.toInt()
}
1 -> {
gender = stringAdapter.fromJson(reader) ?: throw Util.unexpectedNull(“gender”, “gender”,
reader)
// …1110 & …1101 = …1100,次低位0表示第二个默认字段在json中存在,不需要赋默认,以此类推
mask0 = mask0 and 0xfffffffd.toInt()
}
2 -> age = intAdapter.fromJson(reader) ?: throw Util.unexpectedNull(“age”, “age”, reader)
-1 -> {
// Unknown name, skip it.
reader.skipName()
reader.skipValue()
}
}
}
reader.endObject()
if (mask0 == 0xfffffffc.toInt()) {
// 如果所有默认字段都存在于json中,则忽略默认值直接调用构造函数赋值成json中的值
return DefaultPart(
name = name as String,
gender = gender as String,
// age字段非空,如果在json中没有对应key则抛异常
age = age ?: throw Util.missingProperty(“age”, “age”, reader)
)
} else {
// 如果有默认值的字段在Json中不存在,则传入flag反射调用synthetic构造函数,填充默认值
@Suppress(“UNCHECKED_CAST”)
val localConstructor: Constructor = this.constructorRef ?:
DefaultPart::class.java.getDeclaredConstructor(String::class.java, String::class.java,
Int::class.javaPrimitiveType, Int::class.javaPrimitiveType,
Util.DEFAULT_CONSTRUCTOR_MARKER).also { this.constructorRef = it }
return localConstructor.newInstance(
name,
gender,
age ?: throw Util.missingProperty(“age”, “age”, reader),
mask0,
/* DefaultConstructorMarker */ null
)
}
}
}

做的事情其实也很简单,代码中我写了注释

  1. 用一个int记录(字段超过32个使用多个int)默认值字段在将要解析的json中是否存在,从最低位到最高位依次记录第一个到最后一个默认值字段在json中是否有key,0表示存在,1表示不存在
  2. 判断是否所有默认字段在json中都有值,若为true则不用管默认值,直接使用json字段生成实例,若为false则反射调用(synthetic构造器只能够反射调用)synthetic构造器实例化对象,synthetic构造器会根据标志位为默认值字段赋值

一言蔽之,Moshi通过遵循Kotlin的机制做到了兼容。

解决方案

分析了这么多,避免默认值无效的方法已经显而易见了

  1. 定义类时所有字段都给一个默认值,这样gson就可以正常工作
  2. 使用Moshi库

其他问题,Json中value为null的情况

正常情况下后端返回的Json数据中只应该存在Object类型字段为null的情况,但是现实很骨感,不乏String类型/list类型丢过来也是null的情况。

  • 在Java中,null value会覆盖掉默认值,使用时get方法中判空就可以了。
  • 但是在Kotlin中,如果该字段声明为非空类型,使用gson序列化后非空类型字段会被赋予null值,虽然由于空安全检查是在编译器进行不会报异常,但是这明显非常不符合预期。
  • 而Moshi中对这个情况做了处理,非空字段对应的json value为null时抛JsonDataException,对应的key都不存在时也做同样处理

这些处理逻辑看起来都很合情合理,但是实际开发中不可预期的null value情况又确实存在,我们也不太可能将所有字段都声明为可空类型,那么将Json中null value自定义解析成预设值或许是一个比较好的方法。

Gson自定义解析替换null value

Gson自定义解析使用TypeAdapterFactory或者单TypeAdapter,下面示例将声明为String和List的字段通过自定义解析器替换Json中null value为空字符串和空list

class GsonDefaultAdapterFactory: TypeAdapterFactory {
override fun create(gson: Gson, type: TypeToken): TypeAdapter? {
if (type.type == String::class.java) {
return createStringAdapter()
}
if (type.rawType == List::class.java || type.rawType == Collection::class.java) {
return createCollectionAdapter(type, gson)
}
return null
}

/**

  • null替换成空List
    */
    private fun createCollectionAdapter(
    type: TypeToken,
    gson: Gson
    ): TypeAdapter? {
    val rawType = type.rawType
    if (!Collection::class.java.isAssignableFrom(rawType)) {
    return null
    }

val elementType: Type = $Gson$Types.getCollectionElementType(type.type, rawType)
val elementTypeAdapter: TypeAdapter =
gson.getAdapter(TypeToken.get(elementType)) as TypeAdapter

return object : TypeAdapter<Collection>() {
override fun write(writer: JsonWriter, value: Collection?) {
writer.beginArray()
value?.forEach {
elementTypeAdapter.write(writer, it)
}
writer.endArray()
}

override fun read(reader: JsonReader): Collection {
val list = mutableListOf()
// null替换为空list
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return list
}
reader.beginArray()
while (reader.hasNext()) {
val element = elementTypeAdapter.read(reader)
list.add(element)
}
reader.endArray()
return list
}

} as TypeAdapter
}

/**

  • null 替换成空字符串
    */
    private fun createStringAdapter(): TypeAdapter {
    return object : TypeAdapter() {
    override fun write(writer: JsonWriter, value: String?) {
    if (value == null) {
    writer.value(“”)
    } else {
    writer.value(value)
    }
    }

override fun read(reader: JsonReader): String {
// null替换为""
if (reader.peek() == JsonToken.NULL) {
reader.nextNull()
return “”
}
return reader.nextString()
}

} as TypeAdapter
}
}

测试代码

val gson: Gson = GsonBuilder()
.registerTypeAdapterFactory(GsonDefaultAdapterFactory())
.create()

data class Person(
val name: String,
val friends: List
)

fun testGsonNullValue() {
// 这里必须要有age字段,moshi为了保持空安全不允许age为null
val json = “”“{“name”:null, “friends”:null}”“”
val p1 = gson.fromJson(json, Person::class.java)
println(“gson parse json: $p1”)
}

运行结果gson parse json: Person(name=, friends=[]),符合预期

Moshi自定义解析替换null value

Moshi中通过JsonAdapter或者JsonAdapterFactory自定义解析,这边我直接将moshi标准JsonAdapter拿过来进行了修改

public final class MoshiDefaultAdapterFactory {
private MoshiDefaultAdapterFactory() {
}

public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override
public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null;

if (type == String.class) return STRING_JSON_ADAPTER;

return null;
}
};

static final JsonAdapter STRING_JSON_ADAPTER = new JsonAdapter() {
@Override
public String fromJson(JsonReader reader) throws IOException {
// 替换null为""
if (reader.peek() != JsonReader.Token.NULL) {
return reader.nextString();
}
reader.nextNull();
return “”;
}

@Override
public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value);
}

@Override
public String toString() {
return “JsonAdapter(String)”;
}
};
}

替换空null为空list Adapter

/**

  • @author greensun
  • @date 2021/6/2
  • @desc null 转换成空collection 更改自{@link com.squareup.moshi.CollectionJsonAdapter}
  • 如果字段声明为Collection, json中值为null,kotlin下在声明类型为非空情况下会抛异常,这里给一个空Collection填充
    */
    public abstract class MoshiDefaultCollectionJsonAdapter<C extends Collection, T> extends JsonAdapter {
    public static final Factory FACTORY =
    new Factory() {
    @Override
    public JsonAdapter<?> create( Type type, Set<? extends Annotation> annotations, Moshi moshi) { Class<?> rawType = Types.getRawType(type);
    if (!annotations.isEmpty()) return null;
    if (rawType == List.class || rawType == Collection.class) {
    return newArrayListAdapter(type, moshi);
    } else if (rawType == Set.class) {
    return newLinkedHashSetAdapter(type, moshi);
    }
    return null;
    }
    };

private final JsonAdapter elementAdapter;

private MoshiDefaultCollectionJsonAdapter(JsonAdapter elementAdapter) {
this.elementAdapter = elementAdapter;
}

static JsonAdapter<Collection> newArrayListAdapter(Type type, Moshi moshi) {
Type elementType = Types.collectionElementType(type, Collection.class);
JsonAdapter elementAdapter = moshi.adapter(elementType);
return new MoshiDefaultCollectionJsonAdapter<Collection, T>(elementAdapter) {
@Override
Collection newCollection() {
return new ArrayList<>();
}
};
}

static JsonAdapter<Set> newLinkedHashSetAdapter(Type type, Moshi moshi) {
Type elementType = Types.collectionElementType(type, Collection.class);
JsonAdapter elementAdapter = moshi.adapter(elementType);
return new MoshiDefaultCollectionJsonAdapter<Set, T>(elementAdapter) {
@Override
Set newCollection() {

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

尾声

面试成功其实都是必然发生的事情,因为在此之前我做足了充分的准备工作,不单单是纯粹的刷题,更多的还会去刷一些Android核心架构进阶知识点,比如:JVM、高并发、多线程、缓存、热修复设计、插件化框架解读、组件化框架设计、图片加载框架、网络、设计模式、设计思想与代码质量优化、程序性能优化、开发效率优化、设计模式、负载均衡、算法、数据结构、高级UI晋升、Framework内核解析、Android组件内核等。

不仅有学习文档,视频+笔记提高学习效率,还能稳固你的知识,形成良好的系统的知识体系。这里,笔者分享一份从架构哲学的层面来剖析的视频及资料分享给大家梳理了多年的架构经验,筹备近6个月最新录制的,相信这份视频能给你带来不一样的启发、收获。

Android进阶学习资料库

一共十个专题,包括了Android进阶所有学习资料,Android进阶视频,Flutter,java基础,kotlin,NDK模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

image

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

《2017-2021字节跳动Android面试历年真题解析》

模块,计算机网络,数据结构与算法,微信小程序,面试题解析,framework源码!

[外链图片转存中…(img-v4Wq5VdN-1711935436022)]

大厂面试真题

PS:之前因为秋招收集的二十套一二线互联网公司Android面试真题 (含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-FYIjGzt8-1711935436022)]

《2017-2021字节跳动Android面试历年真题解析》

[外链图片转存中…(img-AMZjOf0H-1711935436022)]

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值