W poprzednim wpisie na temat pliku pom.xml wspomniałem, że zajmiemy się zagadnieniem podmodułów. Właśnie w tym artykule chcę Cię przez niego przeprowadzić. Przy okazji poznamy czym jest Super POM, dowiemy się czym różni się dziedziczenie od agregacji w Maven oraz przypomnimy sobie zasadę DRY na przykładzie własności.

Co to jest POM?

Na rozgrzewkę zróbmy sobie małą powtórkę. Czym dokładnie jest POM? To fundamentalna jednostka w Maven, która przyjmuje postać pliku XML. Zawiera wszelkie informacje o projekcie oraz szczegółach konfiguracyjnych, które są niezbędne do zbudowania projektu. Definiuje także domyślne wartości dla wielu projektów takie jak np. nazwy katalogów:

  • target, w którym będzie znajdował się zbudowany projekt
  • src/main/java, w którym umieszczamy kod źródłowy
  • src/test/java. gdzie piszemy kod testowy

Jednak minimum informacji jakie musimy podać w pom.xml to następujące elementy: modelVersion, groupId, artifactId oraz version umieszone w głównym węźle project.

1
2
3
4
5
6
7
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>pl.devcezz</groupId>
  <artifactId>simple-app</artifactId>
  <version>1.0</version>
</project>

To wystarczy, aby prawidłowo zdefiniować projekt, który używa Mavena. Dzięki tym wartościom uzyskujemy pełną nazwę dla naszego artefaktu, która jest w postaci <groupId>:<artifactId>:<version>, czyli dla przykładu wyżej będzie to pl.devcezz:simple-app:1.0. Tylko zaraz, nie widać tutaj żadnego target czy src/main/java. Napisałem, że pom.xml naszej aplikacji definiuje te domyślne wartości. Nasuwa się, więc pytanie - “w którym miejscu?”. Z pomocą przychodzi nam wyjaśnienie koncepcji jaką jest Super POM!

Czym jest Super POM?

Super POM to po prostu domyślny plik pom.xml dla Mavena. Wszystkie tworzone przez programistów POM rozszerzają właśnie Super POM (chyba, że wskażemy innego rodzica, co i tak jednak przekłada się na posiadanie Super POM w hierarchii). W sumie to całe niezbędne wyjaśnienie. Jeśli chcesz możesz zobaczyć jak wygląda Super POM dla wersji Mavena 3.6.3 na głównej stronie narzędzia. To tutaj widać zdefiniowane wartości wcześniej przedstawionych katalogów - <directory>${project.basedir}/target</directory> czy <sourceDirectory>${project.basedir}/src/main/java</sourceDirectory>.

Dodatkowo jeżeli czegoś nie zdefiniujemy w naszym pom.xml to Maven dzięki Super POM użyje wartości domyślnych jak to ma miejsce w przypadku typu pakowania aplikacji. Domyślnie jest to JAR, więc nie ma konieczności pisania <packaging>jar</packaging>. Co więcej odziedziczona zostanie również konfiguracja zdalnego repozytorium przez co niezbędne zależności będą pobierane z https://repo.maven.apache.org/maven2. Warto wejść w ten link i sprawdzić jak wielka jest to baza bibliotek.

W ten oto sposób koncepcja Super POM płynnie przeniosła nas do pojęcia dziedziczenia projektów.

Dziedziczenie projektów

Spróbujmy utworzyć podprojekt, który wykorzysta już wcześniej zdefiniowany projekt pl.devcezz:simple-app:1.0 przy pomocy mechanizmu dziedziczenia. W tym celu musimy na początku stworzyć nowy plik pom.xml, który umieścimy w podkatalogu inner-app.

1
2
3
4
5
6
7
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>pl.devcezz</groupId>
  <artifactId>inner-app</artifactId>
  <version>1.0</version>
</project>

Struktura naszej aplikacji wygląda następująco.

.
|-- inner-app
|   `-- pom.xml
`-- pom.xml

POM znajdujący się w katalogu inner-app należy do pl.devcezz:inner-app:1.0 natomiast POM znajdujący się w głównym katalogu do pl.devcezz:simple-app:1.0. Teraz, aby dodać połączenie pomiędzy tym dwoma projektami musimy lekko zmodyfikować konfigurację pliku inner-app/pom.xml dodając odpowiedni węzeł XML - parent.

1
2
3
4
5
6
7
8
9
10
11
12
13
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <parent>
    <groupId>pl.devcezz</groupId>
    <artifactId>simple-app</artifactId>
    <version>1.0</version>
  </parent>

  <groupId>pl.devcezz</groupId>
  <artifactId>inner-app</artifactId>
  <version>1.0</version>
</project>

Jesteśmy zobligowani do podania pełnej nazwy POM, który rozszerzamy, czyli groupId, artifactId oraz version. W ten oto sposób pl.devcezz:inner-app:1.0 może korzystać ze wszystkich właściwości zdefiniowanych w pl.devcezz:simple-app:1.0.

Czy wyjątkowo możemy nie pisać groupId oraz version?

Tak, jest to możliwe w sytuacji dziedziczenia i, gdy chcielibyśmy, aby groupId oraz version były takie same jak u rodzica. Wtedy możemy usunąć te elementy z POM naszego modułu. Wyglądałoby to następująco.

1
2
3
4
5
6
7
8
9
10
11
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <parent>
    <groupId>pl.devcezz</groupId>
    <artifactId>simple-app</artifactId>
    <version>1.0</version>
  </parent>

  <artifactId>inner-app</artifactId>
