搜狐新闻客户端使用Kotlin之后对JSON解析框架的探索

04ff17e28a31743c7cbf369a208e04b6.jpeg

8807d0b88834fa4655f5b6e6d1681458.gif

本文字数:7488

预计阅读时间:45分钟

39d3db16eedf2142afb870aa5fd11432.png

01

引言

2017年Google发布Kotlin语言之后,Android开发由原来的Java开始向Kotlin
过度,目前绝大部分Android开发岗位基本要求就是熟练使用Kotlin。事实上,很多有着多年历史的项目一开始是Java开发的,在Kotlin日渐趋于Android开发主流的过程中,混合开发成为许多项目的首选。我们的项目也是采用混合开发,面对拥有沉重历史包袱的代码,想用Kotlin重构却不得不考虑时间成本和人力成本,但又不想放弃Kotlin开发的优势,所以新业务均采用Kotlin开发。

Json就不过多介绍了,大家耳熟能详,相信很多伙伴项目中的Json解析依旧在使用FastJson或者Gson等第三方框架进行数据解析,当我们混合开发之后,你会发现Kotlin的数据类写起来很方便,但是将Json解析为数据类对象时出现的问题会让你很头大,尤其是开启混淆之后,各种各样的问题甚至程序崩溃随之出现,随着程序的崩溃,你的内心渐渐开始崩溃,不禁发出疑问,数据类不好用吗?

02

常见Json解析框架

  • FastJson:阿里巴巴公司所开发的 JSON 库,由Java语言开发,对Kotlin有一定的支持,目前FastJson1不在维护,转而维护FastJson2,称FastJson2是为未来十年打造一款高性能的Json解析框架,核心原理是反射。

  • Jackson:Spring 默认的 JSON 库,GitHub上面介绍其是"the best JSON parser for Java"(最好的Java Json解析器),支持解析多种数据格式,核心原理也是反射

    Gson:Google官方开发维护的 JSON 库,目的是为Java开发提供数据解析支持,功能非常强大,核心原理依旧是反射。

    Kotlinx.serialization:Kotlin官方开发的基于Kotlin的序列化与反序列化库,它包括了用于生成代码的插件、具有核心序列化API的运行时库以及具有各种序列化格式的支持库。编译器插件为可序列化的类生成访问者和序列化器,核心原理利用了Kotlin在编译时能够生成代码的特性,从而避免了反射的使用。

03

发生了什么问题

环境:Android Kotlin FastJson1.1.56

背景:新需求初步测试完成,即将合版,所有的Case测试通过,在最后一天用Release包测试时,发现崩溃,经过好几个人三个小时的加班问题解决了。

原因:当客户端与服务器交换数据时,使用数据类作为数据存储的方式,用FastJson将Json字符串解析为对象,用于业务逻辑的使用。Release包开启了代码混淆,导致FastJson在解析时,无法利用反射反射到数据类的构造器,从而抛出异常导致解析失败。

解决方案:改变混淆规则,数据类不进行混淆,因为数据类val关键字修饰的变量不会生成对应的set方法,而FastJson在创建对象后进行赋值时需要调用set方法为其赋值,因此还得将修饰符改为var,并且需要添加默认值,否则在反射创建对象的过程中会抛出异常,导致程序崩溃。

04

FastJson要升级新版本吗

1、问题场景再现


废话不多说,首先我们看一下问题出现的异常:default constructor not found.

888b6e79ad6fb8e5b7ea7d11fb379489.png其中ComplexEntity是我们的数据类,由val关键字修饰,并且没有任何默认值,代码如下:

import java.io.Serializable

data class ComplexEntity(
    val id: Int,
    val name: String,
    val score: Float,
    val userInfo: UserInfo
) : Serializable

data class UserInfo(
   val userName: String,
   val userAge: Int,
   val userFriends: List<FriendsInfo>
) : Serializable

data class FriendsInfo(
    val phoneNumber: String,
    val userTag: String,
    val groupId: Int
) : Serializable

我们的用法也很简单,就是要把一个Json字符串解析成为一个ComplexEntity类型的对象,代码如下,但是这里会抛出开头提到的异常。

val parseObject = JSON.parseObject(json, ComplexEntity::class.java)

我们来看看堆栈报错中JavaBeanInfo.build()方法中为什么会导致该错误:

