16 października 2007

EJB 3.0 - Rozdział 18 Usługa Budzika (Timer Service)

Niedługo mój wymarzony dzień egzaminu EJB 3.0, do którego przygotowuję się od wieków i tak przeciągam to podejście, i przeciągam. Zawsze coś - a to jeszcze książkę przeczytać (nota bene czeka mnie opublikowanie recenzji książki Enterprise JavaBeans 3.0 Billa Burke & Richarda Monson-Haefela), a to jeszcze artykuł, a to rozdział w specyfikacji EJB 3.0 i dodając do tego pytania na różnych grupach dyskusyjnych nie ma nawet czasu pomyśleć o egzaminie ;-) Najwyraźniej doskwiera mi problem z określeniem momentu, w którym należałoby powiedzieć sobie Dość! (coś na wzór powiedzenia, które ostatnio pojawiło mi się w GMailu autorstwa Oswalda Chambersa The whole point of getting things done is knowing what to leave undone.).

Do egzaminu jeszcze chwila, więc zainspirowany pytaniem na pl.comp.lang.java postanowiłem zapoznać się z tematem poprzez lekturę specyfikacji EJB 3.0 - EJB Core Contracts and Requirements. Dzisiaj padło na rozdział 18 Timer Service. Podobnie jak miało to miejsce w przypadku relacji z lektury specyfikacji JPA, dzisiaj przedstawię EJB3.

Moja propozycja tłumaczenia timer service to usługa budzika. Komentarze odnośnie trafności (bądź jego braku) mile widziane.

Usługa Budzika (powiadamiania) w EJB 3.0 (ang. EJB 3.0 Timer Service) jest transakcyjną usługą dostarczaną przez kontener EJB 3.0, która umożliwia zarejestrowanie ziaren EJB (a w zasadzie ich metod) do uruchomienia o zadanym czasie, po upływie zadanego czasu (budzik jednokrotnego wzbudzenia) lub w określonych interwałach czasowych (budzik wielokrotnego wzbudzenia). Dostęp do usługi następuje poprzez mechanizm wstrzeliwania zależności (DI), interfejs javax.ejb.EJBContext (metoda getTimerService()) lub odszukanie w drzewie JNDI.

Specyfikacja podkreśla fakt, że usługa budzika jest usługą dostarczającą powiadamiania do modelowania długotrwałych procesów biznesowych, a nie czasu rzeczywistego. Jakkolwiek przedziały czasowe można wyrazić korzystając z milisekund (ze względu na wykorzystanie API z Java SE), to zakłada się użycie usługi w przedziałach godzinnych, dziennych lub dłuższych.

18.2 Usługa Budzika oczyma dostawcy ziaren EJB


Usługa Budzika dostarcza metody pozwalające na utworzenie lub usunięcie budzika, jak również pozyskania listy budzików związanych z danym ziarnem.

Budzik jest ustawiony na generowanie zdarzeń czasowych (związanych z upływającym czasem). Klasa ziarna EJB, które używa usługi budzika musi dostarczać metodę zwrotną uruchamianą podczas wystąpienia zdarzenia czasowego (czasowa metoda zwrotna). Metoda czasowa wskazana jest przez adnotację @Timeout, element timeout-method w deskryptorze rozmieszczenia, lub będącą metodą ejbTimeout interfejsu javax.ejb.TimedObject, jeśli ziarno go implementuje. Budziki mogą być tworzone dla bezstanowych ziaren sesyjnych (ziarna SLSB), ziaren sterowanych komunikatami (ziarna MDB) oraz ziaren encyjnych, jeśli te spełniają wymagania specyfikacji EJB 2.1 (ukłony, dla wszystkich tych, którzy zechcieli wspierać rozwiązania oparte o EJB 2.1 i dowiadują się o usprawnieniach Korporacyjnej Javy 5 bez możliwości ich wykorzystania - nie jest lekko ;-)). Budziki nie mogą być tworzone dla stanowych ziaren sesyjnych oraz encji JPA.

Wzbudzenie budzika dla ziaren SLSB czy MDB następuje na dowolnym egzemplarzu w stanie oczekiwania (ang. pooled state).

Podczas upłynięcia czasu budzika, kontener wywołuje czasową metodę zwrotną ziarna, z którym jest związany. Budzik może zostać zatrzymany przez ziarno zanim upłynie czas aktywności budzika. Jeśli budzik zostanie zatrzymany, metoda czasowa nie zostanie wywołana (chociaż zgodnie z uwagą w specyfikacji może się zdarzyć nadmiarowe wywołanie - kolejne wskazanie o biznesowej roli usługi budzika a nie do modelowania systemu czasu rzeczywistego). Budzik zatrzymywany jest metodą Timer.cancel().

