-
컴파일 시점에 애노테이션 프로세서를 사용해 소스코드의 AST(abstract syntax tree)를 조작한다.
애노테이션 프로세서 : 컴파일 시점에 끼어들어서, 애노테이션이 붙은 곳에 새로운 코드를 만들어낸다.
ATS : 애노테이션이 붙은 클래스 정보를 참조가 가능하다. 이 때 소스코드 정보를 수정할 수 있다.
실제로 롬복이 컴파일된 바이트 코드를 살펴보면, getter, setter가 추가된 바이트 코드로 올라와 있다.
- 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스코드를 조작한다
- 특히 이클립스의 경우엔 javaAgent를 사용해 컴파일러 클래스까지 조작하여 사용한다. 해당 클래스들 역시 공개된 API가 아니다 보니 버전 호환성에 문제가 생길 수 있고 언제라도 그런 문제가 발생해도 이상하지 않다.
- 그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며 대안이 있지만 롬복의 모든 기능과 편의성을 대체하지는 못한다
- AutoValue / Immutables
- 소스코드를 바꾸지 않고 새로운 소스코드를 만들어낸다.
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 : 소스코드 생성 유틸리티를 이용하자.
- 롬복 : 기존 코드를 변경한다.
- AutoService : java.util.ServiceLoader용 파일 생성 유틸리티 : META를 생성하지 않아도 된다.
- @Override : override가 아닌것이 있으면 알려준다
- Dagger2 : 컴파일타임 DI 를 제공한다.
- 안드로이드 라이브러리
- ButterKinfe : @BindView (뷰 아이디와 애노테이션 붙인 필드 바인딩)
- DeepLinkDispatch : 특정 URI 링크를 Activity로 연결할 때 사용
- 런타임 비용이 제로이다
- 소스코드에 붙은 애노테이션 정보를 읽어서 컴파일러가 컴파일 하는 중에 새로운 소스코드를 생성하거나 소스코드를 변경할 수 있는 강력한 기능이다.
- 기존 클래스 코드를 변경할 때는 약간의 Hack이 필요하다