public static JavaBeanInfo build(Class<?> clazz, //
                             int classModifiers, //
                             Type type, //
                             boolean fieldOnly, //
                             boolean jsonTypeSupport, //
                             boolean jsonFieldSupport, //
                             boolean fieldGenericSupport, //
                             PropertyNamingStrategy propertyNamingStrategy
) {
List<FieldInfo> fieldList = new ArrayList<FieldInfo>();

// DeserializeBeanInfo beanInfo = null;
Constructor<?> defaultConstructor = null;
if ((classModifiers & Modifier.ABSTRACT) == 0) {
    try {
        defaultConstructor = clazz.getDeclaredConstructor();
    } catch (Exception e) {
        // skip
    }

    if (defaultConstructor == null) {
        if (clazz.isMemberClass() && (classModifiers & Modifier.STATIC) == 0) { // for inner none static class
            for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
                Class<?>[] parameterTypes = constructor.getParameterTypes();
                if (parameterTypes.length == 1 && parameterTypes[0].equals(clazz.getDeclaringClass())) {
                    defaultConstructor = constructor;
                    break;
                }
            }
        }
    }
}

Constructor<?> creatorConstructor = null;
Method[] methods = fieldOnly //
    ? null //
    : clazz.getMethods();

final Field[] declaredFields = clazz.getDeclaredFields();

if (defaultConstructor == null // 
        && !(clazz.isInterface() || (classModifiers & Modifier.ABSTRACT) != 0) //
        ) {
    creatorConstructor = null;
    for (Constructor<?> constructor : clazz.getDeclaredConstructors()) {
        JSONCreator annotation = constructor.getAnnotation(JSONCreator.class);
        if (annotation != null) {
            if (creatorConstructor != null) {
                throw new JSONException("multi-json creator");
            }

            creatorConstructor = constructor;
            break;
        }
    }

    if (creatorConstructor != null) {
        ...... 此处省略构造器不为空的时候直接通过构造器创建对象
        return new JavaBeanInfo(clazz, null, creatorConstructor, null, fields, sortedFields, jsonType);
    }

    Method factoryMethod = null;
    {
        for (Method method : methods) {
            if ((!Modifier.isStatic(method.getModifiers())) //
                || !clazz.isAssignableFrom(method.getReturnType()) //
            ) {
                continue;
            }

            JSONCreator annotation = method.getAnnotation(JSONCreator.class);
            if (annotation != null) {
                if (factoryMethod != null) {
                    throw new JSONException("multi-json creator");
                }

                factoryMethod = method;
                break;
            }
        }
    }

    if (factoryMethod != null) {
        ......此处省略创建beanInfo的过程
        return beanInfo;
    }
    //抛出该异常原因是factoryMethod 没有被赋值
    throw new JSONException("default constructor not found. " + clazz);
}

此方法的目的是通过反射获取传入的clazz类的构造器,从而为后面解析创建对象时使用。在Json串解析时将值赋值给对应属性时,也会提前通过反射去构造一个FileInfo,它里面保存了对应的属性名和set方法。在赋值时会调用对应的set方法。

840ddbf9558d54c8e3d385cec67a14e7.png

而我们的数据类声明的是val类型的变量,那么编译器会帮我们生成对应的get、toString方法等,通过Android Studio查看数据类字节码反编译后的结果,可以看到val所修饰的属性不会生成对应的set方法,并且也不会生成默认的无参构造器,只有一个全参构造器,那么执行上面的代码时:构造器获取不到会抛出异常,set方法获取不到无法给属性赋值,因此打断了框架利用反射解析JSON的施法。通过上面的结果我们可以对数据类改造如下:

data class ComplexEntity(
    var id: Int = 0,
    var name: String = "",
    var score: Float = 0f,
    var userInfo: UserInfo = UserInfo()
) : Serializable

data class UserInfo(
    var userName: String ="",
    var userAge: Int = 0,
    var userFriends: List<FriendsInfo> = mutableListOf()
) : Serializable

data class FriendsInfo(
    var phoneNumber: String = "",
    var userTag: String = "",
    var groupId: Int = 0
) : Serializable

这样写之后,在Json解析为对象时,就能够通过反射正确反射出类的构造器以及set方法,并正确为每个属性进行赋值了。

但是依然有一个问题,当我们的Json字符串中某个String或者对象类型的值为null时,例如:{......,"name":null,......}这样的Json在进行解析时则会出现以下异常:

f74b4042184e1e3cc12b48415c6bb7a9.png

很显然,是在反射进行赋值的时候将null赋值给了不可为空的属性,导致异常发生。所以说在遇到这种字段时,我们需要将对应的属性设置为可空类型的。

