본문 바로가기
프로그래밍 놀이터/안드로이드, Java

[android] Annotation Processing 에 대한 이야기

by 돼지왕 왕돼지 2017. 7. 31.
반응형

 [android] Annotation Processing 에 대한 이야기


http://hannesdorfmann.com/annotation-processing/annotationprocessing101


', .jar, .java, 3rd part developer, 64k method limit, @Factory Annotation, Abstract, abstractprocessor, Action, Android, annotating, annotation, annotation processing, annotation processor, Annotations, asElement, astype, authservice, autoservice, beginControlFlow, beginMethod, beginType, boiler plate, buildpath, byte code, CLASS, CLOSE, Code Generation, Compile, compile time, Conclusion, Crash, createSourceFile, datamodel, DeclaredType, Design Pattern, ElementKind, Elements, Elements and TypeMirrors, elements util, elementutils, emitEmptyLine, emitPackage, emitStatement, empty constructor, endControlFlow, endmethod, endtype, Error Message, Evaluation, exception, Factory, file, filer util, Filter, fully qualified name, getCanonicalName, getElementsAnnotatedWith, getEnclosedElements, getEnclosingElement, getKind, getPackageOf, getQualifiedName, getsimplename, getsupportedannotationtypes, getsupportedsourceversion, getTypeMirror, guava, IdAlreadyUsedException, init, input, instanceof, Intellij, Interface, isValidClass, jar, java app, java code, java5, JAVA6, javac, JavaFileObject, javawriter, jvm, jvm crash, kind.error, latestsupported, Level, main 함수, Messager, META-INF, Method, MirroredTypeException, modifier, modifier.public, notice, OOP, openwriter, output, override, Pack, package, Procesing, process, Processing Rounds, processingenvironment, processor, Public, Register Your Processor, release_6, retention, retentionpolicy, Round, roundevnironment, Scanning, Searching for @Factory, Separation of processor and annotation, Services, Set, sourceversion, StackTrace, superclass, typeelement, TypeKind, typemirror, types, types util, typeutils, Warning, writer, [android] Annotation Processing 에 대한 이야기, 상속, 파일 생성

The Basics

-

annotation processing 은 compile time 에 annotation 을 확인하여 어떤 action 을 하는 것을 이야기한다.



-

annotation processing 은 Java 5 부터 가능하다.

그러나 사용할만한 API 는 Java 6 에 release 되었다.



-

annotation processor 는 java code 나 byte code 를 input 으로 받아서 java 파일로 output 을 생성한다.

이 생성된 output 은 compile time 에 자동생성되는 녀석으로 read only 로 작동한다고 보면 된다.




Abstract Processor

-

annotation processor 는 AbstractProcessor 를 상속한다.

이 녀석은 다음의 function 들을 가지고 있다.


synchronized void init(ProcessingEnvironment env)

     모든 annotation processor 는 empty constructor 를 반드시 갖는다.

     그러나 init 을 따로 갖고 있으며, ProcessingEnvironment 를 param 으로 받는다.

     ProcessingEnvironment 는 Elements, Types, Filer 와 같은 util class 를 제공한다.


boolean  process(Set<? extends TypeElement> annotations, RoundEnvironment env)

     processor 의 main 함수라고 보면 된다.

     annotation 에 대한 scanning, evaluation, processing 을 수행하고 java file 을 만들어낸다.

     RoundEnvironment 를 통해서 특정 annotation 을 query 할 수 있다.


Set<String> getSupportedAnnotationTypes()

     해당 annotation processor 가 어떤 type 에 대해 처리할 것인지 등록할 수 있다.

     return 되는 Set 안의 String 은 fully qualified name 이다.

     

SourceVersion getSupportedSourceVersion()

     어떤 Java version 을 이용할지를 명시한다.

     보통 SourceVersion.latestSupported() 를 return 한다.

     특정 상황에서 SourceVersion.RELEASE_6 과 같이 특정 버전을 return 할 수도 있다.

     그러나 역시 lastestSupported 를 호출해서 return 하는 것이 권장된다.



-

Java7  버전에서는 getSupportedAnnotationTypes() 와 getSupportedSourceVersion() 의 overriding 없이annotation 을 이용하여 정의하여 사용할 수 있다.


