반응형
애노테이션 프로세서 작성
- 여러 라운드(rounds)에 거쳐 소스 및 컴파일 된 코드를 처리 할 수 있다.
Filer 인터페이스
- 소스 코드, 클래스 코드 및 리소스를 생성할 수 있는 인터페이스
- 소스 코드 생성 유틸리티
<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 :
- 두번째 라운드
- Input : 첫번째 라운드의 Output(
MagicMoja.java
)
- Output : 없음
- Input : 첫번째 라운드의 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://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#processing
반응형
'Java > 기본' 카테고리의 다른 글
자바 람다(Java Lambda) - 2 (자바 함수형 인터페이스) (0) | 2021.06.16 |
---|---|
자바 람다(Java Lambda) - 1 (함수형 인터페이스, 람다 표현식) (0) | 2021.06.15 |
자바 다이나믹 프록시(Java Dynamic Proxy) - 2 (0) | 2021.06.01 |
자바 다이나믹 프록시(Java Dynamic Proxy) - 1 (0) | 2021.05.31 |
자바 리플렉션(Java Reflection) API - 클래스 정보 수정 및 실행 (0) | 2021.05.30 |