Pierwsza przygoda z Mavenem

Zgodnie z obietnicą doczekałeś się kontynuacji poprzedniego wpisu. Dziś powtórzymy to, co zrobiliśmy poprzednim razem – czyli przygotujemy paczkę jar, dzięki której będziesz mógł się podzielić wynikiem swojej pracy z innymi programistami. Ale tym razem zrobimy to dużo wygodniej i przy okazji poznamy bardzo przydatne narzędzie programistyczne

Czym jest Maven

Maven to jest oprogramowanie typu open source stworzone przez Apache – organizację wspierającą wolne oprogramowanie. To oznacza, że z Mavena może korzystać każdy – nawet w celach komercyjnych (więc jeżeli zarabiasz pieniądze na tworzeniu programów w Javie, to zyskiem nie musisz się dzielić z twórcami Mavena). Maven wspiera zarządzanie zależnościami (czyli bibliotekami, z których chcemy korzystać w naszym programie). Wspiera przeróżne akcje związane z szerokorozumianym budowaniem paczki wynikowej. Jest czymś, bez czego stworzenie rozbudowanej aplikacji w Javie będzie bardzo trudne i uciążliwe.

Zanim przejdziemy do kodu, kilka słów wstępu. W świecie Mavena, wszystko jest artefaktem: wynik naszej pracy jest artefaktem, zależności, z których korzystamy w naszym programie są artefaktami. Artefaktami są też dodatki (plug-in) do Mavena. Każdy artefakt jest paczką jar (czasem paczką innego typu), która powstaje w procesie budowania projektu. Każdy artefakt jest opisany trzema parametrami (czasami nazywa się to współrzędnymi artefaktu). Są to:

  • groupId – identyfikator twórcy oprogramowania. Zazwyczaj wszystkie artefakty jednego twórcy mają taki sam groupId, który zwyczajowo jest również określeniem pakietowania w projektach. Czyli na przykład google korzysta z groupId: com.google
  • artifactId – to jest nazwa artefaktu – powinna być unikalna w ramach jednego groupId. Zwyczajowo jest to nazwa projektu
  • version – naturalne jest że, każdy artefakt jest wersjonowany. Wersjonowanie zależy od twórcy danego artefaktu. Najczęściej są to 2 lub 3 liczby oddzielone kropkami. Na przykład 2.8.1. To, który numerek zwiększamy przy kolejnej wersji artefaktu, zależy od istotności zmian w artefakcie względem poprzedniej wersji. Zanim stwierdzimy, że wersja 2.8.1 nadaje się do wydanie (release), to używamy wersji ze słowem SNAPSHOT: 2.8.1-SNAPSHOT. Artefakt w takiej wersji jest zazwyczaj w trakcie developmentu (za dużo anglicyzmów… chodzi o proces rozwijania oprogramowania)

W internecie znajduje się repozytorium artefaktów Mavena: https://mvnrepository.com/. Gdy Maven potrzebuje jakiegoś artefaktu, odpytuje ten serwer (albo inny serwer, na przykład firmowe repozytorium). Tak naprawdę każdy programista może umieścić swój artefakt w tym serwisie, w ten sposób udostępniając swój soft innym programistom.

Biblioteka stworzona przy użyciu Mavena

Po tym długawym wstępie przechodzimy do kodu. Jeżeli chcemy stworzyć nowy projekt dla programu Maven, musimy stworzyć odpowiednią strukturę katalogów, wewnątrz której będzie się znajdował plik pom.xml – plik konfiguracyjny naszego artefaktu. W nim będą zdefiniowane współrzędne naszego artefaktu, wszelkie zależności oraz wszystkie plug-iny, które chcemy wykorzystać w czasie procesu budowania naszej aplikacji. Całe szczęście nie musimy pamiętać, jaka jest wymagana struktura plików i nie musimy tworzyć pliku pom.xml od zera. Możemy wykorzystać archetyp. Jest to taki artefakt Mavenowy, który definiuje szablon projektu. Swoją drogą każdy może stworzyć swój archetyp. My dziś skorzystamy z archetypu maven-archetype-quickstart, którego dokumentację możesz znaleźć tutaj. Zgodnie z tą dokumentacją możemy wygenerować nowy projekt następującym poleceniem

mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4

Jeżeli nie masz jeszcze zainstalowanego Mavena, zajrzyj do mojego poprzedniego wpisu. Wykonanie tej komendy chwilę zajmie. Maven na samym początku będzie pobierał dużo paczek z centralnego (lub firmowego) repozytorium. Całe szczęście dzieje się tak tylko za pierwszym razem. Ponieważ Maven zapisuje wszystkie pobrane artefakty w katalogu ~/.m2. Czyli w katalogu .m2 w Twoim katalogu domowym. Ta kropka na początku nazwy, oznacza, że katalog byłby ukryty w systemach linuksowych. W Windowsie jest normalnie widoczny. Jeżeli Maven potrzebuje jakiegoś artefaktu, to najpierw sprawdza w katalogu ~/.m2 i jeżeli tam nie ma wymaganego artefaktu, to odpytuje repozytorium w sieci i pobiera stamtąd potrzebny plik. W międzyczasie pobieranie artefaktów się skończyło i teraz proces tworzenia projektu pyta nas o współrzędne naszego artefaktu:

Define value for property 'groupId': pl.backlog.green
Define value for property 'artifactId': my-library
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' pl.backlog.green: :
Confirm properties configuration:
groupId: pl.backlog.green
artifactId: my-library
version: 1.0-SNAPSHOT
package: pl.backlog.green
 Y: : Y

i następnie proces tworzenia artefaktu kończy się w zupełnie Mavenowy sposób:

[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.4
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: pl.backlog.green
[INFOm] Parameter: artifactId, Value: my-library
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: pl.backlog.green
[INFO] Parameter: packageInPathFormat, Value: pl/backlog/green
[INFO] Parameter: package, Value: pl.backlog.green
[INFO] Parameter: groupId, Value: pl.backlog.green
[INFO] Parameter: artifactId, Value: my-library
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Project created from Archetype in dir: C:\Users\pawel\code\my-library
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  10:41           min
[INFO] Finished at: 2020-10-20T20:16:53+02:00
[INFO] ------------------------------------------------------------------------

Maven wygenerował nam następującą strukturę plików i katalogów:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code
$ tree
.
└── my-library
    ├── pom.xml
    └── src
        ├── main
        │   └── java
        │       └── pl
        │           └── backlog
        │               └── green
        │                   └── App.java
        └── test
            └── java
                └── pl
                    └── backlog
                        └── green
                            └── AppTest.java

12 directories, 3 files

Najważniejszy jest plik pom.xml:

<?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>pl.backlog.green</groupId>
  <artifactId>my-library</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>my-library</name>
  <!-- FIXME change it to the project's website -->
  <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><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
      <plugins>
        <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
        <plugin>
          <artifactId>maven-clean-plugin</artifactId>
          <version>3.1.0</version>
        </plugin>
        <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
        <plugin>
          <artifactId>maven-resources-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-compiler-plugin</artifactId>
          <version>3.8.0</version>
        </plugin>
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.22.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-jar-plugin</artifactId>
          <version>3.0.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-install-plugin</artifactId>
          <version>2.5.2</version>
        </plugin>
        <plugin>
          <artifactId>maven-deploy-plugin</artifactId>
          <version>2.8.2</version>
        </plugin>
        <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
        <plugin>
          <artifactId>maven-site-plugin</artifactId>
          <version>3.7.1</version>
        </plugin>
        <plugin>
          <artifactId>maven-project-info-reports-plugin</artifactId>
          <version>3.0.0</version>
        </plugin>
      </plugins>
    </pluginManagement>
  </build>
</project>

Ale można go znacząco uprościć:

<?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>pl.backlog.green</groupId>
  <artifactId>my-library</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>my-library</name>
  <!-- FIXME change it to the project's website -->
  <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>

</project>

Wartość 1.7 z linijki 17. i 18. zastąpmy wersją 14. W linijkach 22-27 widzimy, że nasz projekt będzie korzystał z artefaktu junit. Linijka 26 mówi, że będziemy korzystali z tej zależności tylko na potrzeby testów. Nie jest to najnowsza wersja tej biblioteki, ale narazie taką zostawmy.

Zobaczmy, jak wyglądają pliki java w tym projekcie. Zacznijmy od App.java:

package pl.backlog.green;

/**
 * Hello world!
 *
 */
public class App 
{
    public static void main( String[] args )
    {
        System.out.println( "Hello World!" );
    }
}

Widzimy, że jest to możliwie najprostszy program w języku Java. Teraz zobaczmy plik AppTest.java:

package pl.backlog.green;

import static org.junit.Assert.assertTrue;

import org.junit.Test;

/**
 * Unit test for simple App.
 */
public class AppTest 
{
    /**
     * Rigorous Test :-)
     */
    @Test
    public void shouldAnswerWithTrue()
    {
        assertTrue( true );
    }
}

Jest to bardzo prosta klasa testowa. Zauważ, że obie klasy są w takich samych pakietach, ale te pakiety są zdefiniowane w dwóch katalogach main i test. Maven właśnie w taki sposób logicznie dzieli kod produkcyjny od testów.

Może zanim przejdziemy dalej powiedzmy sobie kilka słów o testach jednostkowych (po angielsku nazywa się je unit tests – i nazwa biblioteki sugeruje, że jest to biblioteka do testów jednostkowych w Javie). Każda klasa, którą piszemy powinna posiadać swoje testy (zwyczajowo nazwę klasy testowej tworzy się przez dodanie słowa Test do nazwy klasy). Oczywiście są różne typy testów i w tym momencie skupimy się na testach jednostkowych, jako że są najprostsze. Każdy taki test powinien testować najmniejszą możliwą część klasy. Najczęściej testuje się pojedyncze metody publiczne. Czyli tworzy się obiekt testowany. Następnie ustawia się jego stan i wykonuje się testowaną metodę. Później sprawdza się, czy metoda zwróciła oczekiwaną wartość lub/i czy odpowiednio zmieniła stan obiektu. Takie sprawdzenia robi się za pomocą asercji. Jeżeli asercja nie jest spełniona, jest rzucany odpowiedni wyjątek, dzięki któremu junit traktuje test jako sfailowany (różni się to od sytuacji rzucenia innym wyjątkiem, kiedy test jest oznaczony jako error). Za chwilę pokażę to na przykładzie.

Usuńmy te domyślnie wygenrowane pliki z naszego projektu oraz skopiujmy do naszego projektu kod klasy GeniusLibrary którą napisałem wcześniej:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ cp ~/OneDrive/Desktop/myLib/pl/backlog/green/GeniusLibrary.java src/main/java/pl/backlog/green/

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ rm src/main/java/pl/backlog/green/App.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ rm src/test/java/pl/backlog/green/AppTest.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ tree
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── pl
    │           └── backlog
    │               └── green
    │                   └── GeniusLibrary.java
    └── test
        └── java
            └── pl
                └── backlog
                    └── green

11 directories, 2 files

Dodajmy też klasę testową do naszej klasy:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ touch src/test/java/pl/backlog/green/GeniusLibraryTest.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ tree
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── pl
    │           └── backlog
    │               └── green
    │                   └── GeniusLibrary.java
    └── test
        └── java
            └── pl
                └── backlog
                    └── green
                        └── GeniusLibraryTest.java

11 directories, 3 files

o zawartości

package pl.backlog.green;

import static org.junit.Assert.*;
import org.junit.Test;


public class GeniusLibraryTest {
    @Test
    public void methodShouldReturnCorrectValue() {
        // given
        
        // when
        double area = GeniusLibrary.calculateAreaOfCircle(4.0);
        
        // then
        assertEquals(50.2655, area);
    }
}

Testy jednostkowo zazwyczaj dzielimy na trzy sekcje:

  • given – w tej sekcji tworzmy obiekt i ustawiamy go w stan, który chcemy przetestować. Akurat w tym przypadku ta sekcja jest pusta
  • when – sekcja, w które wykonujemy testowaną akcję
  • then – sekcja, w której sprawdzamy, czy wynik testowanej metody jest zgodny z oczekiwaniami (ta sekcja zawiera asercje)

Do testów zaraz dojdziemy. Teraz jest ten moment, w którym zaczniemy wykorzystywać Mavena. Z każdym projektem Mavena związany jest Główny Cykl Życia, który jest zbudowany z faz. Faza opisuje akcje, które wykonujemy na projekcie. Cykl ludzkiego życia też jest podzielony na fazy:

  • narodziny
  • dzieciństwo
  • edukacja
  • dorosłość
  • śmierć

Różnica jest taka, że projekt może wielokrotnie wracać do wcześniejszej fazy życia. Człowiek tak jeszcze nie może. Zrobimy teraz przegląd kolejnych faz z głównego cyklu życia aplikacji mavenowej.

validate

Najwcześniejsza faza w cyklu życia aplikacji. Możemy wywołać tę fazę następującym poleceniem:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ mvn validate
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< pl.backlog.green:my-library >---------------------
[INFO] Building my-library 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.117 s
[INFO] Finished at: 2020-10-20T21:29:24+02:00
[INFO] ------------------------------------------------------------------------

Ważne jest to, żeby to polecenie wykonać w tym samym katalogu, w którym jest plik pom.xml projektu, nad którym pracujemy. Maven w tej fazie sprawdza, czy nasz projekt jest właściwie stworzony i czy zawarliśmy w pom.xml wszystkie potrzebne informacje (w tym również, czy plik pom.xml jest prawidłowy)

compile

W tej fazie Maven kompiluje nasze klasy:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ mvn compile
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< pl.backlog.green:my-library >---------------------
[INFO] Building my-library 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ my-library ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\pawel\code\my-library\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ my-library ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to C:\Users\pawel\code\my-library\target\classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.219 s
[INFO] Finished at: 2020-10-20T21:34:53+02:00
[INFO] ------------------------------------------------------------------------

To być może nie rzuca się w oczy w powyższych logach, ale gdy wywołujemy fazę compile, to najpierw wywołuje się faza wcześniejsza, czyli validate. Stąd nazwa „cykl życia”. Oczywiście, gdy będziesz to wykonywać pierwszy raz, Maven będzie ściągał sporo potrzebnych artefaktów.

Ważne jest to, że ten krok odpowiada za kompilację naszego kodu. Więc w odpowiedni sposób wykonuje program javac i odpowiednio ustawia w nim wartość classpath. Po wykonaniu tego kroku dostajemy zupełnie nowy katalog w naszym projekcie:

$ tree
.
├── pom.xml
├── src
│   ├── main
│   │   └── java
│   │       └── pl
│   │           └── backlog
│   │               └── green
│   │                   └── GeniusLibrary.java
│   └── test
│       └── java
│           └── pl
│               └── backlog
│                   └── green
│                       └── GeniusLibraryTest.java
└── target
    ├── classes
    │   └── pl
    │       └── backlog
    │           └── green
    │               └── GeniusLibrary.class
    ├── generated-sources
    │   └── annotations
    └── maven-status
        └── maven-compiler-plugin
            └── compile
                └── default-compile
                    ├── createdFiles.lst
                    └── inputFiles.lst

W nowym katalogu target powinno się nam rzucić w oczy, że jest katalog classes, który zawiera skompilowane klasy naszego projektu (bez klas testowych) we właściwej strukturze katalogów. Ciekawy jest też katalog maven-status. W nim widzimy, że w czasie kompilacji Maven wykorzystał plugin maven-compiler-plugin. Możemy zobaczyć, jakie pliki skompilował i jakie pliki były wynikiem kompilacji:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ cat target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
pl\backlog\green\GeniusLibrary.class

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ cat target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
C:\Users\pawel\code\my-library\src\main\java\pl\backlog\green\GeniusLibrary.java

Program cat jest prostym programem konsolowym do wyświetlania zawartości plików (wyświetla wszystkie rodzaje plików, ale nie zawsze w formie jakiej oczekujesz).

clean

Ta faza nie należy do głównego cyklu życia projektu. Pozwala ona w całości usunąć katalo target. Jeżeli tego nie zrobimy, Maven w kolejnych kompilacjach może zaoszczędzić sobie czas i nie przekompilować czegoś. Z reguły Maven jest dość inteligentny w tej kwestii, ale czasami przez niewyczyszczony katalog target można nabawić się dziwnych błędów.

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ cat target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
C:\Users\pawel\code\my-library\src\main\java\pl\backlog\green\GeniusLibrary.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ mvn clean
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< pl.backlog.green:my-library >---------------------
[INFO] Building my-library 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ my-library ---
[INFO] Deleting C:\Users\pawel\code\my-library\target
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.288 s
[INFO] Finished at: 2020-10-20T21:46:21+02:00
[INFO] ------------------------------------------------------------------------

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ tree
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── pl
    │           └── backlog
    │               └── green
    │                   └── GeniusLibrary.java
    └── test
        └── java
            └── pl
                └── backlog
                    └── green
                        └── GeniusLibraryTest.java

11 directories, 3 files

 

test

Jak w przypadku fazy compile, gdy zawołamy fazę test, to wykonają się wszystkie fazy, które są przed tą fazą w cyklu głównym projektu. Wykorzystamy fakt, że Maven pozwala użyć wielu faz w jednym wywołaniu:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ mvn clean test
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< pl.backlog.green:my-library >---------------------
[INFO] Building my-library 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ my-library ---
[INFO] Deleting C:\Users\pawel\code\my-library\target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ my-library ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\pawel\code\my-library\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ my-library ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to C:\Users\pawel\code\my-library\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ my-library ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\pawel\code\my-library\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ my-library ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to C:\Users\pawel\code\my-library\target\test-classes
[WARNING] /C:/Users/pawel/code/my-library/src/test/java/pl/backlog/green/GeniusLibraryTest.java: C:\Users\pawel\code\my-library\src\test\java\pl\backlog\green\GeniusLibraryTest.java uses or overrides
a deprecated API.
[WARNING] /C:/Users/pawel/code/my-library/src/test/java/pl/backlog/green/GeniusLibraryTest.java: Recompile with -Xlint:deprecation for details.
[INFO]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ my-library ---
[INFO] Surefire report directory: C:\Users\pawel\code\my-library\target\surefire-reports

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running pl.backlog.green.GeniusLibraryTest
Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.059 sec <<< FAILURE!
methodShouldReturnCorrectValue(pl.backlog.green.GeniusLibraryTest)  Time elapsed: 0.007 sec  <<< FAILURE!
java. lang.AssertionError: Use assertEquals(expected, actual, delta) to compare floating-point numbers
        at org.junit.Assert.fail(Assert.java:88)
        at org.junit.Assert.assertEquals(Assert.java:576)
        at org.junit.Assert.assertEquals(Assert.java:565)
        at pl.backlog.green.GeniusLibraryTest.methodShouldReturnCorrectValue(GeniusLibraryTest.java:16)
        at java. base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java. base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
        at java. base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java. base/java. lang.reflect.Method.invoke(Method.java:564)
        at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
        at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
        at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
        at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
        at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
        at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
        at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
        at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
        at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
        at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
        at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
        at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
        at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
        at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
        at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
        at java. base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java. base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
        at java. base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java. base/java. lang.reflect.Method.invoke(Method.java:564)
        at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
        at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
        at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
        at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
        at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)