@SupportedSourceVersion(SourceVersion.latestSupported())

@SupportedAnnotationTypes({

     // set of full qualified annotation type names

})


그러나 호환성을 고려하여 annotation 보다는 overriding 하는 것이 추천된다.



-

annotation processor 는 그 자체 jvm 에서 작동된다.

javac 를 수행하면 annotation processing 을 위해 jvm 을 구동시킨다.




Register Your Processor


-

javac 에 annotation processor 를 등록하기 위해서는 .jar 파일을 제공해야 한다.

.jar 파일을 생성할 때 javax.annoation.processing.Processor 를 META-INF/services 에 넣어 함께 pack 해야 한다.

ExampleProcessor.jar

     -com

          -example

               -ExampleProcessor.class


     - META_INF

          - services

               - javax.annotation.processing.Processor



-

javax.annotation.processing.Processor 에는 full qualified class name 들을 new line 으로 구분해서 넣어야 한다.


com.example.ExampleProcessor

com.foo.OtherProcessor



-

해당 jar 파일을 buildpath 에 등록하면 자동으로 javax.annotation.processing.Processor 파일을 읽고, 정의한processor 를 annotation processor 에 등록한다.




@Factory Annotation


-

@Target(ElementType.TYPE)

@Retention(RetentionPolicy.CLASS)

public @interface Factory{

     Class type();

     String id();

}


-

@Factory{

     id = “Margherita”,

     type = “Meal.class

}

public class MargheritaPizza implements Meal {

      @Override 

public float getPrice(){

          return 6f;

      }

}


-

annotating 은 상속되지 않는다.

예를 들어 class X 를 annotation 한다고, class Y extends X 인 Y 가 자동으로 annotating 되지 않는다.



-

@Factory 는 다음과 같은 규칙을 갖는다.


1. class 만 annotate 될 수 있다.

왜냐하면 interface 와 abstract class 는 new 를 통해 객체화될 수 없기 때문이다.


2. annotate 되는 class 는 empty default constructor 를 반드시 가지고 있어야 한다.


3. type 에 명시된 녀석을 상속해야만 한다.


4. 같은 type 값으로 annotation 이 적용되는 녀석들은 grouping 된다.

type = Meal.class 라면, MealFactory 라는 java 코드가 생겨난다.


5. id 는 unique 해야 한다.




The Processor


-

@AutoService(Processor.class)

public class FactoryProcessor extends AbstractProcessor{

     private Types typeUtils;

     private Elements elementUtils;

     private Filer filer;

     private Messager messager;

     private Map<String, FactoryGroupedClasses> factoryClasses = new LinkedHashMap<String, FactoryGroupedClasses>();


     @Override

     public synchronized void init(ProcessingEnvironment processingEnv){

          super.init(processingEnv);

          typeUtils = processingEnv.getTypeUtils();

          elementUtils = processingEnv.getElementUtils();

          filer = processingEnv.getFiler();

          messager = processingEnv.getMessager();

     }


     @Override

     public Set<String> getSupportedAnnotationTypes(){

          Set<String> annotations = new LinkedHashSet<String>();

          annotations.add(Factory.class.getCanonicalName());

          return annotations;

     }


     @Override

     public SourceVersion getSupportedSourceVersion(){

          return SourceVersion.latestSupported();

     }


     @Override

     public boolean process(Set<? extends TypeEelement> annotations, RoundEnvironment roundEnv){

          ...

     }

}



-

@AutoService(Processor.class) 는 google 에 의해 개발된 annotation processor 이다.

META-INF/services/javax.annotation.processing.Processor 파일을 생성한다.




Elements and TypeMirrors


-

Elements util class 는 Element class 를 조작하는데 사용한다.

Types util class 는 TypeMirror 를 조작하는데 사용한다.

Filer util class 는 File 을 생성하는데 사용한다.



-

annotation processing 은 java source code 를 scan 한다.

source 의 모든 part 는 Element 로 구분된다.

package, class, method 등 모두가 Element 가 된다.


package com.example;    // PackageElement


public class Foo {        // TypeElement

    private int a;        // VariableElement

    private Foo other;     // VariableElement


    public Foo () {}     // ExecuteableElement


