Kotlin core syntax: annotation and reflection

Posted by Aleks on Wed, 11 Dec 2019 02:12:51 +0100

Blog Homepage

1. Declare and apply comments

1.1 application notes

The way to use annotations in kotlin is the same as in java, which takes the @ character as the prefix of the name and puts it at the front of the declaration to be annotated.

Take a look at the @ Deprecated annotation, which is enhanced in Kotlin with the replaceWith parameter.

@Deprecated(message = "Use removeAt(index) instead .", replaceWith = ReplaceWith("removeAt(index)"))
fun remove(index: Int) { }

If the remove function is used, IDEA will not only prompt which function should be used instead of it (in this case, removeAt), but also provide an automatic quick fix.

Annotations can only have parameters of the following types: basic data types, strings, enumerations, class references, other annotation classes, and arrays of the previous types.

The syntax of specifying annotation arguments is slightly different from that of Java:

  • To specify a class as an annotation argument, add:: Class: @ myannotation (MyClass:: class) after the class name
  • To specify another annotation as an argument, remove the @ before the annotation name. For example, the ReplaceWith in the previous example is an annotation, but it is useless when you specify it as the argument of the Deprecated annotation@
  • To specify an array as an argument, use the arrayOf function: @ RequestMapping(path = arrayOf ("I foo /bar"). If the annotation class is declared in java, the parameter named value is automatically converted to variable length parameter on demand, so multiple arguments can be provided without arrayOf function.

Annotation arguments need to be known at compile time, so you can't reference any attribute as an argument. If you want to use a property as an annotation argument, you need to mark it with the const modifier to tell the compiler that the property is a compile time constant:

const val TEST_TIMEOUT = 100L

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

1.2 annotation objectives

The point target declaration is used to describe the element to annotate. Use point targets are placed between symbols and annotation names, separated by colons and annotation names.

// The word get causes the annotation @ Rule to be applied to the getter of the property

@get:Rule

For example, in JUnit, you can specify a rule that will be executed before each test method is executed. The standard TemporaryFolder rules are used to create files and folders and delete them after testing.

To specify a Rule, you need to declare a public field or method annotated with a Rule in Java. If you annotate the property folder with "Rule" in your kotlin test class, you will get a JUnit exception: "the (??) 'folder' must be public.".

This is because the Rule is applied to the domain, and the domain is private by default. To apply it to the getter (public), write it out explicitly, @ get:Rule

class HasTempFolder {
    @get:Rule     // The annotation is getter, not property
    val folder = TemporaryFolder()

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

The complete list of point of use targets supported by Kotlin is as follows:

  • property -- Java annotations cannot apply this kind of use point target
  • Field -- the field generated for the attribute
  • get -- property getter
  • set -- property setter
  • Receiver -- receiver parameter of extension function or extension property
  • param -- parameter of construction method
  • setparam -- parameter of property setter
  • Delegate -- store the fields of the delegate instance for the delegate attribute
  • File -- the class containing the top-level functions and properties declared in the file

Any annotation applied to the file target must be placed at the top of the file, before the package instruction. @JvmName is a common annotation applied to files. It changes the name of the corresponding class, such as:

@file:JvmName("StringFuntions")
package com.example

Unlike Java, Kotlin allows you to annotate any expression, not just the declarations and types of classes and functions. For example: the @ supply annotation suppresses compiler warnings.

fun test(list: List<*>) {
    @Suppress("UNCHECKED_CAST")
    val strings = list as List<String>
}

1.3 using annotations to customize JSON serialization

One of the uses of annotations is to customize the serialization of objects. Serialization is the process of converting objects into binary or text representations that can be stored or transmitted over the network. Its reverse process, deserialization, converts this representation back to an object.

The most common format for serialization is JSON. At present, there are many libraries that can sequence java objects into JSON. Including Jackson (https://github.com/fasterxml/jackson) and gson( https://github.com/google/gson) .

In the next example, use a pure kotlin library called JKid.
http://github.com/yole/jkid

For example: serializing and deserializing instances of the Person class. The serialize function is used to serialize

data class Person(
    val name: String,
    val age: Int
)

val person = Person("Alice", 20)
println(serialize(person))
// {"age": 20, "name": "Alice"}

Deserialize function to deserialize

val json = """{"age": 20, "name": "Alice"}"""
println(deserialize<Person>(json))

Annotations can also be used to customize how objects are serialized and deserialized. When an object is serialized into JSON, the library attempts to serialize all properties by default, using the property name as the key. But using annotations such as JsonExclude and @ JsonName allows you to change the default behavior.

  • @The JsonExclude annotation is used to mark a property that should be excluded from serialization and deserialization.
  • @The JsonName annotation lets you specify that the key in the (JSON) key value pair representing this property should be a given string, not the name of the property.
data class Person(
    @JsonName("alias") val name: String,
    @JsonExclude val age: Int? = null
)

1.4 statement notes

Take annotation in JKi library as an example: annotation @ JsonExclude without parameters

annotation class JsonExclude

The annotation modifier is added before the class key room.

Annotation @ JsonName with parameters

annotation class JsonName(val name: String)

Parameter of annotation class, val keyword is mandatory.

// java
public @interface JsonName {
   String value();
}

If you apply annotations declared in Java to Kotlin elements, you must
There are arguments that use named argument syntax, and value is treated specially by kotlin.

1.5 meta annotation: control how to handle an annotation

Like Java, a Kotlin annotation class can be annotated by itself. Annotations that can be applied to annotation classes are called meta annotations. Some meta annotations are defined in the standard library, which control how the compiler handles annotations. For example: @ Target

@Target(AnnotationTarget.PROPERTY)
annotation class JsonExclude

@The Target meta annotation describes the element types that annotations can be applied to.

To declare your own meta annotation, use annotation? Class as the target:

@Target(AnnotationTarget.ANNOTATION_CLASS)
annotation class BindingAnnotation

@BindingAnnotation
annotation class MyBinding

There is also an important meta annotation in Java: whether the annotation it is used to declare will be stored in the. class file, and whether it can be accessed through reflection at runtime.
The default behavior of kotlin is different from that of java. Kotlin annotations have RUNTIME retention.

1.6 use class as annotation parameter

If you want to be able to reference a class as declared metadata. You can do this by declaring an annotation class that has a class reference as a parameter.
In the JKid library, the @ DeserializeInterface annotation allows you to control the deserialization of those interface type properties.

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
)

When JKid reads a company object nested in an instance of the Person class, it creates and deserializes an instance of Companyimpl and stores it in the company property.

// DeserializeInterface declaration

import kotlin.reflect.KClass

@Target(AnnotationTarget.PROPERTY)
annotation class DeserializeInterface(val targetClass: KClass<out Any>)

KClass is the corresponding type of java.lang.Class in Kotlin.

If you only write kclass < any > without the out modifier, you cannot pass CompanyImpl::class as an argument: the only allowed argument will be Any::class

1.7 using generic classes as annotation parameters

By default, JKid serializes properties of non basic data types as nested objects. But you can change this behavior and provide your own serialization logic for certain values.

@The CustomSerializer annotation takes a reference to a custom serializer class as an argument. This serializer class should implement the ValueSerializer interface:

interface ValueSerializer<T> {
    fun toJsonValue(value: T): Any?
    fun fromJsonValue(jsonValue: Any?): T
}

How the @ CustomSerializer annotation declares:

@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

2. Reflection: Introspection of kotlin object at runtime

When using reflection in Kotlin, there are two different reflection API s:
The first is standard Java reflection, which is defined in the package java.lang.reflect. Because the Kotlin class will be compiled into normal
Java bytecode, java reflection API can support them perfectly.
The second is the kotlin reflection API, which is defined in package kotlin.reflect.

ext.kotlin_version = '1.3.41'

dependencies {
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
    implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
}

2.1 kotlin reflection API: KClass, KCallable, KFunction and KProperty

The main entry to the Kotlin reflection API is KClass, which represents a class. The java.lang.class corresponding to KClass.

To get the class of an object at runtime, first use the javaClass attribute to get its Java class, which is directly equivalent to java.lang.Object.getClass(). Then access the. Kotlin extension property of the class and switch from Java to kotlin reflection API:

data class Person(
    val name: String,
    val age: Int
)

val person = Person("Alice", 20)
// Return an instance of kclass < person >
val kClass = person.javaClass.kotlin;
println(kClass.simpleName)
// Person

kClass.memberProperties.forEach { println(it.name) }
//age
//name

KCallable is a super interface between functions and properties. It declares that the call method allows you to call the corresponding function or getter of the corresponding property:

public actual interface KCallable<out R> : KAnnotatedElement {
     public fun call(vararg args: Any?): R
     // ...
}

How to call a function by reflection:

fun foo(x: Int) = println(x)

val kFunction = ::foo
kFunction.call(23)
// 23

You can use the invoke method to call functions through this interface.

val kFunction = ::foo
kFunction.invoke(12)
// 12

You can also call the call method on a KProperty instance, which calls the getter of the property. But the property interface provides a better way to get property values: the get method

The top-level property is represented as an instance of the KPropertyO interface. It has a get method without parameters:

var counter = 0

fun main() {
    val kProperty = ::counter
    
    kProperty.setter.call(13) // Call setter through reflection and pass 13 as an argument
    println(kProperty.get()) // Get the value of the property by calling get
    // 13
}

A member property is represented by an instance of KProperty1, which has a single parameter get method. To access the value of this property, you must provide the object instance to which the value you need belongs.

val person = Person("Alice", 20)

// KProperty<Person, Int>
// The first type parameter indicates the receiver's type
//The second type parameter represents the type of the property
val memberProperty = Person::age

println(memberProperty.get(person))

You can only use reflection to access the properties defined in the outermost layer or class, not the local variables of the function.

2.2 object serialization with reflection

To serialize the function declaration in JKid:

fun serialize(obj: Any): String

It uses a StringBuilder instance to build JSON results. The implementation is put in the extension function of StringBuilder

private fun StringBuilder.serializeObject(obj: Any) {
    // Get the KClass of the object, get all the properties of the class
    obj.javaClass.kotlin.memberProperties
            .filter { it.findAnnotation<JsonExclude>() == null }
            .joinToStringBuilder(this, prefix = "{", postfix = "}") {
                serializeProperty(it, obj)
            }
}

private fun StringBuilder.serializeProperty(
        prop: KProperty1<Any, *>, obj: Any
) {
    val jsonNameAnn = prop.findAnnotation<JsonName>()
    val propName = jsonNameAnn?.name ?: prop.name
    serializeString(propName)
    append(": ")

    val value = prop.get(obj)
    val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value
    serializePropertyValue(jsonValue)
}

Mark it private to make sure it's not used anywhere else.

2.3 customizing serialization with annotations

How does the serializeObject function handle @ JsonExclude, @ JsonName, and @ CustomSerializer annotations?

Let's take a look at @ JsonExclude, which allows you to exclude certain attributes during serialization. So how to filter out the attributes using JsonExclude annotation?
You can use the extended property memberProperties of the KClass instance to get all the member properties of the class.

The KAnnotatedElement interface defines the attribute annotations, which is a collection of instances of all annotations (with runtime retention period) applied to elements in the source code. Because KProperty inherits KAnnotatedElement, you can access all annotations of a property in the way of property.annotation.

inline fun <reified T> KAnnotatedElement.findAnnotation(): T?
        = annotations.filterIsInstance<T>().firstOrNull()

private fun StringBuilder.serializeObject(obj: Any) {
    obj.javaClass.kotlin.memberProperties
            // Exclude elements annotated with @ JsonExclude
            .filter { it.findAnnotation<JsonExclude>() == null }
            .joinToStringBuilder(this, prefix = "{", postfix = "}") {
                serializeProperty(it, obj)
            }
}

To see the @ JsonName annotation, first of all, if you want to judge whether the @ JsonName annotation exists or not, you need to care about its actual parameter: the annotated attribute
The name that sex should be used in JSON.

// Get an instance of the @ JsonName annotation if it exists
val jsonNameAnn = prop.findAnnotation<JsonName>()
// Get its "name" argument or alternate "prop.name"
val propName = jsonNameAnn?.name ?: prop.name

If the property is not annotated with JsonName, jsonNameAnn is null, but it still needs to use prop.name as the name of the property in JSON. If the attribute is annotated with JsonName, you will use the name specified in the annotation instead of the attribute's own name.

Then there is the @ CustomSerializer annotation. Its implementation is based on the getSerializer function, which returns the ValueSerializer instance registered through the @ CustomSerializer annotation.

// Serialization property, support custom serializer
@Target(AnnotationTarget.PROPERTY)
annotation class CustomSerializer(val serializerClass: KClass<out ValueSerializer<*>>)

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?>
}

private fun StringBuilder.serializeProperty(
        prop: KProperty1<Any, *>, obj: Any
) {
    val jsonNameAnn = prop.findAnnotation<JsonName>()
    val propName = jsonNameAnn?.name ?: prop.name
    serializeString(propName)
    append(": ")

    val value = prop.get(obj)
    // Use the custom serializer for the property if it exists, otherwise use the property value as before
    val jsonValue = prop.getSerializer()?.toJsonValue(value) ?: value
    serializePropertyValue(jsonValue)
}

If KClass represents a normal class, you can create a new instance by calling createInstance. This function is similar to java.lang.Class.newInstance.
If KClass represents a singleton instance, the singleton instance created for the object can be accessed through objectInstance.

2.4 JSON parsing and object deserialization

Let's look at the declaration of deserialization:

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

The JSON deserializer in JKid is implemented in a fairly common way and consists of three main stages: lexical analyzer (commonly known as lexer), parser or parser, and deserialization component itself.

// Top level deserialization function

fun <T: Any> deserialize(json: Reader, targetClass: KClass<T>): T {
    // Create an ObjectSeed to store the properties of the deserialized object
    val seed = ObjectSeed(targetClass, ClassInfoCache())
    // Call the parser and pass the input character stream json to it
    Parser(json, seed).parse()
    // Call the spawn function to build the final object
    return seed.spawn()
}

Look at the implementation of ObjectSeed, which stores the state of the object being constructed ObjectSeed receives a reference to a target class and a classinfoCache object, which contains cached information about the properties of the class. This cached information will later be used to create instances of the class.

// Serialize an object

class ObjectSeed<out T: Any>(
        targetClass: KClass<T>,
        override val classInfoCache: ClassInfoCache
) : Seed {
    // Cache the information needed to create the targetClass instance
    private val classInfo: ClassInfo<T> = classInfoCache[targetClass]

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

    // Build a mapping 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)
        // If the value of a constructor parameter is a simple value, record it
        valueArguments[param] = classInfo.deserializeConstructorArgument(param, value)
    }

    override fun createCompositeProperty(propertyName: String, isList: Boolean): Seed {
        val param = classInfo.getConstructorParameter(propertyName)
        // Load the value of the attribute DeserializeInterface annotation if any
        val deserializeAs = classInfo.getDeserializeClass(propertyName)
        // Create an ObjectSeed or CollectionSeed based on the type of parameter
        val seed = createSeedForType(
                deserializeAs ?: param.type.javaType, isList)
        return seed.apply { seedArguments[param] = this }
    }

    // Pass the actual parameter map and create the targetClass instance as the result
    override fun spawn(): T = classInfo.createInstance(arguments)
}