另外就是很多项目在进行release打包后,会混淆代码,但是在Json解析时,如果代码属性名方法名亦或是类名混淆之后,反射还能正常工作吗,当然不能,因此还需要配置混淆规则,Json解析的类不进行混淆。

2、FastJson现状


首先我们先来了解以下FastJson的版本,目前项目中使用的是1.1.56版本,GitHub上FastJson1的最后一个版本是1.2.83,于2022年五月发布,至今一年多没有迭代新的版本。

16ce0b4316035eee1958a08f521e8b56.png

而我们使用的版本更是2017年一月发布的版本,时至今日,已经六年多时间过去了,期间也出过FastJson漏洞的事件。六年里,FastJson团队一致在积极维护更新。随着Kotlin语言的发展,FastJson也对Kotlin进行了一定的支持。

FastJson2 于2022年四月发布,依旧是之前的团队进行开发维护,该框架从官方介绍上说是对性能有了进一步的提升,目的是为下一个10年提供一个高性能的JSON库,覆盖的场景挺多,对Android和Kotlin进行了一定的支持。并且groupId发生了改变,因此升级需要替换包名并且验证所有API的兼容性,具体可参考GitHub详细介绍。

b6f32636c6b850507b144973e8327c8d.png

3、FastJson1与FastJson2对数据类的支持情况


经过测试,不管是FastJson1还是FastJson2都没有很好的兼容Kotlin Data Class,我们在使用过程中,发现新旧最新版本在对Kotlin数据类进行支持时,都需要引入Kotlin-reflect包,这是一个Kotlin实现反射的包,包大小超过2M,在当下包体大小也是一个App的指标,增加2M包体大小固然不是很好的解决方式。我们主要测试的结果如下:

首先,测试FastJson1,版本1.2.83,混淆导致出错的解决方案已经在上面提到,因此这里不在测试混淆后的结果。

●     数据类中的属性由val修饰,无默认值,结果如下:

599af14ae97619c7e48169fd952a4863.png

我们找到JavaBeanInfo类的build方法中,可以看到有下面一段代码:

