【Kotlin学习】Kotlin中的反射

反射:在运行时对kotlin对象进行自省

反射是一种在运行时动态访问对象属性和方法的方式,而不需事先确定这些属性是什么。一般来说当你访问一个对象的方法或者属性时,程序的源代码会因用一个具体的声明,编译器将静态解析这个引用并确保这个声明是存在的。但有时候你要编写能够使用任意类型的对象的代码,或者只能在运行时才能确定要访问的方法和属性的名称。例子:JSON序列化库要能够把任何对象都序列化成JSON,所以它不能引用具体的类和属性,这时可以使用反射

在kotlin使用反射时,会和两种不同的反射API打交道,一种是标准的java反射,定义在java.lang.reflect中,因为kotlin类会被编译成普通的java字节码,java反射API可以完美支持它们。第二种是kotlin反射API,定义在kotlin.reflect中,它让你能访问那些在java世界里不存在的概念如属性和可空类型。kotlin反射没有局限于kotlin类,你能够使用同样的API访问用任何JVM语言写成的类

Kotlin反射API:KClass、KCallable、KFunction和KProperty

kotlin反射API的主要入口是KClass,它代表了一个类。KClass对应的是java.lang.class,可以用它列举和访问类中包含的所有声明,然后是它的超类中的声明等等。MyClass::class这种写法会带给你一个KClass的实例。要在运行时取得一个对象的类,首先使用javaClass属性获得它的java类,这直接等价于java中的java.lang.Object.getClass()。然后访问该类的.kotlin扩展属性,从java切换到kotlin的反射API

在这里插入图片描述

在这里插入图片描述

KClass有许多有用的特性可以去到类中查看

由类的所有成员组成的列表是一个KCallable实例的集合KCallable是函数和属性的超接口,它声明了call方法,允许你调用对应的函数或者对应属性的getter

在这里插入图片描述

你把(被引用)函数的实参放在varargs列表中提供给它,下图演示了如何通过反射使用call调用参数

在这里插入图片描述

::foo的语法我们之前也看到过,现在你可以发现这个表达式的值来自反射API的KFunction类的一个实例。如果提供错误的实参调用函数将会抛出运行时异常

::foo表达式的类型是KFunction1<Int,Unit>,它包含了形参类型和返回类型的信息,1表示这个函数接收一个形参。使用invoke方法通过这个接口来调用函数,也可以直接调用kFunction。如果你有一个具体类型的KFunction,它的形参类型和返回类型是确定的,那么应优先使用这个具体类型的invoke方法,call方法是对所有类型都有效的通用手段,但它不提供类型安全性

KFunctionN接口是如何、在哪里定义的
N代表参数数量,而且都继承了KFunction并加上一个额外成员invoke,operator fun invoke(p1:P1,p2:P2):R.这些类型称为合成的编译器生成类型,在包kotlin.reflect中找不到它们的声明,这意味着你可以使用任意数量参数的函数接口。避免了对函数类型参数数量的人为限制

也可以在KProperty实例上调用call方法,它会调用该属性的getter,但属性接口为你提供了一个更好的获取属性值的方式:get方法。要访问get方法,你需要根据属性声明的方式来使用正确的属性接口。顶层属性表示为KProperty0接口的实例,它有一个无参数的get方法

在这里插入图片描述

一个成员属性由KProperty1的实例表示,它拥有一个单参数的get方法。要访问该属性的值,必须提供你需要的值所属的那个对象实例

在这里插入图片描述

注意KProperty1是一个泛型类,变量mp的类型是KProperty<Person,Int>,其中第一个类型参数表示接收者的类型,第二个类型参数代表了属性的类型。这样你只能对正确类型的接收者调用它的get方法。注意只能使用发射访问定义在最外层或者类中的属性,而不能访问函数的局部变量

访问源码元素的接口的层级结构

在这里插入图片描述

用反射实现对象序列化

