Tuesday, October 6, 2015

Gradle 기반으로 Android Annotation Apt Processor 만들기.

이 포스팅은 Android Annotation Processing Setup using Gradle - Jens Diller을 기반으로 작성하였습니다.

Annotation Processing은 다른 포스팅에서도 설명했듯이 Compile Time에 java에서 annotation 에 대한 스캔과 처리를 하는 것 입니다. 물론 AndroidAnnotations Framework를 사용하면 편리한 기능을 많이 사용할 수 있지만 개발자라면 누구나 이것이 어떻게 동작하고 만들어지는 방법에 대해서 이해를 할 것이라고 생각합니다. 그래서 Android Project에서 Annotation Processor를 빌드하는 방법을 설명하고자 합니다.

1. Android Project 만들기

"Start a new Android Studio project" 을 선택하여 새로운 프로젝트를 생성합니다.

저는 Application Name : Sample , Company Domain : jurano.com , Package name : com.jurano.annotation으로 프로젝트를 진행하도록 하겠습니다. 이 부분은 사용자가 지정하기 나름이니 알아서 하시면 됩니다.

알맞은 Minimum SDK Version을 선택한 뒤 Next

일단 우리에게는 Activity가 필요없으니 "Add No Activity" 선택 하고 Finish를 선택해줍니다. 그러면 프로젝트 생성끝!

2. 새로운 모듈 추가하기.

Annotation Processor를 적용하려면 두개의 Module이 더 필요합니다. 생성할 모듈은 2개로 Annotation API를 정의하는 Java Library와 Annotation Processor 를 정의하는 라이브러리로 나누어지게 됩니다.File → New → New Module... 을 가면 아래와 같은 화면이 뜨게됩니다.

여기서 Java Library를 선택해줍니다.

선택한 뒤에 Library name : api, Java package name : com.jurano.annotation , Java class name : FullScreen 으로 정의한뒤 프로젝트를 생성시켜줍니다.그럼 API에 대한 Java library 생성이 완료된것이며 위와 똑같은 방식으로 Annotation Processor 프로젝트를 생성하여 줍니다.

Library name : compiler, Java package name : com.jurano.annotation , Java class name : AnnotationProcessor


3. Gradle Build 설정하기

Annotation Processor를 사용하기 위해서는 총 4가지의 build.gradle 파일을 수정해야 합니다. 앞으로 상위 build, api, compiler, app 순으로 설정해야 할 정보들에서 설명하도록 하겠습니다.

3.1. Project: Sample build.gradle (부모 Gradle 파일)

상위 build 파일에서 먼저 android-apt 프로세스 사용을 위한 빌드 정보를 입력해주어야 합니다. 기존에 생성되는 코드는 아래와 같이 구성 됩니다. 첫 줄에 작성되 있는 것과 같이 해당 gradle 파일은 Top-level 빌드 파일입니다. 따라서 아래에 있는 build 파일들에 대한 repostiory 는 jcenter이며, 위에 작성되어 있는 buildscritp 부부은 android를 build할때 사용되는 build tool에 대한 version을 나타내게 됩니다.

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

위에 android-apt에 대한 모듈을 dependency에 추가시켜야 하위 gradle 파일에서 해당되는 프로세서를 사용할 수 있습니다. 따라서 아래의 라인을 dependecy에 추가시켜주어야 합니다.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.3.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' // add
    }
}

현재 1.8 버전까지 나와 있으며 해당버전 사용에 있어 문제가 없기 때문에 최신버전을 이용하였습니다. But 1.7 버전 사용시 android processor가 정상적으로 실행되지 않았습니다. 이유는 잘 모르겠으나 1.7 버전은 사용하지 않는것이 좋습니다.(2틀날림...) 해당 프로젝트의 상위 모듈은 위의 한 줄만 추가하면 모두 끝났습니다.

3.2. Api library

api library의 경우에는 java이고 다른 library를 물리지 않고 사용자 정의 Annotation을 정의하는 부분이기 때문에 다른 사항은 필요하지 않습니다. 아래 코드는 아까 java library 생성시 생성된 코드로 다른 것을 추가하거나 제거할 필요는 없습니다.

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

하지만 위에 있는 포스팅에 "One more thing: If you're using Java 8 as your SDK, you need to ensure and tell the compiler to build using Java 7 or you will get the following error during pre-dexing:" 다음과 같은 문구가 존재합니다. 결국 Java 1.8을 사용하고 있다면 1.7을 사용하기 위해서 1.7하도록 하라는 것인데 이 부분에 있어 저는 1.7을 사용하기 때문에 추가하지 않았습니다.

sourceCompatibility = JavaVersion.VERSION_1_7  
targetCompatibility = JavaVersion.VERSION_1_7  

3.3. Compiler library

Compiler library의 경우에는 추가사항이 존재합니다. 먼저 기존 build정보는 아래와 같습니다.

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
}

위의 정보에서 내가 생성한 library인 api에 대한 사용권한을 추가해야합니다. 따라서 아래의 코드를 추가해줍니다. 아래의 dependency를 추가함으로써 compiler에서 api에서 생성한 annotation에 대한 사용이 가능하게 됩니다. 따라서 무조건 필요한 부분입니다.

apply plugin: 'java'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project (':api') // add
}

3.4. App Sample

마지막으로 App에 대한 build정보입니다. 해당 build 파일이 수정할 부분이 가장 많습니다. 먼저 아래는 기본 소스 코드 입니다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"

    defaultConfig {
        applicationId "com.jurano.annotation"
        minSdkVersion 22
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.0'
}

