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

@Eliminate("Boilerplate")

Ryan Harter
September 29, 2016

 @Eliminate("Boilerplate")

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

Ryan Harter

September 29, 2016
Tweet

More Decks by Ryan Harter

Other Decks in Technology

Transcript

  1. 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; } }
  2. 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; }\
  3. 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; }\ }\
  4. 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(); }\ }\\
  5. Annotation Processor public class FooProcessor extends AbstractProcessor { … @Override

    public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); }\ }\\
  6. 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()); } }\\
  7. Annotation Processor public class FooProcessor extends AbstractProcessor { … @Override

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

    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { ... } }
  9. 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
  10. 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
  11. JavaPoet • Uses Fluent API with builders • Based on

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

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

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

    username; … // methods public UserBuilder username(String username) { this.username = username; return this; } … }\
  15. private String username; JavaPoet Modifiers Type Name FieldSpec username =

    FieldSpec .builder(String.class, "username", Modifier.PRIVATE) .build(); private String username;
  16. JavaPoet public final class UserBuilder {\ // fields private String

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

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

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

    UserBuilder username(String username) {\ this.username = username; return this; }\
  20. 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; }\
  21. 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; }\
  22. 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();
  23. 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();
  24. 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();
  25. 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();
  26. // 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();
  27. JavaPoet public final class UserBuilder {\ // fields private String

    username; … // methods public UserBuilder username(String username) {\ this.username = username; return this; }\ … }\
  28. JavaPoet Modifiers Name Content TypeSpec builder = TypeSpec.classBuilder(builderType) .addModifiers(PUBLIC, FINAL)

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

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

    .addField(username) .addMethod(usernameSetter) .build(); public final class UserBuilder {\ }\
  31. @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
  32. @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
  33. // 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
  34. // 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
  35. // 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
  36. // 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
  37. // 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
  38. // 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
  39. // 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
  40. // 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
  41. // 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
  42. // 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
  43. // 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
  44. // 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
  45. // 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
  46. // 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
  47. // 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
  48. // 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
  49. // 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
  50. Annotation Processor // create the builder TypeSpec builder = TypeSpec.classBuilder(builderType)

    .addModifiers(PUBLIC, FINAL) .addFields(fields) .addMethods(setters) .addMethod(buildMethod) .build();
  51. // 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
  52. @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
  53. Using our Annotation public class User {/ String username;/ String

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

    String firstName;/ String lastName;/ int age;/ }/
  55. 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();
  56. 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