Как стать автором
Обновить

Трансформации AST — Первый шаг к тяжёлым веществам

Java *Groovy & Grails *
А давайте сделаем magic с вашим Java кодом. Вот такой:


Берем это:
import groovy.transform.Canonical
import groovy.transform.TupleConstructor

@Canonical
@TupleConstructor
class Person {
    int id
    String firstName
    String lastName
    Date birthdate
}

Компилируем, и в байткоде получаем аналог вот этого:
Адский бойлерплейт на Джаве на 100 с лишним строк
import java.util.Date;
import java.util.Map;

public class Person {
    private int id;
    private String firstName;
    private String lastName;
    private Date birthdate;

    //Эта штука добавлена @TupleConstructor-ом
    public Person(Map parameters){
        this.id = (int) parameters.get("id");
        this.firstName = (String) parameters.get("firstName");
        this.lastName = (String) parameters.get("lastName");
        this.birthdate = (Date) parameters.get("birthdate");
    }

    public Person(int id, String firstName, String lastName, Date birthdate) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.birthdate =birthdate;
    }

    public Person(int id, String firstName, String lastName) {
        this(id, firstName, lastName, null);
    }

    public Person(int id, String firstName) {
        this(id, firstName, null, null);
    }

    public Person(int id) {
        this(id, null, null, null);
    }

    public Person() {
        this(0, null, null, null);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Person person = (Person) o;

        if (id != person.id) return false;
        if (birthdate != null ? !birthdate.equals(person.birthdate) : person.birthdate != null) return false;
        if (firstName != null ? !firstName.equals(person.firstName) : person.firstName != null) return false;
        if (lastName != null ? !lastName.equals(person.lastName) : person.lastName != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        int result = id;
        result = 31 * result + (firstName != null ? firstName.hashCode() : 0);
        result = 31 * result + (lastName != null ? lastName.hashCode() : 0);
        result = 31 * result + (birthdate != null ? birthdate.hashCode() : 0);
        return result;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", firstName='" + firstName + '\'' +
                ", lastName='" + lastName + '\'' +
                ", birthdate=" + birthdate +
                '}';
    }

    public int getId() {
        return this.id;
    }

    public void setId(int paramInt) {
        this.id = paramInt;
    }

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String paramString) {
        this.firstName = paramString;
    }

    public String getLastName() {
        return this.lastName;
    }

    public void setLastName(String paramString) {
        this.lastName = paramString;
    }

    public Date getBirthdate() {
        return this.birthdate;
    }

    public void setBirthdate(Date paramDate) {
        this.birthdate = paramDate;
    }
}



Ну, как-бы да, приятно. Но ничего уникального, вот-же есть Lombok, не говоря уже о способности любого хорошего IDE сначала генерить, а потом прятать всесь этот бойлерплейт.

Так зачем именно Groovy, почему AST transformations?
В этой статье я попробую вкраце обосновать, зачем пользоваться Groovy AST transformations в Java проектах, и (опять-же вкраце) рассказать какие AST transfromations есть в Groovy сегодня. Если вы уже знаете зачем, и хотите только «как и что», смело листайте к «Введение в AST transformations».

Итак, почему AST transformations а не Lombok?


Начнем с того, что для того чтобы пользоваться AST transformations вам не нужно ни знать Groovy, ни писать на Groovy, ни запускать Groovy в рантайме. Трансформация происходит во время компиляции сорцов и Groovy добавляется одним jar-ом в список зависимостей. Всё.
Таким образом, AST transformations являются прекрасным способом «протащить» Groovy в ваш проект: «Смотри, босс, это ничего страшного, это просто ещё одна библиотека для борьбы с бойлерплетом!». А потом, уже, конечно, шаг за шагом, тестом на Споке, за билдом на Грейдле, в вашем коде появится настоящий Groovy — динамический, фунциональный и элегантный. AST transformations это только первый шаг.
Кроме того, AST transformations намного более расширяемы, мощны и универсальны, чем Ломбок.
Последнее, но не менее важное — AST transformations прекрасно поддерживаются в любом IDE с поддержкой Groovy, а не только в Эклипсе.
Фронтальное сравнение Ломбока с AST transformations явно выходит за рамки этой статьи, так что на этом остановимся.