String[] paramNames = null;
if (kotlin && constructors.length > 0) {
    paramNames = TypeUtils.getKoltinConstructorParameters(clazz);
    creatorConstructor = TypeUtils.getKotlinConstructor(constructors, paramNames);
    TypeUtils.setAccessible(creatorConstructor);
} else {
  ......
}
public static String[] getKoltinConstructorParameters(Class clazz) {
        if (kotlin_kclass_constructor == null && !kotlin_class_klass_error) {
            try {
                Class class_kotlin_kclass = Class.forName("kotlin.reflect.jvm.internal.KClassImpl");
                kotlin_kclass_constructor = class_kotlin_kclass.getConstructor(Class.class);
            } catch (Throwable e) {
                kotlin_class_klass_error = true;
            }
        }
        if (kotlin_kclass_constructor == null) {
            return null;
        }
  ......

我们可以看到在反射创建构造器时,如果是Kotlin类,那么则使用Kotlin-reflect反射来获取对应的构造方法,因此看到这里,就知道为什么需要引入Kotlin-reflect才能够解析成功了。因此我们引入kotlin反射相关的依赖,则解析成功。开启混淆之后,由于我们的实体类都实现了java.io.Serializable接口,因此我们的配置如下:

-keep class kotlin.reflect.jvm.** {*;}
-keep class * implements java.io.Serializable {*;}

接下来测试FastJson2对数据类的支持,我们在项目中导入com.alibaba.fastjson2:fastjson2:2.0.34.android4的依赖,该版本适配了Android4。接下来依旧针对上述内容进行测试。

数据类使用val 修饰,没有默认值,未开启混淆并且为导入kotlin-reflect的情况下,运行程序发生如下异常:

ed936c217cf616025297f4cddd5a7cc0.png对发生异常的地方进行调试,再次遇到了一个熟悉的异常,发现是Kotlin数据类的空安全类型导致的,因为此处创建对象时会给属性赋默认值,而在Java当中,String或者其它Object类型的默认值为null,将一个null值赋值给Kotlin的no-null类型的属性便会发生异常。

e04c48638720be06bbc063cc3a3d8a70.png

将数据类中String或者其它引用类型声明为可空类型的属性,再次测试得到的结果时返回了一个只有默认值的对象,解析失败。因为其底层原理依旧是反射,因此导入kotlin-reflect反射依赖,解析成功。既然是一个全新的框架,可以不依赖反射吗?

当然是可以的,我们根据其FastJson核心是反射的原理将数据类进行改造,让编译器为数据类生成默认构造器以及set方法,改造如下:其中default字段在JSON串中并不存在对应的Key-Value,但是解析后默认值依然存在,另外开启混淆之后,将实体类的混淆关闭即可。

data class ComplexEntity(
    var id: Int = 0,
    var name: String = "",
    var score: Float = 0f,
    var userInfo: UserInfo = UserInfo(),
    val default: Int = 10012,
) : Serializable

data class UserInfo(
    var userName: String ="",
    var userAge: Int = 0,
    var userFriends: List<FriendsInfo> = mutableListOf()
) : Serializable

data class FriendsInfo(
    var phoneNumber: String = "",
    var userTag: String = "",
    var groupId: Int = 0
) : Serializable

05

Gson如何?

Gson是Google团队开发维护的用于Java中JSON解析的框架,其底层原理依旧使用了反射,具体的反射过程与解析这里就不再分析了;数据类亦能够正常解析为对象,但是当Json串中值出现null之后,他不会进行空安全类型检查,会直接赋值为null。

val json =
           "{\"id\":100,\"name\":null,\"score\":99.9,\"userInfo\":{\"userAge\":24,\"userFriends\":null,\"userName\":\"Tom\"}}"

val parseObject = gson.fromJson(json, ComplexEntity::class.java)
Log.d(TAG, parseObject.toString())
//ComplexEntity(id=100, name=null, score=99.9, userInfo=UserInfo(userName=Tom, userAge=24, userFriends=null))

这样在使用数据类对应的属性时容易导致异常造成程序崩溃。

另外一个就是不支持数据类的默认值,当我们数据类定义有具有默认值的属性,而Json串中没有时,此时我们解析之后会发现我们定义的值变成了该类型的默认值,在某些特殊情况下可能会导致异常。

data class ComplexEntity(
    val id: Int,
    val name: String,
    val score: Float,
    val userInfo: UserInfo,
    val default: Int = 1223//默认值为1223  解析后:default属性的值为:0
) : Serializable

06

Kotlinx-Serialization

Kotlinx-Serialization是Kotlin官方序列化库,相比于FastJson,Kotlin-Serializable未使用反射,并且许多生成代码相关的功能添加到了编译器中,GitHub地址: kotlinx-serialization官网 ,它由两个主要部分组成:

  • Gradle插件org.jetbrains.kotlin.plugin.serialization

  • 运行时库

Kotlinx-Serialization由Kotlin语言编写,并且利用了Kotlin编译器能够生成代码的特性从而为可序列化的类生成序列化反序列化相关的代码,从而不需要向其它框架一样使用反射,另外一个特点就是它的体积很小,并且对数据类进行了很好的支持,并且API也和其它JSON解析框架一样丰富,能够满足我们所需。

1、kotlin-serialization的环境配置


导入依赖:

implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1"

开启编译器插件:我的测试项目Gradle版本是7.1.2,不同的版本配置略有差异,但目的都是配置插件。

模块的build.gradle:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'
}

工程的settings.gradle:

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
        maven ()
    }

    plugins {
        id 'com.android.application' version '7.1.2'
        id 'com.android.library' version '7.1.2'
        id 'org.jetbrains.kotlin.android' version '1.8.21'
        id 'org.jetbrains.kotlin.plugin.serialization' version '1.8.21'
    }
}

这样插件就配置好了,接下来sync以下就可以使用kotlin官方的序列化了。

2、kotlin-serialization的基础介绍


在使用kotlin-serialization时,需要给数据类加上注解@Serializable,以此来标识这是一个可以被序列化的Kotlin类,从而编译器会生成对应的代码。

@kotlinx.serialization.Serializable
data class ComplexEntity(
    val id: Int,
    val name: String,
    val score: Float,
    val userInfo: UserInfo,
    val default: Int = 1223
)

@kotlinx.serialization.Serializable
data class UserInfo(
    val userName: String,
    val userAge: Int,
    val userFriends: List<FriendsInfo>
)

@kotlinx.serialization.Serializable
data class FriendsInfo(
    val phoneNumber: String,
    val userTag: String,
    val groupId: Int
)

