JUnit 5 – Wstęp

JUnit 4 jest już stary. Został opublikowany gdzieś w okolicach 2006 roku. Ostatnia aktualizacja (4.12) to rok 2014. Sama biblioteka nie jest zła, jeśli chodzi o testy jednostkowe. Spełnia swoje zadanie i nie ma powodu by się do czegoś przyczepić. Przez ostatnie 11 lat zaszło trochę zmian w języku jak i w sposobach wytwarzania oprogramowania. Przyszedł już czas na aktualizację.

Rzeczy które w JUnit 4 mi nie pasują

JUnit jest świetny, jeśli chodzi o testy jednostkowe. I tylko jeśli chodzi o testy jednostkowe. Jakakolwiek próba użycia go w testach integracyjnych jest z góry skazana na niepowodzenie. JUnit został zaprojektowany tak, by testy w nim pisane były niezależne, szybkie, powtarzalne i automatyzowalne. Innymi słowy, by spełniały zasadę F.I.R.S.T. Piąty element tej zasady jest związany z pokryciem. Jednak zaczęliśmy używać JUnita do tworzenia testów integracyjnych i integracyjno-jednostkowych. W efekcie mechanizm związany z @RunWith stał się zaczepem dla pluginów. Coś, co miało pomagać w migracji z wersji 3.x, służy nam do zupełnie czegoś innego.

W dodatku mamy do czynienia z klasycznym dependency hell, gdy potrzebujemy wykorzystać kilka runnerów. Który będzie pierwszy? Czy kolejność jest prawidłowa? Co będzie, jak zamienię kolejnością runnery? Słabo.

Kolejna rzecz to zarządzanie zależnościami pomiędzy testami. JUnit tego nie ma. Czasami jednak jest to przydatna funkcjonalność. Nie tylko na poziomie testów integracyjnych, ale też po to by testy jednostkowe można było wykonywać według zasady „fail fast”. Co prawda można na poziomie np. mavena ustawić odpowiednią regułę, ale dotyczy ona wszystkich testów. Przydałaby się granulacja na poziomie klasy albo przynajmniej pojedynczego zestawu.

Paskudnie rozwiązane zarządzanie zależnościami. W sumie nie ma się co dziwić, bo prawdziwy test jednostkowy nie ma zależności. Tyle tylko, że czasami warto by jakąś fabryczkę do mocków zrobić, albo dorzucić jakiś niewielki helper… a tu dupa i trzeba to ręcznie wiązać, albo za pomocą runnerów.

Ostatnia rzecz to asercje. Pozostało po starszych wersjach i nie idzie tego zmienić. Z drugiej strony wystarczy podpiąć AssertJ i olać asercje JUnita.

Dlatego lubię TestNG

TestNG jest znacznie młodsze. Wersja 1.0 jest z 2004 roku, ale najstarsza na githubie wersja 5.13 z 2010. Najnowsza 6.11 z końca lutego 2017 🙂 Mamy tutaj wszystko to, co w JUnit jest upierdliwe, uciążliwe, albo czego nie ma, a by się przydało. Po pierwsze zależności pomiędzy testami możemy skonfigurować na poziomie samych testów. Zatem można przygotować zestaw testowy tak, by wywalenie się pojedynczego testu skutkowało nieuruchomieniem tylko części testów. To z kolei oznacza, że można tworzyć testy integracyjne we w miarę łatwy sposób. Co więcej, można budować całe drzewa testów. Wystarczy wykorzystać grupy, które można zagnieżdżać. Do tego dochodzi całkiem dobra granulacja Before/After i już mamy w pełni elastyczne narzędzie.

Po drugie TestNG posiada na pokładzie mechanizm modułów. Chcesz zrobić sobie takie małe DI, by postawić część kontekstu? Proszę bardzo, jeżeli tylko trzymasz się JSR-330, czyli Dependency Injection for Java. Jako implementacja wykorzystywany jest Google Guice, co daje całkiem wygodne API. Swoją drogą im częściej porównuję Springa i Guice tym bardziej lubię Guice.

Po trzecie may też do dyspozycji mechanizm DataProvider, który pozwala nam na szybkie tworzenie np. testów parametryzowanych. Coś, co w JUnit trzeba ogarnąć runnerem, tu działa out of box.

Rzeczy dobre w JUnit

Ciekawie rozwiązano problem weryfikacji wyrzucanych wyjątków. Możemy użyć do tego odpowiedniego parametru w adnotacji, ale to nie jest najlepsze podejście. Co prawda wychwycimy wyjątek, ale nie zawsze możemy powiedzieć, że to co złapaliśmy, jest tym, czego się spodziewamy. Dlaczego? Ponieważ czasami taki sam wyjątek może być wyrzucony w różnych punktach testu. Zamiast tak prymitywnego podejścia, dostępnego też w TestNG, możemy użyć adnotacji @Rule. Z jej pomocą zdefiniujemy odpowiednią regułę, która pozwoli nam na ciche przechwycenie wyjątku oraz jego weryfikację. TestNG posiada podobnie działający mechanizm.

Podejście JUnit5

Jak widać, jest trochę rzeczy złych w JUnit 4. Twórcy postanowili naprawić większość z nich. W tym celu całkowicie zmienili architekturę. Podzielili ją na trzy główne części.

Zanim przejdę do omówienia elementów, drobna uwaga. JUnit 5 jest obecnie w epoce milestonea łupanego. Opis dotyczy tego jak to wygląda na chwilę obecną. Xapewne nie będziemy mieli już wielu zmian, to jednak może okazać się, że gdy czytasz ten tekst jest on w jakimś stopniu nieaktualny.

JUnit Platform

Jest to główna „paczka” nowej biblioteki. Zawiera w sobie najważniejsze elementy. Po pierwsze Launcher, który jest odpowiedzialny za udostępnienie API platformy różnego rodzaju klientom. Dodatkowo wykonuje pracę związaną z wyszukaniem testów w classpath. Klientami są implementacje TestEngine. Sam TestEngine udostępniony przez platformę służy do filtrowania i uruchamiania testów w zależności od potrzeb. Chcesz mieć FitNess na pokładzie i uruchamiać testy pisane za pomocą tej biblioteki? Wystarczy dodać odpowiedni silniczek.

Same silniki testowe są wyszukiwane za pomocą mechanizmu SPI, czyli nie musimy nic dodatkowo konfigurować, by całość śmigała. Wystarczy, by nasza paczka spełniała założenia SPI. Kolejnym elementem jest commons, który zawiera bebechy i narzędzia. Używać na własną odpowiedzialność.

Do tego mamy jeszcze mały programik konsolowy do uruchamiania testów, pluginy do gradle i surefire oraz runner, pozwalający na uruchomienie testów JUnit 5 w środowisku JUnit 4. Ten ostatni element ma ułatwić migrację.

JUnit Jupiter

Jupiter to pierwszy z silników testowych dostarczonych przez bibliotekę. Służy do uruchamiania testów oraz ma API do tworzenia rozszerzeń. Rozszerzenia pozwalają na modyfikowanie środowiska testowego, dodawania pewnych elementów do testów itp. Pełnią podobną rolę co runnery JUnit4, ale robią to w zupełnie inny sposób.

JUnit Vintage

Drugi z silników testowych, służy do uruchamiania testów napisanych w JUnit 4 i Junit 3.

Konfiguracja

Konfiguracja JUnit 5 w mavenie jest śmiesznie prosta:

