Java – отличный язык. Однако порой приходится писать много шаблонного кода. Всякие геттеры и сеттеры для каждого поля, конструкторы, переопределение методов toString(), equals() или hashCode(), создание builder-ов для классов… А что, если я скажу, что можно проще?
Lombok – это проект, который позволяет при помощи аннотаций кратко и лаконично делать всё вышеперечисленное, и даже больше. Вы скажете – почему бы не воспользоваться генерацией кода средой разработки? Всё просто – проблема не только в написании шаблонного кода, но и в его чтении. Когда ваш класс на 80% забит шаблонным кодом, приходится каждый раз тратить силы на поиски нужных методов. При использовании Lombok вы видите только то, что действительно важно.
Библиотеку Lombok можно использовать не только в проектах Spring. Однако я подумала, что он отлично подходит для разбора в теме Spring Framework, так как делает сущности ещё более понятными и лаконичными. Объединяя Spring и Lombok, мы приходим к коду, где повторение однотипных конструкций сводится к минимуму.
Содержание:
Добавление зависимости
Для того, чтобы использовать библиотеку Lombok в своём проекте, нужно включить её в файл сборки Maven pom.xml с помощью следующего кода:
1 2 3 4 |
<dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> |
В целом, после нажатия на кнопку синхронизации плагина Maven, на этом можно закончить подключение зависимости. Однако мы имеем дело с библиотекой, которая завязана на аннотациях. Maven, как правило, автоматически сканирует classpath и успешно находит обработчики аннотаций. Однако мне больше нравится быть точно уверенной, что библиотеки смогут обрабатывать аннотации. Поэтому я указываю это явным образом. В коде ниже указано, что 2 библиотеки должны уметь обрабатывать аннотации: это Spring и Lombok. В будущем к ним добавится третья, MapStruct. Но это уже совсем другая история.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.1</version> <relativePath/> </parent> <groupId>com.myapp</groupId> <artifactId>backend</artifactId> <version>0.0.1-SNAPSHOT</version> <name>backend</name> <description>Demo project for Spring Boot</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <version>${parent.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build> </project> |
Возможности Lombok
После добавления зависимости можно начинать творить удивительные вещи. И эти вещи не будут загромождать код, что очень важно.
Геттеры и сеттеры
Если поставить аннотацию @Getter или @Setter перед полем – Lombok автоматически создаст для него геттер или сеттер соответственно. По умолчанию это публичные методы, однако вы можете изменить уровень доступа, задав параметр AccessLevel.
1 2 3 4 5 6 7 8 |
public class IceCream { @Getter @Setter private String name; @Getter(AccessLevel.PROTECTED) private int ballCount; } |
Аннотации @Getter и @Setter также можно применить ко всему классу целиком. В таком случае геттеры и сеттеры создадутся для всех его полей. Например, для поля с именем name будут созданы методы getName() и setName(). Однако, если поле имеет тип boolean (например, boolean ready), геттер будет называться isReady().
1 2 3 4 5 6 7 |
@Getter @Setter public class IceCream { private String name; private int ballCount; private boolean ready; } |
Теперь попробуем где-нибудь создать экземпляр класса IceCream. Поставив после имени объекта точку, видим, что среда разработки подсказывает 6 методов из этого класса. Всё это – геттеры и сеттеры, автоматически созданные библиотекой Lombok. И мы можем их использовать сразу, как пометили класс аннотациями @Getter и @Setter. Это круто. Но это не предел возможностей этой библиотеки.
Конструкторы
Аннотации @NoArgsConstructor, @RequiredArgsConstructor и @AllArgsConstructor в библиотеке Lombok предоставляют удобные способы автоматической генерации конструкторов в классах.
@NoArgsConstructor генерирует конструктор без аргументов (пустой конструктор). @RequiredArgsConstructor генерирует конструктор, принимающий аргументы для всех final и @NonNull полей класса. @AllArgsConstructor генерирует конструктор, принимающий аргументы для всех полей класса.
Попробуем использовать вместе аннотации @AllArgsConstructor и @NoArgsConstructor:
1 2 3 4 5 6 7 |
@AllArgsConstructor @NoArgsConstructor public class IceCream { private String name; private int ballCount; private boolean ready; } |
Благодаря двум аннотациям можно воспользоваться двумя конструкторами. Первый принимает 3 аргумента для 3 полей класса. Второй не принимает никаких аргументов.
При использовании @RequiredArgsConstructor в конструктор попадут только те поля, которые обязательно нужно заполнить значением в конструкторе. Например, final-поля:
1 2 3 4 5 6 |
@RequiredArgsConstructor public class IceCream { private final String name; private int ballCount; private boolean ready; } |
Только поле String name было помечено как final. Поэтому только это поле попало в конструктор, генерируемый аннотацией @RequiredArgsConstructor.
ToString
Чтобы не переопределять метод toString() вручную, вы можете пометить свой класс аннотацией @ToString. Оставлю аннотацию @AllArgsConstructor, чтобы продемонстрировать @ToString на примере созданных объектов. Вот как это работает:
1 2 3 4 5 6 7 |
@ToString @AllArgsConstructor public class IceCream { private String name; private int ballCount; private boolean ready; } |
1 2 3 4 5 6 |
public class Test { public static void main(String[] args) { IceCream iceCream = new IceCream("Мохито", 3, true); System.out.print(iceCream.toString()); } } |
Сгенерированный метод toString() включает в себя имена и значения всех полей класса. При выводе результата в консоль можно в этом убедиться (поля name и ballCount были отражены в полученной строке).
Однако не всегда нужно включать все поля в генерируемый toString(). Обозначить поле как исключаемое из генерации toString() можно так:
1 2 3 4 5 6 7 8 9 |
@ToString @AllArgsConstructor public class IceCream { private String name; private int ballCount; @ToString.Exclude private boolean ready; } |
А можно это сделать и вот так:
1 2 3 4 5 6 7 |
@ToString(exclude = {"ready"}) @AllArgsConstructor public class IceCream { private String name; private int ballCount; private boolean ready; } |
В обоих случаях результат один. Строка, создаваемая методом toString(), будет включать все поля, кроме ready. Но как именно прописывать это в коде – на ваше усмотрение.
Equals и HashCode
Lombok автоматически создаст за вас методы equals() и hashCode(). Просто пометьте ваш класс аннотацией @EqualsAndHashCode.
1 2 3 4 5 6 |
@EqualsAndHashCode public class IceCream { private String name; private int ballCount; private boolean ready; } |
Если же вы хотите исключить какие-либо поля из сравнения и расчёта хеша, вы можете поступить так же, как с аннотацией @ToString. Можно сделать вот так:
1 2 3 4 5 6 7 8 |
@EqualsAndHashCode public class IceCream { private String name; private int ballCount; @EqualsAndHashCode.Exclude private boolean ready; } |
Или вот так:
1 2 3 4 5 6 |
@EqualsAndHashCode(exclude = {"ready"}) public class IceCream { private String name; private int ballCount; private boolean ready; } |
Data
Аннотация @Data – это очень неплохой ход. Ведь она заменяет собой совокупность аннотаций @Getter, @Setter, @ToString, @EqualsAndHashCode и @RequiredArgsConstructor. Они часто пригождаются вместе, и потому вам не придётся писать над каждым классом по 5 аннотаций. Достаточно просто написать @Data.
Следующие 2 примера кода идентичны по своему действию.
1 2 3 4 5 6 |
@Data public class IceCream { private String name; private int ballCount; private boolean ready; } |
1 2 3 4 5 6 7 8 9 10 |
@Getter @Setter @ToString @EqualsAndHashCode @RequiredArgsConstructor public class IceCream { private String name; private int ballCount; private boolean ready; } |
Builder
Очень крутая аннотация @Builder. Она позволяет генерировать класс-строитель внутри вашего класса для удобного создания объектов при большом количестве полей.
1 2 3 4 5 6 |
@Builder public class IceCream { private String name; private int ballCount; private boolean ready; } |
Теперь для создания нового объекта класса IceCream нужно обратиться к самому классу IceCream и получить класс-строитель при помощи статического метода builder(). После этого можно заполнять поля друг за другом. А в конце нужно собрать объект IceCream, вызвав метод build().
Этих аннотаций хватает с головой, чтобы убирать 90% типового кода. Если вам интересно ещё больше – милости прошу в официальную документацию Lombok.
Что под капотом
Интересно же посмотреть, что там нагенерировал Lombok? Именно этим мы сейчас и займёмся. Смотреть будем вот на этот класс:
1 2 3 4 5 6 7 |
@Data @Builder public class IceCream { private String name; private int ballCount; private boolean ready; } |
Чтобы увидеть своими глазами этот код, нужно собрать проект (Build -> Build Project). После сборки можно тихонечко заглянуть в дерево проекта. Найдём там папку target. В ней хранятся сгенерированные классы и другие прикольные штучки. Из папки classes перейдём по разным другим папкам и доберёмся наконец до заветного класса IceCream. Это то, что нужно. Теперь будем пристально рассматривать код, который создал Lombok по двум аннотациям – @Data и @Builder.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
public class IceCream { private String name; private int ballCount; private boolean ready; IceCream(final String name, final int ballCount, final boolean ready) { this.name = name; this.ballCount = ballCount; this.ready = ready; } public static IceCreamBuilder builder() { return new IceCreamBuilder(); } public String getName() { return this.name; } public int getBallCount() { return this.ballCount; } public boolean isReady() { return this.ready; } public void setName(final String name) { this.name = name; } public void setBallCount(final int ballCount) { this.ballCount = ballCount; } public void setReady(final boolean ready) { this.ready = ready; } public boolean equals(final Object o) { if (o == this) { return true; } else if (!(o instanceof IceCream)) { return false; } else { IceCream other = (IceCream)o; if (!other.canEqual(this)) { return false; } else if (this.getBallCount() != other.getBallCount()) { return false; } else if (this.isReady() != other.isReady()) { return false; } else { Object this$name = this.getName(); Object other$name = other.getName(); if (this$name == null) { if (other$name == null) { return true; } } else if (this$name.equals(other$name)) { return true; } return false; } } } protected boolean canEqual(final Object other) { return other instanceof IceCream; } public int hashCode() { int PRIME = true; int result = 1; result = result * 59 + this.getBallCount(); result = result * 59 + (this.isReady() ? 79 : 97); Object $name = this.getName(); result = result * 59 + ($name == null ? 43 : $name.hashCode()); return result; } public String toString() { String var10000 = this.getName(); return "IceCream(name=" + var10000 + ", ballCount=" + this.getBallCount() + ", ready=" + this.isReady() + ")"; } public static class IceCreamBuilder { private String name; private int ballCount; private boolean ready; IceCreamBuilder() { } public IceCreamBuilder name(final String name) { this.name = name; return this; } public IceCreamBuilder ballCount(final int ballCount) { this.ballCount = ballCount; return this; } public IceCreamBuilder ready(final boolean ready) { this.ready = ready; return this; } public IceCream build() { return new IceCream(this.name, this.ballCount, this.ready); } public String toString() { return "IceCream.IceCreamBuilder(name=" + this.name + ", ballCount=" + this.ballCount + ", ready=" + this.ready + ")"; } } } |
Да, какие-то непонятные доллары $ в названиях переменных и var10000 в toString(). Вообще не содержательные имена. А также большие уровни вложенности в equals(). Однако Lombok всё сделал по-честному.
Что ж, при использовании утилиты Lombok объём кода сильно сокращается, а читаемость – повышается. Это ли не чудо? И, конечно же, время, которое тратит разработчик на прописывание шаблонного кода, можно направить в другое русло. Например, на обдумывание логичной структуры программы в парадигме ООП.
Занимательный факт: Lombok – это тоже остров, как и Java, и находятся они совсем недалеко друг от друга.