¶主要内容
- 注解的使用和定义
- 基于运行时的反射对类进行自省(introspect)
- Kotlin项目的真实例子
kotlin的注解的使用和Java极其相同,然而自定义注解类的声明语法却天壤之别。同样地,反射的API结构和Java也类似,但细节却不同。
¶Declaring and applying annotations
注解允许你以声明的方式关联额外的元数据(metadata),元数据可以在源文件被编译时,或再运行时被访问。
¶Applying annotations
Kotlin的注解使用方式和Java一样,使用@
前缀标识。例如JUnit单元测试,
1 | import org.junit.* |
一个更有趣的例子,让我们看看@Deprecated
注解,它在Kotlin中的意义和Java一样,但添加了replaceWith
参数进行增强,提供了替换模式让你平滑地过渡到新的API。用法如下,
1 |
|
参数由括号中传递,和常规函数一样调用。带这种声明,当你使用到remove
函数时,IntelliJ IDEA不仅会告诉你使用替换为removeAt
,还支持quick fix选项自动替换。
注解的参数仅能包含如下类型:原生类型(primitive type)、字符串(string)、枚举(enum)、类引用(class reference)、其它注解类、以及数组。注解参数的指定和Java有明显不同:
- 将类指定为入参,类名后带
::java
。例如,@MyAnnotation(MyClass::class)
。 - 其它注解作为入参,不带
@
。例如前面的ReplaceWith
注解。 - 数组作为入参,使用
arrayOf
。例如,@RequestMapping(path = arrayOf("/foo", "/bar"))
。如果注解是声明在Java中的,参数为value
的入参是自动转换的,参数名arrayOf
可以声明。
注解的参数是依赖于运行时实现的,所以不能引用字面量的属性(property)作为入参。如果要这样做,需要使用const
标记,让编译器知道该属性是编译期常量(compile-time const)。例如,
1 | const val TEST_TIMEOUT = 100L |
并且记住,带有const
的属性注解必须声明在object
对象文件的top-level并且必须初始化其值为原生类型或String
类型。如果尝试使用常规的属性作为注解的入参,会出现"Only ‘const val’ can be used in constant expressions"的错误。
¶Annotation targets
多数情况下,Kotlin中的单一的声明对应于Java的多个声明,每个都可以带有注解。例如,Kotlin的属性(Property)对应java的字段(Field)、getter和可能有的带参数的setter。属性声明在第一构造函数会有多于一个的构造参数。因此,有必要指定哪些元素需要被注解。
指定元素被注解使用 use-site target 声明。使用@
标记+:
+注解名的形式,
get
触发@Rule
的注解于属性的getter方法实现上。
下面看一个使用注解的例子。在JUnit,你可以指定每个测试方法前执行rule。例如,标准的TemporaryFolder
rule被用于创建执行测试前的文件或文件夹、以及测试后删除临时文件。
在Java中,指定执行JUnit规则的方法是在声明的字段或方法前注解@Rule
。但在Kotlin中,因为不用注解字面量属性folder
,如果带上@Rule
,会出现JUnit异常:“The @Rule
‘folder’ must be public.”。因为@Rule
作用于字段,默认是private的。要作用于getter,你需要显式编写:
1 | class HasTemFolder { |
默认,使用Java的注解作用一个属性,它作用的是对应的字段。Kotlin允许你声明注解以直接作用于属性。
下面列出了use-site target的支持
property
—— Java注解不能作用于use-site目标。field
——属性生成的字段。get
——属性的getter。set
——属性的getter。receiver
——函数或属性的扩展接收参数。param
——构造器参数。setparam
——属性的setter的参数。delegate
——委派属性对应字段存储的委派实例。file
——声明在文件中的类所包含的top-level的函数和属性。
任何作用于file
target的注解,需要放置在文件的top-level,在package
之前。最常见的注解就是@JvmName
,它用于修改对应类编译后生成的字节类名。例如前面章节的例子,@file:JvmName("StringFunctions")
。
注意不同于Java,kotlin允许注解作用于任意表达式,不仅仅作用于类或声明函数或类型。最常见的例子是@Supress
注解,用于编译器抑制编译器的告警。下面是一个抑制转换检查的例子:
1 | fun test(list: List<*>) { |
注意IntelliJ IDEA编辑器可以使用Alt-Enter快捷键快速选择Suppress选项。
Controlling the Java API with annotations
kotlin提供了大量的注解来控制如何将kotlin的声明编译为Java的字节码以暴露给Java调用方。某些注解替换了Java对应的关键字:例如,
@Volatile
和@Strictfp
注解直接替换为Java的volatile
和strictfp
关键字。另外一些则被用于更改kotlin在Java调用方的可见性:
@JvmName
修改来自kotlin声明所生成的Java方法的方法名和字段名。@JvmStatic
作用于一个对象或伴生对象的方法声明,将其保留为Java的静态方法。@JvmOverloads
指示Kotlin编译器为一个带有默认参数值的函数生成重载方法。@JvmField
作用于一个属性,将其保留为public 的Java字段,而不带getter或setter。
¶Using annotations to customize JSON serialization
注解的一个经典例子是自定义对象序列化。Serialization 是指将一个对象转换为一个二进制或文本以用于存储或在网络传输的一种处理。相反的方式,deserialization , 反序列化则是将这些表述性转换为一个对象。最常见的形式是序列化JSON。有非常宽广的库用于序列化Java对象为JSON。包括 Jackson 和 GSON 。和其它标准库一样,它们完全兼容Kotlin。
这里我们将讨论纯Kotlin版的序列化库,叫做jKid。它足够小以更容易阅读源码。
The JKid library source code and exercises
JKid 源码有大量值得借鉴的经验。
让我们以一个最简单的例子开始:序列化和反序列化对象实例Person
。将实例传入serialize
函数,并返回JSON字符串:
1 | data class Person(val name: String, val age: Int) |
一个对象的JSON表述(representation)包含有键值对:属性名作为key、属性值作为value,例如"age": 29。
要将JSON表述转换为一个对象,你可以调用deserialize
函数:
1 | >>> val json = """{"name": "Alice", "age": 29}""" |
当你从JSON数据创建一个实例,你必须显式指定类作为类型参数,因为JSON不知道存储的是什么对象类型。在这里,传递了Person
类。
下图描绘了JSON表述和对象实例的等效性。注意序列化的对象可能不仅仅包含原生类型或字符串,也可能包含其它集合类型或对象类。
你可以使用注解来自定义序列化和反序列化。当序列化一个实例为JSON时,默认该库是序列化所有的属性并使用属性名作为keys。注解允许修改它的默认行为。这里将讨论两个注解,@JsonExclude
和@JsonName
,本章后面会介绍它的实现细节:
@JsonExclude
注解用于标记属性应该从序列化和反序列化排除。@JsonName
用于指定表述属性的key/value中的key的字符串,不是属性的名字。
考虑如下例子:
1 | data class Person ( |
注解属性firstName
的key的值用于表述JSON。注解另一个属性age
将其从序列化和反序列化排除。注意你必须指定属性age
的一个默认值。否则在反序列化是无法创建Person
的一个新的实例。下图是其序列化和发序列化的过程:
你已经看到了JKid的大部分特性:serialize()
、deserialize()
、@JsonName
和@JsonExclude
。现在让我们探索其实现,从注解声明开始。
¶Declaring annotations
以JKid的注解为例。@JsonExclude
注解有最简单的形式,因为它没有任何参数:
1 | annotation class JsonExclude |
该语法和常规类的声明一样,添加annotation
修改器在class
关键字之前。因为注解类仅仅被用于定义元数据关联的声明和表达式的结构,它们不能包含任何代码。因此,编译器禁止指定一个注解类的语句体。
对于注解类包含参数的,参数声明在类的第一构造函数:
1 | annotation class JsonName(val name: String) |
这里使用了常规的第一构造函数声明语法。关键字val
对于一个注解类的所有参数是强制性的(mandatory)。
作为比较,下面是在Java中同样的声明写法:
1 | public JsonName { |
注意Java的注解有一个方法叫value
,同时kotlin注解有一个属性name
。value
方法在Java中是特殊的:当你引用注解时,需要为value
作用的属性显式地提供名字。另外,在kotlin一个注解作用是对常规构造函数的调用。你可以使用命名参数(named-syntax)的语法显式指定,或者省略它们:@JsonName(name = "first_name")
等同于 @JsonName("first_name")
,因为name
是JsonName
构造函数的第一个参数。如果你需要将Java中声明的注解作用到kotlin的元素,然而,你会被要求对所有参数,使用命名参数(named-syntax)语法;唯一例外的是value
,因为kotlin也可以特殊地识别。
¶Meta-annotations: controlling how an annotation is processed
和Java一样,kotlin的注解也可以被注解。这种注解作用于另外一个注解类的注解,称为 meta-annotations 。编译器定义了几个,它们控制了编译器如何处理注解。其它一些框架也使用了元注解——例如,许多注入型的依赖库使用了元注解来标记该注解是被用于同一类型的不同对象实例。
元注解被定义在标准库,常见的有@Target
。JKid中定义的@JsonExclude
和JsonName
使用了它来指定生效的目标。下面是它如何使用的:
1 |
|
元注解@Target
指定了注解所作用的目标类型。如果你不用它,该注解会作用到所有的声明上。对于JKid来说没任何意义,因为该库仅处理属性注解。
枚举AnnotationTarget
列出了一个注解的所有可能的范围。其中包含类、文件、函数、属性、属性访问器、类型、表达式等等。同一目标可以定义多个:@Target(AnnotationTarget.CLASS, AnnotationTarget.METHOD)
。
要声明自定义元注解,使用ANNOTATION_CLASS
作为它的目标:
1 |
|
注意,你不能使用来自Java的PROPERTY
属性;为了令其生效,你可以添加第二个目标AnnotationTarget.FIELD
。这种情况下,注解既作用于Kotlin的属性,也作用于Java的字段。
** The @Retention annotation**
Retention: 持有,存储。
在Java有另外一个重要的元注解:@Retention
。使用它可以决定注解是否应该被存储在.class文件中,还是在运行期由反射实现。Java的注解默认由.class文件持有,不允许它们在运行期访问。大部分注解不需要在运行期提供,在Kotlin中的默认行为不同:注解提供RUNTIME
持有。因此JKid不会有显式的指定。
¶Classes as annotation parameters
某些场合需要用到类作为注解参数的情形:从声明的元数据中获得对一个类的引用。在JKid库就有这样的一个注解@DeserializeInterface
,在接口层面上控制属性的反序列化。因为接口的实例不能直接创建,在反序列化时需要指定哪个类作为反序列化。
1 | interface Company { |
JKid从一个Person
实例读取内嵌的company
对象时,会创建CompanyImpl
的反序列化实例并存储在company
属性上。为此,需要使用@DeserializeInterface
注解来指定反序列化的参数类型CompanyImpl::class
。通常,对类型的引用,使用类型后跟::class
关键字的形式表述。
下面是它的声明。
1 | annotation interface DeserializeInterface(val targetClass: KClass<out Any>) |
kotlin中的KClass
对应于Java的java.lang.Class
类型。它用于持有kotlin类的引用。
KClass
的类型参数指定了哪些kotlin类可以被引用。例如,CompanyImpl::class
有类型KClass<CompanyImpl>
,它是注解类型参数的子类型(参考协变性内容)。
![Figure 10.3](/img/kotlin-in-action/chapter10/Figure_10_03.png]
如果KClass<Any>
不带out
修改器,则不能传递CompanyImpl::class
作为一个参数:仅允许传递Any::class
。out
关键字指定了仅被允许传入继承了Any
的类,而不是Any
自身。
¶Generic classes as annotation parameters
默认,JKid将非原生类型属性作为内嵌对象进行序列化。不过你可以更改这种行为,提供自定义的序列化逻辑。
注解@CustomSerializer
接收一个自定义序列化类作为参数。该序列化类应该实现ValueSerializer
接口:
1 | interface ValueSerializer { |
假设你要对日期进行序列化,并为此创建了DateSerializer
,实现了ValueSerializer<Date>
接口。下面是如何作用到Person
类:
1 | data class Person( |
下面是@CustomSerializer
注解的声明,
1 | annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>) |
你需要确保注解仅引用了实现了ValueSerializer
接口的类。例如,编写@CustomSerializer(Date::class)
应该被禁止使用。因为Date
并没有实现ValueSerializer
接口。
¶Reflection: introspecting Kotlin objects at runtime
反射是一种在运行时访问对象属性和方法的一种动态方式。
Kotlin对反射提供有两种API处理。一种是Java的标准库中的java.lang.reflect
。第二种是Kotlin标准库中的kotlin.reflect
。第二种是Java中没有的概念,诸如属性和可空类型。但它不提供对Java反射的综合性替换。另外,kotlin的反射API是基于JVM的,不强制用于kotlin的类,可用于任何基于JVM的语言。
注意:为了减少平台的运行库的大小,特别是Android,Kotlin的反射API单独分离为一个jar文件,kotlin-reflect.jar。默认不会添加到项目中,你需要自行添加org.jetbrains.ktolin:kotlin-reflect
。
¶The Kotlin reflection API: KClass, KCallable, KFunction, and KProperty
Kotlin反射API的主要入口是KClass
,代表一个类。KClass
对应于java.lang.Class
,你可以用它枚举或访问所有在该类的声明、子类等。你可以通过MyClass::class
获得一个KClass
的实例。要在运行时获得类,首先你可以使用javaClass
属性获得它的Java类,等价于java.lang.Object.getClass()
。然后再由.kotlin
扩展将Java的反射转换为Kotlin的反射:
1 | class Person(val name: String, val age: Int) |
KClass
类包含大量的方法用于访问类的上下文内容:
1 | interface KClass<T : Any> { |
KClass
的许多有用的特性,包括前面用的memberProperties
,被声明为扩展(extensions)。
KCallable
是所有函数和属性的超级接口(superinterface)。它声明了call
方法,允许你属性的对应函数或getter。
1 | interface KCallable<out R> { |
下面的例子证明了如何通过反射用call
来调用一个函数:
1 | fun foo(x: Int) = println(x) |
这个表达式是KFunction
类的一个反射的实例。要调用引用的函数,使用KCallable.call
方法。在这里,你需要提供唯一的入参,42。如果尝试调用错误数量的参数,譬如kFunction.call
,会抛出一个运行时错误:“IllegalArgumentException: Callable expects 1 arguments, but 0 were provided.”。
然而,这个例子可以使用一个更特定的方法来调用函数。:foo
表达式的类型是KFunction1<Int, Unit>
,包含了有关于参数和返回类型的信息。这里的1
表示函数接收一个参数。要从该接口调用函数,使用invoke
方法。它接收固定的参数数量,它的参数类型对应于KFunction1
接口的类型参数。你也可以直接调用kFunction
:
1 | import kotlin.reflect.KFunction2 |
现在我们用不正确的参数个数调用了kFunction
的invoke
方法:不会编译。因此,如果KFunction
有一个指定的类型,带有参数和返回类型,更好用invoke
方法来使用它。call
方法是一个通用的对所有函数的调用方式,但不保证类型安全。
How and where are KFunctionN defined
诸如
KFunction1
类型表示函数有不同的参数个数。每个继承KFunction
的类型添加一个额外的invoke
成员参数。例如,KFunction2
声明了operator fun invoke(p1: P1, p2: P2): R
。其中R1
和R2
表示了函数的参数类型,以及R
表示返回类型。这些函数类型是编译生成类型(compiler-generated types)所合成的,以及你不能在
kotlin.reflect
包里面找到它的声明。意味着你可以为一个任意参数个数的函数使用接口。这种合成类型实现了减少kotlin-runtime.jar的大小以及避免的函数类型参数的
人为限制。
你也可以在KProperty
实例调用call
方法,它将调用属性的getter。但属性的接口提供了更合适的值获取方式:get
方法。
要访问get
方法,你需要使用合适的接口,取决于它如何声明。top-level属性对应于KProperty0
接口,有个无参的get
方法:
1 | var counter = 0 |
一个 member property 由KProperty1
的一个实例表示,只有一个get
方法。要访问它的值,你必须提供对象的实例。下面例子存储了一个属性的变量在memberProperty
;然后你调用memberProperty.call(person)
来获得属性对应person
实例的值。因此如果一个memberProperty
引用的是Person
类里面的age
属性,memberProperty.call(person)
则是动态地获取person.age
的值:
1 | class Person(val name: String, val age: Int) |
注意,KProperty1
是一个泛型类。memberProperty
变量的类型是KProperty<Person, Int>
,其中第一个参数表示接收类型,第二个参数表示属性类型。因此,你可以调用它的get
方法,仅在接收类型正确的情况。调用memberProperty.get("Alice")
则不能编译。
也要注意,仅能用反射访问那些定义在top-level或类里面的属性,对于函数内部的属性是无法访问。如果你定义一个本地变量x
,尝试去获取它的::x
引用,将会得到一个编译错误“References to variables aren’t supported yet”。
下图展示了这些接口的层级关系。因为所有声明可以被注解,接口表示运行时的声明,诸如KClass
、KFunction
以及KParameter
,都是继承自KAnnotatedElement
。KClass
被用于表述类或对象。KProperty
被用表述任何属性,对于它的子类,KMutableProperty
,表示一个可变属性,即用var
声明的。你可以用声明在KProperty
和KMutableProperty
的特定接口Getter和Setter和属性访问器一起作为函数使用——例如,你需要检索所有它的注解。两个访问器的接口都继承KFunction
。为了简化,我们省略了某些特定的诸如KProperty0
的接口。
¶Implementing object serialization using reflection
首先,让我们回顾声明在JKid的序列化函数。
1 | fun serialize(obj: Any): String |
该函数接收一个对象并以字符串返回它的JSON表述。它会由一个StringBuilder
实例构建JSON。因为它的序列化属性和值将被append到这个StringBuilder
对象中。为了使append
调用更加简洁,该实现将被放在StringBuilder
的扩展函数中。这样,你可以方便地调用append
方法而不需要限定:
1 | private fun StringBuilder.serializeObject(x: Any) { |
将一个函数参数转换为一个扩展函数的receiver在kotlin中是一个常规做法。注意serializeObject
不继承StringBuilder
的API。因为在上下文外部处理操作是无意义的,因此使用private
确保它不被其它地方使用。
因此,serialize
函数将所有工作委派给了serializeObject
来处理:
1 | fun serialize(obj: Any): String = buildString { serializeObject(obj) } |
前面介绍过,buildString
创建一个StringBuilder
并以一个lambda对其内容进行填充。这里的内容是serializeObject(obj)
。
现在讨论一下序列化函数的行为。默认它会序列化对象的所有属性。原生类型和字符串将被序列化为合适的JSON 数字、布尔值和字符串。集合会被序列化为JSON数组。其它类型的属性将被序列化为内嵌对象。前面讨论到,这种行为可以通过注解来自定义。
下面看其serializeObject
的实现,
1 | private fun StringBuilder.serializeObject(obj: Any) { |
该函数的实现清晰:一个接一个地序列化类的每个属性。JSON结果看起来会是:{ prop1: value1, prop2: value2 }
。函数joinToStringBuilder
确保了属性被冒号分隔。并避开了JSON格式的特殊字符。serializePropertyValue
函数会检查值是否为原生值,字符串,集合或内嵌对象,对应地序列化其内容。
前面小节讨论过获取KProperty
实例的值的方式:get
方法。这里,工作在Person::age
对成员的引用KProperty1<Person, Int>
类型,可以让编译器知道额外的receiver的类型和属性类型。在这个例子,然而,额外的类型是未知的,因为你枚举的一个对象类的所有属性。因此prop
变量包含有类型KProperty1<Any, *>
,以及prop.get(obj)
返回Any
类型。你不会得到任何编译期的类型检查,但因为你每次传递获得的属性列表都是一样的,接收类型可以被正确处理。
¶Customizing serialization with annotations
前面介绍了一些注解的声明语法,譬如@JsonExclude
、@JsonName
和@CustomSerializer
注解。现在让我们看看这些注解是如何被serializeObject
函数处理的。
我们将从@JsonExclude
开始。该注解允许你在序列化中排除某些属性。让我们探索下serializeObject
如何支持。
回顾一下要获得一个类的所有成员属性,使用KClass
实例的memberProperties
扩展。但现在任务变得更加复杂:带有@JsonExclude
注解的属性需要被过滤掉。让我们看看它如何实现。
KAnnotatedElement
接口定义了属性annotations
,运行期保留(runtime retention)的获取源码所有注解的集合实例。因为KProperty
继承自KAnnotatedElement
,你可以通过property.annoations
访问一个属性的所有注解。
但这里过滤的不是所有注解;需要过滤特定的一个,帮助函数findAnnotation
做了这项工作:
1 | inline fun <reified T> KAnnoatedElement.findAnnotation(): T? = annoations.filterIsInstance<T>().firstOrNull() |
findAnnotation
返回一个指定入参类型的一个注解实例。reified
将类型具现化。
现在可以用findAnnotation
和标准库中的filter
一起过滤掉@JsonExclude
的属性了:
1 | val properties = kClass.memberProperties.filter { it.findAnnotation<JsonExclude>() == null } |
下一个注解是@JsonName
。作为提醒,我们重复一下它的声明和用法:
1 | annoation class JsonName(val name: String) |
注解里面的name将被用于JSON。使用findAnnotation
来处理:
1 | val jsonNameAnn = prop.findAnnoation<JsonName>() // Gets an instance of the @JsonName annotation if it exists |
如果一个属性没有被@JsonName
注解,那么jsonNameAnn
为null
,仍然使用prop.name
作为JSON序列化的name部分,否则使用指定的name。
下面是其完整实现,
1 | private fun StringBuilder.serializeObject(obj: Any) { |
现在带有@JsonExclude
注解的属性被过滤掉了。并提取了对应序列化属性的逻辑,
1 | private fun StringBuilder.serializeProperty(prop: KProperty1<Any, *>, obj: Any) { |
接下来,实现剩余的注解,@CustomSerializer
。它的实现基于getSerializer
函数。它会返回通过@CustomSerializer
注解注册了的ValueSerializer
实例。例如,
1 | data class Person( |
下面是提醒@CustomSerializer
注解是如何声明的,
1 | annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>) |
以及getSerializer
函数的实现,
1 | fun KProperty<*> getSerializer(): ValueSerializer<Any?>? { |
它是KProperty
的一个扩展函数,因为属性是由方法处理的第一个对象。它调用了findAnnotation
函数来获取@CustomSerializer
注解的一个实例。参数部分serializerClass
指定了需要获取对象实例的类型。
这里最有趣的部分是你可以处理类和对象(kotlin的单例),通过@CustomSerializer
注解获得值。他们都由KClass
类表述。不同的是对象拥有非空的objectInstance
属性值,用于访问由object
创建的单例实例。例如,DateSerializer
被声明为一个object
,因此它的objectInstance
属性被存储哎单例的DateSerializer
实例。你可以使用该实例序列化所有对象,但createInstance
不能被调用。
如果KClass
代表的是常规类,则可以使用createInstance
来创建一个新的类实例。该函数类似于java.lang.Class.newInstance
。
下面是serializeProperty
的最终版本实例:
1 | private fun StringBuilder.serializeProperty(prop: KProperty1<Any, *>, obj: Any) { |
serializeProperty
使用序列化toJsonValue
将属性值转换为JSON兼容格式。如果属性没有自定义toJsonValue
,将使用属性值替代。
¶JSON parsing and object deserialization
下面开始第二个环节:实现反序列化逻辑。首先,回顾API,和序列化用法一样,
1 | inline fun <reified T: Any> deserialize(json: String): T |
下面是它的用例:
1 | data class Author(val name: String) |
对象的类型作为一个reified类型参数传递给反序列化函数deserialize
进行反序列化,并返回一个新的对象实例。
JSON的反序列化比序列化更加难实现。因为它涉及到JSON字符串输入额外使用反射访问对象内部。JKid的JSON反序列化完全以惯例约定的方式考量:词法分析,通常认为是一个 lexer ;语法解析 parser ;以及反序列组件自身。
词法分析过程将输入字符串分解为若干的token。包括两种token:character token ,代表了JSON语法的特殊意义(逗号、冒号、中括号和花括号);value token ,代表了对应的字符串、数字、布尔、以及null
常量。一个左花括号({),一个字符串值(“Catch-22”),和一个整型值(22)代表不同的token信息。
语法解析通常职责是用于将一些了的token转换为相应的数据结构。在JKid中为了推断出JSON的高阶结构,各自地将token信息转换为JSON所支持的语义元素:键值对、对象、数组。
JsonObject
接口会跟踪当前反序列化的对象或数组。当发现当前对象新的属性时(值、组合属性、或数组),解析器调用对应的方法。
1 | interface JsonObject { |
propertyName
参数在该方法接收JSON的key。因此,当解析器遇到一个带有author
属性的对象作为它的值时,将会调用createObject("author")
方法。
下图显示了反序列化字符串每一步中的词法和语法分析。再一次说明,词法分析将输入字符串拆分为token列表;然后语法分析(parser)处理这些token列表中每一个元素调用JsonObject
一个适当的方法。
反序列器为JsonObject
实现提供对应类型的一个新实例的构建。它需要从JSON key(title
,author
,和name
)和类属性之间查找,从而构建内嵌的对象值(实例Author
);之后才能创建需求的实例(Book
)。
JKid使用了传统的Builder pattern模式,使用不同的builder来生成不同的对象实例。在JSON中需要构建不同的组合结构类型:对象、集合、映射。这些类包括有ObjectSeed
、ObjectListSeed
和ValueListSeed
。
基础类Seed
继承了JsonObject
接口并提供了一个额外的spawn
方法,用于获取builder处理完成的实例。另外还声明了createCompositeProperty
方法,用于创建内嵌对象和内嵌列表。
1 | interface Seed: JsonObject { |
你可以认为spawn
是build
的类似物——一个方法返回结果值。它为ObjectSeed
返回结构化对象,产生ObjectListSeed
或ValueListSeed
的结果列表。姑且不讨论它的实现细节。
在此之前,先学习一下主要的deserialize
函数,对值进行反序列化。
1 | fun <T: Any> deserialize(json: Reader, targetClass: KClass<T>): T { |
要开始分析,先创建一个存储了对象属性的一个ObjectSeed
对象,它将被序列化,然后调用解析器将json
以stream reader的形式传入。一旦读取到了字节流的末尾,即调用spawn
函数来构建输出结果。
现在先聚焦在ObjectSeed
的实现上,它存储了一个对象被结构化的状态。ObjectSeed
的第一构造函数包含了两个引用,一个是产生结果的类,另一个classInfoCache
对象包含该类属性的缓存信息。这些缓存信息最终被被构建为该类的实例。ClassInfoCache
和ClassInfo
是帮助类。
1 | class ObjectSeed<out T : Any>( |
ObjectSeed
在第一构造函数部分通过入参将属性对象存储在map中。包含两个可变的map:valueArguments
用于存储简单的属性,seedArguments
存储组合属性。简单属性通过调用setSimpleProperty
方法存储在valueArguments
,而组合属性则通过createCompositeProperty
存储在seedArguments
中。最后再通过spawn
方法,递归地将组合属性作为入参构建内嵌的实例。
¶Final deserialization step: callBy()
and creating objects using reflection
KCallable.call
方法提供了一种反射机制来构建实例。实际的入参作为对应类的构造函数的入参。因为不支持默认参数值。需要使用另外一个方法:
1 | interface KCallable<out R> { |
(略…)
¶Summary
Kotlin 的反射特性并没有创新,因为也是基于jvm的运行时。仅仅在原来Java API上进行了一层封装。严格来说,当你的程序需要用到反射的实现的时候,基本离重构的日子已经不远了。