반응형

애노테이션 프로세서 작성

Processor 인터페이스

  • 여러 라운드(rounds)에 거쳐 소스 및 컴파일 된 코드를 처리 할 수 있다.

Filer 인터페이스

  • 소스 코드, 클래스 코드 및 리소스를 생성할 수 있는 인터페이스

Javapoet

  • 소스 코드 생성 유틸리티
<dependency>
  <groupId>com.squareup</groupId>
  <artifactId>javapoet</artifactId>
  <version>1.13.0</version>
</dependency>

Magic.java

  • @Magic : 애노테이션 프로세서가 처리할 애노테이션
@Target(ElementType.TYPE) // Interface, Class, Enum
@Retention(RetentionPolicy.SOURCE)
public @interface Magic {
}

 

MagicMojaProcesser.java

  • @Magic 애노테이션을 처리할 애노테이션 프로세서
@AutoService(Processor.class)
public class MagicMojaProcesser extends AbstractProcessor {

    /**
     * 이 프로세서가 어떤 어노테이션을 처리할 것인가?
     * 프로세서가 처리할 애노테이션 앨리먼트를 넘겨준다.
     * @return
     */
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Magic.class.getName());
    }

    /**
     * 어떤 소스코드 버전을 지원하는가?
     * latestSupported 내부에 보면 지원되는 버전을 볼 수 있다. (RELEASE_11)
     * @return
     */
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    /**
     * 여기서 리턴하는 값이 ture이면 이 애노테이션 타입을 처리한 것이다.
     * 다른 프로세스들에게 이 애노테이션을 처리하라고 부탁하지 않는다.
     * @param annotations
     * @param roundEnv
     * @return
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //1. 이 애노테이션이 적절한 위치에 있는지 확인
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        //2. 인터페이스에 붙은 애노테이션만 처리하도록 설정
        for (Element element : elements) {
            Name elementName = element.getSimpleName();
            if (element.getKind() != ElementKind.INTERFACE) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Magic annotation can not be used on " + elementName);
            } else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + elementName);
            }

            //3. 해당 애노테이션 소스코드 파일 생성
            /**
             * 타입 앨리먼트를 가지고있으면 ClassName 으로 변환할 수 있다.
             * 해당 애노테이션이 붙은 클래스와 같은 패키지에 소스파일을 생성하기 위해 클래스 관련 값을 얻는다.
             */
            TypeElement typeElement = (TypeElement) element;
            ClassName className = ClassName.get(typeElement); //javapoet에서 제공하는 클래스. 클래스에 대한 여러가지 정보들을 참조할 수 있다.

            /**
             *
             * 메소드 생성 절차
             * 1. 메소드 이름 : pullOut
             * 2. 메소드 접근지시자 : public
             * 3. 반환 타입 : String
             * 4. 매개 변수 : 없음 (작성안해도됨)
             * 5. 반환 데이터 : Rabbit!
             */
            MethodSpec pullOut = MethodSpec.methodBuilder("pullOut")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return $S", "Rabbit!")
                    .build();

            //위에서 생성한 메소드를 클래스에 넣어야 한다.
            /**
             * 클래스 생성 절차
             * 1. 클래스 이름 : MagicMoja
             * 2. 클래스 접근지시자 : public
             * 3. 클래스 내부 메소드 : pullOut
             */
            TypeSpec magicMoja = TypeSpec.classBuilder("MagicMoja")
                    .addModifiers(Modifier.PUBLIC)
                    .addSuperinterface(className) //@Magic 애노테이션을 가지고 있는 인터페이스(className)를 구현하는 것을 의미
                    .addMethod(pullOut)
                    .build();

            //클래스에 메소드까지 추가한 것을 이제 소스파일에 쓰면 된다.
            //현재는 메모리상에 클래스를 정의한것과 같고 실제 파일은 없는 상태. (소스코드를 객체로 정의한 상태와 같은 의미)
            //Filer : 소스 코드, 클래스 코드 및 리소스를 생성할 수 있는 인터페이스. AbstractProcessor의 processingEnv 속성을 이용하여 얻을 수 있다.
            Filer filer = processingEnv.getFiler();

            /**
             * 소스파일이 Filer에 만들어지고 자바 컴파일러가 컴파일한 클래스가 생성이 된다.
             * javapoet이 제공하는 JavaFile 클래스를 이용하면 더 쉽게 소스코드를 생성할 수 있다.
             */
            try {
                JavaFile.builder(className.packageName(), magicMoja)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: " + e);
            }
        }
        return true;
    }
}

 

MagicMojaProcesser에서 만든 애노테이션 프로세서를 사용하려면 resources 폴더에 추가해주어야한다.

  • resources-META-INF-services 경로에 자바 애노테이션 프로세스의 풀패키지 경로이름을 가지는 파일을 생성한다. javax.annotation.processing.Processor
  • 이 파일의 내용은 애노테이션 프로세서를 구현한 클래스의 풀패키지 경로를 적어준다.
    • me.devhistory.MagicMojaProcesser
  • 단, 이대로 지금 빌드해서 사용할 수 는 없다.
    • 애노테이션 프로세서가 동작하는 시점이 소스를 컴파일 할 때인데 메이븐 빌드 과정 중 컴파일 하는 시점에서 애노테이션 프로세서가 동작하려고 한다. 이 시점에는 애노테이션 프로세서가 컴파일(MagicMojaProcesser.class)되지 않았기 때문에 존재하지 않는다.
    • 즉, 애노테이션 프로세서(MagicMojaProcessor.class)가 있어야 resources에 등록한 서비스(me.devhistory.MagicMojaProcesser)가 동작할 수 있는데 지금 resources에 등록한 서비스가 없는데 실행하려고해서 에러가 발생한다.
    • 구체적으로는 애노테이션 프로세서(MagicMojaProcessor.java)를 컴파일해서 만들때 조차도 서비스에 등록한 애노테이션 프로세서(me.devhistory.MagicMojaProcesser)를 이용하려고 해서 문제가 발생하는 것이다.
  • 이를 해결하기위해 잠깐 javax.annotation.processing.Processor 내부의 풀패키지 경로 me.devhistory.MagicMojaProcesser를 주석 처리 후 mvn clean install 을 수행한다. 그리고 나서 주석 제거 후 mvn install 을 수행하여 빌드한다.

 

