M. Janic - Rozszerzanie funkcjonalności aplikacji w trakcie działania

Transkrypt

M. Janic - Rozszerzanie funkcjonalności aplikacji w trakcie działania
Rozszerzanie funkcjonalności aplikacji w trakcie działania
programu poprzez dynamiczne ładowanie klas na przykładzie
pluginów do wczytywania plików w języku Java
Marcin Janic1
1
Wydział Inżynierii Mechanicznej i Informatyki
Kierunek informatyka, Rok III
{poonury}@gmail.com
Streszczenie
Celem niniejszej pracy jest omówienie hierarchii ładowania klas oraz prezentacja
sposobu dynamicznego ładowania klas w języku programowania Java. Do realizacji tego
zagadnienia należy utworzyć własną procedurę ładująca, która rozszerza klasę
"ClassLoader" i zastąpi jej metodę "findClass". Utworzona w ten sposób procedura
ładująca pozwoli na ładowanie nieznanych podczas tworzenia aplikacji rozszerzeń plików.
1 Wstęp
Podczas tworzenia aplikacji twórca nie zawsze może przewidzieć dokładnego kierunku
jej rozwoju. Bardzo często podczas działania zauważa się, że potrzebna jest całkiem
nowa funkcjonalność, której analityk bądź programista nie przewidział w fazie
projektowania. Tworząc aplikacje należy używać elastycznych rozwiązań co znacznie
ułatwia późniejszą rozbudowę jej funkcjonalności. Jedną z metod tworzenia programów
jest rozszerzanie ich funkcjonalności w trakcie działania programu poprzez dynamiczne
ładowanie klas. Tak zaprojektowana aplikacja może wybrać, które klasy są jej w tym
momencie niezbędne i je załadować.
2 Klasa w programowaniu obiektowym
Klasa jako byt abstrakcyjny znajduje swoje zastosowanie w programowaniu
obiektowym. Jest ona częściową bądź całkowitą definicją obiektu. Tworząc klasę
tworzymy nowy typ danych, którego składnikami są inne typy danych nazywane polami
klasy. Dodatkowo klasa posiada implementacje funkcji zwanych inaczej metodami,
które operują na polach. Klasa[2] może być odwzorowaniem dowolnego rzeczywistego
bądź abstrakcyjnego obiektu. Ułatwia to programiście spojrzenie na projekt obiektywnie
co wcale nie jest łatwe przy założeniu, że powinien nadawać się do wielokrotnego
użytku. Niektóre problemy z jakimi spotykają się programiści powtarzają się, a ich
rozwiązanie jest uniwersalne, oraz sprawdzone w praktyce co pozwala na wykorzystanie
pewnych wzorców projektowych. Temat wzorców projektowych został opisany przez
Erich Gamma [3].
1
3 Ładowanie klas
Maszyna wirtualna, jest to ogólna nazwa środowiska, gdzie uruchamiane są programy.
Kontroluje ona wszelkie bezpośrednie odwołania do sprzętu bądź systemu operacyjnego
przez program przez co zapewnia ich obsługę. Wirtualna maszyna Javy powszechnie
nazywana jest samodzielnym interpreterem. Istniały też komputery, które zdolne były
uruchamiać kod bajtowy Javy bezpośrednio stąd można ją postrzegać jako emulator
tych maszyn. Kod źródłowy programu napisanego w Javie zostaje przekształcony przez
kompilator w kod maszyny wirtualnej. Kod ten umieszczany jest w plikach
posiadających rozszerzenie .class, które zawierają kody definicji i implementacji klasy
lub interfejsu. Następnie program tłumaczący kod maszyny wirtualnej na kod maszyny
na, której wykonuje się program interpretuje pliki klas. Interpreter ładuje jedynie
potrzebne pliki bez, których nie będzie w stanie wykonać programu. Standardowymi
działaniami maszyny wirtualnej w przypadku uruchomienia klasy są:
1) Maszyna wirtualna posiada mechanizm ładowania plików z klasami, z dysku lub
sieci Internet, który wykorzystuje do załadowania żądanej klasy.
2) Jeśli ładowana klasa posiada zmienne typów innych klas lub klasy bazowe to
ładowane są także pliki tych klas.
3) Maszyna wirtualna wykonuje metodę main ładowanej klasy. Jest to metoda
statyczna, więc nie jest tworzona instancja tejże klasy.
4) Jeśli wywołana metoda main lub jakakolwiek inna wywołana w czasie
wykonywania programu wymaga niezaładowanego dotychczas pliku klasy to
jest on ładowany.
Więcej na temat ładowania klas można przeczytać[4], oraz w dokumentacji języka Java
[5].
4 Procedury ładuj ące
Mechanizm ładowania klas wykorzystuje przynajmniej trzy procedury ładowania klas:
–
początkowa,
–
rozszerzeń,
–
systemowa.
Klasy systemowe ładowane są przez procedurę początkową. Zazwyczaj jest
zaimplementowana w języku C, oraz stanowi integralną część maszyny wirtualnej.
Standardowe rozszerzenia maszyny wirtualnej z katalogu jre/lib/ext ładowane są przez
procedurę rozszerzeń. W podanym katalogu możemy, także umieścić swój własny plik
.jar co pozwoli na ładowanie zawartych w nim klas bez konieczności podawania ścieżki
dostępu.
Pliki klas aplikacji ładowane są przez procedurę systemową. Procedura poszukuje
plików w katalogach, plikach .jar, oraz .zip wykorzystując ścieżki dostępu do klas
zapisane w zmiennej środowiskowej CLASSPATH lub fladze -classpath wpisanej w
wierszu poleceń.
2
5 Hierarchia ładowania klas
Procedury ładowania posiadają swoje hierarchie, każda z nich posiada swoją procedurę
nadrzędną. Procedura początkowa, która jako jedyna nie posiada, jest wyjątkiem.
Procedura podrzędna musi najpierw, umożliwić znalezienie i załadowanie pliku klasy
swojej procedurze nadrzędnej, gdy ładowanie nie powiedzie się wtedy procedura
podrzędna próbuje załadować go sama. Przykładem takiej zależności może być
otrzymanie polecenia załadowania klasy systemowej przez procedurę systemową na
przykład:
import java.util.Vector;
import java.io.FileInputStream;
import java.io.IOException;
Wtedy, najpierw polecenie przekazywane jest do rozszerzonej, a stamtąd z kolei do
procedury początkowej. Ponieważ procedura początkowa odnajdzie i załaduje plik to
żadna z pozostałych procedur nie będzie, już kontynuować procesu poszukiwania tejże
klasy. Poniższy rysunek obrazuje hierarchie procedur ładujących.
Rys. 1 - Hierarchia poszczególnych procedur ładujących
Zazwyczaj programista zwolniony jest z zajmowania się procedurami ładującymi z
powodu iż większość klas ładowana jest automatycznie przez inne klasy, które je
używają. W niektórych wypadkach jednak korzystniej jest zaimplementować własną
procedurę.
3
6 Procedury ładuj ące a przestrzenie nazw
Język Java wykorzystuje pakiety w celu określania przestrzeni nazw, oraz zapobieganiu
konfliktów nazw klas. Istnieje wiele klas o tej samej nazwie, lecz znajdują się w
różnych pakietach co pozwala jednoznacznie określić ich tożsamość. Program
wykonywalny posługuje się tylko i wyłącznie, pełnymi nazwami klas określonymi
poprzez umieszczenie ich w pakietach. Nazwa krótka Date używana jest przez
programistów dla ich wygody i wymaga polecenia import podanego na początku
programu:
import java.util.Date;
import java.sql.Date;
Zdarzają się również przypadki, gdzie dwie klasy posiadają identyczne nazwy, oraz
pakiety. Maszyna wirtualna radzi sobie z takimi przypadkami, ponieważ oprócz pełnej
nazwy klasa określana jest też przez procedurę ładującą. Jest to przydatne podczas
ładowania kodu z różnych źródeł. Przykładem takiej sytuacji może być przeglądarka
internetowa, która dla każdej ze strony internetowej używa osobnej instancji klasy,
która ładuje aplet. Bez względu na to jak nazwano klasy, maszyna wirtualna jest w
stanie rozróżnić klasy apletów na różnych stronach. Zakładając, że strona zawiera 2
aplety dostarczone przez 2 różnych programistów, a każdy z nich zawiera klasę o
nazwie Main. Z powodu iż, każdy aplet ładowany jest przez osobną procedurę ładującą
to obie, klasy Main są jednoznacznie rozróżniane i nie występuje jakikolwiek konflikt
nazw. Zobrazować może to poniższy schemat.
Rys. 2 - Ładowanie dwóch różnych klas o tej samej nazwie
4
7 Implementacja w łasnej procedury ładuj ącej
Tworzenie własnych procedur ładujących nastawione
zastosowania, które dotyczą podjęcia pewnych działań
maszynie wirtualnej. Jednym z przykładowych działań jest
(np. w przypadku barku wykupionej licencji), bądź
alternatywnej o ograniczonych usługach.
By stworzyć w pełni funkcjonalną procedurę ładującą
ClassLoader.
jest na wyspecjalizowane
przed przekazaniem kodu
odmowa załadowania klasy
załadowanie jakieś klasy
musimy rozszerzyć klasę
class PluginLoader extends ClassLoader
{
// ...
}
Następnie zastąpić jej metodę findClass(String className).
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Przeciążona metoda findClass, musi załadować kod danej klasy wykorzystując system
plików lub inne źródło umożliwiające pobranie kodu.
// Utworzenie pustej tablicy bajtów
byte [] classBytes = null;
try
{
// wywołanie metody ładującej kod klasy i nadpisanie tablicy
classBytes = loadClassBytes(name);
}
catch (IOException ex)
{
System.err.println("PluginLoader " + ex);
}
Metodę ładującą, można wykorzystać do odszyfrowywania kodu klasy. W ramach
uproszczenia prezentowanego przykładu zignorowano dorobek ludzkości i zastosowano
prosty szyfr Cezara[6]. Użyta wersja szyfru używa klucza z zakresu od 1 do 255.
Szyfrowanie polega na dodaniu klucza do czytanego bajtu, a następnie wyznaczenia
reszty z dzielenia uzyskanej sumy przez 256.
private byte[] loadClassBytes(String name) throws IOException
{
// zamiast .class można dać dowolne rozszerzenie pliku
String cname = name.replace('/', '.') + ".class";
// stworzenie strumienia wejściowego i otwarcie go dla pliku name.class
FileInputStream in = null;
in = new FileInputStream( cname );
try
{
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
int ch;
// wczytywnie do końca pliku
while((ch = in.read() ) != -1 )
5
{
byte b = (byte) (ch - klucz);
// zapisanie danych do bufora
buffer.write(b);
}
in.close();
// zwrocenie wczytanego pliku w postaci tablicy bajtów
return buffer.toByteArray();
}
finally
{
in.close();
}
} // End loadClassBytes(String name)
W końcowej fazie wczytywania funkcja, musi wywołać metodę defineClass klasy
bazowej ClassLoader by przekazać kod klasy do maszyny wirtualnej. Jako argument
podajemy nazwę klasy, która nie musi mieć wcale rozszerzenia .class. Można ustalić
swoje własne rozszerzenie, aby nie wprowadzać w błąd zwykłych procedur ładowania.
//przekazuje nową klasie maszynie wirtualnej, kod klasy
//umieszczony jest w zakresie off=0 i len=classBytes.length
Class<?> cl = defineClass(name, classBytes, 0, classBytes.length);
if( cl == null ) throw new ClassNotFoundException(name);
//zwrocenie klasy
return cl;
}
Do uruchomienia dowolnej metody z wczytanej klasy można zastosować przykładową
metodę.
public static void runPlugin(String name)
{
try
{
// Utworznie nowej instancji klasy PluginLoader
ClassLoader loader = new PluginLoader();
//Zdefiniowanie nowej klasy
Class <?> c = loader.loadClass(name);
// Pobranie metody o nazwie "main" i parametrach String[]
Method m = c.getMethod("main", String[].class);
// Utworzenie parametrow
Object args = new String[] {};
// Wywołanie metody i przypisanie zwracenej wartości do ret
Object ret = m.invoke(c.newInstance(), args);
System.out.println( "Funkcja zwrucila "+ ret);
}
catch (Exception ex)
{
System.err.print("RunPlugin " + ex);
}
} // End RunPlugin(String name)
6
8 U życie pluginów do wczytywania plików
Prezentowany program przeznaczony jest do wizualizacji obliczeń inżynierskich.
Obliczenia wykonane są za pomocą metody elementów skończonych dla siatek
dwuwymiarowych. Dane wejściowe, takie jak punkty współrzędnych węzłów siatki,
powiązań między tymi węzłami, oraz wartości w węzłach dla poszczególnych kroków
czasowych zapisane są w różnych formatach poczynając od .xml przez .obj, a kończąc
na zwykłym .txt. Klasa, która jest odpowiedzialna za wczytanie danych wejściowych
jest wybierana na podstawie rozszerzenia, a następnie dynamicznie ładowana do
pamięci. Pozwala to na elastyczne dodawanie nowych formatów danych wejściowych
bez konieczności rekompilowania głównego modułu. Poniższy diagram obrazuje
funkcjonalność Wizualizatora dotyczącą wczytywania danych wejściowych.
Rys. 3 – Diagram funkcjonalności Wizualizatora
Za pomocą tej aplikacji, można wygenerować obraz siatki(bez wyników obliczeń) lub
rozkład wartości (np. temperatury) w postaci mapy (przejście kolorów), oraz w postaci
wykresu konturowego (izolinii). Aplikacja umożliwia także przedstawienie wyników w
formie animacji.
9 Podsumowanie
Przedstawiony sposób implementowania własnych procedur ładujących jest narzędziem
bardzo elastycznym, posiadającym swoje plusy jak i minusy. Bardzo szybko można
rozbudować aplikację o nowe możliwości, bez konieczności rekompilowania głównego
modułu, którego funkcjonalność była by rozszerzana. Czynności, które możemy podjąć
przed wczytaniem klasy dają nam cały wachlarz możliwości od prostego wczytania
klasy poprzez weryfikacje kodu bajtowego, aż po wczytywanie zaszyfrowanych klas
bądź po prostu odmowy załadowania klasy. Mechanizm może być, wykorzystany do
autoryzacji użytkowników i wczytywać jedynie klasy na, które użytkownik posiada
licencje. Klasy mogą zostać zaszyfrowane i bez odpowiedniego klucza zawartego w
procedurze ładującej wczytanie ich staje się niemożliwe, co czyni je bezużytecznymi.
7
Klasy mogą mieć różne wersje, co pozwala na wprowadzenie dodatkowej
funkcjonalności o, które potencjalny klient mógłby się upomnieć. Programista nie jest w
stanie przewidzieć kierunku rozwoju aplikacji przez co mechanizm ten staje się bardzo
użyteczny w przypadku bardziej rozbudowanych projektów.
10 Bibliografia
[1] Cay S. Horstmann, Gary Cornell, Java. Techniki zaawansowane. Wydanie VIII,
2009.
[2] Herbert Schildt, Java. Kompendium programisty, 2005
[3] Erich Gamma, Richard Helm, Ralph Johnsin, John Vlissides, Wzorce projektowe.
Elementy oprogramowania obiektowego wielokrotnego użytku, 2010
[4] http://edu.pjwstk.edu.pl/wyklady/mpr/scb/W3/W3.htm
- Dynamiczna Java i
programowanie komponentowe (29 maja 2012)
[5] http://docs.oracle.com/javase/6/docs/ - Dokumentacja języka Java (29 maja 2012)
[6] http://www.kryptografia.com/algorytmy/cezar.html – Opis szyfrowania za pomocą
metody Cezara (29 maja 2012)
8

Podobne dokumenty