通过字节码反编译后,我们看到编译器生成了一个内部类,它实现了GeneratedSerializer,这个接口是给编译器插件使用的,该内部类主要就是为Kotlin序列化提供相关的方法,将对象序列化为Json或者将Json字符串反序列化为对象都要通过该类的方法去实现。

序列化与反序列化示例:

private fun testFastJsonKtBean() {
    val complexEntity = ComplexEntity(100, "null", 99.90f, UserInfo("Tom", 24, mutableListOf<FriendsInfo>().apply {
        add(FriendsInfo("12311111111", "lover", 1))
        add(FriendsInfo("12322222222", "normal", 2))
        add(FriendsInfo("12333333333", "normal", 2))
    }))
    val jsonString = Json.encodeToString(complexEntity)
    Log.d(TAG, jsonString)

    val json = "{\"id\":100,\"name\":\"Android Developer\",\"score\":99.9,\"userInfo\":{\"userName\":\"Tom\",\"userAge\":24,\"userFriends\":[{\"phoneNumber\":\"12311111111\",\"userTag\":\"lover\",\"groupId\":1},{\"phoneNumber\":\"12322222222\",\"userTag\":\"normal\",\"groupId\":2},{\"phoneNumber\":\"12333333333\",\"userTag\":\"normal\",\"groupId\":2}]}}"
    val parseObject = Json.decodeFromString<ComplexEntity>(json)
    Log.d(TAG, parseObject.toString())
}

kotlin-serialization完全符合Kotlin属性的空安全性,解析的过程中对默认值也没有任何的影响,但是JSON字符串中常常会含有null或者空对象{}来表示某个属性为空,因此我们需要将可能出现为空的属性设置上默认值或者声明为可空类型来保证正常解析。

3、kotlin-serialization常用API


  • 序列化与反序列化:

//序列化JSON
Json.encodeToString(value:T):String
//JSON反序列化
Json.decodeFromString(string: String):T
  • 将JSON字符串转化为JsonElement,对应其它框架的JsonObject:

//该方法返回一个JsonElemnt
parseToJsonElement(json:String)
//判断jsonElement中是否含有key
jsonElement?.jsonObject?.containsKey(key:String)
//获取jsonElement
jsonElement?.jsonObject?.get(key)

//获取key对应的各种类型的值
jsonElement?.jsonObject?.get(key)?.jsonPrimitive?.boolean

jsonElement?.jsonObject?.get(key)?.jsonPrimitive?.int

jsonElement?.jsonObject?.get(key)?.jsonPrimitive?.long

jsonElement?.jsonObject?.get(key)?.jsonPrimitive?.float

jsonElement?.jsonObject?.get(key)?.jsonPrimitive?.double

jsonElement?.jsonObject?.get(key)?.jsonArray
  • 构造JsonObject:

val element = buildJsonObject {
    put("name", "kotlinx.serialization")
    putJsonObject("owner") {
        put("name", "kotlin")
    }
    putJsonArray("forks") {
        addJsonObject {
            put("votes", 42)
        }
        addJsonObject {
            put("votes", 9000)
        }
    }
}
  • 指定序列化和反序列化相关的Json配置:

//指定配置
val json = Json {
    //忽略Json字符串中冗余的键值对,否则会发生异常
    ignoreUnknownKeys = true
    //Json串中为null但是属性为no-null类型,或者枚举值不存在
    coerceInputValues = true
}

4、如何封装以适应现有业务


首先,为什么要对第三方框架进行封装呢?做过第三方框架升级的朋友应该很清楚,当我们在项目当中直接使用大量的第三方提供的库时,特别是某些框架对外提供的接口比较多的情况下,每个人在使用的时候写法不同,所使用的方法也不同,因此升级后,需要对每一处调用的地方进行Review,防止兼容性问题导致功能异常。因此最好的解决方式就是根据自己的业务对三方框架进行二次封装,这样在升级框架的时候,如果某些Api变化较大,我们只需要对自己封装的框架进行简单的改动就行。所以我们要针对于数据类解析对kotlin-serialization进行封装。

我们需要明白在客户端场景下对Json的解析最常用的Api是哪些,主要分为两大类,序列化和反序列化,前者用的相对没有反序列化频繁,但也是必须的,我们先来看反序列化。反序列化需要将JSON解析为对象,当然也有解析字段在进行手动构造的对象的情况。因此我们定义了一些适合业务的接口,其中的第一个接口是通过类型将JSON字符串解析为对象,同时要保证每个方法的健壮性以及可维护性。