AutoService

  • 서비스 프로바이더 레지스트리 생성기
    • 위에서 보면 애노테이션 프로세서를 사용하기 위한 메이븐 빌드 과정이 매우 번거롭다. AutoService를 사용하면 resources에 있는 매니페스토 파일을 컴파일할 때 자동으로 생성해준다. (AutoService도 애노테이션 프로세서이다.)
    • 사용법은 의존성을 추가하고 클래스 위에 @AutoService(Processor.class) 라는 애노테이션을 추가해주면 된다. 의미는 이 클래스를 프로세서로 등록해달라는 의미이다.
<dependency>
  <groupId>com.google.auto.service</groupId>
  <artifactId>auto-service</artifactId>
  <version>1.0-rc6</version>
</dependency>
@AutoService(Processor.class)
public class MagicMojaProcesser extends AbstractProcessor {
		...
    ...
		...
}
  • 컴파일 시점에 애노테이션 프로세서를 사용하여 META-INF/services/javax.annotation.processor.Processor 파일을 자동으로 생성해 준다.
  • jar 파일을 열어보면 내부에 매니페스토 파일이 생성된 것을 볼 수 있다.

 

애노테이션 프로세서 사용

  • 위에서 구현한 애노테이션 프로세서를 사용하려면 의존성을 추가해주어야 한다.
  • 애노테이션 프로세서를 작성한 메이븐의 정보를 애노테이션 프로세서를 사용하려는 프로젝트에 의존성 설정에 추가해준다..

애노테이션 프로세서를 사용할 프로젝트의 구조는 다음과 같다.

Moja.java

  • @Magic 애노테이션을 사용하는 인터페이스
  • 애노테이션 프로세서 의존성 추가하지 않은 경우, @Magic 에서 컴파일 에러 발생
@Magic
public interface Moja {
    String pullOut();
}
  • 또한 애노테이션 프로세서에서 설정한 타입이 아닌 곳에 해당 애노테이션을 사용 시, 빌드할 때 에러가 발생한다. java: Magic annotation can not be used on MyMoja
@Magic
public class MyMoja {
}

 

App.java

  • MagicMoja : MagicMojaProcesser가 애노테이션 프로세싱을 통해 생성해낼 클래스
public class App 
{
    public static void main( String[] args )
    {
        Moja moja = new MagicMoja();
        System.out.println(moja.pullOut()); //Rabbit!
    }
}

 

  • 애노테이션 프로세서로 생성한 MagicMoja를 사용하기 위해서는 애노테이션 프로세싱 사용을 체크하고 애노테이션 프로세서로 생성된 파일의 폴더를 소스 디렉토리로 설정하여 IDE에서 인식할 수 있도록 해야한다.
  • MagicMoja 파일을 직접 작성하지 않았지만 애노테이션 프로세서를 통해 생성하여 소스코드 상에서 사용할 수 있다.

 

애노테이션 프로세서 동작 원리

애노테이션 프로세서는 라운드 개념으로 동작한다. (API 문서 참고 Processor 인터페이스 ) 각 라운드마다 어떤 특정한 애노테이션들이 이 프로세서가 처리할 애노테이션을 가지고 있는 엘리먼트를 찾으면 프로세서한테 처리를 시킨다. 처리된 결과는 다음 라운드로 넘어갈 수도 있다. 여러 라운드를 걸쳐서 마치 스프링 시큐리티의 필터 체인과 비슷하다고 생각할 수 있다.

 

라운드 처리(Processing Rounds)

  • 프로세싱 라운드는 어노테이션 프로세서의 process() 를 호출한다.
  • MagicMojaProcesser는 한번 인스턴스화 된다. (새로운 프로세서가 매 라운드마다 생성되지는 않는다.)
  • 하지만 새로운 소스파일이 생겨난다면 process() 는 여러번 호출 될 수 있다.
  • 현재 코드를 기준으로 애노테이션 프로세서의 라운드 동작과정은 다음과 같다.
    • 첫번째 라운드
      • Input : App.java, Moja.java
      • Output : MagicMoja.java (새로운 소스파일 생성, process() 호출)
    • 두번째 라운드
      • Input : 첫번째 라운드의 Output(MagicMoja.java)
      • Output : 없음
    • 세번째 라운드
      • Input : 두번째 라운드의 Output(없음)
      • Output : 없음

 


[참고자료]

더 자바, 코드를 조작하는 다양한 방법, 백기선

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

http://notatube.blogspot.com/2010/12/project-lombok-creating-custom.html

https://medium.com/@jintin/annotation-processing-in-java-3621cb05343a

https://medium.com/@iammert/annotation-processing-dont-repeat-yourself-generate-your-code-8425e60c6657

https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#processing

 

반응형

+ Recent posts