ObjectSeed builds a mapping between constructor parameters and their values.

2.5 the last step of deserialization: callBy() and creating objects with reflection

The KCallable call method does not support the default parameter value. You can use another method KCallable.callBy that supports the default parameter value.

public actual interface KCallable<out R> : KAnnotatedElement {
     public fun callBy(args: Map<KParameter, Any?>): R
     // ...
}

Get serializer based on value type:

// Get serializer based on value type

fun serializerForType(type: Type): ValueSerializer<out Any?>? =
        when (type) {
            Byte::class.java, Byte::class.javaObjectType -> ByteSerializer
            Short::class.java, Short::class.javaObjectType -> ShortSerializer
            Int::class.java, Int::class.javaObjectType -> IntSerializer
            Long::class.java, Long::class.javaObjectType -> LongSerializer
            Float::class.java, Float::class.javaObjectType -> FloatSerializer
            Double::class.java, Double::class.javaObjectType -> DoubleSerializer
            Boolean::class.java, Boolean::class.javaObjectType -> BooleanSerializer
            String::class.java -> StringSerializer
            else -> null
        }

The corresponding ValueSerializer implementation performs the necessary type checking and conversion

// Serializer of Boolean value
object BooleanSerializer : ValueSerializer<Boolean> {
    override fun fromJsonValue(jsonValue: Any?): Boolean {
        if (jsonValue !is Boolean) throw JKidException("Expected boolean, was: $jsonValue")
        return jsonValue
    }

