sobota, 2 października 2010

Mechanizm RMI (Remote Method Invocation) w praktyce cz. 1

Dzisiaj chciałbym Wam przedstawić mechanizm wywoływania zdalnych metod (ang. Remote Method Invocation, RMI). Spotkałem się z nim podczas studiów na jednym z przedmiotów. Pamiętam, że znajomi męczyli się bardzo, aby napisać prostą aplikację wykorzystującą RMI. Narzekali przy tym, że to niewykorzystywane jest, że są nowsze, lepsze metody realizacji podobnej funkcjonalności, że .NET itd. Jako że zaczął się niedawno rok akademicki mam nadzieję, że moje wywody pomogą niektórym w ogarnięciu tematu.

No dobra, ale przejdźmy do rzeczy. Mechanizm RMI dodano do Javy w wersji 1.1. W późniejszych wersjach pojawiały się jakieś modyfikacje tego mechanizmu, ułatwiające trochę pisanie. My napiszemy prostą aplikację typu klient / serwer. Klient będzie miał za zadanie wywołać metodę serwera. Projekt wykonam w środowisku Eclipse (choć nie jest to moje ulubione środowisko).

Tworzymy nowy (Ctrl+N) Java Project w Eclipse, jako Project name podajemy np. RMIServer. Dodatkowo zaznaczę, że projekt trzymam w katalogu (czy też mam tak ustawione Workspace): D:\Java_programy\ - przyda nam się to na później. Zaczniemy od zdefiniowania interfejsu który nasz serwer będzie udostępniać na zewnątrz. Klikamy na src w projekcie prawym przyciskiem myszy i New -> Interface. Wpisujemy nazwę pakietu (w moim przypadku: com.blogspot.mstachniuk.example.rmiserver) i nazwę interfejsu - ja dałem: MyServerInt. Przyrostek Int pochodzi od Interface.

W utworzonym interfejsie definiujemy metody, które będziemy udostępniać na naszym serwerze. Ja utworzyłem jedną metodę, która jako argument przyjmuje String i również go zwraca. W przypadku obiektów sprawa się trochę komplikuje, więc nie będę jej opisywał. Nasz interfejs musi rozszerzać inny interfejs: java.rmi.Remote. Dodatkowo każda metoda musi mieć zadeklarowane, że rzuca wyjątek java.rmi.RemoteException. Jest on rzucany, przy niepowodzeniu operacji wywołania zdalnej metody, przy zerwaniu połączenia itp. Poniżej kod mojego opisanego interfejsu:



No dobra, to teraz napiszmy kod, który będzie implementował przedstawioną powyżej funkcjonalność. Utwórzmy klasę MyServerImpl (przyrostek Impl od Implementation). Będzie on rozszerzał klasę java.rmi.server.UnicastRemoteObject - dla wygody. Można też bez tego - tylko wtedy sami musimy utworzyć obiekt serwera. Nasza klasa dodatkowo będzie implementować nasz wcześniej zdefiniowany interfejs. Przykładowy kod poniżej:



Linia definująca pole serialVersionUID jest do tego aby środowisko Eclipse dało nam spokój (tzn. aby nie wyswietlał sie żółty wykrzyknik przy nazwie klasy). Bez tego też zadziała. Wujek Bob w książce "Czysty kod. Podręcznik dobrego programisty" zaleca jednak aby samemu nie deklarować pola serialVersionUID, a pozwolić kompilatorowi na jego automatyczne wygenerowanie. Ma to znaczenie przy ewentualnej deserializacji różnych wersji klas.

Konstruktor jest wymagany i musi deklarować wyjatek RemoteException, gdyż konstruktory UnicastRemoteObject również deklarują ten wyjątek i może się zdarzyć, że będzie problem z utworzeniem zdalnego obiektu.

Chcąc wywołać zdalną metodę, musimy zarejestrować naszą klasę w rejestrze RMI. Posłuży nam do tego klasa MyServerMain. Utwórzmy więc taką klasę w naszym projekcie. To co musimy w niej zrobić to zarejestrować nasz obiekt pod jakąś nazwą. W tym przypadku będzie to nawa MyRemoteObject i obiekt klasy MyServerImpl. Kod poniżej:



Teraz już możemy spróbować uruchomić nasz serwer. Wciśnięcie Run (Ctrl+F11) w Eclipse powoduje wyjątki. Trzeba skorzystać z linii poleceń. Będę posługiwał się bezpośrednio komendami (Windows) aby lepiej można było zrozumieć co się w aplikacji dzieje.

Na początek utówrzmy sobie plik runServer.bat w katalogu projektu (u mnie: D:\Java_programy\RMIServer). Umieśćmy w nim taką zawartość:



Na początek należy odpalić rmiregistry, czyli rejestr początkowy RMI. Poprzedzmay go komendą start, aby otworzył nam się w osobnym oknie i kolejna komenda mogła się wykonać. Następnie uruchamiamy już naszą aplikację. Podajemy flagę -cp aby wskazać gdzie nasze skompilowane klasy leżą i następnie nazwę klasy (wraz z pakietem).

I tu zaczyuna sie pierwszy problem. Dostajemy spory stos wyjątków. Pośród nich można dostrzeć:



Rozwiązanie jakie kiedyś znalazłem, to można ustawić właściwość (ang. Property) java.rmi.server.codebase. Chodzi o to, że rmiregistry nie wie gdzie jest bytecode, z którego ma korzystać i trzeba mu to jakoś powiedzieć.

Wspomniane property możemy ustawić na 2 sposoby. Pierwszy z nich to wywołanie System.setProperty(key, value), a drugi to odpowiednie wywołanie z lini poleceń:
-Dkey=value