Listing 1. Konfiguracja w pom.xml


    
        
            maven-surefire-plugin
            2.19
            
                
                    org.junit.platform
                    junit-platform-surefire-provider
                    1.0.0-M3
                
                
                    org.junit.jupiter
                    junit-jupiter-engine
                    5.0.0-M3
                
                
                    org.junit.vintage
                    junit-vintage-engine
                    4.12.0-M3
                
            
        
    



    
        org.junit.jupiter
        junit-jupiter-api
        5.0.0-M3
        test
    
    
        junit
        junit
        4.12
        test
    

    
        pl.pragmatists
        JUnitParams
        1.0.6
        test
    

Przy czym jedna uwaga. Jak już chcemy korzystać z JUnit 5, to darujmy sobie próby konfiguracji JUnit 4 jako osobnego zadania dla surefire. Lepiej jest dodać silnik Vintage, który pod spodem po prostu odpala JUnit 4 niż kombinować z konfiguracją. Szkoda czasu, a i wynik będzie popieprzony. Dlaczego? Ponieważ JUnit 5 wykryje testy JUnit 4, ale bez silnika vintage ich nie uruchomi. Zatem w logu dostaniemy:

Listing 2. Uruchomienie testów JUnit 4 bez silnika Vintage

mar 28, 2017 11:38:09 AM org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines
INFO: Discovered TestEngines with IDs: [junit-jupiter]
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4IgnoreTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.001 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4IgnoreTest
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithoutRunnersTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithoutRunnersTest
Running pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithRunnersTest
Tests run: 0, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0 sec - in pl.koziolekweb.blog.fizzbuzz.FizzBuzzJUnit4WithRunnersTest

Klasa jest, testu nie ma, a teraz pomyślcie, jak wygląda raport. Będzie zawierać wiele zer… to nie dobrze?

Podsumowanie

Czas na krótkie podsumowanie. JUnit 5 wygląda na bibliotekę, która pozwoli nam na lepsze zarządzanie testami. Same testy będą pisane trochę inaczej i istnieje szansa, że będą lepsze. Moim zdaniem warto już dziś przyjrzeć się temu projektowi i spróbować go wdrożyć we własnych projektach. Całość jest mniej więcej stabilna, a zatem nie będzie jakiś dziwnych wybuchów. W kolejnych artykułach przybliżę różnice pomiędzy JUnit 5 i 4, w zakresie tworzenia testów.

Zstąpienie Aniołów – Krucjata inna niż wszystkie

Okładka Zstąpienie Aniołów

Tytuł: Zstąpienie Aniołów
Autor: Mitchel Scanlon
Rok: 2016
ISBN: 978-8-36165-644-9

Najtrudniejsza książka z serii Herezji Horusa za mną 😉 Jest inna. Po prostu. Gdy kilka lat temu czytałem recenzje Zstąpienia Aniołów, to przewijał się motyw nudy. Wersję angielską przeczytałem gdzieś 10 lat temu i jakoś nie pokochałem. Ostatnio wróciłem do tematu i przeczytałem polskie tłumaczenie. Książka jest dobra. Tyle tylko, że po pierwszych pięciu tomach HH można mieć trochę inne oczekiwania. W efekcie wielu czytelników dochodzi do wniosku, że całość wieje nudą. Jednak po kolei.

Akcja książki rozpoczyna się na kilka lat przed przybyciem Imperatora na Caliban. Lion el’Jonson nie jest jeszcze mistrzem Zakonu, ale już od początku wiemy, że nominacja to kwestia czasu. Historia opowiedziana jest z perspektywy Zahariela, jednego z zakonnych suplikantów, a później młodego rycerza. Mamy więc tutaj książkę raczej przygodową z elementami fantasy, ale powiedzmy sobie szczerze, to fantasy jest na poziomie bajki o św. Jerzym i smoku. Walczymy z dużym potworem i jest OK. Bliżej temu do „Zabójcy Trolli” niż do HH. Całość podzielona jest na cztery części i dwie pierwsze dotyczą właśnie okresu rycerskiego. Dwie kolejne to już opowieść z perspektywy Zahariela – astares.

Autor pomija tu proces tworzenia kosmicznego marine. Jedynie opisuje wstępną fazę selekcji związana z wyborem najtwardszych i najwytrzymalszych rycerzy i suplikantów. Pomija też kwestię przemiany starszych rycerzy, później jedynie wspomniane jest, że zostali poddani kuracji chemicznej, ale nie otrzymali geno-ziarna. W końcu akcja książki przenosi się na Sarosh, jedną z planet odkrytych w czasie krucjaty. Dalej standard – lokalsi okazali się wyznawcami jakiejś tam formy chaosu, zdradzili siły imperium i próbowali zabić Jonsona. Ten się wkurwił i…

No właśnie i zarządził uderzenie punktowe z wykorzystaniem niewielkich sił. To, co odróżnia Zstąpienie od innych książek z serii HH, to brak wojny totalnej. Nie znajdziemy tu opisów epickich bitw, poza jedną – zdobyciem twierdzy rycerzy Lupusa w czasach przed imperialnych, ale i tu raczej są to Krzyżacy. Nie ma tytanicznych zmagań prowadzonych przez tysiące astares z użyciem setek czołgów, samolotów i wsparciem Collegia Titanica. Nie ma tego wszystkiego, co jest znakiem charakterystycznym HH. W zamian jest opowieść o wewnętrznej rywalizacji w zakonie. Każdy, kto zna choć trochę świat w40k, odnajdzie tu wiele informacji i wyjaśnień dotyczących, takiego, a nie innego losu Calibanu.

Książka jest dobra. Jest dobrym czytadłem. Jednak zanim zacznie się lekturę, należy odrzucić schematy znane z HH. Wtedy mamy do czynienia z całkiem dobrą książką. Na koniec doczepię się do jednej rzeczy. Dialogi w książce są takie jakieś koślawe. Nie są drętwe, ale nie ma w nich tego czegoś. Większość, to pierdolenie o Chopinie, autor starał się chyba stylizować je na „teksty prawdziwych rycerzy”, ale mu nie wyszło. I jest to jedyny poważny minus.

Iterator filtrujący, czyli kontrakt iteratora w dwóch odsłonach

Dziś o tym, że kontrakt zawarty w dokumentacji można interpretować na wiele sposobów oraz dlaczego SRP jest istotne.

Na 4p padło pytanie, jak ogarnąć problem iteratora, który będzie przy okazji filtrował kolekcję. Sama implementacja jest stosunkowo prosta:

Listing 1. Iterator filrtujący

public class FilteringIterator<T> implements Iterator<Optional<T>> {

	private final Iterator<T> iter;

	private final Predicate<T> predicate;

	public FilteringIterator(Collection<T> collection, Predicate<T> predicate) {
		this.iter = collection.iterator();
		this.predicate = predicate;
	}

	@Override
	public boolean hasNext() {
		return iter.hasNext();
	}

	@Override
	public Optional<T> next() {
		T next = iter.next();
		return Optional.ofNullable(next).filter(predicate);
	}
}

Założenie jest następujące. Idziemy przez kolekcję i delegujemy zachowania iteratora do iteratora powiązanego z tą kolekcją. Przy czym metoda next pełni role adaptera, który zwraca Optional[T] zamiast T. jarekr000000, zwrócił mi jednak uwagę, że należało by logikę predykatu przenieść do metody hasNext. Tyle tylko, że w takim wypadku otrzymamy iterator, który będzie iteratorem until (limitującym). To znaczy, będzie zwracać jakąś wartość, dopóki warunek jest spełniony.

Listing 2. Iterator limitujący

public class UntilIterator<T> implements Iterator<T> {

	private final Iterator<T> iter;

	private final Predicate<T> predicate;
	private Optional<T> current;


	public UntilIterator(Collection<T> collection, Predicate<T> predicate) {
		this.iter = collection.iterator();
		this.predicate = predicate;
	}

	@Override
	public boolean hasNext() {
		boolean b = iter.hasNext();
		if (b) {
			current = Optional.ofNullable(iter.next())
					.filter(predicate);
			return current
					.map($ -> Boolean.TRUE) // element istnieje i spełnia warunek
					.orElse(false); // element nieistnieje albo niespełnia waruneku
		}
		return false;
	}

	@Override
	public T next() {
		return current.orElseThrow(NoSuchElementException::new);
	}
}

Implementacja jest oczywiście po łebkach i nie zapewnia prawidłowego działania w środowisku współbieżnym.

Na czym polega różnica?

Na początek rzućmy okiem na kontrakt metody hasNext:

Returns true if the iteration has more elements. (In other words, returns true if next() would return an element rather than throwing an exception.)

źródło

Pierwszy z iteratorów będzie zwracał coś, Optional albo Empty, dla każdego elementu w oryginalnej kolekcji. Oznacza to też, że metoda hasNext będzie zwracać true dopóki w oryginalnej kolekcji są jeszcze elementy. Oznacza to też, że jeżeli element nie spełnia warunków testu, to i tak metoda next nie powinna rzucać wyjątkiem.

Drugi z iteratrów zachowa się zupełnie inaczej. W tym przypadku, hasNext zwróci false dla pierwszego elementu niespełniającego warunku. Stanie się tak nawet wtedy, gdy oryginalna kolekcja będzie miała jeszcze elementy. Tym samym metoda next może rzucić wyjątek, nawet wtedy, gdy kolekcja nadal posiada elementy spełniające warunek.

Kolejna różnica, to wartość zwracana z metody next. Pierwszy z iteratorów opakuje ją w Optional. Drugi zwróci wartość taką jakiej oczekujemy iterując przez kolekcję obiektów o danym typie. Czy ta różnica ma znaczenie? Czasami ma, ponieważ dla pierwszego iteratora musimy wykonać operację „rozpakowania” wartości. W przypadku drugiego już nie. Dobrze obrazują to testy:

Listing 3. Test dla iteratora filtrujacego

public class FilteringIteratorTest {

	@Rule
	public ErrorCollector collector;

	private List<Integer> ints;

	@Before
	public void setUp() throws Exception {
		collector = new ErrorCollector();
		ints = asList(4, 3, 2, 1);
	}

	@Test
	public void filterOutSomeInts() throws Exception {
		FilteringIterator<Integer> it = new FilteringIterator<>(ints, i -> i % 2 == 0);

		// 4
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isPresent().contains(4);

		// 3
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isEmpty();

		// 2
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isPresent().contains(2);

		// 1
		assertThat(it.hasNext()).isTrue();
		assertThat(it.next()).isEmpty();

		// no more elements
		assertThat(it.hasNext()).isFalse();

		catchException(it).next();
		collector.checkThat(caughtException(), instanceOf(NoSuchElementException.class));

	}
}

Listing 4. Test dla iteratora limitującego

public class UntilIteratorTest {

	@Rule
	public ErrorCollector collector;

	private List<Integer> ints;

	@Before
	public void setUp() throws Exception {
		collector = new ErrorCollector();
		ints = asList(4, 3, 2, 1);
	}

	@Test
	public void filterUntilSomeInts() throws Exception {
		UntilIterator<Integer> it = new UntilIterator<>(ints, i -> i % 2 == 0);
		// 4
		Assert.assertTrue(it.hasNext());
		Assert.assertEquals(it.next().intValue(), 4);

		// 3 → false
		Assert.assertFalse(it.hasNext());
		catchException(it).next();
		collector.checkThat(caughtException(), instanceOf(NoSuchElementException.class));
	}

}

Wszystko ładnie pięknie, ale jest jeden problem.

SRP

Zasada pojedynczej odpowiedzialności jest całkiem fajna. Tyle tylko, że tu jest złamana. Jak? Nasze iteratory robią dwie rzeczy naraz. Po pierwsze poruszają się po kolekcji. Po drugie manipulują elementami kolekcji. W efekcie już na samym początku mamy problem. Autor pytania łamiąc SRP, wpędził się w kłopoty. Jarek i ja dyskutowaliśmy nad problemem, który w praktyce nie powinien istnieć.

Pozostaje jeszcze kwestia kontraktu metody hasNext. Jest on na tyle ogólny, że oba iteratory mogą w na swój sposób określić, jak go realizują. Ze szczególnym uwzględnieniem co oznacza „brak kolejnych elementów”.

Zachłannie, leniwie, współbieżnie – czego nauczyłem się od Jose Valima

Warto jeździć na konferencje i słuchać mądrzejszych do siebie. Warto, ponieważ często nie dowiemy się nic nowego, ale sposób przekazania wiedzy pozwoli na uporządkowanie kilku rzeczy.

Jose na tegorocznych LambdaDays opowiadał o GenStage i Flow. Jednak nie to jest najważniejsze. Jakby przy okazji omówił trzy różne podejścia do pracy z danymi.

Podejście zachłanne

Jest najprostsze. Ładujemy wszystko do pamięci i jazda. W takim przypadku program nie zawiera żadnej dodatkowej magii:

Listing 1. Zachłanne przetwarzanie danych.

def eager() do
  File.read!("plwiki-20170301-all-titles")
    |> String.split("\n")
       |> Enum.flat_map(&String.split/1)
       |> Enum.reduce(%{}, fn word, map ->
                                Map.update(map, word, 1, & &1 + 1)
                           end
                     )
end

W powyższym programie wczytujemy plik tekstowy i chcemy policzyć wystąpienia poszczególnych słów. Nie ma tu nic skomplikowanego. Funkcja read! wczytuje cały plik. Następnie dzielimy go na linie, a z linii budujemy listy (enumerable) słów za pomocą flat_map. Na koniec wszystko redukujemy. Takie podejście ma jednak pewną wadę. Musimy zapewnić odpowiednio dużo pamięci dla naszego programu. Oczywistym rozwiązaniem jest…

Podejście leniwe

W tym przypadku ustalamy, że będziemy czytać plik linia po linii i przetwarzać na raz całą linię.

Listing 2. Leniwe przetwarzanie danych.

def lazy() do
  File.stream!("plwiki-20170301-all-titles", [], :line)
    |> Stream.flat_map(&String.split/1)
    |> Enum.reduce(%{}, fn word, map ->
                            Map.update(map, word, 1, & &1 + 1)
                        end
                  )
end

Funkcja stream! jako trzeci parametr przyjmuje :line. Dzięki czemu odczyta na raz tylko jedną linię z pliku. Udało nam się wyeliminować dużą konsumpcję pamięci, ale pytanie, co jak chcemy przyspieszyć?

Podejście współbieżne

Jeżeli dane są wzajemnie niezależne, to możemy spróbować usprawnić proces ich przetwarzania. Wystarczy, by przetwarzanie odbywało się na wielu procesorach.

Listing 3. Równoległe przetwarzanie danych.

def parallel() do
  File.stream!("plwiki-20170301-all-titles")
    |> Flow.from_enumerable()
    |> Flow.flat_map(&String.split/1)
    |> Flow.partition()
    |> Flow.reduce(fn -> %{} end, fn word, map ->
                                     Map.update(map, word, 1, & &1 + 1)
                                  end
                    )
    |> Enum.into(%{})
end

Narzędziem, które nam to ułatwia, jest Flow. Dzięki niemu możemy zaimplementować zarówno mechanizm równoległego przetwarzania danych, z wykorzystaniem GenServer, jak i wykorzystać tzw. Back-pressure, by dostroić obciążenie komponentu odczytującego dane do możliwości systemu.

