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