Пользователь
12,0
рейтинг
19 апреля 2012 в 00:58

Разработка → Проект Lombok, или Объявляем войну бойлерплейту

JAVA*
Открою не Америку, но шкатулку Пандоры: в Java-коде много бойлерплейта. Типовые геттеры, сеттеры и конструкторы, методы ленивой инициализации, методы toString, hashCode, equals, обработчики исключений, которые никогда не выбрасываются, закрывалки потоков, блоки синхронизации. Проблема заключается даже не в том, чтобы написать всё это — современные среды разработки справляются с такими задачами нажатием нескольких клавиш. Сложность в поддержании бойлерплейта в актуальном состоянии по мере внесения модификаций в код. А в некоторых случаях (многопоточность, реализация методов hashCode и equals) и сам шаблонный код написать без ошибок — далеко не простая задача. Одним из решений проблемы является генерация кода, и в этой статье я расскажу про проект Lombok — библиотеку, которая не только может избавить вас от бойлерплейта, но и сделать это максимально прозрачно, с минимальной конфигурацией и, что немаловажно, с поддержкой на уровне среды разработки.

Подключаем Lombok


Lombok использует механизм процессинга аннотаций из Java 6, из чего следуют его минимальные требования к окружению. Для подключения Lombok к проекту достаточно включить его в зависимости. В случае использования Maven это делается следующим образом:

    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>0.11.0</version>
        <scope>provided</scope>
    </dependency>

Для большей части функциональности Lombok эта библиотека необходима только на этапе компиляции. Последняя версия Lombok на текущий момент (0.11.0) ещё не попала в центральный репозиторий Maven, однако её без проблем можно установить в локальный или корпоративный репозиторий, скачав с сайта.

Прощаемся с аксессорами


Одним из главных источников бойлерплейта в Java является отсутствие свойств на уровне языка. Во соблюдение принципов ООП на каждое объявление поля приходится писать как минимум шесть типовых строк — геттер и сеттер. Некоторые библиотеки, вроде Spring или Tapestry, для своих целей в некоторых случаях позволяют разработчику забыть про аксессоры, вставляя их самостоятельно в байт-код. Подобную функциональность предлагает и Lombok.

    public class GetterSetterExample {
    
        @Getter @Setter private int age = 10;
        
        @Setter(AccessLevel.PROTECTED) private String name;
        
        @Getter(lazy=true) private final Map<String, String> map = initMap();
    }

Параметр lazy=true аннотации Getter позволяет реализовать ленивую инициализацию поля: вызов метода initMap() в данном случае будет отложен до первого вызова геттера и обёрнут в потокобезопасную инициализацию в виде блокировки с двойной проверкой.

Деструкция конструкторов


Конструкторы POJO-классов тоже не отличаются сложностью и разнообразием — чаще всего нам необходимо что-либо из этого списка: конструктор без параметров, конструктор со всеми параметрами, конструктор только с некоторыми обязательными параметрами, статический factory-метод. Lombok легко справляется с этой задачей с помощью аннотаций @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor и параметра staticName соответственно.

@RequiredArgsConstructor(staticName = "of")
@AllArgsConstructor(access = AccessLevel.PROTECTED)
public class ConstructorExample<T> {

    private String name;
    @NonNull private T description;
    
}

Вот что мы получим в результате:

public class ConstructorExample<T> {

    private String name;
    @NonNull private T description;

    private ConstructorExample(T description) {
        if (description == null) throw new NullPointerException("description");
        this.description = description;
    }

    public static <T> ConstructorExample<T> of(T description) {
        return new ConstructorExample<T>(description);
    }

    @java.beans.ConstructorProperties({"name", "description"})
    protected ConstructorExample(String name, T description) {
        if (description == null) throw new NullPointerException("description");
        this.name = name;
        this.description = description;
    }


Генерируем типовые методы: toString, hashCode, equals


О правильной реализации методов equals и hashCode написано достаточно много — пожалуй, стоит по этому поводу вспомнить «Effective Java» Блока и статью Одерски. Вкратце можно сказать, что реализовать их корректно — непросто, поддерживать в актуальном состоянии — ещё сложнее, а занимать они вполне могут добрую половину класса. Метод toString не так критичен для корректности кода, но актуализировать его каждый раз при изменении класса — тоже приятного мало. Предоставим возможность Lombok сделать за нас эту неблагодарную работу с помощью двух нехитрых аннотаций:

@ToString(exclude="id") 
@EqualsAndHashCode(exclude="id")
public class Person {
    private Long id;
    private String name;
    ...
}


Логгер-невидимка


Если вы пользуетесь одной из популярных библиотек протоколирования, то, вероятнее всего, в каждом классе у вас присутствует статическое объявление логгера:

public class Controller {