Jednak w tym przypadku spotkamy jeszcze jeden problem. Jeżeli redukcja ma być nieblokująca, to musimy jakoś pogodzić pisanie do mapy przez wiele wątków. Chcemy uniknąć sytuacji, gdy dwa wątki modyfikują wartość pod tym samym kluczem. W tym celu Flow udostępnia funkcję partition, która gwarantuje, że dany klucz będzie „przywiązany” do wątku. Na koniec wystarczy zsumować mapy. Jest to bardzo proste, ponieważ wiemy, że zawierają unikalne klucze.

Podsumowanie

Ok, ale co w tym dziwnego? Zauważcie, że do naszego problemu podeszliśmy tak, by na początek zaaplikować rozwiązanie najprostsze. Następnie komplikowaliśmy rozwiązanie, jednocześnie uzyskując nowe możliwości i lepsze rezultaty. Warto tak działać, ponieważ w ten sposób minimalizujemy szansę na popełnienie błędu. Niestety zauważyłem, że coraz więcej osób chce od razu mieć rozwiązanie, które jest skomplikowane. Jednak produkt końcowy zawiera dużo dziwnych błędów.

Na koniec małe wizualne porównanie działania poszczególnych rozwiązań. Na dole zrzut z htopa. Erlang śmiga na rdzenie od 1 do 4. Na rdzeniach 7 i 8 śmiga nagrywanie pulpitu. Obserwujcie, jak dużo pamięci jest zajęte. Całość odpalać w wysokich detalach na pełnym ekranie.

Pliki w tym tekście to dumpy wikipedii, zanim jednak pobierzesz jakiś plik weź pod uwagę możliwości swojego komputera 😉

Kod znajdziecie tu.

Zmiany, zmiany, zmiany….

Zmiana jest w sumie jedna, ale za to duża. Od dziś cały blog będzie śmigał po https. Co to oznacza? Od strony użytkownika nic się nie zmienia, ponieważ jest też odpowiednia konfiguracja i wejście przez http, przekieruje was w odpowiednie miejsce. Powinno działać. Jeżeli jednak macie jakieś problemy albo przeglądarka krzyczy o niebezpiecznych zasobach, to dajcie znać.

Trochę o mierzeniu kodu z jmh

jmh jest leciwym narzędziem. Jakoś w marcu stuknie mu już 4 lata. Ostatnio zwróciłem na nie uwagę przy okazji rozwiązywania problemu „czyj kod powinniśmy użyć, nasz czy hindusów”. Wiadomo, że do rozwiązywania tego typu problemów najlepiej sprawdzają się jakieś w miarę niezależne od nas narzędzia. Jako że pisanie mikro benchmarków jest sztuką, narzędzie powinno ładnie zamykać skomplikowane elementy w przyzwoicie wyglądające API. Powinno też umieć wyliczyć jakieś statystyki w rozsądny sposób.

No właśnie statystyki. W przypadku większość benchmarków samo wyznaczenie wartości mierzonej jest niewystarczające. Co prawda na poziomie newsa prasowego możemy przeboleć brak informacji o dokładności pomiaru, ale już na poziomie naszego produktu powinniśmy mieć pełnię wiedzy o pomiarach. Dlatego właśnie jmh zdaje się być dobrym narzędziem.

Krótka powtórka z Pracowni Fizycznej I

Na studiach miałem kilka przedmiotów laboratoryjnych. Zaczynało się to jakoś na trzecim semestrze od przedmiotu o nazwie Pracownia Wstępna, gdzie następował odsiew na podstawie umiejętności zrozumienia tekstu polecenia. Następnie były wa semestry zajęć Pracownia Fizyczna, a na koniec była jeszcze Pracownia Elektroniczna. Te ostatnie zajęcia nie były obowiązkowe. Elementem łączącym te zajęcia, poza osobami prowadzących, był wymóg zrozumienia jak należy przeprowadzać badania.

Co mierzymy?

Jest to pierwsze pytanie, na które musimy sobie odpowiedzieć. Jeżeli nie jesteśmy, w stanie określić, co chcemy zmierzyć, to przystępowanie do pomiarów nie ma sensu. Co więcej, należy określić bardziej abstrakcyjny cel pomiaru. Przykładowo, jeżeli mierzę wydajność mojego kodu, to muszę powiedzieć, wobec czego ta wydajność jest mierzona. Wobec innej wersji kodu? W kontekście specyficznych danych? Czy też jest to pomiar wstępny mający za zadanie określić z wielkościami, jakiego rzędu mamy do czynienia?

Jak mierzymy?

Drugie pytanie dotyczy wybranej metody pomiaru. Zazwyczaj daną wielkość można zmierzyć na wiele sposobów. Zapewne spotkaliście się z historią jak, to Ernest Rutherford egzaminował młodego Nilsa Bohra. Podobnie ma się sprawa z każdym pomiarem. Jeżeli chcę zmierzyć wydajność jakiegoś kodu, to muszę określić, w jaki sposób będę ją mierzyć. Czy mierzę czas pojedynczej operacji? Czy też zliczam ilość operacji w przedziale czasu? Czy pomiar prowadzę w środowisku kontrolowanym, czy swobodnym? Jak duża jest próba? W końcu, co ważne w środowisku JVM (i każdym innym z JIT), jak wygląda rozgrzewka maszyny?

Jakie są wady i zalety wybranej metody pomiarowej?

Jak już wybiorę odpowiednią metodę, to należy uzasadnić ten wybór. Przykładowo, mam fragment kodu, który będzie często wykorzystywany na produkcji. Wybrałem metodę pomiaru liczby operacji w ciągu sekundy z rozgrzewką. Pomiar prowadzę w środowisku izolowanym (fizyczna maszyna przeznaczona do tego typu testów). Zaletą tej metody duża precyzja wynikająca z izolacji. Jako że wykonam najpierw rozgrzewkę, to JIT dokona odpowiednich kompilacji. W efekcie otrzymam pomiar precyzyjny oraz powtarzalny (obarczony małym błędem). Jednak metoda ta nie pozwala nam na określenie, czy w środowisku produkcyjnym wydajność będzie podobna. Wynika to z izolacji testów i tym samym braku możliwości odtworzenia warunków produkcyjnych. Czy jest to poważna wada? Zapewne tak, jeżeli przyjmiemy, że od wyników testów zależy wdrożenie naszego kodu. Czy kod spełnia założone parametry?

Jak określić niedokładność metody i błąd pomiaru?

Z wadami wiąże się jeszcze jeden temat. O ile sama identyfikacja wad może być prosta, to już określenie, w jakim stopniu wpływają one na wyniki, jest trudniejsze. Po pierwsze musimy określić, jak dalece niedokładną metodę stosujemy. Zazwyczaj oznacza, to określenie granulacji mierzonej wartości. Liczba operacji na sekundę jest świetnym przykładem. Wiemy jakiej metryki używamy i jaka jest minimalna mierzalna wartość. Potrafimy też wskazać jakie czynniki wpływają na sam pomiar np. dokładność System.nanoTime. Uzbrojeni w taką wiedzę możemy wyliczyć błąd pojedynczego pomiaru albo odchylenie standardoweW.
Na koniec czeka nas jeszcze jedno drobne zadanie. Musimy zweryfikować, czy sposób pomiaru, nie wpłynął na wartość mierzoną. Następnie musimy uwzględnić, to w naszym rachunku błędów.

Jak już przebrniemy przez powyższe zagadnienia, to możemy przystąpić do pomiarów.

Rola jmh

