Spis treści

Java 9: praktyczny przykład - trzy moduły

Programowanie modularne

Dziś szybki “przykład na kolanie” pokazujący jak napisać aplikację modularną. Przykładowa aplikacja będzie generatorem obrazów (bggen). Architektura modułów będzie bardzo prosta. Oto ona:

  • moduł api zwiera zbiór interfejsów i klas pomocniczych
  • moduł generator pełni funkcję biblioteki, potencjalnie jednej z wielu, która implementuje interfejsy zdefiniowane w api
  • moduł client używa modułu generator na swoje potrzeby biznesowe.

Krok po kroku

Opiszę krok po kroku jak stworzyć taką aplikację.

Utworzenie projektu z trzema modułami

Używam edytora IntellijIdea Community. Tworzę standardowy projekt (javowy, nie używam mavena), a wewnątrz niego dodaję trzy moduły (i usuwam moduł “główny” utworzony automatycznie po utworzeniu aplikacji; chę mieć płaską strukturę projektu). Są to moduły w rozumieniu IntellijIdea - mają odrębne struktury katalogów, ustawienia SDK, bibliotek, katalogu wynikowego kompilacji itd.

Implementacja

  • api definiuje interfejs RectangleGenerator, którego jedyną funckją jest dostarczenie strumienia prostokątów (klasa Rect)
  • generator posiada prywatne implementacje tego interfejsu i udostępnia je jedynie przez metody static factory klasy Generators.
  • client będzie używał wygenerowanych przez dostępne implementacje RectangleGeneratora prostokątów i tworzył obrazy o zadanej rozdzielczości (w przykładzie prostokąty są wypisywane na standardowe wyjście).

Definiowanie modułów w rozumieniu java 9

Moduły w rozumieniu IntelliJIdea staną się modułami javy 9 tylko wówczas, gdy będą posiadały deskryptory modułu. Deskryptor modułu jest skompilowanym opisem modułu. Opis modułu to plik o nazwie module-info.java.

Wewnątrz katalogów src każdego z modułów tworzę opisy, z których wynika, że:

  • api eksportuje pakiet com.kamilachyla.bggen.api:
1
2
3
module api {
exports com.kamilachyla.bggen.api;
}
  • generator używa modułu api i eksportuje com.kamilachyla.bggen.generator:
1
2
3
4
module generator {
requires api;
exports  com.kamilachyla.bggen.generator;
}
  • client posiada jedną klasę (Main); używa modułu generator i klas/interfejsów z api:
1
2
3
4
module client {
requires generator;
requires api;
}

Struktura plików na dysku wygląda teraz tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
api
│  ├── api.iml
│  └── src
│     ├── com
│     │  └── kamilachyla
│     │     └── bggen
│     │        └── api
│     │           ├── Rect.java
│     │           └── RectangleGenerator.java
│     └── module-info.java
├── bggen.iml
├── client
│  ├── client.iml
│  └── src
│     ├── com
│     │  └── kamilachyla
│     │     └── Main.java
│     ├── META-INF
│     │  └── MANIFEST.MF
│     └── module-info.java
└── generator
├── generator.iml
└── src
├── com
│  └── kamilachyla
│     └── bggen
│        └── generator
│           ├── Generators.java
│           └── types
│              ├── SimpleGenerator.java
│              └── SquaresGenerator.java
└── module-info.java

Kompilacja

Kompiluję trzy moduły w taki oto sposób:

  • tworzę katalog mlib, gdzie będą się znajdowały klasy wynikowe (mlib będzie katalogiem zawierającym skompilowane moduły)
  • kompiluję - niezależny od innych modułów - moduł api:
1
javac -d mlib/api $(find api -name "*.java")
  • kompilacja modułów zależnych od innych modułów wymaga podania ścieżki do katalogu z wymaganymi modułami, więc do skompilowania modułów generator oraz client używam opcji wiersza poleceń --module-path mlib:
1
2
3
4
5
$ javac --module-path mlib -d mlib/generator $(find generator -na
me "*.java")

$ javac --module-path mlib -d mlib/client $(find client -name "*.
java")

Gotowe! Oto struktura katalogu mlib ze skompilowanymi modułami:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bbgen
├── api
│  ├── com
│  │  └── kamilachyla
│  │     └── bggen
│  │        └── api
│  │           ├── Rect.class
│  │           └── RectangleGenerator.class
│  └── module-info.class
├── client
│  ├── com
│  │  └── kamilachyla
│  │     └── Main.class
│  └── module-info.class
└── generator
├── com
│  └── kamilachyla
│     └── bggen
│        └── generator
│           ├── Generators.class
│           └── types
│              ├── SimpleGenerator.class
│              └── SquaresGenerator.class
└── module-info.class

Uruchomienie

Uruchomienie aplikacji również wymaga podania ścieżki do katalogu z modułami (program java również rozpoznaje opcję --module-path), a samą klasę główną podaję poprzedzoną nazwą modułu w postaci <nazwa_modułu>/<pełna-nazwa-klasy>:

1
java --module-path mlib -m client/com.kamilachyla.Main

I w rezultacie otrzymuję:

1
2
3
4
5
6
7
Generates single Rect with x=0, y=0 and given width and height
Rect[x=0.0, y=0.0, width=800.0, height=600.0]
Generates stream of squares with its side not smaller than 20
Rect[x=0.0, y=0.0, width=600.0, height=600.0]
Rect[x=600.0, y=0.0, width=200.0, height=200.0]
Rect[x=600.0, y=200.0, width=200.0, height=200.0]
Rect[x=600.0, y=400.0, width=200.0, height=200.0]

Generowanie wykresu zależności między modułami i pakietami

Narzędzie jdeps pozwala wygeerować graf zależności między pakietami/modułami w formacie .dot, z którego graphviz generuje wykres.

1
2
3
mkdir dot
jdeps  --dot-output dot mlib/
dot -Tpng dot/mlib.dot -ojava-9-jdeps-small.png

/java-9-jdeps-small.png

Ponwyższy wykres wygenerowałam bez zależności do pakietów z biblioteki standardowej; dla ciekawych - pełny wykres: /java-9-jdeps.png

Źródła projektu dostępne są w projekcie bggen na githubie.