28 czerwca 2008

EJB 3.0 - Rozdział 5. Ziarno sterowane komunikatami MDB

Już dawno nie zaglądałem do specyfikacji EJB 3.0. Ostatni wpis dotyczący stricte lektury specyfikacji EJB 3.0 dotyczył mechanizmów bezpieczeństwa - EJB 3.0 - Rozdział 17: Mechanizm bezpieczeństwa z datą 24 lutego 2008. Ostatnimi jednak czasy pracowałem dosyć intensywnie z IBM WebSphere Application Server 6.1 i jego funkcjonalnością związaną z komunikacją JMS realizowaną przez Service Integration Bus (SIBus). Jednym z wyzwań, jakie przyszło mi podjąć, było zestawienie środowiska IBM WebSphere Portal 6.0 z IBM WebSphere Process Server 6.1, których wykonanie usług sieciowych (ang. web services) realizowane było za pomocą protokołu SOAP "puszczonego" przez JMS (zamiast HTTP), gdzie szyną komunikacyjną był IBM WebSphere MQ 6.0. Dużo dobrej zabawy i z 3 zaplanowanych dni zeszło się bodajże do tygodnia, albo nawet i dłużej. Ostatecznie klient usługi sieciowej uruchomiony po stronie Portala wywołał usługę sieciową po stronie Process Servera za pomocą komunikatu SOAP przesłanego przez JMS i radość zapanowała ogromna ;-)

W trakcie konfiguracji dotknąłem pewnego aspektu serwera aplikacyjnego określanego mianem specyfikacja aktywacji (ang. activation specification), która definiuje sposób wzbudzenia ziarna sterowanego komunikatami (dalej: ziarno MDB - Message-Driven Bean lub ziarno komunikacyjne). Kiedy jednak zadano mi pytanie co to jest dokładnie specyfikacja aktywacji niewiele mogłem powiedzieć, poza kilkoma mętnymi zdaniami (coś ala eee..uuu...no wiesz...i takie tam). Wtedy właśnie zdecydowałem się na powrót do specyfikacji i rozpoznania od podszewki, co i jak w trawie piszczy w kontekście ziaren MDB. I oto jest - relacja z rozdziału 5. Kontrakt ziarna sterowanego komunikatami (Chapter 5 Message-Driven Bean Component Contract) specyfikacji Enterprise JavaBeans 3.0.

Przykładowa aplikacji korporacyjna korzystająca z ziarna MDB oraz JPA uruchomiona w środowisku Apache Geronimo jest opisana w moim artykule Aplikacja Java EE 5 z MDB z JPA w trybie JTA i PostgreSQL w Apache Geronimo 2.

Rozdział 5 opisuje kontrakt ziarna MDB - jego prawa i obowiązki oraz środowisko serwera aplikacyjnego (kontenera EJB).

Ziarna MDB nie posiadają interfejsu klienckiego i w związku z tym są dla klienta niewidoczne, tj. nie można wywołać ziarna MDB bezpośrednio.

Ziarna MDB nie posiadają stanu konwersacyjnego (stan będący wynikiem wielu wywołań), i w tym zakresie można je porównać do bezstanowych ziaren sesyjnych (SLSB). Podobnie jak SLSB, ziarna MDB mogą jednakże przechowywać pewien stan w zmiennych instancji, aczkolwiek jest on identyczny dla wszystkich egzemplarzy MDB. Stąd też wszystkie egzemplarze MDB są identyczne, poza czasem obsługi komunikatów. I tu pojawia się sedno istnienia ziaren MDB - asynchroniczna obsługa komunikatów. Cykl rozwojowy kontrolowany jest całkowicie przez kontener.

Celem udostępnienia MDB w specyfikacji EJB 3.0 jest umożliwienie tworzenia ziarna korporacyjnego, które jest asynchroniczne wywoływane w celu przetwarzania nadchodzących komunikatów, w sposób nie bardziej skomplikowany niż jak to ma miejsce przy konstrukcji dowolnego komponentu nasłuchującego komunikatów. W ten sposób istnieje możliwość równoległego przetwarzania strumienia komunikatów.

W zasadzie nie istnieje pojęcie klienta MDB, gdyż dostęp do ziarna MDB jest wyłącznie poprzez wysłanie komunikatu pod adres przeznaczenia (ang. destination) lub punkt końcowy (ang. endpoint), pod którym nasłuchuje MDB (podobnie jak inni słuchacze, niekoniecznie ziarna MDB).

