Пишите обезжиренный Java-код с помощью Project Lombok
Опубликовано: 2022-03-11Есть ряд инструментов и библиотек, без которых я не могу представить себя в написании Java-кода в эти дни. Традиционно такие вещи, как Google Guava или Joda Time (по крайней мере, для эпохи до Java 8), входят в число зависимостей, которые я в конечном итоге добавляю в свои проекты большую часть времени, независимо от конкретного домена под рукой.
Lombok, безусловно, заслуживает своего места в моих сборках POM или Gradle, хотя и не является типичной утилитой библиотеки/фреймворка. Lombok существует уже довольно давно (впервые выпущен в 2009 году) и с тех пор сильно повзрослел. Однако я всегда чувствовал, что он заслуживает большего внимания — это удивительный способ справиться с естественной многословностью Java.
В этом посте мы рассмотрим, что делает Lombok таким удобным инструментом.
В Java есть много вещей, помимо самой JVM, которая является замечательным программным обеспечением. Java является зрелой и производительной, а сообщество и экосистема вокруг нее огромны и активны.
Однако, как язык программирования, Java имеет некоторые свои особенности, а также варианты дизайна, которые могут сделать его довольно многословным. Добавьте некоторые конструкции и шаблоны классов, которые нам часто приходится использовать разработчикам Java, и мы часто получаем множество строк кода, которые практически не приносят реальной пользы, кроме соблюдения некоторого набора ограничений или соглашений фреймворка.
Вот где Ломбок вступает в игру. Это позволяет нам значительно сократить объем «шаблонного» кода, который нам нужно написать. Создатели Lombok — пара очень умных парней, и у них определенно есть вкус к юмору — вы не можете пропустить это вступление, которое они сделали на прошлой конференции!
Давайте посмотрим, как Lombok творит чудеса, и несколько примеров использования.
Как работает Ломбок
Lombok действует как обработчик аннотаций, который «добавляет» код к вашим классам во время компиляции. Обработка аннотаций — это функция, добавленная в компилятор Java в версии 5. Идея состоит в том, что пользователи могут помещать обработчики аннотаций (написанные самостоятельно или через сторонние зависимости, такие как Lombok) в путь к классам сборки. Затем, когда процесс компиляции продолжается, всякий раз, когда компилятор находит аннотацию, он как бы спрашивает: «Эй, кого-нибудь в пути к классам интересует эта @Annotation?». Для тех процессоров, которые поднимают руки, компилятор затем передает им управление вместе с контекстом компиляции, чтобы они, ну… обработали.
Возможно, наиболее распространенным случаем для обработчиков аннотаций является создание новых исходных файлов или выполнение каких-либо проверок во время компиляции.
Lombok на самом деле не попадает в эти категории: он изменяет структуры данных компилятора, используемые для представления кода; т. е. его абстрактное синтаксическое дерево (AST). Изменяя AST компилятора, Lombok косвенно изменяет саму генерацию окончательного байт-кода.
Этот необычный и довольно навязчивый подход традиционно приводил к тому, что Ломбок считался чем-то вроде взлома. Хотя я сам в какой-то степени согласен с этой характеристикой, вместо того, чтобы рассматривать ее в плохом смысле этого слова, я бы рассматривал Ломбок как «умную, технически достойную и оригинальную альтернативу».
Тем не менее, есть разработчики, которые считают это хаком и не используют Ломбок по этой причине. Это понятно, но, по моему опыту, преимущества Ломбока в плане производительности перевешивают все эти опасения. Я с удовольствием использую его для производственных проектов уже много лет.
Прежде чем вдаваться в подробности, я хотел бы обобщить две причины, по которым я особенно ценю использование Lombok в своих проектах:
- Lombok помогает сделать мой код чистым, лаконичным и точным. Я считаю, что мои аннотированные классы Lombok очень выразительны, и я обычно нахожу аннотированный код довольно показательным, хотя не все в Интернете обязательно с этим согласны.
- Когда я начинаю проект и думаю о модели предметной области, я, как правило, начинаю с написания классов, которые в значительной степени находятся в стадии разработки, и которые я итеративно изменяю по мере того, как думаю дальше и совершенствую их. На этих ранних этапах Lombok помогает мне двигаться быстрее, поскольку мне не нужно перемещаться или преобразовывать шаблонный код, который он генерирует для меня.
Шаблон компонента и общие методы объекта
Многие инструменты и среды Java, которые мы используем, основаны на Bean Pattern. Java Beans — это сериализуемые классы, которые имеют конструктор без аргументов по умолчанию (и, возможно, другие версии) и раскрывают свое состояние через геттеры и сеттеры, обычно поддерживаемые закрытыми полями. Мы пишем их много, например, при работе с JPA или средами сериализации, такими как JAXB или Jackson.
Рассмотрим этот пользовательский компонент, который содержит до пяти атрибутов (свойств), для которого мы хотели бы иметь дополнительный конструктор для всех атрибутов, осмысленное строковое представление и определить равенство/хеширование с точки зрения его поля электронной почты:
public class User implements Serializable { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; // Empty constructor implementation: ~3 lines. // Utility constructor for all attributes: ~7 lines. // Getters/setters: ~38 lines. // equals() and hashCode() as per email: ~23 lines. // toString() for all attributes: ~3 lines. // Relevant: 5 lines; Boilerplate: 74 lines => 93% meaningless code :( }
Для краткости, вместо того, чтобы включать фактическую реализацию всех методов, я просто предоставил комментарии со списком методов и количеством строк кода, которые заняли фактические реализации. Этот шаблонный код составил бы более 90% кода для этого класса!
Более того, если бы я позже захотел, скажем, изменить email
на адрес электронной emailAddress
или сделать registrationTs
Date
, а не Instant
, тогда мне нужно было бы посвятить время (по общему признанию, с помощью моей IDE в некоторых случаях), чтобы изменить такие вещи, как получение /set имена и типы методов, изменить мой конструктор утилит и так далее. Опять же, бесценное время для чего-то, что не приносит практической ценности моему коду.
Давайте посмотрим, как здесь может помочь Ломбок:
import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @Getter @Setter @NoArgsConstructor @AllArgsConstructor @ToString @EqualsAndHashCode(of = {"email"}) public class User { private String email; private String firstName; private String lastName; private Instant registrationTs; private boolean payingCustomer; }
Вуаля! Я просто добавил кучу аннотаций lombok.*
и получил именно то, что хотел. В приведенном выше листинге представлен весь код, который мне нужно написать для этого. Lombok подключается к процессу моего компилятора и сгенерировал все для меня (см. снимок экрана ниже с моей IDE).
Как вы заметили, инспектор NetBeans (и это произойдет независимо от IDE) действительно обнаруживает скомпилированный байт-код класса, включая дополнения, внесенные Lombok в процесс. То, что здесь произошло, довольно просто:
- Используя
@Getter
и@Setter
я поручил Ломбоку сгенерировать геттеры и сеттеры для всех атрибутов. Это потому, что я использовал аннотации на уровне класса. Если бы я хотел выборочно указать, что генерировать для каких атрибутов, я мог бы аннотировать сами поля. - Благодаря
@NoArgsConstructor
и@AllArgsConstructor
я получил пустой конструктор по умолчанию для своего класса, а также дополнительный для всех атрибутов. - Аннотация
@ToString
автоматически генерирует удобный методtoString()
, по умолчанию отображающий все атрибуты класса с префиксом их имени. - Наконец, чтобы определить пару методов
equals()
иhashCode()
с точки зрения поля электронной почты, я использовал@EqualsAndHashCode
и параметризовал его списком соответствующих полей (в данном случае только адрес электронной почты).
Настройка аннотаций Ломбока
Давайте теперь воспользуемся некоторыми настройками Lombok, следуя тому же примеру:
- Я хотел бы уменьшить видимость конструктора по умолчанию. Поскольку мне это нужно только из соображений совместимости компонентов, я ожидаю, что потребители класса будут вызывать только конструктор, который принимает все поля. Чтобы обеспечить это, я настраиваю сгенерированный конструктор с помощью
AccessLevel.PACKAGE
. - Я хочу убедиться, что моим полям никогда не будут присвоены нулевые значения ни через конструктор, ни через методы установки. Достаточно аннотировать атрибуты класса с помощью
@NonNull
; Lombok будет генерировать нулевые проверки,NullPointerException
, когда это уместно в методах конструктора и установщика. - Я добавлю атрибут
password
, но не хочу, чтобы он отображался при вызовеtoString()
по соображениям безопасности. Это достигается с помощью аргумента@ToString
. - Я согласен публично раскрывать состояние через геттеры, но предпочел бы ограничить внешнюю изменчивость. Поэтому я оставляю
@Getter
как есть, но снова используюAccessLevel.PROTECTED
для@Setter
. - Возможно, я хотел бы наложить какое-то ограничение на поле
email
, чтобы в случае его изменения выполнялась какая-то проверка. Для этого я просто сам реализую методsetEmail()
. Lombok просто пропустит генерацию уже существующего метода.
Вот как тогда будет выглядеть класс User:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName; private @NonNull Instant registrationTs; private boolean payingCustomer; protected void setEmail(String email) { // Check for null (=> NullPointerException) // and valid email code (=> IllegalArgumentException) this.email = email; } }
Обратите внимание, что для некоторых аннотаций мы указываем атрибуты класса в виде простых строк. Не проблема, потому что Lombok выдаст ошибку компиляции, если мы, например, опечатаемся или сошлемся на несуществующее поле. С Ломбоком мы в безопасности.
Кроме того, как и в случае с методом setEmail()
, Lombok просто будет в порядке и ничего не сгенерирует для метода, который уже реализован программистом. Это относится ко всем методам и конструкторам.
Неизменяемые структуры данных
Еще один вариант использования Lombok — создание неизменяемых структур данных. Их обычно называют «типами значений». Некоторые языки имеют встроенную поддержку для них, и есть даже предложение включить это в будущие версии Java.
Предположим, мы хотим смоделировать реакцию пользователя на действие входа в систему. Это тип объекта, который мы хотели бы просто создать и вернуть на другие уровни приложения (например, для сериализации JSON в качестве тела ответа HTTP). Такой LoginResponse вообще не должен быть изменчивым, и Ломбок может кратко описать это. Конечно, есть много других вариантов использования неизменяемых структур данных (среди прочих качеств они многопоточны и удобны для кэширования), но давайте остановимся на этом простом примере:
import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.ToString; import lombok.experimental.Wither; @Getter @RequiredArgsConstructor @ToString @EqualsAndHashCode public final class LoginResponse { private final long userId; private final @NonNull String authToken; private final @NonNull Instant loginTs; @Wither private final @NonNull Instant tokenExpiryTs; }
Здесь стоит отметить:

- Добавлена аннотация
@RequiredArgsConstructor
. Правильно названный, он генерирует конструктор для всех конечных полей, которые еще не были инициализированы. - В тех случаях, когда мы хотим повторно использовать ранее выданный LoginResonse (представьте себе, например, операцию «обновить токен»), мы, конечно, не хотим модифицировать наш существующий экземпляр, а скорее хотим сгенерировать новый на его основе. . Посмотрите, как здесь нам помогает аннотация
@Wither
: она говорит Lombok сгенерироватьwithTokenExpiryTs(Instant tokenExpiryTs)
, который создает новый экземпляр LoginResponse, имеющий все значения экземпляра with, кроме нового, который мы указываем. Хотели бы вы такое поведение для всех полей? Вместо этого просто добавьте@Wither
в объявление класса.
@Данные и @Значение
Оба варианта использования, обсуждавшиеся до сих пор, настолько распространены, что Lombok поставляет пару аннотаций, чтобы сделать их еще короче: Аннотирование класса с помощью @Data
приведет к тому, что Lombok будет вести себя так же, как если бы он был аннотирован с @Getter
+ @Setter
+ @ToString
+ @EqualsAndHashCode
+ @RequiredArgsConstructor
. Точно так же использование @Value
превратит ваш класс в неизменяемый (и окончательный), опять же, как если бы он был аннотирован списком выше.
Шаблон строителя
Возвращаясь к нашему примеру с пользователем, если мы хотим создать новый экземпляр, нам нужно будет использовать конструктор с шестью аргументами. Это уже довольно большое число, которое станет еще хуже, если мы дополнительно добавим в класс атрибуты. Также предположим, что мы хотим установить некоторые значения по умолчанию для полей lastName
и payingCustomer
.
Lombok реализует очень мощную функцию @Builder
, позволяющую нам использовать шаблон Builder для создания новых экземпляров. Давайте добавим его в наш класс User:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"email"}) @Builder public class User { private @NonNull String email; private @NonNull byte[] password; private @NonNull String firstName; private @NonNull String lastName = ""; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
Теперь мы можем свободно создавать новых пользователей следующим образом:
User user = User .builder() .email("[email protected]") .password("secret".getBytes(StandardCharsets.UTF_8)) .firstName("Miguel") .registrationTs(Instant.now()) .build();
Легко представить, насколько удобной становится эта конструкция по мере роста наших классов.
Делегация/состав
Если вы хотите следовать очень разумному правилу «предпочитать композицию наследованию», с этим Java не очень поможет, если говорить о многословии. Если вы хотите создавать объекты, вам, как правило, нужно везде писать делегирующие вызовы методов.
Ломбок предлагает решение для этого через @Delegate
. Давайте посмотрим на пример.
Представьте, что мы хотим представить новую концепцию ContactInformation
. Это некоторая информация, которую имеет наш User
, и мы можем захотеть, чтобы другие классы тоже имели ее. Затем мы можем смоделировать это через такой интерфейс:
public interface HasContactInformation { String getEmail(); String getFirstName(); String getLastName(); }
Затем мы введем новый класс ContactInformation
, используя Lombok:
import lombok.Data; @Data public class ContactInformation implements HasContactInformation { private String email; private String firstName; private String lastName; }
И, наконец, мы могли бы реорганизовать User
для компоновки с ContactInformation
и использовать Lombok для генерации всех необходимых вызовов делегирования в соответствии с контрактом интерфейса:
import lombok.AccessLevel; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.ToString; import lombok.experimental.Delegate; @Getter @Setter(AccessLevel.PROTECTED) @NoArgsConstructor(access = AccessLevel.PACKAGE) @AllArgsConstructor @ToString(exclude = {"password"}) @EqualsAndHashCode(of = {"contactInformation"}) public class User implements HasContactInformation { @Getter(AccessLevel.NONE) @Delegate(types = {HasContactInformation.class}) private final ContactInformation contactInformation = new ContactInformation(); private @NonNull byte[] password; private @NonNull Instant registrationTs; private boolean payingCustomer = false; }
Обратите внимание, что мне не нужно было писать реализации для методов HasContactInformation
: это то, что мы говорим Lombok делать, делегируя вызовы нашему экземпляру ContactInformation
.
Кроме того, поскольку я не хочу, чтобы делегированный экземпляр был доступен извне, я настраиваю его с помощью @Getter(AccessLevel.NONE)
, эффективно предотвращая создание для него геттера.
Проверенные исключения
Как мы все знаем, Java различает проверяемые и непроверяемые исключения. Это традиционный источник споров и критики языка, поскольку в результате обработка исключений иногда становится слишком сложной, особенно при работе с API, разработанными для создания проверенных исключений и, следовательно, вынуждающими нас, разработчиков, либо перехватывать их, либо объявлять наши методы для бросить их.
Рассмотрим этот пример:
public class UserService { public URL buildUsersApiUrl() { try { return new URL("https://apiserver.com/users"); } catch (MalformedURLException ex) { // Malformed? Really? throw new RuntimeException(ex); } } }
Это такой распространенный шаблон: мы, безусловно, знаем, что наш URL-адрес правильно сформирован, но, поскольку конструктор URL
-адреса выдает проверенное исключение, мы либо вынуждены его перехватывать, либо объявляем наш метод для его выбрасывания и вывода вызывающих объектов в той же ситуации. Оборачивать эти проверенные исключения в RuntimeException
— очень сложная практика. И это становится еще хуже, если количество проверенных исключений, с которыми нам нужно иметь дело, растет по мере написания кода.
Так что это именно то, для чего предназначен Lombok @SneakyThrows
, он перенесет любые проверенные исключения, подлежащие выбрасыванию в нашем методе, в непроверенные и избавит нас от хлопот:
import lombok.SneakyThrows; public class UserService { @SneakyThrows public URL buildUsersApiUrl() { return new URL("https://apiserver.com/users"); } }
логирование
Как часто вы таким образом добавляете экземпляры логгеров в свои классы? (образец SLF4J)
private static final Logger LOG = LoggerFactory.getLogger(UserService.class);
Я собираюсь угадать совсем немного. Зная это, создатели Lombok реализовали аннотацию, которая создает экземпляр регистратора с настраиваемым именем (по умолчанию — log), поддерживая наиболее распространенные среды ведения журналов на платформе Java. Вот так (опять же, на основе SLF4J):
import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @Slf4j public class UserService { @SneakyThrows public URL buildUsersApiUrl() { log.debug("Building users API URL"); return new URL("https://apiserver.com/users"); } }
Аннотирование сгенерированного кода
Если мы будем использовать Lombok для генерации кода, может показаться, что мы потеряем возможность аннотировать эти методы, поскольку на самом деле мы их не пишем. Но это не совсем так. Скорее, Lombok позволяет нам сказать, как мы хотим, чтобы сгенерированный код был аннотирован, хотя, по правде говоря, используя несколько своеобразную нотацию.
Рассмотрим этот пример, нацеленный на использование инфраструктуры внедрения зависимостей: у нас есть класс UserService
, который использует внедрение конструктора для получения ссылок на UserRepository
и UserApiClient
.
package com.mgl.toptal.lombok; import javax.inject.Inject; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor(onConstructor = @__(@Inject)) public class UserService { private final UserRepository userRepository; private final UserApiClient userApiClient; // Instead of: // // @Inject // public UserService(UserRepository userRepository, // UserApiClient userApiClient) { // this.userRepository = userRepository; // this.userApiClient = userApiClient; // } }
В примере выше показано, как аннотировать сгенерированный конструктор. Lombok позволяет нам делать то же самое для сгенерированных методов и параметров.
Узнать больше
Использование Lombok, описанное в этом посте, сосредоточено на тех функциях, которые я лично считаю наиболее полезными на протяжении многих лет. Однако доступно множество других функций и настроек.
Документация Lombok очень информативна и подробна. У них есть специальные страницы для каждой отдельной функции (аннотации) с очень подробными объяснениями и примерами. Если вы считаете этот пост интересным, я призываю вас глубже погрузиться в ломбок и его документацию, чтобы узнать больше.
На сайте проекта описано, как использовать Lombok в нескольких различных средах программирования. Короче говоря, поддерживаются самые популярные IDE (Eclipse, NetBeans и IntelliJ). Я сам регулярно переключаюсь с одного на другой для каждого проекта и безупречно использую Lombok для всех.
Деломбок!
Delombok является частью «инструментальной цепочки Lombok» и может пригодиться. Что он делает, так это генерирует исходный код Java для вашего аннотированного кода Lombok, выполняя те же операции, что и байт-код, сгенерированный Lombok.
Это отличный вариант для людей, которые рассматривают возможность использования Ломбока, но еще не совсем уверены. Вы можете свободно начать использовать его, и никакой «привязки к поставщику» не будет. Если вы или ваша команда позже пожалеете о своем выборе, вы всегда можете использовать delombok для создания соответствующего исходного кода, который затем можно использовать без какой-либо зависимости от Lombok.
Delombok также является отличным инструментом, чтобы узнать, что именно Lombok будет делать. Есть очень простые способы включить его в процесс сборки.
Альтернативы
В мире Java существует множество инструментов, которые аналогичным образом используют обработчики аннотаций для обогащения или изменения кода во время компиляции, например Immutables или Google Auto Value. Эти (и, конечно же, другие!) пересекаются с Lombok по функциям. Мне особенно нравится подход Immutables, и я также использовал его в некоторых проектах.
Также стоит отметить, что есть и другие замечательные инструменты, предоставляющие аналогичные функции для «расширения байт-кода», такие как Byte Buddy или Javassist. Однако они обычно работают во время выполнения и представляют собой отдельный мир, выходящий за рамки этого поста.
Лаконичная Java
Существует ряд современных языков, ориентированных на JVM, которые обеспечивают более идиоматические подходы к проектированию или даже на уровне языка, помогающие решать некоторые из тех же проблем. Конечно, хорошие примеры — Groovy, Scala и Kotlin. Но если вы работаете над проектом только для Java, то Lombok — хороший инструмент, который поможет вашим программам быть более краткими, выразительными и удобными в сопровождении.