/**
 * 配置解析为对象的时候忽略Json字符串中冗余的键值对
 */
val json = Json {
    ignoreUnknownKeys = true
    coerceInputValues = true
}

/**
 * 调用举例,EntityName为返回的类型
 * 当JSON字符串中的值为 null 时,可以使用两种方式选其一可解决异常:
 * ① 声明对应的属性为可控类型,赋值为null
 * ② 给对应字段赋值默认值
 * ```
 * val entity:EntityName = KJson.parseObject<EntityName>(jsonStr)
 * ```
 * @param jsonStr 需要解析的Json字符串
 * @return 返回解析后对应的对象,对象类需要使用注解 @Serializable
 */
inline fun <reified T> parseObject(jsonStr: String?): T? {
    jsonStr?.let {
        return try {
            json.decodeFromString<T>(jsonStr)
        } catch (e: Exception) {
            null
        }
    }
    return null
}

在FastJson或者Gson中有JsonObject类,在kotlin-serialization中前面介绍到对应的是JsonElement类,因此我们也需要提供对应的方法,这两个方法会把对象或者JSON字符串解析为JsonElement对象。

/**
 * 将对象转换成JsonElement?,发生异常时返回null
 * @param obj 有 @Serializable 注解标注的Kotlin类
 * @return JsonElement?
 */
inline fun <reified T> parseJsonElement(obj: T): JsonElement? {
    return try {
        Json.encodeToJsonElement(obj)
    } catch (e: Exception) {
        null
    }
}

/**
 * 将Json字符串解析为JsonElement,类似与其它框架的JsonObject
 * @param jsonStr Json字符串
 * @return JsonElement?
 */
fun parseJsonElement(jsonStr: String?): JsonElement? {
    jsonStr?.let {
        return try {
            Json.parseToJsonElement(jsonStr)
        } catch (e: Exception) {
            null
        }
    }
    return null
}

在现有业务中,我们使用FastJson在解析数据之前,会对基本的数据进行校验才会进行接下来的解析,类似于下面的代码,我们封装的API接口也需要提供这些基本的方法。

6b7a527868c499abdd99abd9f7a0136d.png
fun containsKey(jsonElement: JsonElement, key: String): Boolean {
    return jsonElement.jsonObject.containsKey(key)
}

当我们尝试封装一个判断JsonElement是否含有某个key时,需要调用JsonElement类的containsKey()方法,这样设计对业务调用者不利,需要传两个参数。仔细分析,内部用于判断的实际方法JsonElement中的jsonObject的判断方法,因此直接使用扩展函数对JsonElement进行扩展,对应的方法如下:

/**
 * 判断JsonElement中是否含有某一个Key-value
 * @param key 需要查找的关键词
 * @return Boolean
 */
fun JsonElement.containsKey(key: String): Boolean {
    return jsonObject.containsKey(key)
}

/**
 *  从JsonElement中获取[key]对应String类型的值,未找到或异常返回默认值[defaultStr]
 * @param key 需要查找的关键词
 * @param defaultStr 默认值
 * @return String
 */
fun JsonElement.getString(key: String): String? {
    return try {
        if (containsKey(key))
            jsonObject[key]?.jsonPrimitive?.contentOrNull
        else null
    } catch (e: Exception) {
        null
    }
}

/**
 * 从JsonElement中获取[key]对应Int类型的值,默认值为[defaultVal]
 * @param key 需要查找的关键词
 * @param defaultVal 不存在或异常情况返回默认值
 * @return Int
 */
fun JsonElement.getInt(key: String, defaultVal: Int = 0): Int {
    return try {
        if (containsKey(key))
            jsonObject[key]?.jsonPrimitive?.int ?: defaultVal
        else defaultVal
    } catch (e: Exception) {
        defaultVal
    }
}

/**
 * 从JsonElement中获取[key]对应的JsonArray,发生异常时返回null
 * @param key 需要查找的关键词
 * @return JsonArray?
 */
fun JsonElement.getJsonArray(key: String): JsonArray? {
    return try {
        if (containsKey(key))
            jsonObject[key]?.jsonArray
        else null
    } catch (e: Exception) {
        null
    }
}

以上封装了获取String和Int类型以及JsonArray类型的API,其它基本数据类型的API类似,就不再赘述了。我们对反序列化的API进行了封装,接下来是对序列化的API进行一个封装:

/**
 * @param obj 需要转换的对象,对象类需要使用注解 @Serializable
 * @return 返回对象转换后的Json字符串
 */