Мне кажется само собой разумеющимся, что генерация байткода имеет огромное преимущество над генерацией (а потом «схлопыванием» в редакторе чтобы не мозолило глаза) исходного кода — генерация происходит во время компилирования, её не нужно «поддерживать». Один пример — IntelliJ IDEA прекрасно генерирует hashcode и equals. При добавлении нового поля, я ручками стираю эти 2 метода и генерю их заново. Фу.

Можно ещё многое сказать о преимуществах AST transformations для разработчиков как Java, так и Groovy, но, я надеюсь, идея ясна. Пора переходить к практике.

Введение в AST transformations


Одна из самых главных плюшек в Groovy, это, конечно метапрограммирование. Оно бывает двух типов — во время компиляции и во время исполнения.
Метапрограммирование во время исполнения это примерно «ах, вы вызвали метод, которого не существует? Не страшно, мы сейчас чего-нить придумаем, на основе того, что вы имели ввиду, когда вызывали этот метод». Примеров такого масса — практически любая библиотека Groovy основана на таких штуках, будь то билдеры, сларперы, Grails, Ratpack, Gradle, и все остальное. Но, сейчас не об этом (если хотите об этом, смотрите пункт 1 наглого пиара в конце поста).

Сейчас мы поговорим о метапрограммировании во время компиляции, а именно о том, как просто написав в коде одно, в байткоде получить другое (ну, или дополнительное).

Начнем мы с трансформации, которая прошита прямо в самом Groovy, без всяких аннотаций и прочих добавок.
Пишем:
class Person {
    String name
}

На выходе получаем байткод, в котором все поля — private (в данном случае — name), и прописаны все getters и setters (ну, в данном случае только getName() и setName(String name), но идея ясна).

Эта прекрасная мелочь является полноценным примером метапрограммирования во время компиляции.

Посмотрев на это небольшое избавление от бойлерплейта, чудесный человек Danno Ferrin сказал себе: «Но ведь есть ещё много бойлерплейта, кроме геттеров и сеттеров, и не у всех они одинаковые! Давайте-ка придумаем чего-нить подключаемое и расширяемое!» И так родились AST Transformations (первая, как ни странно, была @Bindable. Хотя, если посмотреть, сколько кода она выкидывает, может и не странно).

AST transformations это набор аннотаций, которые меняют абстрактное синтаксическое дерево налету во время компиляции Groovy. Можно сказать, что добавление getters и setters это встроенная трансформация AST, которая работает всегда, без добавления аннотаций. Остальные-же включаются только по требованию.