    public void setA (     // ExecuteableElement

                     int newA    // TypeElement

                     ) {}

}



-

source code 를 xml 처럼 읽으면 된다.

다음과 같이 iterate 할 수 있다.


TypeElement fooClass = ... ;

for (Element e : fooClass.getEnclosedElements()){ // iterate over children

    Element parent = e.getEnclosingElement();  // parent == fooClass

}



-

Element 로부터 어떠한 정보를 얻기 위해서는 TypeMirror 를 사용해야 한다.

Element 의 TypeMirror 는 element.asType() 을 통해 얻을 수 있다.




Searching for @Factory


-

다음 코드를 통해 @Factory 로 annotate 된 모든 Element 를 찾을 수 있다.


for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {

      // Check if a class has been annotated with @Factory

      if (annotatedElement.getKind() != ElementKind.CLASS) {

           error(annotatedElement, "Only classes can be annotated with @%s”, Factory.class.getSimpleName());

           return true; // Exit processing

      }

}


private void error(Element e, String msg, Object... args) {

    messager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args), e);

}


class  가 TypeElements 이긴 하지만 instanceof TypeElement 로 체크하는 것은 옳지 않다.

interface 도 TypeElement 이기 때문이다.

그래서 annotation processing 에서는 instanceof 보다는 TypeMirror 와 함께 ElementKind 나 TypeKind 를 사용하는 것이 추천된다.






Error Handling


-

init() 안의 Messager 는 annotation processor 가 error message, warning, notice 등을 전달하는 데 사용된다.

annotation processor 의 개발자를 위한 것이 아니라, 이 annotation 을 사용하는 3rd party developer 를 위함이다.



-

Messager 를 통해 전달되는 로그에는 여러 가지 level 이 있는데,

이 중 가장 중요한 것은 Kind.ERROR 이다.

Kind.ERROR 는 processor 가 processing 을 실패했다는 의미이다.



-

이는 Exception 과는 조금 다르다.

process() 에서 exception 을 던지면 annotation processor 가 도는 jvm 이 crash 가 난다.

그렇게 되면 3rd party developer 는 이해할 수 없는 에러와 함께 javac 로부터 error 를 얻게 된다.

이 때 에러는 FactoryProcessor 로부터 발생한 stacktrace 가 전달된다.



-

Messager class 는 이쁜(?) error message 를 전달한다.

추가적으로 에러를 발생시킨 element 를 담을 수 있고, IntelliJ 와 같은 Modern IDE 에서는 해당 element 로 바로 점프하는 것도 지원한다.



-

Messager 가 제대로 에러를 표기하기 위해서는 annotation processor 가 crash 없이 코드수행을 끝내야 한다.

그래서 Messager 에 에러를 전달한 경우 보통 return 을 바로 해준다.




DataModel


-

Annotation Processor 도 java app 이기 때문에, OOP, interface, design pattern 등을 고려하여 짜는 것이 좋다.



-

public class FactoryAnnotatedClass {


  private TypeElement annotatedClassElement;

  private String qualifiedSuperClassName;

  private String simpleTypeName;

  private String id;


  public FactoryAnnotatedClass(TypeElement classElement) throws IllegalArgumentException {

    this.annotatedClassElement = classElement;

    Factory annotation = classElement.getAnnotation(Factory.class);

    id = annotation.id();


    if (StringUtils.isEmpty(id)) {

      throw new IllegalArgumentException(

          String.format("id() in @%s for class %s is null or empty! that's not allowed",

              Factory.class.getSimpleName(), classElement.getQualifiedName().toString()));

    }


    // Get the full QualifiedTypeName

    try {

      Class<?> clazz = annotation.type();

      qualifiedSuperClassName = clazz.getCanonicalName();

      simpleTypeName = clazz.getSimpleName();

    } catch (MirroredTypeException mte) {

      DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror();

      TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement();

      qualifiedSuperClassName = classTypeElement.getQualifiedName().toString();

      simpleTypeName = classTypeElement.getSimpleName().toString();

    }

  }


  /**

   * Get the id as specified in {@link Factory#id()}.

   * return the id

   */

  public String getId() {

    return id;

  }


  /**

   * Get the full qualified name of the type specified in  {@link Factory#type()}.

   *

   * @return qualified name

   */