Zmodyfikujmy więc nasz skryp uruchomienowy:



Dobra, działa (na razie). Teraz sie zajmijmy klientem. Musi on korzystać z Menadzera bezpieczeństwa RMI, jeśli chcemy ładować kod ze zdalnego serwera. Dla projektu klienta tworzymy osobny projekt o nazwie RMIClient. Tworzymy w nim pakiet (w moim przypadku com.blogspot.mstachniuk.example.rmiclient) i umieszczamy w nim nową klasę MyClientMain. Po zainicjowaniu menażera bezpieczeństwa, musimy się dostać do zdalnego obiektu. Przykładowy kod klasy klienta poniżej:




Aby Eclipse nam nie krzyczało, że nie wie co to MyServerInt, skopiujmy więc ten plik z projektu serwera do projektu klienta, pamiętając aby umieścić go w tym samym pakiecie (nazwa pakietu jest nierozłączną nazwą klasy). Gdybyśmy plik umiescili w innym pakiecie lub zmienili nazwę klasy otrzymalibyśmy wyjątek ClassCastException.

Sprawdzamy czy coś działa. Uruchamiamy najpierw serwer (za pomocą naszego skryptu), a następnie klienta (z poziomu Eclipse). Dostajemy wyjątek java.security.AccessControlException, rzucany przez metdę: lookup(). Metoda ta próbuje odnaleść zdalny obiekt. Okzuje się bowiem, że RMISecurityManager zabrania nawiązywania połączenia w sieci. Musimy więc utworzyć plik polityki bezpieczeństwa. W tym celu tworzymy plik o nazwie client.policy w katalogu bin projektu naszego klienta. W pliku tym umieszczamy następującą zawartość, dającą nam nieograniczony dostęp do wszystkiego:



Podczas wdrożenia aplikacji trzeba bedzie plik ten zmodyfikować, w myśl zasady nie dawać więcej niż trzeba. Czas napisać skrypt uruchomieniowy dla klienta (katalog projektu klienta: D:\Java_programy\RMIClient):



Niestety dalej ten sam błąd. Rozwiązaniem tego problemu moze być dodanie pełnej (bezwzglednej) ścieżki do pliku cient.policy. Modyfikujemy nasz skrypt:



Warto jednak plik policy przenieść do głównego katalogu projektu. Po co? A no podczas czyszczenia projektu w Eclipse zawartość bin jest usuwana i możemy stracić nasz plik. Modyfikujemy więc odpowiednio skrypt uruchomieniowy:



Warto też czasem sprawdzić w kodzie, czy udało nam się uzyskać odpowiednie pozwolenia:



Gdyby druga metoda rzuciła java.security.AccessControlException oznaczało by to, że nie udało się ustawić odpowiedniej polityki bezpieczeństwa, czyli pewnie ścieżka jest błędna.


Oczywiście u Was ścieżki mogą byc trochę inne i musicie je dopasować do tego gdzie trzymacie projekt. Odpalamy skrypt i... udało się. Nie ma żadnego wyjątku:) Tyle że nasz kod klienta za wiele nie robi. Dodajmy więc poniższe 2 liniki do kodu klienta (za wywołaniem lookup()):



W tym momencie ładnie widać, że operowanie na zdalnych obiektach jest tak samo proste jak operowanie na lokalnych. Wywołanie metody niczym się nie różni.
Odpalmy klienta. Na konsoli powiązanej z programem klienta powinniśmy zobaczyć wynik działania:

Wynik: getDescription: Ala ma kota

a na konsoli serwerowej:

MyServerImpl.getDescription Ala ma kota

Oznacza to, że zadziałało. Zmieńmy tylko zawartość pliku client.policy, na następującą:



Powyższy plik oznacza zezwolenie na uzyskanie połaczenia z dowlną maszyną na portach 1024 - 65535. Domyślnie RMI siedzi na porcie 1099, a obiekty serwera mogą korzystać z wyższych portów. Chcąc być bardziej restrykcyjnym zamiast gwiazdki * możemy podać localhost.

Podsumowując, stworzyliśmy prosty serwer i klienta RMI. Przedstawiłem klika wyjątków, na które można się natknąć i podałem sposób radzenia sobie z nimi. Po więcej informacji odsyłam do ksiazki Core Java 2 Techniki Zaawansowane, a także do dokumentacji i tutoriali na stronie Sun'a. Z tego co mi wiadomo istnieje jeszcze wtyczka do Eclipse'a ułatwiająca pisanie aplikacji typu RMI. Ja osobiście nie korzystałem, więc nie wiem jak ona działa.

Więcej informacji:
[1] Tutorial na stronie Oracle: http://download.oracle.com/javase/tutorial/rmi/index.html
[2] Wprowadzenie do RMI w Javie 2 by Seweryn Hejnowicz:
http://www.ii.uni.wroc.pl/~prz/200405/2005lato/java/rmi/referat_rmi.htm
[3] Tworzenia aplikacji rozproszonej RMI by dr inż. Tomasz Kubik:
http://tomasz.kubik.staff.iiar.pwr.wroc.pl/dydaktyka/Java/JavaWyk06-RMI-TK.pdf
[4] Java. Techniki zaawansowane. Wydanie VIII
http://helion.pl/ksiazki/java_techniki_zaawansowane_wydanie_viii_cay_s_horstmann_gary_cornell,javtz8.htm (ja czytałem starsze wydanie i bardzo ładnie było wszystko opisane)
[5] Czysty kod. Podręcznik dobrego programisty:
http://helion.pl/ksiazki/czysty_kod_podrecznik_dobrego_programisty_robert_c_martin,czykod.htm (odnośnie serialVersionUID)

1 komentarz: