OK
Pożegnanie z Javą 8?

Pożegnanie z Javą 8?

Pożegnanie z Javą 8?

DISCLAIMER: Ten artykuł został stworzony na bazie mojej prezentacji z 2018 roku. Gdy zaczynałem go pisać, Java 10 była najnowszą, zaś gdy kończyłem, Java 11 dopiero co wyszła, wraz ze swoimi nowymi feature’ami i bugami. Przed tą publikacją została lekko zaktualizowana, lecz ciągły rozwój i nowe wersje wychodzące co pół roku sprawiają, że gdy to czytasz, część informacji mogła już stać się nieaktualna. Mimo wszystko zapraszam Cię do zapoznania się z tym artykułem. MACIEK

Ludzie z natury boją się zmian. Często rezygnują z planów czy marzeń, znajdując wymówki, choć jedynym, co stoi im na drodze, jest strach przed nieznanym. „Stare, ale przecież działa, po co to ruszać?”. Takie słowa słychać zarówno w domu, jak i w projekcie. Java 9 i jej moduły są z nami ponad półtora roku, a jednak większość moich znajomych nadal pisze projekty w Javie 8. Dlaczego? Czyżby bali się zmiany?

Nie ma powodu! Dobrze natomiast jest wiedzieć, czego się spodziewać, planując migrację do nowej wersji. Jak się za to zabrać i co mieć na uwadze, gdy do tego siadamy?

Po co w ogóle to robimy?

Z natury jestem przeciwny upgrade’om „na hurra”. Zawsze jestem wersję czy dwie „do tyłu” za najnowszą. I nie dlatego, że jestem leniwy czy zapracowany, ale raczej nieufny. Jak się często okazuje – słusznie. Ale nowa Java już dojrzała i naprawiono już krytyczne błędy. Być może to już moment, w którym jedynym problemem stojącym na drodze do upgrade’u jesteśmy my sami?

Nie będę pisał kolejnego artykułu z serii „po co podbijać wersję Javy?” – Google ma na to pytanie dziesiątki milionów odpowiedzi. Jest jednak kilka ważnych powodów, dlaczego nasz zespół zdecydował się na tę zmianę i dla których, być może, Wasz też powinien.

Po pierwsze

Możecie korzystać z nowych fajnych feature’ów Javy, jak na przykład module-path (pytanie, czy chcecie?), który eliminuje niechciany i nieoczekiwany class shadowing, czy nowej składni, dającej – na przykład – więcej możliwych operacji na streamach, wbudowanego klienta HTTP ze wsparciem dla HTTP/2 i wielu innych.

Po drugie

Będziecie używać wersji, która dłużej będzie miała wsparcie (zakładając, że Wasza aktualnie używana wersja w ogóle jeszcze go ma). I choć ten termin może wydawać się odległy, jeśli płacicie za przedłużone wsparcie, to kiedyś jednak koniec nastąpi.

Po trzecie

Będziecie mieli łatwiejsze update’y do kolejnych wersji wydawanych w nowym, co 6-miesięcznym trybie.

Po czwarte

I ten punkt jest szczególnie ważny dla PMów, PO, leaderów i HRu – Wasz projekt będzie atrakcyjniejszy dla programistów. Do takiego projektu łatwiej zrekrutować nowe osoby, jak i zatrzymać obecnych programistów

I przede wszystkim

Nie chcecie odkładać tego w czasie! Większość z Was i tak będzie musiała to kiedyś zrobić (chociażby ze względu na support), a im wcześniej to zrobicie, tym będzie łatwiej.

Jak się do tego zabraliśmy?

Chyba większość z nas woli praktykę od teorii. Dlatego najlepszy sposób, aby zacząć migrację, to po prostu podbić wersję Javy (w Mavenie, Gradle’u itp.), zmienić JAVA_HOME i zbudować.


O dziwo, build bez testów i budowania obrazu dockerowego przeszedł u nas śpiewająco. Niestety, przy uruchomieniu nie było już tak kolorowo. Po zgooglaniu błędu okazało się, iż serwer, którego używaliśmy, nie wspierał Javy 9+. A jeśli błąd jest w Jirze od ponad roku, to raczej szybko stamtąd nie zniknie.

To była pierwsza lekcja, jaką dała nam ta migracja: niektóre kawałki kodu będzie trzeba przepisać. Wiele bibliotek, od których zależymy, po prostu się zbuntowało i nawet nie pracuje nad dostosowaniem się do nowej Javy. Dobrze jest je szybko zidentyfikować. Najlepiej, jeśli zrobimy to, zanim jeszcze zaczniemy ich używać.

Kubeł zimnej wody, wracamy do początku

„Aby znaleźć jak najwięcej problemów, uruchomiliśmy build po raz kolejny – tym razem bez żadnego „skippowania”. Mvn clean install i… wywaliliśmy się już na pierwszym module.


Okazało się, że biblioteki, z których korzystaliśmy w testach, wymagają zmian (podobnie jak wcześniej serwer) lub w najlepszym razie update’ów do nowszych wersji.


Po podbiciu i drobnych zmianach (w niektórych bibliotekach wraz z nową wersją odbyło się przepakietowanie, gdzie indziej biblioteka została „depraceted” na rzecz nowej i należało ją podmienić) aplikacja zbudowała się, a testy poszły pomyślnie.


To była lekcja numer dwa: Dbajmy o to, aby podbijać zależności. Ktoś mógłby powiedzieć „po co?”, ja zapytam „dlaczego nie?”. Nowe zależności to często łatki bezpieczeństwa, czasem nowe, fajne funkcjonalności, a czasem po prostu możliwość łatwego podbicia czegoś innego, gdy przyjdzie nam to zrobić. Wiem, że z nową wersją można wprowadzić nowy błąd, ale od tego przecież mamy testy.

