注解和反射

主要内容

  1. 注解的使用和定义
  2. 基于运行时的反射对类进行自省(introspect)
  3. Kotlin项目的真实例子

kotlin的注解的使用和Java极其相同,然而自定义注解类的声明语法却天壤之别。同样地,反射的API结构和Java也类似,但细节却不同。

Declaring and applying annotations

注解允许你以声明的方式关联额外的元数据(metadata),元数据可以在源文件被编译时,或再运行时被访问。

Applying annotations

Kotlin的注解使用方式和Java一样,使用@前缀标识。例如JUnit单元测试,

1
2
3
4
5
6
7
import org.junit.*

class MyTest {
@Test fun testTrue() { // The @Test annotation instructs the JUnit framework to invoke this method as a test.
Assert.assertTrue(true)
}
}

一个更有趣的例子,让我们看看@Deprecated注解,它在Kotlin中的意义和Java一样,但添加了replaceWith参数进行增强,提供了替换模式让你平滑地过渡到新的API。用法如下,

1
2
@Deprecated("Use removeAt(index) instead.", ReplaceWith("removeAt(index)"))
fun remove(index: Int) { ... }

参数由括号中传递,和常规函数一样调用。带这种声明,当你使用到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
2
3
const val TEST_TIMEOUT = 100L

@Test(timeout = TEST_TIMEOUT) fun testMethod() { ... }

并且记住,带有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 声明。使用@标记+:+注解名的形式,

Figure 10.1

get触发@Rule的注解于属性的getter方法实现上。

下面看一个使用注解的例子。在JUnit,你可以指定每个测试方法前执行rule。例如,标准的TemporaryFolderrule被用于创建执行测试前的文件或文件夹、以及测试后删除临时文件。

在Java中,指定执行JUnit规则的方法是在声明的字段或方法前注解@Rule。但在Kotlin中,因为不用注解字面量属性folder,如果带上@Rule,会出现JUnit异常:“The @Rule ‘folder’ must be public.”。因为@Rule作用于字段,默认是private的。要作用于getter,你需要显式编写:

1
2
3
4
5
6
7
8
9
10
11
class HasTemFolder {
@get:Rule // The getter is annotated, not the property
val folder = TemporaryFolder()

@Test
fun testUsingTempFolder() {
val createdFile = folder.newFile("myfile.txt")
val createdFolder = folder.newFolder("subfolder")
...
}
}

默认,使用Java的注解作用一个属性,它作用的是对应的字段。Kotlin允许你声明注解以直接作用于属性。

下面列出了use-site target的支持

  • property—— Java注解不能作用于use-site目标。
  • field——属性生成的字段。
  • get——属性的getter。
  • set——属性的getter。
  • receiver——函数或属性的扩展接收参数。
  • param——构造器参数。
  • setparam——属性的setter的参数。
  • delegate——委派属性对应字段存储的委派实例。
  • file——声明在文件中的类所包含的top-level的函数和属性。

任何作用于filetarget的注解,需要放置在文件的top-level,在package之前。最常见的注解就是@JvmName,它用于修改对应类编译后生成的字节类名。例如前面章节的例子,@file:JvmName("StringFunctions")

注意不同于Java,kotlin允许注解作用于任意表达式,不仅仅作用于类或声明函数或类型。最常见的例子是@Supress注解,用于编译器抑制编译器的告警。下面是一个抑制转换检查的例子:

1
2
3
4
5
fun test(list: List<*>) {
@Suppress("UNCHECKED_CAST")
val strings = list as List<String>
// ...
}

注意IntelliJ IDEA编辑器可以使用Alt-Enter快捷键快速选择Suppress选项。

Controlling the Java API with annotations

kotlin提供了大量的注解来控制如何将kotlin的声明编译为Java的字节码以暴露给Java调用方。某些注解替换了Java对应的关键字:例如,@Volatile@Strictfp 注解直接替换为Java的volatilestrictfp关键字。另外一些则被用于更改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。包括 JacksonGSON 。和其它标准库一样,它们完全兼容Kotlin。

这里我们将讨论纯Kotlin版的序列化库,叫做jKid。它足够小以更容易阅读源码。

