본문 바로가기
개발/안드로이드

안드로이드 Annotation을 이용한 코드 생성(2)

by 창이2 2021. 8. 8.

안녕하세요.

지난 포스팅에 이어서 이번에는 Annotation을 이용한 코드생성을 진행해보겠습니다.

혹시나 안보셧다면 먼저 보고 이 글을 읽으시길 바랍니다.

https://dog-footprint.tistory.com/49

 

안드로이드 Annotation을 이용한 코드 생성(1)

안녕하세요. 이번 포스팅에서는 Annotation을 이용한 코드 생성하는 방법을 알려드리겠습니다. 이전에 어노테이션과 관련된 포스팅을 한적이 있었는데 이것에 대한 연장선이라고 생각하시면 되겠

dog-footprint.tistory.com

 

일단 코드 생성을 위해선 몇가지 개념을 알아야 합니다.

 

FileSpec : 코드생성 파일을 정의하는 클래스
TypeSpec : 파일생성시 어떤 타입의 파일을 정의하는 지에 대한 클래스(예를 들면 클래스 파일, 인터페이스 파일 등등)
FunSpec : 함수 코드를 생성하기 위한 클래스
PropertySpec : Property 설정을 정의하기 위한 클래스
AnnotationSpec : AnnotationSpec 설정을 정의하기 위한 클래스
ParameterSpec : 함수의 Parameter설정을 정의하기 위한 클래스

 

Spec클래스들은 각각 파일, 클래스, 함수 등등 정의할때 구분해서 정의하기 편하기 위한 클래스 입니다.

 

일단 샘플 코드로 어떻게 코드생성을 하는지 알아보겠습니다. MyProessor 클래스 안에 아래 함수를 정의합니다.

 

    private fun makeMyInformationClass(){
        val testFileBuilder = FileSpec.builder("","MyInformation") // 파일이름 설정
                .addType(TypeSpec.classBuilder("MyInformation") //클래스 이름 설정
                        .primaryConstructor(FunSpec.constructorBuilder()    //클래스 생성자 설정
                                .addParameter("name", String::class, KModifier.PRIVATE) //생성자에 name Parameter 추가
                                .addParameter("age", Int::class, KModifier.PRIVATE) //생성자에 age Parameter 추가
                                .build())                               //클래스 생성자 설정 완료
                        .addProperty(PropertySpec.builder("name", String::class)  // 클래스에 name Property 추가
                                .initializer("name")                                    //초기값 설정(생성자로 받은 name 설정)
                                .build())
                        .addProperty(PropertySpec.builder("age", Int::class)    // 클래스에 age Property 추가
                                .initializer("age")                                   //초기값 설정(생성자로 받은 age 설정)
                                .build())
                        .addProperty(PropertySpec.builder("job", String::class) // 클래스에 job Property 추가
                                .initializer("\"programmer\"")                        //초기값 설정(job = programmer)
                                .build())
                        .addFunction(FunSpec.builder("printMyInfo")             // 클래스에 printMyInfo 함수 추가
                                .addStatement("println(%P)", "name : \$name, age : \$age, job : \$job")        // print 함수 추가
                                .build())
                        .build()).build()
        val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]!!
        testFileBuilder.writeTo(File(kaptKotlinGeneratedDir))                       //파일 생성
    }

주석에서 보시는 것과 같이 각각 파일 이름설정, 생성자 설정, 함수 설정 등등 add 하는 속성마다 Spec클래스들이 쓰인걸 볼 수 있습니다.

 

이렇게 만들어진 결과는 아래와 같습니다.

더보기

 

public class MyInformation(
  private val name: String,
  private val age: Int
) {
  public val job: String = "programmer"

  public fun printMyInfo(): Unit {
    println("""name : $name, age : $age, job : $job""")
  }
}

여기서 왜 job만 property로 생성되고 name과 age는 생성이 안되었느냐고 생각하실 수있는데

클래스 생성자에

MyInformation(private val name: String) ==  MyInformation(name: String){
	private val name: String = name
}

이기 때문입니다.

 

이제 어노테이션과 어떻게 같이 사용하는지 알아보겠습니다.

일단 MyProcessor의 process함수를 수정합니다.

 

    override fun process(
        annotations: MutableSet<out TypeElement>?,
        roundEnv: RoundEnvironment
    ): Boolean {
        val annotatedClasses = roundEnv.getElementsAnnotatedWith(MyAnnotation::class.java)
        if(annotatedClasses.isEmpty()) return false

        annotatedClasses.forEach {
            if(it.kind != ElementKind.CLASS) return false
            makeMyInfoPrintClass(it)
        }

        return true
    }

MyAnnotation이 붙은클래스를 찾고 해당 클래스들을 순회하면서 makeMyInfoPrintClass()를 호출하여 클래스파일을 만들도록 합니다.

 

    private fun makeMyInfoPrintClass(element: Element){
        val typeMetadata = element.getAnnotation(Metadata::class.java)
        val kmClass = typeMetadata.toImmutableKmClass()
        val className = ClassInspectorUtil.createClassName(kmClass.name)
        val printInfoFunSpec = FunSpec.builder("printInfo").receiver(className)

        val myAnnotation = element.getAnnotation(MyAnnotation::class.java)
        myAnnotation.run {
            printInfoFunSpec.addStatement("println(%P)", "name : ${this.name}, age : ${this.age}, job : ${this.job}")
        }
        testFileBuilder = FileSpec.builder("","MyInfoPrint").addFunction(printInfoFunSpec.build()).build()
        val kaptKotlinGeneratedDir = processingEnv.options["kapt.kotlin.generated"]!!
        testFileBuilder.writeTo(File(kaptKotlinGeneratedDir))

    }

MyAnnotation이 붙은 클래스의 Extension 함수를 만들도록 정의하면 

이렇게 Extension 함수 파일이 생깁니다.

public fun MyInfoTest.printInfo(): Unit {
  println("""name : changgyu, age : 30, job : programmer""")
}

이후 메인에서 MyInfoTest()를 생성해서 printInfo()를 찍어보면 

I/System.out: name : changgyu, age : 30, job : programmer

결과가 나오게 됩니다.

 

위 프로젝트는 제 깃허브에 올려두었습니다.

 

https://github.com/jakchang01/BlogProject/tree/master/CodeGenerate

 

GitHub - jakchang01/BlogProject: 개인 블로그 프로젝트를 올리는 곳입니다.

개인 블로그 프로젝트를 올리는 곳입니다. Contribute to jakchang01/BlogProject development by creating an account on GitHub.

github.com

 

아래는 kotlinpoet의 doc 사이트입니다.

https://square.github.io/kotlinpoet

 

KotlinPoet - KotlinPoet

KotlinPoet KotlinPoet is a Kotlin and Java API for generating .kt source files. Source file generation can be useful when doing things such as annotation processing or interacting with metadata files (e.g., database schemas, protocol formats). By generatin

square.github.io

 

댓글