Results :

Failed tests: methodShouldReturnCorrectValue(pl.backlog.green.GeniusLibraryTest): Use assertEquals(expected, actual, delta) to compare floating-point numbers

Tests run: 1, Failures: 1, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.127 s
[INFO] Finished at: 2020-10-20T21:49:36+02:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.12.4:test (default-test) on project my-library: There are test failures.
[ERROR]
[ERROR] Please refer to C:\Users\pawel\code\my-library\target\surefire-reports for the individual test results.
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

W oczy na pewno rzuca się wielki napis: BUILD FAILURE. Jeżeli zagłębimy się w powyższe logi zobaczymy jeszcze: Failed tests: methodShouldReturnCorrectValue(pl.backlog.green.GeniusLibraryTest). Co jest dość oczywistym wskazaniem na to, który dokładnie test nie został zaliczony. Mamy też wskazówkę dlaczego test nie został zaliczony: Use assertEquals(expected, actual, delta) to compare floating-point numbers. Wynika to z tego, że porównywanie liczb zmiennoprzecinkowych powinno być odporne na błędy zaokrągleń. Jest to nieodłączny element związany z obliczeniami numerycznymi. Do metody assertEquals powinniśmy dodać trzeci argument, w którym określimy z jaką dokładnością chcemy porównać liczby.