Wywołania metod tworzących i zatrzymujących budzik oraz metod czasowych są zazwyczaj wykonywane w ramach transakcji. Podczas wycofywania/zatwierdzania transakcji, operacja budzika jest odpowiednio wycofywana/zatwierdzana. Metoda czasowa jest zazwyczaj opatrzona atrybutem transakcyjnym REQUIRED lub REQUIRES_NEW. Wycofanie transakcji oznacza ponowne wykonanie metody czasowej.

Budziki są trwałe. Ich stan jest utrzymywany bez względu na wystąpienie awarii serwera czy cykli pasywacji/aktywacji oraz ładowania/zapisywania ziaren, z którymi są związane.

Interfejs javax.ejb.TimerService składa się z metod do tworzenia budzików (createTimer) z różnymi kombinacjami parametrów określających czas aktywacji oraz metoda zwracająca kolekcję aktywnych budzików getTimers().

Ziarno może przekazać informację specyficzną dla klienta wyłącznie podczas utworzenia budzika (parametr info typu java.io.Serializable w metodach createTimer). Informacja musi być zgodna z mechanizmem serializacji w Javie.

Ziarno może dostarczać co najwyżej jedną metodę czasową (w ramach całego drzewa dziedziczenia). Jeśli ziarno implementuje interfejs javax.ejb.TimedObject, wtedy adnotacja @Timeout lub element timeout-method musi wyłącznie wskazywać na metodę ejbTimeout() z interfejsu.

Dowolna metoda oznaczona adnotacją @Timeout musi mieć następującą sygnaturę:
  void <dowolna-nazwa-metody>(javax.ejb.Timer timer)
Metoda może być dowolnej widoczności: public, private, protected lub domyślnej (package protected) i nie może być final lub static.

Metoda czasowa nie może zgłaszać wyjątków aplikacyjnych.

Podczas wzbudzenia budzika następuje wywołanie metody czasowej, w której można wywołać metodę Timer.getInfo(), która zwróci informacje specyficzne dla budzika przekazane w metodzie createTimer() podczas jego tworzenia.

Na stronie 484 specyfikacja ponownie podkreśla możliwość wystąpienia wielu uruchomień metody czasowej bez jakichkolwiek założeń o moment między wywołaniami innych metod ziarna, czy ich ilość i kolejność.

Metoda czasowa może wykonywać te same operacje jak metody biznesowe ziarna czy metody nasłuchujące MDB.

Wywołanie metody czasowej nie jest związane z kontekstem bezpieczeństwa. Metoda czasowa jest wewnętrzną metodą nie udostępnioną klientom, więc wywołanie EJBContext.getCallerPrincipal() zwróci użytkownika nieuwierzytelnionego tak, jak reprezentowany jest przez serwer.

W przypadku budzika jednokrotnego wzbudzenia, kontener usuwa budzik po poprawnym zakończeniu wykonania metody czasowej, tj. podczas zatwierdzenia transakcji, z którą był związany. Jeśli ziarno wywoła metodę na budziku po zakończeniu metody czasowej zgłaszany jest wyjątek javax.ejb.NoSuchObjectLocalException.

Interfejs javax.ejb.Timer pozwala na zatrzymanie (metoda cancel()) budzika, pozyskanie o nim informacji (metody getTimeRemaining() oraz getNextTimeout()) jak również informacji klienckich specyficznych dla niego (metoda getInfo()) i uchwyt (getHandle()), który umożliwia utrwalenie budzika w wybranym miejscu (baza danych, dysk, itp.). Budziki są obiektami lokalnymi i nie mogą być udostępniane przez biznesowy interfejs zdalny czy interfejs usługi sieciowej.

Porównanie egzemplarzy budzików może być wykonane wyłącznie przy pomocy metody Timer.equals(Object obj).

Jeśli klasa ziarna ma klasy nadrzędne, metoda czasowa może być zadeklarowana przez/udostępniana w klasie ziarna lub jego nadklas.

18.4 Wymagania stawiane kontenerowi EJB (serwerowi aplikacji Java EE)

Egzemplarze budzików nie mogą być serializowalne.

Kontener zobowiązany jest dostarczyć implementację budzika, która będzie do użycia przez cały jego cykl rozwojowy.

Kontener zobowiązany jest dostarczyć właściwą implementację metod Timer equals(Object obj) oraz hashCode().

Jeśli wykorzystywane jest określanie zasięgu transakcji przez kontener (CMTD) i korzysta się z atrybutów REQUIRED lub REQUIRES_NEW (Required lub RequiresNew w deskryptorze rozmieszczenia), kontener musi rozpocząć nową transakcję zanim nastąpi wywołanie metody czasowej. Jeśli transakcja zakończy się niepowodzeniem lub zostanie wycofana, kontener musi ponownie uruchomić metodę czasową co najmniej raz.

Jeśli dostawca ziarna wywoła metodę setRollbackOnly() w ramach metody czasowej, kontener musi wycofać transakcję oraz ponownie wykonać metodę.