Obiekty reprezentujące adres przeznaczenia komunikatów - kolejki (ang. queue) lub tematu (ang. topic) - mogą zostać przekazane przez kontener poprzez mechanizm wstrzeliwania zależności (ang. DI - dependency injection) lub odszukane w przestrzeni nazewniczej (drzewie) JNDI klienta.

Skorzystanie z mechanizmu DI sprowadza się do udekorowania pola egzemplarza adnotacją @Resource:
 @Resource Queue stockInfoQueue;
co równoznaczne jest z odszukaniem w drzewie JNDI (obowiązkowo należy związać jms/stockInfoQueue z odpowiednim, fizycznym bytem dostępnym na serwerze):
 Context initialContext = new InitialContext();
Queue stockInfoQueue = (javax.jms.Queue)initialContext.lookup("java:comp/env/jms/stockInfoQueue");
Egzemplarz MDB żyje w ramach serwera aplikacji (kontenera EJB), który dostarcza wszystkie usługi konieczne do poprawnej pracy MDB: bezpieczeństwo, współbieżność, transakcje, itp. Dostarczenie komunikatu klienta następuje do dowolnego egzemplarza ziarna MDB.

Za pomocą adnotacji @MessageDriven lub elementu message-driven w deskryptorze wdrożenia ejb-jar.xml definiuje się klasę będącą ziarnem MDB. Klasa musi implementować odpowiedni interfejs komunikacyjny, np. javax.jms.MessageListener dla obowiązkowego wsparcia JMS (Java Message Service) lub określić jego realizację elementem messaging-type w deskryptorze wdrożenia ejb-jar.xml. Właściwa dla interfejsu komunikacyjnego metoda, np. onMessage() w przypadku javax.jms.MessageListener, zostanie wywołana w momencie otrzymania komunikatu do obsłużenia.

Jeśli klasa ziarna MDB implementuje więcej niż jeden interfejs inny niż java.io.Serializable, java.io.Externalizable czy dowolny interfejs z pakietu javax.ejb, interfejs komunikacyjny musi być określony atrybutem messageListenerInterface adnotacji @MessageDriven lub atrybutem messaging-type elementu message-driven w deskryptorze wdrożenia.

Ziarno MDB korzysta z całego zakresu mechanizmu wstrzeliwania zależności oferowanego przez kontener EJB. Wstrzeliwanie zależności następuje po utworzeniu klasy ziarna, a przed wykonaniem metody komunikacyjnej.

Dostęp do kontekstu wykonania ziarna następuje poprzez mechanizm DI w postaci adnotacji @Resource na polu typu javax.ejb.MessageDrivenContext lub implementację interfejsu javax.ejb.MessageDrivenBean. W ten sposób ziarno MDB w trybie zarządzania transakcjami przez serwer (CMTD - container-managed transaction demarcation) może oznaczyć bieżącą transakcję jako wyłącznie do wycofania - metoda setRollbackOnly() - czy sprawdzić jej stan - metoda getRollbackOnly(). W trybie samodzielnego zarządzania transakcjami (BMTD - bean-managed transaction demarcation) ziarno MDB korzysta z metody getUserTransaction() do uzyskania dostępu do mechanizmu zarządzania transakcjami przez interfejs javax.transaction.UserTransaction. Bez względu na tryb transakcyjny ziarna MDB można skorzystać z metod:
  • getTimerService() - dostęp do mechanizmu budzika (ang. timer service);
  • getCallerPrincipal() - dostęp do osobowości (java.security.Principal) związanej z bieżącym wywołaniem ziarna;
  • lookup() - metoda dostępu do prywatnego drzewa JNDI (z którego pobierane są obiekty przez mechanizm DI);
Metody isCallerInRole(), getEJBHome() oraz getEJBLocalHome() są oddziedziczone z interfejsu javax.ejb.EJBContext, których wykonanie jest zabronione przez ziarno MDB.

Możliwe jest związanie wykonania ziarna MDB z interceptorami poprzez określenie metod przechwytujących bezpośrednio w ziarnie MDB lub pośrednio w związanej klasie interceptora:
  • Adnotacja @PostConstruct określa metodę wykonywaną przed wykonaniem metody komunikacyjnej a po wykonaniu DI w nieokreślonym kontekście bezpieczeństwa i transakcji;
  • Adnotacja @PreDestroy określa metodę wykonywana przed usunięciem ziarna z puli bądź zniszczeniem w nieokreślonym kontekście bezpieczeństwa i transakcji.
