OSGi jest standardem mającym zastosowanie tam, gdzie wymagana jest możliwość dodawania/modyfikacji/usuwania funkcjonalności w obrębie jednej Maszyny Wirtualnej Javy bez przerw w dostarczaniu usługi. Spotykany jest między innymi w sterownikach SDN (Software-Defined Networking) takich jak ONOS [1] (które wykorzystujemy tworzonych w EXATEL rozwiązaniach SDNbox i SDNcore), czy OpenDaylight[2].
Po 20 latach jego istnienia wiadomo już, że ten koncept, choć swego czasu innowacyjny, nie znalazł zastosowania poza niszowymi, wyspecjalizowanymi aplikacjami. W 2013 roku tylko (aż?) 10% bibliotek opublikowanych w Maven Central Repository (najpopularniejszym repozytorium z zależnościami w języku Java) było bundle’ami[3]. Cofnijmy się jednak w czasie do początków OSGi.
Jest maj roku 2000. Open Services Gateway Initiative publikuje pierwszą wersję specyfikacji OSGi[4].Zaczyna się ona od słów: “The primary goal of the OSGi service framework (“Framework”) is to use the Java (TM) programming language’splatform independence and dynamic code-loading capability to make development and dynamic deployment of applications for small-memory devices easier“. Czym OSGi powinno nas zauroczyć? Jak powinny potoczyć się losy tego standardu tak, aby w roku 2020 był on powszechnie używany?
OSGi pozwala zarządzać cyklem życia poszczególnych komponentów
Bundle to moduł z jawnie zadeklarowanymi zależnościami (import-package) oraz jawnie zadeklarowanym API (export-package). Takimi, z których mogą korzystać inne komponenty. Framework OSGi zapewnia każdemu takiemu modułowi osobny classloader, dzięki czemu każdy z modułów ma niezależny cykl życia. Pozwala to na swobodną aktualizację/wyłączenie wybranych komponentów bez konieczności zatrzymywania całej aplikacji. Ponadto, ponieważ dzieje się to dynamicznie, można stosunkowo łatwo tworzyć aplikacje, do której inni użytkownicy będą mogli dopisywać własny kod. I uruchamiać go, traktując nasze rozwiązanie jak framework.
OSGi, a brakujący modyfikator dostępu w Javie
Jedną z najważniejszych praktyk przy tworzeniu własnej biblioteki, jest utworzenie dobrze zdefiniowanego API i ukrycie szczegółów implementacyjnych. Oryginalnie w Javie (do Javy 8), aby tym sterować, mamy do wyboru cztery modyfikatory dostępu:
- public – tym modyfikatorem oznaczamy API,
- protected – modyfikator sugerujący, że użytkownik biblioteki może nadpisać kod, rozszerzając odpowiednie klasy,
- package-private – elementy są widoczne tylko w obrębie jednego pakietu,
- private – element jest widoczny tylko wewnątrz klasy.
W momencie, gdy kod modułu staje się na tyle duży, że chcemy móc go swobodnie dzielić na wiele pakietów, przy jednoczesnym zachowaniu założenia, że tylko klasy API są oznaczone jako publiczne, zaczyna brakować jeszcze jednego typu dostępu, który można określić jako “modułowy”. OSGi rozwiązuje ten problem poprzez zmianę sposobu definiowania kontraktu między modułami. API definiuje się poprzez podanie w pliku MANIFEST.MF w nagłówku “Export-Package”, które pakiety zawierają klasy dostępne dla innych bundle’i. Wówczas modyfikator “public” oznacza tylko widoczność w obrębie jednego bundle’a (dopóki pakiet zawierający klasę nie zostanie zadeklarowany jako eksportowany w manifeście).
OSGi aktywnie korzysta z wersjonowania semantycznego
Wersjonowanie MAJOR.MINOR.PATCH[5] pomaga użytkownikom (administratorom, programistom) zorientowanie się, czy poszczególne wersje modułów aplikacji są ze sobą kompatybilne. Z punktu widzenia osoby aktualizującej jednego bundle’a, zwiększenie numeru wersji:
- PATCH oznacza, że wraz z wgraniem nowszej wersji, otrzymamy drobne poprawki błędów, a aktualizacja zależnych komponentów jest niepotrzebna.
- MINOR oznacza, że spodziewamy się otrzymać nowe funkcjonalności, które są kompatybilne wstecz, więc zmiana wersji zależnych komponentów nie jest konieczna (za wyjątkiem sytuacji, gdy chcemy by korzystały one z nowej funkcjonalności).
- MAJOR oznacza niekompatybilną zmianę w API, więc wszystkie komponenty, które zależą od podegranego bundle’a również powinny zostać zaktualizowane.
OSGi zobowiązuje do zapisywania tego kontraktu w pliku MANIFEST.MF poprzez zdefiniowanie w nagłówku “Import-Package” zakresu wersji dla każdego z importowanych pakietów z osobna. Walidacja następuje przy każdej próbie instalacji nowego bundle’a.
Przypadki użycia i wyzwania z tym związane
OSGi nie powstał z myślą o dużych aplikacjach webowych, a obecnie to one właśnie są głównym zastosowaniem języka Java. Oczywiście nic nie stoi na przeszkodzie, aby OSGI użyć w mikroserwirsach lub, jeśli zdecydowaliśmy się na monolit, to ustanowić jego wiele instancji. Powoduje to tylko (i aż) tyle, że problem skalowalności jest zupełnie odrębnym od OSGi zagadnieniem. Spójrzmy na poniższe przykłady.
Przykład 1 – projektujemy nową aplikację webową.
Zastanawiamy się nad tym, które jej komponenty będą najczęściej używane, jak skalować ich użycie w zależności od ilości użytkowników, jak aktualizować całość rozwiązania bez przerw w dostępie usługi. Rozwiązaniem na ogół jest zmodularyzowana aplikacja, być może oparta o mikroserwisy, być może monolit w klastrze. Rozwiązania te często uzyskujemy bez użycia standardu OSGi. Gdy po zaprojektowaniu zastanawiamy się, czy dołożyć tam również moduły OSGi, często okazuje się, że może to nas kosztować dużo dodatkowej pracy, a zysk będzie niewielki. Bundle wymagają dodatkowej konfiguracji, zawężają wybór serwera aplikacyjnego, a na dodatek nie zawsze pasują do obranego stosu technologicznego (na przykład do Springa).
Przykład 2 – mamy napisaną aplikację zgodną ze standardem OSGi, którą chcemy przerobić na mikroserwisy.
Okazuje się, że mimo podobieństwa nazewnictwa (OSGi ma nanoserwisy, a my chcemy mikroserwisy[6]), przed nami dużo pracy z wydzielaniem poszczególnych modułów jako osobnych aplikacji, ustanawianiem sposobu komunikacji między tymi modułami, które będą uruchamiane w różnych maszynach wirtualnych Javy, przeprojektowaniem obecnego sposobu instalacji aplikacji.
Zabrakło prostych sposobów na budowanie aplikacji
OSGi zapewnia w pełni modularne podejście do uruchamiania poszczególnych komponentów aplikacji. A co z ich budowaniem? Czy wysiłek włożony w utworzenie bundle’i z jawnie zdefiniowanymi API oraz powiązaniami między modułami (z dokładnością co do pakietu i wersji) zaprocentuje szybszymi buildami dzięki możliwości wykrycia, które moduły zostały zmienione i wymagają przebudowania oraz zrównoleglenia budowania niezależnych zmian? Okazuje się, że niestety nie.
Dopiero stosunkowo niedawno pojawiły się narzędzia, które w odróżnieniu od tradycyjnych (takich jak Maven, Ant, Gradle) pozwalają w łatwy sposób robić buildy inkrementacyjnie. Są to m.in.:
Narzędzia te, choć dobrze współpracują z OSGi, nie powstały z myślą o tym frameworku. Nie istnieją generatory, które na podstawie konfiguracji bundle’i utworzą instrukcję budowania zrozumiałą dla obranego przez nas narzędzia. Ponadto zarówno Buck, jak i Bazel są stosunkowo młode, więc korzystając z nich można napotkać różne nietypowe problemy, które nie występują w dojrzalszych rozwiązaniach.
Zabrakło wsparcia dla popularnych frameworków i bibliotek
Jak już wspomniałam we wstępie, tylko około 10% bibliotek opublikowanych w Maven Central Repository jest gotowe do użycia w aplikacji napisanej we frameworku OSGi. Programista, który zadecyduje, że zależność, mimo, że nie jest bundlem, powinna zostać użyta w projekcie może próbować ją dostosować, choćby poprzez utworzenie shadow jar[9]. Rozwiązanie to jednak wymaga dodatkowej pracy (aby to zrobić prawidłowo, musimy przeanalizować z jakich zależności korzysta potrzebna nam biblioteka oraz w jakich pakietach ulokowane jest API naszej biblioteki) i da się to zrobić tylko w przypadku prostszych zależności. Nie dostosujemy w ten sposób chociażby najpopularniejszego obecnie frameworku – Spring. Były podejmowane próby integracji Springa z OSGi, takie jak Spring Dynamic Module, które później ewoluowało w Eclipse Gemini Blueprint. Na dzień dzisiejszy te projekty są martwe.
Java 9 z projektem Jigsaw
We wrześniu 2017 (czyli ponad 17 lat później, niż pierwsza wersja specyfikacji OSGi!) pojawiła się długo oczekiwana możliwość definiowania modułów w czystej Javie. Nie jest to tak bardzo rozbudowane jak w bundle’ach (brakuje m.in. cyklu życia i dynamicznego ładowania[10]).
Wdraża ona przede wszystkim nowe miejsce do wprowadzenia kontroli dostępu (patrz: OSGi a brakujący modyfikator dostępu w Javie). Mimo, że jest to ten sam pomysł, to jest on inaczej realizowany. Skonfigurowanie modułu w OSGi nie powoduje, że będzie też to moduł w rozumieniu Jigsaw. Są pewne plany połączenia obydwu rozwiązań[11][12], jednak do dziś nie są zrealizowane.
Podsumowując
Pracując z OSGi warto poświęcić chwilę na refleksję, że swego czasu to rozwiązanie wyprzedzało epokę. Można podejrzewać, że gdyby wraz z wdrożeniem OSGi, pojawiły się również dodatkowe korzyści (do których notabene było bardzo blisko – wszystkie niezbędne do tego informacje znajdują się już w dodatkowych nagłówkach manifestu), takie jak możliwość szybszego budowania aplikacji lub łatwość przenoszenia na mikroserwisy, to byłby dziś obowiązujący powszechne standard.
Warto zaznaczyć, że istnieją takie zastosowania, gdzie OSGi wciąż jest wręcz niezastąpione, takie jak wspomniane wyżej sterowniki SDN. Jednak nawet i tam pojawiają się plany rozszerzenia rozwiązania o nowsze stosy technologiczne[13].
Źródło
[1] https://www.opennetworking.org/onos/
[2] https://www.opendaylight.org/
[3] https://blog.osgi.org/2013/09/osgis-popularity-in-numbers.html
[4] https://docs.osgi.org/download/r1/r1.osgi-spec.pdf
[5] https://semver.org/
[6] https://www.oreilly.com/radar/modules-vs-microservices/
[7] https://buck.build/
[8] https://bazel.build/baze
[9] https://icodebythesea.blogspot.com/2012/03/making-osgi-bundle-out-of-third-party.html
[10] https://www.zyxist.com/blog/zrozumiec-moduly-w-javie-9
[11] https://www.infoq.com/articles/java9-osgi-future-modularity/
[12] https://www.infoq.com/articles/java9-osgi-future-modularity-part-2/
[13] https://gonorthforge.com/the-next-generation-architecture-of-onos/