Zrozumieć moduły w Javie 9

Zrozumieć moduły w Javie 9

Próba przedstawienia, w jaki sposób korzystać z najnowszej funkcjonalności Javy 9, systemu modułów, aby nie strzelić sobie i innym w stopę.

sobota, 2 września 2017

Informatyka

System modułów mógłbym nazwać ostatnią wielką funkcjonalnością Javy. Prace nad nim trwają od wielu lat, a projekt był dwukrotnie przekładany z wydania na wydanie. Dla twórców jest on oczkiem w głowie, ponieważ w ich wizji dzięki niemu będzie możliwe przyspieszenie ewolucji języka. Zamiast jednego dużego wydania raz na kilka lat, kolejne wersje Javy miałyby wychodzić nawet dwa razy na rok. Moduły to także największa zmiana w organizacji kodu Javy w historii. Bardzo możliwe, że przed nami jest długi okres przejściowy, kiedy dużo projektów wciąż będzie budowanych w oparciu o classpath, zanim nastąpi ich definitywna migracja. Dotyczy to zwłaszcza bibliotek, gdzie konieczne jest utrzymywanie wstecznej kompatybilności.

Wokół systemu modułów narosło wiele niejasności i nieporozumień dotyczących zarówno jego roli w języku, jak i tego, jak je prawidłowo wykorzystywać. Przyznam się szczerze, że sam jestem dopiero na początku poznawania modułów "w praktyce", natomiast śledzę temat od miesięcy. Mam nadzieję, że przedstawione tutaj podsumowanie - dla mnie będące usystematyzowaniem wiedzy - pomoże Wam wystartować i nie strzelić sobie w stopę.

Czym moduły nie są?

Słowo moduł nie ma w informatyce ściśle sprecyzowanego znaczenia i może oznaczać dwie bardzo odległe od siebie rzeczy. Z tego powodu, a także z powodu obecności na rynku narzędzi oferujących "funkcjonalność modułów" (OSGi, JBoss Modules), w trakcie implementowania projektu Jigsaw powstało bardzo dużo kontrowersji i niedomówień. Poszukując informacji na jego temat trzeba brać poprawkę na to, kto pisze dany artykuł :), tymczasem wydaje mi się, że aby zrozumieć filozofię projektu musimy oderwać się od dotychczasowego sposobu myślenia. Poniżej przedstawiam kilka punktów krytyki.

Jigsaw vs Maven/Gradle

Głosy krytyków:

  • Jigsaw wprowadza nową konwencję nazewnictwa modułów, podczas gdy od wielu lat Maven i Gradle ustanowiły już nazewnictwo 'grupa:artefakt'
  • Deskryptor modułu nie powinien być konstrukcją języka - od tego są systemy budowania
  • Funkcjonalność modułów ma poważne braki, ponieważ w deskryptorze budowania nie można opisać wersji, ani innych meta-danych

Deskryptory modułów wykazują pewne podobieństwa do mechanizmów rozwiązywania zależności oferowanych przez systemy budowania:

module com.zyxist.foo {
   requires com.google.common;
   requires org.slf4j.api;
   exports com.zyxist.foo.api;
}

I na tym podobieństwa się kończą. Zadaniem Mavena/Gradle'a jest kompilacja projektu, zdobycie wszystkich potrzebnych zewnętrznych plików i złożenie z nich działającej paczki do wypuszczenia w świat. Jigsaw nie ma z tym nic wspólnego i rozwiązuje zupełnie inny problem. Zauważmy, że system budowania obsługuje też pliki niebędące archiwami JAR - sam osobiście wykorzystywałem Gradle'a do budowania obrazów baz danych z plików SQL, kompilowania kodu C/C++, generowania obrazów ISO (!), tworzenia archiwów RPM, obrazów Dockera, itd. Wystarczy zrobić coś bardziej zaawansowanego niż przetłumaczyć źródła do postaci binarnej i zapakować je do archiwum, by dostrzec, że system budowania musi zajmować się rzeczami, których nie chcielibyśmy widzieć w systemie modułów. Jigsaw nie wkracza w kompetencje tych narzędzi, a wszystko, czego wymaga się od nich, to rozumieć, jakie pstryczki ustawić w maszynie wirtualnej i kompilatorze, aby wszystko zadziałało.

Jigsaw vs OSGi

