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