fun toJsonString(obj: Any): String? {
    return try {
        Json.encodeToString(obj)
    } catch (e: Exception) {
        printLog("toJsonString", e)
        null
    }
}

到此,业务常用的API及已经封装完成了,这样大家在使用的时候直接调用封装的接口,如果要升级我们只需要检验框架中的API是否会有兼容性的问题即可,以便于业务的快速迭代。


参考

  • Aoe, J. I. (1989). An efficient implementation of static string pattern matching machines. IEEE Transactions on SoftwareEngineering, 15(8), 1010-1016.

  • GitHub - Kotlin/kotlinx.serialization: Kotlin multiplatform / multi-format serialization

  • https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/serialization-guide.md

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/352157.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

单片机学习笔记---矩阵键盘

目录 矩阵键盘的介绍 独立按键和矩阵按键的相同之处&#xff1a; 矩阵按键的扫描 代码演示 代码模块化移植 Keil自定义模板步骤&#xff1a; 代码编写 矩阵键盘就是开发板上右下角的这个模块 这一节的代码是基于上一节讲的LCD1602液晶显示屏驱动代码进行的 矩阵键盘的介…

主成分分析(PCA)Python

实际问题研究中&#xff0c;常常遇到多变量问题&#xff0c;变量越多&#xff0c;问题往往越复杂&#xff0c;且各个变量之间往往有联系。于是&#xff0c;我们想到能不能用较少的新变量代替原本较多的旧变量&#xff0c;且使这些较少的新变量尽可能多地保留原来变量所反映的信…

Idea Community社区版如何添加Run Dashboard

最近在学习spring cloud&#xff0c;跟着视频添加run dashboard&#xff0c;发现里面介绍的方法无法适用于idea community(社区版)。 然后自己研究了一下&#xff0c;成功添加&#xff0c;下面分享自己的方法。 如图&#xff0c;我的项目里添加了两个module&#xff0c;我想通…

【c语言】详解操作符(下)

前言&#xff1a; 在上文中&#xff0c;我们已经学习了 原码、反码、补码、移位 操作符、移位操作符、位操作符、逗号表达式、下标访问[ ]、函数调用&#xff08; &#xff09;&#xff0c;接下来我们将继续学习剩下的操作符。 1. 结构成员访问操作符 1.1 结构体成员的直接访…

79 C++对象模型探索。数据语义学 - 进程内存空间布局分析

不同的数据在内存中会有不同的保存时机&#xff0c;和保存位置&#xff0c;这一节就分析这个。 当运行一个可执行文件时候&#xff0c;操作系统就会把这个可执行文件加载到内存&#xff1b;此时进程有一个虚拟的地址空间&#xff08;内存空间&#xff09;&#xff0c;如下图&a…

Docker部署思维导图工具SimpleMindMap并实现公网远程访问

文章目录 1. Docker一键部署思维导图2. 本地访问测试3. Linux安装Cpolar4. 配置公网地址5. 远程访问思维导图6. 固定Cpolar公网地址7. 固定地址访问 SimpleMindMap 是一个可私有部署的web思维导图工具。它提供了丰富的功能和特性&#xff0c;包含插件化架构、多种结构类型&…

03_2 连续时间信号的傅里叶变换(FT) 非周期信号的傅里叶变换

各位看官&#xff0c;大家好&#xff01;本讲为《数字信号处理理论篇》03_2 连续时间信号的傅里叶变换 非周期信号的傅里叶变换。&#xff08;特别提示&#xff1a;课程内容为由浅入深的特性&#xff0c;而且前后对照&#xff0c;不要跳跃观看&#xff0c;请按照文章或视频顺序…

《30天自制操作系统》 第一周(D1-D7) 笔记

前言&#xff1a;这是我2023年5月份做的一个小项目&#xff0c;最终是完成了整个OS。笔记的话&#xff0c;只记录了第一周。想完善&#xff0c;却扔在草稿箱里许久。最终决定&#xff0c;还是发出来存个档吧。 一、汇编语言 基础指令 MOV: move赋值&#xff0c;数据传送指令…

nginx复现负载均衡案例

这里是下载好了docker&#xff0c;并显示了下镜像这里是拉到了nginx的镜像这里是把容器起来&#xff0c;-itd是容器关闭后销毁这里是显示起来的容器进入到这个容器里面查看许多命令用不了&#xff0c;应该想办法把docker里的文件夹映射到物理机中 这里是如果访问6666端口那么隧…