Tutaj konflikt był już bardziej poważny, bowiem kompetencje Jigsaw częściowo zazębiają się z kompetencjami istniejących "przemysłowych" rozwiązań. OSGi dostarcza funkcjonalności, których nie ma w Jigsaw, np.:

  • cykl życia,
  • dynamiczne ładowanie modułów,
  • zdalne serwisy.

OSGi to rozwiązanie stworzone z myślą o przypadkach użycia występujących w dużych, monolitycznych aplikacjach. Szczerze mówiąc nie widziałem, aby ktokolwiek stosował je np. do bibliotek programistycznych. Sam także tworzę od lat aplikacje, w których architekturze bez problemu da się wyróżnić coś takiego, jak moduły, lecz nigdy dotąd nie znalazłem się w sytuacji, że naprawdę potrzebowałbym użyć OSGi. Nie wyklucza to istnienia projektów, gdzie OSGi jest potrzebne. Dlatego przewiduję, że Jigsaw i OSGi będą współistnieć obok siebie:

  • Jigsaw jako narzędzie ogólnego zastosowania,
  • OSGi jako kombajn tam, gdzie potrzebna jest ciężka artyleria.

Oczywiście OSGi będzie musiał być świadomy istnienia Jigsaw i prawidłowo z nim współpracować.

Związek modułów z pakietami

Umieściwszy Jigsaw w istniejącym ekosystemie, możemy przyjrzeć się temu, jak go prawidłowo zastosować. Przełomowym momentem dla mnie była lektura artykułów Stephena Colebourne'a:

Z moich poszukiwań wynika, że jest to obecnie najbardziej dojrzałe opracowanie tematu, a artykuły są jednymi z nielicznych, które wykraczają poza schemat "tak wygląda deskryptor modułu" i tłumaczą coś więcej - jak zorganizować cały ekosystem i się nie pozabijać. Warto wziąć poprawkę, że Stephen pisał je w kwietniu i od tamtego czasu niektóre z jego propozycji istotnie stały się oficjalnymi rekomendacjami.

Moduły Jigsaw dodają nowy poziom grupowania do języka:

  • klasa to pojemnik na pola i metody,
  • pakiet to pojemnik na klasy oraz interfejsy,
  • moduł to pojemnik na pakiety.

Oznacza to, że zyskujemy zupełnie nowe miejsce do wprowadzenia kontroli dostępu i ukryć część naszych pakietów przed innymi modułami. Wróćmy do naszego przykładu z początku:

module com.zyxist.foo {
   requires com.google.common;
   requires org.slf4j.api;
   exports com.zyxist.foo.api;
}

Powyższy zapis oznacza, że nasz moduł com.zyxist.foo importuje "publiczne" pakiety z Guavy oraz SLF4j oraz wystawia swoje własne publiczne API. Pisząc kod w obrębie tego modułu nie możemy odwoływać się do wewnętrznych klas oraz interfejsów Guavy, co zabezpiecza nas przed przypadkowym wykorzystaniem czegoś, czego nie dotyczy polityka utrzymywania wstecznej kompatybilności. Atrybut exports pozwala nam zrobić to samo w drugą stronę: gdy wypuścimy nasz moduł w świat, jasno zaznaczamy że dozwolone jest korzystanie wyłącznie z klas stworzonych w tym jednym, konkretnym pakiecie, a pozostałe inne są naszą prywatną sprawą, do której nikt nie powinien się wtrącać. Myślę, że jest to też duża pomoc dla developerów, którzy dopiero zaczynają pracę w zespole i poznają projekt. Teraz wystarczy jeden rzut oka, aby zauważyć, co jest publicznym API, a co nim nie jest i czy należy się pilnować, czy nie.

Nazewnictwo

Tematowi nazewnictwa modułów Stephen Colebourne poświęcił osobny wpis, wyjaśniając dokładnie historyczne okoliczności powstania różnych stylów nazywania oraz pokazując, jak pasują one do Jigsaw. W dużym skrócie, istnieją dwa style nazywania większych jednostek w Javie:

  • projektowy: guava, junit, spark.core
  • odwrotny DNS: com.google.common, org.junit, ...