Budziki są trwałe. Podczas awari serwera i jego ponownego uruchomienia, budzik pojedyńczego wzbudzenia, który powinien zostać wzbudzony podczas przerwy, zostanie uruchomiony podczas uruchomienia serwera. Budzik wielokrotnego wzbudzenia zostanie wywołany co najmniej raz w takim przypadku.

Usunięcie ziarna powoduje usunięcie jego budzików.

Na zakończenie przykład ziarna bezstanowego, który dostarcza metodę czasową oznaczoną adnotacją @Timeout.
 package pl.jaceklaskowski.timerservice;

import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.annotation.Resource;
import javax.ejb.Stateless;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.jws.WebService;

@Stateless
@WebService
public class TimedSessionBean implements TimedSessionRemote {

@Resource
TimerService timerService;

public void utworzBudzik(long duration) {
long iloscMinut = duration * 1000 * 60;
wypiszKomunikat("Tworzę budzik o czasie wykonania za " + duration + " minut(y).");
timerService.createTimer(iloscMinut, null);
}

@Timeout
private void metodaCzasowa(Timer timer) {
wypiszKomunikat("Metoda czasowa została wzbudzona");
}

private void wypiszKomunikat(String komunikat) {
Locale polski = new Locale("pl");
Date teraz = Calendar.getInstance(polski).getTime();
DateFormat formatDaty = DateFormat.getTimeInstance(DateFormat.LONG, polski);
System.out.println(formatDaty.format(teraz) + ": " + komunikat);
}
}
Uruchomienie metody utworzBudzik z parametrem 2 (poprzez interfejs usługi sieciowej) spowodowało wyświetlenie następującego komunikatu w dzienniku zdarzeń serwera Glassfish:
 WSTX-COMMON-2007: Map CMT EJB web method 'pl.jaceklaskowski.timerservice.TimedSessionBean'.'utworzBudzik' 
with effective transaction attribute of 'REQUIRED' to wsdl bounded operation
'{http://timerservice.jaceklaskowski.pl/}TimedSessionBeanPort':'utworzBudzik'
with WS-AT policy assertion(s) 'TimedSessionBeanPortBinding_utworzBudzik_WSAT_Policy'
...
08:43:54 CEST: Tworzę budzik o czasie wykonania za 2 minut(y).
08:45:54 CEST: Metoda czasowa została wzbudzona

7 komentarzy:

  1. a jak zrobić budzik w EJB3, który zadzwoni codziennie o 7? Zawsze myślałem, że się nie da i używałem Quartza.

    OdpowiedzUsuń
  2. Jakkolwiek Quartz to (podobno) dobre rozwiązanie, to akurat ten przykład daje się łatwo zrealizować w EJB3 za pomocą metody

    Timer TimerService.createTimer(Date initialExpiration, long intervalDuration, Serializable info)

    czyli

    @Resource TimerService timerService;

    Date siodmaRano = ...
    timerService.createTimer(siodmaRano, 1000 * 60 * 60 * 24, null);

    p.s. Mam jeszcze od Ciebie jedno pytanie, na które nie odpowiedziałem, ale ono nie jest tak trywialne ;-)

    OdpowiedzUsuń
    Odpowiedzi
    1. Muszę zaprotestować, powyższa metoda będzie uruchamiała zadanie o 7 rano przez pół roku a przez pół roku o 8 (jeśli była zastosowana w czasie zimowym) lub o 6 (jeśli w czasie letnim). Dlatego niestety potrzebujemy Quartza, albo przynajmniej wyrażeń cron.

      Usuń
    2. A to ciekawe. Nie zwróciłem na to wcześniej uwagi. Faktycznie, skoro podajemy przedział czasowy między wywołaniami, to przy zmianie czasu będzie przesunięcie o tyle, ile nastąpiła zmiana - w naszym przypadku -1 lub +1h. Gratuluję spostrzegawczości.

      A jak wpadłeś na to, że będzie przesunięcie w czasie? Doświadczyłeś tego na własnej skórze, na projekcie? Ciekawym procesu myślowego, który temu towarzyszył.

      Usuń
  3. Hej,

    a jak sprawić aby metoda 'utworzBudzik' została zawołana automatycznie po restarcie serwera czy redeployu aplikacji - np. na jboss'ie?

    Pozdrawiam,
    Szymon

    OdpowiedzUsuń
  4. Myślę, że konieczne jest skorzystanie z funkcjonalności specyficznej dla serwera aplikacji. Jeśli jednak chcemy być zgodni z korporacyjną javą, to najlepszym rozwiązanem jest stworzenie aplikacji internetowej i podczas startu jednego z jej servletów wzbudzenie MDB (wysłanie komunikatu, bądź poprzez interfejs usługi sieciowej - SEI), który wywoła utworzBudzik. Aplikacja EJB (z MDB) musiałaby być dystrybuowana wraz z WARem jako EAR.

    Jacek

    OdpowiedzUsuń