Ale zanim poprawimy ten błąd w teście, spójrzmy na katalog target:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ tree target/
target/
├── classes
│   └── pl
│       └── backlog
│           └── green
│               └── GeniusLibrary.class
├── generated-sources
│   └── annotations
├── generated-test-sources
│   └── test-annotations
├── maven-status
│   └── maven-compiler-plugin
│       ├── compile
│       │   └── default-compile
│       │       ├── createdFiles.lst
│       │       └── inputFiles.lst
│       └── testCompile
│           └── default-testCompile
│               ├── createdFiles.lst
│               └── inputFiles.lst
├── surefire-reports
│   ├── pl.backlog.green.GeniusLibraryTest.txt
│   └── TEST-pl.backlog.green.GeniusLibraryTest.xml
└── test-classes
    └── pl
        └── backlog
            └── green
                └── GeniusLibraryTest.class

Widzimy kilka różnic względem tego, co mieliśmy po kroku compile:

  1. Pojawił się katalog test-classes, który zawiera skompilowane klasy testowe (we właściwej strukturze katalogów)
  2. Mamy raport z kompilacji klas testowych (która została wykonana przez maven-compiler-plugin)
  3. I mamy katalog surefire-reports w którym są dwa pliki:
    1. pl.backlog.green.GeniusLibraryTest.txt, który zawiera część tego, co Maven wypisał na standardowe wyjście. Między innymi podsumowanie wykonania testów zdefiniowanych w klasie GeniusLibraryTest
    2. TEST-pl.backlog.green.GeniusLibraryTest.xml, jest to plik XML-owy wykorzystywany przez IDE do ładnej prezentacji przebiegu wykonania testów

