Część kodu który jest w naszym projekcie możemy uznać jako „niby nic nie robi, a jest potrzebny”. objętość tego kodu (hashCode, equals, toString, loggery, gettery, settery) jesteśmy w stanie zminimalizować z zastosowaniem Lombok. Po zapoznaniu się z tym wpisem już nigdy nic nie będzie takie same w Twoim projekcie jak do tej pory…
Lombok to stosunkowo niewielka biblioteka, rozwijana na zasadach wolnej licencji (MIT license). Biblioteka udostępnia zbiór adnotacji pozwalających na automatyczne generowanie kodu, w celu ograniczenia kłopotliwego boilerplate code. Funkcjonalności udostępnione są z wykorzystaniem adnotacji na poziomie klas i metod, pełna ich lista dostępna jest na stronie projektu. Lombok po prostu ‘dopisuje’ kod do aplikacji w czasie jej kompilacji. Dzięki temu nie tracimy na wydajności, a jednocześnie honoruje on wszystkie ‘nadpisane’ przez nas metody, dzięki czemu integruje się w sposób nieinwazyjny.
Boilerplate
W największym skrócie oznacza to kod, który nie realizuje żadnej funkcji biznesowej. Jego funkcją zaś jest połączenie elementów w całość, udostępnieniu danych, logowaniu, informacji itp – jest niezbędny, ale powtarzalny, nieciekawy do pisania i zaśmiecający kod aplikacji. Określić tym terminem możemy wszystkie metody takie jak gettery, settery, toString, hashCode oraz equals, również deklaracje loggerów i podobne…Świetną sprawą jest to, że IDE pozwala nam generować ten kod automatycznie, pomimo tego nadal musimy o nim pamiętać
Instalacja
Aby skorzystać z możliwości Lomboka, musimy dołączyć go do naszego projektu – jeśli używany Mavena, dodajemy zależność do pom.xml
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.12</version> <scope>provided</scope> </dependency> </dependencies>
Pozostaje jeszcze jeden krok – musimy „powiedzieć” naszemu ide aby widział metody generowane przez Lombok. W przypadku IntelliJ dostępny jest plugin, który możemy zainstalować w standardowy sposób z poziomu IntelliJ.
Adnotacje
Lombok oferuje funkcjonalność za pośrednictwem adnotacji — zarówno na poziomie klas jak i metod. Poniżej zbiór najważniejszych, pełną dokumentację możesz znaleźć na stronie projektu.
@Getter / @Setter
Adnotacja ta pozwala na dodanie getterów lub setterów dla wybranych pól. Adnotacja umieszczona nad klasą spowoduje dodanie metod dla wszystkich pól tej klasy. Przykładowe zastosowanie:
@Getter @Setter public class User { private String name; @Setter(AccessLevel.PROTECTED) private int age; }
- publiczne gettery dla obu pól
- chroniony (protected) setter dla pola age
- publiczny dla pola name.
@EqualsAndHashCode
Generuje jednocześnie metody equals i hashCode spełniając kontrakt między nimi. Domyślnie brane są pod uwagę wszystkie pola, które nie są oznaczone jako statyczne i transient. Korzystając z opcjonalnej konfiguracji można wykluczyć niektóre pola lub zdefiniować, czy pola z klasy rodzica również mają być brane pod uwagę.
@EqualsAndHashCode(callSuper = false, exclude = {}) public class UserEqualsAndHashCode { private String name; private Integer age; }
@ToString
Generuje metodę toString() z wszystkich pól, które nie są oznaczone jako statyczne lub transient.
Tu również można dokonfigurować zachowanie przez parametry adnotacji: exclude i callSuper.
Domyślnie wyświetlany format to pary: name:value,
UserToString(name=null, age=0)
można jednak wyłączyć nazwy pól przez parametr includeFieldNames.
@RequiredArgsConstructor
Ta adnotacja wygeneruje konstruktor z wszystkimi wymaganymi polami. W poniższym przykładzie będzie to tylko pole name.
@RequiredArgsConstructor public class UserRequiredArgsConstructor { private final String name; private int age; }
@Data
@Data to najbardziej popularna adnotacja, łączy w sobie kilka innych: @Getter, @Setter, @EqualsAndHashCode, @RequiredArgsConstructor oraz @ToString, dlatego poniższe przykłady generują równoważny kod.
@Data public class UserLombokData { private final String name; private Integer age; }
@Getter @Setter @EqualsAndHashCode @RequiredArgsConstructor @ToString public class UserDataEquivalent { private final String name; private int age; }
val
Val to nowa konstrukcja dla programistów Javy, bardziej jest znana np w Scali. Dzięki niej nie trzeba deklarować typu zmiennej, a kompilator sam go wywnioskuje na podstawie przypisanej do niej wartości. Ponieważ ten mechanizm działa już podczas kompilacji, wymagane jest zainicjowanie zmiennej przy jej deklaracji.
To rozwiązanie ma również swoje ograniczenia. Działa tylko dla zmiennych lokalnych oraz w pętlach foreach. Występują również kłopoty z podpowiadaniem składni dla takich zmiennych w IDE.
@Test public void localVariableTest() { val list = Arrays.asList("item"); String listElement = list.get(0); Assert.assertEquals("item", listElement); } @Test public void foreachTest() { val list = new ArrayList<>(); for (val item : Arrays.asList("item1", "item2")) { list.add(item); } Assert.assertEquals(Arrays.asList("item1", "item2"), list); }
@Cleanup
Celem tej adnotacji jest automatyczna obsługa zamykania zasobów. Od momentu wprowadzenia w Javie try with resources straciła na znaczeniu.
@Value
@Value to odpowiednik @Data dla niemodyfikowalnych obiektów
Dla tej adnotacji kompilator wygeneruje:
- metodę toString
- hashCode oraz equals
- konstruktor dla wszystkich niezainicjowanych pól
- oznaczy pola jako final
- wygeneruje również gettery, ale BEZ setterów
@Value public class UserValue { private String name; private int age; }
@Builder
W celu implementacji wzorca projektowego builder wystarczy adnotować główną klasę adnotacją @Builder, a kompilator wygeneruje statyczną zagnieżdżoną klasę buildera oraz niezbędny konstruktor w głównej klasie. To podejście bardzo dobrze współgra z adnotacją @Value. Dzięki takiej kombinacji uzyskujemy w pełni funkcjonalny value object z obsługującym go builderem.
@Synchronized
Jest to alternatywa dla synchronizowanych metod. Domyślne synchronizowane metody zakładają blokadę na obiekcie this (lub metody statyczne na obiekcie klasy), natomiast @Synchronized generuje prywatną zmienną, którą wykorzystuje w tym celu. Daje to trochę większe możliwości, jednak osobiście preferowałbym standardowe rozwiązania z pakietu: java.util.concurrent.
@SneakyThrows
@SneakyThrows pozwala uniknąć deklarowania w definicji metod wyjątków oznaczonych (checked exceptions). To dość ryzykowny mechanizm, dlatego zalecam ostrożność przy jego wykorzystywaniu.
Można wyróżnić dwie główne sytuacje, kiedy takie rozwiązanie będzie pomocne:
- gdy mamy pewność, że dany wyjątek nigdy nie poleci i nie chcemy dodatkowo zaciemniać kodu. Jak w przykładzie z metodą utf8ToString, gdzie wiemy, że kodowanie UTF-8 jest zawsze wspierane przez Javę
- gdy wyjątki są przechwytywane i obsługiwane w dalszych warstwach aplikacji. Alternatywnym rozwiązaniem jest przechwycenie takiego wyjątku i ponowne rzucenie go opakowując wcześniej w RuntimeException.
public class SneakyThrowsExample { @Test(expected = Exception.class) @SneakyThrows public void throwsExcpeptionTest() { throw new Exception(); } @Test @SneakyThrows(UnsupportedEncodingException.class) public void utf8ToString() { String str = new String("".getBytes(), "UTF-8"); Assert.assertEquals("", str); } }
@Log
@Log jest to wsparcie dla loggerów. Poniższy kod wygeneruje w locie prywatną zmienną w klasie:
private static final java.util.logging.Logger log =
java.util.logging.Logger.getLogger(LogExample.class.getName());
@Log public class LogExample { @Test public void infoTest() { log.log(Level.INFO, "Info message"); } }
Lombok umożliwia również skorzystanie z innych bibliotek do logowanie przed adnotacje: @CommonsLog, @JBossLog, @Log4j, @Log4j2, @Slf4j oraz @XSlf4j.
Adnotacje te nie dodają odpowiednich bibliotek! Musisz samodzielnie dodać dla nich wszystkie niezbędne zależności.
Ciekawostka:
Lombok wykorzystuje nieudokumentowaną funkcjonalność kompilatora Javy — mówimy o modyfikacji AST, w momencie kiedy Java ‘czyta’ kod przed kompilacją i zamienia go na reprezentację w pamięci, Lombok modyfikuje tą reprezentację dodając odpowiednie metody i pola. Dzięki temu dla Javy dodane funkcjonalności wyglądają jak standardowy kod, a jednocześnie nie mamy dodatkowych ograniczeń związanych z koniecznością dziedziczenia po jakiejś klasie lub odwoływania się do innych klas. Ponadto Lombok jest wymagany tylko w momencie kompilacji i nie musi być obecny w skompilowanej aplikacji.