[Kotlin Tutorial] Annotation 과 Reflection #2
참조 : Kotlin in action
10.2. Reflection: introspecting Kotlin objects at runtime
-
Kotlin 에서의 reflection 은 java.lang.reflect package 의 API 들과 kotlin.reflect package 의 API 들을 사용한다.
Kotlin 의 reflection 은 Java 에 없는 nullable type 이나 properties 들의 접근을 가능하게 한다.
-
runtime lib 의 size 를 줄이기 위해, kotlin 의 reflection API 는 kotlin-reflect.jar 라는 별개의 jar 로 pakcage 되어 있다.
그리고 이것은 새 project 에 기본 dependency 로 들어가지 않는다.
그래서 Kotlin reflection 을 사용하려면 꼭 dependency 를 추가해야 한다.
10.2.1. The Kotlin reflection API: KClass, KCallable, KFunction, and KProperty
-
KClass 는 Java 의 java.lang.Class 에 해당하는 녀석이다.
KClass 는 MyClass::class 의 형태로 얻을 수 있다.
-
Runtime 에서 얻을 경우에는 javaClass property 를 통해 Java class 를 얻는다.
이는 Object.getClass() 와 동일하다.
.kotlin extension property 를 통해 Java 에서 Kotlin reflection API 로 옮겨올 수 있다.
class Person(val name: String, val age: Int)
val person = Person(“Alice”, 29)
val kClass = person.javaClass.kotlin
println(kClass.simpleName) // Person
kClass.memberProperties.forEach{ println(it.name) } // age 와 name 출력, non-extension property 들만 가져온다.
-
interface KClass<T : Any>{
val simpleName: String?
val qualifiedName: String?
val members: Collection<KCallable<*>>
val constructors: Collection<KFunction<T>>
val nestedClasses: Collection<KClass<*>>
...
}
memberProperties 는 extension 으로 정의되어 있다.
-
KCallable 은 function, properties 의 super interface, call 함수를 가지고 있다
interface KCallable<out R> {
fun call(vararg args: Any?): R
}
fun foo(x:Int) = println(x)
val kFunction = ::foo // KFunction1<Int, Unit> 타입, 1 은 param 이 하나라는 의미
kFunction.call(42) // argument mismatch case 에는 IllegalArgumentException 이 발생
-
import kotlin.reflect.KFunction2
fun sum(x: Int, y: Int) = x+ y
val kFunction : KFunction2<Int, Int, Int> = ::sum
println(kFunction.invoke(1,2) + kFunction(3, 4))
kFunction(1) // error
KFunction 의 경우 invoke 로 수행한다. ( KFunction 의 interface 에는 invoke 가 있으며, 갯수와 type 이 명시 )
물론 call 로도 가능하다. 그러나 type safe 하지 않다.
-
KFunction2 같은 것은 어디에 정의되어 있을까?
이런 녀석들은 synthetic compiler-generated types 라고 부른다.
compiler 가 필요할 때 정의해서 만드는 녀석이라는 의미로, 이 코드는 package 에서 찾을 수 없다.
이 접근 방법으로 kotlin-runtime.jar 의 사이즈를 줄일 수 있고, 갯수 제한에 대한 문제로 뛰어 넘을 수 있다.
-
KProperty 에도 call 함수를 호출할 수 있다. 이는 getter 를 호출한다.
그러나 getter 를 얻어와서 호출하는 것이 당연히 더 좋다.
top-level property 는 KProperty0 interface 를 상속하며, 이 녀석은 get method 를 가지고 있다.
var counter = 0
val kProperty = ::counter
kProperty.setter.call(21)
println(kProperty.get()) // 21
-
member property 는 KProperty1 이며 한개의 argument 를 갖는 get method 를 가지고 있다.
첫번째 param 에는 접근하려는 object instance 를 전달해야 한다.
class Person(val name:String, val age:Int)
val person = Person(“Alice”, 29)
val memberProperty = Person::age
println(memberProperty.get(person)) // 29
또한 KProperty1 은 generic class 이다.
위의 예에서 memberProperty 는 KProperty<Person, Int> 이다.
-
KClass, KCallable, KParameter 는 모두 KAnnotatedElement interface 를 구현한다.
10.2.2. Implementing object serialization using reflection
-
private fun StringBuilder.serializeObject(obj: Any){
val kClass = obj.javaClass.kotlin
val properties = kClass.memberProperties
properties.jointToStringBuilder(this, prefix = “{“, postfix = “}”) { prop -> // KProperty1<Any, *> type
serializeString(prop.name) // JSON format 으로 escape 한다
append(“: “)
serializePropertyValue(prop.get(obj)) // primitive, string, collection, nested object 로 serialize
}
fun serialize(obj: Any): String = buildString { serializeObject(obj) }
buildString 은 StringBuilder 를 만들고 lambda 를 이용해 fill 하도록 한다.
10.2.3. Customizing serialization with annotations
-
KAnnotatedElement interface 는 annotations 라는 property 를 가지고 있다.
이는 annotation instance collection 이다. ( runtime retention 인 )
-
inline fun <reified T> KAnnotatedElement.findAnnotation(): T? = annotations.filterIsInstance<T>().firstOrNull()
val properties = kClass.memberProperties.filter{ it.findAnnotation<JSONExclude>() == null }
val jsonNameAnn = prop.findAnnotation<JsonName>()
val propName = jsonNameAnn?.name ?: prop.name
-
annotation class CustomSerializer{
val serializerClass: KClass<out ValueSerializer<*>>
}
data class Person(val name: String, @CustomSerializer(DateSerializer::class) val birthDate: Date)
fun KProperty<*>.getSerializer(): ValueSerializer<Any?>?{
val customSerializerAnn = findAnnotation<CustomSerializer>() ?: return null
val serializerClass = customSerializerAnn.serializerClass
val valueSerializer = serialzierClass.objectInstance?: serializerClass.createInstance()
@Suppress(“UNCHECKED_CAST”)
return valueSerializer as ValueSerializer<Any?>
}
class 와 singleton object 는 동일하게 KClass 이다.
objectInstance 로는 singleton object 가 접근된다.
singletone 이 아닐 경우에는 createInstance 로 instance 를 만든다.
-
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) ?: value
serializePropertyValue(jsonValue)
}
10.2.4. JSON parsing and object deserialization
-
inline fun <reified T: Any> deserialize(json: String): T
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)
-
JKid 의 deserializer 는 lexical analyzer(lexer), syntax analyzer, 그리고 parser 로 구성되어 있다.
lexical analysis 는 string 을 token 으로 나눈다.
두가지 형태의 token 이 있는데 character token ( comma, colon, braces, brackets ) 과 value token ( string, number, boolean, null ) 이 있다.
parser 는 plain token list 를 structured form 으로 변경하는 것을 이야기한다.
JKid 는 JSON 이 structured form 이다.
10.2.5. Final deserialization step: callBy() and creating objects using reflection
-
KCallable.call 은 function 이나 constructor 를 호출할 수 있고, argument 들을 list 형태로 받을 수 있다.
많은 경우에 잘 작동하지만 제약사항이 있다.
default parameter value 를 제공하지 않는다.
interface KCallable<out R> {
fun callBy(args: Map<KParameter, Any?>): R
....
}
callBy 는 전달하는 Map 에 빠진 param 이 있다면 default value 가 자동으로 사용된다. 즉 default value 를 잘 지원한다.
Map 으로 전달하니, named-argument 를 사용하는 것처럼 순서에도 영향을 받지 않는다.
Primary constructor 를 호출하는 데에도 사용될 수 있다.
-
기타 여러가지 내용들이 있었으나... 저도 잘 모르겠으니.. 따로 공부해보세요… ㅎㅎ
10.3. Summary
-
Kotlin 의 annotation 적용 syntax 는 Java 와 거의 같다.
-
Kotlin 은 Java 보다 더 넓은 target range 를 제공한다.
file, expression, property 에 추가 적용 가능하다.
-
annotation argument 는 primitive value, string, enum, class reference, 다른 annotation class, 그리고 앞선 애들의 array 가 가능하다.
-
@get:Rule 과 같은 use-site target annotation 지정은 Kotlin 이 single line 을 bytecode 에서는 여러 라인으로 만드는 경우에 선택적 적용을 할 수 있게 한다.
-
모든 param 이 val 로 마킹되며, body 가 없는 primary constructor 를 가진 annotation class 정의가 가능하다.
( primary constructor 는 없어도 된다 )
-
Meta-annotation 은 target, retention mode, 그리고 다른 annotation 의 attribute 를 명시할 때 사용된다.
-
Reflection API 는 object 의 method 와 property 들을 runtime 에 동적으로 접근할 수 있게 한다.
KClass, KFunction 등의 다른 종류의 정의 interface 를 포함한다.
-
KClass instance 를 얻기 위해서는 ClassName::class 를 하면 된다.
정적으로 알 수 없는 경우는 obj.javaClass.kotlin 으로 KClass 를 얻을 수 있다.
-
KFunction 과 KProperty interface 는 KCallable 을 상속했다.
그들은 generic “call” method 를 가지고 있다.
-
KCallable.callBy 는 default parameter value 를 가진 method 를 호출할 때 쓰일 수 있다.
-
KFunction0, KFunction1 등은 invoke 를 통해 호출 될 수 있다.
-
KProperty0, KProperty1 등은 receiver 와 여러개의 갯수의 param 을 받을 수 있다.
그리고 get method 를 제공한다.
KMutableProperty0 과 KMutableProperty1 은 set method 도 가지고 있다.
댓글