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

@Eliminate("Boilerplate")

E86c7de302b490b7f2a67e54960510d0?s=47 Ryan Harter
September 29, 2016

 @Eliminate("Boilerplate")

DroidconNYC talk about using Annotation Processors to eliminate boilerplate in Java.

E86c7de302b490b7f2a67e54960510d0?s=128

Ryan Harter

September 29, 2016
Tweet

Transcript

  1. @Eliminate(“Boilerplate”) Ryan Harter @rharter

  2. JAVA HAS BOILERPLATE

  3. JAVA IS BOILERPLATE

  4. public class User { String username; String firstname; String lastname;

    int age; public String getUsername() { return username; } public String getFirstname() { return firstname; } public String getLastname() { return lastname; } public int getAge() { return age; } }
  5. public final class UserBuilder { private String username; private String

    firstName; private String lastName; private int age; public UserBuilder() { }\ public UserBuilder username(String username) { this.username = username; return this; }\ public UserBuilder firstName(String firstName) { this.firstName = firstName; return this; }\
  6. return this; }\ public UserBuilder lastName(String lastName) { this.lastName =

    lastName; return this; }\ public UserBuilder age(int age) { this.age = age; return this; }\ public User build() { User user = new User(); user.username = this.username; user.firstName = this.firstName; user.lastName = this.lastName; user.age = this.age; return user; }\ }\
  7. Annotation Processing + Code Generation

  8. Annotation Processing

  9. Annotation Processing • Part of javac • Read @Annotated source

    • Generate .java source files
  10. APT (.java) Package (.apk) Dex (.dex) Package (.jar) Compile (.class)

    Write (.java) Annotation Processing
  11. APT (.java) Package (.apk) Dex (.dex) Package (.jar) Compile (.class)

    Write (.java) Annotation Processing
  12. APT (.java) Package (.apk) Dex (.dex) Package (.jar) Compile (.class)

    Write (.java) Annotation Processing
  13. APT (.java) Package (.apk) Dex (.dex) Package (.jar) Compile (.class)

    Write (.java) Annotation Processing
  14. APT (.java) Package (.apk) Dex (.dex) Package (.jar) Compile (.class)

    Write (.java) Annotation Processing
  15. APT (.java) Package (.apk) Dex (.dex) Package (.jar) Compile (.class)

    Write (.java) Annotation Processing
  16. Benefits • Write your code generator once • Trust the

    generated code
  17. Components • Annotations • Processor • APT (Annotation Processing Tool)

    • Annotated Source
  18. Annotations @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Builder { }

  19. Annotations @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Builder { }

  20. Annotations @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Builder { }

  21. Annotations @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Builder { }

  22. Annotations @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Builder { }

  23. Annotations @Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface Builder { } @Builder public

    class Builder
  24. Annotation Processor public class FooProcessor extends AbstractProcessor { }\\

  25. Annotation Processor public class FooProcessor extends AbstractProcessor { public FooProcessor()

    { super(); }\ }\\
  26. Annotation Processor public class FooProcessor extends AbstractProcessor { private Messager

    messager; private Filer filer; … @Override public synchronized void init( ProcessingEnvironment processingEnv) { super.init(processingEnv); this.messager = processingEnv.getMessager(); this.filer = processingEnv.getFiler(); }\ }\\
  27. Annotation Processor public class FooProcessor extends AbstractProcessor { … @Override

    public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }\ }\\
  28. Annotation Processor public class FooProcessor extends AbstractProcessor { … @Override

    public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } @Override public Set<String> getSupportedAnnotationTypes() { return ImmutableSet.of(Builder.class.getCanonicalName()); } }\\
  29. Annotation Processor public class FooProcessor extends AbstractProcessor { … @Override

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { ... } }\\
  30. Annotation Processor public class FooProcessor extends AbstractProcessor { … @Override

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { ... } }
  31. Processing Rounds Java Sources APT Completed

  32. Processing Rounds Java Sources APT Completed Round 1

  33. Processing Rounds Java Sources APT Completed Round 1

  34. Processing Rounds Java Sources APT Completed Round 1

  35. Processing Rounds Java Sources APT Completed Round 2

  36. Processing Rounds Java Sources APT Completed Complete

  37. Annotation Processor • ServiceLoader Discovery File com.ryanharter.example.annotations.BuilderProcessor META-INF/services/javax.annotation.processing.Processor

  38. Add to APT Classpath dependencies { compile project(':annotation') apt project(':processor')

    } app/build.gradle https://github.com/tbroyer/gradle-apt-plugin https://bitbucket.org/hvisser/android-apt
  39. Code Generation

  40. Code Generation • Pair well with Annotation Processors • Used

    to generate new Java source files • JavaPoet represents Java source as model https://github.com/square/javapoet
  41. JavaPoet • Uses Fluent API with builders • Based on

    Specs • TypeSpec • MethodSpec • ParameterSpec • FieldSpec https://github.com/square/javapoet
  42. JavaPoet public final class UserBuilder { // fields private String

    username; … // methods public UserBuilder username(String username) { this.username = username; return this; } … }\
  43. JavaPoet public final class UserBuilder { // fields private String

    username; … // methods public UserBuilder username(String username) { this.username = username; return this; } … }\
  44. JavaPoet public final class UserBuilder String builderName = String.format("%sBuilder", type.getSimpleName());

    ClassName builderType = ClassName.get(packageName, builderName);
  45. JavaPoet public final class UserBuilder { // fields private String

    username; … // methods public UserBuilder username(String username) { this.username = username; return this; } … }\
  46. private String username; JavaPoet

  47. JavaPoet Modifiers private String username;

  48. JavaPoet Modifiers Type private String username;

  49. JavaPoet Modifiers Type private String username; Name

  50. JavaPoet FieldSpec username = FieldSpec .builder(String.class, "username", Modifier.PRIVATE) .build(); private

    String username; Modifiers Type private String username; Name
  51. private String username; JavaPoet Modifiers Type Name FieldSpec username =

    FieldSpec .builder(String.class, "username", Modifier.PRIVATE) .build(); private String username;
  52. JavaPoet FieldSpec username = FieldSpec .builder(String.class, "username", Modifier.PRIVATE) .build(); private

    String username;
  53. JavaPoet public final class UserBuilder {\ // fields private String

    username; … // methods public UserBuilder username(String username) {\ this.username = username; return this; }\ … }\
  54. JavaPoet // methods public UserBuilder username(String username) {\ this.username =

    username; return this; }\
  55. JavaPoet Modifiers // methods public UserBuilder username(String username) {\ this.username

    = username; return this; }\
  56. JavaPoet Modifiers Return Type // methods public UserBuilder username(String username)

    {\ this.username = username; return this; }\
  57. JavaPoet Modifiers Return Type Name // methods public UserBuilder username(String

    username) {\ this.username = username; return this; }\
  58. JavaPoet Modifiers Return Type Name Parameters // methods public UserBuilder

    username(String username) {\ this.username = username; return this; }\
  59. JavaPoet Modifiers Return Type Name Parameters Statements // methods public

    UserBuilder username(String username) {\ this.username = username; return this; }\
  60. JavaPoet MethodSpec usernameSetter = MethodSpec.methodBuilder("username") .addModifiers(PUBLIC) .returns(builderType) .addParameter(String.class, "username") .addStatement("this.$N

    = username", username) .addStatement("return this") .build(); Modifiers Return Type Name Parameters Statements // methods public UserBuilder username(String username) {\ this.username = username; return this; }\
  61. JavaPoet Modifiers Return Type Name Parameters Statements MethodSpec usernameSetter =

    MethodSpec.methodBuilder("username") .addModifiers(PUBLIC) .returns(builderType) .addParameter(String.class, "username") .addStatement("this.$N = username", username) .addStatement("return this") .build(); // methods public UserBuilder username(String username) {\ this.username = username; return this; }\
  62. JavaPoet Modifiers Return Type Name Parameters Statements // methods public

    UserBuilder username(String username) {\ this.username = username; return this; }\ MethodSpec usernameSetter = MethodSpec.methodBuilder("username") .addModifiers(PUBLIC) .returns(builderType) .addParameter(String.class, "username") .addStatement("this.$N = username", username) .addStatement("return this") .build();
  63. JavaPoet Modifiers Return Type Name Parameters Statements // methods public

    UserBuilder username(String username) {\ this.username = username; return this; }\ MethodSpec usernameSetter = MethodSpec.methodBuilder("username") .addModifiers(PUBLIC) .returns(builderType) .addParameter(String.class, "username") .addStatement("this.$N = username", username) .addStatement("return this") .build();
  64. JavaPoet Modifiers Return Type Name Parameters Statements // methods public

    UserBuilder username(String username) {\ this.username = username; return this; }\ MethodSpec usernameSetter = MethodSpec.methodBuilder("username") .addModifiers(PUBLIC) .returns(builderType) .addParameter(String.class, "username") .addStatement("this.$N = username", username) .addStatement("return this") .build();
  65. JavaPoet Modifiers Return Type Name Parameters Statements // methods public

    UserBuilder username(String username) {\ this.username = username; return this; }\ MethodSpec usernameSetter = MethodSpec.methodBuilder("username") .addModifiers(PUBLIC) .returns(builderType) .addParameter(String.class, "username") .addStatement("this.$N = username", username) .addStatement("return this") .build();
  66. // methods public UserBuilder username(String username) {\ this.username = username;

    return this; }\ JavaPoet MethodSpec usernameSetter = MethodSpec.methodBuilder("username") .addModifiers(PUBLIC) .returns(builderType) .addParameter(String.class, "username") .addStatement("this.$N = username", username) .addStatement("return this") .build();
  67. JavaPoet public final class UserBuilder {\ // fields private String

    username; … // methods public UserBuilder username(String username) {\ this.username = username; return this; }\ … }\
  68. JavaPoet public final class UserBuilder {\ }\

  69. JavaPoet Modifiers public final class UserBuilder {\ }\

  70. JavaPoet Modifiers Name public final class UserBuilder {\ }\

  71. JavaPoet Modifiers Name Content public final class UserBuilder {\ }\

  72. JavaPoet TypeSpec builder = TypeSpec.classBuilder(builderType) .addModifiers(PUBLIC, FINAL) .addField(username) .addMethod(usernameSetter) .build();

    Modifiers Name Content public final class UserBuilder {\ }\
  73. JavaPoet Modifiers Name Content TypeSpec builder = TypeSpec.classBuilder(builderType) .addModifiers(PUBLIC, FINAL)

    .addField(username) .addMethod(usernameSetter) .build(); public final class UserBuilder {\ }\
  74. JavaPoet Modifiers Name Content TypeSpec builder = TypeSpec.classBuilder(builderType) .addModifiers(PUBLIC, FINAL)

    .addField(username) .addMethod(usernameSetter) .build(); public final class UserBuilder {\ }\
  75. JavaPoet Modifiers Name Content TypeSpec builder = TypeSpec.classBuilder(builderType) .addModifiers(PUBLIC, FINAL)

    .addField(username) .addMethod(usernameSetter) .build(); public final class UserBuilder {\ }\
  76. JavaPoet TypeSpec builder = TypeSpec.classBuilder(builderType) .addModifiers(PUBLIC, FINAL) .addField(username) .addMethod(usernameSetter) .build();

    public final class UserBuilder {\ }\
  77. Annotation Processor @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment

    roundEnv) { }
  78. @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

    for (Element el : roundEnv.getElementsAnnotatedWith(Builder.class)) { } } @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (Element el : roundEnv.getElementsAnnotatedWith(Builder.class)) { } } Annotation Processor
  79. @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

    for (Element el : roundEnv.getElementsAnnotatedWith(Builder.class)) { // get element metadata // create private fields and public setters // create the build method // create the builder type // write the java source file } } Annotation Processor
  80. // get element metadata String packageName = getPackageName(type); String targetName

    = lowerCamelCase(type.getSimpleName().toString()); Set<VariableElement> vars = getNonPrivateVariables(type); String builderName = String.format("%sBuilder", type.getSimpleName()); ClassName builderType = ClassName.get(packageName, builderName); Annotation Processor
  81. // get element metadata String packageName = getPackageName(type); String targetName

    = lowerCamelCase(type.getSimpleName().toString()); Set<VariableElement> vars = getNonPrivateVariables(type); String builderName = String.format("%sBuilder", type.getSimpleName()); ClassName builderType = ClassName.get(packageName, builderName); Annotation Processor
  82. // get element metadata String packageName = getPackageName(type); String targetName

    = lowerCamelCase(type.getSimpleName().toString()); Set<VariableElement> vars = getNonPrivateVariables(type); String builderName = String.format("%sBuilder", type.getSimpleName()); ClassName builderType = ClassName.get(packageName, builderName); Annotation Processor
  83. // get element metadata String packageName = getPackageName(type); String targetName

    = lowerCamelCase(type.getSimpleName().toString()); Set<VariableElement> vars = getNonPrivateVariables(type); String builderName = String.format("%sBuilder", type.getSimpleName()); ClassName builderType = ClassName.get(packageName, builderName); Annotation Processor
  84. // get element metadata String packageName = getPackageName(type); String targetName

    = lowerCamelCase(type.getSimpleName().toString()); Set<VariableElement> vars = getNonPrivateVariables(type); String builderName = String.format("%sBuilder", type.getSimpleName()); ClassName builderType = ClassName.get(packageName, builderName); Annotation Processor
  85. // create private fields and public setters List<FieldSpec> fields =

    new ArrayList<FieldSpec>(vars.size()); List<MethodSpec> setters = new ArrayList<MethodSpec>(vars.size()); for (VariableElement var : vars) { TypeName typeName = TypeName.get(var.asType()); String name = var.getSimpleName().toString(); // create the field fields.add(FieldSpec.builder(typeName, name, PRIVATE).build()); // create the setter setters.add(MethodSpec.methodBuilder(name) .addModifiers(PUBLIC) .returns(builderType) .addParameter(typeName, name) .addStatement("this.$N = $N", name, name) .addStatement("return this") .build()); } Annotation Processor
  86. // create private fields and public setters List<FieldSpec> fields =

    new ArrayList<FieldSpec>(vars.size()); List<MethodSpec> setters = new ArrayList<MethodSpec>(vars.size()); for (VariableElement var : vars) { TypeName typeName = TypeName.get(var.asType()); String name = var.getSimpleName().toString(); // create the field fields.add(FieldSpec.builder(typeName, name, PRIVATE).build()); // create the setter setters.add(MethodSpec.methodBuilder(name) .addModifiers(PUBLIC) .returns(builderType) .addParameter(typeName, name) .addStatement("this.$N = $N", name, name) .addStatement("return this") .build()); } Annotation Processor
  87. // create private fields and public setters List<FieldSpec> fields =

    new ArrayList<FieldSpec>(vars.size()); List<MethodSpec> setters = new ArrayList<MethodSpec>(vars.size()); for (VariableElement var : vars) { TypeName typeName = TypeName.get(var.asType()); String name = var.getSimpleName().toString(); // create the field fields.add(FieldSpec.builder(typeName, name, PRIVATE).build()); // create the setter setters.add(MethodSpec.methodBuilder(name) .addModifiers(PUBLIC) .returns(builderType) .addParameter(typeName, name) .addStatement("this.$N = $N", name, name) .addStatement("return this") .build()); } Annotation Processor
  88. // create private fields and public setters List<FieldSpec> fields =

    new ArrayList<FieldSpec>(vars.size()); List<MethodSpec> setters = new ArrayList<MethodSpec>(vars.size()); for (VariableElement var : vars) { TypeName typeName = TypeName.get(var.asType()); String name = var.getSimpleName().toString(); // create the field fields.add(FieldSpec.builder(typeName, name, PRIVATE).build()); // create the setter setters.add(MethodSpec.methodBuilder(name) .addModifiers(PUBLIC) .returns(builderType) .addParameter(typeName, name) .addStatement("this.$N = $N", name, name) .addStatement("return this") .build()); } Annotation Processor
  89. // create private fields and public setters List<FieldSpec> fields =

    new ArrayList<FieldSpec>(vars.size()); List<MethodSpec> setters = new ArrayList<MethodSpec>(vars.size()); for (VariableElement var : vars) { TypeName typeName = TypeName.get(var.asType()); String name = var.getSimpleName().toString(); // create the field fields.add(FieldSpec.builder(typeName, name, PRIVATE).build()); // create the setter setters.add(MethodSpec.methodBuilder(name) .addModifiers(PUBLIC) .returns(builderType) .addParameter(typeName, name) .addStatement("this.$N = $N", name, name) .addStatement("return this") .build()); } Annotation Processor
  90. // create private fields and public setters List<FieldSpec> fields =

    new ArrayList<FieldSpec>(vars.size()); List<MethodSpec> setters = new ArrayList<MethodSpec>(vars.size()); for (VariableElement var : vars) { TypeName typeName = TypeName.get(var.asType()); String name = var.getSimpleName().toString(); // create the field fields.add(FieldSpec.builder(typeName, name, PRIVATE).build()); // create the setter setters.add(MethodSpec.methodBuilder(name) .addModifiers(PUBLIC) .returns(builderType) .addParameter(typeName, name) .addStatement("this.$N = $N", name, name) .addStatement("return this") .build()); } Annotation Processor
  91. // create the build method TypeName targetType = TypeName.get(type.asType()); MethodSpec.Builder

    buildMethodBuilder = MethodSpec.methodBuilder("build") .addModifiers(PUBLIC) .returns(targetType) .addStatement("$1T $2N = new $1T()", targetType, targetName); for (FieldSpec field : fields) { buildMethodBuilder .addStatement("$1N.$2N = this.$2N", targetName, field); } buildMethodBuilder.addStatement("return $N", targetName); MethodSpec buildMethod = buildMethodBuilder.build(); Annotation Processor
  92. // create the build method TypeName targetType = TypeName.get(type.asType()); MethodSpec.Builder

    buildMethodBuilder = MethodSpec.methodBuilder("build") .addModifiers(PUBLIC) .returns(targetType) .addStatement("$1T $2N = new $1T()", targetType, targetName); for (FieldSpec field : fields) { buildMethodBuilder .addStatement("$1N.$2N = this.$2N", targetName, field); } buildMethodBuilder.addStatement("return $N", targetName); MethodSpec buildMethod = buildMethodBuilder.build(); Annotation Processor
  93. // create the build method TypeName targetType = TypeName.get(type.asType()); MethodSpec.Builder

    buildMethodBuilder = MethodSpec.methodBuilder("build") .addModifiers(PUBLIC) .returns(targetType) .addStatement("$1T $2N = new $1T()", targetType, targetName); for (FieldSpec field : fields) { buildMethodBuilder .addStatement("$1N.$2N = this.$2N", targetName, field); } buildMethodBuilder.addStatement("return $N", targetName); MethodSpec buildMethod = buildMethodBuilder.build(); Annotation Processor
  94. // create the build method TypeName targetType = TypeName.get(type.asType()); MethodSpec.Builder

    buildMethodBuilder = MethodSpec.methodBuilder("build") .addModifiers(PUBLIC) .returns(targetType) .addStatement("$1T $2N = new $1T()", targetType, targetName); for (FieldSpec field : fields) { buildMethodBuilder .addStatement("$1N.$2N = this.$2N", targetName, field); } buildMethodBuilder.addStatement("return $N", targetName); MethodSpec buildMethod = buildMethodBuilder.build(); Annotation Processor
  95. // create the build method TypeName targetType = TypeName.get(type.asType()); MethodSpec.Builder

    buildMethodBuilder = MethodSpec.methodBuilder("build") .addModifiers(PUBLIC) .returns(targetType) .addStatement("$1T $2N = new $1T()", targetType, targetName); for (FieldSpec field : fields) { buildMethodBuilder .addStatement("$1N.$2N = this.$2N", targetName, field); } buildMethodBuilder.addStatement("return $N", targetName); MethodSpec buildMethod = buildMethodBuilder.build(); Annotation Processor
  96. // create the build method TypeName targetType = TypeName.get(type.asType()); MethodSpec.Builder

    buildMethodBuilder = MethodSpec.methodBuilder("build") .addModifiers(PUBLIC) .returns(targetType) .addStatement("$1T $2N = new $1T()", targetType, targetName); for (FieldSpec field : fields) { buildMethodBuilder .addStatement("$1N.$2N = this.$2N", targetName, field); } buildMethodBuilder.addStatement("return $N", targetName); MethodSpec buildMethod = buildMethodBuilder.build(); Annotation Processor
  97. Annotation Processor // create the builder TypeSpec builder = TypeSpec.classBuilder(builderType)

    .addModifiers(PUBLIC, FINAL) .addFields(fields) .addMethods(setters) .addMethod(buildMethod) .build();
  98. // write java source file JavaFile file = JavaFile .builder(builderType.packageName(),

    builder.build()) .build(); try { file.writeTo(filer); } catch (IOException e) { messager.printMessage(Diagnostic.Kind.ERROR, "Failed to write file for element", el); } Annotation Processor
  99. @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

    for (Element el : roundEnv.getElementsAnnotatedWith(Builder.class)) { // get element metadata // create private fields and public setters // create the build method // create the builder type // write the java source file } } Annotation Processor
  100. Using our Annotation public class User {/ String username;/ String

    firstName;/ String lastName;/ int age;/ }/
  101. Using our Annotation @Builder public class User {/ String username;/

    String firstName;/ String lastName;/ int age;/ }/
  102. Using our Annotation @Builder public class User { String username;

    String firstName; String lastName; int age; } User user = new UserBuilder() .username("rharter") .firstName("Ryan") .lastName("Harter") .age(30) .build();
  103. References • Android APT Plugin - https://bitbucket.org/hvisser/android-apt • Gradle APT

    Plugin - https://github.com/tbroyer/gradle-apt-plugin • JavaPoet - https://github.com/square/javapoet
  104. Find Me • @rharter • http://ryanharter.com