Jak można się dzielić skompilowanymi programami napisanymi w Javie

Dzisiejszy wpis miał dotyczyć Mavena. Moja Siostra – zdolna i utalentowana programistka Pythona – ostatnio dostała zadanie związane z aplikacją w Javie. I o ile z Javą nie miała najmniejszego problemu, to nie wiedziała jak ugryźć Mavena. A jak wiedzą miłośnicy Javy – nie da się napisać większego programu w Javie bez korzystania z Mavena (albo Gradle’a). Jestem już dość starym programistą – bo na studiach uczyłem się Anta, więc wybacz, że opowiadam o Mavenie. Gradle’a też się kiedyś nauczę.

Dziś zacząłem robić wpis o Mavenie. Ale jak to ja, chciałem zacząć od początku. Artykuł urósł do niebezpiecznych rozmiarów, więc zdecydowałem się go podzielić na dwie części. Pierwsza, którą właśnie czytasz, opisuje jakie kroki należy podjąć, aby ręcznie (czyli bez Mavena czy jakiegokolwiek innego oprogramowania) dystrybuować skompilowany kod w Javie tak, aby inni użytkownicy mogli z niego skorzystać.

Już wkrótce pojawi się druga część artykułu, która przedstawi jak kroki wykonane dzisiaj można uprościć przez użycie Mavena.

Kompilacja ręczna

Jeżeli w swoim życiu napisałeś choć jedną aplikację w Javie, to powinieneś wiedzieć, że program, na przykład taki:

public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello Java!");
    }
}

najpierw trzeba skompilować, przy użyciu programu javac:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop
$ javac Hello.java

żeby w wyniku dostać plik z rozszerzeniem .class:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop
$ ls
Hello.class  Hello.java

który tak naprawdę zostanie użyty do uruchomienia klasy Hello:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop
$ java Hello
Hello Java!

I zasadniczo, możemy udostępnić plik Hello.class i każdy, kto ma u siebie zainstalowane JRE (Java Runtime Environment) będzie w stanie uruchomić nasz program. W pakiecie JRE jest program java. Żeby mieć kompilator, czyli program javac, musisz zainstalować JDK (Java Development Kit). Instrukcję, jak zainstalować JDK, znajdziesz w moim poprzednim wpisie.

Problem zaczyna się wtedy, gdy nasz program ma więcej niż jedną klasę. Najczęściej program napisany w Javie dystrybuuje się w plikach z rozszerzeniem .jar. Są to tak naprawdę archiwa ZIP, które posiadają w sobie potrzebne klasy (czyli pliki z rozszerzeniem .class) oraz specjalny plik o nazwie MANIFEST.MF. Taki plik jar możemy przygotować sobie sami. Będziemy do tego potrzebowali plik tekstowy o zawartości

Main-Class: Hello

Plik koniecznie musi się kończyć znakiem nowej linii. Plik może nazywać się dowolnie, ja go nazwałem myfile.mf:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop
$ ls
Hello.class  Hello.java  myfile.mf

Teraz stworzę paczkę: ziels-app.jar. Ten plik będę mógł wysłać innym ludziom, aby mogli uruchamiać mój program. Aby stworzyć taki plik, wykonuję polecenie:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop
$ jar -cvmf myfile.mf ziels-app.jar Hello.class
added manifest
adding: Hello.class(in = 415) (out= 284)(deflated 31%)

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop
$ ls
Hello.class Hello.java myfile.mf ziels-app.jar

I teraz mogę uruchomić mój program:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop
$ java -jar ziels-app.jar
Hello Java!

Biblioteka stworzona ręcznie

Rozważmy teraz trochę bardziej skomplikowany przypadek użycia. Chcę stworzyć bibliotekę. Czyli kod, w którym nie ma metody main, a który inni programiści będą mogli używać w swoich programach. Wyobraź sobie, że jestem genialnym matematykiem, który wymyślił wzór na pole powierzchni koła. Jestem wspaniałomyślny, więc napisałem bibliotekę. W tym wypadku będzie to jedna klasa z jedną funkcją:

package pl.backlog.green;

public class GeniusLibrary {
    public static double calculateAreaOfCircle(double radius) {
        return radius * radius * Math.PI;
    }
}

I chcę udostępnić innym programistom moją funkcję. Prześledzimy teraz krok po kroku, co powinienem zrobić. Tak jak wcześniej, powinienem zacząć od kompilacji. Zwróć uwagę, że moją klasę GeniusLibrary umieściłem w pakiecie pl.backlog.green. To znaczy, że będę potrzebował struktury katalogów:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myLib
$ tree
.
└── pl
    └── backlog
        └── green
            └── GeniusLibrary.java

3 directories, 1 file

Na potrzeby kompilacji nie jest może to zbyt istotne, ale struktura katalogów będzie nam jeszcze potrzebna. Jak już napisałem, zaczynamy od kompilacji:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myLib
$ javac pl/backlog/green/GeniusLibrary.java

Która wyprodukuje mi plik GeniusLibrary.class w odpowiedniej lokalizacji:

$ tree
.
└── pl
    └── backlog
        └── green
            ├── GeniusLibrary.class
            └── GeniusLibrary.java

3 directories, 2 files

Teraz będziemy chcieli utworzyć plik jar. W związku z tym, że nie chcemy uczynić z naszego pliku jar programu wykonywalnego, nie potrzebujemy specyfikować, która klasa jest klasą główną, więc nie potrzebujemy tworzyć pliku MANIFEST.MF. Wystarczy więc zaklęcie:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myLib
$ jar -cvf myLib.jar pl/backlog/green/GeniusLibrary.class
added manifest
adding: pl/backlog/green/GeniusLibrary.class(in = 323) (out= 252)(deflated 21%)

to nam utworzyło plik myLib.jar:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myLib
$ tree
.
├── myLib.jar
└── pl
    └── backlog
        └── green
            ├── GeniusLibrary.class
            └── GeniusLibrary.java

3 directories, 3 files

Program wykorzystujący bibliotekę stworzony ręcznie

Teraz możemy podzielić się plikiem myLib.jar z innymi programistami, którzy będą chcieli skorzystać z naszej funkcji do liczenia pola koła. Mogą napisać na przykład taki prosty program:

package pl.acme.vip;

import pl.backlog.green.GeniusLibrary;
import java. util.Scanner;

public class Calculator {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        System.out.print("Podaj promien kola: ");
        double radius = sc.nextDouble();
        double area = GeniusLibrary.calculateAreaOfCircle(radius);
        System.out.println("Pole kola o promieniu " + radius + " wynosi " + area);
    }
}

Który będzie korzystał z funkcji, którą udostępniłem w mojej bibliotece. Mogę więc spróbować skompilować ten program:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ javac pl/acme/vip/Calculator.java
pl\acme\vip\Calculator.java:3: error: package pl.backlog.green does not exist
import pl.backlog.green.GeniusLibrary;
                       ^
pl\acme\vip\Calculator.java:11: error: cannot find symbol
                double area = GeniusLibrary.calculateAreaOfCircle(radius);
                              ^
  symbol:   variable GeniusLibrary
  location: class Calculator
2 errors

Jak można było się domyślić, kompilacja się nie powiodła. Muszę w jakiś sposób wskazać kompilatorowi (czyli programowi javac) gdzie znajduje się definicja klasy pl.backlog.green.GeniusLibrary. Załóżmy więc, że mam następującą strukturę plików i katalogów (moje aktualne położenie to katalog myApp):

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ tree ..
..
├── Hello.class
├── Hello.java
├── myApp
│   └── pl
│       └── acme
│           └── vip
│               └── Calculator.java
├── myfile.mf
├── myLib
│   ├── myLib.jar
│   └── pl
│       └── backlog
│           └── green
│               ├── GeniusLibrary.class
│               └── GeniusLibrary.java
└── ziels-app.jar

8 directories, 8 files

Kompilator ma opcję uruchomienia -classpath, która zawiera ścieżki do katalogów z potrzebnymi plikami jar:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ javac -classpath "../myLib" pl/acme/vip/Calculator.java

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ tree ..
..
├── Hello.class
├── Hello.java
├── myApp
│   └── pl
│       └── acme
│           └── vip
│               ├── Calculator.class
│               └── Calculator.java
├── myfile.mf
├── myLib
│   ├── myLib.jar
│   └── pl
│       └── backlog
│           └── green
│               ├── GeniusLibrary.class
│               └── GeniusLibrary.java
└── ziels-app.jar

8 directories, 9 files

Zanim przejdziemy do uruchomienia programu.

Co to oznacza classpath? Pamiętasz, w poprzednim wpisie mówiłem o zmiennej systemowej Path, która przechowuje informacje o katalogach, w których system operacyjny będzie szukał plików wykonywalnych dla wywoływanych przez nas w terminalu programów. classpath to bardzo podobny koncept. W tej opcji uruchomienia przekazujemy katalogi, które ma przeszukać kompilator w poszukiwaniu definicji używanych przez nas w programie klas. W naszym wypadku będzie to plik jar ze skompilowanym kodem naszej biblioteki.

No to teraz możemy uruchomić nasz program:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ java pl.acme.vip.Calculator
Podaj promien kola: 8
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 jak widzimy, program zakończył się rzuceniem wyjątku (wyjątka? – jak jest poprawnie? Nigdy nie wiem :(). Musimy więc programowi java wskazać lokalizację pliku jar z definicją klasy pl/backlog/green/GeniusLibrary. Do tego też użyjemy opcji -classpath:
pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ java -classpath "../myLib" pl.acme.vip.Calculator
Error: Could not find or load main class pl.acme.vip.Calculator
Caused by: java. lang.ClassNotFoundException: pl.acme.vip.Calculator

Widzisz jednak, że to nie jest wystarczające. Dlaczego? Ponieważ teraz program java nie wie, gdzie znaleźć klasę pl.acme.vip.Calculator. Dlatego, że jest jakaś domyślna wartość parametru classpath. Tą domyślną wartością jest katalog . (kropka) który oznacza bieżący katalog. Więc uruchamiając program java, tak jak powyżej nadpisujemy kropkę. Powinniśmy wykonać następujące polecenie:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ java -classpath ".;../myLib" pl.acme.vip.Calculator
Podaj promien kola: 4
Pole kola o promieniu 4.0 wynosi 50.26548245743669

Co teraz musiałbym zrobić, żeby podzielić się z innymi moim wspaniałym programem o nazwie Calculator? Podobnie jak wcześniej mogę zbudować plik jar:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ echo "Main-Class: pl.acme.vip.Calculator" > manifest.mf

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ tree
.
├── manifest.mf
└── pl
    └── acme
        └── vip
            ├── Calculator.class
            └── Calculator.java

3 directories, 3 files

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ jar -cvmf manifest.mf myApp.jar pl/acme/vip/Calculator.class
added manifest
adding: pl/acme/vip/Calculator.class(in = 1183) (out= 683)(deflated 42%)

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ tree
.
├── manifest.mf
├── myApp.jar
└── pl
    └── acme
        └── vip
            ├── Calculator.class
            └── Calculator.java

3 directories, 4 files

Teraz chciałbym go uruchomić, więc próbuję tak jak w poprzednim przypadku:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ java -cp ".;../myLib/myLib.jar" -jar myApp.jar
Podaj promien kola: 4
Exception in thread "main" java. lang.NoClassDefFoundError: pl/backlog/green/Geni
usLibrary
        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(BuiltinCla
ssLoader.java:606)
        at java. base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(C
lassLoaders.java:168)
        at java. base/java. lang.ClassLoader.loadClass(ClassLoader.java:522)
        ... 1 more

Nie udało się… Można to uruchomić mając te dwie paczki jar, przez zdefiniowanie odpowiedniej classpath i wskazanie klasy z metodą main:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ java -cp ".;../myLib" pl.acme.vip.Calculator
Podaj promien kola: 4
Pole kola o promieniu 4.0 wynosi 50.26548245743669

Ale w praktyce tak się nie robi. Jeżeli już się udostępnia paczkę jar ze swoim programem, to dołącza się wszystkie potrzebne paczki wewnątrz dystrybuowanej paczki. Żeby tak zrobić, musimy trochę zmienić drzewo naszych katalogów. Czyli tworzymy katalog lib i do niego kopiujemy paczkę z naszą biblioteką:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ mkdir lib

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ cp ../myLib/myLib.jar lib/

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ tree ..
..
├── Hello.class
├── Hello.java
├── myApp
│   ├── lib
│   │   └── myLib.jar
│   ├── manifest.mf
│   ├── myApp.jar
│   └── pl
│       └── acme
│           └── vip
│               ├── Calculator.class
│               └── Calculator.java
├── myfile.mf
├── myLib
│   ├── myLib.jar
│   └── pl
│       └── backlog
│           └── green
│               ├── GeniusLibrary.class
│               └── GeniusLibrary.java
└── ziels-app.jar

9 directories, 12 files

Następnie dopisujemy jedną linijkę do pliku manifest.mf, aby plik wyglądał następująco:

Main-Class: pl.acme.vip.Calculator
Class-Path: lib/myLib.jar

I teraz możemy stworzyć paczkę jar:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ jar -cvmf manifest.mf myApp.jar pl/acme/vip/Calculator.class lib/myLib.jar
added manifest
adding: pl/acme/vip/Calculator.class(in = 1183) (out= 683)(deflated 42%)
adding: lib/myLib.jar(in = 752) (out= 520)(deflated 30%)

Którą już bez problemów uruchomimy:

pawel@DESKTOP-0BB9FUV MINGW64 ~/OneDrive/Desktop/myApp
$ java -jar myApp.jar
Podaj promien kola: 4
Pole kola o promieniu 4.0 wynosi 50.26548245743669

Podsumowanie

Uffff. Udało się. Po wielu ręcznych operacjach udało nam się stworzyć prostą aplikację. Zainteresowanych odsyłam do repozytorium na githubie. Ciężko było? Dla mnie było bardzo ciężko, mimo mojego dużego doświadczenia jako programista Javy, musiałem się posiłkować internetem oraz tym. Teraz wyobraź sobie, że sam musisz sobie załatwić wszystkie paczki jar bibliotek, z których chcesz korzystać. Weź pod uwagę, że nie wszyscy twórcy bibliotek na wieki wieków będą trzymać wszystkie buildy swojej biblioteki. Nie myśl sobie, że zawsze będą dbać o wsteczną kompatybilność wszystkich wersji swojej biblioteki. Jest też druga strona medalu, postaw się w roli twórcy biblioteki. Jako autor musisz dbać o budowanie kolejnych wersji swojej biblioteki, udostępnianie jej innym programistom, przyjmowanie zgłoszeń błędów itd. Przedstawiłem bardzo prosty scenariusz budowania swojego programu. W rzeczywistości w proces budowania aplikacji włącza się wykonanie testów jednostkowych, statyczną analizę kodu źródłowego oraz wystawianie paczki tak, aby inni programiści mogli z niej skorzystać.

Odpowiedzią na te wszystkie bolączki jest użycie Mavena. Ale o tym w następnym wpisie!

 

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