Popularne frameworki #1: Lombok

developeronthego.pl 2 lat temu
Zdjęcie: Project Lombok


%title%

Język Java często jest krytykowany za konieczność pisania kodu, który mógłby być automatycznie wygenerowany przez JDK. Ciągłe pisanie getterów i setterów do każdej klasy POJO, tworzenie toStringów, czy projektowanie builderów, choć początkującemu programiście daje wiele satysfakcji, to po pewnym czasie staje się bardzo nudne. W takiej sytuacji na pomoc przychodzi biblioteka Lombok, której celem jest uprościć kod Java.

Table of Contents
  • Instalacja
  • Generowanie składowych klasy
    • Akcesory
      • @Getter i @Setter
      • @Accessors
    • Konstruktory
    • Equals, hashCode oraz toString
  • Generacja gotowych klas
    • Klasa typu builder
    • Klasa typu data
    • Klasa typu immutable
  • Inne interesujące adnotacje
    • Walka z nullpointerami
    • Wyjątki sprawdzane (checked)
    • Logger
    • Klasa pomocnicza

Instalacja

Aby wykorzystać bibliotekę w kodzie korzystającym z Mavena, należy dodać zależność poprzez wpis:

<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.24</version> <scope>provided</scope> </dependency> </dependencies>

Jeśli korzystasz z innego menadżera projektu (np. Gradle’a) to zajrzyj na oficjalną stronę projektu Lombok. Dodatkowo, jeżeli korzystasz z IDE (a zakładam, iż tak ;)), potrzebna Ci będzie poprawnie zainstalowana wtyczka do biblioteki. W przypadku IntelliJ nie powinno być problemu, bowiem twórcy systemu zaprojektowali całkiem dobrze działający plugin. Niestety trochę gorzej jest, jeżeli korzystasz z drugiego popularnego IDE Eclipse. Tutaj należy ją samodzielnie pobrać i skonfigurować*.

Generowanie składowych klasy

Lombok pozwala na wygenerowanie wiele powtarzalnych składowych klasy, stosując odpowiednie adnotacje. Największą zaletą tego rozwiązania jest możliwość zastosowania różnej ich kombinacji. Dlatego, jeżeli chcesz napisać klasę z samymi getterami i jednym konstruktorem, to bez problemu możesz to zrobić. Dodatkowo Twój kod będzie dużo czytelniejszy i krótszy.

Spójrz na poniższy przykład:

public class RealEstate { private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(RealEstate.class.getName()); private Long id; private String code; private String address; public RealEstate() { } public RealEstate(Long id, String code, String address) { this.id = id; this.code = code; this.address = address; } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public void writeFile() { Path filePath = Paths.get("real_estate.txt"); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) { bufferedWriter.write(toString() + "\n"); } catch (IOException e) { LOGGER.warning("Can't write result to file with content:" + e.getMessage()); } } public void showMessage() { Logger.getGlobal().info("Real estate class"); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; RealEstate that = (RealEstate) o; return Objects.equals(id, that.id) && Objects.equals(code, that.code); } @Override public int hashCode() { return Objects.hash(id, code); } @Override public String toString() { return "RealEstate{" + "id=" + id + ", address='" + address + '\'' + '}'; } }

Powyższa klasa jest bardzo długa. Zawiera pola, konstruktory, metody equals, hashcode, toString, użyty logger oraz metodę obsługującą wyjątek.

Akcesory

Jedną z najczęściej powtarzających się składowych klasy są gettery i settery dla pól. Często te proste metody zajmują połowę lub więcej miejsca, a z reguły nie posiadają żadnych wewnętrznych reguł poza przypisaniem wartości lub jej zwracaniem. Adnotacje Getter i Setter nakładane nad klasą wygenerują wszelkie gettery i settery do pól (oczywiście można zastosować także tylko jedną z nich)

@Getter i @Setter

Po zastosowaniu adnotacji:

@Getter @Setter public class RealEstateLombok { private Long id; private String code; private String address; }

@Accessors

Ta adnotacja jest rozwinięciem @Getter i @Setter. Posiada kilka ciekawych flag. Omówię tu dwie najczęściej używane. Pierwsza o nazwie fluent umożliwia, używanie akcesorów bez prefiksu get i set. Kolejna chain, pozwala stosować akcesory w jednej linii, podobnie jak w wzorcu projektowym „budowniczy”.

Klasa RealEstate zaktualizowana o adnotację Accessors:

@Getter @Setter @Accessors(fluent = true, chain = true) public class RealEstateLombok { private Long id; private String code; private String address; }

Przy przykład użycia flagi fluent ustawionej na true:

realEstateLombok.address("ul. Nowowiejska 54/3 Katowice");

