|
10
XML-RPC |
|
W tym rozdziale zostanie omówione kolejne ciekawe zastosowanie języka XML — model żądań i odpowiedzi XML-RPC. XML-RPC to po prostu specyficzna odmiana RPC, czyli technologii wywoływania zdalnych procedur. Jeśli Czytelnik nie jest doświadczonym programistą, bądź też do tej pory posługiwał się tylko językiem Java, pojęcie zdalnego wywoływania procedur może być mu obce; dla innych zaś poruszanie tego tematu może wydać się dziwne — RPC w ostatnich czasach stracił na popularności. W trakcie lektury niniejszego rozdziału Czytelnik dowie się, dlaczego te trzy niepozorne literki przed skrótem RPC zrewolucjonizowały coś, co wydawało się być dinozaurem w świecie programistycznym. Czytelnik nauczy się również korzystać z XML-RPC z poziomu programów w Javie. Zakończenie rozdziału poświęcone jest praktycznym zastosowaniom modeli XML-RPC.
Jeśli Czytelnik poddał się fali programowania obiektowego, tak popularnego w ostatnich latach, to pewnie wzdryga się na dźwięk słowa „procedura”. Języki proceduralne, takie jak PL/SQL czy ANSI C, nie są „modne” i jest ku temu wiele powodów. Zapewne niejednokrotnie Czytelnikowi zdarzyło się „oberwać” za nazwanie metody Javy funkcją lub procedurą; wie też, jak unikać „kodu spaghetti” — czyli kodu polegającego na łączeniu wielu metod w jeden długi łańcuch. Wraz z tymi językami i sposobami programowania odsunięto na bok technologię RPC — nowe, obiektowe techniki umożliwiają osiągnięcie tych samych rezultatów przy bardziej przejrzystej strukturze programu i większej wydajności. Jednak, co ciekawe, popularyzacja XML-a przyczyniła się do upowszechnienia interfejsów API zbudowanych z myślą o XML-RPC oraz do coraz częstszego wykorzystywania XML-RPC mimo niedobrych skojarzeń, jakie się z tym skrótem wiążą.
Zanim Czytelnik zacznie stosować te interfejsy, powinien przyjrzeć się technologii RPC i porównać ją z podobnymi rozwiązaniami Javy, a szczególnie z technologią RMI (Remote Method Invocation). Jeśli zdecydujemy się na wykorzystanie XML-RPC w aplikacji (a na pewnym etapie pracy z pewnością tak się stanie), na pewno będziemy musieli uzasadnić swój wybór innym programistom — szczególnie tym, którzy mają właśnie za sobą lekturę o EJB czy RMI. Wszystkie te technologie mają na pewno swoje miejsce, ale zrozumienie właściwego ich stosowania jest bardzo istotne. Najbardziej popularnymi sposobami operowania na obiektach poprzez sieć są RPC i RMI.
RPC kontra RMI
Technologia RMI zyskuje ogromną popularność wśród programistów Javy. Cała specyfikacja EJB (Enterprise JavaBeans) została oparta na bazie RMI; umiejętność pisania trzywarstwowych aplikacji RMI jest koniecznością. Jeśli Czytelnik jeszcze tego nie potrafi, powinien sięgnąć po Java Enterprise in a Nutshell (autorzy: David Flanagan, Jim Farley, William Crawford i Kris Magnusson) lub Java Distributed Computing Jima Farleya (obie książki wydawnictwa O'Reilly & Associates).
Co to jest RMI?
Technologia RMI to wywoływanie zdalnych metod (remote method invocation). Koncepcja wydaje się dość prosta — RMI umożliwia wywoływanie metod na obiekcie znajdującym się na innym komputerze niż program. Na tym opiera się całe programowanie rozproszone w Javie i to właśnie stanowi szkielet technologii EJB oraz wielu implementacji aplikacji dla przedsiębiorstw. Bez zagłębiania się w detale można powiedzieć, że RMI za pomocą procedur pośredniczących (ang. stub) oraz szkieletów opisuje metody dostępne do zdalnego wywołania. Klient wykorzystuje owe procedury pośredniczące (zazwyczaj mające postać interfejsów Javy), a RMI obsługuje całą „magię” tłumaczenia żądań wysyłanych do procedury pośredniczącej na wywołania sieciowe. Metoda działa na faktycznym obiekcie komputera zdalnego; wynik przesyłany jest z powrotem drogą sieciową. Na koniec procedura pośrednicząca zwraca wynik klientowi, który jako pierwszy wywołał metodę, i klient może działać dalej. Przede wszystkim trzeba pamiętać, że klienta „nie interesują” szczegółowe zasady działania RMI i sieci; procedura pośrednicząca wykorzystywana jest tak, jak gdyby był to faktyczny obiekt implementujący określone metody. Technologia RMI (za pomocą protokołu zdalnego JRMP™) działa tak, że cała komunikacja sieciowa odbywa się poza kulisami; klient ma dostęp do ogólnego wyjątku (java.rmi.RemoteException), a programista może skupić się na regułach biznesowych i logice aplikacji. Technologia RMI umożliwia również korzystanie z różnych protokołów (np. Internet Inter-ORB Protocol) — dzięki temu można uruchomić komunikację pomiędzy obiektami Java i CORBA, często także w innych językach, np. C lub C++.
Technologia RMI ma również jednak pewne wady. Po pierwsze, intensywnie wykorzystuje zasoby. Używanych jest całkiem sporo klas i choć stanowią one standardowe wyposażenie pakietu JDK, to przecież stworzenie ich egzemplarza pochłania pamięć i inne zasoby. Protokół JRMP charakteryzuje się słabą wydajnością, a zastąpienie go wcale nie jest prostym zadaniem. Kiedy klienty wysyłają żądania RMI, muszą zostać otwarte i utrzymane gniazda, a im jest ich więcej, tym bardziej spada wydajność systemu — szczególnie wtedy, gdy system dostępny jest w sieci (wtedy dla dostępu HTTP konieczne jest otworzenie dalszych gniazd). Technologia RMI wymaga również istnienia serwera lub usługodawcy, do których można dowiązać obiekty. Dopóki obiekt nie zostanie dowiązany do nazwy odpowiadającej takiemu usługodawcy, dopóty nie będzie dostępny z poziomu innych programów. To wymaga użycia rejestru RMI, serwera usług katalogowych LDAP lub innych usług typu Java Naming and Directory Interface (JNDI). I jeszcze jedno — korzystanie z RMI może wiązać się z koniecznością pisania dużej ilości kodu, nawet biorąc pod uwagę to, jak wiele pomocnych klas udostępnia JDK. Konieczne jest zaprogramowanie zdalnego interfejsu opisującego metody dostępne do wywołania oraz (jeśli korzystamy z EJB) szeregu innych interfejsów. Oznacza to również, że oddanie kolejnej metody do klasy serwera powoduje zmianę interfejsu i przekompilowanie procedur pośredniczących, co często nie jest pożądane (a niejednokrotnie jest po prostu niemożliwe).
Co to jest RPC?
Technologia RPC to wywoływanie zdalnych procedur (remote procedure calls). RMI umożliwia bezpośrednie działanie na obiekcie Javy, natomiast RPC pozwala korzystać z metod samodzielnych (tak, tutaj nazywane są one procedurami!) za pośrednictwem sieci. To ogranicza interaktywność, ale upraszcza interfejs, z którym komunikuje się klient. Technologię RPC można sobie wyobrazić jako sposób korzystania z „usług” komputerów zdalnych, z kolei RMI umożliwia korzystanie z „serwerów”. Z tej subtelnej różnicy wynika fakt, że proces RMI jest zazwyczaj sterowany w całości przez klienta — zdarzenia następują po zdalnym wywołaniu metod. RPC to częściej klasa lub zestaw klas wykonujących zadania bez interwencji klienta; jednakże czasami klasy te obsługują żądania przesłane przez klienty i wykonują ich „minizadania”. Przedstawione w dalszej części rozdziału przykłady pomogą zrozumieć te teoretyczne wywody.
RPC nie jest środowiskiem tak interaktywnym jak RMI, ale oferuje szereg zalet względem tego ostatniego. Umożliwia współpracę oddzielnych systemów. Technologia RMI pozwala co prawda na łączenie Javy z serwerami i klientami CORBA (poprzez IIOP), ale RPC umożliwia komunikację dosłownie dowolnych aplikacji — protokołem transportowym może być bowiem HTTP. Ponieważ niemal dowolny wykorzystywany obecnie język oferuje sposób komunikacji za pośrednictwem HTTP, RPC stanowi niezwykle atrakcyjne rozwiązanie w przypadku tych zastosowań, w których konieczne jest porozumiewanie się z systemami zastanymi. RPC jest zazwyczaj również mniej „zasobożerny” niż RMI (szczególnie wtedy, gdy do kodowania wykorzystywany jest XML — o czym za chwilę); RMI często wymaga przesłania przez sieć całych klas Javy (np. kodów apletów czy własnych klas pomocniczych EJB), a RPC musi tylko przekazać przez sieć parametry żądania i wynik działania, zazwyczaj w postaci danych tekstowych. RPC pasuje również świetnie do modelu interfejsów API — systemy nie stanowiące części określonej aplikacji i tak mogą pobierać z niej informacje; oznacza to, że zmiany poczynione w serwerze nie przekładają się na zmiany w kodzie aplikacji klientów. Przy transferze opartym na zwykłym tekście dodatkowe metody można dodawać bez konieczności rekompilacji klienta; użycie tych metod wymaga zaś tylko niewielkich zmian.
Odwieczny problem z technologią RPC związany jest z kodowaniem przesyłanych danych. Wyobraźmy sobie, że chcemy stworzyć tekstową i niewielką objętościowo reprezentację struktur Javy Hastable lub Vector. Jeśli weźmiemy pod uwagę, że te struktury mogą z kolei zawierać inne obiekty Javy, taka reprezentacja nagle przestaje być zadaniem banalnym; stworzony format musi ponadto nadawać się do użycia przez dowolne języki programowania — inaczej umniejszymy korzyści płynące z zastosowania technologii RPC. Do niedawna zachodziła odwrotnie proporcjonalna relacja pomiędzy jakością i użytecznością kodowania w RPC a jego prostotą; innymi słowy, im prościej reprezentowane były złożone obiekty, tym trudniej było wykorzystać takie kodowanie w wielu językach bez tworzenia dodatkowego, własnego kodu. Rozbudowane tekstowe reprezentacje danych nie zostały ustandaryzowane i wymagały całkowicie nowych implementacji w poszczególnych językach. W tej chwili Czytelnik zapewne domyśla się już, do czego zmierzam.
XML-RPC
Największą przeszkodą w używaniu technologii RPC był zawsze sposób kodowania. Kiedy pojawił się XML-RPC, wszystko uległo zmianie. XML oferował nie tylko prostą, tekstową reprezentację danych; stanowił także standard strukturyzacji danych. Kiedy grupa W3C opublikowała specyfikację XML 1.0, zniknęły obawy o konieczność stosowania rozwiązań własnych — korzystający z RPC mieli pewność, że XML w najbliższym czasie nie sprawi im żadnej niespodzianki. Co więcej, standard SAX stworzył możliwość uzyskania dostępu do XML-a w sposób wydajny i standardowy; to uprościło implementację bibliotek RPC. Pozostała więc już tylko do uruchomienia transmisja poprzez HTTP (coś, co robimy już od ponad 10 lat) oraz specyficzne interfejsy kodowania i dekodowania. Po kilku implementacjach beta bibliotek XML-RPC stało się jasne, że oprócz pozostałych zalet, XML jest również szybki i mało wymagający — wydajność bibliotek XML-RPC była większa niż oczekiwano. Model XML-RPC stanowi obecnie prężne i stabilne rozwiązanie zdalnego wywoływania procedur.
Czytelnikowi, jako programiście Javy, XML-RPC oferuje sposób prostego tworzenia „punktów zaczepienia” w aplikacji i usługach, zarówno do użytku własnego, jak i do wykorzystania przez klienty aplikacji w różnych oddziałach, a nawet firmach. Interfejsy API są odseparowane od Javy, klienty nie muszą więc korzystać bezpośrednio z tego języka. Ponadto dzięki XML-RPC nie trzeba już się uczyć o RMI, aby korzystać z usług rozproszonych (przynajmniej na początku). Niniejszy rozdział stanowi opis implementacji serwera i klienta XML-RPC. Czytelnik przekona się również, że serwer może działać niezależnie od klientów, wciąż jednak udostępniając interfejsy współpracujące z XML-RPC i pozwalając na komunikację i pobieranie danych. Porównanie technologii RMI z XML-RPC pozwoli wykazać wyższość tego ostatniego w wielu zastosowaniach.
Powiedział „Witaj!”
Jeśli Czytelnikowi udało się przebrnąć przez te kilka stron wywodów o zdalnym wywoływaniu procedur, z pewnością jest przekonany, że XML-RPC to narzędzie przydatne i że może rozwiązać wiele problemów programistycznych. Aby rozwinąć temat, spróbujemy teraz zbudować prawdziwy program wykorzystujący tę technologię. Zgodnie z tradycją, zaczniemy od prostego programu „Witaj świecie!”. W naszym serwerze XML-RPC zostanie zarejestrowana procedura obsługi. Procedura ta pobiera parametr String Javy (nazwę użytkownika) i zwraca łańcuch zawierający „Witaj” oraz pobraną nazwę, np. „Witaj Brett”. Następnie procedura ta zostanie udostępniona przez serwer klientom XML-RPC. Na koniec stworzymy prostego klienta łączącego się z serwerem i żądającego wywołania tej metody.
W rzeczywistości serwer i procedura obsługi XML-RPC znajdowałyby się na jednym komputerze (zazwyczaj wydajnym serwerze), a klient na innym i cała operacja odbywałaby się zdalnie. Jednakże, jeśli nie mamy pod ręką kilku komputerów, przykłady możemy przećwiczyć lokalnie. Proces będzie przebiegał wtedy szybciej niż w rzeczywistym zastosowaniu, ale i tak będzie można zaobserwować, jak poszczególne elementy układają się w jedną całość i jak ten cały XML-RPC działa.
Skąd wziąć biblioteki XML-RPC?
Jak to zostało powiedziane wcześniej, w rozwój RPC, a ostatnio XML-RPC, włożono już wiele pracy. Podobnie jak w przypadku używania interfejsów SAX, DOM i JDOM do obsługi XML-a, nie trzeba drugi raz wynajdywać koła — na pewno istnieją dobre, a nawet świetne pakiety Javy spełniające nasze potrzeby. Centrum informacji o XML-RPC, a także zasób odsyłaczy do odpowiednich bibliotek Javy i innych języków stanowi witryna pod adresem http://www.xml-rpc.com. Serwis ten, sponsorowany przez Userland (http://www.userland.com), zawiera publiczną specyfikację XML-RPC, informacje o obsługiwanych typach danych i samouczki. Co ważniejsze jednak, można tam znaleźć odsyłacz do miejsca w sieci, z którego pobierzemy pakiet XML-RPC dla Javy, a mianowicie do strony Hannesa Wallnofera http://helma.at/hannes/xmlrpc.
Na stronie tej znajduje się opis klas zawartych w pakiecie XML-RPC Hannesa, a także instrukcja ich obsługi. Pobieramy archiwum i rozpakowujemy je w katalogu roboczym lub środowisku programistycznym IDE. Następnie kompilujemy klasy; jeden z przykładów serwletów wymaga obecności klas serwleta (servlet.jar dla interfejsu Servlet PI 2.2). Odpowiednią klasę dla mechanizmu serwletów Tomcat znaleźć można na stronie http://jakarta.apache.org. Jeśli Czytelnik nie zamierza „bawić się” przykładem z serwletem, to nie musi go kompilować — nie jest on wymagany do wykonania przykładów w tym rozdziale.
|
Klasy XML-RPC są spakowane w pliku zip, xmlrpc-java.zip. Z tego archiwum trzeba wydobyć cały kod źródłowy znajdujący się w katalogu xmlrpc-java/src/. Nie dołączono dystrybucji w postaci jar, więc wymagana jest ręczna kompilacja klas. Po skompilowaniu Czytelnik może sam utworzyć archiwum jar, co uprości włączanie klas do ścieżki. (W wersji pobranej przez tłumacza w styczniu 2001 r. odpowiedni plik jar był już dołączony — przyp. tłum.). |
Zasadnicza dystrybucja (nie zawierająca przykładów apletów i wyrażeń regularnych) składa się z ośmiu klas pakietu helma.xmlrpc: klasa zasadnicza XmlRpc, klient XmlRpcClient, serwer XmlRpcServer, XmlRpcHandler (precyzyjne sterowanie kodowaniem i przetwarzaniem XML-a) oraz szereg klas pomocniczych. Klasy zgłaszają wyjątek XmlRpcException; natomiast XmlRpcServlet demonstruje użycie serwleta jako procedury obsługi odpowiedzi HTTP; z kolei WebServer to „lekki” serwer HTTP zbudowany specjalnie do obsługi żądań XML-RPC; zaś Benchmark umożliwia zmierzenie czasu obsługi żądania XML-RPC z wykorzystaniem specyficznego sterownika SAX. Wymagane do pracy całego mechanizmu, a nie zawarte w dystrybucji, są klasy SAX (które Czytelnik powinien posiadać po wykonaniu wcześniejszych przykładów) oraz sterownik SAX; innymi słowy, trzeba mieć kompletną implementację parsera XML obsługującą SAX. W naszych przykładach będziemy ponownie korzystali z parsera Apache Xerces, ale biblioteki obsługują dowolny parser zgodny z SAX 1.0.
Po skompilowaniu plików źródłowych należy upewnić się, czy klasy XML-RPC, SAX i parsera XML znajdują się w ścieżce dostępu do klas. Po tym Czytelnik powinien już potrafić napisać pierwszy program. Warto trzymać gdzieś pod ręką pliki źródłowe XML-RPC, bo pozwalają one na podejrzenie, co dzieje się „pod maską” naszego przykładu.
Pisanie procedury obsługi
Po pierwsze, konieczne jest napisanie klasy i metody, która ma być uruchamiana zdalnie. Nazywa się ją zazwyczaj procedurą obsługi. Pamiętajmy jednak, że mechanizm XML-RPC obsługujący żądania często określany jest taką samą nazwą — znów daje się we znaki dwuznaczność przyjętego nazewnictwa. Procedura obsługi XML_RPC to metoda lub metody pobierające żądanie XML-RPC, dekodujące jego zawartość i oddelegowujące to żądanie do jakiejś klasy lub metody. Procedura obsługi odpowiedzi, czy też po prostu procedura obsługi, to metoda wywoływana przez procedurę obsługi XML-RPC. Posiadając biblioteki XML-RPC dla Javy, nie musimy pisać procedury obsługi XML-RPC, ponieważ jest już ona zawarta w klasie helma.xmlrpc.XmlRpcServer. Trzeba jedynie napisać klasę z jedną lub dwoma metodami, które zamierzamy zarejestrować w serwerze.
Niespodzianką jest fakt, że tworzenie procedury obsługi nie wymaga budowania podklas ani innych specjalnych zabiegów w kodzie. Poprzez XML-RPC można wywołać dowolną metodę, o ile tylko XML-RPC obsługuje jej parametry i zwracany typ danych (potrafi je zakodować). W tabeli 10.1 przedstawiono wszystkie obecnie obsługiwane typy Javy, jakie można wykorzystać w sygnaturach metod XML-RPC.
Lista zawiera tylko nieliczne typy, ale — jak się okazuje — umożliwiają one obsługę większości żądań XML-RPC wysyłanych poprzez sieć. Ponieważ my chcemy pobrać tylko jeden parametr String i również String zwrócić, w naszym programie powyższe typy zupełnie wystarczą. Zbudujmy więc teraz prostą klasę procedury obsługi (przykład 10.1).
Tabela 10.1. Typy Javy obsługiwane przez XML-RPC
Typ danych XML-RPC |
Typ danych Javy |
int |
int |
boolean |
boolean |
string |
java.lang.String |
double |
double |
dateTime.iso8601 |
java.util.Date |
struct |
java.util.Hashtable |
array |
java.util.Vector1 |
base64 |
byte[] |
Przykład 10.1. Klasa procedury obsługi z metodą uruchamianą zdalnie
/**
* <b><code>HelloHandler</code></b> to prosta procedura obsługi
* rejestrowana w serwerze XML-RPC.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class HelloHandler {
/**
* <p>
* Pobieramy <code>String</code> i zwracamy komunikat
* powitalny wskazanemu użytkownikowi.
* </p>
*
* @param name <code>String</code> nazwa użytkownika.
* @return <code>String</code> - komunikat Witaj.
*/
public String sayHello(String name) {
return "Witaj " + name;
}
}
I to naprawdę tylko tyle. Metoda pobiera i zwraca dozwolone parametry XML-RPC, a więc możemy spokojnie zarejestrować ją w serwerze XML-RPC — mamy gwarantowane, że będzie ją można przez XML-RPC wywołać.
Budowanie serwera
Skoro procedura obsługi jest już gotowa, musimy napisać program uruchamiający serwer XML-RPC, nasłuchujący żądań i przekazujący je do procedury obsługi. W naszym przypadku w funkcji procedury obsługującej żądania wykorzystamy klasę helma.xmlrpc.WebServer. Można byłoby w tym celu użyć serwletu Javy, ale skorzystanie z „lekkiej” implementacji serwera pozwoli uniknąć konieczności uruchamiania mechanizmu serwletów. W zakończeniu rozdziału zostaną omówione serwlety w kontekście serwera XML-RPC. Wracając do naszego serwera — chcemy pozwolić na określenie portu, na którym serwer zostanie uruchomiony, a następnie spowodować, aby serwer nasłuchiwał żądań XML-RPC dopóty, dopóki nie zostanie wyłączony. Następnie zarejestrujemy w tym serwerze stworzoną wcześniej klasę i określimy inne parametry specyficzne dla naszej aplikacji.
Stwórzmy teraz szkielet tej klasy (przykład 10.2); konieczne jest zaimportowanie klasy WebServer oraz pobranie portu z wiersza poleceń.
Przykład 10.2. Szkielet serwera XML-RPC „Witaj!”
import helma.xmlrpc.WebServer;
/**
* <b><code>HelloServer</code></b> to prosty serwer XML-RPC,
* który udostępni klasę <code>HelloHandler</code>
* wywołaniom XML-RPC.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class HelloServer {
/**
* <p>
* Uruchamiamy serwer XML-RPC i rejestrujemy procedurę obsługi.
* </p>
*/
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Użycie: java HelloServer [port]");
System.exit(-1);
}
// Tu uruchomimy serwer na określonym porcie
}
}
Przed uruchomieniem serwera trzeba podać sterownik SAX, jaki zostanie wykorzystany do przetworzenia i kodowania XML. Domyślny sterownik SAX dla tych bibliotek to parser XP Jamesa Clarka (http://www.jclark.com). Jednak w naszym przykładzie zastosujemy parser Apache Xerces, określając w mechanizmie XML-RPC implementację SAX Parser. Czynność tę wykonuje się poprzez wywołanie statycznej metody setDriver(), należącej do klasy XmlRpc. Klasa ta leży u podstaw klasy WebServer, ale w celu użycia określonych sterowników SAX konieczne jest oddzielne jej zaimportowanie i bezpośrednie użycie. Metoda zgłasza wyjątek ClassNotFoundException, a więc trzeba go przechwycić w celu określenia, czy klasa sterownika została odnaleziona. Wprowadźmy teraz opisywane zmiany:
import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;
...
/**
* <p>
* Uruchamiamy serwer XML-RPC i rejestrujemy procedurę obsługi.
* </p>
*/
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Użycie: java HelloServer [port]");
System.exit(-1);
}
try {
// Korzystamy ze sterownika Apache Xerces SAX
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
// Uruchamiamy serwer
} catch (ClassNotFoundException e) {
System.out.println("Nie odnaleziono sterownika SAX");
}
}
...
Teraz można już wstawić główny fragment kodu, nasłuchujący HTTP i obsługujący żądania XML-RPC, a następnie rejestrujący klasy procedur obsługi dostępne do wywoływania zdalnego. Tworzenie modułu nasłuchującego jest niezwykle proste; egzemplarz klasy pomocniczej WebServer, o której mówiliśmy, można utworzyć poprzez podanie jej portu do nasłuchiwania — i już nasz serwer będzie obsługiwał żądania XML-RPC! Choć nie mamy jeszcze klas dostępnych do wywołania, to serwer XML-RPC jest już gotowy do pracy. Dodamy linijkę tworzącą i uruchamiającą serwer oraz kod wyświetlający komunikat o stanie. Dodamy także kolejną dyrektywę import dla wyjątku java.io.IOException. Ponieważ serwer musi uruchamiać się na pewnym porcie, wyjątek ten może zostać zgłoszony w przypadku niedostępności portu lub innych problemów z uruchomieniem serwera. Zmodyfikowany kod wygląda następująco:
import java.io.IOException;
import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;
...
* <p>
* Uruchamiamy serwer XML-RPC i rejestrujemy procedurę obsługi.
* </p>
*/
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Użycie: java HelloServer [port]");
System.exit(-1);
}
try {
// Korzystamy ze sterownika Apache Xerces SAX
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
// Uruchamiamy serwer
System.out.println("Uruchamianie serwera XML-RPC...");
WebServer server = new WebServer(Integer.parseInt(args[0]));
} catch (ClassNotFoundException e) {
System.out.println("Nie odnaleziono sterownika SAX");
} catch (IOException e) {
System.out.println("Uruchomienie serwera nie jest możliwe: " +
e.getMessage());
}
}
...
Możemy teraz skompilować i wypróbować powyższą klasę. Powinien zostać wyświetlony komunikat o stanie, a następnie program powinien zatrzymać się, oczekując na żądania. Teraz trzeba dodać klasę procedury obsługi przyjmującą żądania.
Jedną z najistotniejszych różnic pomiędzy technologiami RMI i RPC jest sposób udostępniania metod. W RMI zdalny interfejs posiada sygnatury metod odpowiadające wszystkim metodom zdalnym. Jeśli metoda implementowana jest na klasie serwera, ale do zdalnego interfejsu nie dodano pasującej sygnatury, nowa metoda nie będzie mogła zostać wywołana przez klienta RMI. Z całym procesem łączy się więc konieczność modyfikacji sporej ilości kodu i rekompilacji klas RMI. W RPC wszystko to wygląda inaczej — a sposób obsługi uznawany jest za elastyczniejszy i prostszy. Serwer RPC otrzymuje żądanie zawierające parametry i wartość tekstową (zazwyczaj w postaci „nazwaklasy.nazwametody”). Serwer RPC „widzi”, że metoda znajduje się w klasie „nazwaklasy” i nosi nazwę „nazwametody”. Próbuje więc odnaleźć pasującą klasę i metodę pobierającą parametr takiego typu, jaki jest w żądaniu RPC. Po znalezieniu określona metoda zostaje wywołana, a rezultat tego wywołania jest kodowany i odsyłany klientowi.
Całe to nieco zawiłe wyjaśnienie sprowadza się do jednego — żądana metoda nigdy nie jest jawnie definiowana w serwerze XML-RPC, a jedynie w żądaniu otrzymanym od klienta. W serwerze rejestrowany jest jedynie egzemplarz klasy. Do klasy tej można dodawać metody, a ich udostępnienie wymaga jedynie ponownego uruchomienia serwera — a nie zmian w kodzie. O ile tylko potrafimy określić i wysłać poprawne parametry do serwera, to metoda taka jest od razu dostępna. To właśnie jedna z zalet XML-RPC względem RMI — interfejs API jest reprezentowany w sposób bliższy rzeczywistości; w kliencie nie trzeba aktualizować żadnych procedur pośredniczących, szkieletów czy interfejsów. Po dodaniu metody jej sygnatura może zostać opublikowana klientom i natychmiast wykorzystana.
Skoro wiemy już, w jak prosty sposób wykorzystywana jest procedura obsługi RPC, zarejestrujmy taką procedurę w naszym przykładzie. Klasa WebServer umożliwia dodanie procedury poprzez metodę addHandler(). Metoda ta jako dane wejściowe pobiera nazwę, pod jaką procedura obsługi zostanie zarejestrowana, oraz sam egzemplarz klasy tej procedury. Zazwyczaj tworzy się w tym celu egzemplarz nowej klasy za pomocą jej konstruktora (poprzez słowo kluczowe new), choć w dalszej części Czytelnik zobaczy, w jaki sposób można zrobić to inaczej (gdy egzemplarz ma być współdzielony przez wiele klientów, a nie tworzony dla każdego oddzielnie). Ale w poniższym przykładzie stworzenie egzemplarza nowej klasy jest dobrym rozwiązaniem. Zarejestrujemy klasę HelloHandler pod nazwą „hello”. Dodamy także instrukcje wyświetlające dalsze komunikaty, tak by możliwe było „podejrzenie”, co dzieje się w serwerze w czasie dodawania procedury obsługi:
import java.io.IOException;
import helma.xmlrpc.WebServer;
import helma.xmlrpc.XmlRpc;
/**
* <b><code>HelloServer</code></b> to prosty serwer XML-RPC,
* który udostępni klasę <code>HelloHandler</code>
* wywołaniom XML-RPC.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class HelloServer {
/**
* <p>
* Uruchamiamy serwer XML-RPC i rejestrujemy procedurę obsługi.
* </p>
*/
public static void main(String[] args) {
if (args.length < 1) {
System.out.println(
"Użycie: java HelloServer [port]");
System.exit(-1);
}
try {
// Korzystamy ze sterownika Apache Xerces SAX
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
// Uruchamiamy serwer
System.out.println("Uruchamianie serwera XML-RPC...");
WebServer server = new WebServer(Integer.parseInt(args[0]));
// Rejestrujemy klasę procedury obsługi
server.addHandler("hello", new HelloHandler());
System.out.println(
"Klasa procedury obsługi zarejestrowana jako \"hello\"");
System.out.println("Teraz czekamy na żądania...");
} catch (ClassNotFoundException e) {
System.out.println("Nie odnaleziono sterownika SAX");
} catch (IOException e) {
System.out.println("Uruchomienie serwera nie jest możliwe: " +
e.getMessage());
}
}
}
Po skompilowaniu powyższego kodu źródłowego można uruchomić serwer. Wynik powinien być podobny do tego z przykładu 10.3.
Przykład 10.3. Uruchamianie klasy serwera XML-RPC HelloServer
[adam@maly ch10]$ java HelloServer 8585
Uruchamianie serwera XML-RPC...
Klasa procedury obsługi zarejestrowana jako "hello"
Teraz czekamy na żądania...
Tak, to właśnie jest takie proste! Teraz możemy utworzyć klienta i „bawić się” komunikacją w sieci poprzez XML-RPC. To właśnie kolejna zaleta XML-RPC — niewiele potrzeba, aby taki mechanizm uruchomić, szczególnie jeśli porównamy to rozwiązanie z technologią RMI. W trakcie lektury kolejnego podrozdziału Czytelnik przekona się, że utworzenie klienta jest równie łatwe.
Budowanie klienta
Skoro serwer działa i przyjmuje żądania, to mamy już za sobą najtrudniejszy etap programowania aplikacji XML-RPC (tak, to była ta trudniejsza część!). Teraz zbudujemy prostego klienta wywołującego zdalnie naszą metodę sayHello(). Korzystamy w tym celu z klasy helma.xmlrpc.XmlRpcClient. Odpowiedzialna jest ona za wiele czynności odbywających się po stronie klienta; jej odpowiednikami po stronie klienta są XmlRpcServer i WebServer. Aby utworzyć klienta, oprócz wspomnianej klasy będziemy jeszcze potrzebowali klasy XmlRpc — klient musi umieć zakodować żądanie, a więc znów musimy ustawić klasę sterownika SAX do wykorzystania w metodzie setDriver(). Zacznijmy tworzenie klienta właśnie od tych ważnych instrukcji, sprawdzając argument przekazywany do metody sayHello() na serwerze oraz wykonując podstawową obsługę wyjątków. Tworzymy plik źródłowy Javy HelloClient.java — taki jak w przykładzie 10.4.
Przykład 10.4. Klient XML-RPC
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
/**
* <b><code>HelloClient</code></b> to prosty klient XML-RPC
* wysyłający żądanie do <code>HelloServer</code>.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class HelloClient {
/**
* <p>
* Łączymy się z serwerem XML-RPC i wysyłamy żądanie.
* </p>
*/
public static void main(String args[]) {
if (args.length < 1) {
System.out.println(
"Użycie: java HelloClient [twoje imie]");
System.exit(-1);
}
try {
// Korzystamy ze sterownika Apache Xerces SAX
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
// Określamy serwer
// Tworzymy żądanie
// Wysyłamy żądanie i drukujemy wynik
} catch (ClassNotFoundException e) {
System.out.println("Nie odnaleziono sterownika SAX");
}
}
}
Tak jak cały kod w tym rozdziale, powyższy przykład nie powinien sprawiać Czytelnikowi żadnych trudności. Zbudowanie klienta XML-RPC wymaga najpierw stworzenia klasy XmlRpcClient, której dostarcza się nazwę serwera XML-RPC. Nazwa tego serwera powinna mieć postać pełnego adresu URL, łącznie z przedrostkiem http://. Jeśli adres ten ma nieodpowiedni format, to w czasie tworzenia klienta zostanie zgłoszony wyjątek java.net.MalformedURLException. Dodajmy wspomnianą klasę do listy klas importowanych, stwórzmy egzemplarz naszego klienta i dodajmy wymaganą procedurę obsługi:
import java.io.IOException;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
/**
* <b><code>HelloClient</code></b> to prosty klient XML-RPC
* wysyłający żądanie do <code>HelloServer</code>.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class HelloClient {
/**
* <p>
* Łączymy się z serwerem XML-RPC i wysyłamy żądanie.
* </p>
*/
public static void main(String args[]) {
if (args.length < 1) {
System.out.println(
"Użycie: java HelloClient [twoje imie]");
System.exit(-1);
}
try {
// Korzystamy ze sterownika Apache Xerces SAX
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
// Określamy serwer
XmlRpcClient client =
new XmlRpcClient("http://localhost:8585/");
// Tworzymy żądanie
// Wysyłamy żądanie i drukujemy wynik
} catch (ClassNotFoundException e) {
System.out.println("Nie odnaleziono sterownika SAX");
} catch (MalformedURLException e) {
System.out.println(
"Niepoprawny format URL serwera XML-RPC: " +
e.getMessage());
}
}
...
Choć faktyczne żądania RPC nie są jeszcze wysyłane, to jest to już w pełni funkcjonalna aplikacja klienta. Można ją skompilować i uruchomić, choć nie dostrzeżemy żadnych objawów jej działania — przecież aż do wysłania żądania nie następuje komunikacja pomiędzy obydwoma komponentami naszego rozwiązania.
|
Oczywiście, w kodzie źródłowym należy wpisać numer portu, na jakim zamierzamy uruchomić serwer. Rzecz jasna, nie jest to rewelacyjny sposób uzyskiwania komunikacji pomiędzy klientem a serwerem; zmiana portu, na którym nasłuchuje serwer, wymaga zmiany kodu klienta! W następnym rozdziale przedstawiony zostanie sposób rozwiązania tego problemu. |
Widać teraz, jak proste jest tworzenie serwera i klienta oraz jak niewiele potrzeba, aby uruchomić mechanizm XML-RPC.
Jednak z naszego programu nie będzie dużego pożytku, dopóki nie zdefiniujemy, jak ma wysyłać żądania i otrzymywać odpowiedzi. Zakodowanie żądania wymaga wywołania na egzemplarzu XmlRpcClient metody execute(). Pobiera ona dwa parametry: nazwę identyfikatora klasy oraz metodę do wywołania (mające postać pojedynczego parametru String) oraz Vector parametrów przekazywanych do określonej metody. Identyfikator klasy to nazwa, jaką zarejestrowaliśmy dla klasy HelloHandler na serwerze XML-RPC. Identyfikator ten może być po prostu nazwą klasy, ale zazwyczaj korzysta się z bardziej czytelnej i zrozumiałej dla klienta nazwy — tak jak u nas „hello”. Dalej dołączana jest nazwa wywoływanej metody, oddzielona od identyfikatora klasy kropką — [identyfikator klasy].[nazwa metody]. Parametry muszą mieć postać wektora Vector Javy i powinny zawierać wszystkie obiekty parametrów wymagane przez określoną metodę. W naszej prostej metodzie sayHello() jest to String zawierający nazwę użytkownika, którą podajemy w wierszu poleceń.
Kiedy już klient XML-RPC zakoduje żądanie, wysyła je do serwera. Serwer odnajduje klasę pasującą do podanego identyfikatora i szuka odpowiedniej metody. Jeśli zostanie ona znaleziona, przyjmowane przez nią parametry porównywane są z tymi z żądania. Jeśli parametry się zgadzają, następuje wykonanie metody. W przypadku znalezienia wielu metod o tej samej nazwie, metodę do wywołania rozpoznaje się po parametrach; jest to zwykły proces w Javie, określany mianem przeładowania (ang. overloading). Wynik działania metody kodowany jest przez serwer XML-RPC i odsyłany klientowi jako Object Javy (który z kolei może być wektorem obiektów!). Wynik ten może być potem rzutowany na odpowiedni typ Javy i zwyczajnie wykorzystywany w kliencie. Jeśli nie zostanie znaleziona pasująca sygnatura identyfikatora lub metody lub parametrów, klientowi zgłasza się wyjątek XmlRpcException. To gwarantuje, że klient nie będzie próbował wywoływać nieistniejących metod lub metod istniejących, ale z niewłaściwymi parametrami.
Cała powyższa procedura zawarta jest w zaledwie kilku wierszach kodu Javy. Przede wszystkim konieczne jest zaimportowanie klasy XmlRpcException oraz java.io.IOException; ten ostatni wyjątek zgłaszany jest w przypadku nieprawidłowej komunikacji pomiędzy klientem a serwerem. Następnie dodajemy klasę Vector i tworzymy jej egzemplarz, podając mu jeden parametr String. To umożliwi wywołanie metody execute() z nazwą procedury obsługi, metodą
do wywołania i jej parametrami; wynik jej działania rzutowany jest na String, ten zaś zostaje wyświetlony na ekranie. W niniejszym przykładzie zakładamy, że serwer XML-RPC działa na komputerze lokalnym, na porcie 8585:
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Vector;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;
...
/**
* <p>
* Łączymy się z serwerem XML-RPC i wysyłamy żądanie.
* </p>
*/
public static void main(String args[]) {
if (args.length < 1) {
System.out.println(
"Użycie: java HelloClient [twoje imie]");
System.exit(-1);
}
try {
// Korzystamy ze sterownika Apache Xerces SAX
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
// Określamy serwer
XmlRpcClient client =
new XmlRpcClient("http://localhost:8585/");
// Tworzymy żądanie
Vector params = new Vector();
params.addElement(args[0]);
// Wysyłamy żądanie i wyświetlamy wynik.
String result =
(String)client.execute("hello.sayHello", params);
System.out.println("Odpowiedź serwera: " + result);
} catch (ClassNotFoundException e) {
System.out.println("Nie odnaleziono sterownika SAX");
} catch (MalformedURLException e) {
System.out.println(
"Niepoprawny format URL serwera XML-RPC: " +
e.getMessage());
} catch (XmlRpcException e) {
System.out.println("Wyjątek XML-RPC: " + e.getMessage());
} catch (IOException e) {
System.out.println("Wyjątek IO: " + e.getMessage());
}
}
...
To już wszystko! Przykład kompilujemy i uruchamiamy zgodnie z poniższym opisem.
Mów do mnie...
Należy sprawdzić, czy w ścieżce dostępu do klas znajdują się klasy XML-RPC i klasy opisywanych wyżej przykładów. Musi się tam znaleźć również Apache Xerces lub inny sterownik SAX
—z jego pomocą programy wykonują przetwarzanie. Kiedy klasy są już dostępne, uruchamiamy serwer z odpowiednim numerem portu. W systemie Windows, aby uruchomić serwer jako oddzielny proces, korzystamy z polecenia start:
D:\prod\Java and XML\WEB-INF\classes>start java HelloServer 8585
Uruchamianie serwera XML-RPC...
Klasa procedury obsługi zarejestrowana jako "hello"
Teraz czekamy na żądania...
W systemach Unix uruchamiamy proces w tle (wstawiając na końcu &), co umożliwi nam uruchomienie na tej samej konsoli również klienta; ewentualnie klienta można uruchomić na innej konsoli, kopiując wcześniej środowisko uruchomieniowe:
$ java HelloServer 8585 &
Uruchamianie serwera XML-RPC...
Klasa procedury obsługi zarejestrowana jako "hello"
Teraz czekamy na żądania...
Teraz uruchamiamy klienta, jako argument wywołania podając własne imię. Wkrótce pojawi się odpowiedź podobna do tej w przykładzie 10.5 — serwer HelloServer otrzymał i obsłużył żądanie oraz zwrócił wynik działania metody sayHello():
Przykład 10.5. Działanie klasy HelloClient
$ java HelloClient Brett
Odpowiedź serwera: Witaj Brett
I tak właśnie działa XML-RPC. Oczywiście, aplikacja taka jak powyższa nie jest zbyt przydatna, ale na pewno dobrze ilustruje podstawy działania i łatwość budowania serwera i klienta XML-RPC w Javie. Po takim wprowadzeniu możemy przejść do bardziej realistycznego przykładu. W dalszej części rozdziału zbudujemy praktyczniejszy i pożyteczniejszy serwer; Czytelnik będzie mógł również zobaczyć, jak wyglądają procedury obsługi XML-RPC. Następnie stworzymy klienta (podobnego do HelloClient) i przetestujemy działanie nowego kodu.
Po co się męczyć? Niech serwer nas wyręczy!
Opisany wyżej przykład Hello dobrze demonstruje sposób korzystania z XML-RPC w Javie, ale nie jest zbyt realistyczny. Kod jest banalny, serwer mało elastyczny, a sama procedura obsługi nie ukazuje praktycznego zastosowania technologii XML-RPC. Poniżej przedstawione zostanie zastosowanie technologii XML-RPC w środowisku produkcyjnym — zbudujemy bardziej użyteczną procedurę obsługi i bardziej praktyczny serwer. Kod taki, choć może wciąż jeszcze nieprzydatny w „prawdziwym” projekcie, na pewno zilustruje, do czego Czytelnik mógłby wykorzystać model XML-RPC w swojej przyszłej pracy.
Współużytkowana procedura obsługi
Utworzona wcześniej klasa HelloHandler była prosta i... bezużyteczna. Trzeba pamiętać, że z technologii XML-RPC w dużej mierze korzysta się po to, aby zrzucić ciężar wykonywania bardziej złożonych zadań na serwer — uproszczony klient ma tylko żądać wywołania procedury i korzystać ze zwróconych wyników. Co więcej, możliwe jest, że część, a nawet wszystkie obliczenia wymagane do zwrócenia odpowiedzi na żądanie można wykonać zawczasu; inaczej mówiąc, klasa procedury obsługi sama wykonuje zadania i kiedy nadchodzi wywołanie metody, wyniki są już gotowe do odesłania. W tej chwili programistom Javy powinny przyjść na myśl wątki i współużytkowane egzemplarze danych. Właśnie te zagadnienia zostaną poniżej zilustrowane za pomocą prostej klasy Scheduler (rozkład zadań).
Do stworzonego rozkładu zadań klienty będą mogły dodawać i usuwać zdarzenia. Będzie również możliwość pobrania z rozkładu listy wszystkich zdarzeń w kolejce. Aby aplikację jeszcze bardziej zbliżyć do rzeczywistości (i żeby serwer miał co robić), nakażemy sortowanie zwróconych zadań według kolejności ich wykonywania. Zdarzenia będą miały po prostu postać łańcucha String, zawierającego nazwę i czas zdarzenia w formacie java.util.Date. Nie jest to może kompletna implementacja rozkładu zadań, ale dobrze demonstruje działanie serwera, wykonuje pewne zadania „za kulisami”.
Po pierwsze, stworzymy metody addEvent() i removeEvent() (odpowiednio: dodaj i usuń zdarzenie). Ponieważ obie te metody są wywoływane przez klienta, nie ma tutaj nic nowego; ciekawe jest natomiast to, jak zachowamy te zadania w klasie Scheduler. Serwer XML-RPC stworzy egzemplarz tej klasy i egzemplarz ten będzie potem wykorzystywany we wszystkich żądaniach przychodzących do serwera; jest jednak możliwe, a nawet prawdopodobne, że z naszym rozkładem zajęć będą komunikowały się inne klasy lub wręcz inne serwery XML-RPC. Jeśli listę zdarzeń będziemy składować jako zmienną przynależną do klasy, nie będzie możliwe współużytkowanie tych danych przez wiele egzemplarzy. Trzeba więc stworzyć dane składowane statycznie i współużytkowane przez wszystkie egzemplarze klasy Scheduler. Do przechowania nazwy i czasu zdarzenia najbardziej odpowiedni wydaje się typ Hashtable, w którym dane przechowywane są w parach klucz-wartość. Oprócz tego nazwy zdarzeń zachowamy w wektorze Vector. Wymaga to wykorzystania dodatkowej pamięci masowej (oraz pamięci w wirtualnej maszynie Javy), ale za to sortować będziemy Vector, a nie Hashtable — zaleta takiego rozwiązania polega na tym, że będziemy zamieniać miejscami pozycje wektora (za każdym razem tylko pojedyncza zamiana), a nie Hashtable (każdorazowo dwie zamiany). Stwórzmy szkielet takiej klasy i dodajmy dwie pierwsze metody, pozwalające dodać lub usunąć zdarzenie. Dodamy także miejsce składowania zdarzeń, ale implementację pobierania i sortowania zdarzeń zostawimy na później. Odpowiedni kod można obejrzeć w przykładzie 10.6.
Przykład 10.6. Klasa rozkładu zadań Scheduler
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;
/**
* <b><code>Scheduler</code></b> to klasa umożliwiająca
* dodawanie, usuwanie i pobieranie posortowanych według czasu
* występowania zadań.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class Scheduler {
/** Lista nazw zdarzeń (do sortowania) */
private static Vector events = null;
/** Szczegóły zdarzeń (nazwa, czas) */
private static Hashtable eventDetails = null;
/**
* <p>
* Tutaj inicjalizujemy miejsce składowania zdarzeń.
* </p>
*/
public Scheduler() {
events = new Vector();
eventDetails = new Hashtable();
}
/**
* <p>
* Tutaj dodajemy żądane zdarzenie.
* </p>
*
* @param eventName <code>String</code> nazwa zdarzenia do dodania.
* @param eventTime <code>Date</code> data zdarzenia.
* @return <code>boolean</code> - wskazuje, czy zdarzenie zostało dodane.
*/
public boolean addEvent(String eventName, Date eventTime) {
// Dodajemy tę listę do listy zdarzeń
if (!events.contains(eventName)) {
events.addElement(eventName);
eventDetails.put(eventName, eventTime);
}
return true;
}
/**
* <p>
* Tutaj usuwamy żądane zdarzenie.
* </p>
*
* @param eventName <code>String</code> nazwa zdarzenia do usunięcia.
* @return <code>boolean</code> - wskazuje, czy usunięto zdarzenie.
*/
public synchronized boolean removeEvent(String eventName) {
events.remove(eventName);
eventDetails.remove(eventName);
return true;
}
}
Metoda addEvent() dodaje nazwę zdarzenia do obu obiektów, w których składujemy dane, a czas zdarzenia — tylko do obiektu Hashtable. Metoda removeEvent() ma działanie przeciwne. Obie zwracają wartość boolean. Choć w przykładzie ta wartość to zawsze true, w bardziej złożonych aplikacjach można ją wykorzystać do informowania o problemach związanych z dodawaniem lub usuwaniem zdarzeń.
Skoro dodawanie i usuwanie zdarzeń jest już możliwe, teraz musimy dodać metodę zwracającą listę zdarzeń. Metoda ta zwraca wszystkie składowane zdarzenia, bez względu na to, który klient (lub aplikacja) to zdarzenie dodał. Mogą więc to być zdarzenia dodane przez innego klienta XML-RPC, inny serwer XML-RPC, inną aplikację albo samodzielną implementację tego samego rozkładu zadań. Ponieważ zwracane dane to pojedynczy Object, można zwrócić Vector sformatowanych wartości String, zawierających nazwę i czas każdego zdarzenia. Oczywiście, w aplikacji bliższej rzeczywistości mógłby zostać zwrócony Vector zdarzeń albo jakiś inny typ danych (z datą w obiekcie Date itd.). Nasza metoda ma jednak umożliwić podgląd danych, a nie wykonywanie na nich dalszych operacji. Do zwrócenia listy zdarzeń wykorzystamy klasę java.text.SimpleDateFormat, umożliwiającą tekstowe formatowanie obiektów Date. Odpowiedni String, zawierający nazwę i czas zdarzenia, tworzony jest w pętli dla każdego zdarzenia; potem dołączany jest do listy wynikowej Vector, która to lista zostanie zwrócona klientowi. Dodajmy więc wymaganą instrukcję importującą oraz kod zwracający zdarzenia:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Hashtable;
import java.util.Vector;
...
/**
* <p>
* Tutaj zwracamy bieżącą listę zdarzeń.
* </p>
*
* @return <code>Vector</code> - lista zdarzeń.
*/
public Vector getListOfEvents() {
Vector list = new Vector();
// Tworzymy format danych.
SimpleDateFormat fmt =
new SimpleDateFormat("hh:mm a MM/dd/yyyy");
// Dodajemy każde zdarzenie do listy
for (int i=0; i<events.size(); i++) {
String eventName = (String)events.elementAt(i);
list.addElement("Zdarzenie \"" + eventName +
"\" zaplanowane na " +
fmt.format(
(Date)eventDetails.get(eventName)));
}
return list;
}
...
Teraz można byłoby już bez żadnych problemów użyć powyższej klasy jako procedury obsługi XML-RPC. Jednak chcemy dowiedzieć się, co należy zrobić, aby serwer wykonywał swoją pracę w czasie, gdy klient robi coś innego. Metoda getListOfEvents() zakłada, że lista zdarzeń (zmienna events typu Vector) jest posortowana — czyli że sortowanie miało już wcześniej miejsce. Nie napisaliśmy jeszcze kodu sortującego, ale — co istotniejsze — nie napisaliśmy również kodu uruchamiającego to sortowanie. Co więcej, w miarę rozrastania się listy zdarzeń sortowanie będzie zajmowało coraz więcej czasu, a przecież klient nie może czekać. Najpierw dodamy metodę, za pomocą której nasza klasa posortuje zdarzenia. Dla uproszczenia zastosujemy technikę sortowania pęcherzykowego; omawianie algorytmów sortowania wykracza poza tematykę niniejszej książki, a więc nie będziemy szczegółowo opisywali akurat tego fragmentu kodu. W każdym razie na końcu metody zmienna Vector events zawiera już zdarzenia posortowane według zaplanowanych dat. Więcej informacji o tym i o innych algorytmach sortowania można znaleźć w książce Algorithms in Java Roberta Sedgewicka i Tima Lindholma (Addison-Wesley). W następujący sposób dodamy algorytm i metodę sortującą zdarzenia:
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
/**
* <b><code>Scheduler</code></b> to klasa umożliwiająca
* dodawanie, usuwanie i pobieranie posortowanych według czasu
* występowania zadań.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class Scheduler {
/** Lista nazw zdarzeń (do sortowania) */
private static Vector events = null;
/** Szczegóły zdarzeń (nazwa, czas) */
private static Hashtable eventDetails = null;
/** Znacznik określający, czy zdarzenia są posortowane */
private static boolean eventsSorted;
// Implementacje innych metod
/**
* <p>
* Sortujemy zdarzenia na bieżącej liście.
* <p>
*/
private synchronized void sortEvents() {
if (eventsSorted) {
return;
}
// Tworzymy tablicę zdarzeń "tak jak są" (nieposortowanych)
String[] eventNames = new String[events.size()];
events.copyInto(eventNames);
// Sortowanie pęcherzykowe
String tmpName;
Date date1, date2;
for (int i=0; i<eventNames.length - 1; i++) {
for (int j=0; j<eventNames.length - i - 1; j++) {
// Porównanie dat zdarzeń
date1 = (Date)eventDetails.get(eventNames[j]);
date2 = (Date)eventDetails.get(eventNames[j+1]);
if (date1.compareTo(date2) > 0) {
// W razie potrzeby -- zamiana zdarzeń
tmpName = eventNames[j];
eventNames[j] = eventNames[j+1];
eventNames[j+1] = tmpName;
}
}
}
// Wstawienie posortowanych zdarzeń do nowego wektora
Vector sortedEvents = new Vector();
for (int i=0; i<eventNames.length; i++) {
sortedEvents.addElement(eventNames[i]);
}
// Aktualizacja zdarzeń globalnych
events = sortedEvents;
eventsSorted = true;
}
...
}
Poza wstawieniem samego algorytmu trzeba było jeszcze zaimportować klasę java.util.Enumeration oraz dodać zmienną boolean o nazwie eventsSorted. Znacznik ten umożliwia obejście całej procedury sortowania, jeśli zdarzenia są już posortowane. Nie ma jeszcze kodu, który wpływałby na stan tego znacznika, ale jego dodanie nie powinno sprawić kłopotów. Nasza metoda sortująca już teraz potrafi wskazać, czy po ukończeniu zdarzenia są posortowane. Konstruktor powinien więc na początku ustawić tę wartość na true, co oznacza, że zdarzenia są wstępnie posortowane. Dopiero po dodaniu nowych zdarzeń ten stan może się zmienić na „nieposortowane”, a więc w metodzie addEvents() ustawiamy znacznik na false. Dzięki temu klasa Scheduler będzie „wiedzieć”, że sortowanie powinno zostać uruchomione. Kiedy więc wywołana zostanie metoda getListOfEvents(), zdarzenia będą posortowane i gotowe do zwrócenia. Dodajmy odpowiedni kod do naszego konstruktora:
/**
* <p>
* Tutaj inicjalizujemy miejsce składowania zdarzeń.
* </p>
*/
public Scheduler() {
events = new Vector();
eventDetails = new Hashtable();
eventsSorted = true;
}
/**
* <p>
* Tutaj dodajemy żądane zdarzenie.
* </p>
*
* @param eventName <code>String</code> nazwa zdarzenia do dodania.
* @param eventTime <code>Date</code> data zdarzenia.
* @return <code>boolean</code> - wskazuje, czy zdarzenie zostało dodane.
*/
public boolean addEvent(String eventName, Date eventTime) {
// Dodajemy tę listę do listy zdarzeń
if (!events.contains(eventName)) {
events.addElement(eventName);
eventDetails.put(eventName, eventTime);
eventsSorted = false;
}
return true;
}
W metodzie removeEvent() nie trzeba wprowadzać żadnych zmian — usunięcie wpisu nie zmienia przecież kolejności zdarzeń. Idealnym rozwiązaniem przetwarzania po stronie serwera bez angażowania klienta jest stworzenie wątku sortującego. Po uruchomieniu takiego wątku w wirtualnej maszynie Javy przetwarzanie po stronie klienta może być kontynuowane bez konieczności czekania na zakończenie procesu po stronie serwera. Jest to szczególnie istotne w środowiskach wielowątkowych, gdzie wykorzystuje się synchronizację i wątki oczekujące na zwolnienie blokad obiektów. W tym przykładzie staramy się uniknąć tego typu zagadnień (to rozdział o XML-RPC, a nie o wątkach), ale dodanie odpowiedniego kodu nie stanowiłoby dużego problemu. W naszym programie stworzymy wewnętrzną klasę Thread, która nie robi nic poza wywołaniem metody sortEvents(). Następnie dodamy metodę addEvents(), która tworzy i uruchamia ten wątek po dodaniu zdarzenia. Tak więc dodanie zdarzenia powoduje ponowne posortowanie zdarzeń, ale bez zatrzymywania pracy klienta (czyli np. dodawania kolejnych zdarzeń, a więc uruchamiania kolejnych wątków itd.). Kiedy klient zażąda listy zdarzeń, powinny one być już posortowane, tak aby klient nie musiał oczekiwać albo wykonywać tej czynności samodzielnie. Dodanie wewnętrznej klasy sortującej oraz kodu uruchamiającego tę klasę w wątku do metody addEvents() to już ostatnie zmiany, jakie wprowadzimy w klasie Scheduler:
public class Scheduler {
...
public boolean addEvent(String eventName, Date eventTime) {
// Dodajemy tę listę do listy zdarzeń
if (!events.contains(eventName)) {
events.addElement(eventName);
eventDetails.put(eventName, eventTime);
eventsSorted = false;
// Uruchamiamy na serwerze wątek sortujący
SortEventsThread sorter = new SortEventsThread();
sorter.start();
}
return true;
}
...
/**
* <p>
* Klasa wewnętrzna uruchamiająca sortowanie jako wątek
* <code>Thread</code>.
*/
class SortEventsThread extends Thread {
/**
* <p>
* Uruchamianie sortowania.
* </p>
*/
public void run() {
sortEvents();
}
}
}
Po skompilowaniu zmodyfikowanego kodu powstaje wątkowy rozkład zadań, wykonujący „zasobożerne” zadanie sortowania po stronie serwera, nie przerywając w tym celu pracy klientów. Jest
to prosty przykład klasy procedury obsługi, ale dobrze ilustruje proces rozpraszania zasobów i sposób przekładania ciężaru obsługi programu na serwer. Bardziej zaawansowana klasa procedury obsługi jest już gotowa. Teraz jeszcze usprawnimy nieco sam serwer.
Serwer z możliwością konfiguracji
Klasa serwera XML-RPC wymaga zmian. Obecna wersja wymaga wpisywania klas obsługi w kod — a więc również rekompilacji. To rozwiązanie niepożądane, niewygodne i czasochłonne — konieczne jest uzyskiwanie najświeższego kodu z systemu kontroli plików źródłowych, wprowadzanie zmian i testowanie procedur obsługi. Lepiej byłoby, gdyby serwer potrafił odczytywać tego typu informacje z pliku konfiguracyjnego i ładować odpowiednie klasy dynamicznie. Spróbujmy teraz stworzyć taki „lekki” serwer.
Zaczniemy od zbudowania nowej klasy serwera. Możemy zacząć od podstaw albo skopiować kod z klasy HelloServer opisywanej we wcześniejszej części rozdziału. Rozpoczniemy od stworzenia struktury i po kolei dodamy wymagane instrukcje importujące i inne — podobnie jak robiliśmy to wcześniej. Tym razem jednak nie dodamy kodu rejestrującego procedury obsługi; posłużymy się metodą pomocniczą, ładującą wymagane informacje z pliku. Będzie więc trzeba również obsłużyć dodatkowy parametr wiersza poleceń, określający nazwę pliku. Stworzymy klasę LightweightXmlRpcServer (część pakietu narzędziowego com.oreilly.xml), która cały czas korzysta z niewielkiej klasy pomocniczej WebServer. Jej kod przedstawiony jest w przykładzie 10.7. Pełny pakiet com.oreilly.xml można również pobrać ze stron http://www.oreilly.com/ catalog/javaxml/ lub http://www.newInstance.com.
Przykład 10.7. Klasa LightweightXmlRpcServer
package com.oreilly.xml;
import java.io.IOException;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.WebServer;
/**
* <b><code>LightweightXmlRpcServer</code></b> to klasa narzędziowa
* uruchamiająca serwer XML-RPC nasłuchujący żądań HTTP
* i rejestrująca procedury obsługi zdefiniowane w pliku konfiguracyjnym.
*
* @author Brett McLaughlin
* @version 1.0
*/
public class LightweightXmlRpcServer {
/** Klasa narzędziowa serwera XML-RPC */
private WebServer server;
/** Port, na którym nasłuchujemy */
private int port;
/** Plik konfiguracyjny */
private String configFile;
/**
* <p>
* Tutaj będziemy przechowywać nr portu i plik konfiguracyjny
* wykorzystywane przez serwer.
* </p>
*
* @param port <code>int</code> port, na którym nasłuchujemy
* @param configFile <code>String</code> nazwa pliku do
* pobrania informacji o konfiguracji.
*/
public LightweightXmlRpcServer(int port, String configFile) {
this.port = port;
this.configFile = configFile;
}
/**
* <p>
* Uruchomienie serwera.
* </p>
*
* @throws <code>IOException</code> zgłaszany w razie problemów.
*/
public void start() throws IOException {
try {
// Korzystamy z parsera SAX Apache Xerces
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
System.out.println("Uruchamianie serwera XML-RPC...");
server = new WebServer(port);
// Rejestracja procedur obsługi.
} catch (ClassNotFoundException e) {
throw new IOException("Błąd ładowania parsera SAX: " +
e.getMessage());
}
}
/**
* <p>
* Statyczny punkt rozpoczęcia programu.
* </p>
*/
public static void main(String[] args) {
if (args.length < 2) {
System.out.println(
"Użycie: " +
"java com.oreilly.xml.LightweightXmlRpcServer " +
"[port] [plikKonfiguracyjny]");
System.exit(-1);
}
LightweightXmlRpcServer server =
new LightweightXmlRpcServer(Integer.parseInt(args[0]),
args[1]);
try {
// Uruchomienie serwera.
server.start();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
W powyższym kodzie nie ma nic nadzwyczajnego. Sprawdzamy, czy przekazywane są odpowiednie parametry, i uruchamiamy serwer na żądanym porcie. Teraz trzeba dodać metody ładujące procedury obsługi z pliku, a następnie dodające procedury jedna po drugiej do serwera.
Ponieważ każda procedura obsługi ma postać nazwy i odpowiadającej jej klasy, plik konfiguracyjny oparty będzie właśnie na takich parach informacji. W przypadku Javy załadowanie i stworzenie egzemplarza klasy, której pełna nazwa jest nam znana, nie przedstawia żadnych trudności — tak więc nową procedurę obsługi opiszemy za pomocą prostej pary łańcuchów tekstowych. W pliku konfiguracyjnym dodamy zarówno naszą wcześniejszą klasę HelloHandler, jak i nową — Scheduler. Ponieważ piszemy również parser pliku, decydujemy, że ogranicznikami wpisów będą przecinki, a znak <CODE># oznaczać będzie komentarz. Możliwe jest zresztą zastosowanie dowolnej konwencji, o ile tylko dostosujemy odpowiednio kod. Tworzymy teraz plik konfiguracyjny taki jak ten w przykładzie 10.8. W wyniku jego odczytania do serwera zostaną dodane klasy HelloHandler (pod nazwą „hello”) oraz Scheduler (pod nazwą „scheduler”). Plik zachowujemy pod nazwą xmlrpc.conf.
Przykład 10.8. Plik konfiguracyjny XML-RPC
# Procedura obsługi hello: sayHello()
hello,HelloHandler
# Scheduler: addEvent(), removeEvent(), getEvents()
scheduler,Scheduler
Na potrzeby dokumentacji w komentarzach podajemy metody dostępne dla każdej procedury obsługi. Osobom zajmującym się naszym kodem w przyszłości taki opis na pewno się przyda.
Ładowanie pliku i odczytanie jego zawartości jest — dzięki klasom wejścia-wyjścia Javy — bardzo proste. Tworzymy metodę pomocniczą odczytującą określony plik i składującą pary wartości w strukturze Hashtable. Obiekt ten może zostać następnie przekazany innej metodzie pomocniczej, ładującej i rejestrującej poszczególne procedury obsługi. W tej przykładowej metodzie nie sprawdzamy zbyt intensywnie, czy nie wystąpiły błędy, a każda linijka nie będąca parą wartości jest po prostu ignorowana; w serwerze produkcyjnym prawdopodobnie posłużylibyśmy się bardziej zaawansowanym kodem (zresztą dodanie kodu sprawdzającego błędy byłoby naprawdę proste). Po odnalezieniu wiersza zawierającego parę wartości jest on rozbijany na części, a identyfikator i nazwa klasy zostają zachowane w strukturze Hashtable. Dodamy teraz instrukcje importujące oraz nową metodę getHandlers():
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Hashtable;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.WebServer;
...
/**
* <p>
* Metoda przetwarzająca plik konfiguracyjny
* (w minimalistyczny sposób) i odczytująca dostarczone
* definicje procedur obsługi.
* </p>
*
* @return <code>Hashtable</code> - pary identyfikator/klasa.
* @throws <code>IOException</code> - zgłaszany w razie wystąpienia
* błędu przy przetwarzaniu/odczycie pliku.
*/
private Hashtable getHandlers() throws IOException {
Hashtable handlers = new Hashtable();
BufferedReader reader =
new BufferedReader(new FileReader(configFile));
String line = null;
while ((line = reader.readLine()) != null) {
// Składnia to "nazwaProceduryObslugi, klasaProceduryObslugi"
int comma;
// Pomijamy komentarze
if (line.startsWith("#")) {
continue;
}
// Pomijamy wiersze puste lub bezużyteczne
if ((comma = line.indexOf(",")) < 2) {
continue;
}
// Dodajemy nazwę i klasę procedury obsługi
handlers.put(line.substring(0, comma),
line.substring(comma+1));
}
return handlers;
}
...
Zamiast dodawać kod zachowujący wynik działania takiej metody, wyniku tego użyjemy w funkcji danych wejściowych metody przetwarzającej Hashtable i dodającej poszczególne procedury obsługi do serwera. Kod wykonujący to zadanie nie jest skomplikowany; warto jedynie zaznaczyć, że metoda addHandler() klasy WebServer wymaga podania jako parametru egzemplarza klasy. Trzeba więc pobrać nazwę klasy do zarejestrowania ze struktury Hashtable, załadować tę klasę do wirtualnej maszyny Javy za pomocą Class.forName() i stworzyć jej egzemplarz za pomocą newInstance(). Właśnie w ten sposób postępuje się w programach ładujących klasy i innych aplikacjach dynamicznych w Javie; Czytelnik może nie znać tej metody, jeśli dopiero zaczyna przygodę z tym językiem lub nie musiał tworzyć dynamicznie egzemplarzy klas z nazwy tekstowej. Po załadowaniu klasy w ten sposób, zarówno ona, jak i jej identyfikator przekazywane są do metody addHandler() i pętla jest kontynuowana. Po załadowaniu całej zawartości Hashtable serwer jest gotowy do działania. Do „przechodzenia” po kluczach Hashtable używamy klasy Enumeration, a więc trzeba dodać odpowiednią instrukcję importującą:
package com.oreilly.xml;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Hashtable;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.WebServer;
...
/**
* <p>
* Tutaj zarejestrujemy procedury dostarczone do serwera
* (zazwyczaj z <code>{@link #getHandlers()}</code>.
* </p>
*
* @param handlers <code>Hashtable</code> procedur do zarejestrowania
*/
private void registerHandlers(Hashtable handlers) {
Enumeration handlerNames = handlers.keys();
// Przetwarzanie procedur obsługi
while (handlerNames.hasMoreElements()) {
String handlerName = (String)handlerNames.nextElement();
String handlerClass = (String)handlers.get(handlerName);
// Tę procedurę obsługi dodajemy do serwera
try {
server.addHandler(handlerName,
Class.forName(handlerClass).newInstance());
System.out.println("Zarejestrowana w serwerze nazwa " + handlerName +
" odpowiada klasie " + handlerClass);
} catch (Exception e) {
System.out.println("Nie jest możliwe zarejestrowanie procedury " +
handlerName + " odpowiadającej klasie " +
handlerClass);
}
}
}
...
Mamy tu do czynienia z uzupełnieniem wcześniejszej metody getHandlers(); faktycznie jako wejście pobierany jest właśnie jej wynik działania. Wartości String z klasy Hashtable są pobierane i rejestrowane. Serwer załaduje i udostępni do zdalnych wywołań wszystkie procedury obsługi podane w pliku konfiguracyjnym. Należy pamiętać, że metody te można byłoby równie dobrze skonsolidować do postaci jednej, większej metody. Jednakże obie opisywane metody mają różne cele: getHandlers() przetwarza plik, a registerHandlers() rejestruje procedury obsługi, gdy tylko informacje o nich zostaną udostępnione. To wszystko umożliwia prostą zmianę sposobu odczytywania pliku konfiguracyjnego (można nawet sprawić, że informacje będą odczytywane z bazy danych), bez konieczności pamiętania o samym sposobie rejestrowania procedur obsługi. W następnym rozdziale pójdziemy dalej — usuniemy metodę getHandlers(), zastępując ją klasą pomocniczą odczytującą te informacje z pliku XML! W tym przypadku podjęcie właściwej decyzji odpowiednio wcześnie (a więc w tej chwili) oszczędzi nam potem wiele pracy związanej ze zmienianiem kodu (w następnym rozdziale).
Po dodaniu tych dwóch metod pomocniczych dodajemy ich wywołanie do metody start() klasy serwera.
/**
* <p>
* Uruchomienie serwera.
* </p>
*
* @throws <code>IOException</code> zgłaszany w razie problemów.
*/
public void start() throws IOException {
try {
// Korzystamy z parsera SAX Apache Xerces
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
System.out.println("Uruchamianie serwera XML-RPC...");
server = new WebServer(port);
// Rejestracja procedur obsługi.
registerHandlers(getHandlers());
} catch (ClassNotFoundException e) {
throw new IOException("Błąd ładowania parsera SAX: " +
e.getMessage());
}
}
Wokół dodanych metod budujemy blok try-catch, dzięki czemu będziemy potrafili odróżnić wyjątki występujące w samym serwerze (blok zewnętrzny) od tych związanych z budowaniem procedur obsługi. W tym ostatnim przypadku zgłaszamy błąd jako komunikat generowany przez metody procedur obsługi. Kompilujemy kod, upewniamy się, że plik konfiguracyjny jest na miejscu, i otrzymujemy serwer gotowy do użycia.
Użyteczny klient
W kliencie nie zastosujemy żadnych nowych koncepcji czy technik; klasa SchedulerClient będzie równie prosta jak HelloClient. Będzie uruchamiała klienta XML-RPC, wywoływała metody procedur obsługi i wyświetlała wynik ich działania. Cały kod znajduje się poniżej. O tym, co się dzieje w programie, informują komentarze; ponieważ tę stronę aplikacji już omówiliśmy, wystarczy teraz tylko skompilować kod podany w przykładzie 10.9.
Przykład 10.9. Klasa SchedulerClient
import java.io.IOException;
import java.net.MalformedURLException;
import java.util.Calendar;
import java.util.Date;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;
import helma.xmlrpc.XmlRpc;
import helma.xmlrpc.XmlRpcClient;
import helma.xmlrpc.XmlRpcException;
/**
* <b><code>SchedulerClient</code></b> to klient XML-RPC wysyłający
* żądania XML-RPC do programu <code>Scheduler</code>.
*
* @version 1.0
*/
public class SchedulerClient {
/**
* <p>
* Dodajemy zdarzenia do programu Scheduler.
* </p>
*
* @param <code>XmlRpcClient</code> - klient, z którym się łączymy
*/
public static void addEvents(XmlRpcClient client)
throws XmlRpcException, IOException {
System.out.println("\nDodawanie zdarzeń...\n");
// Parametry zdarzeń.
Vector params = new Vector();
// Zdarzenie na następny miesiąc.
params.addElement("Zweryfikować ostateczną wersję roboczą".);
Calendar cal = Calendar.getInstance();
cal.add(Calendar.MONTH, 1);
params.addElement(cal.getTime());
// Dodanie zdarzenia.
if (((Boolean)client.execute("scheduler.addEvent", params))
.booleanValue()) {
System.out.println("Zdarzenie zostało dodane.");
} else {
System.out.println("Zdarzenie nie mogło zostać dodane.");
}
// Zdarzenie na jutro.
params.clear();
params.addElement("Wysłać ostateczną wersję roboczą");
cal = Calendar.getInstance();
cal.add(Calendar.DAY_OF_MONTH, 1);
params.addElement(cal.getTime());
// Dodanie zdarzenia.
if (((Boolean)client.execute("scheduler.addEvent", params))
.booleanValue()) {
System.out.println("Zdarzenie zostało dodane.");
} else {
System.out.println("Zdarzenie nie mogło zostać dodane.");
}
}
/**
* <p>
* Wyświetlenie zdarzeń obecnych w programie Scheduler.
* </p>
*
* @param <code>XmlRpcClient</code> - klient, z którym się łączymy.
*/
public static void listEvents(XmlRpcClient client)
throws XmlRpcException, IOException {
System.out.println("\nWyświetlanie zdarzeń...\n");
// Pobieranie zdarzeń z planu zadań.
Vector params = new Vector();
Vector events =
(Vector)client.execute("scheduler.getListOfEvents", params);
for (int i=0; i<events.size(); i++) {
System.out.println((String)events.elementAt(i));
}
}
/**
* <p>
* Statyczny punkt wyjścia programu.
* </p>
*/
public static void main(String args[]) {
try {
// Korzystamy z implementacji parsera SAX Apache Xerces.
XmlRpc.setDriver("org.apache.xerces.parsers.SAXParser");
// Łączenie z serwerem.
XmlRpcClient client =
new XmlRpcClient("http://localhost:8585/");
// Dodanie zdarzeń.
addEvents(client);
// Wyświetlenie zdarzeń
listEvents(client);
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
Warto zauważyć, że dodawanie zdarzeń następuje w kolejności odwrotnej do kolejności ich występowania. Nasz serwer sam zajmie się ich posortowaniem (metoda sortEvents()) i gdy wywołamy metodę getListOfEvents(), zdarzenia zostaną zwrócone we właściwej kolejności.
Mów do mnie jeszcze...
Po wpisaniu kodu procedury obsługi serwera i klienta wszystkie pliki źródłowe należy skompilować. Trzeba również stworzyć plik konfiguracyjny, w którym umieszczone będą procedury obsługi do zarejestrowania w serwerze XML-RPC. Następnie najpierw uruchamiamy serwer XML-RPC jako oddzielny proces:
D:\prod>start java com.oreilly.xml.LightweightXmlRpcServer 8585 D:\prod\conf\xmlrpc.conf
...czego odpowiednikiem w systemach Unix jest:
[adam@maly ch10]$ java com.oreilly.xml.LightweightXmlRpcServer 8585 conf/xmlrpc.conf &
Serwer powinien wyświetlić procedury obsługi odczytane z pliku konfiguracyjnego oraz odpowiadające im nazwy:
Uruchamianie serwera XML-RPC...
Zarejestrowana w serwerze nazwa scheduler odpowiada klasie Scheduler.
Zarejestrowana w serwerze nazwa hello odpowiada klasie HelloHandler.
|
Jeśli wcześniej nie zatrzymaliśmy utworzonego wcześniej serwera XML-RPC, HelloServer, otrzymamy komunikat o błędzie wynikający z próby uruchomienia kolejnego serwera na tym samym porcie. Przed uruchomieniem obecnego serwera należy zatrzymać poprzedni. |
Teraz uruchamiamy klienta i obserwujemy wyniki:
[adam@maly ch10]$ java SchedulerClient
Dodawanie zdarzeń...
Zdarzenie zostało dodane.
Zdarzenie zostało dodane.
Wyświetlanie zdarzeń...
Zdarzenie "Wysłać ostateczną wersję roboczą" zaplanowane na 03:42 PM 01/20/2001
Zdarzenie "Zweryfikować ostateczną wersję roboczą" zaplanowane na 03:42 PM 02/19/2001
W czasie dodawania i wyświetlania zdarzeń nie powinniśmy odczuć znacznego opóźnienia, a mimo to zdarzenia są sortowane przez serwer. Odbywa się to po prostu w oddzielnym wątku wirtualnej maszyny Javy (a trzeba wiedzieć, że sortowanie pęcherzykowe nie należy do szybkich algorytmów!). I tak właśnie napisaliśmy pierwszą użyteczną aplikację XML-RPC.
Praktyczne zastosowania technologii XML-RPC
W zakończeniu rozdziału zostaną omówione zagadnienia zwiazane z wykorzystywaniem XML-RPC w rzeczywistych zastosowaniach. Czytelnik po raz kolejny będzie mógł się przekonać, że XML używany jest nie dlatego, że to taka nowoczesna i modna technologia, ale dlatego, że w niektórych sytuacjach po prostu nie ma sobie równych. Cała wiedza zawarta w tej książce, specyfikacje otaczające XML i inne książki opisujące ten standard — wszystko to na nic się nie zda, jeśli programista nie będzie wiedział, jak korzystać z języka XML oraz modelu XML-RPC poprawnie! W niniejszej części zostaną przedstawione typowe problemy trapiące programistów XML-RPC.
Gdzie właściwie jest ten XML?
Po lekturze dotychczasowej części rozdziału Czytelnik może zastanawiać się, dlaczego w tworzonym kodzie nie zostały użyte interfejsy SAX, DOM lub JDOM. Co więcej, kod prawie wcale nie bazował bezpośrednio na XML-u. Wynika to stąd, że kodowanie i dekodowanie żądań przesyłanych pomiedzy klientem a serwerem wykonywane było przez biblioteki XML-RPC. Choć XML nie został użyty bezpośrednio, to jednak całe to rozwiązanie oparte jest właśnie na technologii XML. Proste żądanie wysyłane do metody sayHello() zostało tak naprawdę przetłumaczone na wywołanie HTTP o postaci przedstawionej w przykładzie 10.10.
Przykład 10.10. Żądanie XML-RPC po rozkodowaniu
POST /RPC2 HTTP/1.1
User-Agent: Tomcat Web Server/3.1 Beta (Sun Solaris 2.6)
Host: newInstance.com
Content-Type: text/xml
Content-length: 234
<?xml version="1.0"?>
<methodCall>
<methodName>hello.sayHello</methodName>
<params>
<param>
<value><string>Brett</string></value>
</param>
</params>
</methodCall>
Biblioteki XML-RPC po stronie serwera otrzymują takie żądanie i rozkodowują je, dopasowując odpowiednią metodę procedury obsługi (o ile taka istnieje). Następnie żądana metoda Javy jest uruchamiana, a serwer ponownie koduje wynik do postaci XML, tak jak to pokazano w przykładzie 10.11.
Przykład 10.11. Odpowiedź XML-RPC po zakodowaniu
HTTP/1.1 200 OK
Connection: close
Content-Type: text/xml
Content-length: 149
Date: Mon, 11 Apr 2000 03:32:19 CST
Server: Tomcat Web Server/3.1 Beta-Sun Solaris 2.6
<?xml version="1.0"?>
<methodResponse>
<params>
<param>
<value><string>Hello Brett</string></value>
</param>
</params>
</methodResponse>
Komunikacja trwa, chociaż użytkownik wcale nie musiał zaprzątać sobie głowy szczegółami.
Egzemplarze współużytkowane
Przedstawione przykłady zobrazowały sposoby wykorzystania statycznych obiektów do współużytkowania danych przez wiele egzemplarzy tej samej klasy. Jednakże istnieją także sytuacje, w których współużytkowany jest sam egzemplarz. Nie musi to wynikać z zastosowania modelu XML-RPC, ale z konieczności różnego użycia klasy po stronie serwera. Na przykład Java dopuszcza tworzenie tylko jednego egzemplarza danej klasy i egzemplarz ten współużytkowany jest przez wszystkie aplikacje. Zazwyczaj uzyskuje się to za pomocą statycznej metody getInstance(), a nie poprzez konstruowanie obiektu:
Scheduler scheduler;
// Pobieramy jeden egzemplarz, obsługiwany przez klasę Scheduler
scheduler = Scheduler.getInstance()
// Dodajemy zdarzenie ważne w tej chwili
scheduler.addEvent("Piknik", new Date());
Aby zagwarantować, że żadne klasy nie stworzą bezpośrednio egzemplarza klasy Scheduler, konstruktor zazwyczaj otrzymuje status prywatnego lub chronionego. Powoduje to, że do stworzenia egzemplarza klienty muszą użyć przedstawionego wyżej kodu; jednak rozwiązanie takie pociąga za sobą trudności z użyciem takiej klasy jako procedury obsługi XML-RPC. Czytelnik pamięta, że zarejestrowanie procedury obsługi odbywało się poprzez stworzenie egzemplarza jej klasy. Jednak klasa WebServer wymaga jako parametru poprawnego egzemplarza, ale niekoniecznie nowego egzemplarza. Na przykład zupełnie poprawnym sposobem dodania procedury obsługi jest zastosowanie poniższego kodu:
WebServer server = new WebServer(8585);
// Tworzymy nową klasę procedury obsługi.
HelloHandler hello = new HelloHandler();
server.addHandler("hello", hello);
Klasa serwera nie rozróżnia powyższych sposobów, o ile tylko egzemplarz klasy procedury obsługi stworzono przed przekazaniem jej metodzie addHandler(). Tak więc, jeśli chcemy dodać egzemplarz do opisanej wyżej jednorodnej klasy Scheduler, kod musimy nieco zmienić:
WebServer server = new WebServer(8585);
// Przekazujemy jednorodny egzemplarz
server.addHandler("scheduler", Scheduler.getInstance());
Przekazywany jest egzemplarz współużytkowany — wszystko odbywa się tak, jakby egzemplarz klasy został utworzony poprzez konstruktor za pomocą słowa kluczowego new; wszystkie informacje współużytkowane w klasie jednorodnej zostają zachowane. Czytelnik będzie mógł się przekonać, że wiele klas wykorzystywanych w usługach typu XML-RPC tworzonych jest jako klasy jednorodne, co zapobiega użyciu statycznych danych. Zastosowanie egzemplarza współużytkowanego umożliwia przechowywanie danych w zmiennych przynależnych, a więc ten jeden egzemplarz operuje na tych zmiennych w celu obsłużenia wszystkich żądań klienta.
Z serwletem czy bez?
Ostatnio jako serwer XML-RPC często wykorzystywany jest program działający jako serwlet (więcej informacji o serwletach można znaleźć w książce Jasona Huntera Java Servlet Programming, wyd. O'Reilly & Associates). Nawet w dystrybucji XML-RPC dla Javy znajduje się serwlet. Użycie serwleta w ten sposób jest zupełnie poprawne i często spotykane — serwlet zajmuje się wtedy wyłącznie obsługą żądań XML-RPC, jednakże nie zawsze jest to rozwiązanie najlepsze.
Jeśli programista ma komputer, który musi obsłużyć inne żądania HTTP związane z wykonywaniem zadań w Javie, wtedy na pewno użycie mechanizmu serwletów do obsługi takich żądań mija się z celem. W takim przypadku użycie serwleta jako serwera XML-RPC jest dobrym rozwiązaniem. Jednak jedną z zalet XML-RPC jest możliwość odseparowania złożonych, „zasobożernych” zadań klas procedur obsługi od pozostałego kodu aplikacji. Nasza klasa Scheduler mogłaby zostać umieszczona na serwerze wraz z klasami wykonującymi złożone indeksowanie, modelowanie algorytmiczne czy transformacje graficzne. Wszystkie te funkcje bardzo obniżają wydajność klientów. Jednak dodanie mechanizmu serwletów i przyjmowanie żądań aplikacji dotyczących innych zadań oraz obsługi XML-RPC zmniejsza moc przetwarzania dostępną dla klas procedur obsługi. W takim przypadku jedyne żądania przychodzące do serwera powinny dotyczyć właśnie klas procedur obsługi.
W przypadku, gdy przyjmowane są tylko żądania XML-RPC (jak wykazano powyżej), zastosowanie serwleta jako serwera XML-RPC rzadko zdaje egzamin. Dostępna klasa WebServer jest bardzo mała, „lekka” i opracowana właśnie do obsługi XML-RPC poprzez HTTP. Mechanizm serwletów obsługuje dowolne żądania HTTP i nie jest przystosowany do żądań XML-RPC. Z biegiem czasu zacznie być zauważane obniżenie wydajności związane z użyciem mechanizmu serwletów zamiast klasy WebServer. Jeśli więc serwer nie będzie obsługiwał innych zadań niż związane z XML-RPC, lepiej pozostać przy „lekkim” serwerze XML-RPC, opracowanym właśnie do takich zastosowań.
Co dalej?
Teraz Czytelnik powinien już potrafić stosować XML na różne sposoby, tak jak to zostało przedstawione w rozdziałach „tematycznych”. W następnym rozdziale kontynuowane będzie omawianie praktycznych zastosowań XML-a. Tym razem skoncentrujemy się na wykorzystaniu tego języka w konfiguracji. Języka XML używa się w plikach wdrożeniowych Enterprise JavaBeans, wielu serwerów i coraz większej liczby aplikacji. W kolejnym rozdziale zostaną omówione przyczyny tej ekspansji XML-a; zostaną również przedstawione sposoby zastosowania tego języka do wyłącznych celów aplikacji, a nie tylko do przesyłania danych pomiędzy aplikacjami. Czytelnik dowie się, dlaczego w najbliższej przyszłości nie grozi nam likwidacja baz danych i serwerów usług katalogowych, dowie się również, gdzie warto stosować XML, a gdzie zastosowanie tego języka nie jest uzasadnione.
Oczywiście, typy struct i array muszą zawierać tylko inne dozwolone typy XML-RPC.
Obecnie nie istnieją biblioteki XML-RPC obsługujące SAX 2.0 i implementujące interfejs XMLReader. Oczekuje się, że wkrótce ten stan rzeczy ulegnie zmianie; ponieważ klasa Apache Xerces SAXParser implementuje zarówno stary, jak i nowy interfejs, po aktualizacji XML-RPC do SAX-a 2.0 nie trzeba będzie wprowadzać w kodzie żadnych zmian. Jeśli jednak korzystamy z parsera innego producenta, może się okazać konieczne jawne określenie klasy SAX 2.0.
Jeśli Czytelnik korzysta z systemu Unix i chce uruchomić usługę na porcie niższym niż 1024, musi być zalogowany jako root. Aby uniknąć problemu, warto skorzystać z wyższego portu, tak jak to pokazano na przykładzie.
270 Rozdział 10. XML-RPC
Co dalej? 271
C:\Helion\Java i XML\jAVA I xml\10-08.doc — strona 270
C:\Helion\Java i XML\jAVA I xml\10-08.doc — strona 271
C:\Helion\Java i XML\jAVA I xml\10-08.doc — strona 239