가장먼저 추가해야 하는 것은 android-apt를 사용할 것이라는 정의 입니다. 2번째 라인에 추가되어 있는 부분으로 이를 사용함을 정의합니다. 그리고 dependency 에 내가 사용하는 내 로컬 library인 api와 compiler에 대한 빌드 정보를 아래와 같이 추가해줍니다. 컴파일러 library의 경우에는 apt 를 이용하여 빌드되게 아래 코드와 같이 구성해야 합니다.

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'

android {
    compileSdkVersion 23
    buildToolsVersion "23.0.1"
    ...

}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:23.0.0'
    compile project(':api')
    apt project(':compiler')
}
    

위와 같이 작성하면 모든 build 파일에 대한 설정을 완료하였습니다. Annntation Processor에 대한 동작원리를 이해한다면 충분히 따라오실 수 있으셨을 겁니다.

4. Java Class 생성하기

이제 사용자가 정의한 새로운 Anntation과 그것에 대한 사용법을 보도록 하겠습니다. 이것도 동일하게 api → compiler → app 방식으로 진행하도록 하겠습니다.

4.1. API 클래스 구현

먼저 저희가 장으로 생성한 Class는 아래와같이 구성이 되어있습니다. 하지만 저희는 @interface가 필요 함으로 과감하게 바꿔줍니다. 또한 api라는 package안에 annotation을 만들도록 하겠습니다.

package com.jurano.annotation;

public class FullScreen {
}
package com.jurano.annotation.api;

public @interface FullScreen {
}

또한 타켓이나 용도 지정이 가능해지는데 이부분에 있어서는 이 포스팅을 참고해주시기 바랍니다. [What is java annotation 03] - Creating your Own Annotations?


4.2. Annotation Processor 정의

이제 Annotation에 대한 정의는 위와 같이 하면 끝난것입니다. 이제 다음으로 Annotation Processor를 정의하러 Compiler 모듈로 이동합니다. 기존에 있는 소스코드를 package를 추가하여 아래와 같이 이동시켰습니다. 또한 Annotation Processor를 사용하기 위해서 시작이 되는 class는 AbstractProcessor class를 상속받아야 함으로 변경된 코드는 이를 반영하였습니다.

package com.jurano.annotation;

public class AnnotationProcessor {
}
package com.jurano.annotation.compiler;

import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.TypeElement;

public class AnnotationProcessor extends AbstractProcessor{
    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
        return false;
    }
}

또한 AnnotationProcessor를 실행시키기 위해서는 몇가지 작업이 필요합니다. 따라서 "compiler/src/main/resources/META-INF/services"에 폴더를 생성해주고 "javax.annotation.processing.Processor"파일을 생성해주어야 합니다. 구조가 만들어지면 다음과 같은 프로젝트 형식이 되게 됩니다.

해당 파일의 내부에는 Annotation Processor가 처음 실행되는 class의 이름을 작성해줍니다. 저는 Annotation Processor의 시작점은 AnnotationProcessor로 할것이기 때문에 아래와 같이 작성해주었습니.

com.jurano.annotation.compiler.AnnotationProcessor

위와같이 작성해주고 함수 내부는 클래스를 생성하는 함수를 작성해야 하는데 함수를 작성하는 모듈에 대한 내용은 다른 포스팅을 통하여 설명하겠습니다. 여기서는 간단하게 Annotation이 있을 경우에 특정경로에 파일이 작성되도록 하는 것으로 대체 하겠습니다. 코드는 아래와 같습니다.

package com.jurano.annotation.compiler;

import com.jurano.annotation.api.FullScreen;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Collections;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;

public class AnnotationProcessor extends AbstractProcessor{

    @Override
    public Set getSupportedAnnotationTypes() {
        return Collections.singleton(FullScreen.class.getCanonicalName());
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public boolean process(Set set, RoundEnvironment roundEnvironment) {
        for(Element annotatedElement : roundEnvironment.getElementsAnnotatedWith(FullScreen.class)){
            TypeElement annotatedClass = (TypeElement) annotatedElement;
            // TODO Code generate
            String annotatedClassName = annotatedClass.getSimpleName().toString();

            try {
                FileOutputStream output = new FileOutputStream("E://log.txt");
                output.write(annotatedClassName.getBytes());
                output.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

        return false;
    }
}

4.3. Android Application에서 빌드되는지 확인하기.

마지막으로 Android Application에서 이것이 사용이 가능한 것인지 이제 구현하여 확인해보도록 하겠습니다. 만들어진 App에서 ConfirmFullScreen이라는 class 를 생성하고 Annotation을 위에다가 선언해 줍니다. 코드는 아래와 같습니다.

package com.jurano.annotation;

import com.jurano.annotation.api.FullScreen;

@FullScreen
public class ConfirmFullScreen {
}

빌드를 실행하게 되면 아래와 같이 파일에 텍스트가 작성되게 됩니다. 필요한 기능에 대해서 Java class를 generated 하도록 소스를 작성한다면 자주 반복되는 코드와 같은 기능을 손쉽게 작업을 할 수 있습니다.

소스는 GradleAndroidAnnotationProcessor에서 확인할 수 있습니다. 다음 포스팅은 소스코드 생성에 대해서 다뤄보도록 하겠습니다. 끝

5. 현재문제점

Compiler 부분의 java process가 죽지않아서 매번 프로세스를 강제 종료하고 있습니다. 이부분은 해결해서 다시올리도록하겠습니다.. ㅠ 참고한 프로젝트에서도 compiler가 죽지 않는 문제가 있어서 이 부분은 확인이 필요한 부분입니다.

No comments:

Post a Comment