JDK Incubator

Skoro się kompiluje i testy przechodzą, to chyba skończyliśmy? Niestety, nie do końca. W naszym przypadku dopiero integracja z prawdziwymi systemami po drugiej stronie ukazała kolejną słabość – komunikację po HTTP/2. Wcześniej dostarczone mocki działały w testach, gdyż miały fallback do HTTP 1.1, jednak prawdziwa platforma, z którą się integrowaliśmy – nie.

Z pomocą przyszedł nam klient HTTP, który od wersji 11 jest w standardowym API Javy. W Javie 10 był w tak zwanym incubatorze.

JDK incubator to nowy sposób, podobno „gamechanging”, w jaki wprowadzane są niefinalne API do Javy. Idea jest taka, że moduły z inkubatora przy następnym releasie są zatwierdzane i są pełnoprawnymi modułami Javy, bądź też są z niej usuwane, jeśli nie zostały zatwierdzone. Opieranie się na nich jest więc ryzykowne, gdyż po kolejnym updacie mogą zniknąć.

Jigsaw i Jlink

W tym momencie można śmiało powiedzieć, że zmigrowaliśmy nasz projekt do nowszej Javy, jednak my chcieliśmy więcej: chcieliśmy wprowadzić moduły Javy do naszej aplikacji, tak, aby używać ich zgodnie ze sztuką. Ta historia jednak też nie była tak kolorowa.

Musiałbym napisać osobny artykuł, znacznie dłuższy, żeby opisać całości naszych przejść, jakie mieliśmy z modularyzacją, dlatego przejdę przez ten temat w telegraficznym skrócie.


Na początku próbowaliśmy się pozbyć classpatha i zastąpić go w całości module-pathem. Dodaliśmy module-info do każdego z modułów, jednak nadal nie rozwiązywało to problemów z zależnościami zewnętrznymi. Choć Java daje nam mechanizm „automatycznych modułów”, jego działanie powoduje, że na module-pathie często pojawiają się konflikty. Problem ten nazywany jest „split packages” i często pojawia się, gdy mamy jary „api” i „impl”, jak na przykład JAXB. Używanie zarówno module-patha, jak i classpatha, nie dało nam z kolei korzyści, na które liczyliśmy, gdyż moduły nie mogę odwoływać się do classpatha.


Co więcej, gdy próbowaliśmy zminimalizować wielkość obrazu dockerowego, używając w tym celu multi-stage dockerfile’a i JLinka, okazało się, iż JLink nie może wspierać modułów automatycznych tak, jak robi to module-path. Po pierwszych próbach patchowania zależności aby zażegnać oba powyższe problemy zobaczyliśmy, jak absurdalne jest to zadanie i odpuściliśmy. Całe szczęście, istnieje sposób aby używać JLinka na aplikacjach nie korzystających z modułów. Opisał je w artykule Simon Ritter na portalu medium.com.


Wtedy przyszedł czas na trzecią lekcję, na którą i tak byliśmy gotowi: Keep It Simple, Stupid. Jeśli migrujemy się do nowej Javy, dobrze ograniczyć się do samej migracji, zwłaszcza na początku. Jeśli później będziemy chcieli walczyć z modułami – ok. Ale podstawą jest po prostu kompilacja i uruchamianie na nowym JVM jako nowa Java, niekoniecznie ze wszystkimi jej feature’ami.

Zawsze najnowsza wersja?

W naszym projekcie podbiliśmy się do Javy 10, choć przed pierwszą wersją tego artykułu istniała już wersja 11. Pewnie teraz pomyślicie: gość mówi, aby się upgrade’ować, po czym spoczęli na starej wersji? WTF?.  

Javę 11 też przez chwilę mieliśmy.

Przejście z Javy 10 do Javy 11 jest banalne – zajęło mi ok. 15 minut i to tylko z powodu incubatora. Zmiany miałem przez to w kodzie (pakiety), zależnościach (nazwa) oraz dockerfile’ach (moduły z inkubatora trzeba ręcznie wskazywać). Wycofaliśmy się jednak z tej zmiany, gdyż pierwszy release Javy 11 miał problemy z pewną funkcjonalnością związaną z TLS 1.2., której akurat używaliśmy. Więc musieliśmy cofnąć się, do Javy 10.

Ważne jest, żeby spróbować. Jednak, jak się okazuje, mój wrodzony sceptycyzm tym razem nie był bezpodstawny – najnowsza wersja mnie zawiodła. Ale tak nie musi być w Waszych projektach.

Aktualnie, projekt używa jednego z kolejnych update-ów Javy 11, w którym problem z bezpiecznym połączeniem jest już rozwiązany. Wprawdzie Java 12 jest już dostępna, jednak z dość oczywistych względów postanowiliśmy pozostać przy wersji LTS.

Podsumowanie

Java 9, 10 czy jej dalsze wersje, a moduły, to dwa różne tematy. Te mają nadal wielu krytyków, a nawet wśród ludzi nastawionych neutralnie, często słyszę: Teraz jest za późno, już się nie przyjmie. To trzeba było zrobić 15 lat temu. I chyba muszę im przyznać rację. O modułach postaram się powiedzieć więcej w osobnym artykule.

Ważne jest natomiast, aby nie wylewać dziecka z kąpielą. Nowe wersje Javy dają dużo nowych funkcjonalności, API, no i mają dłuższe wsparcie. Podbicie „w wersji minimum” jest naprawdę szybkie i proste, a korzyści spore, dlatego zdecydowanie polecam zrobić to jak najszybciej.

ul. Jaracza 62
90-251 Łódź
Bitnami