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

Know-how as code: прикладная кодогенерация для Java-разработчика

Michael Storozhilov
February 29, 2020
470

Know-how as code: прикладная кодогенерация для Java-разработчика

Java-разработчики, особенно те, кто заняты в заказной разработке, находятся в постоянной борьбе за то, чтобы писать меньше бойлерплейт кода, будь то тривиальные геттеры-сеттеры, конструкторы и т.п. или CRUD репозитории и контроллеры. Часто решением является кодогенерация в разных видах: поддержка генерации в IDE, генерация байткода при помощи Lombok, процессоры аннотаций, порождающие новый код, фреймворки, позволяющие по описанию модели получить готовое (почти) приложение и много чего еще, не исключая новые и не очень JVM-языки, которые позволяют писать более лаконичный код и реализовывать DSL-и для решения прикладных задач.

С неоспоримой пользой в самых простых случаях приходят и ограничения, не позволяющие реализовать то, что требуется в конкретном проекте, и хуже всего, когда генерируемый код является источником дефектов, которые сложно обнаружить, а для исправления требуются специальные "костыли" поверх того, что генерируется.

В докладе мы сделаем ретроспективу наиболее часто применяемых подходов, поговорим об их сильных сторонах, ограничениях и практической применимости, а главное, попробуем взять ситуацию с кодогенерацией под контроль, чтобы она стала реально полезным инструментом эффективного разработчика.

Michael Storozhilov

February 29, 2020
Tweet

More Decks by Michael Storozhilov

