Spis treści

Maven i projekt wielomodułowy - moduły Javy

Ilustracja - czarodziejka rzuca piorunem w filiżankę

Dziś zastanawiam się, czym jest moduł (jaki moduł? javowy? mavenowy? IDE-owy?) i próbuję przekształcić projekt modularny, który utworzyłam na potrzeby wpisu o Javie 9 i budowałam używając make w projekt budowany i uruchamiany przy użyciu Mavena.

Moduły Javy, moduły Mavena, moduły w IDE

Zanim w Javie 9 zostały prowadzone moduły, programiści znali już pojęcie modułu z innych kontekstów. Używali modułów w IntellijIdea, używali modułów w Mavenie. A teraz doszły jeszcze moduły w Javie.

Choć każde z tych pojęć oznacza coś zupełnie innego, to w modelu mentalnym programisty byty o tej samej nazwie były dość często utożsamiane. IntellijIdea, importując wielomodułowy projekt mavenowy, tworzy “swój” moduł dla każdego podprojektu. Przy odpowiedniej strukturze katalogów i umiejscowieniu module-info.java moduły te mogą stać się również modułami javowymi. Dlatego mówiąc o modułach warto zawsze mieć świadomość tego, o jakim module jest mowa.

Po kolei i bardzo krótko zrobię teraz porządek w swoim modelu mentalnym. Kiedy mowa o modułach, to w zależności od kontekstu chodzi o jeden z trzech typów modułów:

  1. Moduły w Javie
  2. Moduły w IDE
  3. Moduły Mavena

Moduły w Javie

Moduły w Javie są obecne od wydania nr 9. Pisałam o nich wcześniej, możesz rzucić okiem tutaj. Jeśli jako programistka/programista nie miałaś/miałeś okazji ich używać, to warto o nich poczytać. Choćby po to, żeby rozumieć, co oznaczają i jak należy interpretować pliki module-info.java, które zobaczysz w źródłach.

Polecam również serię artykułów na stronie https://developers.ibm.com (patrz źródła)

Moduły w IDE

Jeden z bardziej popularnych IDE w świecie Javy - IntellijIdea - od dawna posiada pojęcie modułów:

  • Każdy projekt zawiera przynajmniej jeden, tworzony podczas tworzenia projektu, moduł. Docelowo może zawierać ich wiele.
  • Każdy z modułów wchodzących w skład projektu posiada odrębną konfigurację - tzw. plik modułu z rozszerzeniem iml..
  • Każdy może (lecz nie musi) mieć tzw. content root, czyli katalog zawierający standardową strukturę podkatalogów ze źródłami, testami czy “zasobami”.
  • Każdy może korzystać z odrębnego zbioru bibliotek czy frameworków, każdy w końcu może być modułem z kodem źródłowym w innym języku programowania (Ruby, Kotlin, Python etc.)

IntellijIdea obsługuje moduły Java od wersji 2017.1 (post o obsłudze Javie 9, post o nowościach dla Java 9)

Moduły w Mavenie

Standardowym sposobem tworzenia projektu wielomodułowego w Mavenie jest utworzenie projektu-rodzica, definiującego parent pom, zawierającego w podkatalogach projekty-dzieci, które są właściwymi “modułami” w rozumieniu Mavena.

parent pom definiuje listę swoich dzieci w sekcji modules. Każdy z projektów-dzieci (mavenowych modułów):

  • jest projektem ze standardowym układem katalogów
  • posiada swój pom.xml
  • definiuje swoje współrzędne artefactId oraz version
  • może również wskazywać parent pom jako swojego rodzica

W takim układzie budowanie projektu rodzica oznacza budowanie w odpowiedniej kolejności wszystkich projektów - dzieci.

Przygotowanie projektu do budowania przy użyciu Mavena

W ramach ćwiczenia przekształcę projekt bggen (dostępny na githubie jako java-9-modules-example i do kommita 139f25d budowany przy pomocy make) na projekt budowany przy pomocy mavena.