Gdzie można umieścić jmh? Narzędzie to ułatwia nam tworzenie benchmarków, ponieważ pozwala w łatwy sposób zdefiniować sam pomiar. Co więcej, na zakończenie wyliczy za nas odpowiednie wartości związane z błędami pomiarów. Tym samym nie musimy samodzielnie rzeźbić kodu do matematyki. Dostarcza nam też odpowiednich narzędzi pomiarowych. Zatem nasze zadanie ogranicza się jedynie do określenia co i w jaki sposób chcemy zmierzyć. Oczywiście po naszej stronie jest też odpowiedź na pytanie, czy wybrana metoda ma sens.

Praktyka

Po tym przydługim wstępie teoretycznym przejdźmy do praktyki. W jej ramach porównamy sobie cztery różne implementacje operacji sumowania elementów kolekcji/streamu.

  • Sumowanie z wykorzystaniem pętli for
  • Sumowanie z wykorzystaniem stream
  • Sumowanie z wykorzystaniem parallel stream
  • Sumowanie z wykorzystaniem ForkJoin Framework

Żeby nie było prosto, w naszym kodzie wykorzystamy BigInteger, a następnie porównamy wyniki z kodem zaimplementowanym z użyciem long.

Sam kod wygląda następująco.

Listing 1. Implementacja w oparciu o BigInteger

public class BigIntBenchmark {

    @Benchmark
    public void loopWithSum(Blackhole blackhole) {
        BigInteger sum = new BigInteger("0");

        for (int i = 0; i < MAX; i++) {
            sum = sum.add(new BigInteger(i + ""));
        }
        blackhole.consume(sum);
    }

    @Benchmark
    public void streamWithSum(Blackhole blackhole) {
        blackhole.consume(IntStream.range(0, MAX)
                .mapToObj(i -> new BigInteger(i + ""))
                .reduce(new BigInteger("0"), BigInteger::add));
    }

    @Benchmark
    public void pStreamWithSum(Blackhole blackhole) {
        blackhole.consume(IntStream.range(0, MAX).parallel()
                .mapToObj(i -> new BigInteger(i + ""))
                .reduce(new BigInteger("0"), BigInteger::add));
    }

    @Benchmark
    public void fjWithSum(Blackhole blackhole) {
        ForkJoinPool pool = new ForkJoinPool();
        blackhole.consume(pool.invoke(new BigIntFJT(0, MAX)));
    }

}


class BigIntFJT extends RecursiveTask<BigInteger> {

    private final int start;
    private final int end;
    private final int MAX = 1_000;


    public BigIntFJT(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected BigInteger compute() {
        if (end - start <= MAX) {
            BigInteger sum = new BigInteger("0");

            for (int i = start; i < end; i++) {
                sum = sum.add(new BigInteger(i + ""));
            }

            return sum;
        }
        BigIntFJT fjt1 = new BigIntFJT(start, start + ((end - start) / 2));
        BigIntFJT fjt2 = new BigIntFJT(start + ((end - start) / 2), end);

        fjt1.fork();
        fjt2.fork();
        return fjt1.join().add(fjt2.join());
    }
}

To samo w przypadku użycia long wygląda następująco:

Listing 2. Implementacja w oparciu o long

public class LongBenchmark {

    @Benchmark
    public void loopWithSum(Blackhole blackhole) {
        long sum = 0L;

        for (int i = 0; i < MAX; i++) {
            sum += i;
        }
        blackhole.consume(sum);
    }

    @Benchmark
    public void streamWithSum(Blackhole blackhole) {
        blackhole.consume(LongStream.range(0, MAX).sum());
    }

    @Benchmark
    public void pStreamWithSum(Blackhole blackhole) {
        blackhole.consume(LongStream.range(0, MAX).parallel().sum());
    }

    @Benchmark
    public void fjWithSum(Blackhole blackhole) {
        ForkJoinPool pool = new ForkJoinPool();
        blackhole.consume(pool.invoke(new BigIntFJT(0, MAX)));
    }
}


class LongFJT extends RecursiveTask<Long> {

    private final int start;
    private final int end;
    private final int MAX = 1_000;


    public LongFJT(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Long compute() {
        if (end - start <= MAX) {
            long sum = 0L;

            for (int i = 0; i < MAX; i++) {
                sum += i;
            }

            return sum;
        }
        LongFJT fjt1 = new LongFJT(start, start + ((end - start) / 2));
        LongFJT fjt2 = new LongFJT(start + ((end - start) / 2), end);

        fjt1.fork();
        fjt2.fork();
        return fjt1.join() + fjt2.join();
    }
}

Jednym odstępstwem od normy jest wykorzystanie Long w przypadku ForkJoin, bo to typ generyczny.

Nasz benchmark jest już gotowy. Możemy go skompilować za pomocą mavena i uruchomić. Będzie działać w domyślnych ustawieniach. Warto jednak rzucić okiem w bebechy naszego kodu.

@Benchmark i Blackhole

Adnotacja @Benchmark wskazuje na metody, które są mierzone. Dla nich zostanie wygenerowane odpowiednie środowisko i wykonane pomiary. Warto zwrócić uwagę, na fakt, że w trakcie kompilacji jmh generuje dodatkowe klasy, które są właściwym pomiarem. W nich właśnie dzieje się cała pomiarowa magia.

Czym jest Blackhole? Żeby zrozumieć zadanie tej klasy, należy najpierw sięgnąć w głąb JVM. Jak uruchamiamy maszynę wirtualną, to jednym z elementów, które wstają praktycznie na samym początku, jest JIT. Narzędzie to śledzi kod pod kątem "gorących metod". Po angielsku hot spots stąd też nazwa maszyny Suna. Jeżeli zidentyfikuje on takie miejsce, to następuje proces kompilacji bytecodu do kodu maszynowego. Dzięki temu kod ten będzie wykonywany bezpośrednio, z pominięciem interpretera. Jednakże JIT potrafi też kilka innych rzeczy. Potrafi optymalizować kod. Przyjrzyjmy się naszej metodzie sumującej kolejne liczby w pętli, ale z lekką modyfikacją:

Listing 3. Metoda loopWithSum

public void loopWithSum(Blackhole blackhole) {
    BigInteger sum = new BigInteger("0");
    
    for (int i = 0; i < MAX; i++) {
        sum = sum.add(new BigInteger(i + ""));
    }
    // blackhole.consume(sum);
}

Co dzieje się ze zmienną sum na koniec działania kodu? Nic się nie dzieje. Znika ona ze stosu i nie jest nigdzie zapisywana. Zatem można pominąć wszystkie jej modyfikacje w kodzie, bo są bezproduktywne. Oczywiście w tym przypadku nie jest tak do końca, bo jednak JIT nie wie, co dzieje się w BigInteger i nie może na pałę dokonać optymalizacji. Jednak w przypadku kodu z long nic nie stoi na przeszkodzie, by po prostu usunąć ciało metody.
Jak temu można zapobiec? Na dwa sposoby. Pierwszy to zwracanie wartości z naszej metody. Osobiście jestem na nie, ponieważ chciałbym tworzyć kod, który jest spójny z testami. Drugi sposób to przekazać rezultat gdzieś w świat. W tym przypadku wrzucić go do czarnej dziury. Takie coś wystarczy, by JIT nie dokonał eksterminacji naszego kodu.

Konfigurowanie pomiarów

Jak już wspomniałem, uruchomienie naszego kodu odbędzie się z wartościami domyślnymi. Każda metoda zostanie uruchomiona dwadzieścia razy w ramach rozgrzewki, następnie dwadzieścia razy w ramach właściwych pomiarów, a cały cykl zostanie powtórzony dziesięć razy. Sam pomiar będzie domyślnie zajmować jedną sekundę. Jak łatwo obliczyć cały test zajmie nam około 53 minut (2 klasy * 10 cykli * 4 metody per klasa w każdym cyklu * 40 pomiarów i rozgrzewek). Trochę dużo...

Konfiguracji możemy dokonać na trzy sposoby. Po pierwsze z linii poleceń możemy ustawić odpowiednie parametry. Uruchomienie naszego pomiaru z przełącznikiem -h wyświetli listę i RTFMW. Jest to niezła metoda, jeżeli chcemy coś na szybko odpalić z jakimiś nietypowymi ustawieniami. Parametry te nadpisują konfigurację w kodzie. Drugą metodą jest wspomniana przed chwilą konfiguracja w kodzie:

Listing 4. Przykładowa konfiguracja w kodzie

@Benchmark
@Warmup(iterations = LOOP, time = TIME, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations = LOOP, time = TIME, timeUnit = TimeUnit.MILLISECONDS)
@Fork(LOOP)
public void loopWithSum(Blackhole blackhole) {
    BigInteger sum = new BigInteger("0");

    for (int i = 0; i < MAX; i++) {
        sum = sum.add(new BigInteger(i + ""));
    }
    blackhole.consume(sum);
}

Ten rodzaj konfiguracji sprawdzi się najlepiej, jeżeli chcemy śledzić zmiany. W takim przypadku dodanie konfiguracji do kodu pozwoli nam na zarządzanie zmianami za pomocą na przykład gita. Trzecią metodą jest użycie klasy OptionBuilder. W tym przypadku z poziomu naszego kodu możemy uruchomić pomiar, konstruując odpowiedni zestaw parametrów. Jest to najlepsza opcja, jeżeli chcemy zintegrować jmh z serwerem CI.

Rodzaje pomiarów

jmh domyślnie wykonuje pomiar wydajności wyrażonej w liczbie operacji na sekundę. Tego typu pomiary są najpopularniejsze i sprawdzą się w większości przypadków. Jeżeli jednak chcemy zmierzyć inne wartości, to musimy wykorzystać adnotację @BenchmarkMode z odpowiednim parametrem:

