[android] Annotation Processing 에 대한 이야기
http://hannesdorfmann.com/annotation-processing/annotationprocessing101
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 를 만들어야 한다.
'프로그래밍 놀이터 > 안드로이드, Java' 카테고리의 다른 글
[android] 추가된 유용한 annotations (2) | 2017.08.02 |
---|---|
[android] VSYNC 가 뭐하는 녀석인지 간단히 이야기하면? (0) | 2017.08.01 |
[android] minSdk 를 올려 market update 하면 무슨 일이 발생하나요? (0) | 2017.07.30 |
[android] minSdkVersion vs. targetSdkVersion (0) | 2017.07.29 |
[android] ObjectAnimator 이야기 (0) | 2017.07.22 |
댓글