Trzy kropki pojawiły się celowo, bowiem znaleźliśmy konflikt - czy w spark.core chodzi o Spark Framework używający pakietów com.sparkjava.core, czy o Apache Spark z pakietami org.apache.spark.core? W zasadzie ten przykład już powinien w zupełności wystarczyć do wydedukowania, którego stylu powinniśmy używać dla modułów - maszyna wirtualna bowiem odmówi nam startu, jeśli podrzucimy jej dwa różne moduły mające tę samą nazwę. Styl projektowy doskonale sprawdza się w systemach budowania, gdzie towarzyszy mu jeszcze grupa. Na poziomie kodu źródłowego mamy jednak bardzo dobrze ugruntowaną konwencję, która chroni nas z powodzeniem przed kolizjami od lat 90. A skoro moduł to pojemnik na pakiety, rozsądnym wydaje się nazwanie modułu od "wspólnego mianownika". Wtedy moduł staje się jednocześnie oświadczeniem, że przejmujemy na własność ten konkretny kawałek dostępnej przestrzeni nazw.

Obecnie używanie stylu odwrotnych nazw DNS jest oficjalną rekomendacją.

Automatyczne moduły

Jigsaw działa w ten sposób, że zastępuje istniejący obecnie mechanizm classpath przez modulepath. W języku programowania mającym za sobą 20 lat historii oznacza to jednak, że trzeba coś zrobić z napisanym dotychczas kodem. W szczególności może wydarzyć się sytuacja, w której chcielibyśmy zbudować modułową aplikację, ale jedna z naszych zależności nie miałaby odpowiednich deskryptorów. Rozwiązaniem zaproponowanym przez twórców Javy są automatyczne moduły. Innymi słowy, Java potraktuje zwykłe archiwum JAR jak moduł zbudowany wg następujących reguł:

  • wszystkie pakiety są publicznie dostępne,
  • automatyczny moduł zależy od wszystkiego, co znajduje się w modulepath ORAZ WYJĄTKOWO także classpath (normalnie nie jest to dozwolone),
  • nazwa modułu wyliczana jest z nazwy archiwum, np. guava.jar » guava.

Ostatni z tych punktów spotkał się z ogromną krytyką ze strony środowiska zakończoną przegranym majowym głosowaniem. Oczywiście w głosowaniu chodziło też o kilka innych kwestii, ale osiągnięty miesiąc później konsensus wskazuje, że to właśnie wyliczanie nazw automatycznych modułów było czynnikiem decydującym, i moim zdaniem słusznie.

Przyjrzyjmy się bliżej wyliczaniu nazwy na podstawie tego, co powiedzieliśmy dotychczas o pakietach. Styl odwrotnej domeny naturalnie pasuje do zaadoptowania w nazewnictwie modułów, tymczasem algorytm wyliczania bierze domyślne nazwy z nazw archiwum, które z kolei wynikają przeważnie z nazwy projektu. Oznacza to, że w okresie przejściowym ta sama biblioteka mogłaby zaistnieć pod dwoma różnymi nazwami. Starsza wersja używałaby wyliczonej nazwy foo, zaś nowsza z poprawnie zdefiniowanymi deskryptorami - com.example.foo. W deskryptorach modułów nie ma wersji, jednak są zależności, które trzeba rozwiązać:

  • przypuśćmy, że chcemy zastosować nową wersję foo, wpisujemy więc w naszym deskryptorze: com.example.foo,
  • mamy jednak zależność od modułu bar, który korzysta ze starszej wersji foo, która nie miała jeszcze deskryptorów i wpisana została jako requires: foo

W tym momencie powstaje nam konflikt nie-do-rozwiązania, bowiem maszyna wirtualna odmówi współpracy, jeśli dwa różne (z jej perspektywy) moduły będą posiadać dokładnie takie same pakiety. Gdyby Jigsaw przeszedł z automatycznymi modułami zaimplementowanymi w takiej formie, oznaczałoby to, że migracja ekosystemu musiałaby odbywać się metodą "od dołu do góry": najpierw najbardziej podstawowe biblioteki, a potem stopniowo dopiero te, które na nich budują. Na szczęście wygrał zdrowy rozsądek i Java 9 będzie dawała możliwość wymuszenia określonej nazwy automatycznego modułu poprzez dodanie atrybutu Automatic-Module-Name do manifestu. W ten sposób biblioteka może pozostać jeszcze przez pewien czas przy starym sposobie dystrybucji (np. na potrzeby kompatybilności ze starszą Javą), a jednocześnie przygotować już drogę pod migrację na Jigsaw i "zająć" sobie nazwę modułu. API java.lang.module.ModuleDescriptor.Builder pozwala też na sterowanie całym procesem tworzenia automatycznego modułu. A całe zamieszanie mogło wynikać stąd, że Mark Reinhold początkowo mocno optował za stylem projektowym i dopiero później pod wpływem dyskusji zmienił zdanie.

Relacja do systemów budowania

Wróćmy teraz do systemów budowania i powiedzmy sobie, co wciąż jest ich odpowiedzialnością:

  • zdobycie SKĄDŚ plików JAR,
  • zdobycie tych plików w odpowiednich wersjach,
  • rozwiązanie konfliktów wersji.

Warto jednak zauważyć, że Jigsaw daje możliwość zapisania informacji o wersji oraz o użytych wersjach zależności w binarnym deskryptorze modułu. Można się do nich później dostać przez refleksję. Jest to ukłon w stronę systemów budowania, który otwiera też furtkę do dalszych prac nad wersjonowaniem w przyszłych wersjach Javy. I rzeczywiście, jeśli zerkniemy na listę propozycji, to widać, że możliwość dodania takich mechanizmów w przyszłości jest wciąż otwarta. Innym dodatkiem czekającym na rozwiązanie w przyszłych wersjach Javy jest tworzenie archiwów JAR zawierających wiele modułów, a w szczególności wykonywalnych uber-JARów. Twórcy przyznali, że jest to użyteczna funkcjonalność, ale jednocześnie zaznaczyli, że nie jest ona krytyczna i Jigsaw może wystartować bez niej.

Dla ciekawych zamieszczam odnośnik do listy wszystkich propozycji dalszego rozwoju Jigsaw: http://openjdk.java.net/projects/jigsaw/spec/issues/

Jigsaw i Gradle

Pisząc o budowaniu modularnej aplikacji, mam także małą aktualizację dotyczącą stanu wsparcia dla Jigsaw ze strony Gradle'a. W czerwcu opublikowałem na tym blogu artykuł Jak pożenić Gradle'a z Javą 9, który przedstawiał moje próby ręcznego zmuszenia go do kompilowania modułów, które zakończyły się połowicznym sukcesem. W międzyczasie braki informacyjne zostały uzupełnione przez jego twórców i obecnie mamy do dyspozycji oficjalny poradnik, który przedstawia dwie drogi:

  • ręczne dodanie odpowiednich flag kompilatora do skryptu Gradle'a,
  • wykorzystanie eksperymentalnej wtyczki experimental-jigsaw, która robi to samo za nas.

Sam poradnik dostępny jest tutaj: Building Java 9 modules @ Gradle.org

Podsumowanie

Na kilkanaście dni przed premierą Javy 9 moje zaufanie do Jigsaw ponownie jest całkiem duże. Mój styl i filozofia programowania rozwija się w kierunku tworzenia mniejszych, zwinnych aplikacji i jest mała szansa, że kombajn w stylu OSGi będzie mi w najbliższej przyszłości potrzebny. Jednak mniejsze i zwinne aplikacje wcale nie oznaczają, że nie ma w nich problemów z panowaniem nad API albo że nie chcielibyśmy mieć aplikacji składanych z zestawu klocków. Z takimi przypadkami użycia już się w moich projektach spotykałem i wygląda na to, że Jigsaw odpowiada na te potrzeby wystarczająco dobrze. Bardzo ważne jest też dla mnie, że moduły będzie można zastosować w odniesieniu do całego ekosystemu, czyli również do samego JDK oraz do zwykłych bibliotek.

Tomasz Jędrzejewski

Programista Javy, lider techniczny. W wolnych chwilach podróżuje, realizując od kilku lat projekty długodystansowych wypraw pieszych.

Autor zdjęcia nagłówkowego: Horia Varlan, CC-BY-2.0

Komentarze (1)

av

Tomasz Jędrzejewski

Zapraszam do lektury kolejnego wpisu na temat modułów: Gradle Chainsaw Plugin. Omawia on budowanie aplikacji modułowych w Gradle'u przy pomocy nowej wtyczki, a przy okazji prezentuje praktyczne użycie modułów.

Skomentuj

Od 3 do 40 znaków.

Wymagany, nie będzie publikowany.

Odpowiedz na pytanie.

Edycja Podgląd

Od 10 do 8000 znaków.

Wszystkie komentarze są moderowane i muszą być zatwierdzone przed publikacją.