Zobaczymy, z jakimi wyzwaniami przyjdzie mi się zmierzyć.

Do dzieła!

Przygotowanie pom.xml dla projektu-rodzica

Na początek należy się zaopatrzyć we właściwe narzędzia. Maven, który będzie w stanie obsłużyć javę 9 to maven w wersji 3.5.0 oraz maven-compiler-plugin w wersji 3.6.0.

Sprawdzę, która wersja mavena jest zainstalowana w moim systemie:

1
2
3
4
5
6
karma@tpd~/dev/java/bggen  (main +%) λ  mvn -v
Apache Maven 3.6.3
Maven home: /usr/share/maven
Java version: 15, vendor: Private Build, runtime: /usr/lib/jvm/java-15-openjdk-amd64
Default locale: pl_PL, platform encoding: UTF-8
OS name: "linux", version: "5.8.0-44-generic", arch: "amd64", family: "unix"

Werja 3.6.3 może być. Zatem zabieram się do pisania głównego pom.xml:

W konfiguracji głównego pom.xml definiuję koordynaty projektu i ustawiam odpowiednią wersję pluginu kompilatora. Przygotowuję też sekcję modules, w której wymienię po kolei wszystkie “podmoduły” wchodzące w skład, albo raczej budowane w ramach głównego builda.

 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
<groupId>com.kamilachyla</groupId>
<artifactId>bggen</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<properties>
  <maven.compiler.release>15</maven.compiler.release>
  <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<modules>
  <module>api</module>
</modules>

<build>
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

Powyższy pom.xml wymienia w sekcji modules moduł api, dla którego również przygotowuję pom.xml (wskazujący na powyższy jako swojego parenta). pom.xml dla modułu api wygląda tak:

Przygotowanie pom.xml dla podprojektów

Moduł API: pom.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.kamilachyla</groupId>
<artifactId>bggen</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<groupId>com.kamilachyla</groupId>
<artifactId>api</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>api</name>
</project>

Podobnie definiuję pliki pom.xml dla pozostałych modułów: client, guiclient i generator, umieszczając je w katalogach głównych tych modułów, zmieniając <artifactId> oraz <name> tak, aby - dla uproszczenia - odpowiadała nazwie modułu.

Budowanie

Buduję aplikację z poziomu katalogu głównego - tam, gdzie znajduje sie parent pom:

1
mvn package

Zbudowanie aplikacji pozornie zakończyło się sukcesem:

 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
