Maven i projekt wielomodułowy - moduły Javy
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:
- Moduły w Javie
- Moduły w IDE
- 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
artefactIdorazversion - 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:
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.
<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
<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:
mvn packageZbudowanie aplikacji pozornie zakończyło się sukcesem:
(...)
[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:
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.propertiesJak 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:
<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…
[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] ';' expectedKompilator 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:
<configuration>
<compilerArgs>
<arg>--enable-preview</arg>
</compilerArgs>
</configuration>I teraz po mvn package, mimo oczywistych ostrzeżeń:
[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:
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.propertiesUruchomienie
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:
<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.
mvn -pl guiclient exec:execUwaga!
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):
<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
- Moduły w IntellijIdea
- Projekt wielomodułowy - Baeldung
- Jak używać Mavena gdy nie można użyć konwencj
- Exec maven plugin examples: exec:exec
Seria artykułów o modułach w javie:
- Java 9+ modularity: The theory and motivations behind modularity
- Java 9+ modularity: Module basics and rules
- Java 9+ modularity: How to design packages and create modules, Part 1
- Java 9+ modularity: How to design packages and create modules, Part 2
- Java 9+ modularity: The difficulties and pitfalls of migrating from Java 8 to Java 9+