APT demo 01
APT(Annotation Processing Tool) 를 사용하여 컴파일 타임(Compile Time)에 Annotation을 검사하는 예
JDK 1.5부터 apt.exe라는 툴이 자바 개발툴로서 포함되어 있다. 이 툴을 이용하면 Compile Time에 Annotation을 조사하여 지시에 따라 보조파일을 생성할 수 있으며, 컴파일 경로를 출력하거나 소스내용을 확인하고 그 소스에 반응하는 작업을 컴파일시에 할 수 있게 된다.
APT를 사용하면 다음과 같은 작업을 수행하게 된다.
2. 지정한 소스파일들이 Annotation을 사용하고 있다면, 개발자가 지정한 AnnotationProcessorFactory 를 찾는다.
3. APT는 AnnotationProcessorFactory 에서 AnnotationProcessor객체를 생성하기 위한 기본 데이터를 설정하고 getProcessorFor()를 호출하여 AnnotationProcessor객체를 리턴받는다.
4. APT는 리턴된 AnnotationProcessor객체를 이용하여 process()호출한다.
5. process()에서는 Annotation 의 내용을 검사하여 파일을 생성하거나 경고 메시지를 출력하는 등 개발자가 필요로 하는 작업을 한다(개발자가 오버라이드함)
6. 만약 process()에서 새로 생성한 소스파일이 있고 그 소스파일 안에도 Annotation이 있다면 위와 동일한 절차가 진행된다.
7. 더 이상 새로 생성되는 파일이 없다면 APT는 소스파일들(새로 생성된 소스파일 포함)을 컴파일한다.
반드시 새로운 파일을 생성해야 하는 것은 아니고 보조파일이나 새로운 소스파일을 개발자의 손을 거치지 않고도 편리하게 Annotation을 이용하여 지시하고 컴파일시에 자동으로 생성할 수 있다는 것이다.
주목할 만한 일은 컴파일시에도 개발자가 지정한 로직을 실행할 수 있다는 것인데, 이 때 실행되는 로직을 담을 클래스를 개발자가 작성해야 한다. 컴파일 직전에 Annotation을 검사하고 Annotation에 따라 반응하는 로직을 담을 클래스는 AnnotationProcessor의 process()인데 개발자는 이 클래스를 상속하여 파생된 클래스를 작성하여야 한다. 그리고 AnnotationProcessor의 객체는 AnnotationProcessorFactory 클래스에 의해서 APT에 리턴되기 때문에 개발자는 AnnotationProcessorFactory 클래스를 상속하여 파생된 클래스를 작성해야 한다.
1. AnnotationProcessorFactory : APT에서 이 클래스의 getProcessorFor()를 호출하면 APT에게 AnnotationProcessor객체를 리턴한다.
2. AnnotationProcessor : APT에서 이 클래스의 process()를 호출하여 Annotation처리를 시작한다.
그러므로 APT를 실행하기 위해서는 팩토리 클래스와 프로세서 클래스가 필요하고 Annotation검사의 대상이 되는 소스파일들이 필요하며 다음과 같은 명령으로 APT를 실행할 수 있다.
예) factory.jar안의 annotest.factory.MyFactory클래스가 팩토리 클래스이고 동일 jar파일안에 프로세서 클래스가 저장되어 있다고 할 때는 다음과 같은 명령으로 APT를 실행할 수 있다.
AnnotationProcessorFactory , AnnotationProcessor 등을 사용하기 위해서는 tools.jar에 저장된 라이브러리를 사용하기 때문에 시스템의 CLASSPATH환경변수에 tools.jar파일의 경로를 등록해 주어야 한다.
테스트에 사용된 소스파일들 (팩토리와 프로세서 클래스는 컴파일하여 jar파일로 압축하는 것이 편리한 사용법이다)
NoteAnnotationProcessorFactory.java
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
public class NoteAnnotationProcessorFactory implements AnnotationProcessorFactory {
/**
* Returns a note annotation processor.
*
* @return An annotation processor for "Note" annotations if requested,
* otherwise, returns the NO_OP annotation processor.
*/
@Override
public AnnotationProcessor getProcessorFor(
Set<AnnotationTypeDeclaration> declarations,
AnnotationProcessorEnvironment env) {
AnnotationProcessor result;
if(declarations.isEmpty()) {
result = AnnotationProcessors.NO_OP;
}
else {
// Next Step - implement this class:
result = new NoteAnnotationProcessor(env);
}
return result;
}
/**
* This factory only builds processors for the "Note" annotation.
* @return a collection containing only the "Note" annotation name.
*/
@Override
public Collection<String> supportedAnnotationTypes() {
return Collections.singletonList("Note");
}
/**
* No options are supported by this annotation processor.
* @return an empty list.
*/
@Override
public Collection<String> supportedOptions() {
return Collections.emptyList();
}
}
NoteAnnotationProcessor.java
import java.util.*;
import com.sun.mirror.apt.*;
import com.sun.mirror.declaration.*;
import com.sun.mirror.util.*;
public class NoteAnnotationProcessor implements AnnotationProcessor {
private AnnotationProcessorEnvironment environment;
private AnnotationTypeDeclaration noteDeclaration;
public NoteAnnotationProcessor(AnnotationProcessorEnvironment env) {
environment = env;
// get the annotation type declaration for our 'Note' annotation.
// Note, this is also passed in to our annotation factory - this
// is just an alternate way to do it.
noteDeclaration = (AnnotationTypeDeclaration) environment.getTypeDeclaration("Note");
}
@Override
public void process() {
// Get all declarations that use the note annotation.
Collection<Declaration> declarations = environment.getDeclarationsAnnotatedWith(noteDeclaration);
for (Declaration declaration : declarations) {
processNoteAnnotations(declaration);
System.out.println("-----------------------------------------------------");
}
}
private void processNoteAnnotations(Declaration declaration) {
// Get all of the annotation usage for this declaration.
// the annotation mirror is a reflection of what is in the source.
Collection<AnnotationMirror> annotations = declaration.getAnnotationMirrors();
System.out.println("Annotation 수:"+annotations.size());
// iterate over the mirrors.
for (AnnotationMirror mirror : annotations) {
// if the mirror in this iteration is for our note declaration...
if(mirror.getAnnotationType().getDeclaration().equals(noteDeclaration)) {
// print out the goodies.
SourcePosition position = mirror.getPosition();
Map<AnnotationTypeElementDeclaration, AnnotationValue> values = mirror.getElementValues();
System.out.println("Declaration: " + declaration.toString());
System.out.println("Position: " + position);
System.out.println("Values:");
for (Map.Entry<AnnotationTypeElementDeclaration, AnnotationValue> entry : values.entrySet()) {
AnnotationTypeElementDeclaration elemDecl = entry.getKey();
AnnotationValue value = entry.getValue();
System.out.println(" " + elemDecl + "=" + value);
}
}
}
}
}
위에 제시된 팩토리, 프로세서 클래스는 컴파일하여 jar파일로 압축하여 사용하는 것이 편리할 것이다. 즉, 위의 2개 클래스는 아래의 소스파일을 컴파일할 때 실행될 로직을 가진 파일이므로 아래의 소스를 컴파일하기 전에 실행될 준비가 되어 있어야 한다.
Note.java (Note Annotation의 선언)
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.PACKAGE, ElementType.FIELD})
public @interface Note {
String value();
String priority() default "MEDIUM";
}
MyClass.java (위에 선언된 Note Annotation을 사용하는 클래스)
public class MyClass {
@Note("This field isn't finished")
private String fieldA;
@Note("Constructor isn't finished")
public MyClass() {
}
@Note("methodA isn't finished")
public void methodA() {
}
}