Давайте посмотрим что у нас есть:
  • Итак, аннотация-пионер @Bindable и её подруга @Vetoable превращают геттеры и сеттеры в настоящие properties, с возможностью нацеплять на них listener-ы и слушать, регаривать и запрещать изменения.
  • Очень модные нынче словечки @Category и @Mixin добавляют природу одного класса в другой класс. Ну, примеси!
  • @Delegate добавляет все методы, которые существуют у делегата, имплементируя их, натурально, делегацией, да?
    Пишем:
    class Event {
        String name
        @Delegate Date date
    }
    

    Получаем готовую делегацию
    import java.util.Date;
    
    public class Event {
        private String name;
        private Date date;
    
        public String getName() {
            return this.name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Date getDate() {
            return this.date;
        }
    
        public void setDate(Date paramDate) {
            this.date = paramDate;
        }
    
    
        public boolean after(Date otherDate) {
            return date.after(otherDate);
        }
    
        public boolean before(Date otherDate) {
            return date.before(otherDate);
        }
    
        public long getTime() {
            return date.getTime();
        }
    
        public void setTime(long timestamp) {
            date.setTime(timestamp);
        }
    }
    


  • @Immutable делает класс неизменяемым, а конкретно:
    1. сеттеры кидают ReadOnlyPropertyException
    2. класс становится final
    3. поля становятся private и final
    4. появляются конструкторы со всеми полями: как просто в параметрах, так и в мапе (как в первом примере)
    5. появляется код, который создает резервные копии для изменяемых компонентов
    6. появляются equals, hashcode и toString

  • Вот ещё пачка подобных борцов с бойлерплейтом: @InheritConstructors добавляет все конструкторы из супер-класса, @TupleConstructor добавляет конструктор map, в котором ключи — названия полей, а значения — значения (см. первый пример этой стати), @AutoClone и @AutoExternalize добавляет соответствующие методы, а @Canonical делает «правильный Джава класс» — с конструктором без параметров, конструкторами, которые все параметры принимают (как подряд, так и мапой), и equals, hashCode и toString-ом. Ну, как @Immutable, только mutable — его мы тоже видели в первом примере.
  • Ещё один модный термин! @Lazy создаст лениво-инициализруемое поле (по первому требованию), опционально, обвернутое в soft-reference
  • @Newify позволяет создавать объекты с помощью метода new вместо названия конструктора (как в Руби), или, наоборот, только по названию конструктора, без new (как в Пайтоне). Тут, пожалуй, не помешает пример:
    @Newify rubyLikeNew() {
        assert Integer.new(42) == 42
    }
    

    или даже
    @Newify([Tree, Leaf]) buildTree() {
        Tree(Tree(Leaf(1), Leaf(2)), Leaf(3))
    }
    

    В последнем примере мы создаем Tree и Leaf без использования new. Сравните с аналогом на Java:
    public Tree buildTree() {
        return new Tree(new Tree(new Leaf(1), new Leaf(2)), new Leaf(3));
    }
    

  • А вот вам исправление давней несправедливости: в Груви по умолчанию все поля public. A как сделать package? Через @PackageScope трансформацию!
  • Вне зависимости от того, считаете ли вы Singleton паттерном, или анти-паттерном, иногда приходится его писать. Ну, или просто поставить @Singleton над классом, и лениво-инициализируемый синглтон с двойной проверкой локинга готов.
  • Наш, #razborpoletov-ный Андрей написал чудесную, вошедшую в Груви 2.2 @Memoized, которая запоминает результат работы метода, и если он вызывается ещё раз, отдает результат немедленно (и да, параметры имеют значение)
  • И напоследок — аннотация-анекдот @NotYetImplemented — она переворачивает результаты JUnit тестов: те, которые должны падать, проходят, и наоборот. Кроме того, что это напоминает def true=false //happy debugging, эта штука полезна для TDD — накидываем тесты для всех методов, включая те, которые ещё не прописаны, и заставляем тесты, которые ещё не должны и не могут работать, проходить с помощью @NotYetImplemented. Таким образом, падения этих тестов не будут мешать нам тестировать остальные.

И это ещё не всё! Есть ещё архи-важный @CompileStatic, @Field, и целый набор аннотаций для облечения страданий по concurency, но это, всё-же, в другой раз (ну, или, смотрите пункт 1 наглого пиара в конце поста).

P.S. Теперь, когда вы знаете о чем речь, вот вам две интересные хабра-статьи, о том как и чем писать новые AST трансформации. Об этом же смотрите ниже, в пункте 2 наглого пиара.

А теперь наглый пиар из 2 пунктов:
  1. Кому нужно Грувей с нуля и до достаточно продвинутого упора, айда на мои трейнинги, 17-го апреля в Москве и 15-го апреля в Казани (стучать alexbel)
  2. Кому расчленёнки абстрактного синтаксического дерева и написания собственных AST трансформаций для борьбы с вашими собственными тараканами бойлерплейтами, айда на мои доклады на JPoint 18-го апреля и на JavaDay Kazan 16-го апреля (стучать опять alexbel)
Теги:
Хабы:
Всего голосов 41: ↑31 и ↓10 +21
Просмотры 18K
Комментарии 35
Комментарии Комментарии 35

Публикации

Истории

Работа

Java разработчик
440 вакансий