1. Basic concepts of reflection

Reflection is to allow the program to obtain the structure of the program code at runtime, including the program structure information: classes, interfaces, methods, properties and other syntactic properties.

Common uses of reflection:

  1. Get all the properties, methods, inner classes, and so on of a class at run time;
  2. Can call methods with a given name and signature or access properties with a given name;
  3. Through signature information (signature in bytecode, so needed when confused-keepattributes Signature) gets the exact type of the generic argument;
  4. Access runtime annotations and their information for injection or configuration operations.

Common data structures for reflection in Kotlin:

  1. KType:Describes type or generic parameters that are not erased, for exampleMap<String,Int>; The corresponding parent class, attribute, function parameter, and so on can be obtained via typeOf or the following types;
  2. KClass: describes the actual type of the object, without generic parameters, such as Map, which can be obtained directly from the object, type name;
  3. KProperty: describes the attribute, which can be obtained through the attribute reference and the KClass of the attribute class.
  4. KFunction: describes a function, which can be obtained by a function reference, the KClass of the class in which the function resides.

Use of Kotlin reflection:

The Java reflection library is embedded in the JDk and does not need to rely on other libraries, but Kotlin has created an additional set of apis to implement reflection, which the library relies on:

implementation "org.jetbrains.kotlin:kotlin-reflect"
Copy the code

It is perfectly possible to use Java reflection in Kotlin because Kotlin and Java are fully compatible. The pros and cons of both are listed below:

