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

в 20:58, , рубрики: boilerplate, java, synchronization, метки: , ,

Открою не Америку, но шкатулку Пандоры: в 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 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 плагин присутствует в стандартном репозитории:

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

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

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

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

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

Я перечислил основные фишки Lombok, однако это ещё не всё. Более подробно все возможные аннотации со всеми атрибутами и со сравнением кода «до» и «после» описаны в документации.

Автор: forketyfork

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js