이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.
작성 시점: 2018-03-16
Annotation Processor (주석 처리기) 는 컴파일 시점에 특정 어노테이션을 기반으로 하여 클래스를 생성하고, 메서드를 생성하고, 필드를 생성하는 기술로 JDK 1.5 부터 도입되었다.
그 외 설명은 아래 글을 참고하는게 더 빠를지도 모른다.
(번역) Annotaiton Processing 101
Annotation Processing : Don’t Repeat Yourself, Generate Your Code.
이미 Glide, Dagger, ButterKnife, Data-binding 등이 이 Annotation Processor 기능을 사용해서 코드를 생성하고 하여금 개발자가 모든 코드를 직접 작성하지 않아도 자동으로 생성하는 기능을 가지고 있다.
현재는 주로 자바로 자바의 코드를 생성하는 방법이 주로 쓰이고 있지만, 오늘은 Kotlin을 이용해 Kotlin 코드를 작성하는 법을 작성해보려 한다.
본 프로젝트의 예제로 사용된 프로젝트는 WindSekirun/AttributeParser 이다.
총 3개의 모듈이 필요하다.
Annotation을 담을 annotation 모듈, APK 파일 내에 포함되지 않고 컴파일 시간에 실행되는 complier 모듈, 그리고 이 두 개를 의존해서 사용하는 demo 모듈이다.
annotation 모듈, complier 모듈은 android-library 플러그인이 아닌 kotlin 플러그인을 사용한다.
annotation 모듈의 build.gradle
apply plugin: 'java'
apply plugin: 'kotlin'
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
complier 모듈의 build.gradle
apply plugin: 'kotlin'
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_1_8
dependencies {
compile "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
compile project(':attribute-parser')
compile 'com.squareup:kotlinpoet:0.7.0'
compile 'com.google.auto.service:auto-service:1.0-rc3'
}
간단하게 Class에 부착해 해당 클래스의 이름을 가져와 그 이름을 기반으로 클래스를 만들어보자.
package pyxis.uzuki.live.attribute.parser.annotation
@Target(AnnotationTarget.CLASS, AnnotationTarget.FILE)
@Retention(AnnotationRetention.SOURCE)
annotation class CustomView
Target 는 Class 및 File, 어노테이션 실행 시점은 SOURCE로 고정한다.
이 어노테이션에는 별도의 필드는 필요 없기에, 두지 않는다.
complier 모듈에 kt 파일을 하나 생성해보고 AbstractProcessor 를 상속하면 된다.
package pyxis.uzuki.live.attribute.parser.compiler
import com.google.auto.service.AutoService
import pyxis.uzuki.live.attribute.parser.compiler.utils.supportedAnnotationSet
import javax.annotation.processing.AbstractProcessor
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.annotation.processing.SupportedSourceVersion
import javax.lang.model.SourceVersion
import javax.lang.model.element.TypeElement
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor::class)
class Processor : AbstractProcessor() {
override fun getSupportedAnnotationTypes() = supportedAnnotationSet
override fun process(p0: MutableSet<out TypeElement>?, p1: RoundEnvironment?): Boolean {
// TODO: implement something
return true
}
}
하나씩 살펴보면..
val supportedAnnotationSet = classNameSetOf(CustomView::class)
fun <T : KClass<*>> classNameSetOf(vararg elements: T) = elements.map { it.java.name }.toHashSet()
이런 식으로 정의하면 된다.
RoundEnvironment 는 getSupportedAnnotationTypes 에서 선언한 각종 대상 어노테이션을 사용중인 Element (구조 요소) 를 반환하는 역할을 한다.
주로 사용되는 메서드는 RoundedEnvironment.getElementsAnnotatedWith(Class<*>) 인데, 이름대로 해당 어노테이션 클래스를 사용하는 element 의 리스트를 반환한다.
for (element in env?.getElementsAnnotatedWith(CustomView::class.java)!!) {
val typeElement = element as TypeElement
val className = typeElement.asClassName()
className.canonicalName
className.packageName()
className.simpleName()
}
element 는 VariableElement, TypeElement 등이 있는데 여기서는 TypeElement 로 cast를 시켜서 ClassName 객체를 구해내게 한다.
여기서 ClassName 는 Kotlin 코드를 좀 더 쉽게 작성할 수 있게 도와주는 KotlinPoet 의 클래스이다. packageName, simpleName 등을 파싱할 수 있게 한다.
원래라면 AutoService 가 생성해야 하지만 18년 3월 17일 기준으로는 자동 생성되지 않는다.
src/main/resources/META-INF/services/javax.annotation.processing.Processor 라는 파일을 만들고, 내부에 구동할 Annotation Processor 의 풀네임을 적는다.
pyxis.uzuki.live.attribute.parser.compiler.AttributeParserProcessor
ClassName 를 가지고 클래스를 생성해보자.
private fun writeAttributes(simpleName: String) {
val fileName = "${simpleName}Attributes"
val builder = TypeSpec.objectBuilder(fileName)
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
?.replace("kaptKotlin", "kapt")
?.let { File(it, "$fileName.kt") }
?: throw IllegalArgumentException("No output directory")
val typeSpec = builder.build()
val fileSpec = FileSpec.builder("", fileName).addType(typeSpec).build()
fileSpec.writeTo(kaptKotlinGeneratedDir)
}
companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
맨 첫번째 줄부터 읽어보면,
이 순서가 된다.
이 코드를 넣고, 데모에서 annotation 모듈과 complier 모듈을 의존한 후 어떤 특정 클래스에 @CustomView 어노테이션을 붙이고 빌드하면 그 클래스의 이름을 가진 Attribute 클래스가 생성될 것이다.
만일 이 클래스에 Property, function 등을 붙이고 싶다면 builder 밑에 작성해주면 된다.
아래는 실제로 AttributeParser 에서 사용중인 코드이다.
private fun writeAttributes(holder: CustomViewHolder) {
val classTypeName = holder.className
val classTypeParameterName = holder.simpleName.substring(0, 1).toLowerCase() + holder.simpleName.substring(1)
val simpleName = holder.simpleName
val fileName = simpleName + Constants.ATTRIBUTES
val builder = TypeSpec.objectBuilder(fileName)
val models = getModelList(simpleName, mAttrBooleanMap, mAttrColorMap, mAttrDimensionMap,
mAttrIntegerMap, mAttrIntMap, mAttrFractionMap, mAttrFloatMap, mAttrResourceMap, mAttrStringMap)
for (model in models) {
builder.addProperty(createAttrsFieldSpec(model))
}
builder.addProperty(createRFieldSpec())
builder.addFunction(createObtainApplyMethodSpec(classTypeName, classTypeParameterName, holder.simpleName))
builder.addFunction(createApplyMethodSpec(classTypeName, classTypeParameterName, models))
builder.addFunction(createPrintVariableMethodSpec(simpleName, models))
builder.addFunction(createBindAttributesMethodSpec(simpleName, models))
val kaptKotlinGeneratedDir = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
?.replace("kaptKotlin", "kapt")
?.let { File(it, "$fileName.kt") }
?: throw IllegalArgumentException("No output directory")
val typeSpec = builder.build()
val fileSpec = FileSpec.builder(holder.packageName, fileName).addType(typeSpec).build()
fileSpec.writeTo(kaptKotlinGeneratedDir)
}
원본 라이브러리인 JavaPoet 와 달리 KotlinPoet 는 문서의 양이 너무 적긴 하지만, 충분히 클래스 파일을 생성할 수 있다.
다음 글에서는 KotlinPoet 의 구체적 사용법 등을 몇 가지 예제로 살펴보려고 한다.