The Java reflection:

  • Advantages: No additional dependencies are introduced and it is relatively fast to use for the first time (because the information is in the virtual machine).
  • Disadvantages: The Kotlin syntax features are not accessible, and you need to know enough about the bytecode generated by Kotlin. (This is because the Kotlin program is also a Java class after being compiled, so Kotlin compiled from the Perspective of Java reflection looks like a normal Java class. To access some method properties of the Kotlin class through reflection, you must know what bytecode it compiles.

Kotlin reflection:

  • Advantages: Supports access to almost all of Kotlin’s features, better API design compatibility.
  • Disadvantages: An additional Kotlin reflection library (around 2.5m, 400KB compiled) is introduced. The first call is slower because Kotlin’s reflection information is written to the Metadata annotation (see bytecode, below). The data inside is written binary through the Protobuf data format serialization, so the first reflection call has a process of retrieving annotations and deserializing them.
@Metadata( mv = {1, 6, 0}, k = 1, d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0002\n\u0002\u0010\u0002\n \u0002\b\u0003\u0018\u0000*\n\b\u0000\u0010\u0001 \ \ u00020 \ u00022 u0000 * \ u00020 \ u0003B \ u0005 ¢\ u0006 \ u0002 \ u0010 \ u0004J \ u0013 \ u0010 \ u0005 \ u001a \ u00020 \ u00062 \ u0006 \ u0010 \ u0 007 \ u001a \ u00028 \ u0000 ¢\ u0006 \ u0002 \ u0010 ¨ \ \ b u0006 \ t "}, d2 = {" Lcom/qisan/kotlinstu/refuse;" , "T", "Lcom/qisan/kotlinstu/Waste;" , "", "()V", "put", "", "t", "(Lcom/qisan/kotlinstu/Waste;) V", "KotlinStu.app"} )Copy the code

Second, the use of Kotlin reflection common data structures

Get KClass and KProperty:

Var CLS :KClass<String> = String::class //cls. Java converts to Java class <String>,cls.java.kotlin converts to KClass var ClsJava :Class<String> = cls.java // Get attributes defined in the String Class, Returns the Collections val property = CLS. DeclaredMemberProperties println (property. The toString ()) to print the results: [val kotlin.string.length: kotlin.int] public override val length: IntCopy the code

KClass has the following declareFunctions to obtain KFunction:

cls.declaredFunctions
cls.declaredMemberFunctions
cls.declaredMemberExtensionFunctions
Copy the code

To note here is that declaredMemberExtensionFunctions for the extension of the method is not the expansion of the class itself, but the definition in the class of extension methods, such as:

class A{
    fun String.hello(){}
}
Copy the code

We through A: : class declaredMemberExtensionFunctions is access to the hello () method, but is unable to get through the String declaredMemberExtensionFunctions, Because declaredMemberExtensionFunctions only to get the method of this class, you cannot get the extension method, and its extension methods are compiled into a static function.

KClass also defines many methods, such as:

IsCompanion cls.nestedclasses cls.iscompanion class cls.nestedclasses cls.iscompanion class You can get it directly through this method and there are a lot of other methods that are not listed in one, you need to experience it in practice.Copy the code

Get KType

Val mapType = typeOf < Map < String, Int > > () mapType. The arguments. The forEach {println (it)} printing results: kotlin. String kotlin. IntCopy the code

As you can see from the code above, generic arguments can be found through KType.

Get generic arguments by reflection

1, get the generic argument type returned by the method in the interface:

interface Api{ fun getUsers():List<UserModel> } data class UserModel( var id: Int, var name: String, ) fun main(){ Api::class.declaredFunctions.first{ it.name == "getUsers" } .returnType.arguments.forEach { println(it) } / / and the above written equivalent Api: : getUsers function reference is a KFunction Api: : getUsers. ReturnType. The arguments. The forEach {println (it)}} printing results:  com.qisan.kotlinstu.reflect.UserModel com.qisan.kotlinstu.reflect.UserModelCopy the code

2. Get the generic argument of the parent class

Abstract class SuperType<T>{val typeParamter by lazy{//this represents an instance of a subclass because abstract classes cannot be used to create instances. Only back as the parent class is a subclass inherits this: : class. Supertypes. The first (). The arguments. The first (). The type / / if there are multiple subclasses of inheritance Such as class SubType2: SubType () {} His ::class.supertype will be empty because SubType has no generic arguments.  //this::class.allSupertypes.first().arguments.first().type } } open class SubType:SuperType<String>() class SubType2:SubType() fun main(){val SubType = SubType() subtype.typeParamter.let (::println)} The result is displayed: kotlin.stringCopy the code

4. Practice case study

There are not many conceptual things related to reflection, and there are many API methods. Instead, we will deepen our understanding and use in practice. Let’s analyze and learn two practice cases of reflection written by Bennyhuo, the head of Kotlin Chinese community.

4.1. Implement DeepCopy for data classes

We know that the data class implements a copy method, but this copy is a shallow copy, how to shallow copy? Let’s take a look at the following code:

Person(val id: Int, val name: String, val group: group) Data class group (val id: Int, val name: group) String, val location: String) // Copy an instance of Person val Person = Person(...) If (person === copyPerson) false if(person.group === copyPerson.group) True This case is a shallow copyCopy the code

Obviously, since (Person.group === copyPerson.group) is true, when we modify the value in Person.group, the value of copyPerson.group will also be modified, which can be unpredictable in real development. The problem with deep copy is to make (Person.group === copyPerson.group) false.

First, we need to provide a deep-copy method for Any data class, so when we define a generic type, its parent type must be Any, because Any is the parent of all types. Then we simply define an extension method that extends Any.

Defining data classes

data class Person(val id: Int, val name: String, val group: Group)

data class Group(val id: Int, val name: String, val location: String)
Copy the code

Add the DeepCopy() method

fun <T : Any> T.deepCopy(): T {the if (this: : class isData) {/ / to judge whether the data type of the return} / / by reflecting access to the data class constructor return this: : class. PrimaryConstructor!! .let {primaryConstructor -> // Explicitly declare the parameters of this lamba expression primaryConstructor, do not write the default is it, which prevents other lamba arguments from being nested. And a map transform primaryConstructor. The parameters. The map {parameter - > val value = (this: : class as KClass<T>).memberproperties.first {it. Name == parameter.name} // return kproperty.get (this) // return the value of the corresponding property // If the attribute is of type KClass, check whether it is a data class. If it is keep deep copy Or directly with the current parameter mapping assignment if ((parameter) type) classifier as? KClass < * >)? The isData = = true) {parameter to Value?. DeepCopy ()} else {parameter to value}}. ToMap () / / convert the map list into the map. Let {primaryConstructor. CallBy (it)} //primaryConstructor (KFunction<T>); //primaryConstructor (KFunction<T>);Copy the code

Build call:

fun main() {
    val person = Person(
        1001,
        "QiSan",
        Group(
            1024,
            "Kotliner.cn",
            "JiangNanXi"
        )
    )

    val copyPerson = person.copy()
    val deepCopyPerson = person.deepCopy()

    println(person === copyPerson)
    println(person === deepCopyPerson)

    println(person.group === copyPerson.group)
    println(person.group === deepCopyPerson.group)

    println(deepCopyPerson)
}
Copy the code

False true true Person(id=1001, name=QiSan, group= group (id=1024, name= kotliner.cn, location=JiangNanXi))

At this point, deep copy is implemented.

4.2. Model Mapping

In our actual development scenario, when the attribute names in two data models are the same or part of them are the same, then I need to assign the data of one model to the other model only with the corresponding value of each attribute. It is quite troublesome to do so when there is a large amount of data. The problem that model mapping solves is that the data model directly generates a model data class that meets the requirements through reflection.

Declare two data classes that fit the model mapping:

data class UserVO(val login: String, val avatarUrl: String)

data class UserDTO(
    var id: Int,
    var login: String,
    var avatarUrl: String,
    var url: String,
    var htmlUrl: String
)
Copy the code

You can see that UserDTO and UserVO share login and avatarUrl attributes, so we can map UserDTO to UserVO.

There is also a case where we can convert a map into a suitable data Model object, such as the following case:

val userMap = mapOf(
    "id" to 0,
    "login" to "Bennyhuo",
    "avatarUrl" to "https://api.github.com/users/bennyhuo",
    "url" to "https://api.github.com/users/bennyhuo"
)
Copy the code

So you can define two methods for mapping. One is to use map to map to model, and the other is to use model to map to get a new model. See the following two methods:

// inline fun <reified From : Any, reified To : Any> From.mapAs(): To {// Convert the existing data into a map, To call the map type mapAs conversion return From: : class. MemberProperties. Map {it. A name to it. Get (this)}. ToMap () mapAs ()} the inline fun <reified To : Any> Map<String, Any?>.mapAs(): To { return To::class.primaryConstructor!! .let {it. Parameters. map {parameter -> // this[parameter.name] is the map that is called. Parameter to (this[parameter.name]?: if(parameter.type.isMarkedNullable) null else throw IllegalArgumentException("${parameter.name} is required but missing.")) }.toMap() .let(it::callBy) } }Copy the code

This model map is actually simpler. The object is constructed using a map transform from the reflection constructor.

fun main() { val userDTO = UserDTO( 0, "QiSan", "https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image", "https://juejin.cn/user/1820446987653816", "https://juejin.cn/user/1820446987653816" ) val userVO: UserVO = userDTO.mapAs() println(userVO) val userMap = mapOf( "id" to 0, "login" to "QiSan", "avatarUrl" to "https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image", "url" to "https://juejin.cn/user/1820446987653816" ) val userVOFromMap: UserVO = usermap.mapas () println(userVOFromMap)}  UserVO(login=QiSan, avatarUrl=https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image) UserVO(login=QiSan, avatarUrl=https://p3-passport.byteacctimg.com/img/user-avatar/27af052325f5dc48aac3bc578aeb3e8e~300x300.image)Copy the code