The JKid library source code and exercises

JKid 源码有大量值得借鉴的经验。

让我们以一个最简单的例子开始:序列化和反序列化对象实例Person。将实例传入serialize函数,并返回JSON字符串:

1
2
3
4
5
data class Person(val name: String, val age: Int)

>>> val person = Person("Alice", 29)
>>> println(serialize(person))
["age": 29, "name": "Alice"]

一个对象的JSON表述(representation)包含有键值对:属性名作为key、属性值作为value,例如"age": 29。

要将JSON表述转换为一个对象,你可以调用deserialize函数:

1
2
3
>>> val json = """{"name": "Alice", "age": 29}"""
>>> println(deserialize<Person>(json))
Person(name=Alice, age=29)

当你从JSON数据创建一个实例,你必须显式指定类作为类型参数,因为JSON不知道存储的是什么对象类型。在这里,传递了Person类。

下图描绘了JSON表述和对象实例的等效性。注意序列化的对象可能不仅仅包含原生类型或字符串,也可能包含其它集合类型或对象类。

Figure 10.2

你可以使用注解来自定义序列化和反序列化。当序列化一个实例为JSON时,默认该库是序列化所有的属性并使用属性名作为keys。注解允许修改它的默认行为。这里将讨论两个注解,@JsonExclude@JsonName,本章后面会介绍它的实现细节:

  • @JsonExclude 注解用于标记属性应该从序列化和反序列化排除。
  • @JsonName 用于指定表述属性的key/value中的key的字符串,不是属性的名字。

考虑如下例子:

1
2
3
4
data class Person (
@JsonName("alias") val firstName: String,
@JsonExclude val age: Int? = null
)

注解属性firstName的key的值用于表述JSON。注解另一个属性age将其从序列化和反序列化排除。注意你必须指定属性age的一个默认值。否则在反序列化是无法创建Person的一个新的实例。下图是其序列化和发序列化的过程:

Figure 10.3

你已经看到了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
2
3
public @interface JsonName {
String value();
}

注意Java的注解有一个方法叫value,同时kotlin注解有一个属性namevalue方法在Java中是特殊的:当你引用注解时,需要为value作用的属性显式地提供名字。另外,在kotlin一个注解作用是对常规构造函数的调用。你可以使用命名参数(named-syntax)的语法显式指定,或者省略它们:@JsonName(name = "first_name") 等同于 @JsonName("first_name"),因为nameJsonName构造函数的第一个参数。如果你需要将Java中声明的注解作用到kotlin的元素,然而,你会被要求对所有参数,使用命名参数(named-syntax)语法;唯一例外的是value,因为kotlin也可以特殊地识别。

Meta-annotations: controlling how an annotation is processed

和Java一样,kotlin的注解也可以被注解。这种注解作用于另外一个注解类的注解,称为 meta-annotations 。编译器定义了几个,它们控制了编译器如何处理注解。其它一些框架也使用了元注解——例如,许多注入型的依赖库使用了元注解来标记该注解是被用于同一类型的不同对象实例。

元注解被定义在标准库,常见的有@Target。JKid中定义的@JsonExcludeJsonName使用了它来指定生效的目标。下面是它如何使用的:

1
2
@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

元注解@Target指定了注解所作用的目标类型。如果你不用它,该注解会作用到所有的声明上。对于JKid来说没任何意义,因为该库仅处理属性注解。

枚举AnnotationTarget列出了一个注解的所有可能的范围。其中包含类、文件、函数、属性、属性访问器、类型、表达式等等。同一目标可以定义多个:@Target(AnnotationTarget.CLASS, AnnotationTarget.METHOD)

要声明自定义元注解,使用ANNOTATION_CLASS作为它的目标:

1
2
3
4
5
@target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation

@BindingAnnotation
annotation class MyBinding

注意,你不能使用来自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
2
3
4
5
6
7
8
9
10
interface Company {
val name: String
}

data class CompanyImpl(override val name: String): Company