  public String getQualifiedFactoryGroupName() {

    return qualifiedSuperClassName;

  }



  /**

   * Get the simple name of the type specified in  {@link Factory#type()}.

   *

   * @return qualified name

   */

  public String getSimpleFactoryGroupName() {

    return simpleTypeName;

  }


  /**

   * The original element that was annotated with @Factory

   */

  public TypeElement getTypeElement() {

    return annotatedClassElement;

  }

}



-

annotation processing 은 compile 전에 작동하기 때문에 2가지를 고려해야 한다.


1. class 가 이미 compile 이 되어 있다.

     third party .jar 가 @Factory annotation 을 가진 compile 된 .class 를 가지고 있을 때.

     이 때는 위의 try block 에서 하듯 바로 class 에 접근해서 사용할 수 있다.


2. class 가 compile 이 안 되어 있을 때.

     보통 마딱뜨리는 경우로 source code 가 compile 이 안 된 경우다.

     이 때는 catch block 에서 하듯 MirroredTypeException 을 통해 DeclaredType, TypeElement 를 순차적으로 구할 수 있다.



-

public class FactoryGroupedClasses {


  private String qualifiedClassName;


  private Map<String, FactoryAnnotatedClass> itemsMap =

      new LinkedHashMap<String, FactoryAnnotatedClass>();


  public FactoryGroupedClasses(String qualifiedClassName) {

    this.qualifiedClassName = qualifiedClassName;

  }


  public void add(FactoryAnnotatedClass toInsert) throws IdAlreadyUsedException {


    FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId());

    if (existing != null) {

      throw new IdAlreadyUsedException(existing);

    }


    itemsMap.put(toInsert.getId(), toInsert);

  }


  public void generateCode(Elements elementUtils, Filer filer) throws IOException {

    ...

  }

}




Matching Criteria


-

public class FactoryProcessor extends AbstractProcessor {


  @Override

  public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {


    for (Element annotatedElement : roundEnv.getElementsAnnotatedWith(Factory.class)) {


      ...


      // We can cast it, because we know that it of ElementKind.CLASS

      TypeElement typeElement = (TypeElement) annotatedElement;


      try {

        FactoryAnnotatedClass annotatedClass =

            new FactoryAnnotatedClass(typeElement); // throws IllegalArgumentException


        if (!isValidClass(annotatedClass)) {

          return true; // Error message printed, exit processing

         }

       } catch (IllegalArgumentException e) {

        // @Factory.id() is empty

        error(typeElement, e.getMessage());

        return true;

       }


          ...

   }



 private boolean isValidClass(FactoryAnnotatedClass item) {


    // Cast to TypeElement, has more type specific methods

    TypeElement classElement = item.getTypeElement();


    if (!classElement.getModifiers().contains(Modifier.PUBLIC)) {

      error(classElement, "The class %s is not public.",

          classElement.getQualifiedName().toString());

      return false;

    }


    // Check if it's an abstract class

    if (classElement.getModifiers().contains(Modifier.ABSTRACT)) {

      error(classElement, "The class %s is abstract. You can't annotate abstract classes with @%",

          classElement.getQualifiedName().toString(), Factory.class.getSimpleName());

      return false;

    }


    // Check inheritance: Class must be childclass as specified in @Factory.type();

    TypeElement superClassElement =

        elementUtils.getTypeElement(item.getQualifiedFactoryGroupName());

    if (superClassElement.getKind() == ElementKind.INTERFACE) {

      // Check interface implemented

      if (!classElement.getInterfaces().contains(superClassElement.asType())) {

        error(classElement, "The class %s annotated with @%s must implement the interface %s",

            classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),

            item.getQualifiedFactoryGroupName());

        return false;

      }

    } else {

      // Check subclassing

      TypeElement currentClass = classElement;

      while (true) {

        TypeMirror superClassType = currentClass.getSuperclass();


        if (superClassType.getKind() == TypeKind.NONE) {

          // Basis class (java.lang.Object) reached, so exit

          error(classElement, "The class %s annotated with @%s must inherit from %s",

              classElement.getQualifiedName().toString(), Factory.class.getSimpleName(),

              item.getQualifiedFactoryGroupName());

          return false;

        }


        if (superClassType.toString().equals(item.getQualifiedFactoryGroupName())) {

          // Required super class found

          break;

        }


        // Moving up in inheritance tree

        currentClass = (TypeElement) typeUtils.asElement(superClassType);

      }

    }


    // Check if an empty public constructor is given

    for (Element enclosed : classElement.getEnclosedElements()) {

      if (enclosed.getKind() == ElementKind.CONSTRUCTOR) {

        ExecutableElement constructorElement = (ExecutableElement) enclosed;

        if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers()

            .contains(Modifier.PUBLIC)) {

          // Found an empty constructor

          return true;

        }

      }

    }


