Sporo teorii za nami, więc pora teraz zagłębić się w strukturę pliku pom.xml, który jest sercem Mavena. Zawiera on wszelkie informacje na temat tego w jaki sposób należy zbudować prowadzony przez nas projekt. W tym artykule przejdziemy przez części składowe przykładowego pliku oraz zobaczymy z jakich elementów się on składa.

Przykładowa struktura pliku pom.xml

Weźmy jako przykład plik pom.xml z projektu, który utworzyliśmy z poziomu konsoli w pierwszym artykule (korzystając z archetypów). Tak się on prezentuje po małej obróbce (usunąłem zbędne komentarze i wyciąłem pluginy, którymi dokładniej zajmiemy się kiedy indziej).

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
<?xml version="1.0" encoding="UTF-8"?>

<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>

  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>my-app</name>
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.7</maven.compiler.source>
    <maven.compiler.target>1.7</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <pluginManagement>
      <plugins>
        ...
      </plugins>
    </pluginManagement>
  </build>
</project>

Z racji, że jest to plik XML musi on zawierać prolog z deklaracją rodzaju pliku. W tym przypadku zawiera ona numer wersji oraz sposób kodowania znaków - <?xml version="1.0" encoding="UTF-8"?>. Rekomendowane jest używanie wersji 1.0 oraz domyślnego UTF-8, którego niekoniecznie trzeba zapisać.

Główny element - project

Następnie przechodzimy do elementu głównego dokumentu - project, który zawiera kilka atrybutów:

  • xmlns - deklaracja przestrzeni nazw, w Maven dla jego elementów jest to http://maven.apache.org/POM/4.0.0
  • xmlns:xsi - przypisanie przestrzeni nazw używającej prefiksu xsi do głównej przestrzeni nazw XSD (XML Schema Definition): http://www.w3.org/2001/XMLSchema-instance, aby można było poprawnie ustawić następujące atrybuty - xsi:type, xsi:nil, xsi:schemaLocation** i xsi:noNamespaceSchemaLocation**
  • xsi:schemaLocation - dostarcza wskazówki dla procesora XML, aby wiedział w jaki sposób powiązać plik XSD z plikiem XML, kiedy istnieje zadeklarowana przestrzeń nazw - w przypadku Maven jest to połączenie http://maven.apache.org/POM/4.0.0 z http://maven.apache.org/xsd/maven-4.0.0.xsd

Ta cała ceremonia pozwala nam weryfikować na bieżąco czy dokument XML jest prawidłowy, czy spełnia standardy zdefiniowane przez Mavena w pliku pod adresem http://maven.apache.org/xsd/maven-4.0.0.xsd (wskazaliśmy go w atrybucie xsi:schemaLocation).

Elementy definiujące projekt

Na samym początku musimy zadeklarować wersję - modelVersion - deskryptora projektu, który zgadza się z POM (Project Object Model) - reprezentacją projektu Maven w postaci XML. Jest ona obligatoryjna i na tą chwilę twórcy wspierają tylko wersję 4.0.0 POM. Kolejne trzy elementy identyfikują naszą aplikację. Mamy tutaj do dyspozycji groupId, artifactId oraz version, które koniecznie musimy podać.

  • groupId - unikalny identyfikator dla projektu - dobrą praktyką jest, aby utrzymywał się w konwencji nazewnictwa pakietowego w Javie czyli np. org.apache.maven
  • artifactId - identyfikator dla artefaktu, który jest unikalny dla grupy w obrębie groupId
  • version - aktualna wersja artefaktu wytwarzanego dla danego projektu

Wymagalność groupId, artifactId oraz version
Wymagalność na groupId, artifactId oraz version

Artefakt to nic innego jak skompilowana aplikacja spakowana do pliku JAR albo WAR. Oczywiście możemy zmienić sposób pakowania używając elementu packaging (domyślnie jest to JAR przez co nie musimy podawać tego elementu explicite).

Możemy jeszcze dodać opcjonalne elementy, name oraz url, tak jak ma to miejsce w przykładzie wyżej. Pierwszy z nich to po prostu pełna nazwa naszego projektu, natomiast drugi to adres URL prowadzący do strony projektu.

Definicja właściwości

Kolejnym elementem wartym uwagi jest properties, czyli właściwości. W nim możemy zawrzeć inne elementy, które przechowują nam wartości możliwe do wykorzystania w innych elementach. Dozwolone jest tworzenie własnych właściwości oraz wykorzystanie tych zdefiniowanych w dokumentacji. Podając wartości dla maven.compiler.source oraz maven.compiler.target przekażemy informację do pluginu maven-compiler-plugin jakiej wersji Javy chcemy korzystać przy tworzeniu oprogramowania oraz do jakiej wersji chcemy skompilować aplikację. W przypadku powyższego przykładu będzie to w obu przypadkach Java w wersji 7.

