Rozdział 21. Pakiety i ich tworzenie

Jedną z dużych zalet języka Java jest fakt, że został w nim zdefiniowany bardzo przejrzysty mechanizm klasyfikacji zewnętrznych interfejsów programistycznych i zarządzania nimi. Porównajmy te rozwiązania z większością innych języków, w których symbole można znajdować w samej bibliotece języka lub wielu innych bibliotekach, przy czym nie obowiązują tu żadne reguły nazewnictwa[79]. Interfejs programistyczny składa się z jednego lub większej liczby pakietów, pakiety z kolei składają się z klas, a klasy z metod i pól. Pakiety może tworzyć każdy, przy czym obowiązuje tu jedno ograniczenie — nikt nie jest w stanie tworzyć pakietów, których nazwy rozpoczynają się od czterech liter „java”. Pakiety o nazwach java oraz javax są zarezerwowane dla twórców języka Java zatrudnionych w firmie Oracle, a prace nad nimi są prowadzone w oparciu o proces społeczności języka Java — JCP (ang. Java Community Process). W czasie gdy język Java został udostępniony, w skład jego interfejsu programistycznego wchodziło około tuzina pakietów, z których większość wciąż jest stosowana, choć ich wielkość wzrosła niemal czterokrotnie. Niektóre z tych pakietów przedstawiłem w Tabela 21-1.

Od tego czasu do języka zostało dodanych wiele nowych pakietów, jednak ich początkowa struktura dosyć dobrze wytrzymała próbę czasu. W niniejszym rozdziale pokażę, w jaki sposób można tworzyć własne pakiety oraz ich dokumentację, a następnie przedstawię kilka zagadnień związanych z różnymi sposobami udostępniania pakietów na różnych platformach systemowych.

Instrukcja package musi być pierwszą instrukcją umieszczoną w pliku źródłowym, nawet instrukcje importu należy umieszczać za nią. Sama instrukcja powinna określać pełną nazwę pakietu. Według ogólnie przyjętej konwencji nazwy pakietów odpowiadają nazwom domenowym ich twórców zapisanym w odwrotnej kolejności. Na przykład mój adres domenowy to darwinsys.com i dlatego nazwy większości moich pakietów rozpoczynają się od łańcucha znaków darwinsys.com, po którym umieszczana jest nazwa projektu. Klasy pomocnicze wykorzystywane w niniejszej książce zostały umieszczone w pakiecie com.darwinsys, opisanym w „1.5. Pobieranie przykładów dołączonych do tej książki i korzystanie z nich”, a każdy plik źródłowy wchodzący w skład tego pakietu rozpoczyna się od instrukcji, takiej jak ta pokazana poniżej:

package com.darwinsys.util;

Demonstracyjne klasy dołączone do tej książki i umieszczone w katalogu /javacooksrc/main nie zostały rozmieszczone zgodnie z tą zasadą. Umieściłem je w pakietach, których nazwy odpowiadają tematyce rozdziałów lub pakietom java.*, z którymi są powiązane. Na przykład w pakiecie lang umieściłem kody prezentujące podstawowe zagadnienia związane z językiem Java, w pakiecie struct — przykłady do rozdziału poświęconego strukturom danych (Rozdział 7.), w pakiecie threads — przykłady do rozdziału poświęconego wątkom (Rozdział 22.) i tak dalej. Mam przy tym nadzieję, że jeśli Czytelnik będzie chciał wykorzystać te kody we własnej aplikacji, umieści je w „prawdziwych” pakietach!

Należy pamiętać, że po umieszczeniu instrukcji package w plikach źródłowych zarówno kompilator, jak i środowisko wykonawcze Javy będą oczekiwać, że skompilowane pliki klasowe (.class) zostaną umieszczone w odpowiednim miejscu (czyli w strukturze katalogów, których nazwa odpowiada pełnej nazwie pakietu umieszczonej w jednym z katalogów podanych w zmiennej środowiskowej CLASSPATH). Na przykład plik klasowy klasy com.darwinsys.util.FileIO musi nosić nazwę FileIO.class, ale nie może być umieszczony bezpośrednio w jednym z katalogów wymienionych w zmiennej środowiskowej CLASSPATH, lecz w pliku com/darwinsys/util/FileIO.class, a cała ta struktura musi się znajdować w jednym z katalogów podanych w zmiennej CLASSPATH lub w jednym z archiwów przechowywanych w tych katalogach. Podczas kompilacji klas należących do pakietu zazwyczaj (choć właściwie jest to niemal niezbędne) umieszcza się w wywołaniu kompilatora opcję -d. Za tą opcją należy podać nazwę katalogu, w którym trzeba utworzyć wynikowe drzewo katalogów (przy czym bieżący katalog jest oznaczany przy użyciu kropki (.)). Aby na przykład skompilować wszystkie pliki .java umieszczone w bieżącym katalogu i utworzyć przy tym odpowiednią strukturę katalogów z plikami klasowymi (kontynuując poprzedni przykład, byłaby to struktura ./com/darwinsys/util), należałoby użyć następującego polecenia:

java -d . *.java

które powoduje utworzenie ścieżki (na przykład: com/darwinsys/util/) w bieżącym katalogu i umieszcza wszystkie skompilowane pliki klasowe w utworzonym katalogu util. Powyższe rozwiązanie znacznie ułatwia kompilowanie programu w przyszłości, jak również upraszcza tworzenie archiwów (opisane w „21.4. Stosowanie programu archiwizującego jar”).

Oczywiście, w przypadku korzystania z narzędzia do budowania, takiego jak Ant (patrz „1.6. Automatyzacja kompilacji przy użyciu programu Ant”) lub Maven (patrz „1.7. Automatyzacja zależności, kompilacji, testowania i wdrażania przy użyciu programu Apache Maven”), niezbędne ustawienia zostaną wprowadzone w pliku konfiguracyjnym (Ant) lub też zostaną wykonane prawidłowo bez naszej ingerencji (w razie korzystania z programu Maven); zatem nie będziemy musieli pamiętać o tym, by ciągle to robić.

Należy zwrócić uwagę, że we wszystkich nowoczesnych środowiskach języka Java klas, które nie należą do żadnego pakietu (czyli są umieszczone w „pakiecie anonimowym”), nie można podawać w instrukcjach importu, choć inne klasy należące do tego samego pakietu mogą się do nich odwoływać.

Javadoc jest jednym z wielkich wynalazków powstałych w pierwszych latach stosowania języka Java. Podobnie jak wiele innych bardzo dobrych rozwiązań, nie został on w całości wymyślony przez twórców Javy — już wcześniejsze projekty, takie jak Literate Programming Donalda Knutha, łączyły kod źródłowy programu oraz jego dokumentację w jednym pliku źródłowym. Jednak twórcy Javy w doskonały sposób wykorzystali tę ideę i zrobili to we właściwym czasie. Javadoc jest dla klas języka Java tym samym, czym dokumentacja man dla systemów Unix lub Windows Help dla aplikacji przeznaczonych dla systemów Windows: to standardowy format, którym każdy potrafi się posługiwać; każdy też oczekuje, że zawsze będzie dostępny i stosowany przez innych. Należy go poznać, zastosować, stworzyć dokumentację, po czym działać długo i owocnie (cóż, być może nie zawsze). A cała dokumentacja języka Java i jego interfejsu programistycznego, która jest używana podczas pisania programów? Czy firma Sun wynajęła setki pisarzy, którzy ją stworzyli? Nie, w języku Java takie rzeczy robi się w inny sposób. Twórcy języka Java już podczas pisania kodu źródłowego umieszczają w nim komentarze dokumentujące, a później — w momencie udostępniania nowej wersji języka — przetwarzają tysiące publicznych klas przy użyciu programu Javadoc, generując dokumentację, która jest następnie udostępniana wraz z JDK. Wszyscy programiści tworzący klasy przeznaczone do wykorzystania przez innych mogą, powinni, a w rzeczywistości muszą postępować tak samo.

Jedyną rzeczą, jaką należy zrobić, aby móc skorzystać z systemu dokumentacyjnego Javy, jest umieszczanie specjalnych komentarzy dokumentujących w plikach źródłowych. Komentarze te zaczynają się od znaku ukośnika i dwóch gwiazdek (/**), kończą gwiazdką i znakiem ukośnika, a należy je umieszczać bezpośrednio przed definicją dokumentowanej klasy, metody lub pola. Komentarze dokumentujące znajdujące się w jakichkolwiek innych miejscach są ignorowane.

Istnieje grupa słów kluczowych poprzedzanych znakiem @, które w określonych sytuacjach można umieszczać w komentarzach dokumentujących. Niektóre z nich są dodatkowo zapisywane w nawiasach klamrowych. Słowa te, dostępne w wersji Java 8, przedstawiłem w Tabela 21-2.

Przykład 21-1 przedstawia niezbyt realistyczny kod źródłowy programu, w którym zostały wykorzystane niemal wszystkie słowa kluczowe z Tabela 21-2. Wyniki przetworzenia tego kodu przez program Javadoc zostały przedstawione na Rysunek 21-1.

Program Javadoc działa dobrze nawet w przypadku pojedynczych klas, jednak jego prawdziwe możliwości uwidaczniają się w przypadku całych pakietów lub nawet grup pakietów. Można na przykład podać plik zawierający informacje o pakiecie, który zostanie dołączony do wygenerowanej dokumentacji. Program Javadoc generuje dokumentację zawierającą precyzyjne połączenia wewnętrzne oraz zewnętrzne, takie same jak te, które można znaleźć w standardowej dokumentacji języka Java. Program posiada kilka opcji podawanych w wierszu wywołania. Osobiście zazwyczaj używam opcji -author oraz -version, które powodują umieszczenie w generowanej dokumentacji informacji o autorze i wersji kodu; jak również opcji -link, określającej położenie standardowej dokumentacji Javy, z którą będą tworzone odpowiednie połączenia.

Rysunek 21-1 przedstawia dokumentację wygenerowaną na podstawie klasy JavadocDemo w wyniku wykonania następującego polecenia:

$ javadoc -author -version JavadocDemo.java

Należy pamiętać, że jeden z (wielu) wygenerowanych plików będzie mieć tę samą nazwę co klasa oraz rozszerzenie .html. A zatem jeśli stworzymy aplet oraz dokument HTML o tej samej nazwie, służący do wyświetlania apletu, to dokument ten zostanie zastąpiony plikiem wygenerowanym przez program Javadoc, i to bez żadnego ostrzeżenia. Z tego względu twórcom apletów sugerowałbym korzystanie z opcji -d katalog, aby poinformować program Javadoc, gdzie ma umieszczać generowane pliki, i aby nie zapisywał ich w katalogu z kodami źródłowymi; opcja ta działa tak samo jak analogiczna opcja programu javac. Ewentualnie można także zmienić rozszerzenie strony wyświetlającej aplet na .htm.

Cieszące się nieustannym powodzeniem otwarte narzędzie XDoclet (http://xdoclet.sourceforge.net/xdoclet/index.html), które początkowo zostało stworzone w celu generowania uciążliwych klas pomocniczych oraz deskryptorów wdrażania używanych przez powszechnie krytykowaną platformę EJB2, sprawiło, że pojawiła się potrzeba wprowadzenia podobnego mechanizmu w standardowej wersji języka Java. W efekcie opracowano adnotacje Javy. Mechanizm adnotacji korzysta ze składni nieco przypominającej interfejsy, w której zarówno deklaracje adnotacji, jak i ich zastosowanie wymagają podania nazwy poprzedzonej znakiem @. Zgodnie z twierdzeniami projektantów tego mechanizmu zapis ten został wybrany dlatego, że przypominał „znaczniki Javadoc — istniejący już wcześniej tymczasowy pierwowzór adnotacji w języku Java”. Javadoc można określić jako tymczasowy wyłącznie w takim znaczeniu, że jego znaczniki rozpoczynające się od znaku @ nigdy nie zostały w pełni zintegrowane z samym językiem, a większość z nich była ignorowana przez kompilator, choć nie dotyczy to znacznika @depreciated, który kompilator zawsze rozpoznawał i uwzględniał (patrz „1.9. Komunikaty o odrzuconych metodach”).

Adnotacje można odczytywać w trakcie działania programu przy wykorzystaniu API odzwierciedlania, co pokazałem w „23.9. Stosowanie i definiowanie adnotacji”, w której zaprezentowałem także, w jaki sposób można definiować swoje własne adnotacje. Adnotacje mogą być również odczytywane po kompilacji, jak to jest w przypadku takich narzędzi jak generatory używane przez technologie RMI i EJB (bądź też innych narzędzi, które dopiero zostaną wymyślone, może nawet przez Ciebie, drogi Czytelniku!).

Także kompilator Javy, program javac, odczytuje adnotacje podczas kompilacji, aby pobrać z nich dodatkowe informacje.

Na przykład często popełnianym błędem jest przeciążenie metody, kiedy tak naprawdę chodziło o jej przesłonięcie; błąd ten można popełnić bardzo łatwo, gdyż wystarczy podać w deklaracji metody nieprawidłowe typy argumentów. W ramach przykładu przeanalizujmy przesłonięcie metody equals klasy Object. Jeśli przez pomyłkę zostanie ona zapisana w następujący sposób:

public boolean equals(MyClass obj) {
    ...
}

to w efekcie powstanie jej przeciążona wersja, która najprawdopodobniej nigdy nie zostanie wywołana — wywoływana będzie domyślna wersja metody zdefiniowana w klasie Object. Aby uniemożliwić popełnianie błędów tego typu, w pakiecie java.lang zastosowano adnotację Override. Nie wymaga ona podawania żadnych dodatkowych parametrów, a należy ją umieścić bezpośrednio przed deklaracją metody. Poniżej został przedstawiony stosowny przykład:

/lang/AnnotationOverrideDemo.java

/**
 * AnnotationOverrideDemo - Prosty przykład zastosowania metadanych do
 * sprawdzenia, czy metoda z klasy bazowej jest przesłaniana
 * (a nie przeciążana). Ta klasa udostępnia metodę.
 */
abstract class Top {
    public abstract void myMethod(Object o);
}

/** Prosty przykład zastosowania metadanych do
 * sprawdzenia, czy metoda z klasy bazowej jest przesłaniana
 * (a nie przeciążana). W tej klasie metoda klasy bazowej ma
 * zostać przesłonięta; kod celowo jednak zawiera błąd, aby
 * pokazać działanie nowoczesnych kompilatorów.
 */
class Bottom {

    @Override
    public void myMethod(String s) {    // MOŻNA OCZEKIWAĆ BŁĘDU KOMPILACJI
        // Tu coś robimy...
    }
}

Próba skompilowania tego programu spowoduje zgłoszenie błędu informującego, że metoda nie przesłania metody z klasy bazowej, choć adnotacja informuje o tym, że powinna. Jest to krytyczny błąd uniemożliwiający skompilowanie kodu:

C:> javac AnnotationOverrideDemo.java
AnnotationOverrideDemo.java:21: method does not override a method
            from its superclass
        @Override public void myMethod(String s) { // MOŻNA OCZEKIWAĆ BŁĘDU KOMPILACJI
         ^
1 error
C:>

Program jar jest standardowym narzędziem służącym do tworzenia archiwów. Archiwa w Javie pełnią tę samą funkcję co biblioteki wykorzystywane w niektórych innych językach programowania. Standardowe klasy Javy są zazwyczaj pobierane z archiwów, o czym można się przekonać, uruchamiając program HelloWorld z opcją -verbose:

java -verbose HelloWorld

Tworzenie archiwów jest bardzo prostym zadaniem. W wywołaniu programu jar można podać kilka opcji. Najpopularniejszą z nich jest c, nakazująca utworzenie nowego archiwum, t — tworząca spis treści archiwum, oraz x — pobierająca zawartość archiwum. Nazwa pliku archiwum jest określana przy użyciu opcji -f. Po opcjach podawane są nazwy plików i katalogów, jakie należy umieścić w archiwum. Poniżej podałem przykład wywołania programu jar:

jar cvf /tmp/MyClasses.jar .

Kropka umieszczona na końcu polecenia jest bardzo ważna i oznacza bieżący katalog. Powyższe polecenie tworzy archiwum zawierające wszystkie pliki z bieżącego katalogu oraz jego podkatalogów, po czym zapisuje je w pliku /tmp/MyClasses.jar.

Niektóre z zastosowań plików JAR wymagają, aby był w nich dostępny jeden dodatkowy plik, nazywany manifestem, w którym znajduje się lista zawartości archiwum oraz atrybuty wszystkich umieszczonych w nim plików. Atrybuty te są zapisywane jako pary nazwa: wartość, podobnie jak nagłówki wiadomości poczty elektronicznej, zawartość plików właściwości (patrz „7.12. Zapisywanie łańcuchów znaków w obiektach Properties i Preferences”) i tak dalej. Niektóre atrybuty są wymagane w konkretnym zastosowaniu plików JAR, inne natomiast są opcjonalne. Na przykład „21.5. Uruchamianie programu zapisanego w pliku JAR” przedstawia sposób wykonywania programu głównego bezpośrednio z archiwum. Aby rozwiązanie takie było możliwe, w manifeście należy umieścić nagłówek Main-Program. Można także tworzyć i stosować własne, niestandardowe nagłówki, takie jak:

MySillyAttribute: true
MySillynessLevel: high(5'11'')

Informacje te zapisuje się w pliku o dowolnej nazwie, powiedzmy mainfest.stub[80], którego nazwa jest podawana w wywołaniu programu jar za pomocą opcji -m. Program jar umieszcza przekazane atrybuty w tworzonym pliku manifestu:

jar -cv -m manifest.stub -f /tmp/com.darwinsys.util.jar .

Zarówno program jar, jak i inne związane z nim narzędzia dodają do pliku manifestu dodatkowe informacje, na przykład listę wszystkich plików umieszczonych w archiwum.

Polecenie java posiada opcję -jar, której użycie nakazuje uruchomienie programu zapisanego w pliku JAR. W przypadku zastosowania tego sposobu uruchamiania programu wszystkie klasy konieczne do jego działania także będą poszukiwane w tym samym pliku JAR. Ale skąd Java będzie wiedzieć, jaki program ma uruchomić? Cóż, musimy go wskazać. W tym celu należy stworzyć plik zawierający jeden prosty wpis przypominający ten przedstawiony poniżej; należy przy tym pamiętać, że w nazwach atrybutów uwzględniana jest wielkość liter, a po dwukropku koniecznie należy umieścić znak odstępu:

Main-class: com.jakasdomena.HelloWorld

Taki plik można zapisać na przykład pod nazwą manifest.stub, zakładając, że program HelloWorld ma być uruchamiany w danym pakiecie. Poniższe polecenia pozwolą przygotować aplikację i uruchomić ją z pliku JAR:

C:> javac HelloWorld.java
C:> jar cvmf manifest.stub hello.jar HelloWorld.class
C:> java -jar hello.jar
Witaj, świecie
C:>

Teraz można umieścić plik JAR w dowolnym miejscu i uruchamiać go w ten sam sposób. Katalogu, w którym został umieszczony plik JAR, nie trzeba dodawać do zmiennej środowiskowej CLASSPATH, podobnie jak nie trzeba podawać nazwy uruchamianej klasy.

W platformach systemowych wyposażonych w graficzny interfejs użytkownika takie aplikacje można uruchamiać także poprzez dwukrotne kliknięcie pliku JAR. Rozwiązanie to działa przynajmniej w systemach Mac OS X oraz Windows, w których zostało zainstalowane środowisko wykonawcze Javy.

W języku Java istnieje kilka rodzajów komponentów określanych mianem komponentów JavaBeans:

Wszystkie cztery wymienione powyżej rodzaje komponentów łączą jednolite zasady nazewnictwa. Wszystkie operacje na właściwościach publicznych muszą być wykonywane za pośrednictwem specjalnych metod umożliwiających określenie wartości właściwości oraz jej odczytanie. Dla właściwości typu Typ o nazwie Prop trzeba utworzyć dwie metody (koniecznie należy zwrócić uwagę na wielkość liter w ich nazwach):

public Typ getProp();
public void setProp(Typ);

Jedyne dopuszczalne odstępstwo od tego wzorca dotyczy argumentów typu boolean lub Boolean, w których metoda pobierająca wartość zazwyczaj nosi nazwę isProp(), a nie getProp().

Na przykład wszystkie komponenty należące do pakietów AWT oraz Swing, posiadające tekstowe etykiety, definiują następujące metody:

public String getText();
public void setText(String newText);

Ten wzorzec projektowy bazujący na metodach set i get należy stosować w metodach kontrolujących działanie komponentu. Dla zapewnienia spójności warto go wykorzystywać także w pozostałych klasach, które nie mają być komponentami JavaBeans. Tak zwane „kontenery komponentów” wykorzystują introspekcję (opisaną w Rozdział 23.) w celu odszukania par metod set i get, niektóre z nich dodatkowo używają ich do stworzenia edytorów właściwości dla komponentów. Zintegrowane środowiska programistyczne, które są w stanie obsługiwać komponenty JavaBeans, udostępniają edytory dla wszystkich standardowych typów (kolorów, czcionek, etykiet i tak dalej). Ich możliwości można jednak wzbogacić lub zmodyfikować, dodając odpowiednią klasę BeanInfo.

Poniżej podałem minimalne warunki konieczne, jakie musi spełniać klasa, aby mogła być stosowana jako komponent JavaBeans w środowiskach programistycznych dysponujących narzędziami do tworzenia graficznego interfejsu użytkownika:

Należy zauważyć, że komponent JavaBeans, który nie ma żadnych określonych wymogów odnośnie do klasy, po której ma dziedziczyć, lub interfejsów, które ma implementować, jest określany jako POJO — stary, zwyczajny obiekt Javy. Większość nowoczesnych platform Javy akceptuje takie komponenty, zamiast wymuszać (jak to bywało wcześniej) określoną strukturę dziedziczenia (na przykład w platformie Struts 1 komponenty musiały dziedziczyć po klasie org.struts.Action) lub wymagać implementacji określonych interfejsów (na przykład technologia EJB2 wymaga implementacji interfejsu javax.ejb.SessionBean).

W dalszej części receptury przedstawiłem komponent LabelText, który może stanowić cenne i przydatne narzędzie dla wszystkich osób tworzących aplikacje z graficznym interfejsem użytkownika. Komponent ten łączy w sobie etykietę oraz pole tekstowe, dzięki czemu ułatwia tworzenie interfejsu użytkownika. Wśród plików dołączonych do książki można znaleźć program testowy, którego interfejs graficzny składa się z trzech komponentów LabelText. Jego wygląd przedstawiłem na Rysunek 21-3.

Kod komponentu LabelText przedstawiłem na Przykład 21-2. Warto zwrócić uwagę, że komponent można serializować, a niemal wszystkie jego publiczne metody są zgodne z paradygmatem zakładającym wykorzystanie metod set i get. W przeważającej większości przypadków działanie metod set i get ogranicza się do wywołania odpowiednich metod etykiety lub pola tekstowego. W zasadzie nie można wiele więcej powiedzieć na temat tego komponentu, jednak stanowi on dobry przykład agregacji oraz tworzenia komponentów.

Przykład 21-2. /com/darwinsys/swingui/LabelText.java

// package com.darwinsys.swingui;
public class LabelText extends JPanel implements java.io.Serializable {

    private static final long serialVersionUID = -8343040707105763298L;
    /** Komponent etykiety. */
    protected JLabel theLabel;
    /** Komponent pola tekstowego. */
    protected JTextField theTextField;
    /** Używana czcionka. */
    protected Font myFont;

    /** Tworzymy obiekt bez wartości początkowych.
     * Aby klasa mogła być komponentem JavaBeans, MUSI mieć konstruktor
     * bezargumentowy.
     */
    public LabelText() {
        this("(Test etykiety)", 12);
    }

    /** Tworzymy obiekt z etykietą o podanej treści i polem tekstowym
     * o domyślnej wielkości.
     */
    public LabelText(String label) {
        this(label, 12);
    }

    /** Tworzymy obiekt z etykietą o podanej treści i polem tekstowym
     * o podanej wielkości.
     */
    public LabelText(String label, int numChars) {
        this(label, numChars, null);
    }

    /** Tworzymy obiekt z etykietą o podanej treści, polem tekstowym
     * o podanej wielkości i "dodatkowym" komponentem.
     * @param label Wyświetlany tekst.
     * @param numChars Wielkość pola tekstowego.
     * @param extra Trzeci komponent, taki jak przycisk Anuluj.
     * Może być równy null, w takim przypadku zostaną wyświetlone
     * jedynie etykieta i pole tekstowe.
     */
    public LabelText(String label, int numChars, JComponent extra) {
        super();
        setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
        theLabel = new JLabel(label);
        add(theLabel);
        theTextField = new JTextField(numChars);
        add(theTextField);
        if (extra != null) {
            add(extra);
        }
    }

    /** Pobieramy wyrównanie etykiety w poziomie. */
    public int getLabelAlignment() {
        return theLabel.getHorizontalAlignment();
    }

    /** Określamy wyrównanie etykiety w poziomie. */
    public void setLabelAlignment(int align) {
        theLabel.setHorizontalAlignment(align);
    }

    /** Pobieramy tekst wyświetlany aktualnie w polu tekstowym. */
    public String getText() {
        return theTextField.getText();
    }

    /** Określamy tekst, który będzie wyświetlony w polu tekstowym. */
    public void setText(String text) {
        theTextField.setText(text);
    }

    /** Pobieramy tekst etykiety. */
    public String getLabel() {
        return theLabel.getText();
    }

    /** Określamy tekst etykiety. */
    public void setLabel(String text) {
        theLabel.setText(text);
    }

    /** Określamy czcionkę używaną w obu komponentach. */
    public void setFont(Font f) {
        // Wywołanie super() wykonywane w konstruktorze tej klasy
        // może spowodować wywołanie metody setFont() (z metody
        // Swing.LookAndFeel.installColorsAndFont), jeszcze zanim
        // utworzymy nasz komponent, obchodzimy ten problem.
        if (theLabel != null)
            theLabel.setFont(f);
        if (theTextField != null)
            theTextField.setFont(f);
    }

    /** Dodajemy ActionListener, aby otrzymywać zdarzenia akcyjne z pola
        tekstowego. */
    public void addActionListener(ActionListener l) {
        theTextField.addActionListener(l);
    }

    /** Usuwamy ActionListener z pola tekstowego. */
    public void removeActionListener(ActionListener l) {
        theTextField.removeActionListener(l);
    }
}

Po skompilowaniu powyższej klasy można ją umieścić w pliku JAR.

Choć większość kontenerów odwołuje się obecnie do komponentów POJO, korzystając z mechanizmów odzwierciedlania, to może się zdarzyć, że będziemy chcieli umieścić nasz komponent JavaBeans w niezależnym pliku JAR, by móc łatwiej go rozpowszechniać. W takim pliku JAR, oprócz skompilowanych plików klasowych komponentu, musi się znaleźć prototyp manifestu zawierający przynajmniej dwa przedstawione poniżej wiersze tekstu, choć można tam podać także informacje o twórcy oraz prawach autorskich:

Name: LabelText.class
Java-Bean: true

Zakładając, że informacje te zostaną umieszczone w pliku LabelText.stub, komponent można będzie przygotować do rozpowszechniania, wykonując polecenie jar (patrz „21.4. Stosowanie programu archiwizującego jar”). Ponieważ plik JAR musi zawierać pliki klasowe umieszczone w odpowiednich katalogach odpowiadających pakietowi, do jakiego dane klasy należą (patrz „21.1. Tworzenie pakietu”), oraz ze względu na umieszczenie komponentu LabelText w pakiecie com.darwinsys.swingui (patrz „1.5. Pobieranie przykładów dołączonych do tej książki i korzystanie z nich”), przed utworzeniem pliku archiwum przeszedłem do odpowiedniego katalogu, a następnie w poleceniu jar podałem pełną ścieżkę dostępu do pliku klasowego (plik .stub może się znajdować w dowolnym miejscu, ja jednak umieściłem go w tym samym katalogu, w którym jest plik źródłowy, aby później nie mieć problemów z jego odszukaniem; dlatego też także do niego musiałem się odwołać, podając pełną ścieżkę dostępu):

$ cd $js/darwinsys/src
$ jar cvfm labeltext.jar com/darwinsys/swingui/LabelText.stub \
com/darwinsys/swingui/LabelText.class
added manifest
adding: com/darwinsys/swingui/LabelText.class(in=1607) (out=776)(deflated 51%)
$

Oczywiście, podczas rzeczywistych prac nad programowaniem chcielibyśmy zautomatyzować cały ten proces, używając przy tym odpowiednich narzędzi, takich jak Ant (patrz „1.6. Automatyzacja kompilacji przy użyciu programu Ant”) lub Maven (patrz „1.7. Automatyzacja zależności, kompilacji, testowania i wdrażania przy użyciu programu Apache Maven”).

Teraz można już zainstalować labeltext.jar jako komponent JavaBeans. Niemniej jednak z ciekawości sprawdźmy zawartość utworzonego pliku JAR. Opcja x programu jar pozwala na pobranie całej zawartości wskazanego archiwum:

$ jar xvf labeltext.jar
created: META-INF/
extracted: META-INF/MANIFEST.MF
extracted: com/darwinsys/swingui/LabelText.class
$

Plik MANIFEST.MF jest tworzony na podstawie pliku manifestu (w naszym przypadku jest to plik LabelText.stub); sprawdźmy jego zawartość:

$ more META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 1.4.2_03 (Apple Computer, Inc.)
Java-Bean: true
Name: LabelText.class

Jak widać, z plikiem manifestu nie stało się nic ciekawego — pojawiły się w nim jedynie dwa dodatkowe wiersze tekstu. Niemniej teraz nasza klasa jest już gotowa do wykorzystania jako komponent JavaBeans. Aby wykorzystać ją w programach służących do tworzenia graficznego interfejsu użytkownika, należy skopiować przygotowany plik JAR do katalogu beans bądź zainstalować go przy użyciu odpowiedniego kreatora.

Instalacja oprogramowania nie jest zadaniem trywialnym. Użytkownicy systemów Unix preferujący wykonywanie operacji z poziomu wiersza poleceń będą szczęśliwi, mogąc własnoręcznie rozpakować archiwum i określić ścieżkę (zmienną środowiskową PATH) konieczną do poprawnego działania programu. Jeśli jednak chcemy, by nasze aplikacje były dostępne dla szerszych kręgów użytkowników, konieczne będzie zastosowanie jakiegoś prostszego rozwiązania. Na przykład „wskaż i kliknij”. Istnieje kilka narzędzi podejmujących próbę ułatwienia procesu instalacji. Najlepsze spośród nich są w stanie tworzyć ikony uruchamiające program w systemach Mac OS, Windows, a nawet w niektórych środowiskach graficznych wykorzystywanych w systemach Unix (takich jak CDE, KDE bądź GNOME).

Osobiście uzyskiwałem całkiem zadowalające rezultaty, posługując się komercyjnym programem InstallAnywhere firmy Zero G Software. Program ten jest w stanie sprawdzić, czy na komputerze jest zainstalowana wirtualna maszyna Javy (JVM), oraz może przeprowadzać instalację normalną i sieciową (czyli pozwala na bezpośrednie zainstalowanie programu bądź też na skopiowanie go z podanej witryny WWW). Program ten kilkakrotnie zmieniał właścicieli, a obecnie należy do firmy Flexera Software (http://www.flexerasoftware.com).

Program InstallShield przez długi czas był liderem wśród programów instalacyjnych przeznaczonych dla systemów Windows, jednak w świecie użytkowników języka Java zawsze miał on więcej konkurentów.

W „21.11. Java Web Start” przedstawiłem Java Web Start — opracowaną przez firmę Sun technologię instalacji oprogramowania bazującą na wykorzystaniu internetu.

Moje własne wczesne próby stworzenia niezależnego programu instalacyjnego dla oprogramowania pisanego w Javie można znaleźć na GitHubie w formie projektu yajinstaller (ang. Yet Another unfinished Java Installer — jeszcze jeden niedokończony instalator Javy). Jeśli Czytelnikowi uda się go usprawnić, to proszę o podesłanie pliku z różnicami lub wygenerowanie żądania ściągnięcia.

Aby jakiś program mógł być pełnoprawną aplikacją systemu Mac OS X, musi zostać przygotowany zgodnie z konkretnym formatem instalacyjnym, który na szczęście jest całkiem prosty. Każda aplikacja systemu Mac OS X — bez względu na język, w jakim została napisana — jest instalowana w niezależnym katalogu nazywanym „pakietem aplikacji” (ang. Application Bundle), którego nazwa powinna się kończyć ciągiem znaków .app. To preferowany sposób instalowania aplikacji w systemie Mac OS X. W odróżnieniu od zwyczajnych plików JAR, pakiety aplikacji są wyświetlane jako ikony zarówno w programie Finder („eksploratorze”), jak i w innych aplikacjach, można je zapisywać na pasku Doku w celu uruchamiania przy użyciu jednego kliknięcia, można również kojarzyć z nimi rozszerzenia plików (dzięki czemu po dwukrotnym kliknięciu takiego pliku lub otworzeniu go zostanie uruchomiona odpowiednia aplikacja, a w niej otworzony kliknięty plik).

Rysunek 21-4 przedstawia strukturę plików w katalogu prostej aplikacji napisanej w Javie.

Jak widać, pakiet aplikacji składa się z katalogu Contents zawierającego dwa katalogi podrzędne: MacOS oraz Resources. Pierwszy z nich zawiera plik wykonywalny, a w przypadku aplikacji napisanych w Javie jest to plik JavaApplicationStub, natywny program używany w systemie Mac OS do uruchamiania aplikacji napisanych w Javie (wchodzi on w skład pakietu Developer Tools). Plik Contents/Resources/*.icns zawiera ikony przeznaczone do wyświetlania (w różnych rozdzielczościach) w programie Finder. Plik ten można utworzyć, używając programu IconComposer (dostępnego w katalogu /Developer/Applications/Utilities/Icon Composer.app). Katalog Contents/Resources/Java zawiera skompilowane pliki klasowe aplikacji, jak również (ewentualnie) pliki JAR. Plik Contents/Info.plist scala ze sobą wszystkie elementy aplikacji, określając nazwy różnych plików, typy plików, które aplikacja może otwierać, oraz wszelkie inne informacje na jej temat.

Lepsze, komercyjne narzędzia do tworzenia programów instalacyjnych (opisane w „21.9. „Zapisz raz, instaluj wszędzie””) są w stanie wygenerować taką strukturę plików i katalogów samodzielnie. Można to także zrobić przy użyciu programu narzędziowego Ant. Również Eclipse 3.0 (w wersji Milestone 7) potrafi generować aplikacje dla systemu Mac OS X. Wystarczy wybrać Project w widoku Navigator, w menu Export wskazać opcję Export Mac OS X application, a następnie w dwóch kolejnych oknach podać informacje dotyczące na przykład miejsca, gdzie należy umieścić aplikację (patrz Rysunek 21-5). W przypadku stosowania programu Ant lub Eclipse będziemy zapewne chcieli skorzystać z opcji Disk Copy, by utworzyć plik dmg (obraz dysku) naszego katalogu. Pliki dmg mogą być pobierane przez innych użytkowników systemu Mac OS X i zazwyczaj są automatycznie rozpakowywane po pobraniu, co pozwala odtworzyć gotowy pakiet aplikacji.

Pakiety aplikacji zostały opisane w Rozdział 7. książki Mac OS X for Java Geeks autorstwa Willa Iversona wydanej przez wydawnictwo O’Reilly (http://shop.oreilly.com/product/9780596004002.do). Książkę tę polecam wszystkim programistom używającym języka Java, którym zależy na pisaniu dobrych aplikacji przeznaczonych do użycia w systemie Mac OS X, a zwłaszcza dla osób rozpowszechniających aplikacje, które chciałyby zrobić dobre wrażenie na innych użytkownikach systemu Mac OS X.

Java Web Start (JWS) jest technologią służącą do pobierania aplikacji przez internet. Rozwiązanie to nie ma nic wspólnego z apletami, które wymagają specjalnych metod i są wykonywane przez przeglądarki WWW. JWS pozwala na wykonywanie standardowych aplikacji dysponujących graficznym interfejsem użytkownika. Technologia ta jest przeznaczona dla osób poszukujących łatwości dostępu do aplikacji, jaką zapewniają przeglądarki WWW z pełnymi możliwościami funkcjonalnymi typowymi dla standardowych aplikacji. Z punktu widzenia użytkownika cały proces wykorzystania technologii Java Web Start ma następującą postać: na stronie WWW umieszczane jest połączenie wskazujące aplikację, którą użytkownik chce uruchomić; jeśli na komputerze użytkownika już wcześniej zostało zainstalowane oprogramowanie JWS (zagadnienie to opisałem w dalszej części receptury), to wystarczy kliknąć odpowiednie połączenie (nazwijmy je umownie „Uruchom”), a po kilku chwilach lub minutach (zależnie od szybkości łącza) aplikacja zostanie pobrana i uruchomiona. Rysunek 21-6 przedstawia powitalną winietę wyświetlaną po kliknięciu połączenia Run wskazującego stworzoną przeze mnie aplikację JabaDex.

Po pomyślnym zakończeniu pobierania aplikacji zostanie ona uruchomiona. W nieco „skondensowany” sposób przedstawiłem to na Rysunek 21-7.

Dla wygody użytkownika pliki JAR oraz wszelkie inne zasoby konieczne do wykonania aplikacji są przechowywane w pamięci podręcznej. Dzięki temu można później uruchomić aplikację (nawet bez połączenia z internetem), posługując się specjalnym programem służącym do uruchamiania aplikacji JWS. Rysunek 21-8 przedstawia program Java Web Start Application Manager informujący o możliwości uruchomienia aplikacji JabaDex. Technologia JWS pozwala także na tworzenie skrótów do „aplikacji JWS” i umieszczania ich na pulpicie systemu bądź uruchamiania aplikacji z menu Start; oczywiście jeśli tylko używany system operacyjny na to pozwala.

Podstawowe czynności, jakie należy wykonać, aby przygotować aplikację do rozpowszechniania przy użyciu technologii JWS, przedstawiłem w poniższej notatce.

Przeanalizujmy teraz szczegółowo powyższe czynności. Pierwszym krokiem jest umieszczenie aplikacji w jednym lub kilku plikach JAR. Sposoby korzystania z programu jar zostały opisane we wcześniejszej części niniejszego rozdziału. Główny plik JAR powinien zawierać klasy tworzące aplikację oraz wszelkie inne zasoby, takie jak pliki właściwości, obrazy i tak dalej.

W witrynie WWW oprócz pliku JAR z samą aplikacją należy także udostępnić inne pliki JAR zawierające wszelkie dodatkowe interfejsy programistyczne, takie jak JavaMail, com.darwinsys.util i tak dalej. Można także zamieścić programy stworzone w innych językach, choć ich poprawne działanie będzie zależeć od używanej platformy systemowej.

Opcjonalnie można także podać ikony, które będą reprezentować publikowaną aplikację w formacie JWS. Ikony te powinny być zapisane w formatach GIF lub JPEG i mieć wymiary 64×64 piksele.

Kolejnym etapem jest stworzenie pliku opcji JNLP zawierającego informacje o aplikacji. Pliki JNLP to zwyczajne pliki XML. Oficjalną specyfikację plików JNLP można znaleźć pod adresem http://java.sun.com/products/javawebstart/download-spec.html. Plik, którego użyłem w celu umożliwienia wykonywania aplikacji JabaDex przy użyciu technologii Java Web Start, zawiera jedynie podzbiór wszystkich dostępnych elementów XML. Niemniej jednak w zasadzie można go zrozumieć bez żadnych dodatkowych wyjaśnień. Kod tego pliku przedstawiłem na Przykład 21-3.

W razie konieczności należy zmodyfikować listę typów MIME serwera WWW w taki sposób, aby pliki JNLP były zwracane jako dane typu application/x-java-jnlp-file. Sposób, w jaki należy to zrobić, zależy tylko i wyłącznie od używanego serwera WWW, choć wszystko powinno się sprowadzić do określenia typu skojarzonego z rozszerzeniem .jnlp.

Kolejną czynnością, jaką należy wykonywać tylko w razie konieczności, jest zmodyfikowanie aplikacji w taki sposób, aby pobierała obiekt ClassLoader i zamiast bezpośredniego odczytywania plików uzyskiwała dostęp do zasobów, posługując się jego metodami getResource(). Obrazki oraz wszelkie inne zasoby należy pobierać właśnie w taki sposób. Aby na przykład jawnie pobrać plik właściwości, można by się posłużyć metodami getClassLoader() oraz getResource() w sposób przedstawiony na Przykład 21-4.

// Wczytanie pliku właściwości do obiektu Properties.
try {
    p.load(is);
} catch (IOException ex) {
    System.err.println("Pobranie właściwości nie powiodło się: " + ex);
    return;
}

// Wyświetlenie zawartości w celu potwierdzenia wczytania.
p.list(System.out);

Warto zauważyć, że w tym przypadku metoda getResource() zwraca obiekt java. net.URL, a metoda getResourceAsStream() — obiekt InputStream.

Aby możliwości funkcjonalne uruchamianej aplikacji nie były ograniczane (a zapewne w przypadku uruchamiania standardowych aplikacji będzie to ze wszech miar pożądane), konieczne będzie podpisanie pliku JAR. Proces cyfrowego podpisywania pliku JAR został szczegółowo opisany w „21.12. Podpisywanie plików JAR”. Jeśli aplikacja będzie chciała korzystać z pełnych możliwości, a jej pliki JAR nie zostaną podpisane, to na ekranie zostanie wyświetlony smutny komunikat informacyjny przedstawiony na Rysunek 21-9.

W przypadku samodzielnego podpisania plików JAR (na przykład przy użyciu testowego certyfikatu) użytkownik uruchamiający aplikację zobaczy ostrzeżenie przedstawione na Rysunek 21-10.

Ostatnią czynnością, jaką należy wykonać, jest umieszczenie w witrynie połączenia wskazującego na plik JNLP przygotowany z myślą o udostępnianej aplikacji oraz, ewentualnie, połączenia umożliwiającego pobranie oprogramowania JWS. Oprogramowanie to jest udostępniane w formie skompilowanego programu wykonywanego przez przeglądarkę jako „aplikacja pomocnicza”. Użytkownik musi je pobrać i zainstalować, nim będzie mógł uruchamiać aplikacje rozpowszechniane za pomocą JWS. Niezbędny program w skompilowanej formie jest dostępny na stronie poświęconej technologii Java Web Start. Teoretycznie rzecz biorąc, w razie konieczności można by stworzyć własną implementację tej aplikacji, bazując na informacjach podanych w specyfikacji JNLP.

W rzeczywistości, jeśli oprogramowanie JWS jest już zainstalowane na komputerze użytkownika, to połączenie umożliwiające pobranie tego oprogramowania nie jest potrzebne. Jeśli jednak oprogramowanie nie zostało jeszcze zainstalowane, to połączenie Uruchom nie będzie działać poprawnie. Na stronie Developer’s Guide można znaleźć informacje opisujące, jak wykorzystać skrypty działające po stronie klienta (JavaScript lub VBScript), aby na stronie było widoczne tylko jedno z tych połączeń. Połączenie Uruchom musi wskazywać na plik JNLP:

Jeśli zainstalowałeś oprogramowanie JWS, możesz <a href=""jabadex.jnlp>uruchomić aplikację JabaDex</a>.
W przeciwnym razie <a href="http://www.oracle.com/technetwork/java/javase/javawebstart/">
zdobądź więcej informacji na temat tej technologii</a>.

Teraz aplikację będzie już można pobierać i uruchamiać za pośrednictwem internetu!

Pliki JAR można podpisywać cyfrowo, co pozwala na określenie tożsamości ich twórcy. Rozwiązanie to przypomina cyfrowe podpisywanie witryn WWW: użytkownicy wiedzą już, że nie należy podawać ważnych informacji, takich jak numery kart kredytowych, jeśli ikonka kłódki w przeglądarce informuje, że witryna nie jest podpisana cyfrowo. Proces cyfrowego podpisywania plików JAR wykorzystuje zabezpieczające mechanizmy programistyczne stanowiące integralną część jądra platformy Java 2. Pliki JAR należy podpisywać, gdy zawierają one aplety (patrz Rozdział 16.) lub aplikacje JWS (patrz „21.11. Java Web Start”). W obu przypadkach wykorzystywany jest ten sam program — jarsigner — należący do standardowej wersji języka Java 2.

Gdy tworzona aplikacja jest gotowa do opublikowania, należy wystąpić z prośbą o wydanie certyfikatu do jednej z agencji komercyjnych. Oczekując na otrzymanie właściwego certyfikatu, można testować aplikację, posługując się testowym certyfikatem stworzonym własnoręcznie. Poniżej opisałem czynności, jakie należy wykonać, aby podpisać plik JAR przy użyciu certyfikatu testowego:

Program jarsigner zmodyfikuje zawartość katalogu META-INF podpisywanego pliku JAR, dodając do niego informację o certyfikacie oraz cyfrowe podpisy wszystkich elementów umieszczonych w archiwum.

Podpisanie archiwum może zabrać nieco czasu, zależnie od szybkości procesora, liczby plików umieszczonych w archiwum oraz innych czynników. W efekcie uzyskujemy jednak podpisany cyfrowo plik JAR, który będzie można wykorzystywać do rozpowszechniania apletów, aplikacji uruchamianych przy użyciu technologii Java Web Start oraz we wszystkich innych mechanizmach wymagających podpisanych archiwów.

Więcej informacji na temat podpisywania cyfrowego oraz zezwoleń można znaleźć w książce Java Security Scotta Oaksa wydanej przez wydawnictwo O’Reilly (http://shop.oreilly.com/product/9780596001575.do). Z kolei najlepszym źródłem informacji na temat narzędzi JDK, o których wspominałem w tym rozdziale, jest dokumentacja dostarczana wraz z używaną wersją JDK.



[79] To ostatnie stwierdzenie nie do końca jest prawdą. W systemach Unix, przynajmniej w języku C, istnieje rozróżnienie pomiędzy normalnymi plikami dołączanymi a plikami przechowywanymi w podkatalogu sys, a nazwy pól wielu struktur rozpoczynają się od jednej bądź dwóch liter i znaku podkreślenia, na przykład pola struktury hasła mają następujące nazwy: pw_name, pw_password, pw_home i tak dalej. Niemniej jednak i tak nie można tego w żaden sposób porównać ze spójną strukturą nazewnictwa pakietów stosowaną w Javie.

[80] Niektórzy lubią używać takich nazw jak MyPackage.mf, tak by było jasne, czego dotyczy plik manifestu; rozszerzenie pliku jest dowolne, nie musi to być .mf, jednak stosowanie właśnie tego rozszerzenia stanowi dobry, zwyczajowy sposób oznaczania plików manifestów.