Metody przechwytujące oznaczone @PrePassivate lub @PostActivate są ignorowane.

Ziarno MDB nie jest zobowiązane do implementowania interfejsu javax.ejb.MessageDrivenBean. Jest to zaszłość z poprzednich wersji specyfikacji EJB. Dostęp do zasobów oferowanych ziarnom MDB realizującym ten interfejs jest możliwa przez skorzystanie z mechanizmu DI oraz metod przechwytujących (interceptorów). Interfejs oferuje dwie metody:
  • setMessageDrivenContext() - przekazanie kontekstu wykonania MDB (analogiczna do @Resource MessageDrivenContext);
  • ejbRemove() - wykonana przed zniszczeniem egzemplarza ziarna MDB (analogiczna do @PreDestroy)
Na uwagę zasługuje wymaganie, które nakłada na kontener EJB obowiązek określenia metody ejbRemove() jako wyłączną metodę przechwytującą @PreDestroy (alternatywnie przez deskryptor wdrożenia) w przypadku implementacji interfejsu javax.ejb.MessageDrivenBean.

Ziarno MDB może zostać zarejestrowane jako odbiorca zdarzenia wzbudzenia budzika.

Utworzenie ziarna MDB przez kontener następuje trzykrokowo:
  1. Wykonanie metody newInstance()
  2. Przekazanie kontekstu wykonania jako egzemplarz typu MessageDrivenContext, jeśli wymagane oraz wstrzelenie zależności
  3. Wykonanie @PostConstruct, jeśli istnieje (jeśli istnieje metoda ejbCreate() to jest ona traktowana jako metoda @PostConstruct)
Można zdefiniować interceptor AroundInvoke dla ziarna MDB bezpośrednio w ramach ziarna lub pośrednio poprzez klasę interceptora.

Kontener szereguje wywołania metod ziarna MDB zdejmując troskę o współbieżność z programisty. Wykonanie metod ziarna, interceptory czy metody budzika są wszystkie wykonywane szeregowo (pojedyńczo, jedna po drugiej).

Nie ma gwarancji przetwarzania komunikatów w kolejności ich nadawania. Obsługa komunikatów odbywa się równolegle przez wiele egzemplarzy ziarna MDB.

Metoda komunikacyjna ziarna MDB oraz metoda budzika wykonywane są w kontekście transakcji określonym przez adnotacje lub deskryptor wdrożenia ejb-jar.xml. Dla ziarna MDB w trybie CMTD metoda komunikacyjna może być oznaczona przez atrybuty REQUIRED lub NOT_SUPPORTED, a metoda budzika przez REQUIRED, REQUIRES_NEW lub NOT_SUPPORTED. Ziarno MDB w trybie BMTD korzysta z javax.transaction.UserTransaction do określenia zasięgu trwania transakcji. Przyjęcie komunikatu nie należy do transakcji w BMTD, a dla CMTD jest jej częścią w przypadku atrybutu REQUIRED.

Za pomocą elementu activationConfig adnotacji @MessageDriven lub elementowi activation-config deskryptora wdrożenia dostawca ziarna (ang. bean provider) określa wdrażającemu (ang. deployer) konfigurację ziarna w środowisku uruchomieniowym, np. trybie potwierdzenia komunikatów, selektorów komunikatów, rodzajowi adresu przeznaczenia czy punktowi końcowemu. Konfiguracja w deskryptorze wdrożenia jest dodawana do tej określonej przez adnotację @MessageDriven. W przypadku wystąpienia parametru w obu konfiguracjach konfiguracja z deskryptora nadpisuje konfigurację z adnotacji.

Konfiguracja (aktywacji) ziarna MDB jest specyficzna dla dostawcy komunikacyjnego.

