Открою не Америку, но шкатулку Пандоры: в Java-коде много бойлерплейта. Типовые геттеры, сеттеры и конструкторы, методы ленивой инициализации, методы toString, hashCode, equals, обработчики исключений, которые никогда не выбрасываются, закрывалки потоков, блоки синхронизации. Проблема заключается даже не в том, чтобы написать всё это — современные среды разработки справляются с такими задачами нажатием нескольких клавиш. Сложность в поддержании бойлерплейта в актуальном состоянии по мере внесения модификаций в код. А в некоторых случаях (многопоточность, реализация методов hashCode и equals) и сам шаблонный код написать без ошибок — далеко не простая задача. Одним из решений проблемы является генерация кода, и в этой статье я расскажу про проект Lombok — библиотеку, которая не только может избавить вас от бойлерплейта, но и сделать это максимально прозрачно, с минимальной конфигурацией и, что немаловажно, с поддержкой на уровне среды разработки.
Lombok использует механизм процессинга аннотаций из Java 6, из чего следуют его минимальные требования к окружению. Для подключения Lombok к проекту достаточно включить его в зависимости. В случае использования Maven это делается следующим образом:
Для большей части функциональности Lombok эта библиотека необходима только на этапе компиляции. Последняя версия Lombok на текущий момент (0.11.0) ещё не попала в центральный репозиторий Maven, однако её без проблем можно установить в локальный или корпоративный репозиторий, скачав с сайта.
Одним из главных источников бойлерплейта в Java является отсутствие свойств на уровне языка. Во соблюдение принципов ООП на каждое объявление поля приходится писать как минимум шесть типовых строк — геттер и сеттер. Некоторые библиотеки, вроде Spring или Tapestry, для своих целей в некоторых случаях позволяют разработчику забыть про аксессоры, вставляя их самостоятельно в байт-код. Подобную функциональность предлагает и Lombok.
Параметр lazy=true аннотации Getter позволяет реализовать ленивую инициализацию поля: вызов метода initMap() в данном случае будет отложен до первого вызова геттера и обёрнут в потокобезопасную инициализацию в виде блокировки с двойной проверкой.
Конструкторы POJO-классов тоже не отличаются сложностью и разнообразием — чаще всего нам необходимо что-либо из этого списка: конструктор без параметров, конструктор со всеми параметрами, конструктор только с некоторыми обязательными параметрами, статический factory-метод. Lombok легко справляется с этой задачей с помощью аннотаций @NoArgsConstructor, @AllArgsConstructor, @RequiredArgsConstructor и параметра staticName соответственно.
Вот что мы получим в результате:
О правильной реализации методов equals и hashCode написано достаточно много — пожалуй, стоит по этому поводу вспомнить «Effective Java» Блока и статью Одерски. Вкратце можно сказать, что реализовать их корректно — непросто, поддерживать в актуальном состоянии — ещё сложнее, а занимать они вполне могут добрую половину класса. Метод toString не так критичен для корректности кода, но актуализировать его каждый раз при изменении класса — тоже приятного мало. Предоставим возможность Lombok сделать за нас эту неблагодарную работу с помощью двух нехитрых аннотаций:
Если вы пользуетесь одной из популярных библиотек протоколирования, то, вероятнее всего, в каждом классе у вас присутствует статическое объявление логгера:
Вместо этого Lombok предлагает воспользоваться аннотациями Log, @CommonsLog, @Log4j или @Slf4j — в зависимости от предпочитаемого средства протоколирования:
Хорошим стилем программирования является использование финальных локальных переменных, однако, учитывая статическую типизацию и отсутствие в Java выведения типов, объявление какой-нибудь особенно навороченной карты вполне может вылезть за пределы экрана. Если с Java переходить на Scala или Groovy пока не хочется, то можно воспользоваться следующим хаком Lombok:
Переменная map в данном случае будет объявлена как final, в то же время описание её типа будет взято из правой части выражения присваивания.
Далеко не все разработчики, к сожалению, читали уже упоминавшуюся здесь «Effective Java» Блока или «Robust Java» Стелтинга. А может, читали, но не очень внимательно. Или, возможно, у них и впрямь была какая-то вполне обоснованная мотивация объявить вот это конкретное исключение проверяемым — но вам от этого не легче, ведь вы-то знаете, что оно не возникнет никогда! Что делать, например, с UnsupportedEncodingException, если вы абсолютно уверены, что без системной поддержки кодировки UTF-8 ваше приложение всё равно работать не будет? Приходится заключать код в try-catch и писать бессмысленный вывод в лог, который никогда не дождётся своего часа, переоборачивать исключение в runtime-обёртку, которой не суждено появиться на свет, или и вовсе игнорировать пустым блоком перехвата (который, не знаю, как у вас, а у меня лично всегда вызывает желание схватиться за револьвер). Lombok и здесь предлагает альтернативу.
Для этого колдунства, в отличие от всего прочего, понадобится подключить Lombok в рантайме. Всё просто: Lombok усыпляет бдительность компилятора, перехватывая исключение в try, а затем в рантайме незаметно перевыбрасывает его в catch. Фишка в том, что на уровне байт-кода можно выбросить любое исключение, даже то, которое не объявлено в сигнатуре метода.
Многопоточность — весьма сложная область программирования, имеющая в Java свою идиоматику и паттерны. Одной из правильных практик является использование для синхронизации приватных финальных полей, так как на любом общедоступном локе может пожелать синхронизироваться какой-либо не связанный кусок функциональности, что приведёт к ненужным блокировкам и трудно отлавливаемым ошибкам. Lombok умеет корректно синхронизировать содержимое метода, отмеченного аннотацией @Synchronized — как статического, так и метода экземпляра:
Вот что получим:
Все эти замечательные фишки ничего бы не стоили, если бы при открытии проекта в Eclipse или IntelliJ IDEA строчки кода разгорались бы красным пламенем от праведного гнева компилятора. К счастью, интеграция со средами разработки имеется, и достаточно неплохая. Для IntelliJ IDEA плагин присутствует в стандартном репозитории:

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

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

Я перечислил основные фишки 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, однако это ещё не всё. Более подробно все возможные аннотации со всеми атрибутами и со сравнением кода «до» и «после» описаны в документации.
