反射是一种在运行时动态访问对象属性和方法的方式,而不需事先确定这些属性是什么。一般来说当你访问一个对象的方法或者属性时,程序的源代码会因用一个具体的声明,编译器将静态解析这个引用并确保这个声明是存在的。但有时候你要编写能够使用任意类型的对象的代码,或者只能在运行时才能确定要访问的方法和属性的名称。例子:JSON序列化库要能够把任何对象都序列化成JSON,所以它不能引用具体的类和属性,这时可以使用反射
在kotlin使用反射时,会和两种不同的反射API打交道,一种是标准的java反射,定义在java.lang.reflect
中,因为kotlin类会被编译成普通的java字节码,java反射API可以完美支持它们。第二种是kotlin反射API,定义在kotlin.reflect
中,它让你能访问那些在java世界里不存在的概念如属性和可空类型。kotlin反射没有局限于kotlin类,你能够使用同样的API访问用任何JVM语言写成的类
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
,它包含了形参类型和返回类型的信息,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
,其中第一个类型参数表示接收者的类型,第二个类型参数代表了属性的类型。这样你只能对正确类型的接收者调用它的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。
最后一步要理解的时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
拥有正确的类型实现。注意getOrPut
的用法:如果mapcacheData已经包含了一个cls的值,你就返回这个值,否则调用传递进来的lambda,它会计算出这个键对应的值并存储到map中然后返回它
ClassInfo
类负责按目标类创建新实例并缓存必要的信息。其代码中会抛出一个带有丰富信息的异常。
在初始化时,这段代码找到了每个构造方法参数对应的属性并取回了他们的注解。它把这些数据存储在三个map中:jsonNameToParamMap
说明了JSON文件中的每个键对应的形参,paramToSerializerMap
存储了每个形参对应的序列化器,还有jsonNameToDeserializeClassMap
存储了指定为@DeserializeInterface注解的实参的类,如果有的话。然后ClassInfo就能根据属性名称提供构造方法的形参并调用使用形参的代码,这些代码中这个形参将作为形参和实参之间map的键使用
验证需要的参数被提供了
这个函数检查你是否提供了全部需要的参数的值。如果一个参数有默认值,那么param.isOptional
时true,你就可以为它省略一个实参,如果一个参数类型是可空的(param.type.isMarkedNullable
会告诉你),null将会被作为默认参数使用。对所有的形参来说你都必须提供对应的实参。反射缓存保证了只会搜索一次那些定制反序列化过程的注解,而不会为JSON数据中出现的每一个属性都执行搜索
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)