Właściwości konfiguracji aktywacji dla ziarna MDB opartego o dostawcę komunikacyjnego zgodnego z JMS:
  • Potwierdzenie otrzymania komunikatu realizowane jest wyłącznie przez kontener EJB (serwer aplikacyjny). W przypadku trybu CMTD komunikat potwierdzany jest jako część protokołu zatwierdzania transakcji. W BMTD przyjęcie komunikatu nie jest częścią transakcji inicjowanej przez ziarno MDB i jest potwierdzane przez kontener. Dla BMTD dostawca ziarna określa Auto-acknowledge lub Dups-ok-acknowledge jako wartość atrybutu acknowledgeMode elementu activationConfig adnotacji @MessageDriven lub activation-config-property w deskryptorze. Brak acknowledgeMode oznacza Auto-acknowledge.
  • Ziarno MDB może być związane z selektorem komunikatów, który określa (zawęża), jakie komunikaty mogą być dostarczone. Wartość atrybutu propertyName adnotacji @ActivationConfigProperty dla selektora to messageSelector, gdzie w atrybucie propertyValue określa się warunek, który musi być spełniony przez komunikaty dostarczane do ziarna, np. "JMSType = 'car' AND color = 'blue' and weight > 2500". Składający aplikację (ang. application assembler) może dalej zawęzić komunikaty przez zmianę wartości elementu activation-config-property-value deskryptora wdrożenia, ale nie całkowicie zamienić (w ramach obsługi komunikatu ziarno oczekuje pewnych wartości przenoszonych przez nie i zamiana selektora mogłaby doprowadzić do niepoprawnego działania ziarna).
  • Obowiązkiem wdrażającego ziarno jest określenie adresu przeznaczenia lub punktu końcowego, na którym nasłuchuje ziarno. Dostawca ziarna może jedynie wskazać (ale nie nakazać), czy ziarno ma nasłuchiwać na kolejce (ang. queue) lub temacie (ang. topic) przez element activationConfig adnotacji @MessageDriven lub activation-config-property w deskryptorze. Nazwa parametru to destinationType, dla którego wartością może być javax.jms.Queue lub javax.jms.Topic.
Jeśli ziarno związane jest z tematem, dostawca może dalej określić tryb subskrypcji ziarna - trwała (ang. durable) lub ulotna (ang. non-durable) w elemencie activationConfig adnotacji @MessageDriven lub activation-config-property w deskryptorze. Nazwa parametru to subscriptionDurability, dla którego wartością może być Durable lub NonDurable. Brak subscriptionDurability wskazuje na subskrypcję ulotną (nietrwałą). Trwała subskrypcja to taka, która gwarantuje dostarczenie komunikatu, nawet przy braku pracy kontenera EJB. Osoba wdrażająca ziarno MDB powinna zagwarantować, że z pojedyńczą kolejką nie związano wielu różnych ziaren MDB.

Zabronione jest, aby metoda komunikacyjna zgłosiła wyjątek java.rmi.RemoteException. Zaleca się niezgłaszanie wyjątków niedeklarowanych (ang. runtime exception).

Jeśli ziarno MDB pracujące w trybie BMTD zgłosi RuntimeException, kontener nie powinien potwierdzić otrzymania komunikatu.

Metody @PreDestroy mogą nie zostać wywołane na egzemplarzu ziarna MDB przy padzie serwera lub po zgłoszeniu wyjątku systemowego przez ziarno. Nie można polegać na wykonaniu @PreDstroy jako pary do wykonania @PostConstruct i utworzone zasoby muszą być w inny sposób sprzątane.

Dostawca ziarna MDB jest zobowiązany do bezpośredniego (przez implements) lub pośredniego (przez deklarację w @MessageDriven bądź deskryptorze) implementowania interfejsu komunikacyjnego wymaganego przez wspierany system komunikacyjny. Dla JMS będzie to javax.jms.MessageListener.

Klasa ziarna MDB musi być publiczna, nieoznaczona słowem final czy abstract i musi być klasą głównego poziomu (ang. top-level class). Klasa musi dostarczać publiczny bezparametrowy konstruktor. Klasa nie może definiować metody finalize(). Klasa może implementować interfejs javax.ejb.MessageDrivenBean, javax.ejb.TimedObject oraz posiadać metodę ejbCreate(). Klasa ziarna może posiadać klasy i interfejsy nadrzędne.

Kontener EJB musi wspierać ziarna MDB jako konsumentów kolejek JMS lub trwałej subskrypcji.

Pytanie konkursowe: Co definiuje element activationConfig adnotacji @MessageDriven? Pytanie "wyciągajace": Jaki element deskryptora wdrożenia ejb-jar.xml odpowiada elementowi activationConfig adnotacji @MessageDriven?