To samo tyczy się właściwości project.build.sourceEncoding, której możemy przypisać wartość taką jak np. ASCII, UTF-8 czy UTF-16. Będzie ona użyta przez plugin maven-resources-plugin. Jak można się domyślić jest to określenie sposobu w jaki będą kodowane znaki podczas czytania i zapisywania plików. Dodam, że zamiast umieszczać ten element w properties możemy go również zdefiniować w konfiguracji pluginu. Jednak jest to mniej przejrzyste i warto na wejściu przekazać informację o kodowaniu znaków zamiast chować ją w gąszczu elementu plugins. Taka sama możliwość istnieje również w przypadku wskazania wersji Javy.

Tworzenie własnej właściwości jest naprawdę proste. Wystarczy utworzyć własny element wewnątrz properties i podać wartość dla niego np. <my.own.property>123</my.own.property>. Teraz możemy z niej skorzystać w dowolnym miejscu poprzez wykorzystanie składni ${X}, gdzie X to właśnie nazwa właściwości - <usage.of.property>${my.own.property}</usage.of.property> (w tym przypadku usage.of.property będzie miało tą samą wartość co my.own.property).

Doprecyzowanie wykorzystania właściwości w pom.xml

Poza wcześniejszym sposobem wykorzystania wartości zdefiniowanej w properties istnieją jeszcze cztery inne. Zestawmy je zatem wszystkie w jednym miejscu:

  1. Wykorzystanie właściwości zaczynających się od env. zwraca nam wartości zmiennych środowiskowych. Jako przykład możemy wykorzystać ${env.PATH}, aby dostać wartość znajdującą się pod nazwą PATH w systemie. (UWAGA: na Windowsie zmienne środowiskowe są case-insensitive, natomiast Maven rozróżnia pomiędzy np. ${env.PATH} a ${env.Path}. Dla braku nieporozumień wszystkie zmienne środowiskowe są zmieniane jako pisane wielkimi literami.)
  2. Notacja używająca kropek ‘.’ w pom.xml pozwala na użycie wartości elementów zdefiniowanych właśnie w tym pliku. Oznacza to, że np. dostanie się do wartości <project><version>2.3.1<version><project> będzie możliwe poprzez zastosowanie składni ${project.version}
  3. Notacja używająca kropek ‘.’ w settings.xml (ustawienia lokalne Mavena) pozwala na użycie wartości elementów zdefiniowanych właśnie w tym pliku. Oznacza to, że np. dostanie się do wartości <settings><interactiveMode>true<interactiveMode><settings> będzie możliwe poprzez zastosowanie składni ${settings.interactiveMode}
  4. Wszystkie wartości możliwe do wyciągnięcia z metody java.lang.System.getProperties() są również dostępne jako wartości POM - np. ${java.home}
  5. Wartości zdefiniowane w elemencie properties (np. <properties><myProperty>hello</myProperty><properties>) są również dostępne przy wykorzystaniu składni ${...} (np. ${myProperty}).

Zależności projektowe

W celu korzystania z dobrodziejstw dostępnych frameworków i bibliotek musimy dodać niezbędne pliki JAR do naszego projektu. Tutaj z pomocą przychodzi nam Maven i jego element dependencies. Wewnątrz niego możemy zawrzeć dowolną ilość elementów dependency wskazujące na interesujące nas biblioteki. Ich struktura powinna wyglądać następująco.

1
2
3
4
5
6
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.11</version>
  <scope>test</scope>
</dependency>

Czyli tak jak w przypadku projektu, nad którym pracujemy, musimy zawrzeć groupId, artifactId oraz version. Teraz zaczyna to nabierać sensu, ponieważ dzięki tym elementom możemy zidentyfikować dany projekt, aby z niego po prostu móc skorzystać. Oczywiście Maven jest na tyle sprytny, aby samodzielnie pobierać i dodawać zależności do rozwijanej aplikacji. Jedynym warunkiem musi być to, aby znajdowały się one w centralnym repozytorium Mavena (można to oczywiście zmienić wchodząc do pliku settings.xml i zmieniając element mirrors, aby korzystać z innych repozytoriów).

Czym jest scope w dependency?