Moim zdaniem warte odnotowania jest, że za uruchomienie testów jednostkowych odpowiada maven-surefire-plugin, stąd nazwa katalogu z podsumowaniem testów.

Zanim pójdziemy dalej, przyjrzyjmy się jeszcze jednej linijce: Tests run: 1, Failures: 1, Errors: 0, Skipped: 0. To oznacza, że jeden test został uruchomiony i ten jedyny test zakończył się niepowodzeniem. Testy można opuszczać (skip – służy do tego annotacja @Disabled). Gdy w czasie uruchomienia testu zostanie zgłoszony wyjątek, to test jest oznaczony jako Error.

Gdy poprawimy nasz test:

package pl.backlog.green;

import static org.junit.Assert.*;
import org.junit.Test;


public class GeniusLibraryTest {
    @Test
    public void methodShouldReturnCorrectValue() {
        // given
        
        // when
        double area = GeniusLibrary.calculateAreaOfCircle(4.0);
        
        // then
        assertEquals(50.2655, area, 0.001);
    }
}

to wywołanie mvn clean test zakończy się (nie wklejam całości):

Results :

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.165 s
[INFO] Finished at: 2020-10-20T22:07:57+02:00
[INFO] ------------------------------------------------------------------------

I to jest to, co tygryski lubią najbardziej – wszystkie testy uruchomione, żaden nie zgłosił problemu.

package

Faza package tworzy paczkę jar z naszym projektem (czyli to jest pierwszy artefakt, który otrzymamy):

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ mvn clean package

Czyli w katalogu target pojawi się nowa zawartość:

$ tree target
target
├── classes
│   └── pl
│       └── backlog
│           └── green
│               └── GeniusLibrary.class
├── generated-sources
│   └── annotations
├── generated-test-sources
│   └── test-annotations
├── maven-archiver
│   └── pom.properties
├── maven-status
│   └── maven-compiler-plugin
│       ├── compile
│       │   └── default-compile
│       │       ├── createdFiles.lst
│       │       └── inputFiles.lst
│       └── testCompile
│           └── default-testCompile
│               ├── createdFiles.lst
│               └── inputFiles.lst
├── my-library-1.0-SNAPSHOT.jar
├── surefire-reports
│   ├── pl.backlog.green.GeniusLibraryTest.txt
│   └── TEST-pl.backlog.green.GeniusLibraryTest.xml
└── test-classes
    └── pl
        └── backlog
            └── green
                └── GeniusLibraryTest.class

20 directories, 10 files

Został wygenerowany plik: my-library-1.0-SNAPSHOT.jar.

verify

Ten krok sprawdza, czy wygenerowana paczka jar jest prawidłowa. Oprócz tego wykonywane są testy integracyjne (które testują czy komponenty naszej aplikacji działają prawidłowo ze sobą oraz czy nasza aplikacja działa prawidłowo z innymi aplikacjami).

install

To jest faza, której nadużywam. Mam na myśli, że to tę fazę traktuję jako domyślną, gdy chcę zbudować mój projekt. Ten krok umieszcza zbudowany w poprzednim kroku plik jar w lokalnym repozytorium (czyli w katalogu ~/.m2).

Sprawdźmy to:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ ls ~/.m2/repository/
antlr/  asm/  backport-util-concurrent/  classworlds/  com/  commons-cli/  commons-codec/  commons-collections/  commons-io/  commons-lang/  commons-logging/  junit/  log4j/  net/  org/

po wykonaniu komendy $ mvn clean install ten sam katalog ma następującą zawartość:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ ls ~/.m2/repository/
antlr/  asm/  backport-util-concurrent/  classworlds/  com/  commons-cli/  commons-codec/  commons-collections/  commons-io/  commons-lang/  commons-logging/  junit/  log4j/  net/  org/  pl/

Mam nadzieję, że widzisz, że pojawił się katalog pl, który pasuje do groupId mojego porjektu. Tak wygląda zawartość tego katalogu:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ tree ~/.m2/repository/pl
/c/Users/pawel/.m2/repository/pl
└── backlog
    └── green
        └── my-library
            ├── 1.0-SNAPSHOT
            │   ├── _remote.repositories
            │   ├── maven-metadata-local.xml
            │   ├── my-library-1.0-SNAPSHOT.jar
            │   └── my-library-1.0-SNAPSHOT.pom
            └── maven-metadata-local.xml

4 directories, 5 files

deploy

Ten krok służy do wysłania swojej paczki jar do repozytorium plików. Nigdy nie dzieliłem się ze światem swoimi artefaktami, więc nie wiem, jaka konfiguracja jest wymagana. Ale myślę, że już wkrótce się to zmieni.

site

Ten krok równieżnie należy do głównego cyklu projektu. Generuje on stronę html z dokumentacją o naszym projekcie. Aby wykonać ten krok należy do pliku pom.xml dodać następujący fragment (wewnątrz tagu project):

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-site-plugin</artifactId>
      <version>3.7.1</version>
    </plugin>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-project-info-reports-plugin</artifactId>
      <version>3.0.0</version>
    </plugin>
  </plugins>
</build>

Po wykonaniu mvn site w katalogu target pojawia się katalog site:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-library (master)
$ tree target/site/
target/site/
├── css
│   ├── maven-base.css
│   ├── maven-theme.css
│   ├── print.css
│   └── site.css
├── dependencies.html
├── dependency-info.html
├── images
│   ├── close.gif
│   ├── collapsed.gif
│   ├── expanded.gif
│   ├── external.png
│   ├── icon_error_sml.gif
│   ├── icon_info_sml.gif
│   ├── icon_success_sml.gif
│   ├── icon_warning_sml.gif
│   ├── logos
│   │   ├── build-by-maven-black.png
│   │   ├── build-by-maven-white.png
│   │   └── maven-feather.png
│   └── newwindow.png
├── index.html
├── plugin-management.html
├── plugins.html
├── project-info.html
└── summary.html

3 directories, 23 files

Jak widzisz, jest plik index.html, który jest stroną główną Twojej dokumentacji. Ten wpis robi się już zdecydowanie za długi, więc temat dokumentacji zostawiam na przyszłość.

Program wykorzystujący bibliotekę stworzony przy wykorzystaniu Mavena