在JKid库中序列化函数的声明

在这里插入图片描述

这个函数接受一个对象然后返回JSON表示法的字符串,它通过一个StringBuilder实例来构建JSON结果。这个函数在序列化对象属性和它们的值的同时,这些内容会被附加到这个StringBuilder对象之中。我们把实现放在StringBuilder的扩展函数中,好让append的调用更加简洁

把一个函数参数转化成一个扩展函数的接收者是kotlin代码的常见模式,上图执行的操作在这个特殊的上下文之外毫无意义,所以用private标记保证它不会在其他地方使用,结果serialize函数把所有工作委托给了serializeObject。buildString会创建一个StringBuilder,并让你在lambda中填充它的内容

默认情况下它将序列化对象的所有属性:基本数据类型和字符串将被酌情序列化成JSON数值、布尔值和字符串值,集合将会被序列化成JSON数组其它类型的属性将会被序列化成嵌套的对象

在这里插入图片描述

在这里插入图片描述

joinToStringBuilder函数保证属性与属性之间用逗号隔开,serializeString函数按照JSON的格式要求对特殊字符进行转义。serializeString函数检查一个值是否是一个基本数据类型的值、字符串、集合或嵌套对象,然后相应序列化它的内容

用注解定制序列化

之前我们讨论过@JsonExclude、@JsonName、@CustomSerializer这几个注解,现在来看看serializeObject是如何处理这些注解的

从@JsonExclude开始,这个注解允许你在序列化的时候排除某些属性
我们使用了KClass实例的扩展属性memberProperties,来取得类的所有成员属性。现在我们要过滤使用了@JsonExclude注解的属性。KAnnotateElement接口定义了属性annotations,它是一个由应用到源码中元素上的所有注解(具有运行时保留期)的实例组成的集合。因为KProperty继承了KAnnotatedElement,可以用property.annotations这样的写法来访问一个属性的所有注解。但这里的过滤并不会用到所有的注解,它只需找到那个特定的注解(@JsonExclude)辅助函数findAnnotation完成了这项工作

在这里插入图片描述

返回一个注解,其类型就是指定为类型实参的类型,如果这个注解存在。它让类型形参变成reified,以期把注解类作为类型实参传递。filter语句过滤了带@JsonExclude注解的属性

@JsonName
这种情况下你关心的不仅是注解存不存在,还要关心它的实参:被注解的属性在JSON中应该用的名称

在这里插入图片描述

jsonNameAnn取得@JsonName注解的实例,如果它存在。propName取得它的"name"实参或者备用的prop.name。如果没有用@JsonName注解,jsonNameAnn就是null,而你仍然需要使用prop.name作为属性在JSON中的名称。如果使用了该注解,你就会使用在注解中指定的名称而不是属性自己的名称

@CustomSerializer
它的实现基于getSerializer函数,该函数返回通过@CustomSerializer注解注册的ValueSerializer实例。

在这里插入图片描述

如果像上图一样声明Person类,并在序列化age属性时调用getSerializer(),它会返回一个IntSerializer实例

取回属性值的序列化器

在这里插入图片描述

它是KProperty的扩展函数,因为属性是这个方法要处理的主要对象(接收者)。它调用findAnnotation函数取得一个@CustomSerializer注解的实例,如果实例存在。它的实参serializerClass指定了你需要获取哪个类的实例

作为@CustomSerializer注解的值和对象,它们都用KClass表示。不同的是,对象拥有非空值的objectInstance属性,可以用它来访问为object创建的单例实例。例如IntSerializer被声明成了一个object,所以它的ojectInstance属性存储了IntSerializer的单例实例。你将用这个实例序列化所用对象,而不会调用createInstance。如果KClass表示的是一个普通的类,可以通过调用createInstance来创建一个新的实例。它和java中的java.lang.Class.newInstance类似。最终你可以在serializeProperty的实现中用上getSerializer

serializeProperty通过调用序列化器的toJsonValue,来把属性值转换成JSON兼容的格式,如果属性没有自定义序列化器,它就使用属性的值

JSON解析和对象反序列化

在这里插入图片描述

我们要把反序列化的对象的类型作为实化类型参数传给deserialize函数并拿回一个新的对象实例。JSON反序列化涉及解析JSON字符串输入和使用反射访问对象的内部细节。JKid的JSON反序列化器使用相当普通的方式实现,有三个重要阶段组成:词法分析器(通常被称为lexer)语法分析器或解析器以及反序列化组件本身

词法分析把由字符组成的输入字符串切分成一个由标记组成的列表。这里有两类标记:代表JSON语法中具有特殊意义的字符(逗号、冒号、花括号和方括号)的字符标记,对应到字符串、数字、布尔值以及null常量的值标记。

解析器通常负责将无格式的标记列表转换成结构化的表示法。它在JKid中的任务是理解JSON的更高级别的结构,并将各个标记转换为JSON中支持的语义元素:键值对、对象和数组。解析器在发现当前对象的新属性(简单值、复合属性或数组)时调用相应的方法

在这里插入图片描述

这些方法中的propertyName接收到了JSON键。当解析器遇到一个使用对象作为值的author属性时,createObject(“author”)方法会被调用。简单值属性被报告为setSimpleProperty调用,实际的标记值作为value实参传递给这次调用。JsonObject实现负责创建属性的新对象,并在外部对象中存储对它们的引用

在这里插入图片描述

然后反序列化器为JsonObject提供一种实现,逐步构建相应类型的新实例。他需要找到类属性和JSON键之间的关系,如上图的title、author、name。在这之后才可以创建一个最终需要的类的新实例(Book)。

JKid库打算使用数据类,因此它将从JSON文件加载的所有名称-值的配对作为参数传递给要被反序列化的类的构造方法。它不支持在对象实例创建后设置其属性,这意味着从JSON中读取数据时它需要将数据存储在某处,然后才能构建该对象

在创建对象之前保存其组件的要求看起来与传统的构造器模式相似,区别在于构建器通常用于创建一种特定类型的对象,并且解决方案需要完全通用。我们在这个实现中使用了一个有趣的词语种子(Seed)。在JSON中,你需要构建不同类型的复合结构:对象、集合和map。ObjectSeed、ObjectListSeed、ValueListSeed类负责构建适当的对象、复合对象列表以及简单值的列表

基本的Seed接口继承了JsonObject,并在构建过程完成后提供了一个额外的spawn方法来获取生成的实例。它还声明了用于创建嵌套对象和嵌套列表的createCompositeProperty方法,它们使用相同的底层逻辑通过种子来创建实例

在这里插入图片描述

你可以认为spawn就是返回结果值的build方法的翻版。它返回的是为ObjectSeed构造的对象,以及为ObjectListSeed或ValueListSeed生成的列表。

在研究创建对象之前先来研究下deserialize的主要功能,它能完成反序列化一个值的所有操作

在这里插入图片描述

一开始会创建一个ObjectSeed来存储反序列化对象的属性,然后调用解析器并将输入字符流json传递给它。当达到输入数据的结尾时,你就可以调用spawn函数来构建最终对象

现在我们聚焦ObjectSeed的实现

在这里插入图片描述

它存储了正在构造的对象的状态。ObjectSeed接受了一个目标类的引用和一个classInfoCache对象,该对象包含缓存起来的关于该类属性的信息,这些缓存起来的信息稍后被用于创建该类的实例。

ObjectSeed构建了一个构造方法形参和它们的值之间的映射。这用到了两个可见的map:给简单值用的valueArguments和给复合属性用的seedArguments。当结果开始构建时,新的实参通过serSimpleProperty调用被添加到valueArguments,通过createCompositeProperty调用被添加到seedArguments。新的复合种子被添加时状态是空的,然后被来自输入流的数据填充,最终spawn方法递归地调用每个种子的spawn方法来构建所有嵌套的种子。注意spawn的方法体重arguments调用是怎样启动递归的复合(种子)实参的构建过程的:arguments自定义的getter调用seedArguments中每一个元素的spawn方法。createSeedForType函数分析形参的类型并根据形参是哪种集合来创建ObjectSeed、ObjectListSeed或者ValueListSeed。

反序列化最后一步:callBy()和使用反射创建对象

最后一步要理解的时ClassInfo类,它创建了作为结果的实例,还缓存了关于构造方法参数的实例,ObjectSeed用到了它。

首先研究通过反射来创建对象的API
之前的KCallable.call方法调用函数或者构造方法,并接收一个实参组成的列表。但它有一个限制:不支持默认参数值。这种情况下如果用户试图用带默认参数值的构造方法,绝对不想这些实参还要在JSON中说明,所以我们使用另一个支持默认参数值的方法:KCallable.callBy。这个方法接受一个形参和他们对应值之间的map,这个map将被作为参数传给这个方法。若map缺少一个形参,可行的话它的默认值将会被使用。你不必按照顺序写入形参,可以从JSON中读取名称-值的配对,找到每个实参名称对应的形参,把它写入map中。要注意取得正确的类型,map中值的类型需要跟构造方法的参数类型相匹配。你需要知道参数接受的是个什么类型,并把来自JSON的算术值转换成正确的类型,可以使用KParameter.type来做到

这里的类型转换通过ValueSerializer接口完成,这个接口和定制序列化时使用的是同一个

根据值类型取得序列化器

在这里插入图片描述

Boolean值的序列化器

在这里插入图片描述

callBy方法给了你一种调用一个对象的主构造方法的方式,需要传一个形参和对应值之间的map。ValueSerializer机制保证了map中的值拥有正确的类型

研究如何调用这个API
ClassInfoCache旨在减少反射操作的开销,@JsonName和@CustomSerializer是用在了属性上而不是形参上。当你反序列化一个对象时,你打交道的是构造方法参数,而不是属性。要获取注解你要先找到对应的属性,在读取每个(JSON)键值对的时候都执行一次这样的搜索会极其缓慢,所以每个类只会做一次这样的搜索并把信息缓存

缓存的反射数据的存储

在这里插入图片描述

这里在map中存储值的时候去掉了类型信息,但get方法的实现保证了返回的ClassInfo<T>拥有正确的类型实现。注意getOrPut的用法:如果mapcacheData已经包含了一个cls的值,你就返回这个值,否则调用传递进来的lambda,它会计算出这个键对应的值并存储到map中然后返回它

ClassInfo类负责按目标类创建新实例并缓存必要的信息。其代码中会抛出一个带有丰富信息的异常。

在这里插入图片描述

在初始化时,这段代码找到了每个构造方法参数对应的属性并取回了他们的注解。它把这些数据存储在三个map中:jsonNameToParamMap说明了JSON文件中的每个键对应的形参,paramToSerializerMap存储了每个形参对应的序列化器,还有jsonNameToDeserializeClassMap存储了指定为@DeserializeInterface注解的实参的类,如果有的话。然后ClassInfo就能根据属性名称提供构造方法的形参并调用使用形参的代码,这些代码中这个形参将作为形参和实参之间map的键使用

验证需要的参数被提供了

在这里插入图片描述

这个函数检查你是否提供了全部需要的参数的值。如果一个参数有默认值,那么param.isOptional时true,你就可以为它省略一个实参,如果一个参数类型是可空的(param.type.isMarkedNullable会告诉你),null将会被作为默认参数使用。对所有的形参来说你都必须提供对应的实参。反射缓存保证了只会搜索一次那些定制反序列化过程的注解,而不会为JSON数据中出现的每一个属性都执行搜索

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值