OSGi Declarative Services
OSGi, coraz popularniejszy temat dyskusji wśród programistów Javy, wg. Burton group przeżywa obecnie okres największego rozkwitu. Należy jednak pamiętać, że sam standard został zainicjowany już dawno, bo w 1999 roku. Powstałe wówczas OSGi Alliance zrzeszało na początku głównie firmy telekomunikacyjne. Misją OSGi jest utworzenie specyfikacji uniwersalnego mechanizmu modułów dla języka Java. Najnowsze wydanie specyfikacji, o numerze 4.1, posiada szereg implementacji zarówno komercyjnych (Knoplerfish Pro, Prosyst mBedded Server Professional), jak i darmowych (Apache Felix, Eclipse Equinox). Sama organizacja OSGi nie zajmuje się implementacją, a jedynie (lub aż!) opracowywaniem standardu – kompromisu, który skłonni są zaakceptować jej członkowie.
Mimo, że składnia języka Java przewiduje pojęcie pakietów (których zadaniem jest grupowanie klas powiązanych ze sobą logicznie), to we współczesnych, rozbudowanych systemach zapanowanie nad dziesiątkami, czy setkami pakietów jest nie lada wyzwaniem. W końcu pakiety klas nigdy nie działają w próżni, a cały czas komunikują się między sobą. Egzekwowanie, czy w ogóle śledzenie właściwych zależności między różnymi pakietami – tak by np. były one zgodne z przyjętą architekturą – jest nie jest łatwe i staje się coraz trudniejsze z kolejnymi wersjami produkowanego systemu.
OSGi upraszcza całe to piekiełko pakietów wprowadzając koncepcję modułu - zbioru klas Java (pogrupowanych w pakiety) i zasobów (np. pliki tekstowe, graficzne, itp.), opisanych plikiem manifestu. Moduły są dostarczane w postaci jednego pliku JAR i na pierwszy rzut oka niewiele różnią się od typowych plików JAR znanych ze specyfikacji Javy. Kluczową różnicę stanowi jednak manifest. Opisuje on interfejsy komunikacyjne modułu, a dokładniej pakiety publikowane przez siebie oraz wymagane z innych modułów. Informacje te są przechowywane w tekście manifestu, a nie w kodzie Javy, by środowisko OSGi mogło spełnić wymagania każdego modułu, zanim maszyna wirtualna Javy zacznie ładować jego klasy. Ze względu na telekomunikacyjne korzenie standardu, jednym z kluczowych wymagań była zawsze możliwość nieprzerwanej pracy środowiska modułowego, a więc także w trakcie aktualizacji oprogramowania i wymiany modułów systemu. Z punktu widzenia programisty, oznacza to tyle, że każdy moduł (klasy Java!) może w dowolnej chwili zniknąć, zostawiając na pastwę losu wszystkie pozostałe wymagające go moduły. To jest zupełnie nowe wymaganie, w porównaniu do dotychczasowych aplikacji pisanych w Javie.
Komunikacja w OSGi: Usługi
Najprostszy sposób komunikacji w OSGi to zwyczajna interakcja między klasami. Może być zaimplementowana dokładnie w taki sam sposób, jak w przypadku komunikacji między różnymi plikami JAR, jednak nie mamy wówczas żadnej kontroli nad dynamiczną naturą modułów. W tej sytuacji próba zdefiniowania usług, które klasy z różnych modułów mogłyby świadczyć sobie nawzajem, szybko sprowadzi się jedynie do ograniczonego wzorca Singleton.
Z drugiej strony, moduły zainteresowane wybranymi usługami mogą uzyskać do nich dostęp poprzez rejestr usług (ang. service registry). W oferowanym przez OSGi rejestrze każdy moduł może ogłosić (zareklamować!) usługi, które świadczy. Usługa jest powiązana z interfejsem (lub wieloma interfejsami), który implementuje. W rejestrze może być wiele różnych implementacji tej samej usługi, co pozwala na bardziej abstrakcyjne projektowanie systemu (zamiast skupiania się na jednej implementacji). Korzystając z rejestru, można sprawdzić czy usługa jest cały czas dostępna, czy też nie zniknęła wraz z udostępniającym ją modułem.
Moduł rejestruje usługę poprzez interfejs BundleContext
, od którego otrzymuje referencję zawsze przy starcie. Poniżej zarejestrowano przykładową usługę org.example.MailOffice
. Implementacja usługi jest w klasie MailOfficeImpl
. Z tą usługą nie są związane żadne dodatkowe własności – stąd trzeci argument to null
.
ServiceRegistration reg = context .registerService("org.example.MailOffice", new MailOfficeImpl(), null);
Poniższy fragment kodu prezentuje w jaki sposób uzyskać dostęp do udostępnionej przed chwilą usługi:
ServiceReference ref = context .getServiceReference("org.example.MailOffice"); if (ref != null) { MailOffice office = (org.example.MailOffice) context .getService(ref); office.sendMessage("I’m using you, service!"); }
W pierwszej linii odpytujemy rejestr, czy zna usługę o typie org.example.MailOffice
. Jeżeli jest taka zarejestrowana, to otrzymamy referencję do niej w postaci interfejsu ServiceReference
. W kolejnym kroku, ponownie poprzez kontekst modułu, można uzyskać bezpośrednio klasę realizującą interesujący nas interfejs. W naszym przypadku jest to org.example.MailExample
.
Usługa zostanie usunięta z rejestru w momencie zatrzymania modułu, lub poprzez bezpośrednie wywołanie ServiceRegistration.unregister()
.
Korzystanie z usług w sposób przedstawiony powyżej ma niestety kilka wad. Moduł musi najpierw zostać uruchomiony, by uzyskać dostęp do kontekstu BundleContext
. Podczas startu systemu z dużą ilością modułów może to znacznie wydłużyć czas uruchamiania. W przypadku dużej liczby usług, łatwo stracić nad nimi kontrolę, gdyż odwołania są zaszyfrowane bezpośrednio w kodzie programu. Jeżeli między usługami występują zależności, również samemu trzeba się zatroszczyć o ich wcześniejsze spełnienie. Sprawia to, że korzystanie z usług wiąże się z dużą ilością powtarzalnego kodu, który jest dodatkowo trudny w utrzymaniu ze względu na dynamizm całego środowiska. Główną zaletą jest pełna kontrola nad usługami.
Declarative Services
Zamiast programowo uruchamiać i zatrzymywać usługi oraz spełniać zależności między nimi, można przecież wykorzystać podobny fortel jak z samymi modułami! Zrzucimy tą czynność na środowisko, a całą konfigurację umieścimy w plikach tekstowych. W ten sposób usługa zostanie uruchomiona dopiero, gdy jej wszystkie wymagania zostaną spełnione, a zarządzanie zależnościami między usługami jest dużo prostsze. By aktywować usługi nie jest już konieczne uruchamianie wszystkich modułów – jedynie te usługi, które muszą być aktywne, zostaną aktywowane wraz ze swoimi modułami.
Aby móc korzystać z Declarative Services (w skrócie DS), należy się upewnić że w środowisku OSGi jest uruchomiony moduł z implementacją tej usługi. Przy tworzeniu tego artykułu wykorzystano moduł z Eclipse Equinox – org.eclipse.equinox.ds
.
Wracając do przykładu z MailService
, zamiast programowo rejestrować usługę poprzez odwołanie do kontekstu BundleContext
, należy zmodyfikować plik manifestu – wskazać plik z opisem komponentu realizującego usługę:
Service-Component: mailOffice.xml
Plik mailOffice.xml zawiera definicję usługi. Usługa ta powinna być uruchamiana zaraz po starcie systemu i realizować interfejs org.example.MailOffice
. Jak pamiętamy z wcześniejszego kodu, klasa implementująca tą usługę to org.example.MailOfficeImpl
. Cała definicja wygląda następująco:
<?xml version="1.0" encoding="UTF-8"?> <component immediate="true" name="mailOffice"> <implementation class= "org.example.MailOfficeImpl"/> <service> <provider interface="org.example.MailOffice"/> </service> </component>
Poprawnie zdefiniowana usługa jest dostępna poprzez rejestr i może zostać wykorzystana podobnie jak w poprzednim przykładzie – poprzez programowe odwołanie. Wówczas jednak powiązanie między modułami udostępniającym i wykorzystującym komponent mailOffice
nadal nie będzie czytelne. Zdefiniujmy zatem drugi komponent, tym razem wykorzystujący mailOffice
. Nadamy mu nazwę Person, a implementowany będzie przez klasę home.Person
. Wymaga ona komponentu o nazwie mailOffice
dostarczającego interfejs org.example.MailOffice
, by móc wysyłać wiadomości do urzędu. By komponent Person został aktywowany potrzebna jest dokładnie jedna usługa MailOffice
. Jeśli nasza osoba (Person) wykorzystywałaby wielu dostawców poczty (np. różne konta pocztowe), byłaby powiadamiana o wszystkich pojawiających się i znikających dostawcach.
Poniżej przykład definicji komponentu:
<?xml version="1.0" encoding="UTF-8"?> <component immediate="true" name="person"> <implementation class="home.Person"/> <reference cardinality="1..1" interface="org.example.MailOffice" name="MailOffice" policy="static"/> </component>
Jak tylko w systemie pojawią się usługi MailOffice
, zostanie utworzony nowy obiekt Person
, a referencja do MailOffice
zostanie przekazana poprzez metodę Person.activate(ComponentContext ctx)
. Korzystając z kontekstu na bieżąco można odwoływać się do usługi i sprawdzać czy ona istnieje (strategia typu lookup).
Jest także drugi sposób odwoływania się do wymaganej usługi. DS umożliwiają automatyczne przekazywanie referencji do usługi za każdym razem, gdy nowa się pojawia w systemie, oraz gdy któraś z implementacji znika (strategia zdarzeniowa, z ang. event based). W tym podejściu trzeba uzupełnić znacznik <reference>
w definicji komponentu o dwa nowe argumenty – bind oraz unbind:
<reference cardinality="1..1" interface="org.example.MailOffice" name="MailOffice" policy="static" bind="setMail" unbind="unsetMail"/>
Z kolei klasę Person
uzupełniamy o metody:
protected void setMail(MailOffice o); protected void unsetMail(MailOffice o);
Wraz ze zmianą widoczności referencji MailOffice
, metody te na bieżąco będą wywoływane przez środowisko.
Poza tym prostym przykładem biblioteka DS oferuje jeszcze dwa inne rodzaje komponentów: delayed component oraz component factory. Komponent typu „delayed” (oraz cały deklarujący go moduł) zostanie uruchomiony dopiero, gdy inny komponent będzie go wymagał. Mechanizm ten przypomina dobrze znaną koncepcję lazy-loading. To rozwiązanie jest możliwe dlatego, że moduł DS rejestruje usługi na rzecz innych modułów bez ich uruchamiania – tworzy proxy, a prawdziwa implementacja uruchamiana jest dopiero przy pierwszym odwołaniu.
Z kolei fabryka komponentów ma zastosowanie wszędzie tam gdzie potrzebne jest wiele instancji danego komponentu. Chociażby komponent mailOffice
– zdefiniowany z dodatkową opcją factory=<nazwaFabryki>
spowoduje utworzenie usługi typu org.osgi.service.component.ComponentFactory
, która pozwala tworzyć nowe instancje komponentu (kolejne usługi mailOffice
) poprzez metodę newInstance(Dictionary)
. W każdym wywołaniu tej metody można konfigurować dodatkowe opcje poprzez jej argument.
Podsumowanie
DS to jednak tylko jedno z istniejących rozwiązań zarządzania usługami pomiędzy modułami OSGi. Specyfikacja ta pojawiła się dopiero w ostatniej wersji standardu, pozostawiając czas projektom korzystającym z OSGi na utworzenie swoich własnych mechanizmów abstrakcji usług. Jednym ze zbliżonych narzędzi są punkty rozszerzeń znane programistom wtyczek w Eclipse. Innym, opisanym także w tym numerze, jest Spring-DM, który najprawdopodobniej wkrótce także stanie się częścią standardu OSGi w wersji 4.2. Każdy zainteresowany przetestowaniem DS powinien zajrzeć do Eclipse w wersji developerskiej (3.5), by wypróbować powstające tam narzędzia do konfiguracji komponentów. W znaczący sposób upraszczają one pracę z DS.
Witam,
chciałbym sie dowiedzieć kiedy pojawi sie kolejna cześć artykułu (w marcowym wydaniu niestety się nie pojawił). Z góry dzięki za info!
Nie było w planach kontynuacji tego artykułu. Czy jest jakiś temat, który Cię interesuje? Może Jacek byłby chętny napisać kolejny artykuł.
Przepraszam ale nie tutaj miał być ten komentarz napisany. Proszę o jego usunięcie.
interesuje mnie temat połączenia Maven'a z Eclipse i OSGi