data class Person(
val name: String,
@DeserializeInterface(CompanyImpl::class) val company: 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::classout关键字指定了仅被允许传入继承了Any的类,而不是Any自身。

Generic classes as annotation parameters

默认,JKid将非原生类型属性作为内嵌对象进行序列化。不过你可以更改这种行为,提供自定义的序列化逻辑。

注解@CustomSerializer接收一个自定义序列化类作为参数。该序列化类应该实现ValueSerializer接口:

1
2
3
4
interface ValueSerializer {
fun toJsonValue(value: T): Any?
fun fromJsonValue(jsonValue: Any?): T
}

假设你要对日期进行序列化,并为此创建了DateSerializer,实现了ValueSerializer<Date>接口。下面是如何作用到Person类:

1
2
3
4
data class Person(
val name String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)

下面是@CustomSerializer注解的声明,

1
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

你需要确保注解仅引用了实现了ValueSerializer接口的类。例如,编写@CustomSerializer(Date::class)应该被禁止使用。因为Date并没有实现ValueSerializer接口。

Figure 10.5

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
2
3
4
5
6
7
8
9
class Person(val name: String, val age: Int)

>>> val person = Person("Alice", 29)
>>> val kClass = person.javaClass.kotlin // Return an instance of KClass<Person>
>>> println(kClass.simpleName)
Person
>>> kClass.memberProperties.forEach { println(it.name) }
age
name

KClass类包含大量的方法用于访问类的上下文内容:

1
2
3
4
5
6
7
interface KClass<T : Any> {
val simpleName: String?
val qualifiedName: String?
val members: Collection<KCallbale<*>>
val constructors: Collection<KFunction<T>>
...
}

KClass的许多有用的特性,包括前面用的memberProperties,被声明为扩展(extensions)。

KCallable是所有函数和属性的超级接口(superinterface)。它声明了call方法,允许你属性的对应函数或getter。

1
2
3
interface KCallable<out R> {
fun call(vararg args: Any?): R
}

下面的例子证明了如何通过反射用call来调用一个函数:

1
2
3
4
fun foo(x: Int) = println(x)
>>> val kFunction = ::foo
>>> kFunction.call(42)
42

这个表达式是KFunction类的一个反射的实例。要调用引用的函数,使用KCallable.call方法。在这里,你需要提供唯一的入参,42。如果尝试调用错误数量的参数,譬如kFunction.call,会抛出一个运行时错误:“IllegalArgumentException: Callable expects 1 arguments, but 0 were provided.”。

然而,这个例子可以使用一个更特定的方法来调用函数。:foo表达式的类型是KFunction1<Int, Unit>,包含了有关于参数和返回类型的信息。这里的1表示函数接收一个参数。要从该接口调用函数,使用invoke方法。它接收固定的参数数量,它的参数类型对应于KFunction1接口的类型参数。你也可以直接调用kFunction

1
2
3
4
5
6
7
8
import kotlin.reflect.KFunction2

fun sum(x: Int, y: Int) = x + y
>>> val kFunction: KFunction2<Int, Int, Int> = ::sum
>>> println(kFunction.invoke(1, 2) + kFucntion(3, 4))
10
>>> kFunction(1)
ERROR: No value passed for parameter 2

现在我们用不正确的参数个数调用了kFunctioninvoke方法:不会编译。因此,如果KFunction有一个指定的类型,带有参数和返回类型,更好用invoke方法来使用它。call方法是一个通用的对所有函数的调用方式,但不保证类型安全。

How and where are KFunctionN defined

诸如KFunction1类型表示函数有不同的参数个数。每个继承KFunction的类型添加一个额外的invoke成员参数。例如,KFunction2声明了operator fun invoke(p1: P1, p2: P2): R。其中R1R2表示了函数的参数类型,以及R表示返回类型。

这些函数类型是编译生成类型(compiler-generated types)所合成的,以及你不能在kotlin.reflect包里面找到它的声明。意味着你可以为一个任意参数个数的函数使用接口。这种合成类型实现了减少kotlin-runtime.jar的大小以及避免的函数类型参数的
人为限制。

你也可以在KProperty实例调用call方法,它将调用属性的getter。但属性的接口提供了更合适的值获取方式:get方法。

要访问get方法,你需要使用合适的接口,取决于它如何声明。top-level属性对应于KProperty0接口,有个无参的get方法:

1
2
3
4
5
var counter = 0
>>> val kProperty = ::counter
>>> kProperty.setter.call(21) // Calls a setter through reflection, pasing 21 as an argument
>>> println(kProperty.get()) // Obtains a property value by calling "get"
21

一个 member propertyKProperty1的一个实例表示,只有一个get方法。要访问它的值,你必须提供对象的实例。下面例子存储了一个属性的变量在memberProperty;然后你调用memberProperty.call(person)来获得属性对应person实例的值。因此如果一个memberProperty引用的是Person类里面的age属性,memberProperty.call(person)则是动态地获取person.age的值:

1
2
3
4
5
6
class Person(val name: String, val age: Int)

>>> val person = Person("Alice", 29)
>>> val memberProperty = Person::age // reference to property age
>>> pirntln(memberProperty.get(person))
29

注意,KProperty1是一个泛型类。memberProperty变量的类型是KProperty<Person, Int>,其中第一个参数表示接收类型,第二个参数表示属性类型。因此,你可以调用它的get方法,仅在接收类型正确的情况。调用memberProperty.get("Alice")则不能编译。

也要注意,仅能用反射访问那些定义在top-level或类里面的属性,对于函数内部的属性是无法访问。如果你定义一个本地变量x,尝试去获取它的::x引用,将会得到一个编译错误“References to variables aren’t supported yet”。

下图展示了这些接口的层级关系。因为所有声明可以被注解,接口表示运行时的声明,诸如KClassKFunction以及KParameter,都是继承自KAnnotatedElementKClass被用于表述类或对象。KProperty被用表述任何属性,对于它的子类,KMutableProperty,表示一个可变属性,即用var声明的。你可以用声明在KPropertyKMutableProperty的特定接口Getter和Setter和属性访问器一起作为函数使用——例如,你需要检索所有它的注解。两个访问器的接口都继承KFunction。为了简化,我们省略了某些特定的诸如KProperty0的接口。

Figure 10.6

Implementing object serialization using reflection

首先,让我们回顾声明在JKid的序列化函数。

1
fun serialize(obj: Any): String

该函数接收一个对象并以字符串返回它的JSON表述。它会由一个StringBuilder实例构建JSON。因为它的序列化属性和值将被append到这个StringBuilder对象中。为了使append调用更加简洁,该实现将被放在StringBuilder的扩展函数中。这样,你可以方便地调用append方法而不需要限定:

1
2
3
private fun StringBuilder.serializeObject(x: Any) {
append(...)
}

将一个函数参数转换为一个扩展函数的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
2
3
4
5
6
7
8
9
10
11
private fun StringBuilder.serializeObject(obj: Any) {
val kClass = obj.javaClass.kotlin // Gets the KClass for the object
val properties = kClass.memberProperties // Get all the properties of the class

properties.joinToStringBuilder(
this, prefix = "{", postfix = "}") { prop ->
serializeString(prop.name) // Gets the property name
append(": ")
serializePropertyValue(prop.get(obj)) // Gets the property value
}
}

该函数的实现清晰:一个接一个地序列化类的每个属性。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
2
3
4
5
6
annoation class JsonName(val name: String)

data class Person(
@JsonName("alias") val firstName: String,
val age: Int
)

注解里面的name将被用于JSON。使用findAnnotation来处理:

1
2
val jsonNameAnn = prop.findAnnoation<JsonName>()  // Gets an instance of the @JsonName annotation if it exists
val propName = jsonNameAnn?.name ?: prop.name // Gets its "name" argument or uses "prop.name" as a fallback

如果一个属性没有被@JsonName注解,那么jsonNameAnnnull,仍然使用prop.name作为JSON序列化的name部分,否则使用指定的name。

下面是其完整实现,

1
2
3
4
5
6
private fun StringBuilder.serializeObject(obj: Any) {
obj.javaClass.kotlin.memberProperties.filter { it.findAnnotation<JsonExclude>() == null }
.joinToStringBuilder(this, prefix = "{", postfix = "}") {
serializeProperty(it, obj)
}
}

现在带有@JsonExclude注解的属性被过滤掉了。并提取了对应序列化属性的逻辑,

1
2
3
4
5
6
7
8
private fun StringBuilder.serializeProperty(prop: KProperty1<Any, *>, obj: Any) {
val jsonNameAnn = prop.findAnnotation<JsonName>()
val propName = jsonNameAnn?.name ?: prop.name
serializeString(propName)
append(": ")

serializePropertyValue(prop.get(obj))
}

接下来,实现剩余的注解,@CustomSerializer。它的实现基于getSerializer函数。它会返回通过@CustomSerializer注解注册了的ValueSerializer实例。例如,

1
2
3
4
data class Person(
val name: String,
@CustomSerializer(DateSerializer::class) val birthDate: Date
)

下面是提醒@CustomSerializer注解是如何声明的,

1
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

以及getSerializer函数的实现,

1
2
3
4
5
6
7
8
fun KProperty<*> getSerializer(): ValueSerializer<Any?>? {
val customSerializerAnn = findAnnotation<CustomSerializer>() ?: return null
val serializerClass = customSerializerAnn.serializerClass

val valueSerializer = serializerClass.objectInstance ?: serializerClass.createInstance()
@Suppress("UNCHECKED_CAST")
return valueSerializer as ValueSerializer<Any?>
}

它是KProperty的一个扩展函数,因为属性是由方法处理的第一个对象。它调用了findAnnotation函数来获取@CustomSerializer注解的一个实例。参数部分serializerClass指定了需要获取对象实例的类型。

这里最有趣的部分是你可以处理类和对象(kotlin的单例),通过@CustomSerializer注解获得值。他们都由KClass类表述。不同的是对象拥有非空的objectInstance属性值,用于访问由object创建的单例实例。例如,DateSerializer被声明为一个object,因此它的objectInstance属性被存储哎单例的DateSerializer实例。你可以使用该实例序列化所有对象,但createInstance不能被调用。

如果KClass代表的是常规类,则可以使用createInstance来创建一个新的类实例。该函数类似于java.lang.Class.newInstance

下面是serializeProperty的最终版本实例:

1
2
3
4
5
6
7
8
9
10
11
private fun StringBuilder.serializeProperty(prop: KProperty1<Any, *>, obj: Any) {
val name = prop.findAnnotation<JsonName>()?.name ?: prop.name
serializeString(name)
append(": ")

val value = prop.get(obj)
val jsonValue =
prop.getSerializer()?.toJsonValue(value) // Uses a custom seralizer for the property if it exists
?: value // Otherwise uses the property value as before
serializePropertyValue(jsonValue)
}

serializeProperty使用序列化toJsonValue将属性值转换为JSON兼容格式。如果属性没有自定义toJsonValue,将使用属性值替代。

JSON parsing and object deserialization

下面开始第二个环节:实现反序列化逻辑。首先,回顾API,和序列化用法一样,

1
inline fun <reified T: Any> deserialize(json: String): T

下面是它的用例:

1
2
3
4
5
6
7
data class Author(val name: String)
data class Book(val title: String, val author: Author)

>>> val json = """{"title": "Catch-22", "author": {"name": "J. Heller"}}"""
>>> val book = deserialize<Book>(json)
>>> println(book)
Book(title=Catch-22, author=Author(name=J. Heller))

对象的类型作为一个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
2
3
4
5
6
7
interface JsonObject {
fun setSimpleProperty(propertyName: String, value: Any?)

fun createObject(propertyName: String): JsonObject

fun createArray(propertyName: String): JsonObject
}

propertyName参数在该方法接收JSON的key。因此,当解析器遇到一个带有author属性的对象作为它的值时,将会调用createObject("author")方法。

下图显示了反序列化字符串每一步中的词法和语法分析。再一次说明,词法分析将输入字符串拆分为token列表;然后语法分析(parser)处理这些token列表中每一个元素调用JsonObject一个适当的方法。

反序列器为JsonObject实现提供对应类型的一个新实例的构建。它需要从JSON key(titleauthor,和name)和类属性之间查找,从而构建内嵌的对象值(实例Author);之后才能创建需求的实例(Book)。

Figure 10.7

JKid使用了传统的Builder pattern模式,使用不同的builder来生成不同的对象实例。在JSON中需要构建不同的组合结构类型:对象、集合、映射。这些类包括有ObjectSeedObjectListSeedValueListSeed

基础类Seed继承了JsonObject接口并提供了一个额外的spawn方法,用于获取builder处理完成的实例。另外还声明了createCompositeProperty方法,用于创建内嵌对象和内嵌列表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Seed: JsonObject {
fun spawn(): Any?

fun createCompositeProperty(
propertyName: String,
isList: Boolean
): JsonObject

override fun createObject(propertyName: String) = createCompositeProperty(propertyName, false)

override fun createArray(propertyName: String) = createCompositeProperty(propertyName, true)

// ...
}

你可以认为spawnbuild的类似物——一个方法返回结果值。它为ObjectSeed返回结构化对象,产生ObjectListSeedValueListSeed的结果列表。姑且不讨论它的实现细节。

在此之前,先学习一下主要的deserialize函数,对值进行反序列化。

1
2
3
4
5
fun <T: Any> deserialize(json: Reader, targetClass: KClass<T>): T {
val seed = ObjectSeed(targetClass, ClassInfoCache())
Parser(json, seed).parse()
return seed.spawn()
}

要开始分析,先创建一个存储了对象属性的一个ObjectSeed对象,它将被序列化,然后调用解析器将json以stream reader的形式传入。一旦读取到了字节流的末尾,即调用spawn函数来构建输出结果。

现在先聚焦在ObjectSeed的实现上,它存储了一个对象被结构化的状态。ObjectSeed的第一构造函数包含了两个引用,一个是产生结果的类,另一个classInfoCache对象包含该类属性的缓存信息。这些缓存信息最终被被构建为该类的实例。ClassInfoCacheClassInfo是帮助类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class ObjectSeed<out T : Any>(
targetClass: KClass<T>,
override val classInfoCache: ClassInfoCache
) : Seed {

// Caches the information needed to create an instance of targetClass
private val classInfo: ClassInfo<T> = classInfoCache[targetClass]

private val valueArguments = mutableMapOf<KParameter, Any?>()
private val seedArguments = mutableMapOf<KParameter, Seed>()

// Builds a map from constructor parameters to their values
private val arguments: Map<KParameter, Any?>
get() = valueArguments + seedArguments.mapValues { it.value.spawn() }

override fun setSimpleProperty(propertyName: String, value: Any?) {
val param = classInfo.getConstructorParameter(propertyName)
// Records a value for the constructor parameter, if it's a simple value
valueArguments[param] = classInfo.deserializeConstructorArgument(param, value)
}

override fun createCompositeProperty(propertyName: String, isList: Boolean): Seed {
val param = classInfo.getConstructorParameter(propertyName)
// Loads the value of the DeserializeInterface annotation for the property, if any
val deserializeAs = classInfo.getDeserializeClass(propertyName)
// Creates an ObjectSeed or CollectionSeed according to the parameter type...
val seed = createSeedForType(
deserializeAs ?: param.type.javaType, isList
)
// ...and records it in the seedArguments map
return seed.apply { seedArguments[param] = this }
}

// Creates the resulting instance of targetClass, passing an arguments map
override fun spawn(): T = classInfo.createInstance(arguments)
}

ObjectSeed在第一构造函数部分通过入参将属性对象存储在map中。包含两个可变的map:valueArguments用于存储简单的属性,seedArguments存储组合属性。简单属性通过调用setSimpleProperty方法存储在valueArguments,而组合属性则通过createCompositeProperty存储在seedArguments中。最后再通过spawn方法,递归地将组合属性作为入参构建内嵌的实例。

Final deserialization step: callBy() and creating objects using reflection

KCallable.call方法提供了一种反射机制来构建实例。实际的入参作为对应类的构造函数的入参。因为不支持默认参数值。需要使用另外一个方法:

1
2
3
interface KCallable<out R> {
fun callBy(args: Map<KParameter, Any?>): R
}

(略…)

Summary

Kotlin 的反射特性并没有创新,因为也是基于jvm的运行时。仅仅在原来Java API上进行了一层封装。严格来说,当你的程序需要用到反射的实现的时候,基本离重构的日子已经不远了。