  • Throughput – wydajność ops/s domyślny pomiar.
  • AverageTime – średni czas wykonania operacji. Jest to odwrotność poprzedniego.
  • SampleTime – sampling. Uruchamia test i losowo co pewien czas mierzy czas wykonania metody.
  • SingleShotTime – pomiar pojedynczego wywołania. Bez rozgrzewki.
  • All – wszystkie pomiary, ale w ramach osobnych cykli.

Wybierając odpowiedni tryb, należy zwrócić uwagę na to, co mierzymy. Średni czas wykonania metody raczej jest słabą metryką w przypadku prostych metod jak nasze. Z drugiej strony może być świetnym rozwiązaniem w przypadku metod, które wykonują złożone operacje na dużych zbiorach danych.

Podsumowanie

Czas na podsumowanie. jmh daje nam całkiem duże możliwości, jeśli chodzi o pomiar wydajności. Jest to narzędzie, które odciąża nas w zakresie przygotowania maszyny wirtualnej (rozgrzewka), poprawności pomiaru (mierzy różne rzeczy) oraz statystyki (dostajemy odchylenie jako element wyniku). Warto wdrożyć to narzędzie do naszego arsenału.

Na koniec jeszcze jedna uwaga. Przemyślcie dokładnie co i jak chcecie mierzyć, ponieważ źle zaprojektowany pomiar może prowadzić do bardzo dziwnych wniosków i tym samym problemów na produkcji. Co gorsza, źle wykonany pomiar podważy zaufanie do narzędzia, bo przecież mierniczy się nie myli 😉

Kod dostępny tu

Lambda Days 2017 – czego tu nie było…

Tegoroczna edycja Lambda Days pokazała, że programowanie funkcyjne ściąga pod swoje skrzydła coraz większą rzeszę programistów. Ponad 400 uczestników, trzy ścieżki, dwa dni. To jest już coś. W tej relacji nie chcę omawiać poszczególnych wystąpień. Klasyczna formuła ta nie do końca pasuje do mojego uczestnictwa w konferencji. W zamian pozwolę sobie na podzielenie relacji według tematyki.

Ścieżka historyczna

Trzy prezentacje poświęcone historii. John Hughes i Mary Sheeran pokazali, w jaki sposób programowanie funkcyjne wpływało na rozwój inżynierii oprogramowania. Jak zazębiały się pewne koncepcje naukowe i praktyka rozwoju oprogramowania. Następnie Sydney Padua omówiła historię Ady Lovelace i Charlesa Babbage. Było to o tyle ciekawe, że Sydney jest grafikiem, a nie programistą. Jest też autorką serii komiksów o Lady Adzie. Drugiego dnia David Turner opowiedział nam historię języków funkcyjnych. Cały wykład był poprowadzony w ciekawy sposób. Słuchając go, można było zrozumieć decyzje kolejnych pokoleń programistów.

Dwie poświęcone wpływowi inżynierii na historię. Heather Miller, która pokazała, w jaki sposób ruch open source zmienił nasze postrzeganie świata. A Brian L. Troutwine w keynocie otwierającym drugi dzień konferencji omówił, jak polityka ma wpływ na decyzje podejmowane w projektach. I jakie są tego konsekwencje.

Ścieżka Elixira

Najpierw trafiłem na prezentację Jose Valima o GenStage i Flow. Prezentacja bardzo fajna, ale zabrakło mi trochę fajerwerków. Następnie Wojciech Turek mówił o symulacji ruchu samochodowego z wykorzystaniem Erlanga. Całość na superkomputerze Prometeusz. Drugiego dnia Tomek Kowal pokazał nam, jak używa Elixira w aplikacji finansowej.

Cała reszta, czyli wypełniacze

Wypełniaczy pierwszego dnia dużo nie było. Troels Henriksen omówił krótko Futhark. Język programowania współbieżnego dla kart graficznych. Następnie Annette Bieniusa zrobiła wprowadzenie do haskellowego BPMa. Serio, to był BPM, który wykorzystywał system typów z Haskella. Drugi dzień to troll prezentacja Jonas Winje i Einar Høsta o praktycznym rachunku lambda z wykorzystaniem Emaca. Kontr-troll z publiczności już w pierwszym pytaniu – a co z użytkownikami vim? Następnie Paweł Szulc zrobił wprowadzenie do schematów rekursji w scali. A całość zakończyłem prezentacją Roba Martina. W sumie nie wiem o czym, bo prezentacja zaczęła się nieźle, ale potem był chaos.

Podsumowanie

Konferencja była całkiem udana. Tradycyjnie odstałem w kolejce po paszę, ale ta była super smaczna. A za rok wyślę coś o elixirze 🙂

Seksizm z certyfikatem koszerności

Była sobie aferka z kalendarzem Sii. Była i przebrzmiała. Znajome z IT albo nie widziały w tym nic złego, albo wręcz przeciwnie. Widziały w tym całe spektrum opresji. Ta druga grupa dodatkowo twierdziła, że ta pierwsza grupa jest ślepa. Padło też określenie SeksizmW. Rzecz w tym, że część pań ma mentalność Mei (jak uważałeś w szkole, to wiesz, o kogo chodzi).

W dużym skrócie – jak naszym zdaniem ktoś robi coś, czego mu zazdrościmy, to będzie to seksizm i dyskryminacja. Na przykład ładniejsza koleżanka trafiła na okładkę firmowego kalendarza, a ja się nawet nie zgłosiłam do tej zabawy. Chuj, że koleżanka sama chciała. Dobrze się bawiła i było super. Swoją zazdrość co do jej sukcesu, przeniosę na organizatora/pomysłodawcę/jakiegokolwiek samca związanego z danym wydarzeniem.

Jeżeli jednak ktoś jawnie dyskryminuje ze względu na płeć, tak że mi kobiecie jest lepiej, to nie jest to seksizm. Takie zachowanie jest OK, otrzymać może certyfikat koszerności.

Dlaczego o tym piszę? Ponieważ właśnie w ten sposób postępują organizatorzy WebSummit w Lizbonie. Za samo to, że jesteś kobietą, otrzymujesz 90% zniżki. Hm… gdzie się podziały wszystkie obrończynie równości ze względu na płeć?

Swoją drogą ciekawe co by się stało, jakbym się tam zarejestrował. Albo zrobił taką promocję dla panów na jakiejś konferencji. Ciekawe…

ps. post nie trafia do kategorii programowanie, ponieważ nie chcę zaśmiecać maila z jvm-bloggers. Choć może powinienem. Powinienem.

Kotlin z JPA – rzeczy nieoczywiste

Zgodnie z tradycją kończąc szkolenie, pokazuję coś ekstra. Dziś tym czymś ekstra była implementacja prościutkiego silnika blogowego. Samo zadanie jest „egzaminem końcowym” kursu JPA. Nie tworzymy niczego ambitnego, bo mamy tylko 4 godziny na to zadanie. Uczestnicy mają samodzielnie przygotować klasy, skonfigurować zależności i odpalić całość. Moja wersja różniła się jednak od tej, którą mieli wykonać uczestnicy. Ja pisałem w Kotlinie.

Wprowadzenie

Na stronie Springa jest bardzo krótkie wprowadzenie do pracy z Kotlinem i JPA. Po jego lekturze możemy czuć się „mocni”, bo tak naprawdę nie ma tam nic odkrywczego. Nie do końca.

Nasz model danych składać się będzie z kilku klas:

