Site icon Blog programisty Java

Popularne frameworki #1: Lombok

Project Lombok

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.

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śli korzystasz z IDE (a zakładam, że tak ;)), potrzebna Ci będzie poprawnie zainstalowana wtyczka do biblioteki. W przypadku IntelliJ nie powinno być problemu, bowiem twórcy oprogramowania zaprojektowali całkiem dobrze działający plugin. Niestety trochę gorzej jest, jeśli 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śli 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śli dziwisz się, do czego może służyć ten drugi, to przypominam Ci, że 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ą właściwie zaimplementowane metody hashCode i equals. Często ich wewnętrzna struktura jest podobna do siebie, więc dlaczego ich również nie generować? Za pomocą 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 za pomocą 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 za pomocą 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 ciekawe 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.

*Instrukcja konfiguracji Lomboka na przykładzie Eclipse: https://projectlombok.org/setup/eclipse

**Przykładem jest tworzenie encji w Hibernate

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

Exit mobile version