    private static final Logger log = LoggerFactory.getLogger(Controller.class);

    public void someMethod() {
        log.debug("someMethod started");

Вместо этого Lombok предлагает воспользоваться аннотациями Log, @CommonsLog, @Log4j или @Slf4j — в зависимости от предпочитаемого средства протоколирования:

@Slf4j
public class Controller {

    public void someMethod() {
        log.debug("someMethod started");


Финализируем локальные переменные


Хорошим стилем программирования является использование финальных локальных переменных, однако, учитывая статическую типизацию и отсутствие в Java выведения типов, объявление какой-нибудь особенно навороченной карты вполне может вылезть за пределы экрана. Если с Java переходить на Scala или Groovy пока не хочется, то можно воспользоваться следующим хаком Lombok:

public class ValExample {

    public String example() {
        val map = new HashMap<String, Map<String, String>>();
        
        for (val entry: map.entrySet()) {
        ...

Переменная map в данном случае будет объявлена как final, в то же время описание её типа будет взято из правой части выражения присваивания.

Безнаказанно бросаем исключения


Далеко не все разработчики, к сожалению, читали уже упоминавшуюся здесь «Effective Java» Блока или «Robust Java» Стелтинга. А может, читали, но не очень внимательно. Или, возможно, у них и впрямь была какая-то вполне обоснованная мотивация объявить вот это конкретное исключение проверяемым — но вам от этого не легче, ведь вы-то знаете, что оно не возникнет никогда! Что делать, например, с UnsupportedEncodingException, если вы абсолютно уверены, что без системной поддержки кодировки UTF-8 ваше приложение всё равно работать не будет? Приходится заключать код в try-catch и писать бессмысленный вывод в лог, который никогда не дождётся своего часа, переоборачивать исключение в runtime-обёртку, которой не суждено появиться на свет, или и вовсе игнорировать пустым блоком перехвата (который, не знаю, как у вас, а у меня лично всегда вызывает желание схватиться за револьвер). Lombok и здесь предлагает альтернативу.

    public class SneakyThrowsExample implements Runnable {

        @SneakyThrows // "Здесь исключений нет!"
        public void run() {
            throw new Throwable(); // "Ёжик исключения не брал!"
        }
    
    }

Для этого колдунства, в отличие от всего прочего, понадобится подключить Lombok в рантайме. Всё просто: Lombok усыпляет бдительность компилятора, перехватывая исключение в try, а затем в рантайме незаметно перевыбрасывает его в catch. Фишка в том, что на уровне байт-кода можно выбросить любое исключение, даже то, которое не объявлено в сигнатуре метода.

Правильная синхронизация


Многопоточность — весьма сложная область программирования, имеющая в Java свою идиоматику и паттерны. Одной из правильных практик является использование для синхронизации приватных финальных полей, так как на любом общедоступном локе может пожелать синхронизироваться какой-либо не связанный кусок функциональности, что приведёт к ненужным блокировкам и трудно отлавливаемым ошибкам. Lombok умеет корректно синхронизировать содержимое метода, отмеченного аннотацией @Synchronized — как статического, так и метода экземпляра:

@Synchronized
public static void hello() {
    System.out.println("World");
}

@Synchronized
public int answerToLife() {
    return 42;
}

Вот что получим:

private static final Object $LOCK = new Object[0];
private final Object $lock = new Object[0];

public static void hello() {
    synchronized($LOCK) {
        System.out.println("world");
    }
}

public int answerToLife() {
    synchronized($lock) {
        return 42;
    }
}


А что на это скажет IDE?


Все эти замечательные фишки ничего бы не стоили, если бы при открытии проекта в Eclipse или IntelliJ IDEA строчки кода разгорались бы красным пламенем от праведного гнева компилятора. К счастью, интеграция со средами разработки имеется, и достаточно неплохая. Для IntelliJ IDEA плагин присутствует в стандартном репозитории:



Для Eclipse и NetBeans установка немного необычная. Необходимо запустить файл lombok.jar, и он покажет симпатичный инсталлятор, предлагающий накатить Lombok на существующие инсталляции Eclipse:



Указанные плагины не только убеждают среду разработки в том, что отсутствие геттеров, сеттеров и прочих элементов бойлерплейта ей только чудится — они ещё и корректно подсвечивают ошибочные ситуации, например, когда геттер, которому надлежит быть сгенерированным, уже присутствует в классе:



Я перечислил основные фишки Lombok, однако это ещё не всё. Более подробно все возможные аннотации со всеми атрибутами и со сравнением кода «до» и «после» описаны в документации.
Сергей Петунин @forketyfork
карма
30,7
рейтинг 12,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (38)

  • +2
    Смотрел в свое время Lombok, но ощущение как от каких-то подпорок. Сейчас использую более натуральный Eclipse Xtend.
    • +1
      Тоже вариант. Вопрос выбора заключается лишь в необходимой функциональности и плате за неё. Мы обнаружили, что для наших целей Lombok убивает именно то, что нужно, и не требует смены парадигмы, стиля кодирования или привязки к инструментам. Переход на альтернативный JVM-язык — тоже решение проблемы, но это достаточно серьёзный шаг. Особенно если этот язык привязан к конкретной среде разработки, что в нашем случае недопустимо.
    • +1
      он как groovy? Может быть внутри джава классов? Или нет?
      • 0
        Xtend — это код-генератор. То есть на выходе он генерит обычные .java файлы. Кроме того в рантайме не требует никаких лишних библиотек. Интегрирован в Eclipse (syntax highlighting, подсказки, code-completion, debug), но не зависит от него.
  • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    Для тех, кто не может себе позвонить использовать scala, kotlin, xtend и пр. это наверное вариант. Ну и вообще интересно, спасибо.
    • +1
      На Scala сейчас активно смотрим, но ввиду его сложности и необходимости менять парадигму пока остерегаемся применять где-либо, кроме тестов и служебного кода. Kotlin ещё явно не готов к продакшну. А с переходом на Xtend пришлось бы отказывать разработчикам в свободном выборе IDE и пересадить всех на Eclipse, чего мы пока что стараемся избегать.
      • 0
        Мы уже несколько лет успешно используем Scala, пока жалеть не приходилось. Сложность и парадигму можно перенимать постепенно. Самые большие трудности это:
        1. Выработать единый стиль и понимание внутри команды (у нас маленькая команда, так что это не проблема)
        2. Неидеальная поддержка IDE (но имхо вполне юзабельная, мы используем IntelliJ IDEA)
        3. Сложнее найти людей (но толковый java программер довольно быстро начинает неплохо писать на scala)
  • 0
    Переход на groovy++ может быть вариантом.
    Он со статическими типами как Ява, он позволяет всякие такие штуки, syntactic sugar, как груви, и вообще на груви легче переходить чем на скалу — у него синтакс максимально близок к Яве.
    • 0
      Спасибо за наводку. Вообще, от Groovy в своё время остались не очень хорошие впечатления в плане производительности, требований к памяти и скорости разработки. Возможно, Groovy++ может решить часть этих проблем, хотя пока что он производит несколько «сырое» впечатление.
      • 0
        Насчет производительности не знаю, у нас на груви не основной код, но в плане скорости разработки — мне очень нравится. Для меня основною плюс груви по сравнению со скалой — скорость перехода на него. Если не помнишь как писать что то в groovy-way, пишешь как писал бы на Яве и это будет вполне законный груви код.
        А groovy++ — да, сыроват. Но можно писать и на обычном груви, просто не пользоваться def вместо имён классов, ну и покрывать всё юнит тестами :)
      • 0
        Скоро будет groovy 2.0 со статической типизацией. Я вот тогда его и буду пробовать :) Новость от вчера последняя бета
  • 0
    Шикарно! Спасибо! Наконец-то не надо писать геттеры-сеттеры)
  • 0
    Интересно. Спасибо!

    А что Вы можете сказать по поводу производительности Lombook-а? влияет ли, или это просто кодогенератор?
    • 0
      Да, это препроцессор аннотаций, и он генерирует весь код, который в любом случае пришлось бы писать. Почти всю функциональность он реализует на этапе компиляции проекта. В рантайме библиотека lombok.jar не нужна, соответственно, никакого влияния на производительность она не оказывает.

      Кажется, единственной функцией, которая требует включения библиотеки в рантайм, является перехват исключений с помощью @SneakyThrows. Но к этой функции я отнёсся с осторожностью и пока решил не применять в продакшне. Хотя я не ожидаю каких-либо проблем с производительностью и в этом случае.
      • 0
        Отлично, спасибо! Пойду ковырять:)
  • +2
    Я сторонник прогресса, но:

    1. Не всегда геттеры сводятся к простому getProperty. Да, должны сводится, но не сводятся. И тогда надо отказываться от генерации и все равно их писать.

    2. Сгенерировать геттеры-сеттеры это одна комбинация горячих клавиш. Конечно, они занимают место, но есть code-folding. К hashCode() и equals() это тоже относится. Я к чему — проект не решает какой-то адской проблемы.

    3. Как любая прослойка, она будет добавлять досадные баги, вылавливание которых может запросто привести к потере сэкономленного времени.

    Вещь может и хорошая, но слишком много но.
    • +1
      1. Всё верно, но Lombok позволяет указать геттеры и сеттеры отдельно для каждого поля, а если в аксессоре требуется какая-то дополнительная функциональность — мы просто убираем соответствующую аннотацию с поля и пишем этот аксессор вручную.

      2. Всё так, но, как я сказал в самом начале статьи, наибольшие проблемы причиняет не генерация бойлерплейта, а его поддержание в актуальном состоянии. Например, добавили поле — нужно не забыть перегенерировать equals и hashCode, вставить его в конструктор, написать геттеры. Это всего лишь один из способов решения проблемы, которая, возможно, и не «адская», но, тем не менее, существует.

      3. Я бы не сказал, что это какая-либо прослойка. Это, скорее, кодогенератор. Конечно, любая дополнительная библиотека в проекте добавляет сложность. Но большой плюс Lombok заключается в том, что он не нужен в рантайме.
  • +1
    Попробовал пример:

    @Getter(lazy=true) private Map<String, String> map = initMap();


    Но при компиляции ошибка:

    .../test/LombokTest.java:[23,4] 'lazy' requires the field to be private and final.


    Сделал поле final, скомпилировал, потом декомпилировал jad-ом и не увидел геттеров.

    Интересно, как будет выглядеть код ленивой инициализации, сгенерированный lombok, учитывая что double checked locking является антипаттерном и в java работает только если поле volatile.
    • +1
      Похоже библиотека не совместима с aspectj-maven-plugin. Из-за этого плагина в классах отсутствовали сгенерированные геттеры. Отключил aspectj-maven-plugin, получил следующий код после декомпиляции:

          private final AtomicReference map = new AtomicReference();
      
          public Map getMap()
          {
              AtomicReference value = (AtomicReference)map.get();
              if(value == null)
                  synchronized(map)
                  {
                      value = (AtomicReference)map.get();
                      if(value == null)
                      {
                          Map actualValue = initMap();
                          value = new AtomicReference(actualValue);
                          map.set(value);
                      }
                  }
              return (Map)value.get();
          }
      

      • 0
        Да, благодарю, с отсутствием final — это была моя ошибка. Исправил.
        Разумеется, в многопоточности очень легко допустить трудноотлавливаемую ошибку. Но double-checked locking при грамотной реализации, на мой взгляд, является антипаттерном не в большей степени, чем вся система многопоточности с общей памятью. Иногда его приходится всё-таки использовать, и я бы вполне доверил его этой библиотеке, так как она учитывает общеизвестные питфоллы, как, например, упомянутый вами volatile.
  • 0
    Спасибо за статью! Нужно попробовать, будет не плохим дополнением к коду, который написан не на груви.

    P.S. А посмотреть сгенерированный код можно только с помощью декомпиляции?
    • 0
      В библиотеке присутсвует утилита delombok, которая позволяет преобразовать любой исходник к тому виду, в котором он будет скомпилирован. Подробно её работа описана здесь. Например, вот так можно преобразовать одиночный файл:
      java -jar lombok.jar delombok -p MyJavaFile.java
      

      • 0
        Спасибо за ответ! Быстрый гуглинг подсказал, что для этого также можно воспользоваться maven-lombok-plugin.
  • 0
    Что-то есть в этом неправильное — писать на джаве некорректный джава-код.
    • 0
      Да, возможно, вы правы. С другой стороны, когда в языке появляются стандартные средства расширения функциональности компилятора (препроцессинг аннотаций), понятие «корректного кода» несколько размывается.
  • 0
    А как работает логгер? Я добавил @Log4j, но после прохода delobok'ом код не изменился.
    • 0
      У меня вроде изменился. Я создал в качестве теста такой класс:
      import lombok.extern.log4j.Log4j;
      @Log4j
      public class Test {
          public static void main(String... args) {
              log.info("lol");
          }
      }
      

      Затем выполнил:
      java -jar lombok.jar delombok -p Test.java
      

      Утилита вывела в консоль следующее:
      error: package org.apache.log4j does not exist
      error: package org.apache.log4j does not exist
      // Generated by delombok at Thu Apr 19 20:27:33 YEKT 2012
      
      public class Test {
      	private static final org.apache.log4j.Logger log = org.apache.log4j.Logger.getLogger(Test.class);
      	
      	
      	public static void main(String... args) {
      		log.info("lol");
      	}
      }
      

      Ошибки связаны с отсутствием log4j в classpath, но если учесть это при запуске утилиты, то проблем быть не должно.
      • 0
        Проверил вручную, получил такой же код. Возможно, у delombok из состава maven-lombok-plugin есть какие-то особенности, но с помощью него упорно логгер не генерируется…
  • 0
    А как такие вещи во время дебага разворачиваются?
    • 0
      Никаких особых проблем во время дебага под IntelliJ IDEA я не обнаружил. Например, вызов сгенерированного аксессора корректно «прошагивается», хотя войти внутрь метода, конечно, не получится.
  • 0
    В видео класс был скомпилирован как «javac -cp lombok.jar Mountain.java» при этом появились getter'ы и setter'ы на уровне bytecode'а. Как сторонняя библиотека из classpath смогла повлиять на компилятор?
    • +2
      В Java 6 появился так называемый Pluggable Annotation Processing API.
      Он позволяет определять процессоры аннотаций, которые подключаются автоматически на этапе компиляции проекта. Достаточно включить процессор в classpath.
      Пример см. здесь.
      Точно так же, например, работает генератор метамодели в JPA.
  • 0
    Поясните пожалуйста, исходя из каких принципов ООП на каждое объявление поля приходится писать геттер и сеттер?
    • 0
      Приватная реализация, публичный интерфейс — это в самом общем виде принцип инкапсуляции. Для полей объекта в Java он реализуется с помощью аксессоров.
      • 0
        По-моему это неверное (хотя и практически общепринятое ныне) представление об инкапсуляции. Наличие большого количества геттеров-сеттеров в классе напротив скорее свидетельствует о том, что класс спроектирован не в соответствии с принципами ООП. Генерируя по сеттеру-геттеру на каждый (пусть даже закрытый) член — вы необоснованно раскрываете реализацию хранения данных классом и, по мере того как клиенты класса начинают связываются с этими сеттерами-геттерами, фиксируете её. То, что члены данных при этом остаются закрытыми, уже практически ничего не меняет.
        • 0
          Разумеется, использование геттеров и сеттеров должно быть разумным. Например, иногда более правильным будет генерировать аксессоры только на определённые поля. Или сгенерировать только геттеры, задавая значения полей в конструкторе. Или переписать геттеры для объектов так, чтобы они возвращали только immutable-значения. Всё это достаточно подробно описано у Блока в «Effective Java». В целом можно сказать, что аксессоры оставляют возможность для достаточно гибкого изменения реализации. Так что я не вижу в их использовании ничего принципиально неверного.
  • 0
    Хоть посту и месяц с небольшим, но сегодня я открыл для себя Lombok и он сделал остаток моего дня лучше!
    Дописываю проект на Struts2 и понял, на сколько мне надоело в каждом классе писать аннотации писать геттеры/сеттеры — теряется концентрация на идее. А сделать поле публичным религия, что называется, не позволяет.

    Вспомнил об этом замечательном проекте именно благодаря этой статье. За что автору «респект и уважуха»! :)

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.