Znajduje się jeszcze wewnątrz dependency element scope, który decyduje o tym na jakich etapach będzie widoczna dana zależność i czy będzie propagowana do innych projektów w przypadku użycia w nich naszego kodu. Dostępnych jest pięć wartości scope:

  • compile - domyślny zakres, w którym zależności są dostępne we wszystkich classpaths oraz są propagowane do dalszych projektów
  • provided - podobny do compile, ale wymaga dostarczenia tych zależności w runtime, bo są one tylko dostępne podczas kompilacji oraz testów (nie są przekazywane do dalszych projektów)
  • runtime - określa, że dane zależności są niezbędne tylko podczas uruchomienia aplikacji, a nie do jej kompilacji - są dostępne w runtime oraz podczas testów a nie kompilacji (są przekazywane do dalszych projektów)
  • test - wybrane zależności są tylko potrzebne do kompilacji oraz uruchomienia testów (nie są przekazywane do dalszych projektów)
  • system - podobny do provided, ale trzeba dostarczyć pliki JAR z tymi zależnościami, bo zakładamy, że artefakt jest zawsze dostępny i nie przeszukuje zdalnego repozytorium

Warto zwracać uwagę na ten element, aby nasz wygenerowany plik JAR nie spuchł za bardzo. Nie chcemy dopuścić również do wprowadzenia w projekcie tzw. “dependency hell”.

Pluginy

Ostatnim elementem, na który chciałbym zwrócić w tym artykule jest ten odpowiedzialny za pluginy, czyli plugins. Możemy go zdefiniować na poziomie ogólnym lub wybranego profilu (o profilach powiemy sobie kiedy indziej). Dodajemy go, więc do elementu build oraz opcjonalnie, jak w przykładzie, do pluginManagement (używany do konfiguracji projektu, aby wykorzystać go przez inne aplikacje).

Teraz możemy dodawać nieograniczoną ilość elementów plugin, które podobnie jak dependency, powinny zawierać groupId, artifactId oraz version. W ten sposób oznajmiamy z jakiego dokładnie pluginu chcemy korzystać. Nie chcę się rozwodzić jednak na temat wszystkich elementów dostępnych w elemencie plugin. Skupię się na dwóch, według mnie, najistotniejszych - configuration oraz execution.

Element configuration jest określany indywidualnie dla każdego pluginu. Są to niezbędne właściwości do działania wybranego goal pluginu. Nie są, więc walidowane przez schemat POM. Ich poprawność tak naprawdę będzie dopiero sprawdzana na poziomie uruchomienia Mavena. Warto również nadmienić, że jeżeli dodamy elementy konfiguracji wewnątrz pluginu w głównym pom.xml to będą one dostępne w tym samym pluginie projektu Mavena dziedziczącego po nim. Ten mechanizm wytłumaczymy sobie w innym artykule, gdy będziemy tworzyć podmoduły Mavena dla głównego projektu.

Natomiast element executions jest bardzo przydatny, gdy dany plugin ma wiele goals. Dzięki niemu możemy przypisywać poszczególne goals do różnych phases Mavena. Gdybyśmy chcieli przypisać goal run pluginu maven-antrun-plugin do phase verify musielibyśmy napisać taką oto instrukcję. W ten sposób wpisując w konsolę mvn verify wywoła się wybrany przez nas goal pluginu.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<plugins>
  <plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.1</version>
    <executions>
      <execution>
        <id>echodir</id>
        <goals>
          <goal>run</goal>
        </goals>
        <phase>verify</phase>
        <inherited>false</inherited>
        <configuration>
          <tasks>
            <echo>Build Dir: ${project.build.directory}</echo>
          </tasks>
        </configuration>
      </execution>
    </executions>
  </plugin>
  ...
</plugins>

Przy okazji został jeszcze ustawiony element id pozwalający odróżnić dany blok od pozostałych, inherited określający czy dana konfiguracja może zostać odziedziczona przez inne projekty zawierające ten sam plugin oraz configuration, które poznaliśmy wcześniej.

Podsumowanie

Mam nadzieję, że chociaż w niewielkim stopniu opisałem Ci co się dzieje w pliku pom.xml Mavena. Liczę również, że Cię to nie zniechęciło, a wręcz zachęciło do studiowania innych mechanizmów tego narzędzia do budowania projektów. Nie poruszyłem tutaj wielu istotnych kwestii, ale chciałem dać ogólny pogląd na to co robimy często nieświadomie korzystając z Mavena. W następnych artykułach postaram się zagłębiać bardziej w niektóre aspekty, gdy będzie tego wymagała sytuacja. Daj znać czy ten wpis chociaż trochę uświadomił Ci co się kryje pod niektórymi pojęciami w POM.