    // No empty constructor found

    error(classElement, "The class %s must provide an public empty default constructor",

        classElement.getQualifiedName().toString());

    return false;

  }

}



-

isValidClass() 에서는 annotation 이 따라야 하는 rule 을 체크한다.

다음과 같은 것들을 체크할 수 있다.


     Modifier 가 public 인지

     Modifier 에 Abstract 가 있는지

     어떤 class 를 상속했는지

     SuperClass 가 interface 인지

     public empty constructor 가 있는지 






Code Generation


-

public void generateCode(Elements elementUtils, Filer filer) throws IOException {


    TypeElement superClassName = elementUtils.getTypeElement(qualifiedClassName);

    String factoryClassName = superClassName.getSimpleName() + SUFFIX;


    JavaFileObject jfo = filer.createSourceFile(qualifiedClassName + SUFFIX);

    Writer writer = jfo.openWriter();

    JavaWriter jw = new JavaWriter(writer);


    // Write package

    PackageElement pkg = elementUtils.getPackageOf(superClassName);

    if (!pkg.isUnnamed()) {

      jw.emitPackage(pkg.getQualifiedName().toString());

      jw.emitEmptyLine();

    } else {

      jw.emitPackage("");

    }


    jw.beginType(factoryClassName, "class", EnumSet.of(Modifier.PUBLIC));

    jw.emitEmptyLine();

    jw.beginMethod(qualifiedClassName, "create", EnumSet.of(Modifier.PUBLIC), "String", "id");


    jw.beginControlFlow("if (id == null)");

    jw.emitStatement("throw new IllegalArgumentException(\"id is null!\")");

    jw.endControlFlow();


    for (FactoryAnnotatedClass item : itemsMap.values()) {

      jw.beginControlFlow("if (\"%s\".equals(id))", item.getId());

      jw.emitStatement("return new %s()", item.getTypeElement().getQualifiedName().toString());

      jw.endControlFlow();

      jw.emitEmptyLine();

    }


    jw.emitStatement("throw new IllegalArgumentException(\"Unknown id = \" + id)");

    jw.endMethod();


    jw.endType();


    jw.close();

  }



-

Filer 에서 제공하는 JavaFileObject, JavaWriter 등을 사용해서 코드를 생성할 수 있다.




Processing Rounds


-

annotation processing 은 여러 round 에 걸쳐 진행이 된다.

매 회에 processor 는 지난 round 에 생성된 소스 코드들을 다시 processing 을 한다. ( 다음 round )



-

매 round 마다 Processor 가 새로 생성되지는 않고, process 함수만 여러 번 불린다.

이 때 경우에 다라 “Attempt to recreate a file for type xxx.xxx.xxx.xxx” 에러가 날 수 있다.

이를 기억하고 새로 생성된 코드에도 설정한 annotation 에 대한 processing 을 해야 하는 경우 이를 방지하는 코드가 들어가야 한다.




Separation of processor and annotation

-

annotation 과 processor 를 코드에 그냥 넣게 되면 보통 guava 를 사용하게 되면 64k method limit 문제가 생길 수 있다.

또한 다른 사람에게 이식하기도 어렵다.

그래서 그 둘을 분리하는 작업이 필요하다.




Conclusion

-

annotation processing 은 매우 강력하며 boiler plate 코드 생성을 막는 것이 주 목적이다.



-

annotation processing 에도 2가지 문제가 있다.

     ElementUtils, TypeUtils, Messager 를 다른 클래스에서 쓰려면 어떻게든 그들을 전달해야 한다.

     Elements 에 대한 query 를 만들어야 한다.





반응형

댓글