</project>

Teraz inner-app również ma groupId pl.devcezz oraz version o wartości 1.0 odziedziczone z pl.devcezz:simple-app:1.0.

Co w przypadku, gdy główny moduł znajduje się w podkatalogu?

W tej sytuacji należy skorzystać z elementy relativePath w parent, w której podajemy ścieżkę relatywną do pliku pom.xml z perspektywy wybranego podmodułu. Czyli dla przykładu poniżej będzie to ../simple-app/pom.xml.

.
|-- inner-app
|   `-- pom.xml
|-- simple-app
|   `-- pom.xml

Agregacja projektów

Koncept jest dosyć podobny do dziedziczenia. Jednak zamiast precyzować POM rodzica w podmodule dodajemy podmoduły w rodzicu. W ten sposób rodzic ma świadomość istnienia wybranych podmodułów dzięki czemu jeżeli wywołamy komendę Mavena na rodzicu wykona się ona również na każdym podmodule. Jeśli chcemy skorzystać z tych dobrodziejstw musimy zrobić dwie następujące rzeczy.

  • zmienić wartość elementu packaging na pom - <packaging>pom</packaging>
  • wymienić w POM rodzica wszystkie katalogi podmodułów w elemencie modules

Aby zobaczyć agregację w akcji przypomnijmy sobie wcześniejszy przypadek. Mamy POM rodzica, POM dziecka oraz strukturę katalogów.

1
2
3
4
5
6
7
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>pl.devcezz</groupId>
  <artifactId>simple-app</artifactId>
  <version>1.0</version>
</project>
1
2
3
4
5
6
7
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>pl.devcezz</groupId>
  <artifactId>inner-app</artifactId>
  <version>1.0</version>
</project>
.
|-- inner-app
|   `-- pom.xml
`-- pom.xml

Teraz, żeby zagregować moduł inner-app w simple-app musimy zmodyfikować POM simple-app.

1
2
3
4
5
6
7
8
9
10
11
12
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>pl.devcezz</groupId>
  <artifactId>simple-app</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>

  <modules>
    <module>inner-app</module>
  </modules>
</project>

Podsumowując, tak jak wspomniałem wcześniej, musieliśmy zmienić typ pakowania aplikacji na pom - <packaging>pom</packaging>. Również należy dodać element modules zawierający wiele elementów module z relatywnymi ścieżkami do podmodułów względem rodzica. W naszym przypadku jest on tylko jeden - <module>inner-app</module>. Warto dodać, że pod względem praktycznym najlepiej nazywać katalog modułu używając jego artifactId.

W tym momencie każda komenda wydana rodzicowi będzie również uruchomiana dla jego dzieci zdefiniowanych w pliku pom.xml. Jeśli, więc uruchomimy mvn package dla pl.devcezz:simple-app:1.0 to ta komenda wykona się również dla pl.devcezz:inner-app:1.0. Twórcy Mavena jednak przestrzegają, że niektóre komendy (zwłaszcza goals) mogą zachowywać się różnie w przypadku agregacji.

Co w przypadku, gdy główny moduł znajduje się w podkatalogu?

Powtarzając wcześniejszą sytuację, mamy taką strukturę katalogów w projekcie.

.
|-- inner-app
|   `-- pom.xml
|-- simple-app
|   `-- pom.xml

Wtedy również w elemencie module musimy podać relatywną ścieżkę do podmodułu.

1
2
3
4
5
6
7
8
9
10
11
12
<project>
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>pl.devcezz</groupId>
  <artifactId>simple-app</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>

  <modules>
    <module>../inner-app</module>
  </modules>
</project>

Czym się różni dziedziczenie od agregacji?

Dziedziczenie jest przydatne wtedy, gdy chcemy mieć podobną konfigurację w wielu projektach. Możemy wtedy zrefaktorować nasze moduły w taki sposób, aby wyciągnąć z nich powtarzającą się konfigurację i umieścić ją w rodzicu. Wtedy zostaje już tylko w tych modułach wskazanie rodzica jako tego, z którego dziedziczymy, aby uzyskać w nich wyniesioną konfigurację.

Natomiast mechanizm agregacji służy do tego, aby grupować projekty i dla nich uruchamiać te same procesy. Jedyne co trzeba zrobić to utworzyć moduł rodzica, wskazać w nim interesujące nas moduły i tyle. Teraz wystarczy tylko wywołać komendę dla rodzica, a ona rozpropaguje się na wybrane podmoduły.

Oczywiście tych dwóch mechanizmów można używać w tym samym czasie. Oznacza to, że dla modułów można wskazać rodzica, aby dzielić ze sobą konfigurację oraz w rodzicu można wskazać moduły, aby wywoływać dla nich te same komendy za pomocą jednej linijki w wierszu poleceń. Trzeba spełnić przy tym 3 zasady, które były już wcześniej przedstawione.

  • wskazać w POM dzieci wybranego rodzica
  • zmienić tryb pakowania aplikacji na pom w rodzicu
  • wymienić moduły dzieci w POM rodzica

Podsumowanie

Dziedziczenie oraz agregacja to bardzo przydatne mechanizmy służące do tego, aby lepiej ustrukturyzować nasz projekt budowany w oparciu o Maven. Możemy w łatwy sposób pominąć redundancję danych (zasada DRY), sprawniej wywoływać komendy oraz lepiej porządkować kod tak jak to uczyniłem przy aplikacji AnimalShelter. Polecam, więc przyjrzeć się tym mechanizmom i postarać się je zastosować w swoim projekcie!