Flaga chain ustawiona na true:

realEstateLombok.address("ul. Nowowiejska 54/3 Katowice").code("123");

Konstruktory

Kolejnym często schematycznym blokiem kodu w klasie są konstruktory. Bardzo rzadko zachodzi tu coś więcej, niż zwykłe przypisanie danych do pól. Lombok pozwala generować zarówno konstruktor zawierający wszystkie pola, jak i konstruktor bezargumentowy. jeżeli dziwisz się, do czego może służyć ten drugi, to przypominam Ci, iż w przypadku napisania jakiegokolwiek konstruktor Java nie będzie już niejawnie tworzyć konstruktora bezargumentowego. Niestety czasami posiadanie takiego konstruktora jest wymagane przez niektóre frameworki.**

Zastosowanie: @NoArgsConstructor, @AllArgsConstructor

@Getter @Setter @Accessors(fluent = true, chain = true) @AllArgsConstructor @NoArgsConstructor public class RealEstateLombok { private Long id; private String code; private String address; }

Teraz możesz stworzyć zarówno instancję klasy, wypełniając wszystkie pola:

RealEstateLombok realEstateLombok = new RealEstateLombok(1L, "12345", "ul. Nowowiejska 54/3 Katowice");

jak i „pusty” obiekt:

RealEstateLombok emptyRealEstate = new RealEstateLombok();

Equals, hashCode oraz toString

Jak wiesz z poprzednich lekcji, aby poprawnie korzystać ze struktur opierających się o algorytmy haszujące, potrzebne są adekwatnie zaimplementowane metody hashCode i equals. Często ich wewnętrzna struktura jest podobna do siebie, więc dlaczego ich również nie generować? dzięki frameworku Lombok można to wykonać używając @EqualsAndHashCode.

Używając tej adnotacji należy uważać na pola, będące typami referencyjnymi. Powodem jest po pierwsze częste niepotrzebne spowolnienie działanie aplikacji, po drugie w przypadku cyklu (referencja do tej samej referencji) framework może zwariować. Dlatego polecam wypełnić adnotacje stosujące opcje exclude lub of. Do pierwszej z nich, wpisuje się pola, które mają być pominięte podczas generacji. Analogicznie flaga of, będzie korzystać tylko z tych pól, które zostaną w niej zawarte.

Podobnie jak @EqualsAndHashCode można używać @ToString.

Klasa po dodaniu powyższych dwóch adnotacji:

@Getter @Setter @AllArgsConstructor @NoArgsConstructor @ToString(of={"id", "address"}) @EqualsAndHashCode(exclude={"address"}) public class RealEstateLombok { private Long id; private String code; private String address; }

Efekt skorzystania z toString z wykorzystaniem flagi of:

RealEstateLombok(id=1, address=ul. Nowowiejska 54/3 Katowice)

Testowanie kolizji dzięki kolekcji HashSet:

Set<RealEstateLombok> realEstates = new HashSet<>(); realEstates.add(new RealEstateLombok(1L, "12345", "ul. Nowowiejska 17 Katowice")); realEstates.add(new RealEstateLombok(1L, "12345", "ul. Nowowiejska 17 Katowice")); System.out.println(realEstates);

Prawidłowy wynik (brak kolizji) na konsoli

[RealEstateLombok(id=1, address=ul. Nowowiejska 17 Katowice)]

Generacja gotowych klas

Klasa typu builder

@Builder tworzy z Twojego POJO klasę typu budowniczy. Pozwala ona na szybkie tworzenie rozbudowanych obiektów i jednoczesne unikanie pisania wielu konstruktorów lub setterów. Niestety sama implementacja wzorca budowniczy zajmuje trochę czasu. Na szczęście w projekcie Lombok wystarczy jedna adnotacja, która robi robotę.

@Builder public class CarBuilder { private String brand; private Long age; private String colour; }

Przykład użycia:

CarBuilder.builder() .brand("Opel") .age(5) .colour("red") .build();

Klasa typu data

@Data to kombinacja wielu znanych już adnotacji: @Getter, @Setter, @ToString, @EqualsAndHashCode oraz @RequiredArgsConstructor.

@Data public class CarDataObject { private String brand; private Long age; private String colour; }

Powyższe adnotacje były już omawiane, więc nie będę wrzucał dodatkowych przypadków użycia.

Klasa typu immutable

Analogicznie do @Data, @Value to adnotacja tworząca obiekt immutable dzięki kombinacji adnotacji, takich jak: @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter. Dodatkowo klasa będzie klasą finalną. Ciekawą adnotacją jest też FieldDefaults, która pozwala zaoszczędzić czasu przy pisaniu modyfikatorów dostępu (flaga level), a także podczas ustawianiu pól jako finalne (flaga makeFinal).

@Value public class CarValueObject { private String brand; private Long age; private String colour; }

Użycie:

CarValueObject carVO = new CarValueObject("Opel", 5, "red");

Jako klasa niemodyfikowalna CarValueObject musi inicjować wszystkie pola w konstruktorze (są typu final).

Inne interesujące adnotacje

Walka z nullpointerami

Częstym problemem w programowaniu obiektowym jest brak kontroli nad występującymi wyjątkami typu nullpointer. Pewnym rozwiązaniem tego problemu może być stosowanie adnotacji NonNull (podobne adnotacje występują nie tylko w bibliotece Lombok). Bardzo dobrze ta adnotacja sprawdza się razem z adnotacją RequiredArgsConstructor, która tworzy konstruktor na podstawie pól, które nie mogą być puste (nullem).

Wykorzystanie @NonNull oraz @RequiredArgsConstructor

@RequiredArgsConstructor public class NonNullCar { @NonNull private String brand; private Integer age; @NonNull private String colour; }

Wyjątki sprawdzane (checked)

Biblioteka Lombok zawiera też „featury”, które są uważane za eksperymentalne (używaj na własne ryzyko). Jedną z takich adnotacji jest @SneakyThrows, która pozwala obejść regułę wymagalność obsługiwania wyjątków typu „checked”.

Przykładowa obsługa wyjątku sprawdzalnego:

public void writeFile() { Path filePath = Paths.get("real_estate.txt"); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) { bufferedWriter.write(toString() + "\n"); } catch (IOException e) { LOGGER.warning("Can't write result to file with content:" + e.getMessage()); } }

Po zastosowaniu adnotacji SneakyThrows:

@SneakyThrows public void writeFile() { Path filePath = Paths.get("real_estate.txt"); try (BufferedWriter bufferedWriter = Files.newBufferedWriter(filePath, UTF_8)) { bufferedWriter.write(toString() + "\n"); } }

Logger

Kolejną bardzo przydatnymi adnotacjami są wszelkie adnotacje dotyczące logowania. Są nimi m. in. @Log, @Log4j, @Slf4j. Upraszczają one odrobinę kod. Tutaj przykład działania adnotacji Log, która jest tożsama z użyciem standardowego loggera Javy.

private static final java.util.logging.Logger LOGGER = java.util.logging.Logger.getLogger(RealEstate.class.getName()); public void showMessage() { Logger.getGlobal().info("Real estate class"); }

Z wykorzystaniem @Log:

public void showMessage() { log.info("Real estate class"); }

Klasa pomocnicza

Ostatnią ciekawą klasą jest możliwość tworzenia tzw. klasę pomocniczą (ang. helper class). Klasa tego typu jest z reguły klasą finalną, co zabezpiecza przed dziedziczeniem, oraz każda metoda publiczna powinna być statyczną. Taka klasa powinna też posiadać jedynie stałe, jak i prywatny konstruktor. Dzięki takim założeniom zastosowane jest tu bardziej funkcjonalne podejście do programowania. Możesz utworzyć taką klasę automatycznie poprzez użycie @UtilityClass

Wersja podstawowa:

public final class FibonacciCalculator { private static final int FIRST_POSITION = 0; private static final int SECOND_POSITION = 1; private FibonacciCalculator() { } public static int countValue(int position) { int previouspreviousNumber, previousNumber = FIRST_POSITION, currentNumber = SECOND_POSITION; for (int i = 1; i < position ; i++) { previouspreviousNumber = previousNumber; previousNumber = currentNumber; currentNumber = previouspreviousNumber + previousNumber; } return currentNumber; } }

Z wykorzystaniem biblioteki:

@UtilityClass public class FibonacciCalculatorLombok { private final int FIRST_POSITION = 0; private final int SECOND_POSITION = 1; public int countValue(int position) { int previouspreviousNumber, previousNumber = FIRST_POSITION, currentNumber = SECOND_POSITION; for (int i = 1; i < position ; i++) { previouspreviousNumber = previousNumber; previousNumber = currentNumber; currentNumber = previouspreviousNumber + previousNumber; } return currentNumber; } }

To wszystkie omawiane przeze mnie adnotacje, dostarczane przez projekt Lombok. Nie są to jednak jedyne przykłady, bo sam framework jest dość rozbudowany. W kwestii rozszerzenia sobie wiedzy o nim tradycyjnie polecam przeczytać oficjalną dokumentację twórców.

**Przykładem jest tworzenie encji w Hibernate

Kod do lekcji: https://github.com/developeronthego/java-frameworks/tree/main/src/main/java/frameworks/lombok

Idź do oryginalnego materiału