1. Czym jest module-info.java i gdzie się znajduje?
Moduł w Javie zawsze zaczyna się od pliku module-info.java. To swoisty „paszport” modułu, w którym deklarujesz jego nazwę, eksportowane pakiety oraz zależności od innych modułów.
Gdzie szukać?
Plik module-info.java powinien znajdować się w katalogu źródeł modułu, w jego katalogu głównym. Na przykład, jeśli struktura projektu jest taka:
project-root/
└── src/
└── my.module.name/
├── module-info.java
└── com/
└── example/
└── api/
└── MyClass.java
Tutaj my.module.name — to nazwa modułu (o zasadach nazewnictwa za chwilę).
Bez tego pliku moduł po prostu nie jest uznawany za moduł!
Jeśli go nie ma — to zwykły „stary” projekt Java, nawet jeśli ma mnóstwo katalogów i klas.
2. Składnia module-info.java: elementy podstawowe
Spójrzmy od razu na przykład najprostszego pliku — to naprawdę nic strasznego!
module my.module.name {
exports com.example.api;
requires java.sql;
}
Rozłóżmy to na części:
module my.module.name { ... }
To deklaracja modułu o nazwie my.module.name. Nazwa modułu — to unikalny identyfikator, który zazwyczaj pokrywa się z pakietem korzeniowym (na przykład com.example.app).
Ciekawostka: jeśli nazwiesz moduł java.base, kompilator nie będzie zachwycony. Nie próbuj podmieniać standardowych modułów.
exports com.example.api;
Ten wiersz mówi: „Ja, moduł, udostępniam wszystko, co znajduje się w pakiecie com.example.api”. Wszystko, co w tym pakiecie jest oznaczone jako public, będzie widoczne dla innych modułów. Cała reszta — tylko do użytku wewnętrznego.
requires java.sql;
A tutaj szczerze przyznajemy kompilatorowi: „Do działania potrzebuję standardowego modułu java.sql”. Bez tego kompilator nie pozwoli używać klas z tego modułu.
Dodatkowe słowa kluczowe (dla ogólnej orientacji)
- opens <package>; — otwiera pakiet na refleksję (np. dla bibliotek serializacji takich jak Jackson).
- uses <service-interface>; — informuje, że moduł używa danego serwisu (interfejsu).
- provides <service-interface> with <implementation-class>; — oznacza, że moduł dostarcza implementację serwisu.
W tym wykładzie skupimy się na exports i requires — są potrzebne w zdecydowanej większości projektów zarówno edukacyjnych, jak i produkcyjnych.
3. Przykłady module-info.java
Przykład 1. Minimalny moduł
module com.example.hello {
exports com.example.hello.api;
}
- Ten moduł eksportuje wyłącznie pakiet com.example.hello.api.
- Wszystko, co znajduje się np. w com.example.hello.internal, będzie ukryte przed innymi modułami, nawet jeśli znajdują się tam klasy public.
Przykład 2. Moduł z zależnością
module com.example.dbclient {
exports com.example.db.api;
requires java.sql;
}
Możemy używać JDBC, ale tylko dlatego, że uczciwie zadeklarowaliśmy zależność od java.sql.
Przykład 3. Wiele eksportowanych pakietów
module com.example.library {
exports com.example.library.api;
exports com.example.library.utils;
}
Można eksportować dowolną liczbę pakietów (ale nie warto eksportować wszystkiego jak leci — na tym polega sens modułów!).
4. Ograniczenia i zasady
Nazwa modułu
- Zazwyczaj pokrywa się z pakietem korzeniowym (na przykład com.example.app).
- Nie może pokrywać się z nazwami standardowych modułów (java.base, java.sql itp.).
- Nie może zawierać spacji, znaków specjalnych, zaczynać się cyfrą itd.
- Rekomendacja: używaj odwróconej nazwy domeny swojej organizacji lub projektu, aby uniknąć konfliktów.
Jeden moduł — jeden module-info.java
W jednym module może być tylko jeden taki plik. Jeśli będą dwa — kompilator zrobi z tego „modułowy skandal”.
Eksport pakietu tylko z jednego modułu
Ten sam pakiet nie może być eksportowany przez dwa różne moduły. To jak dwa paszporty na jedno nazwisko — państwo tego nie zaakceptuje.
Pakiety wewnątrz modułu
Można eksportować tylko te pakiety, które faktycznie istnieją w strukturze źródeł tego modułu. Eksport nieistniejącego pakietu spowoduje błąd kompilacji.
5. Praktyka: tworzymy module-info.java w projekcie
Załóżmy, że mamy prosty projekt o następującej zawartości:
project-root/
└── src/
└── com.example.greetings/
├── module-info.java
└── com/
└── example/
└── greetings/
├── api/
│ └── Greeter.java
└── internal/
└── SecretSauce.java
Krok 1. Tworzymy module-info.java
module com.example.greetings {
exports com.example.greetings.api;
}
Krok 2. Próbujemy użyć klasy z pakietu internal w innym module
Załóżmy, że mamy drugi moduł com.example.app, który chce uzyskać dostęp do SecretSauce:
module com.example.app {
requires com.example.greetings;
}
import com.example.greetings.internal.SecretSauce; // BŁĄD!
Wynik:
Kompilator powie: „Pakiet com.example.greetings.internal nie jest eksportowany przez moduł com.example.greetings”. Nawet jeśli klasa SecretSauce — public, pozostaje niedostępna dla innych modułów.
To właśnie prawdziwa enkapsulacja na poziomie modułów!
Krok 3. Próbujemy nie zadeklarować requires
Jeśli w com.example.app nie napiszemy requires com.example.greetings;, a spróbujemy użyć klasy z com.example.greetings.api, kompilator zgłosi błąd:
package com.example.greetings.api is not visible
6. Typowe błędy podczas pracy z module-info.java
Błąd nr 1: Niezgodność nazwy modułu ze strukturą projektu.
Jeśli nazwiesz moduł com.example.app, a struktura katalogów będzie src/main/java/app, kompilator nie zrozumie, o co chodzi. Nazwa modułu zwykle pokrywa się z pakietem korzeniowym, a katalogi powinny to odzwierciedlać.
Błąd nr 2: Eksport wszystkiego jak leci.
Eksportować należy tylko to, co faktycznie powinno być widoczne dla innych modułów. Nie rób exports com.example; tylko dlatego, że „tak jest prościej”. To narusza enkapsulację.
Błąd nr 3: Zapomniano dodać requires.
Jeśli używasz klas z innego modułu lub standardowej biblioteki (na przykład java.sql), ale zapomniałeś zadeklarować zależność — pojawi się błąd kompilacji.
Błąd nr 4: Nieistniejący pakiet w exports.
Jeśli napisałeś exports com.example.foo;, a taki pakiet nie istnieje — kompilator zgłosi, że „eksportujesz powietrze”.
Błąd nr 5: Publiczne klasy w nieeksportowanym pakiecie.
Jeśli klasa jest oznaczona jako public, ale znajduje się w pakiecie, który nie jest eksportowany, będzie widoczna tylko wewnątrz modułu. To nie błąd, ale często zaskakuje początkujących.
Błąd nr 6: Wiele module-info.java w jednym module.
W jednym module powinien być tylko jeden plik module-info.java. Jeśli będą dwa — kompilator nie zdoła zbudować projektu.
GO TO FULL VERSION