Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Annotation Processing 101

Annotation Processing 101

Writing Java application can be annoying because the Java programming language requires to write boilerplate code. Writing Android apps makes no difference. Moreover, on Android you have to write a lot of code for doing simple things like setting up a Fragment with arguments, implement the Parcelable interface, define ViewHolder classes for RecyclerViews and so on.

Fortunately, the writing of boilerplate code can be reduced by using Annotation processing, a feature of the Java compiler that allows to setup hooks to generate code based on annotations.

The aim of the talk is to understand the annotation processing fundamentals and to be able to write an annotation processor. Furthermore, existing annotation processors for Android will be showcased.

Video Link: https://www.youtube.com/watch?v=43FFfTyDYEg

Hannes Dorfmann

June 04, 2015
Tweet

More Decks by Hannes Dorfmann

Other Decks in Programming

Transcript

  1. Annotation Processing • Java 5 (Sep. 2004) • JSR 269:

    6 months after Java 5 released • Plugin system for Annotation Processors
  2. public class MyActivity extends Activity { @InjectView(R.id.title) TextView title; @InjectView(R.id.text)

    TextView text; @InjectView(R.id.submit) Button submit; @InjectView(R.id.imageView) ImageView image; } Butter Knife http://jakewharton.github.io/butterknife/
  3. public class MyActivity extends Activity { @Icicle String username; @Icicle

    int counter; } Icepick https://github.com/frankiesardo/icepick
  4. public class ItemFragment extends Fragment { @Arg long itemId; @Arg

    int categoryId; } FragmentArgs https://github.com/sockeqwe/fragmentargs
  5. public class AnimalsAdapter extends SupportAnnotatedAdapter implements AnimalsAdapterBinder { @ViewType(layout =

    R.layout.list_dog) public final int dog = 0; @ViewType(layout = R.layout.list_cat) public final int cat = 1; AnnotatedAdapter
  6. public class AnimalsAdapter extends SupportAnnotatedAdapter implements AnimalsAdapterBinder { @Override public

    void bindViewHolder(DogViewHolder vh, int position) { Dog dog = (Dog) animals.get(position); vh.name.setText(dog.getName()); } AnnotatedAdapter
  7. public class AnimalsAdapter extends SupportAnnotatedAdapter implements AnimalsAdapterBinder { @Override public

    void bindViewHolder(CatViewHolder vh, int position) { Cat cat = (Cat) animals.get(position); vh.image.setImageBitmap(cat.getImage()); } } AnnotatedAdapter https://github.com/sockeqwe/AnnotatedAdapter
  8. Performance • Compile time code generation • Full JVM •

    “Native” java code (as hand-written) • No reflections!
  9. Dagger 2 • Google Scale • Replacing Dagger 1 with

    Dagger 2 • Saving 13 % CPU by just moving DI https://www.youtube.com/watch?v=oK_XtfXPkqw (36:00)
  10. Limitation • Can generate only new files • Can’t manipulate

    already existing files ◦ Bytecode manipulation (ASM, Afterburn) ◦ AST manipulation (Project Lombok)
  11. public interface Meal { public float getPrice(); } public class

    MargheritaPizza implements Meal { @Override public float getPrice() { return 6f; } }
  12. public class CalzonePizza implements Meal { @Override public float getPrice()

    { return 8.5f; } } public class Tiramisu implements Meal { @Override public float getPrice() { return 4.5f; } }
  13. public class PizzaStore { private MealFactory factory = new MealFactory();

    public static void main(String[] args) throws IOException { PizzaStore pizzaStore = new PizzaStore(); Meal meal = pizzaStore.order( readConsole() ); System.out.println("Bill: $" + meal.getPrice()); } public Meal order(String mealName) { return factory.create(mealName); } }
  14. public class MealFactory { public Meal create(String id) { if

    ("Margherita".equals(id)) return new MargheritaPizza(); if ("Calzone".equals(id)) return new CalzonePizza(); if ("Tiramisu".equals(id)) return new Tiramisu(); throw new IllegalArgumentException("Unknown meal”); } }
  15. @Factory( id = "Margherita", type = Meal.class ) public class

    MargheritaPizza implements Meal { @Override public float getPrice() { return 6f; } }
  16. @Factory( id = "Calzone", type = Meal.class ) public class

    CalzonePizza implements Meal { @Override public float getPrice() { return 8.5f; } }
  17. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor
  18. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor 3. Same types are grouped to one factory
  19. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor 3. Same types are grouped to one factory 4. id is String and must be unique in factory group
  20. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor 3. Same types are grouped to one factory 4. id is String and must be unique in factory group 5. @Factory classes must inherit from specified type
  21. @Factory( id = "Margherita", type = Meal.class ) public class

    MargheritaPizza implements Meal { @Override public float getPrice() { return 6f; } }
  22. @Factory( id = "Margherita", type = Meal.class ) public class

    MargheritaPizza implements Meal { @Override public float getPrice() { return 6f; } }
  23. public class FactoryProcessor extends AbstractProcessor { private Types typeUtils; private

    Elements elementUtils; private Filer filer; private Messager messager; private Map<String, FactoryGroupedClasses> factoryClasses; @Override public synchronized void init(ProcessingEnvironment processingEnv) { typeUtils = processingEnv.getTypeUtils(); elementUtils = processingEnv.getElementUtils(); filer = processingEnv.getFiler(); messager = processingEnv.getMessager(); }
  24. public class FactoryProcessor extends AbstractProcessor { @Override public Set<String> getSupportedAnnotationTypes()

    { Set<String> annotations = new LinkedHashSet<String>(); annotations.add(Factory.class.getCanonicalName()); return annotations; }
  25. public class FactoryProcessor extends AbstractProcessor { @Override public boolean process(Set<?

    extends TypeElement> annotations, RoundEnvironment rv) { for (Element annotatedElement : rv.getElementsAnnotatedWith(Factory.class)) { } } }
  26. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } }
  27. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement
  28. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement VariableElement
  29. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement VariableElement VariableElement
  30. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement VariableElement VariableElement ExecuteableElement
  31. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement VariableElement VariableElement ExecuteableElement ExecuteableElement
  32. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement VariableElement VariableElement ExecuteableElement ExecuteableElement TypeElement
  33. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement VariableElement VariableElement ExecuteableElement ExecuteableElement TypeElement
  34. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeElement VariableElement VariableElement ExecuteableElement ExecuteableElement TypeElement
  35. @Example public class Foo { private int a; private Other

    other; public Foo () { ... } public void setA ( int newA ) { ... } } TypeMirror
  36. public class FactoryProcessor extends AbstractProcessor { @Override public synchronized void

    init(ProcessingEnvironment processingEnv); @Override public Set<String> getSupportedAnnotationTypes(); @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment rv); }
  37. FactoryProcessor.jar - com - example - FactoryProcessor.class - META-INF -

    services - javax.annotation.processing.Processor
  38. @AutoService(Processor.class) public class FactoryProcessor extends AbstractProcessor { @Override public synchronized

    void init(ProcessingEnvironment processingEnv); @Override public Set<String> getSupportedAnnotationTypes(); @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment rv); }
  39. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor 3. Same types are grouped to one factory 4. id is String and must be unique in factory group 5. @Factory classes must inherit from specified type
  40. public boolean process(Set<> annotations, RoundEnvironment rv) { for (Element annotatedElement

    : rv.getElementsAnnotatedWith(Factory.class)) { if (! (annotatedElement instanceof TypeElement) ){ ... } } }
  41. public boolean process(Set<> annotations, RoundEnvironment rv) { for (Element annotatedElement

    : rv.getElementsAnnotatedWith(Factory.class)) { if (! (annotatedElement instanceof TypeElement) ){ ... } } }
  42. public boolean process(Set<> annotations, RoundEnvironment rv) { for (Element annotatedElement

    : rv.getElementsAnnotatedWith(Factory.class)) { if (annotatedElement.getKind() != ElementKind.CLASS) { messager.printMessage( Diagnostic.Kind.ERROR, "Only classes can be annotated with @Factory", annotatedElement ); } } }
  43. public boolean process(Set<> annotations, RoundEnvironment rv) { for (Element annotatedElement

    : rv.getElementsAnnotatedWith(Factory.class)) { if (annotatedElement.getKind() != ElementKind.CLASS) { error( annotatedElement, "Only classes can be annotated with @Factory"); } } } public void error(Element e, String msg) { messager.printMessage(Diagnostic.Kind.ERROR, "Error", e); }
  44. public boolean process(Set<> annotations, RoundEnvironment rv) { for (Element annotatedElement

    : rv.getElementsAnnotatedWith(Factory.class)) { if (annotatedElement.getKind() != ElementKind.CLASS) { error( annotatedElement, "Only classes can be annotated with @Factory"); annotatedElement = null; } annotatedElement.getSimpleName(); } }
  45. public boolean process(Set<> annotations, RoundEnvironment rv) { for (Element annotatedElement

    : rv.getElementsAnnotatedWith(Factory.class)) { if (annotatedElement.getKind() != ElementKind.CLASS) { error( annotatedElement, "Only classes can be annotated with @Factory"); annotatedElement = null; } annotatedElement.getSimpleName(); } }
  46. public boolean process(Set<> annotations, RoundEnvironment rv) { for (Element annotatedElement

    : rv.getElementsAnnotatedWith(Factory.class)) { if (annotatedElement.getKind() != ElementKind.CLASS) { error( annotatedElement, "Only classes can be annotated with @Factory"); } checkOtherThings( annotatedElement ); } } public void checkOtherThings(Element element) { ... }
  47. public boolean process(Set<> annotations, RoundEnvironment rv) { try { for

    (Element annotatedElement : rv.getElementsAnnotatedWith(Factory.class)) { if (annotatedElement.getKind() != ElementKind.CLASS) { error( annotatedElement, "Only classes can be annotated with @Factory"); } checkOtherThings( annotatedElement ); } } catch (ProcessingException e) { error(e.getElement(), e.getMessage()); } }
  48. public void checkOtherThings(Element element) throws ProcessingException public class ProcessingException extends

    Exception { Element element; public ProcessingException(Element element, String msg, Object... args) { super(String.format(msg, args)); this.element = element; } public Element getElement() { return element; } }
  49. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor 3. Same types are grouped to one factory 4. id is String and must be unique in factory group 5. @Factory classes must inherit from specified type
  50. public void checkConstructor(TypeElement e) throws ProcessingException { for (Element enclosed

    : e.getEnclosedElements()) { if (enclosed.getKind() == ElementKind.CONSTRUCTOR) { ExecutableElement constructorElement = (ExecutableElement) enclosed; if (constructorElement.getParameters().size() == 0 && constructorElement.getModifiers().contains(Modifier.PUBLIC)) { return; } } throw new ProcessingException(e, "No public empty constructor"); }
  51. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor 3. Same types are grouped to one factory 4. id is String and must be unique in factory group 5. @Factory classes must inherit from specified type
  52. public class FactoryAnnotatedClass { TypeElement annotatedClassElement; String id; String qualifiedGroupClassName;

    public FactoryAnnotatedClass(TypeElement element) throws ProcessingException { this.annotatedClassElement = classElement; } }
  53. public FactoryAnnotatedClass(TypeElement element) throws ProcessingException { Factory annotation = classElement.getAnnotation(Factory.class);

    this.id = annotation.id(); if (StringUtils.isEmpty(id)) { throw new ProcessingException(classElement, "id() is null or empty!); } }
  54. public FactoryAnnotatedClass(TypeElement element) throws ProcessingException { Factory annotation = classElement.getAnnotation(Factory.class);

    try { Class<?> clazz = annotation.type(); qualifiedGroupClassName = clazz.getCanonicalName(); } catch (MirroredTypeException mte) { } }
  55. public FactoryAnnotatedClass(TypeElement element) throws ProcessingException { Factory annotation = classElement.getAnnotation(Factory.class);

    try { Class<?> clazz = annotation.type(); qualifiedGroupClassName = clazz.getCanonicalName(); } catch (MirroredTypeException mte) { DeclaredType classTypeMirror = (DeclaredType) mte.getTypeMirror(); TypeElement classTypeElement = (TypeElement) classTypeMirror.asElement(); qualifiedGroupClassName = classTypeElement.getQualifiedName().toString(); } }
  56. public class FactoryGroupedClasses { String qualifiedClassName; Map<String, FactoryAnnotatedClass> itemsMap =

    new LinkedHashMap<String, FactoryAnnotatedClass>(); public FactoryGroupedClasses(String qualifiedClassName) { this.qualifiedClassName = qualifiedClassName; } }
  57. public class FactoryGroupedClasses { public void add(FactoryAnnotatedClass toInsert) throws ProcessingException

    { FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId()); if (existing != null) { throw new ProcessingException(toInsert.getTypeElement(), "Conflict: Class with same id found“ ); } } }
  58. public class FactoryGroupedClasses { public void add(FactoryAnnotatedClass toInsert) throws ProcessingException

    { FactoryAnnotatedClass existing = itemsMap.get(toInsert.getId()); if (existing != null) { throw new ProcessingException(toInsert.getTypeElement(), "Conflict: Class with same id found“ ); } itemsMap.put(toInsert.getId(), toInsert); } }
  59. public boolean process(Set<> annotations, RoundEnvironment rv) { try { for

    (Element annotatedElement : rv.getElementsAnnotatedWith(Factory.class)) { } } catch (ProcessingException e) { ... } }
  60. public boolean process(Set<> annotations, RoundEnvironment rv) { try { for

    (Element annotatedElement : rv.getElementsAnnotatedWith(Factory.class)) { FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass( annotatedElement ); } } catch (ProcessingException e) { ... } }
  61. public boolean process(Set<> annotations, RoundEnvironment rv) { try { for

    (Element annotatedElement : rv.getElementsAnnotatedWith(Factory.class)) { FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass( annotatedElement ); FactoryGroupedClasses factoryClass = getOrCreateFactoryClass( annotatedClass.getQualifiedGroupName() ); } } catch (ProcessingException e) { ... } }
  62. public boolean process(Set<> annotations, RoundEnvironment rv) { try { for

    (Element annotatedElement : rv.getElementsAnnotatedWith(Factory.class)) { FactoryAnnotatedClass annotatedClass = new FactoryAnnotatedClass( annotatedElement ); FactoryGroupedClasses factoryClass = getOrCreateFactoryClass( annotatedClass.getQualifiedGroupName()); factoryClass.add(annotatedClass); } } catch (ProcessingException e) { ... } }
  63. Pizza Store Factory 1. Only classes can be @Factory 2.

    @Factory classes must have empty constructor 3. Same types are grouped to one factory 4. id is String and must be unique in factory group 5. @Factory classes must inherit from specified type
  64. public void checkInheritance(FactoryAnnotatedClass ac) throws ProcessingException { TypeElement currentClass =

    ac.getTypeElement(); while (true) { TypeMirror superClassType = currentClass.getSuperclass(); currentClass = (TypeElement) typeUtils.asElement(superClassType); } }
  65. public void checkInheritance(FactoryAnnotatedClass ac) throws ProcessingException { TypeElement currentClass =

    ac.getTypeElement(); while (true) { TypeMirror superClassType = currentClass.getSuperclass(); if (superClassType.getKind() == TypeKind.NONE) throw new ProcessingException( classElement, "Class doesn’t inherit Meal” ); currentClass = (TypeElement) typeUtils.asElement(superClassType); } }
  66. public void checkInheritance(FactoryAnnotatedClass ac) throws ProcessingException { TypeElement currentClass =

    ac.getTypeElement(); while (true) { TypeMirror superClassType = currentClass.getSuperclass(); if (superClassType.getKind() == TypeKind.NONE) throw new ProcessingException( classElement, "Class doesn’t inherit Meal” ); if (superClassType.toString().equals( ac.getQualifiedFactoryGroupName() ) ) return; currentClass = (TypeElement) typeUtils.asElement(superClassType); } }
  67. private Map<String, FactoryGroupedClasses> factoryClasses; public boolean process(Set<> annotations, RoundEnvironment rv)

    { try { ... } catch (ProcessingException e) { ... } for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { factoryClass.generateCode(elementUtils, filer); } }
  68. public void generateCode(Elements elementUtils, Filer filer) throws IOException { MethodSpec.Builder

    method = MethodSpec.methodBuilder("create") .addModifiers(Modifier.PUBLIC) .addParameter(String.class, "id") .returns(TypeName.get(superClassName.asType())); }
  69. public void generateCode(Elements elementUtils, Filer filer) throws IOException { MethodSpec.Builder

    method = MethodSpec.methodBuilder("create") ... for (FactoryAnnotatedClass item : itemsMap.values()) { method .beginControlFlow("if ($S.equals(id))", item.getId()) .addStatement("return new $L()", item.getTypeElement().getQualifiedName().toString()) .endControlFlow(); } }
  70. public Meal create(String id) { if ("Margherita".equals(id)) return new MargheritaPizza();

    if ("Calzone".equals(id)) return new CalzonePizza(); if ("Tiramisu".equals(id)) return new Tiramisu(); }
  71. public void generateCode(Elements elementUtils, Filer filer) throws IOException { MethodSpec.Builder

    method = MethodSpec.methodBuilder("create") ... method.addStatement( "throw new IllegalArgumentException($S + id)", "Unknown id = " ); }
  72. public Meal create(String id) { if ("Margherita".equals(id)) return new MargheritaPizza();

    if ("Calzone".equals(id)) return new CalzonePizza(); if ("Tiramisu".equals(id)) return new Tiramisu(); throw new IllegalArgumentException( "Unknown id = ” +id ); }
  73. public void generateCode(Elements elementUtils, Filer filer) throws IOException { MethodSpec.Builder

    method = MethodSpec.methodBuilder("create") ... TypeSpec typeSpec = TypeSpec.classBuilder( factoryClassName ) .addMethod( method.build() ).build(); // Write MealFactory.java JavaFile.builder(packageName, typeSpec).build().writeTo(filer); }
  74. public class MealFactory { public Meal create(String id) { if

    ("Margherita".equals(id)) return new MargheritaPizza(); if ("Calzone".equals(id)) return new CalzonePizza(); if ("Tiramisu".equals(id)) return new Tiramisu(); throw new IllegalArgumentException( "Unknown id = ” +id ); } }
  75. Processing Rounds Round Input Output 1. PizzaStore.java Meal.java CalzonePizza.java Tiramisu.java

    Magerita.java MealFactory.java 2. MealFactory.java -- none -- 3. -- none -- -- none --
  76. public class FactoryProcessor extends AbstractProcessor { private Map<String, FactoryGroupedClasses> factoryClasses;

    public boolean process(Set<> annotations, RoundEnvironment rv) { try { // scan annotations } catch (ProcessingException e) { ... } for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { factoryClass.generateCode(elementUtils, filer); } } }
  77. public class FactoryProcessor extends AbstractProcessor { private Map<String, FactoryGroupedClasses> factoryClasses;

    public boolean process(Set<> annotations, RoundEnvironment rv) { try { // scan annotations } catch (ProcessingException e) { ... } for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { factoryClass.generateCode(elementUtils, filer); } } }
  78. public class FactoryProcessor extends AbstractProcessor { private Map<String, FactoryGroupedClasses> factoryClasses;

    public boolean process(Set<> annotations, RoundEnvironment rv) { try { // scan annotations } catch (ProcessingException e) { ... } for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { factoryClass.generateCode(elementUtils, filer); } } }
  79. public class FactoryProcessor extends AbstractProcessor { private Map<String, FactoryGroupedClasses> factoryClasses;

    public boolean process(Set<> annotations, RoundEnvironment rv) { try { // scan annotations } catch (ProcessingException e) { ... } for (FactoryGroupedClasses factoryClass : factoryClasses.values()) { factoryClass.generateCode(elementUtils, filer); } factoryClasses.clear(); } }
  80. public class PizzaStore { private MealFactory factory = new MealFactory();

    public static void main(String[] args) throws IOException { PizzaStore pizzaStore = new PizzaStore(); Meal meal = pizzaStore.order( readConsole() ); System.out.println("Bill: $" + meal.getPrice()); } public Meal order(String mealName) { return factory.create(mealName); } }