Jak już przed chwilą napisałem, ten wpis zrobił się dużo dłuższy niż się spodziewałem. Więc szybko biegnijmy do celu. Tworzymy kolejny projekt Mavenowy korzystając z tego samego archetypu, co poprzednio:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code
$ mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4
[INFO] Scanning for projects...
[INFO]
[INFO] ------------------< org.apache.maven:standalone-pom >-------------------
[INFO] Building Maven Stub Project (No POM) 1
[INFO] --------------------------------[ pom ]---------------------------------
[INFO]
[INFO] >>> maven-archetype-plugin:3.2.0:generate (default-cli) > generate-sources @ standalone-pom >>>
[INFO]
[INFO] <<< maven-archetype-plugin:3.2.0:generate (default-cli) < generate-sources @ standalone-pom <<<
[INFO]
[INFO]
[INFO] --- maven-archetype-plugin:3.2.0:generate (default-cli) @ standalone-pom ---
[INFO] Generating project in Interactive mode
[INFO] Archetype repository not defined. Using the one from [org.apache.maven.archetypes:maven-archetype-quickstart:1.4] found in catalog remote
Define value for property 'groupId': pl.acme.vip
Define value for property 'artifactId': my-app
Define value for property 'version' 1.0-SNAPSHOT: :
Define value for property 'package' pl.acme.vip: :
Confirm properties configuration:
groupId: pl.acme.vip
artifactId: my-app
version: 1.0-SNAPSHOT
package: pl.acme.vip
 Y: : Y
[INFO] ----------------------------------------------------------------------------
[INFO] Using following parameters for creating project from Archetype: maven-archetype-quickstart:1.4
[INFO] ----------------------------------------------------------------------------
[INFO] Parameter: groupId, Value: pl.acme.vip
[INFO] Parameter: artifactId, Value: my-app
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Parameter: package, Value: pl.acme.vip
[INFO] Parameter: packageInPathFormat, Value: pl/acme/vip
[INFO] Parameter: package, Value: pl.acme.vip
[INFO] Parameter: groupId, Value: pl.acme.vip
[INFO] Parameter: artifactId, Value: my-app
[INFO] Parameter: version, Value: 1.0-SNAPSHOT
[INFO] Project created from Archetype in dir: C:\Users\pawel\code\my-app
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  25.453 s
[INFO] Finished at: 2020-10-20T23:00:03+02:00
[INFO] ------------------------------------------------------------------------

I szybko go doprowadzamy do bazowej postaci:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code
$ cd my-app/

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app
$ rm src/main/java/pl/acme/vip/App.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app
$ rm src/test/java/pl/acme/vip/AppTest.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app
$ tree
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── pl
    │           └── acme
    │               └── vip
    └── test
        └── java
            └── pl
                └── acme
                    └── vip

11 directories, 1 file

A plik pom.xml edytujemy do postaci:

<?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>pl.acme.vip</groupId>
  <artifactId>my-app</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>my-app</name>

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

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

 
</project>

Gdy skopiujemy nasz program z początku wpisu:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app
$ cp ~/OneDrive/Desktop/myApp/pl/acme/vip/Calculator.java src/main/java/pl/acme/vip/

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app
$ tree
.
├── pom.xml
└── src
    ├── main
    │   └── java
    │       └── pl
    │           └── acme
    │               └── vip
    │                   └── Calculator.java
    └── test
        └── java
            └── pl
                └── acme
                    └── vip

11 directories, 2 files

To kompilacja

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app (master)
$ mvn clean compile

się nie powiedzie (pozwolę sobie już nie wklejać całości):