常见电源电路(LDO、非隔离拓扑和隔离拓扑结构)

一、常见电路元件和符号 二、DC-DC转换器 DC-DC转换器&#xff1a;即直流-直流转换器&#xff0c;分为三类&#xff1a;①线性调节器&#xff1b;②电容性开关解调器&#xff1b;③电感性开关调节器&#xff1b; 2.1线性稳压器&#xff08;LDO&#xff09; 2.1.1 NMOS LDO…

UI自动化定位元素之js操作

前言 在UI自动化测试中&#xff0c;元素定位是一个至关重要的步骤。准确地定位到页面上的元素&#xff0c;是实现自动化测试的前提和保障。本文将介绍使用JavaScript进行元素定位的常见方法&#xff0c;并分析页面的组成&#xff0c;帮助读者更好地理解和应用元素定位技术。 页…

Oracle RAC 集群的安装(保姆级教程)

文章目录 一、安装前的规划1、系统规划2、网络规划3、存储规划 二、主机配置1、Linux主机安装&#xff08;rac01&rac02&#xff09;2、配置yum源并安装依赖包&#xff08;rac01&rac02&#xff09;3、网络配置&#xff08;rac01&rac02&#xff09;4、存储配置&#…

深度强化学习(王树森)笔记01

深度强化学习&#xff08;DRL&#xff09; 本文是学习笔记&#xff0c;如有侵权&#xff0c;请联系删除。本文在ChatGPT辅助下完成。 参考链接 Deep Reinforcement Learning官方链接&#xff1a;https://github.com/wangshusen/DRL 源代码链接&#xff1a;https://github.c…

网安渗透攻击作业(1)

实现负载均衡 第一步&#xff1a;安装依赖 sudo apt insta11 libgd-dev 第二步&#xff1a;下载nginx wget http://nginx.org/download/nginx-1.22.1.tar.gz 第三步&#xff1a;对nginx进行解压 tar -zvxf nginx-1.22.1.tar.g2 第四步&#xff1a;编译安装nginx cd ngi…

短剧小程序分销系统开发:创新与机遇的融合

一、引言 随着移动互联网的快速发展&#xff0c;短剧作为一种新兴的娱乐形式&#xff0c;正逐渐成为人们生活中的一部分。短剧小程序分销系统的开发&#xff0c;不仅为短剧的传播提供了新的渠道&#xff0c;同时也为相关产业带来了新的商业机会。本文将探讨短剧小程序分销系统…

【JavaEE】网络原理: 网络编程套接字(概念)

目录 1.什么是网络编程 2.网络编程中的基本概念 2.1发送端和接收端 2.2请求和响应 2.3客户端和服务端 3.Socket套接字 4.Socket编程注意事项 1.什么是网络编程 网络编程&#xff0c;指网络上的主机&#xff0c;通过不同的进程&#xff0c;以编程的方式实现网络通信 (…

PVE更换LXC源教程,如何在PVE上使用LXC容器

PVE更换LXC源教程&#xff0c;如何在PVE上使用LXC容器 Proxmox Virtual Environment (PVE) 是一种基于开源的虚拟化平台&#xff0c;它允许您轻松地在单个物理服务器上管理和部署虚拟机和容器。其中的LXC容器是一种轻量级容器化技术&#xff0c;可提供更高的性能和资源利用率。…

python连接sqlserver

1、安装sqlserver 用的是sqlserver2012的版本 网上很多&#xff0c;参考下 https://blog.csdn.net/weixin_44889709/article/details/123769722 2、安装python3.7及以下环境 尝试安装python3.8的环境不能连接成功 conda create -n pytorch python3.73、安装sqlserver的pyt…

Dijkstra算法-lanqiao1122

#include <bits/stdc.h> using namespace std; const long long INF 0x3f3f3f3f3f3f3f3fLL; const int N 3e5 5; struct edge{int from, to;//边&#xff1a;起点&#xff0c;终点&#xff0c;权值&#xff1b;起点from没有用到&#xff0c;e[i]的i就是fromlong long …

JUC-synchronized无锁、偏向锁、轻量级锁、重量级锁

1 synchronized实操 关键字synchronized可以用来保证多线程并发安全的**原子性、可见、有序性。**关键字synchronized不仅可以作用于方法还可以作用于同步代码块&#xff0c;能够保证作用范围中线程访问安全。 注意&#xff1a;局部变量是线程安全的。线程不安全问题只存在于实…