Transcript

  1. 2 # ЧТО ХОТИМ • Делать микросервисы (и монолиты) на

    Spring-e • Spring Framework • Spring Boot • Spring Data (JPA)
  2. 3 # Хотя, все происходит автомагически, не обойтись, без того,

    чтобы не написать немного кода: • Configuration • Entity • Repository • Service • Controller SPRING BOOT: МИНИМАЛЬНОЕ ПРИЛОЖЕНИЕ http://foxminded.com.ua/news/55-sravnenie-stekov-java-ee-i-spring-vozmozhnosti-i-ogranicheniya/
  3. 4 # А КАК ЭТО ОБЫЧНО ВЫГЛЯДИТ? @Entity public class

    User { @Id @GeneratedValue private Long id; @Transient private transient boolean isNew; @Column(nullable = false) private String name; }
  4. 5 # А КАК МЫ ХОТИМ? @Entity public class User

    { @Column(nullable = false) private String name; }
  5. 6 # А КАК МЫ ХОТИМ? @Entity class User {

    @Column(nullable = false) String name; }
  6. 7 # А КАК МЫ ХОТИМ? @Entity class User {

    @Column(nullable = false) String name @ManyToOne @Column(nullable = false) Country country }
  7. 8 # А КАК МЫ ХОТИМ? @Entity class User {

    @Column(nullable = false) String name @ManyToOne @Column(nullable = false) Country country }
  8. 9 # А КАК МЫ ХОТИМ? @Entity class User {

    @Column(nullable = false) String name @ManyToOne @JoinColumn(nullable = false) Country country }
  9. 12 # Когунь Андрей — программист • Работаю в компании

    КРОК • Преподаю Java • Провожу встречи Московского JUG @mskjug, #jugmsk О СЕБЕ
  10. 14 # Департамент разработки программного обеспечения • Заказная (в основном)

    разработка • Для “быстрой” разработки учетных систем реализован собственный фреймворк - jXFW О КРОК ДРПО
  11. 17 # • Design / Сompile time vs Runtime •

    Чем раньше – тем лучше (дешевле) ВЫБИРАЕМ ПОДХОД
  12. 18 # • Design / Сompile time vs Runtime •

    Чем раньше – тем лучше (дешевле) • Используем Reflection? ВЫБИРАЕМ ПОДХОД
  13. 19 # • Design / Сompile time vs Runtime •

    Чем раньше – тем лучше (дешевле) • Используем Reflection? • Постараемся обойтись, не проблема сгенерировать немного больше кода ВЫБИРАЕМ ПОДХОД
  14. 20 # • Design / Сompile time vs Runtime •

    Чем раньше – тем лучше (дешевле) • Используем Reflection? • Постараемся обойтись, не проблема сгенерировать немного больше кода • Исходный код или байткод? ВЫБИРАЕМ ПОДХОД
  15. 22 # О ЧЕМ ПОГОВОРИМ • Кодогенерация в IDE •

    Java Annotation Processing (apt) • Lombok • JavaPoet • Kotlin
  16. 23 # О ЧЕМ ПОГОВОРИМ • Кодогенерация в IDE •

    Java Annotation Processing (apt) • Lombok • JavaPoet • Kotlin • Xtend
  17. 24 # О ЧЕМ ПОГОВОРИМ • Кодогенерация в IDE •

    Java Annotation Processing (apt) • Lombok • JavaPoet • Kotlin • Xtend – а что это?
  18. 25 # КОДОГЕНЕРАЦИЯ В IDE • Итеративный (и не самый

    быстрый) процесс • Генерировать можно исходный java код, любые другие ресурсы https://youtu.be/EEAiyRuaUXU
  19. 28 # РАЗ ЕСТЬ АННОТАЦИИ, БЕРЕМ ANNOTATION PROCESSING По Сгенерируем

    public interface SystemSettingRepository extends JpaRepository<User, String>, QuerydslPredicateExecutor<User> { } @Entity public class User { public String name; }
  20. 29 # РАЗ ЕСТЬ АННОТАЦИИ, БЕРЕМ ANNOTATION PROCESSING По Сгенерируем

    public interface SystemSettingRepository extends JpaRepository<User, String>, QuerydslPredicateExecutor<User> { User findByName(String name); } @Entity public class User { public String name; }
  21. 30 # РАЗ ЕСТЬ АННОТАЦИИ, БЕРЕМ ANNOTATION PROCESSING По Сгенерируем

    public interface SystemSettingRepository extends JpaRepository<User, String>, QuerydslPredicateExecutor<User> { User findПожалуйстаByName(String name); } @Entity public class User { public String name; }
  22. 31 # КАК РАБОТАЕТ ANNOTATION PROCESSING • Генерировать можно исходный

    java код, любые другие ресурсы • Процессор должен располагаться в отдельном от прикладного кода модуле (единице сборки) • Процессор не может менять код, только создавать новый
  23. 32 # КАК РАБОТАЕТ ANNOTATION PROCESSING • Генерировать можно исходный

    java код, любые другие ресурсы • Процессор должен располагаться в отдельном от прикладного кода модуле (единице сборки) • Процессор не может менять код, только создавать новый • Итеративный (и не самый быстрый) процесс
  24. 34 # ДЕЛАЕМ СВОЙ PROCESSOR ДЛЯ ENTITY @AutoService(Processor.class) public class

    EntityProcessor extends AbstractProcessor { @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); // processingEnv.getFiler(); // processingEnv.getMessager(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> set = new HashSet<>(); set.add(Entity.class.getCanonicalName()); return set; } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }
  25. 35 # ДЕЛАЕМ СВОЙ PROCESSOR ДЛЯ ENTITY @AutoService(Processor.class) public class

    EntityProcessor extends AbstractProcessor { … @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation); // … } return true; } }
  26. 36 # ДЕЛАЕМ СВОЙ PROCESSOR ДЛЯ ENTITY … @Override public

    boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { … JavaFileObject repositoryFile = filer.createSourceFile(repositoryClassName); try (PrintWriter out = new PrintWriter(repositoryFile.openWriter())) { if (packageName != null) { out.print("package "); out.print(packageName); out.println(";"); out.println(); } out.print("public class "); out.print(repositorySimpleClassName); out.println(" {"); out.println(); … } }
  27. 37 # ДЕЛАЕМ СВОЙ PROCESSOR ДЛЯ ENTITY … @Override public

    boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { … JavaFileObject repositoryFile = filer.createSourceFile(repositoryClassName); try (PrintWriter out = new PrintWriter(repositoryFile.openWriter())) { if (packageName != null) { out.print("package "); out.print(packageName); out.println(";"); out.println(); } out.print("public class "); out.print(repositorySimpleClassName); out.println(" {"); out.println(); … } }
  28. 38 # ДЕЛАЕМ СВОЙ PROCESSOR ДЛЯ ENTITY … @Override public

    boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { … JavaFileObject repositoryFile = filer.createSourceFile(repositoryClassName); try (PrintWriter out = new PrintWriter(repositoryFile.openWriter())) { if (packageName != null) { out.print("package "); out.print(packageName); out.println(";"); out.println(); } out.print("public class "); out.print(repositorySimpleClassName); out.println(" {"); out.println(); … } }
  29. 41 # СГЕНЕРИРУЕМ МЕТОД С JAVAPOET @Autowired public void setQueryParamsBuilderFactory(QueryParamsBuilderFactory

    queryParamsBuilderFactory) { this.queryParamsBuilderFactory = queryParamsBuilderFactory; } • JavaPoet - https://github.com/square/javapoet
  30. 42 # СГЕНЕРИРУЕМ МЕТОД С JAVAPOET @Autowired public void setQueryParamsBuilderFactory(QueryParamsBuilderFactory

    queryParamsBuilderFactory) { this.queryParamsBuilderFactory = queryParamsBuilderFactory; } MethodSpec setQueryParamsBuilderFactory = MethodSpec .methodBuilder("setQueryParamsBuilderFactory") .addAnnotation(Autowired.class) .addModifiers(Modifier.PUBLIC) .addParameter(QueryParamsBuilderFactory.class, "factory") .addStatement("this.queryParamsBuilderFactory = factory") .build(); • JavaPoet - https://github.com/square/javapoet
  31. 43 # СГЕНЕРИРУЕМ МЕТОД С JAVAPOET @Autowired public void setQueryParamsBuilderFactory(QueryParamsBuilderFactory

    queryParamsBuilderFactory) { this.queryParamsBuilderFactory = queryParamsBuilderFactory; } MethodSpec setQueryParamsBuilderFactory = MethodSpec .methodBuilder("setQueryParamsBuilderFactory") .addAnnotation(Autowired.class) .addModifiers(Modifier.PUBLIC) .addParameter(QueryParamsBuilderFactory.class, "factory") .addStatement("this.queryParamsBuilderFactory = factory") .build(); • JavaPoet - https://github.com/square/javapoet
  32. 44 # ДЕЛАЕМ СВОЙ PROCESSOR ДЛЯ ENTITY C JAVAPOET …

    @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { … JavaFile.builder(repositoryPackage, repositorySpec) .build() .writeTo(filer); }
  33. 45 # ЕСЛИ ЧТО-ТО ПОШЛО НЕ ТАК • Проверяем состояние

    и выводим информацию для разработчика messager.printMessage(Diagnostic.Kind.WARNING, "You do not set nullability", nameElement);
  34. 48 # НЕ ЗАБЫТЬ ПРОСТАВИТЬ НЕМНОГО АННОТАЦИЙ • А еще

    можно добавить @SuppressWarnings("all") • Обязательно добавить @Generated
  35. 50 # ЧЕГО НЕ ХВАТАЕТ? • Генерация в контексте одного

    класса, порядок срабатывания процессоров
  36. 51 # ЧЕГО НЕ ХВАТАЕТ? • Генерация в контексте одного

    класса, порядок срабатывания процессоров • Отсутствие инкрементальной компиляции • Отсутствие отношения генерируемого кода с исходным
  37. 52 # ЧЕГО НЕ ХВАТАЕТ? • Генерация в контексте одного

    класса, порядок срабатывания процессоров • Отсутствие инкрементальной компиляции • Отсутствие отношения генерируемого кода с исходным
  38. 53 # ЧЕГО НЕ ХВАТАЕТ? • Генерация в контексте одного

    класса, порядок срабатывания процессоров • Отсутствие инкрементальной компиляции • Отсутствие отношения генерируемого кода с исходным • Генерация только нового кода
  39. 55 # APT + LOMBOK? • Можно пользоваться готовыми аннотациями

    • А можно сделать поддержку своей: @Entity Не забудь добавить id и version
  40. 56 # APT + LOMBOK? • Можно пользоваться готовыми аннотациями

    • А можно сделать поддержку своей: @Entity Не забудь добавить id и version @MetaInfServices(JavacAnnotationHandler.class) public class EntityJavacHandler extends JavacAnnotationHandler<Entity> { … }
  41. 57 # APT + LOMBOK? • Удачи с JCTree This

    is NOT part of any supported API. If you write code that depends on this, you do so at your own risk. This code and its internal interfaces are subject to change or deletion without notice. @Override public void handle(AnnotationValues<Entity> annotation, JCTree.JCAnnotation ast, JavacNode annotationNode) { Context context = annotationNode.getContext(); Javac8BasedLombokOptions options = Javac8BasedLombokOptions.replaceWithDelombokOptions(context); options.deleteLombokAnnotations(); JavacHandlerUtil.deleteAnnotationIfNeccessary(annotationNode, Entity.class); …
  42. 58 # APT + LOMBOK? • JCTree • С этим

    разбирается Шерлок зато с вашим кодом будет разбираться Шерлок Это больно и неприятно,
  43. 59 # МОЖЕТ, НАПИШЕМ СВОЙ ЯЗЫК? • Описываем доменную модель

    на собственном диалекте • Реализуем maven (gradle) плагин • Генерацию запускаем из IDE
  44. 60 # МОЖЕТ, НАПИШЕМ СВОЙ ЯЗЫК? • Описываем доменную модель

    на собственном диалекте • Реализуем maven (gradle) плагин • Генерацию запускаем из IDE • Иван Пономарев, КУРС – JavaCC + JavaPoet
  45. 61 # МОЖЕТ, НАПИШЕМ СВОЙ ЯЗЫК? • Описываем доменную модель

    на собственном диалекте • Реализуем maven (gradle) плагин • Генерацию запускаем из IDE • Иван Пономарев, КУРС – JavaCC + JavaPoet
  46. 62 # РЕАЛИЗУЕМ MAVEN (GRADLE) ПЛАГИН • Контроль порядка генерации

    • Возможность генерировать общий код для нескольких модулей
  47. 66 # А ЧТО KOTLIN? KAPT @Entity data class User

    { val name: String } • kotlin-metadata • KotlinPoet
  48. 67 # А ЧТО KOTLIN? COMPILER PLUGIN • Есть возможность

    реализовать плагин для компилятора, но:
  49. 68 # Xtend (http://xtend-lang.org) — это статически типизированный язык программирования,

    приемник Xpand, построенный с использованием Xtext и компилирующийся в Java исходный код. • Начало разработки – 2011 год • Текущая версия – 2.20.0 О XTEND https://jaxenter.com/pirates-of-the-jvm-the-infographic-132524.html
  50. 69 # • Как в Java – • Классы, методы,

    поля • Лямбда-выражения • var (val) О XTEND • Не как в Java – • Active annotations • Template Expressions • Switch Expression • Нет continue, break
  51. 70 # • Как в Java – • Классы, методы,

    поля • Лямбда-выражения • var (val) О XTEND – РАНЬШЕ • Не как в Java – • Active annotations • Template Expressions • Switch Expression • Нет b ? e1 : e2; • Нет continue, break
  52. 71 # • Как в Java – • Классы, методы,

    поля • Лямбда-выражения • var (val) • b ? e1 : e2; • “Сырые” строки • Switch Expression О XTEND – ТЕПЕРЬ • Не как в Java – • Active annotations • Нет continue, break • Template Expressions
  53. 72 # Возможность повлиять на результат компиляции Xtend Java @Active(EntityProcessor.class)

    public @interface Entity { … } АКТИВНЫЕ АННОТАЦИИ Xtend @Active(EntityProcessor) annotation Entity { … }
  54. 73 # Возможность повлиять на результат компиляции Xtend, из коробки:

    Xtend @Accessors(PUBLIC_GETTER, PUBLIC_SETTER) @ToString @EqualsHashCode @FinalFieldsConstructor … АКТИВНЫЕ АННОТАЦИИ
  55. 74 # Возможность повлиять на результат компиляции Xtend, из коробки:

    Xtend АКТИВНЫЕ АННОТАЦИИ Java @Entity public class User { public String name; } @Entity class User { String name }
  56. 75 # Возможность повлиять на результат компиляции Xtend, из коробки:

    Xtend АКТИВНЫЕ АННОТАЦИИ Java @Entity public class User { @Id @GeneratedValue public String id; @Version public Long ts; public String name; … } @Entity class User { String name }
  57. 78 # ТАК ЧТО МНЕ ДЕЛАТЬ? You own Platform IDE

    plugin Frameworks DSLs maven (gradle) plugin apt (kapt)
  58. 79 # ВЫВОДЫ • Технологии идеальны, …. • Не обязательно

    переводить проект на некоторую технологию целиком • Лучше потратить время на разработку “интересного” кода, будь то генератор или бизнес-логика и не писать “скучный” код • Меньше кода написано – меньше ошибок