    override fun toJsonValue(value: Boolean) = value
}

ClassinfoCache is designed to reduce the overhead of reflection operations.

// Mixed reflection data storage

class ClassInfoCache {
    private val cacheData = mutableMapOf<KClass<*>, ClassInfo<*>>()

    @Suppress("UNCHECKED_CAST")
    operator fun <T : Any> get(cls: KClass<T>): ClassInfo<T> =
            cacheData.getOrPut(cls) { ClassInfo(cls) } as ClassInfo<T>
}

The ClassInfo class is responsible for creating new instance wells based on the target class to cache necessary information.

// Parameters of construction method and cache of annotation data

class ClassInfo<T : Any>(cls: KClass<T>) {
    private val className = cls.qualifiedName
    private val constructor = cls.primaryConstructor
            ?: throw JKidException("Class ${cls.qualifiedName} doesn't have a primary constructor")

    private val jsonNameToParamMap = hashMapOf<String, KParameter>()
    private val paramToSerializerMap = hashMapOf<KParameter, ValueSerializer<out Any?>>()
    private val jsonNameToDeserializeClassMap = hashMapOf<String, Class<out Any>?>()

    init {
        constructor.parameters.forEach { cacheDataForParameter(cls, it) }
    }

    fun getConstructorParameter(propertyName: String): KParameter = jsonNameToParamMap[propertyName]
            ?: throw JKidException("Constructor parameter $propertyName is not found for class $className")

    fun deserializeConstructorArgument(param: KParameter, value: Any?): Any? {
        val serializer = paramToSerializerMap[param]
        if (serializer != null) return serializer.fromJsonValue(value)

        validateArgumentType(param, value)
        return value
    }

    fun createInstance(arguments: Map<KParameter, Any?>): T {
        ensureAllParametersPresent(arguments)
        return constructor.callBy(arguments)
    }
}

The data is stored in three map s:

  • jsonNameToParamMap describes the parameters corresponding to each key in the JSON file
  • paramToSerializerMap stores the serializer for each parameter
  • jsonNameToDeserializeClassMap stores the class specified as an argument of the @ Deserializeinterface annotation

If my article is helpful to you, please give me a compliment

Topics: Android Java JSON Attribute github