  • Author – reprezentuje autora bloga. Taki debilny value object.
  • Blog – będzie zawierać nazwę bloga oraz autora.
  • BlogPost – pojedynczy post zawierający datę, treść, odwołanie do bloga.

Do tego mamy jeszcze klasę DomainObject, w której zdefiniujemy zasady nadawania id oraz wersji. Prościej się nie da.

Dodatkowo chciałem pokazać użycie REST Repositories, których opis znajdziecie tu oraz Actuatora, o którym poczytacie tu. Warstwę prezentacji na chwilę obecną olejmy, zrobi się później za pomocą Angulara czy innego Reacta. Przy czym w tym poście skupię się na elementach związanych encjami.

Aplikacja ma pozwalać na zarządzanie autorami, blogami i postami. Wykorzystując REST Repositories, nie będziemy musieli dużo pisać. Podstawowe operacje są już zmapowane na odpowiednie metody HTTP. Dodatkowo oczekujemy od niej, że będzie można wykonywać proste zapytania wykorzystujące Criteria API. Będziemy musieli zatem dopisać jakiś prościutki serwis, który nam to umożliwi.

Zaczynamy

Używając Spring Initializr, czy to z poziomu IDE, czy to za pośrednictwem strony wygenerowałem pakiet startowy. Projekt jest mavenowy. Po zaimportowaniu do Idei spróbowałem go uruchomić i od razu mała niespodzianka:

Listing 1. Co my tu mamy…

Exception in thread "main" java.lang.NoSuchMethodException: com.luxoft.jva014.blog.Jva014BlogApplication.main([Ljava.lang.String;)
	at java.lang.Class.getMethod(Class.java:1786)
	at com.intellij.rt.execution.application.AppMain.main(AppMain.java:126)

Problem, który tu mamy nie wynika z błędu w konfiguracji startera. Ma on swoje źródło w sposobie, w jaki działa Idea. Rzućmy okiem na kod:

Listing 2. Konfiguracja startowa

@SpringBootApplication
class Jva014BlogApplication

fun main(args: Array<String>) {
    SpringApplication.run(Jva014BlogApplication::class.java, *args)
}

Rzecz w tym, że Idea będzie chciała uruchomić klasę z adnotacją @SpringBootApplication. Klasa ta nie posiada metody main, ponieważ ta jest zdefiniowana jako funkcja. Jest to absolutnie prawidłowe zachowanie, ponieważ Kotlin zamiast metod statycznych wprowadza funkcje „globalne”. Funkcja main w wyniku kompilacji trafi do klasy Jva014BlogApplicationKt. Zatem musimy uruchamiać tę klasę. Nie pozostaje nam zatem nic innego jak zgłosić buga. Nie jest to jednak problem, ale raczej ciekawostka.

Jak już uruchomimy naszą aplikację, to mając na pokładzie Actuatora, możemy sprawdzić jej stan. Wystarczy otworzyć localhost:8080/health i bangla.

Encje

Czas coś zaimplementować. Zacznijmy zabawę od DomainObject, która zawiera id i wersję. Implementacja jest banalnie prosta:

Listing 3. Implementacja DomainObject

@MappedSuperclass
class DomainObject(
        @Id var id: UUID = UUID.randomUUID(),
        @Version var version: Int = 0
)

Pominąłem implementację hashCode i equlas, ale ta jest. Pozostałe encje wyglądają podobnie:

Listing 4. Pozostałe encje modelu

@Entity
@AttributeOverrides(
        AttributeOverride(name = "id", column = Column(name = "AUTHOR_ID")),
        AttributeOverride(name = "version", column = Column(name = "AUTHOR_VERSION"))
)
@Table(name = "AUTHOR")
class Author(var name: String = "") : DomainObject()

@Entity
@NamedQuery(name = "findAll", query = "select B from Blog B order by B.name")
@AttributeOverrides(
        AttributeOverride(name = "id", column = Column(name = "BLOG_ID")),
        AttributeOverride(name = "version", column = Column(name = "BLOG_VERSION"))
)
@Table(name = "BLOG")
class Blog(

        @Column(name = "BLOG_NAME")
        val name: String = "",

        @JoinColumn(name = "BLOG_AUTHOR")
        @OneToOne(cascade = arrayOf(CascadeType.ALL))
        var author: Author = Author(),

        @OneToMany(mappedBy = "blog", fetch = FetchType.EAGER, cascade = arrayOf(CascadeType.ALL))
        var posts: MutableList<BlogPost> = ArrayList<BlogPost>()
) : DomainObject()

@Entity
@NamedQuery(name = "findByBlog", query = "select P from BlogPost P where P.blog.id = :id")
@AttributeOverrides(
        AttributeOverride(name = "id", column = Column(name = "POST_ID")),
        AttributeOverride(name = "version", column = Column(name = "POST_VERSION"))
)
@Table(name = "BLOG_POST")
class BlogPost(
        @Column(name = "POST_DATE")
        var date: Date = Date(),

        @Column(name = "POST_TITLE")
        var title: String = "",

        @Lob
        @Basic(fetch = FetchType.EAGER)
        @Column(name = "POST_TEXT")
        var text: String = "",

        @ManyToOne(fetch = FetchType.EAGER)
        @JoinColumn(name = "BLOG_ID")
        var blog: Blog = Blog()
) : DomainObject()

Uwaga. Wysokie stężenie AttributeOverride wynika z założeń zadania, które mieliśmy wykonać. W rzeczywistości można to olać.

Coś jednak tutaj nie pasuje. Zresztą uważny czytelnik dostrzeże, to już na Listingu 2. Klasy w Kotlinie są domyślnie finalne. Tu musimy omówić kilka kwestii. Kiedyś już pisałem o data class w kontekście JPA. DC choć zacne, to do pracy z JPA średnio się nadają. Z jednej strony finalne, a z drugiej nie mogą nic rozszerzać. Świetnie sprawdzą się jako DTO, ale nie pełnoprawne encje. Tu mamy jednak do czynienia ze zwykłą klasą. Jakim cudem mogę użyć takiej klasy jako niefinalnej? Ano mogę. Z pomocą przychodzi mi plugin do kompilatora o wdzięcznej nazwie allopen. Pozwala on na zdefiniowanie listy adnotacji, których użycie na klasie spowoduje wygenerowanie klasy otwartej. Gdy tworzymy projekt za pomocą springowego inicjalizera, to plugin ten zostanie dodany (pod nazwą kotlin-spring) i skonfigurowany tak, by otwierać klasy, które muszą być niefinalne dla springa. My musimy użyć jeszcze innej wersji tego pluginu pod nazwą kotlin-jpa, ale…

I dupa – czas na przesiadkę na gradle

Problem z JPA i Kotlinem polega na tym, że jeżeli używamy Mavena, to nie mamy od ręki dostępu do wielu ważnych rzeczy. Najistotniejszym brakiem jest brak procesora adnotacji, który potrafiłby poradzić sobie z klasami kotlinowymi. Jest co prawda kapt, ale ten działa tylko z gradlem. Tracimy zatem możliwość automatycznego generowania metamodelu. Rykoszetem dostajemy też w przypadku otwierania klas, bo nie możemy użyć kotlin-jpa, bo nie ma wersji na mavena, a musimy ręcznie skonfigurować wszystko w pom.xml.

Przy czym warto zaznaczyć, że problem ten wynika z niedopasowania narzędzia. Kotlin jest zdecydowanie zorientowany gradlowo i trzeba mieć, to na uwadze.

Mapowania

Tworząc mapowania pomiędzy encjami, nie musimy się ograniczać. Jeżeli tylko mamy podpięty plugin allopen, to pracujemy ze zwykłymi kotlinowymi klasami. Ciekawostką jest to, że allopen otwiera też dc. Jednakże próba kompilacji kodu, w którym dc dziedziczą po sobie, skończy się błędem.

Rzućmy jeszcze raz okiem na encję Blog, ponieważ zawiera ona kilka ciekawostek:

Listing 5. Encja Blog

@Entity
@NamedQuery(name = "findAll", query = "select B from Blog B order by B.name")
@AttributeOverrides(
        AttributeOverride(name = "id", column = Column(name = "BLOG_ID")),
        AttributeOverride(name = "version", column = Column(name = "BLOG_VERSION"))
)
@Table(name = "BLOG")
class Blog(

        @Column(name = "BLOG_NAME")
        val name: String = "",

        @JoinColumn(name = "BLOG_AUTHOR")
        @OneToOne(cascade = arrayOf(CascadeType.ALL))
        var author: Author = Author(),

        @OneToMany(mappedBy = "blog", fetch = FetchType.EAGER, cascade = arrayOf(CascadeType.ALL))
        var posts: MutableList<BlogPost> = ArrayList<BlogPost>()
) : DomainObject()

Pierwszą rzeczą na, którą chciałbym zwrócić uwagę, to wykorzystanie typu Author? zamiast Author. Wynika to z zasady generowania konstruktora domyślnego. Jeżeli wszystkie pola w konstruktorze podstawowym są zainicjowane wartościami domyślnymi, to zostanie wygenerowany konstruktor domyślny. Będzie on publiczny. Po drugie możemy używać zarówno var, jak i val. Przy czym trzeba pamiętać, że pole oznaczone jako val będzie finalne. Po trzecie w przypadku mapowania kolekcji musimy użyć MutableList zamiast List. Kolekcje niezmienne w Kotlinie są co do zasady kowariantne, czyli mówiąc językiem kodu:

Listing 6. Lista niezmienna

@OneToMany(mappedBy = "blog", fetch = FetchType.EAGER, cascade = arrayOf(CascadeType.ALL))
var posts: List<BlogPost> = ArrayList<BlogPost>()

Wygeneruje nam:

Listing 7. Lista niezmienna w bytecodzie

Compiled from "Jva014BlogApplication.kt"
public class pl.koziolekweb.blog.Blog extends pl.koziolekweb.blog.DomainObject {
  //...
  private java.util.List<? extends pl.koziolekweb.blog.BlogPost> posts;
  //...
  public java.util.List<pl.koziolekweb.blog.BlogPost> getPosts();
  public void setPosts(java.util.List<? extends pl.koziolekweb.blog.BlogPost>);
  //...
}

Co przy próbie uruchomienia zakończy się klasycznym błędem ze strony Hibernate:

Listing 8. Lista niezmienna w runtime

Caused by: org.hibernate.AnnotationException: Collection has neither generic type or OneToMany.targetEntity() defined: pl.koziolekweb.blog.Blog.posts
	at org.hibernate.cfg.annotations.CollectionBinder.getCollectionType(CollectionBinder.java:694) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
	at org.hibernate.cfg.annotations.CollectionBinder.bind(CollectionBinder.java:488) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
	at org.hibernate.cfg.AnnotationBinder.processElementAnnotations(AnnotationBinder.java:2140) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]
	at org.hibernate.cfg.AnnotationBinder.processIdPropertiesIfNotAlready(AnnotationBinder.java:911) ~[hibernate-core-5.0.11.Final.jar:5.0.11.Final]

Oczywiście zmiana kodu z listingu 6 na poniższy:

Listing 9. Lista niezmienna

@OneToMany(mappedBy = "blog", fetch = FetchType.EAGER, cascade = arrayOf(CascadeType.ALL), targetEntity = BlogPost::class)
var posts: List<BlogPost> = ArrayList<BlogPost>()

Nie zmienia nic w bytecodzie, ale za to błąd jest z serii „magicznych”:

Listing 10. Dodanie targetEntity

Caused by: org.hibernate.annotations.common.AssertionFailure: Fail to process type argument in a generic declaration. Member : pl.koziolekweb.blog.Blog#posts Type: class sun.reflect.generics.reflectiveObjects.WildcardTypeImpl
	at org.hibernate.jpa.internal.metamodel.AttributeFactory$PluralAttributeMetadataImpl.getClassFromGenericArgument(AttributeFactory.java:875) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]
	at org.hibernate.jpa.internal.metamodel.AttributeFactory$PluralAttributeMetadataImpl.<init>(AttributeFactory.java:784) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]
	at org.hibernate.jpa.internal.metamodel.AttributeFactory$PluralAttributeMetadataImpl.<init>(AttributeFactory.java:758) ~[hibernate-entitymanager-5.0.11.Final.jar:5.0.11.Final]

Dlaczego? Ponieważ Kotlin wymaga typu KClass (klasy kotlinowej) w miejsce zwykłej javowej dla targetEntity. Jedyną opcją pozostaje zatem użycie MutableList. Swoją drogą jest, to babol samego Kotlina, bo na pałę podkłada własny typ zamiast javowego.

Podsumowanie

Czas na małe podsumowanie. Kotlin z JPA ma sens, ale trzeba zwracać uwagę na pewne pułapki. Na pewno należy używać gradle zamiast mavena. Dzięki temu będziemy mieli lepsze wsparcie ze strony narzędzi około kotlinowych. W tym większy wybór, jeśli chodzi o pluginy kompilatora. Jak pokazuje nam przykład z targetEntity, Kotlin ogranicz nas w pewnych sytuacjach, uniemożliwiając użycie wszystkich elementów JPA.

Za plus należy uznać zwięzłość kodu, którą w Javie uzyskujemy z wykorzystaniem Lomboka.

O czym nie napisałem, ale napiszę, to przede wszystkim REST Repositories i użycie Criteria API. Szczególnie ten drugi temat jest dość rozbudowany, bo język daje nam tu wiele możliwości.