[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------

To jest dość oczywiste. Musimy dodać nową zależność do naszego pliku pom.xml:

<dependency>
  <groupId>pl.backlog.green</groupId>
  <artifactId>my-library</artifactId>
  <version>1.0-SNAPSHOT</version>
</dependency>

Teraz kompilacja się powiedzie:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app (master)
$ mvn clean compile
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< pl.acme.vip:my-app >-------------------------
[INFO] Building my-app 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ my-app ---
[INFO] Deleting C:\Users\pawel\code\my-app\target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ my-app ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory C:\Users\pawel\code\my-app\src\main\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ my-app ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to C:\Users\pawel\code\my-app\target\classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.430 s
[INFO] Finished at: 2020-10-20T23:10:42+02:00
[INFO] -----------------------              -------------------------------------------------

Korzystając z Mavena możemy również uruchomić nasz program:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app (master)
$ mvn exec:java -Dexec.mainClass="pl.acme.vip.Calculator"
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------------< pl.acme.vip:my-app >-------------------------
[INFO] Building my-app 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- exec-maven-plugin:3.0.0:java (default-cli) @ my-app ---
Podaj promien kola: 4
Pole kola o promieniu 4.0 wynosi 50.26548245743669
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.004 s
[INFO] Finished at: 2020-10-20T23:13:03+02:00
[INFO] ------------------------------------------------------------------------

Na tej stronie możesz doczytać, jak uprościć uruchamianie aplikacji przez użycie odpowiedniego plugina.

No tak, ale żeby w ten sposób uruchomić program, potrzebny jest kod źródłowy. Gdy wydałeś polecenie $ mvn clean install, to po drodze wykonała się faza package. Oznacza to, że w katalogu target znajduje się paczka jarartefakt, który stworzyliśmy.

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app (master)
$ ls target/
classes/            maven-archiver/  my-app-1.0-SNAPSHOT.jar
generated-sources/  maven-status/

Możemy spróbować go uruchomić:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app (master)
$ java -jar target/my-app-1.0-SNAPSHOT.jar
no main manifest attribute, in target/my-app-1.0-SNAPSHOT.jar

Taka próba zakończy się niepowodzeniem. Dlaczego? Bo nigdzie, w czasie budowania aplikacji, nie zdefiniowaliśmy która klasa jest klasą główną. Możemy to zrobić bardzo łatwo wykorzystując Mavena. Wystarczy dodać do pliku pom.xml (wewnątrz taga project) definicję użycia następującego plug-ina:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifest>
            <mainClass>
              pl.acme.vip.Calculator
            </mainClass>
          </manifest>
        </archive>
      </configuration>
    </plugin>
  </plugins>	  
</build>

Gdy ponownie zbudujemy artefakt, będziemy mogli go uruchomić:

pawel@DESKTOP-0BB9FUV MINGW64 ~/code/my-app (master)
$ java -jar target/my-app-1.0-SNAPSHOT.jar
Podaj promien kola: 4
Exception in thread "main" java.lang.NoClassDefFoundError: pl/backlog/green/GeniusLibrary
        at pl.acme.vip.Calculator.main(Calculator.java:11)
Caused by: java.lang.ClassNotFoundException: pl.backlog.green.GeniusLibrary
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:606)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:168)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 1 more

Ale uruchomienie nie powiedzie się. Dostaniemy wyjątek spowodowany brakiem definicji klasy GeniusLibrary. Możemy ją dodać przez dodanie dwóch linijek wewnątrz taga manifest:

<addClasspath>true</addClasspath>
<classpathPrefix>libs/</classpathPrefix>

I jeżeli wszystkie potrzebne artefakty mamy w katalogu lib, to utworzy się nam artefakt ze wszystkimi potrzebnymi paczkami jar w środku. Ale w tym przypadku, w ogóle nie korzystamy z dobrodziejstwa Mavena, który zarządza zależnościami do projektu. Dużo lepsze jest użycie plugina maven-assembly-plugin:

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-assembly-plugin</artifactId>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>single</goal>
          </goals>
          <configuration>
            <archive>
              <manifest>
                <mainClass>
                  pl.acme.vip.Calculator
                </mainClass>
              </manifest>
            </archive>
            <descriptorRefs>
              <descriptorRef>jar-with-dependencies</descriptorRef>
            </descriptorRefs>
          </configuration>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Ten plug-in wymaga kilku dodatkowych słów, nieprawdaż? Tak więc w linii 8 definiujemy w której fazie cyklu życia aplikacji ma się wykonać goal zdefiniowany w linii 10. Każdy plug-in posiada goale, które w rzeczywistości definiują konkretną akcję. Goal zdefiniowany w plug-inie można wykonać osobno. Dla plug-ina maven-assembly-plugin są zdefiniwane dwa goale:

  • assembly:help – wyświetla tekstowy opis tego plug-ina
  • assembly:single – wykonuje określoną paczkę wynikową

Sekwencja wykonania goala składa się z trzech części:

  • nazwy plug-ina
  • dwukropka
  • nazwy goala

W tagu manifest definiujemy zawartość pliku MANIFEST.MF w tworzonym przez plug-in archiwum. Za pomocą <descriptorRef>jar-with-dependencies</descriptorRef> definiujemy jak będzie się nazywała wynikowa paczka. Więcej informacji o tym plug-inie można znaleźć na stronie z dokumentacją.

Oczywiście nie napisałem definicji użycia plug-inów z głowy. Posiłkowałem się Internetem. Mam nadzieję, że Ty kiedyś będziesz sie posiłkował moim blogiem:)

Podsumowanie

Nawet po podzieleniu artykułu na dwie części, dzisiejszy wpis wyszedł naprawdę potężny. Ale w usprawiedliwieniu powiem, że liznęliśmy kilka tematów i w przyszłości część tematów rozwinę w osobnych wpisach.

Mam nadzieję, że mój wpis okaże się przydatny, przynajmniej dla jednej osoby. Mam nadzieję, że choć dwie osoby dotarły do końca tego artykułu (w sensie ktoś oprócz mojej Siostry).

Oba projekty, nad którymi pracowałem w tym artykule są oczywiście dostępne na githubie:

Post Scriptum

Tutaj jest opis, jak można zainstalować paczki jar przesłane przez kogoś luzem.

 

Aspirujący twórca internetowy, który zna się na programowaniu i chce się dzielić wiedzą

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Scroll to top