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

Кодогенерация от А до Я, Мобиус 2017

Кодогенерация от А до Я, Мобиус 2017

Grigoriy Dzhanelidze

April 22, 2017
Tweet

More Decks by Grigoriy Dzhanelidze

Other Decks in Programming

Transcript

  1. 2 Кодогенерируешь небось? • Google Desugar • Dagger 2 •

    DataBinding • Butterknife • IcePick • Retrolambda • LoganSquare • …
  2. 6 Немного про AspectJ @Override
 protected void onCreate(Bundle savedInstanceState) {

    Debug.startMethodTracing();
 super.onCreate(savedInstanceState);
 /* doing the do */
 Debug.stopMethodTracing(); }
  3. 7 Немного про AspectJ Сross-cutting concerns @Override
 protected void onCreate(Bundle

    savedInstanceState) { Debug.startMethodTracing();
 super.onCreate(savedInstanceState);
 /* doing the do */
 Debug.stopMethodTracing(); }
  4. 8 Немного про AspectJ Сross-cutting concerns @Override
 protected void onCreate(Bundle

    savedInstanceState) { Debug.startMethodTracing();
 super.onCreate(savedInstanceState);
 /* doing the do */
 Debug.stopMethodTracing(); } • Чтобы удалить или поменять нужно целых два телодвижения • А что если мы хотим применить ко всем Activity#onCreate? • А что если мы хотим добавить этот код ко всем функциям во всех классах com.sample.package начинающимся с set...?
  5. 9 Подключаем AspectJ к проекту buildscript {
 repositories {
 mavenCentral()


    maven { url 'https://github.com/Archinamon/GradleAspectJ-Android/raw/master' }
 }
 dependencies {
 classpath 'com.android.tools.build:gradle:2.1.0'
 classpath 'com.archinamon:android-gradle-aspectj:2.0.2'
 }
 } apply plugin: 'com.archinamon.aspectj' https://github.com/Archinamon/GradleAspectJ-Android
  6. 10 Пример аспекта @Aspect
 public class LogAspect {
 @Around("within(mobiusconf.*) &&

    execution(public onCreate(..))")
 public Object around(ProceedingJoinPoint point) throws Throwable {
 Debug.startMethodTracing();
 Object result = point.proceed();
 Debug.stopMethodTracing();
 return result;
 }
 }
  7. 11 Пример аспекта @Aspect
 public class LogAspect {
 @Around("within(mobiusconf.*) &&

    execution(public onCreate(..))")
 public Object around(ProceedingJoinPoint point) throws Throwable {
 Debug.startMethodTracing();
 Object result = point.proceed();
 Debug.stopMethodTracing();
 return result;
 }
 }
  8. 12 Аспекты превращаются в… public class MainActivity extends AppCompatActivity {


    private static final JoinPoint.StaticPart ajc$tjp_0;
 
 private static void ajc$preClinit() {
 Factory localFactory = new Factory("MainActivity.java", MainActivity.class);
 ajc$tjp_0 = localFactory.makeSJP("method-execution", localFactory.makeMethodSig("4", "onCreate", "me.strlght.MainActivity", "android.os.Bundle", "savedInstanceState", "", "void"), 9);
 }
 
 private static final void onCreate_aroundBody0(MainActivity paramMainActivity, Bundle paramBundle, JoinPoint paramJoinPoint) {
 paramMainActivity.onCreate(paramBundle);
 }
 
 private static final Object onCreate_aroundBody1$advice(MainActivity paramMainActivity, Bundle paramBundle, JoinPoint paramJoinPoint, SampleAspect paramSampleAspect, ProceedingJoinPoint paramProceedingJoinPoint) {
 
 try {
 onCreate_aroundBody0(paramMainActivity, paramBundle, paramProceedingJoinPoint);
 return null;
 } catch (Throwable paramMainActivity) {
 throw paramMainActivity;
 } finally {
 Debug.stopMethodTracing();
 }
 }
 
 protected void onCreate(Bundle paramBundle) {
 JoinPoint localJoinPoint = Factory.makeJP(ajc$tjp_0, this, this, paramBundle);
 onCreate_aroundBody1$advice(this, paramBundle, localJoinPoint, SampleAspect.aspectOf(), (ProceedingJoinPoint) localJoinPoint);
 }
 }
  9. 13 Что хорошо? • Можно буквально в пару строк вынести

    сквозной код • Работает с .class-файлами • Из коробки генерирует список классов, которые были затронуты • Рантайм всего ~700 методов
  10. 14 Что хорошо? А что не так? • Удачи с

    тестированием • Можно буквально в пару строк вынести сквозной код • Работает с .class-файлами • Из коробки генерирует список классов, которые были затронуты • Рантайм всего ~700 методов
  11. 15 Что хорошо? А что не так? А кто использует?

    • https://github.com/JakeWharton/hugo • Удачи с тестированием • Можно буквально в пару строк вынести сквозной код • Работает с .class-файлами • Из коробки генерирует список классов, которые были затронуты • Рантайм всего ~700 методов
  12. 17 public class MainActivity extends AppCompatActivity {
 @BindView(R.id.activity_main)
 RelativeLayout rootLayout;


    
 @BindView(R.id.sample_text)
 TextView sampleText;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 Butterknife.bind(this);
 sampleText.setText("Hello butterknife!");
 }
 } Butterknife
  13. 18 Annotation Processor class SampleProcessor implements Processor { public Set<String>

    getSupportedAnnotationTypes() {
 return Collections.singleton("mobiusconf.BindView");
 } public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7;
 } }
  14. 19 Annotation Processor @AutoService(Processor.class)
 @SupportedAnnotationTypes({"mobiusconf.BindView"})
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
 class SampleProcessor extends AbstractProcessor

    class SampleProcessor implements Processor { public Set<String> getSupportedAnnotationTypes() {
 return Collections.singleton("mobiusconf.BindView");
 } public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7;
 } }
  15. 21 @AutoService(Processor.class) public class AwesomeProecessor extends Processor { /* …

    */ } resources/META-INF/services/javax.annotation.processing.Processor: mobiusconf.AwesomeProcessor https://github.com/google/auto/ ServiceLoader
  16. 22 @AutoService(Processor.class) public class AwesomeProecessor extends Processor { /* …

    */ } resources/META-INF/services/javax.annotation.processing.Processor: mobiusconf.AwesomeProcessor ServiceLoader
  17. 23 Annotation Processor @AutoService(Processor.class)
 @SupportedAnnotationTypes({"mobiusconf.BindView"})
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
 class SampleProcessor extends AbstractProcessor

    class SampleProcessor implements Processor { public Set<String> getSupportedAnnotationTypes() {
 return Collections.singleton("mobiusconf.BindView");
 } public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7;
 } }
  18. 24 Annotation Processor @AutoService(Processor.class)
 @SupportedAnnotationTypes({"mobiusconf.BindView"})
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
 class SampleProcessor extends AbstractProcessor

    class SampleProcessor implements Processor { public Set<String> getSupportedAnnotationTypes() {
 return Collections.singleton("mobiusconf.BindView");
 } public SourceVersion getSupportedSourceVersion() { return SourceVersion.RELEASE_7;
 } }
  19. 25 Annotation Processor @AutoService(Processor.class)
 @SupportedAnnotationTypes({"mobiusconf.BindView"})
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
 public class BindViewBasicProcessor extends

    AbstractProcessor {
 @Override
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
 if (annotations.isEmpty()) {
 return false;
 }
 final Map<TypeElement, BindViewBasicVisitor> visitors = new HashMap<>();
 final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
 for (Element element : elements) {
 final TypeElement object = (TypeElement) element.getEnclosingElement();
 BindViewBasicVisitor visitor = visitors.get(object);
 if (visitor == null) {
 visitor = new BindViewBasicVisitor(processingEnv, object);
 visitors.put(object, visitor);
 }
 element.accept(visitor, null);
 }
 for (BindViewBasicVisitor visitor : visitors.values()) {
 visitor.generate();
 }
 return true;
 }
 }
  20. 26 Annotation Processor @AutoService(Processor.class)
 @SupportedAnnotationTypes({"mobiusconf.BindView"})
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
 public class BindViewBasicProcessor extends

    AbstractProcessor {
 @Override
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
 if (annotations.isEmpty()) {
 return false;
 }
 final Map<TypeElement, BindViewBasicVisitor> visitors = new HashMap<>();
 final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
 for (Element element : elements) {
 final TypeElement object = (TypeElement) element.getEnclosingElement();
 BindViewBasicVisitor visitor = visitors.get(object);
 if (visitor == null) {
 visitor = new BindViewBasicVisitor(processingEnv, object);
 visitors.put(object, visitor);
 }
 element.accept(visitor, null);
 }
 for (BindViewBasicVisitor visitor : visitors.values()) {
 visitor.generate();
 }
 return true;
 }
 }
  21. 27 Annotation Processor @AutoService(Processor.class)
 @SupportedAnnotationTypes({"mobiusconf.BindView"})
 @SupportedSourceVersion(SourceVersion.RELEASE_7)
 public class BindViewBasicProcessor extends

    AbstractProcessor {
 @Override
 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
 if (annotations.isEmpty()) {
 return false;
 }
 final Map<TypeElement, BindViewBasicVisitor> visitors = new HashMap<>();
 final Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(BindView.class);
 for (Element element : elements) {
 final TypeElement object = (TypeElement) element.getEnclosingElement();
 BindViewBasicVisitor visitor = visitors.get(object);
 if (visitor == null) {
 visitor = new BindViewBasicVisitor(processingEnv, object);
 visitors.put(object, visitor);
 }
 element.accept(visitor, null);
 }
 for (BindViewBasicVisitor visitor : visitors.values()) {
 visitor.generate();
 }
 return true;
 }
 }
  22. 28 Annotation Processor public class BindViewBasicVisitor extends ElementScanner7<Void, Void> {


    private final CodeBlock.Builder bindViewBlock = CodeBlock.builder();
 private final Messager logger;
 private final Filer filer;
 private final TypeElement originElement;
 
 public BindViewBasicVisitor(ProcessingEnvironment env, TypeElement element) {
 super();
 logger = env.getMessager();
 filer = env.getFiler();
 originElement = element;
 } }
  23. 29 Annotation Processor public class BindViewBasicVisitor extends ElementScanner7<Void, Void> {


    private final CodeBlock.Builder bindViewBlock = CodeBlock.builder();
 private final Messager logger;
 private final Filer filer;
 private final TypeElement originElement;
 
 public BindViewBasicVisitor(ProcessingEnvironment env, TypeElement element) {
 super();
 logger = env.getMessager();
 filer = env.getFiler();
 originElement = element;
 } }
  24. 30 Annotation Processor public class BindViewBasicVisitor extends ElementScanner7<Void, Void> {


    private final CodeBlock.Builder bindViewBlock = CodeBlock.builder();
 private final Messager logger;
 private final Filer filer;
 private final TypeElement originElement;
 
 public BindViewBasicVisitor(ProcessingEnvironment env, TypeElement element) {
 super();
 logger = env.getMessager();
 filer = env.getFiler();
 originElement = element;
 } }
  25. 31 Annotation Processor public class BindViewBasicVisitor extends ElementScanner7<Void, Void> {


    @Override
 public Void visitVariable(VariableElement e, Void aVoid) {
 if (e.getModifiers().contains(Modifier.PROTECTED)
 || e.getModifiers().contains(Modifier.PRIVATE)) {
 logger.printMessage(Diagnostic.Kind.ERROR, "Field " + e.getSimpleName() + "must be package-local or public");
 }
 if (e.getModifiers().contains(Modifier.FINAL)) {
 logger.printMessage(Diagnostic.Kind.ERROR, "Field " + e.getSimpleName() + "must be non-final");
 }
 final BindView bindView = e.getAnnotation(BindView.class);
 bindViewBlock.addStatement("param.$L = ($T) param.findViewById($L)",
 e.getSimpleName(), ClassName.get(e.asType()), bindView.value());
 return super.visitVariable(e, aVoid);
 } }
  26. 32 Annotation Processor public class BindViewBasicVisitor extends ElementScanner7<Void, Void> {


    @Override
 public Void visitVariable(VariableElement e, Void aVoid) {
 if (e.getModifiers().contains(Modifier.PROTECTED)
 || e.getModifiers().contains(Modifier.PRIVATE)) {
 logger.printMessage(Diagnostic.Kind.ERROR, "Field " + e.getSimpleName() + "must be package-local or public");
 }
 if (e.getModifiers().contains(Modifier.FINAL)) {
 logger.printMessage(Diagnostic.Kind.ERROR, "Field " + e.getSimpleName() + "must be non-final");
 }
 final BindView bindView = e.getAnnotation(BindView.class);
 bindViewBlock.addStatement("param.$L = ($T) param.findViewById($L)",
 e.getSimpleName(), ClassName.get(e.asType()), bindView.value());
 return super.visitVariable(e, aVoid);
 } }
  27. 33 Annotation Processor public void generate() {
 String className =

    originElement.getSimpleName() + "_ViewBinding";
 TypeSpec typeSpec = TypeSpec.classBuilder(className)
 .addModifiers(Modifier.FINAL)
 .addMethod(MethodSpec.methodBuilder("bind")
 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
 .addParameter(TypeName.get(originElement.asType()), "param")
 .addCode(bindViewBlock.build())
 .build())
 .build();
 JavaFile javaFile = JavaFile.builder(originElement.getEnclosingElement().toString(), typeSpec)
 .addFileComment("Generated, do not modify")
 .build();
 try {
 javaFile.writeTo(filer);
 } catch (IOException exception) {
 logger.printMessage(Diagnostic.Kind.ERROR, "Can't write file: " + exception.getMessage());
 }
 }
  28. 34 Annotation Processor // Generated, do not modify
 package me.strlght.basic_processor_sample;


    
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 final class MainActivity_ViewBinding {
 public static void bind(MainActivity param) {
 param.rootLayout = (RelativeLayout) param.findViewById(2131492944);
 param.sampleText = (TextView) param.findViewById(2131492945);
 }
 }
  29. 35 Что хорошо? • Работоспособность зависит от компилятора и/или языка

    • Легко тестировать • Сгенерированный код можно безболезненно дебажить • Никаких ограничений на генерацию нового кода по аннотациям
  30. 36 Что хорошо? • Работоспособность зависит от компилятора и/или языка

    • Легко тестировать • Сгенерированный код можно безболезненно дебажить • Никаких ограничений на генерацию нового кода по аннотациям А кто использует? • Dagger 2 • Butterknife • IcePick • …
  31. 37 Тестируем: Compile Testing @Test public void testProcessor() {
 JavaFileObject

    source = JavaFileObjects .forSourceString("test.Source", "/* source */");
 
 JavaFileObject expected = JavaFileObjects .forSourceString("test.Source_ViewBinding", "/* source */");
 
 assertAbout(javaSource())
 .that(source)
 .processedWith(new BindViewBasicProcessor())
 .compilesWithoutError()
 .and()
 .generatesSources(expected);
 } https://github.com/google/compile-testing
  32. 38 А что с Котлином? • kapt1: • Компилятор где-то

    у себя сохраняет всю информацию про аннотации • Свой annotation processor запихивается в javac, достаёт всю эту информацию и кодогенерирует • Сперва не было стабов, потом они всё же появились • Работало медленно • kapt2: • Оборачивает IntelliJ AST для annotation processor’а • AST был не очень готов для этого – работает ещё медленее • kapt3: • Создает Java AST • Короче говоря – та же задумка, что и у kapt2, с другим AST внутри
  33. 41 А что если мы хотим генерировать код на Котлине?

    • kapt1: • достаточно положить файл в нужную папку из ProcessingEnvironment’а, дальше компилятор сделает всё за нас • Только на первом раунде процессинга • kapt2/3: • через боль и страдания вручную дергать компилятор • или ждать KT-14070
  34. 42 А что не так? public class MainActivity extends AppCompatActivity

    {
 @BindView(R.id.activity_main)
 RelativeLayout rootLayout;
 
 @BindView(R.id.sample_text)
 TextView sampleText;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 Butterknife.bind(this);
 sampleText.setText("Hello annotations!");
 }
 }
  35. 43 А что не так? public class MainActivity extends AppCompatActivity

    {
 @BindView(R.id.activity_main)
 RelativeLayout rootLayout;
 
 @BindView(R.id.sample_text)
 TextView sampleText;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 Butterknife.bind(this);
 sampleText.setText("Hello annotations!");
 }
 }
  36. 44 А что не так? public class MainActivity extends AppCompatActivity

    {
 @BindView(R.id.activity_main)
 RelativeLayout rootLayout;
 
 @BindView(R.id.sample_text)
 TextView sampleText;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 Butterknife.bind(this);
 sampleText.setText("Hello annotations!");
 }
 }
  37. 45 Proguard? А что не так? public class MainActivity extends

    AppCompatActivity {
 @BindView(R.id.activity_main)
 RelativeLayout rootLayout;
 
 @BindView(R.id.sample_text)
 TextView sampleText;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 Butterknife.bind(this);
 sampleText.setText("Hello annotations!");
 }
 }
  38. 46 Proguard? А что не так? public class MainActivity extends

    AppCompatActivity {
 @BindView(R.id.activity_main)
 RelativeLayout rootLayout;
 
 @BindView(R.id.sample_text)
 TextView sampleText;
 
 @Override
 protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 Butterknife.bind(this);
 sampleText.setText("Hello annotations!");
 }
 }
  39. 48 Идём глубже public class BindViewAdvancedVisitor extends ElementScanner7<Void, Void> {


    private final CodeBlock.Builder bindViewBlock = CodeBlock.builder();
 private final Messager logger;
 private final Filer filer;
 private final TypeElement originElement; private final Trees trees;
 private final TreeMaker treeMaker;
 private final Names names;
 
 public BindViewAdvancedVisitor(ProcessingEnvironment env, TypeElement element) {
 super();
 logger = env.getMessager();
 filer = env.getFiler();
 originElement = element; trees = Trees.instance(env);
 final JavacProcessingEnvironment javacEnv = (JavacProcessingEnvironment) env;
 treeMaker = TreeMaker.instance(javacEnv.getContext());
 names = Names.instance(javacEnv.getContext());
 } }
  40. 49 Идём глубже public class BindViewAdvancedVisitor extends ElementScanner7<Void, Void> {


    private final CodeBlock.Builder bindViewBlock = CodeBlock.builder();
 private final Messager logger;
 private final Filer filer;
 private final TypeElement originElement; private final Trees trees;
 private final TreeMaker treeMaker;
 private final Names names;
 
 public BindViewAdvancedVisitor(ProcessingEnvironment env, TypeElement element) {
 super();
 logger = env.getMessager();
 filer = env.getFiler();
 originElement = element; trees = Trees.instance(env);
 final JavacProcessingEnvironment javacEnv = (JavacProcessingEnvironment) env;
 treeMaker = TreeMaker.instance(javacEnv.getContext());
 names = Names.instance(javacEnv.getContext());
 } }
  41. 50 Идём глубже @Override
 public Void visitVariable(VariableElement e, Void aVoid)

    {
 ((JCTree) trees.getTree(e)).accept(new TreeTranslator() {
 @Override
 public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
 super.visitVarDef(jcVariableDecl);
 jcVariableDecl.mods.flags &= ~Flags.PRIVATE;
 }
 });
 final BindView bindView = e.getAnnotation(BindView.class);
 bindViewBlock.addStatement("(($T) this).$L = ($T) findViewById($L)",
 ClassName.get(originElement), e.getSimpleName(), ClassName.get(e.asType()), bindView.value());
 return super.visitVariable(e, aVoid);
 }
  42. 51 Идём глубже @Override
 public Void visitVariable(VariableElement e, Void aVoid)

    {
 ((JCTree) trees.getTree(e)).accept(new TreeTranslator() {
 @Override
 public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
 super.visitVarDef(jcVariableDecl);
 jcVariableDecl.mods.flags &= ~Flags.PRIVATE;
 }
 });
 final BindView bindView = e.getAnnotation(BindView.class);
 bindViewBlock.addStatement("(($T) this).$L = ($T) findViewById($L)",
 ClassName.get(originElement), e.getSimpleName(), ClassName.get(e.asType()), bindView.value());
 return super.visitVariable(e, aVoid);
 }
  43. 52 Идём глубже @Override
 public Void visitVariable(VariableElement e, Void aVoid)

    {
 ((JCTree) trees.getTree(e)).accept(new TreeTranslator() {
 @Override
 public void visitVarDef(JCTree.JCVariableDecl jcVariableDecl) {
 super.visitVarDef(jcVariableDecl);
 jcVariableDecl.mods.flags &= ~Flags.PRIVATE;
 }
 });
 final BindView bindView = e.getAnnotation(BindView.class);
 bindViewBlock.addStatement("(($T) this).$L = ($T) findViewById($L)",
 ClassName.get(originElement), e.getSimpleName(), ClassName.get(e.asType()), bindView.value());
 return super.visitVariable(e, aVoid);
 }
  44. 55 Идём глубже String className = originElement.getSimpleName() + "$$ViewBinding";
 TypeSpec

    typeSpec = TypeSpec.classBuilder(className)
 .addModifiers(Modifier.ABSTRACT)
 .superclass(ClassName.get(originElement.getSuperclass()))
 .addOriginatingElement(originElement)
 .addMethod(MethodSpec.methodBuilder("setContentView")
 .addAnnotation(Override.class)
 .addModifiers(Modifier.PUBLIC)
 .addParameter(TypeName.INT, "layoutResId")
 .addStatement("super.setContentView(layoutResId)")
 .addCode(bindViewBlock.build())
 .build())
 .build();
 JavaFile javaFile = JavaFile.builder(originElement.getEnclosingElement().toString(), typeSpec)
 .addFileComment("Generated, do not modify")
 .build();
  45. 56 Идём глубже String className = originElement.getSimpleName() + "$$ViewBinding";
 TypeSpec

    typeSpec = TypeSpec.classBuilder(className)
 .addModifiers(Modifier.ABSTRACT)
 .superclass(ClassName.get(originElement.getSuperclass()))
 .addOriginatingElement(originElement)
 .addMethod(MethodSpec.methodBuilder("setContentView")
 .addAnnotation(Override.class)
 .addModifiers(Modifier.PUBLIC)
 .addParameter(TypeName.INT, "layoutResId")
 .addStatement("super.setContentView(layoutResId)")
 .addCode(bindViewBlock.build())
 .build())
 .build();
 JavaFile javaFile = JavaFile.builder(originElement.getEnclosingElement().toString(), typeSpec)
 .addFileComment("Generated, do not modify")
 .build();
  46. 57 Идём глубже String className = originElement.getSimpleName() + "$$ViewBinding";
 TypeSpec

    typeSpec = TypeSpec.classBuilder(className)
 .addModifiers(Modifier.ABSTRACT)
 .superclass(ClassName.get(originElement.getSuperclass()))
 .addOriginatingElement(originElement)
 .addMethod(MethodSpec.methodBuilder("setContentView")
 .addAnnotation(Override.class)
 .addModifiers(Modifier.PUBLIC)
 .addParameter(TypeName.INT, "layoutResId")
 .addStatement("super.setContentView(layoutResId)")
 .addCode(bindViewBlock.build())
 .build())
 .build();
 JavaFile javaFile = JavaFile.builder(originElement.getEnclosingElement().toString(), typeSpec)
 .addFileComment("Generated, do not modify")
 .build();
  47. 58 Идём глубже try {
 javaFile.writeTo(filer);
 JCTree.JCExpression selector = treeMaker.Ident(names.fromString(javaFile.packageName));


    selector = treeMaker.Select(selector, names.fromString(typeSpec.name));
 ((JCTree.JCClassDecl) trees.getTree(originElement)).extending = selector;
 } catch (IOException exception) {
 logger.printMessage(Diagnostic.Kind.ERROR, "Can't write file: " + exception.getMessage());
 }
  48. 59 Идём глубже try {
 javaFile.writeTo(filer);
 JCTree.JCExpression selector = treeMaker.Ident(names.fromString(javaFile.packageName));


    selector = treeMaker.Select(selector, names.fromString(typeSpec.name));
 ((JCTree.JCClassDecl) trees.getTree(originElement)).extending = selector;
 } catch (IOException exception) {
 logger.printMessage(Diagnostic.Kind.ERROR, "Can't write file: " + exception.getMessage());
 }
  49. 60 Идём глубже // Generated, do not modify
 package me.strlght.advanced_processor_sample;


    
 import android.support.v7.app.AppCompatActivity;
 import android.widget.RelativeLayout;
 import android.widget.TextView;
 import java.lang.Override;
 
 abstract class MainActivity$$ViewBinding extends AppCompatActivity {
 @Override
 public void setContentView(int layoutResId) {
 super.setContentView(layoutResId);
 ((MainActivity) this).rootLayout = (RelativeLayout) findViewById(2131492944);
 ((MainActivity) this).sampleText = (TextView) findViewById(2131492945);
 }
 } public class MainActivity extends MainActivity$$ViewBinding { /* … */ }
  50. 62 Что хорошо? А что не так? • Не работает

    со всем, что не Java • Документация? • В любой момент может отвалиться • Переделываем AST как угодно без ограничений, без регистрации и смс
  51. 63 Что хорошо? А что не так? А кто использует?

    • ¯\_(ツ)_/¯ • Переделываем AST как угодно без ограничений, без регистрации и смс • Не работает со всем, что не Java • Документация? • В любой момент может отвалиться
  52. 66 Assert assert sampleObject == null; public class Sample {


    static final boolean $assertionsDisabled;
 
 static {};
 Code: // $assertionsDisabled = !Class. desiredAssertionStatus();
 0: ldc #6 // class Sample
 2: invokevirtual #7 // Method java/lang/Class.desiredAssertionStatus:()Z
 5: ifne 12
 8: iconst_1
 9: goto 13
 12: iconst_0
 13: putstatic #3 // Field $assertionsDisabled:Z
 16: iconst_0
 17: putstatic #2 // Field sample:Z
 20: return
 }
  53. 67 Assert assert sampleObject == null; public class Sample {


    static final boolean $assertionsDisabled;
 
 static {};
 Code: // $assertionsDisabled = !Class. desiredAssertionStatus();
 0: ldc #6 // class Sample
 2: invokevirtual #7 // Method java/lang/Class.desiredAssertionStatus:()Z
 5: ifne 12
 8: iconst_1
 9: goto 13
 12: iconst_0
 13: putstatic #3 // Field $assertionsDisabled:Z
 16: iconst_0
 17: putstatic #2 // Field sample:Z
 20: return
 }
  54. 68 Transform API class AssertTransform extends Transform {
 @Override
 String

    getName() {
 return "assert"
 }
 
 @Override
 Set<QualifiedContent.ContentType> getInputTypes() {
 return EnumSet.of(QualifiedContent.DefaultContentType.CLASSES)
 }
 
 @Override
 Set<QualifiedContent.Scope> getScopes() {
 return EnumSet.of(QualifiedContent.Scope.PROJECT)
 }
 
 @Override
 boolean isIncremental() {
 return true
 } }
  55. 69 Transform API class AssertTransform extends Transform {
 @Override
 String

    getName() {
 return "assert"
 }
 
 @Override
 Set<QualifiedContent.ContentType> getInputTypes() {
 return EnumSet.of(QualifiedContent.DefaultContentType.CLASSES)
 }
 
 @Override
 Set<QualifiedContent.Scope> getScopes() {
 return EnumSet.of(QualifiedContent.Scope.PROJECT)
 }
 
 @Override
 boolean isIncremental() {
 return true
 } }
  56. 70 Transform API class AssertTransform extends Transform {
 @Override
 String

    getName() {
 return "assert"
 }
 
 @Override
 Set<QualifiedContent.ContentType> getInputTypes() {
 return EnumSet.of(QualifiedContent.DefaultContentType.CLASSES)
 }
 
 @Override
 Set<QualifiedContent.Scope> getScopes() {
 return EnumSet.of(QualifiedContent.Scope.PROJECT)
 }
 
 @Override
 boolean isIncremental() {
 return true
 } }
  57. 71 Transform API class AssertTransform extends Transform {
 @Override
 String

    getName() {
 return "assert"
 }
 
 @Override
 Set<QualifiedContent.ContentType> getInputTypes() {
 return EnumSet.of(QualifiedContent.DefaultContentType.CLASSES)
 }
 
 @Override
 Set<QualifiedContent.Scope> getScopes() {
 return EnumSet.of(QualifiedContent.Scope.PROJECT)
 }
 
 @Override
 boolean isIncremental() {
 return true
 } }
  58. 72 Transform API class AssertTransform extends Transform {
 @Override
 String

    getName() {
 return "assert"
 }
 
 @Override
 Set<QualifiedContent.ContentType> getInputTypes() {
 return EnumSet.of(QualifiedContent.DefaultContentType.CLASSES)
 }
 
 @Override
 Set<QualifiedContent.Scope> getScopes() {
 return EnumSet.of(QualifiedContent.Scope.PROJECT)
 }
 
 @Override
 boolean isIncremental() {
 return true
 } }
  59. 73 Transform API @Override
 void transform(Context context, Collection<TransformInput> inputs,
 Collection<TransformInput>

    referencedInputs, TransformOutputProvider outputProvider,
 boolean isIncremental) throws IOException, TransformException, InterruptedException {
 final DirectoryInput directoryInput = inputs.first().directoryInputs.first()
 final File output = outputProvider.getContentLocation(
 directoryInput.name, EnumSet.of(QualifiedContent.DefaultContentType.CLASSES),
 EnumSet.of(QualifiedContent.Scope.PROJECT), Format.DIRECTORY) final Collection<File> files = extractFiles(referencedInputs, isIncremental)
 final def processor = new AssertProcessor(files, output)
 try {
 processor.process()
 } catch (final Exception exception) {
 throw new TransformException(exception)
 }
 }
  60. 74 Transform API Collection<File> extractFiles(Collection<TransformInput> referencedInputs, boolean isIncremental) {
 if

    (isIncremental) { return referencedInputs.collectMany { it.directoryInputs.collect { it.file } + it.jarInputs.collect { it.file } } } else { // extract modified files only return referencedInputs.collectMany { it.directoryInputs.collect { it.changedFiles.findAll { it.value == Status.ADDED || it.value == Status.CHANGED } .collect { it.key } } } }
 }
  61. 76 Bytecode class Patcher {
 fun copyAndPatchClasses(inputPath: File, outputPath: File)

    {
 for (sourceFile in inputPath.walk()) {
 val relativePath = sourceFile.toRelativeString(inputPath)
 val targetFile = File(outputPath, relativePath)
 if (sourceFile.isFile) {
 val type = getObjectTypeFromFile(relativePath)
 if (type != null) {
 patchClass(sourceFile, targetFile)
 }
 } else {
 targetFile.mkdirs()
 }
 }
 }
 
 private fun getObjectTypeFromFile(relativePath: String): Type? {
 if (relativePath.endsWith(".class")) {
 val internalName = relativePath.substringBeforeLast(".class")
 return Type.getObjectType(internalName)
 }
 return null
 }
 }
  62. 77 Bytecode class Patcher {
 fun copyAndPatchClasses(inputPath: File, outputPath: File)

    {
 for (sourceFile in inputPath.walk()) {
 val relativePath = sourceFile.toRelativeString(inputPath)
 val targetFile = File(outputPath, relativePath)
 if (sourceFile.isFile) {
 val type = getObjectTypeFromFile(relativePath)
 if (type != null) {
 patchClass(sourceFile, targetFile)
 }
 } else {
 targetFile.mkdirs()
 }
 }
 }
 
 private fun getObjectTypeFromFile(relativePath: String): Type? {
 if (relativePath.endsWith(".class")) {
 val internalName = relativePath.substringBeforeLast(".class")
 return Type.getObjectType(internalName)
 }
 return null
 }
 }
  63. 78 Bytecode private fun patchClass(sourceFile: File, targetFile: File) {
 val

    reader = ClassReader(sourceFile.readBytes())
 val writer = ClassWriter(reader, ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES)
 val visitor = AssertionVisitor(writer)
 reader.accept(visitor, ClassReader.SKIP_FRAMES)
 targetFile.parentFile.mkdirs()
 targetFile.writeBytes(writer.toByteArray())
 } http://asm.ow2.org/ ClassReader AssertionVisitor ClassWriter
  64. 79 Bytecode private val STATIC_INITIALIZER_METHOD = Method("<clinit>", Type.VOID_TYPE, arrayOf())
 


    class AssertionVisitor(
 delegate: ClassVisitor
 ) : ClassVisitor(ASM5, delegate) {
 override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?,
 exceptions: Array<out String>?): MethodVisitor {
 val visitor = super.visitMethod(access, name, desc, signature, exceptions)
 if (name == STATIC_INITIALIZER_METHOD.name) {
 return object : GeneratorAdapter(ASM5, visitor, access, name, desc) {
 override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, desc: String?) {
 if (opcode == Opcodes.PUTSTATIC && name == "\$assertionsDisabled") {
 pop()
 push(false)
 }
 super.visitFieldInsn(opcode, owner, name, desc)
 }
 }
 } else {
 return visitor
 }
 }
 }
  65. 80 Bytecode private val STATIC_INITIALIZER_METHOD = Method("<clinit>", Type.VOID_TYPE, arrayOf())
 


    class AssertionVisitor(
 delegate: ClassVisitor
 ) : ClassVisitor(ASM5, delegate) {
 override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?,
 exceptions: Array<out String>?): MethodVisitor {
 val visitor = super.visitMethod(access, name, desc, signature, exceptions)
 if (name == STATIC_INITIALIZER_METHOD.name) {
 return object : GeneratorAdapter(ASM5, visitor, access, name, desc) {
 override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, desc: String?) {
 if (opcode == Opcodes.PUTSTATIC && name == "\$assertionsDisabled") {
 pop()
 push(false)
 }
 super.visitFieldInsn(opcode, owner, name, desc)
 }
 }
 } else {
 return visitor
 }
 }
 }
  66. 81 Bytecode private val STATIC_INITIALIZER_METHOD = Method("<clinit>", Type.VOID_TYPE, arrayOf())
 


    class AssertionVisitor(
 delegate: ClassVisitor
 ) : ClassVisitor(ASM5, delegate) {
 override fun visitMethod(access: Int, name: String?, desc: String?, signature: String?,
 exceptions: Array<out String>?): MethodVisitor {
 val visitor = super.visitMethod(access, name, desc, signature, exceptions)
 if (name == STATIC_INITIALIZER_METHOD.name) {
 return object : GeneratorAdapter(ASM5, visitor, access, name, desc) {
 override fun visitFieldInsn(opcode: Int, owner: String?, name: String?, desc: String?) {
 if (opcode == Opcodes.PUTSTATIC && name == "\$assertionsDisabled") {
 pop()
 push(false)
 }
 super.visitFieldInsn(opcode, owner, name, desc)
 }
 }
 } else {
 return visitor
 }
 }
 }
  67. 83 Что хорошо? • Работает с .class-файлами А что не

    так? • Как это тестировать и дебажить?
  68. 84 Что хорошо? • Работает с .class-файлами А что не

    так? А кто использует? • ProGuard • Retrolambda • AspectJ • Как это тестировать и дебажить?
  69. 85 boolean compileJavaFiles(Collection<File> files, List<String> options) throws IOException { JavaCompiler

    javaCompiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>(); StandardJavaFileManager fileManager = javaCompiler .getStandardFileManager(diagnosticCollector, Locale.ENGLISH, Charset.forName("utf-8")); try { Iterable<? extends JavaFileObject> javaFileObjectsFromFiles = fileManager.getJavaFileObjectsFromFiles(files); JavaCompiler.CompilationTask task = javaCompiler.getTask( new StringWriter(), // do not write to System.err fileManager, diagnosticCollector, options, null, javaFileObjectsFromFiles); return task.call(); } finally { fileManager.close(); } } Компилируем новый код
  70. 86 boolean compileJavaFiles(Collection<File> files, List<String> options) throws IOException { JavaCompiler

    javaCompiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>(); StandardJavaFileManager fileManager = javaCompiler .getStandardFileManager(diagnosticCollector, Locale.ENGLISH, Charset.forName("utf-8")); try { Iterable<? extends JavaFileObject> javaFileObjectsFromFiles = fileManager.getJavaFileObjectsFromFiles(files); JavaCompiler.CompilationTask task = javaCompiler.getTask( new StringWriter(), // do not write to System.err fileManager, diagnosticCollector, options, null, javaFileObjectsFromFiles); return task.call(); } finally { fileManager.close(); } } Компилируем новый код
  71. 87 boolean compileJavaFiles(Collection<File> files, List<String> options) throws IOException { JavaCompiler

    javaCompiler = ToolProvider.getSystemJavaCompiler(); DiagnosticCollector<JavaFileObject> diagnosticCollector = new DiagnosticCollector<JavaFileObject>(); StandardJavaFileManager fileManager = javaCompiler .getStandardFileManager(diagnosticCollector, Locale.ENGLISH, Charset.forName("utf-8")); try { Iterable<? extends JavaFileObject> javaFileObjectsFromFiles = fileManager.getJavaFileObjectsFromFiles(files); JavaCompiler.CompilationTask task = javaCompiler.getTask( new StringWriter(), // do not write to System.err fileManager, diagnosticCollector, options, null, javaFileObjectsFromFiles); return task.call(); } finally { fileManager.close(); } } Компилируем новый код
  72. 91 Jack Plugin public class SamplePlugin extends SchedAnnotationProcessorBasedPlugin { @Nonnull


    @Override
 public List<Class<? extends RunnableSchedulable<? extends Component>>> getSortedRunners() {
 return Collections.
 <Class<? extends RunnableSchedulable<? extends Component>>>singletonList(
 Transformer.class);
 } 
 @Nonnull
 @Override
 public Collection<Class<? extends RunnableSchedulable<? extends Component>>> getCheckerRunners() {
 return Collections.emptyList();
 } 
 }
  73. 92 Jack Plugin @Description("Instrument a method by inserting statement to

    print its name")
 @Transform(
 add = {JMethodCall.class, JFieldRef.class, JStatement.class}
 )
 @Filter(SourceTypeFilter.class)
 public class Transformer implements RunnableSchedulable<JMethod>
  74. 93 Jack Plugin public class Transformer implements RunnableSchedulable<JMethod> {
 private

    final TypeAndMethodFormatter formatter = BinarySignatureFormatter.getFormatter();
 private final JDefinedClass javaLangSystemClass;
 private final JDefinedClass javaIoPrintStreamClass;
 private final JFieldId systemOutFieldId;
 private final JMethodId printlnMethodId;
 
 public Transformer() {
 JNodeLookup lookup = Jack.getSession().getLookup();
 javaLangSystemClass = lookup.getClass("Ljava/lang/System;");
 javaIoPrintStreamClass = lookup.getClass("Ljava/io/PrintStream;");
 systemOutFieldId = javaLangSystemClass.getFieldId("out", javaIoPrintStreamClass,
 FieldKind.STATIC);
 JType javaLangStringClass = lookup.getClass(CommonTypes.JAVA_LANG_STRING);
 List<JType> methodArgTypes = Collections.singletonList(javaLangStringClass);
 printlnMethodId = javaIoPrintStreamClass.getMethodId("println", methodArgTypes,
 MethodKind.INSTANCE_VIRTUAL, JPrimitiveType.JPrimitiveTypeEnum.VOID.getType());
 } }
  75. 94 Jack Plugin public class Transformer implements RunnableSchedulable<JMethod> {
 @Override


    public void run(@Nonnull JMethod method) {
 if (method.isNative() || method.isAbstract()) {
 return;
 }
 JStatement printStatement = createPrintlnStatement(method);
 TransformationRequest transformationRequest = new TransformationRequest(method);
 JMethodBody methodBody = (JMethodBody) method.getBody();
 TransformationStep ts = new PrependStatement(methodBody.getBlock(), printStatement);
 transformationRequest.append(ts);
 transformationRequest.commit();
 } }
  76. 95 Jack Plugin public class Transformer implements RunnableSchedulable<JMethod> {
 private

    String getMethodName(@Nonnull JMethod method) {
 StringBuilder sb = new StringBuilder();
 sb.append(formatter.getName(method.getEnclosingType()));
 sb.append(formatter.getName(method));
 return sb.toString();
 }
 
 private JStatement createPrintlnStatement(@Nonnull JMethod method) {
 SourceInfo sourceInfo = SourceInfo.UNKNOWN;
 String methodName = getMethodName(method);
 JStringLiteral methodNameString = new JStringLiteral(sourceInfo, methodName);
 JFieldRef systemOutFieldAccess = new JFieldRef(sourceInfo, null /* static access */,
 systemOutFieldId, javaLangSystemClass);
 JMethodCall methodCall = new JMethodCall(sourceInfo, systemOutFieldAccess,
 javaIoPrintStreamClass, printlnMethodId, true /* virtual method */);
 methodCall.addArg(methodNameString);
 return new JExpressionStatement(methodCall.getSourceInfo(), methodCall);
 }
 }
  77. 98 А что не так? Что хорошо? • Возможность трансформировать

    AST без костылей • Возможность верификации из коробки • Мертвое ¯\_(ツ)_/¯ • Очень низкоуровненный API
  78. 99 А кто использует? • Конкретно плагины – никто •

    А модификации AST – сам компилятор: com.android.jack.transformations.* – включение assert’ов, трансформация лямбд в анонимные классы, различные микрооптимизации, … А что не так? Что хорошо? • Возможность трансформировать AST без костылей • Возможность верификации из коробки • Мертвое ¯\_(ツ)_/¯ • Очень низкоуровненный API
  79. 100 Заключение • Кодогенерация – это хорошо и позволяет решать

    проблемы, с которыми разработчики сталкиваются чуть ли не каждый день • Инструменты для кодогенерации есть под все нужды и вкусы • Есть много полезных библиотек: JavaPoet, Compile Testing, AutoService, … • Но иногда для тестирования нужны костыли