33
34
35
36
37
38
39
40
41
42
(...)
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ guiclient ---
[WARNING] JAR will be empty - no content was marked for inclusion!
[INFO]
[INFO] ---------------------< com.kamilachyla:generator >----------------------
[INFO] Building generator 1.0-SNAPSHOT                                    [5/5]
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ generator ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/karma/dev/java/bggen/generator/src/main/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ generator ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ generator ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /home/karma/dev/java/bggen/generator/src/test/resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ generator ---
[INFO] No sources to compile
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ generator ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ generator ---
[WARNING] JAR will be empty - no content was marked for inclusion!
[INFO] Building jar: /home/karma/dev/java/bggen/generator/target/generator-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for bggen 1.0-SNAPSHOT:
[INFO]
[INFO] bggen .............................................. SUCCESS [  0.002 s]
[INFO] api ................................................ SUCCESS [  0.618 s]
[INFO] client ............................................. SUCCESS [  0.016 s]
[INFO] guiclient .......................................... SUCCESS [  0.017 s]
[INFO] generator .......................................... SUCCESS [  0.035 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.779 s
[INFO] Finished at: 2021-03-12T14:47:34+01:00
[INFO] ------------------------------------------------------------------------

Pojawiły się jednak ostrzeżenia, że jary są puste. Coś w nich jest, ale nie są to moje skompilowanie klasy:

1
2
3
4
5
6
7
8
karma@tpd~/dev/java/bggen  (main %) λ  jar tf  client/target/client-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
META-INF/maven/
META-INF/maven/com.kamilachyla/
META-INF/maven/com.kamilachyla/client/
META-INF/maven/com.kamilachyla/client/pom.xml
META-INF/maven/com.kamilachyla/client/pom.properties

Jak wskazać Mavenowi katalog ze źródłami

Odpowiednia konwencja

Nie trzymałam się konwencji. Moje źródła są bezpośrednio w katalogu src, a nie - jak nakazuje zwyczaj - w src/main/java, dlatego też muszę pokazać mavenowi, gdzież te źródła są. Dzięki szybkiemu spojrzeniu na artykuł Using Maven When You Can’t Use the Conventions wiem, że muszę zdefiniować w projektach właściwość sourceDirectory wewnątrz sekcji build. Dodaję do client/pom.xml odpowiedni fragment:

1
2
3
<build>
  <sourceDirectory>src</sourceDirectory>
</build>

… i aktualizuję w ten sposób wszystkie pliki pom.xml w modułach.

Jak skompilować z odpowiednimi opcjami kompilatora

Rekordy to wciąż preview feature

Maven widzi już katalog ze źródłami, ale twierdzi, że brakuje mi średników. Gdzieś w okolicy deklaracji rekordu…

1
2
3
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project generator: Compilation failure: Compilation failure:
[ERROR] /home/karma/dev/java/bggen/generator/src/com/kamilachyla/bggen/generator/types/SquaresGenerator.java:[31,18] ';' expected
[ERROR] /home/karma/dev/java/bggen/generator/src/com/kamilachyla/bggen/generator/types/SquaresGenerator.java:[31,26] ';' expected

Kompilator ma kłopot - używam rekordów, które w wersji javy 15 wciąż są w fazie preview (objawia się to komunikatem o braku średnika, ponieważ parser nie rozpoznaje składni w deklaracji rekordu). Dodaję więc do opcji kompilatora w parent pomie --enable-preview:

1
2
3
4
5
<configuration>
  <compilerArgs>
    <arg>--enable-preview</arg>
  </compilerArgs>
</configuration>

I teraz po mvn package, mimo oczywistych ostrzeżeń:

1
2
3
4
5
6
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ guiclient ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 5 source files to /home/karma/dev/java/bggen/guiclient/target/classes
[WARNING] /home/karma/dev/java/bggen/guiclient/src/com/kamilachyla/guigen/ImageData.java:[5,1] records are a preview feature and may be removed in a future release.
[WARNING] /home/karma/dev/java/bggen/guiclient/src/module-info.java: classfile for /home/karma/dev/java/bggen/generator/target/generator-1.0-SNAPSHOT.jar(/module-info.class) uses preview features of Java SE 15.
[WARNING] /home/karma/dev/java/bggen/guiclient/src/com/kamilachyla/guigen/ImageGenerator.java: classfile for /home/karma/dev/java/bggen/generator/target/generator-1.0-SNAPSHOT.jar(/com/kamilachyla/bggen/generator/Generators.class) uses preview features of Java SE 15.

…widzę już dobrą zawartość jara:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
karma@tpd~/dev/java/bggen  (main %) λ  jar tf client/target/client-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
com/
com/kamilachyla/
module-info.class
com/kamilachyla/Main.class
META-INF/maven/
META-INF/maven/com.kamilachyla/
META-INF/maven/com.kamilachyla/client/
META-INF/maven/com.kamilachyla/client/pom.xml
META-INF/maven/com.kamilachyla/client/pom.properties

Uruchomienie

Klasy wykonywalne znajdują się w modułach client (klient testowy ilustrujący korzystanie z modułów: client/com.kamilachyla.Main) i guiclient (generujący obrazek .png: guiclient/com.kamilachyla.guigen.ImageGenerator).

Klasa główna w guiclient

Zaczynam od modułu guiclient. W guiclient/pom.xml używam plugina exec-maven-plugin, w którym:

  • używam celu “exec”, który pozwala na uruchomienie nowego procesu (w tym wypadku projesu javy)

  • podaję listę argumentów odzwierciedlającą linię poleceń w moim Makefile:

  • używam --enable-preview (klasa główna używa rekordów, które nawet w wydaniu 15. są oznaczone jako preview features)

  • wskazuję na --module-path, jednak nie wprost, lecz korzystam z automatycznie wygenerowanej przez Mavena listy modułów, od których zależy moduł z klasą wykonywalną

  • wskazuję na klasę główną, używając argumentu --module

  • oraz - w końcu - podaję listę argumentów do mojego programu (wymiary obrazu, nazwę pliku wynikowego oraz nazwę genenratora kwadratów (“squares”)

Konfiguracja pluginu w guiclient/pom.xml wygląda następująco:

 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
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
    <execution>
      <goals>
        <goal>exec</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <executable>java</executable>
    <arguments>
      <argument>--enable-preview</argument>
      <argument>--module-path</argument> <!-- or -p  -->
      <modulepath/>
      <argument>--module</argument> <!-- or -m -->
      <argument>guiclient/com.kamilachyla.guigen.ImageGenerator</argument>
      <argument>100</argument>
      <argument>100</argument>
      <argument>from-maven.png</argument>
      <argument>squares</argument>
    </arguments>
  </configuration>
</plugin>

Z wiersza poleceń uruchamiam aplikację używając opcji Mavena -pl guiclient, dzięki czemu proces budowania zacznie się od guiclient i zostanie wykonany cel exec:exec.

1
mvn -pl guiclient exec:exec

Uwaga!

Podczas uruchamiania programu maven określa lokalizacje potrzebnych modułów na podstawie koordynatów zdefiniowanych w pom.xml, a więc próbuje je odnaleźć w lokalnym repozytorium (nie wystarczą jary zbudowane przy użyciu mvn package). Dlatego należy zainstalować potrzebne zależności uzywając wcześniej mvn install. Dopiero wtedy będzie możliwe uruchomienie aplikacji z mavena w podany wyżej sposób.

Klasa główna w client

Analogicznie przygotowuję konfigurację pluginu dla modułu client (który służy jedynie do celów demonstracyjnych - nie generuje obrazków, lecz wypisuje nazwy generatorów i oraz współrzędne wygenerowanych prostokątów):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
    <execution>
      <goals>
        <goal>exec</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <executable>java</executable>
    <arguments>
      <argument>--enable-preview</argument>
      <argument>--module-path</argument> <!-- or -p  -->
      <modulepath/>
      <argument>--module</argument> <!-- or -m -->
      <argument>client/com.kamilachyla.Main</argument>
    </arguments>
  </configuration>
</plugin>

Wszystkie pliki pom.xml dodałam do repozytorium w kommicie 758073a7f.

Wnioski

Całe powyższe ćwiczenie miało na celu migrację systemu budowania z make do Mavena. Wcześniej budowałam projekt przy użyciu make - napisanie własngo Makefile pozwoliło mi przećwiczyć użycie nowych opcji kompilatora i jvm-a w przypadku aplikacji modularnej.

Użycie Mavena to jednak standard świecie Javy, w którym progrmiści używają go (ew. Gradle-a) produkcyjnie. Dlatego “zmejwenizowałam” swoją małą aplikację - może kiedyś przestanie być mała i wtedy warto mieć przy sobie porządny system budowania.

Zresztą, tak sobie myślę, być może dałoby się napisać Makefile w projekcie javowym w sposób bardzo przenośny. Mój Makefile nie aspiruje tak wysoko. Jest pisany na kolanie i służy jako szybki test do sprawdzenia zachowania różnych opcji. I jako taki również ma swoje skromne miejsce w projekcie.

Żródła

Seria artykułów o modułach w javie: