1. Problemy starego podejścia
„Classpath party”: kiedy wszyscy w jednej kuchni
Przed pojawieniem się modułów w Javie (w wersji dziewiątej) cały kod aplikacji, biblioteki i zależności trafiały do jednej wielkiej sterty — classpath. Granic między bibliotekami praktycznie nie było: każda klasa, która trafiła do classpath, mogła zostać znaleziona i użyta przez kogokolwiek.
Pojawiały się konflikty nazw: dwie biblioteki zawierają klasę o tej samej w pełni kwalifikowanej nazwie, na przykład com.example.Util — wybierano „jakąś jedną”, a o problemie dowiadywaliście się dopiero po dziwnym zachowaniu w czasie działania.
Klasyka gatunku — dependency hell: jedna biblioteka wymaga loggera w wersji 1.2, inna — 1.3, a w classpath lądują obie. JVM nie wie, czego użyć, i trafia się zagadkowy NoSuchMethodError.
I jeszcze: nawet jeśli klasa jest oznaczona jako public, ale przeznaczona do wewnętrznych potrzeb biblioteki, inny kod wciąż może jej użyć — i przy okazji coś sobie zepsuć. Duże projekty zamieniały się w pole minowe, a utrzymanie — w przygodę.
2. Czym jest moduł w Javie?
Moduł: jak pudełko z otworkami
W Java 9 pojawił się system modułowy (JPMS). Moduł to pudełko z klasami i pakietami, w którym sami robicie „otworki”: wprost wskazujecie, co pokazujecie na zewnątrz (exports), a co ukrywacie w środku. I tak: nawet klasa public nie jest widoczna dla innych modułów, jeśli jej pakiet nie jest eksportowany.
Definicja formalna
Moduł — logiczna jednostka podziału kodu, łącząca powiązane pakiety i klasy. Każdy moduł jawnie deklaruje:
- co eksportuje — udostępnia innym modułom (exports);
- oraz co importuje — od jakich innych modułów zależy (requires).
Kluczowe właściwości modułu:
- Jawne granice. Jasno określone, co jest dostępne na zewnątrz, a co nie.
- Jawne zależności. Moduł nie „widzi” innego modułu bez jawnego requires.
- Izolacja. Wewnętrzne szczegóły mogą być całkowicie ukryte przed światem zewnętrznym.
3. Zalety modułowości
Lepsza enkapsulacja
Pojawił się nowy poziom widoczności — na poziomie modułu. Nawet jeśli klasa jest public, ale jej pakiet nie jest eksportowany, jest widoczna tylko wewnątrz modułu. Wnętrzności biblioteki już nie „przeciekają” na zewnątrz.
Jasny opis zależności
Wymagane zależności są wymienione w module-info.java. Kompilator i IDE zawczasu ostrzegą, jeśli zapomnieliście wskazać moduł, od którego zależycie.
Lepsze bezpieczeństwo i niezawodność
Ograniczenie dostępu do wewnętrznych API utrudnia przypadkową lub celową ingerencję. To krytyczne dla dużych bibliotek i modułów platformy.
Możliwość tworzenia „okrojonego” JRE
Za pomocą jlink można złożyć minimalne środowisko uruchomieniowe tylko z potrzebnych modułów. W chmurze i scenariuszach wbudowanych (embedded) oszczędza to miejsce na dysku i pamięć.
Bonus: szybszy start i mniejszy rozmiar
Ładowane są nie wszystkie moduły, a jedynie faktycznie używane — aplikacje startują szybciej i zużywają mniej zasobów.
4. Gdzie używa się modułów
W standardowej bibliotece Javy
Począwszy od Java 9, sama platforma jest modułowa. Przykłady:
- java.base — moduł podstawowy (wymagany dla każdego programu).
- java.sql — obsługa baz danych.
- java.xml — obsługa XML.
Jeśli nie korzystasz z XML, moduł java.xml nawet nie trafi do twojego środowiska uruchomieniowego.
W dużych aplikacjach i bibliotekach
Must-have dla systemów korporacyjnych, gdzie dziesiątki zespołów rozwijają części monorepozytorium. Modułowość zmniejsza konflikty i upraszcza utrzymanie.
We własnych projektach
Nawet w projekcie hobbystycznym moduły pomagają ćwiczyć myślenie architektoniczne i utrzymywać porządek w kodzie, unikając „spaghetti”.
5. Krótki przegląd składni: module-info.java
Najważniejszy jest plik module-info.java
Plik module-info.java leży w katalogu głównym źródeł modułu i deklaruje jego granice oraz zależności.
module my.awesome.module {
exports com.example.api; // Eksportowany pakiet
requires java.sql; // Zależność od standardowego modułu
}
Podstawowe słowa kluczowe:
- module <nazwa> — deklaruje moduł.
- exports <pakiet> — udostępnia pakiet innym modułom.
- requires <moduł> — deklaruje zależność od innego modułu.
Minimalny przykład:
module com.myproject.core {
exports com.myproject.core.api;
}
Przykład z zależnością:
module com.myproject.app {
requires com.myproject.core;
requires java.sql;
}
Dodatkowe możliwości (w skrócie): do refleksji służy opens, do usług — uses i provides ... with ... (szczegóły — w zaawansowanych wykładach).
6. Przydatne niuanse
Moduły są jak „paszporty” dla klas. Wcześniej każda klasa z wizą public mogła „podróżować” po aplikacji. Teraz potrzebny jest jeszcze „paszport” modułowy — eksport pakietu (exports). Bez niego klasa zostaje „w domu”.
Dependency hell to prawdziwy termin. Jeśli widzieliście ClassNotFoundException: com.google.common.base.Strings, byliście tam. Moduły mają przepędzić te „diabły” dzięki ścisłym granicom i zależnościom.
Cała Java jest teraz modułowa. Nawet jeśli nie piszecie własnych modułów, platforma jest już podzielona na dziesiątki. Wypróbujcie polecenie:
java --list-modules
Jak to wpływa na rozwój oprogramowania
Jawnie kontrolujecie widoczność kodu i wewnętrzne pakiety nie stają się dostępne wszystkim „przez przypadek”. IDE i kompilator wyłapują błędy wcześniej: zapomnieliście zadeklarować zależność — projekt się nie zbuduje. Utrzymanie dużych systemów się upraszcza: łatwiej zrozumieć, kto od kogo zależy i co może się zepsuć przy zmianach.
7. Typowe błędy przy przejściu na moduły
Błąd nr 1: zapomnieliście wyeksportować pakiet, ale klasa jest public. Zadeklarowaliście klasę jako public, ale nie wyeksportowaliście jej pakietu — inne moduły nie będą mogły jej użyć. Kompilator zgłosi: „Pakiet nie jest eksportowany przez moduł”.
Błąd nr 2: nie zadeklarowano zależności przez requires. Używacie typu z innego modułu, ale zapomnieliście dodać requires do module-info.java. W efekcie — błąd kompilacji „moduł nie może znaleźć wymaganej klasy”.
Błąd nr 3: zduplikowane nazwy modułów. W dużym projekcie dwa moduły przypadkowo otrzymały tę samą nazwę. JVM tego nie wybacza — zmieńcie nazwę i trzymajcie się spójnego nazewnictwa.
Błąd nr 4: zapomniano o standardowych modułach. Na przykład używacie JDBC, ale nie dodaliście requires java.sql;. W Javie 8 „i tak było widać”, a w 9+ — już nie.
Błąd nr 5: próba użycia wewnętrznej klasy innego modułu. Jeśli pakiet nie jest eksportowany, nawet klasa public pozostaje niewidoczna z zewnątrz. Eksportujcie pakiet albo wynieście API do „publicznej” warstwy.
GO TO FULL VERSION