Skip to content

Latest commit

 

History

History
196 lines (137 loc) · 7.45 KB

5. 애노테이션 프로세서.md

File metadata and controls

196 lines (137 loc) · 7.45 KB

5. 애노테이션 프로세서

1. Lombok은 어떻게 동작하는 걸까

롬복의 동작 원리
  • 컴파일 시점에 애노테이션 프로세서를 사용해 소스코드의 AST(abstract syntax tree)를 조작한다.

    애노테이션 프로세서 : 컴파일 시점에 끼어들어서, 애노테이션이 붙은 곳에 새로운 코드를 만들어낸다.

    ATS : 애노테이션이 붙은 클래스 정보를 참조가 가능하다. 이 때 소스코드 정보를 수정할 수 있다.

    실제로 롬복이 컴파일된 바이트 코드를 살펴보면, getter, setter가 추가된 바이트 코드로 올라와 있다.

논란 거리 : 롬복은 해킹이다
  • 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스코드를 조작한다
  • 특히 이클립스의 경우엔 javaAgent를 사용해 컴파일러 클래스까지 조작하여 사용한다. 해당 클래스들 역시 공개된 API가 아니다 보니 버전 호환성에 문제가 생길 수 있고 언제라도 그런 문제가 발생해도 이상하지 않다.
  • 그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며 대안이 있지만 롬복의 모든 기능과 편의성을 대체하지는 못한다
대안
  • AutoValue / Immutables
  • 소스코드를 바꾸지 않고 새로운 소스코드를 만들어낸다.

2. 애노테이션 프로세서 실습

Processer 인터페이스 : 여러 rounds에 걸쳐서 소스 및 컴파일된 코드를 처리할 수 있다.

@Magic이 붙으면 특별한 처리를 하는 프로세서를 만들자.

// project user Usage
package annotationProcessor;

import mojaTest.Magic;

/**
 * Created by jyami on 2020/04/05
 */
@Magic
public interface Moja {
    String pullOut();
}

@Retention(RetentionPolicy.SOURCE) : 소스레벨에서 읽어서 애노테이션 프로세서가 처리하도록한다. 애노테이션 프로세서가 처리했을 때 특정한 소스파일을 만들고 컴파일러가 컴파일 하고, 클래스인 MagicMoja를 만들게 설계한다.

@Retention(RetentionPolicy.CLASS) : 바이트 코드까지 유지하겠다.

@Retention(RetentionPolicy.SOURCE)
public @interface Magic {
}

extends AbstractProcessor : 어노테이션 프로세서를 생성함을 알린다.

getSupportedAnnotationTypes() : 어떤 Annotation을 처리할 것인지를 세팅한다.

RoundEnvironment : 여러 라운드에 따라 처리한다.

getSupportedSourceVerison() : 지원하는 소스코드 버전을 정의한다.

process() : true = 이 어노테이션 타입으로 처리를 끝낸다. / False = 다른 어노테이션에서도 처리과정을 거쳐야한다.

process.getMessage() : 이용해서 ERROR / NOTE 타입을 지정하여 애너테이션이 올바르지 않을 경우 등을 에러로 지정할 수 있다.

// project Annotation Processor
package mojaTest;

import com.google.auto.service.AutoService;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.TypeSpec;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.io.IOException;
import java.util.Set;

/**
 * Created by jyami on 2020/04/05
 */
@AutoService(Process.class)
public class MagicMojaProcessor extends AbstractProcessor {
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Magic.class.getName());
    }

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

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for(Element element: elements){
            Name elementName = element.getSimpleName();
            if(element.getKind() != ElementKind.INTERFACE){
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"Magic aanotation can not be used on" + elementName);
            }else{
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,"Processing " + elementName);
            }
        return true;
    }
}

@AutoService(Process.class) : Annotation을 적용할 때 원래, META_INF를 넣어주면서 설명을 해야하는데, 이것이 자동으로 등록을 해준다.

컴파일 시점에 애노테이션 프로세서를 사용하여 META_INF/services/javax.annotation.processor.Processor 파일을 자동으로 생성해준다.

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for(Element element: elements){
            Name elementName = element.getSimpleName();
            if(element.getKind() != ElementKind.INTERFACE){
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,"Magic aanotation can not be used on" + elementName);
            }else{
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,"Processing " + elementName);
            }

            TypeElement typeElement = (TypeElement) element;
            ClassName className = ClassName.get(typeElement);

            MethodSpec pullOut = MethodSpec.methodBuilder("pullOut")
            .addModifiers(Modifier.PUBLIC)
            .returns(String.class)
            .addStatement("return $S", "Rabbit!")
            .build();

            TypeSpec magicMoja = TypeSpec.classBuilder("MagicMoja")
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(className)
                    .addMethod(pullOut)
                    .build();

            Filer filer = processingEnv.getFiler();
            try {
                JavaFile.builder(className.packageName(), magicMoja)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: "+ e);
            }
        }
        return true;

Filer 인터페이스를 이용해서 소스코드, 클래스 코드 및 리소스를 생성할 수 있다.

유틸리티 : Javapoet : 소스코드 생성 유틸리티를 이용하자.

3. 애노테이션 프로세서 정리

애노테이션 프로세서의 사용 예
  • 롬복 : 기존 코드를 변경한다.
  • AutoService : java.util.ServiceLoader용 파일 생성 유틸리티 : META를 생성하지 않아도 된다.
  • @Override : override가 아닌것이 있으면 알려준다
  • Dagger2 : 컴파일타임 DI 를 제공한다.
  • 안드로이드 라이브러리
    • ButterKinfe : @BindView (뷰 아이디와 애노테이션 붙인 필드 바인딩)
    • DeepLinkDispatch : 특정 URI 링크를 Activity로 연결할 때 사용
애노테이션 프로세서의 장점
  • 런타임 비용이 제로이다
  • 소스코드에 붙은 애노테이션 정보를 읽어서 컴파일러가 컴파일 하는 중에 새로운 소스코드를 생성하거나 소스코드를 변경할 수 있는 강력한 기능이다.
애노테이션 프로세서의 단점
  • 기존 클래스 코드를 변경할 때는 약간의 Hack이 필요하다