Archive for Grudzień, 2008


mod_cluster — nowe spojrzenie na load balancery

Dostępne rozwiązania równoważenia obciążenia dla aplikacji napisanych w Javie praktycznie ograniczają się do kilku modułów do serwera HTTP Apache. Nie, nie twierdzę, że rozwiązanie jest złe! Powiedziałbym wręcz przeciwnie — serwer ten ma ugruntowaną pozycję na rynku, jest stabilny, a co najważniejsze — jest darmowy! Są oczywiście rozwiązania sprzętowe (np. urządzenia oferowane przez loadbalancer.org), jednak, co tu dużo mówić — ich cena jest dosyć zniechęcająca…

Jak to było do tej pory?

Podstawowym modułem serwera Apache obsługującym połączenia na linii Apache — serwer aplikacji był (a w zasadzie nadal jest) moduł mod_proxy (wraz z modułami zależnymi przeznaczonymi dla konkretnego protokołu np. AJP, HTTP). Jego konfiguracja jest naprawdę bardzo prosta. Mając raz utworzony plik konfiguracyjny można go wykorzystać w innych instalacjach zmieniając tylko potrzebne opcje. Dodatkowo, jeżeli skonfiguruje się moduł mod_proxy_balancer (domyślny sposób równoważenia obciążenia w serwerze Apache — pisałem o tym  wcześniej), otrzymujemy możliwość zdefiniowania wielu serwerów aplikacji, jak i zasad równoważenia obciążenia między nimi. Po szczegóły odsyłam do (krótkiej, ale konkretnej) dokumentacji. Dodatkowo w Internecie można znaleźć tony instrukcji w jaki sposób uruchomić load balancer na podstawie serwera Apache na wybranej dystrybucji Linuksa (uruchamia to ktoś na Windowsie?!). Rozwiązanie oparte na mod_proxy i mod_proxy_balancer jest naprawdę dobre, ale głupie. Moduły nie wiedzą nic o stanie serwerów aplikacji poza tym, czy działają :) Na ogół takie postawienie sytuacji powinno wystarczyć. Jednak w przypadku gdybyśmy chcieli obciążać tylko jeden serwer, a drugi traktować jako dodatkową moc obliczeniową wykorzystywaną tylko i wyłącznie w przypadku, gdy pierwszy serwer nie jest w stanie obsłużyć wszystkich zapytań, rozwiązanie oparte na mod_proxy_balancer nie będzie pomocne.

Z pomocą nadchodzi mod_cluster

Od początku listopada tego roku na stronach społeczności JBossa udostępniony został projekt pod kierownictwem Jean-Frederic Clerea o dumnej nazwie mod_cluster. Oprócz niego w projekcie uczestniczą Brian Stansberry oraz Paul Ferraro.  Zadaniem mod_cluster, zupełnie tak jak w przypadku mod_proxy, jest przekierowywanie zapytań z serwera Apache do serwerów aplikacji. W takim razie, czym różnią się te rozwiązania? W odróżnieniu od mod_proxy, w przypadku mod_cluster komunikacja jest dwukierunkowa. Serwer aplikacji dodatkowo nawiązuje połączenie z serwerem Apache. Właśnie to połączenie odpowiada za inteligencję tego rozwiązania. Za jego pomocą przekazywane są informacje z cyklem życia aplikacji, jak również inne informacje potrzebne do zarządzania load balancerem (np. aktualne obciążenie serwera aplikacji). Informacje przesyłane są za pomocą protokołu Mod-Cluster Management Protocol (MCMP), który w rzeczywistości jest zbiorem metod protokołu HTTP. Co ciekawe — nie jest wymagane utworzenie nowego gniazda dla tych połączeń — serwer aplikacji komunikuje się z serwerem Apache na porcie, na którym jest on skonfigurowany! W rzeczywistości mod_cluster nie jest pojedynczym modułem. Jest to raczej zbiór następujących modułów: mod_slotmem, mod_manager, mod_proxy_cluster oraz mod_advertise. Duża część kodu źródłowego jest oparta na aktualnych implementacjach mod_proxy. Co ciekawe, to konfiguracja po stronie serwera aplikacji odpowiedzialna jest za mapowanie odpowiednich adresów do aplikacji. Szczególny wpływ ma na to konfiguracja domeny i ścieżki danej aplikacji na serwerze aplikacji. Dane te są przesyłane za pomocą wiadomości CONFIG do wszystkich load balancerów.  Jeżeli przychodzące do serwera Apache zapytanie dla wybranej domeny i ścieżki zostanie odnalezione wśród wpisów posiadanych przez moduł mod_cluster, zapytanie to zostanie automatycznie przekierowane do wybranego węzła.

Wymagania

Aby móc uruchomić mod_cluster potrzebny jest serwer Apache w wersji powyżej 2.2.8. Niestety aktualna wersja httpd dostępna dla systemu Red Hat / CentOS 5.2 oznaczona jest numerem 2.2.3. Pozostaje skompilować samemu odpowiednie moduły z dostępnego kodu źródłowego, gdy chcemy wykorzystać standardową instalację demona httpd. Należy zwrócić uwagę, że deweloperzy przygotowali odpowiednie paczki dla wielu systemów operacyjnych zawierające całą potrzebną instalację (łącznie z serwerem Apache!). Wszystko jest kompletne, skonfigurowane i gotowe do użytku! Można to lepiej zrobić?

Do uruchomienia mod_cluster potrzebny jest również odpowiedni kontener. W grę wchodzi JBoss Application Server 5.0.0+, Apache Tomcat, oraz JBoss Web 2.1.1+. Niestety nie doszukałem się informacji z jakimi wersjami Apache działa mod_cluster.

Najważniejsze cechy mod_cluster

Rozwiązanie JBossa wprowadza wiele usprawnień wpływających na konfigurację i zarządzanie klastrem. Oto niektóre z nich.

Dynamiczna konfiguracja workerów (a po polsku?)

Do tej pory to load balancer musiał wiedzieć jakie serwery aplikacji wchodzą w skład klastra. Powodowało to potrzebę rekonfiguracji load balancerów przy dodawaniu nowego węzła w klastrze. Od tej pory, przy użyci mod_cluster, to serwery aplikacji będą posiadały informację o dostępnych load balancerach. Informacja ta może być umieszczona na stałe w konfiguracji serwerów aplikacji jako lista lub mogą one zostać wykryte automatycznie przez serwer aplikacji przy użyciu muticastu za pośrednictwem mechanizmu advertise. W środowiskach, w których nie można użyć multicastu (np. VMware) należy skonfigurować ręcznie listę dostępnych load balancerów.

Współczynnik obciążenia obliczany po stronie serwerów aplikacyjnych

Współczynniki rozkładu obciążenia w przypadku mod_proxy_balancer (jak i w starym mod_jk) były umieszczane na stałe w konfiguracji tych modułów. Statyczna konfiguracja mogła doprowadzić do przeciążenia jednego serwera, gdy inne w klastrze nie były obciążone. Oczywiście zależało to od poprawnego zdefiniowania współczynników. W przypadku mod_cluster problem ten został wyeliminowany. Współczynniki są obliczane po stronie serwerów aplikacji a następnie przekazywane do load balancera, który znając współczynniki wszystkich węzłów w klastrze może efektywnie zarządzać zapytaniami w zależności od obciążenia serwerów.

Należy dodać, że rozwiązanie to sugeruje się informacjami otrzymanymi od serwerów — po stronie serwera http nie ma obliczania współczynników!

Pełna kontrola cyklu życia aplikacji

Rozwiązania dostępne obecnie na rynku w postaci modułów serwera Apache nie potrafią poprawnie obsłużyć takich zdarzeń jak wdrożenie, czy usunięcie aplikacji z serwera. Odwołanie się do usuniętej z serwera aplikacji powoduje po prostu wyświetlenie błędu 404. W przypadku mod_cluster błędy te zostały wyeliminowane, ponieważ informacje o cyklu życia aplikacji są przesyłane do load balancera.

Architektura mod_cluster

Architekturę mod_cluster można podzielić na dwie części. Z jednej strony znajdują się usługi uruchomione na serwerze http, z drugiej strony usługi uruchomione na serwerze aplikacji. Po stronie serwera http uruchomiony jest zestaw modułów wspomnianych wcześniej, natomiast po stronie serwera aplikacji uruchomiona jest usługa ModClusterService. Usługa ta odpowiedzialna jest za komunikację z dostępnymi load balancerami. W szczególności dotyczy to informowania ich o stanie serwera i aplikacji na nim uruchomionych. Szczegółowe informacje można znaleźć na stronach wiki projektu.

Wyróżnić można dwa tryby pracy klastra: z klastrowaniem lub bez.

Konfiguracja bez klastrowania

W tym trybie serwery aplikacji nie wymieniają danych między sobą.

Konfiguracja bez klastrowania

Konfiguracja bez klastrowania

Oznacza to, że dwie, te same, aplikacje uruchomione na dwóch serwerach nie będą wiedziały o swoim istnieniu, oraz żadne dane nie będą replikowane do innych serwerów. Klient, po awarii jednego z serwerów i przełączeniu do innego, straci wszystkie dane znajdujące się aktualnie sesji, będzie się musiał również ponownie zalogować. Oczywiście przesyłane są informacje do serwera http odpowiedzialne za rejestrację serwera apliakcji w load balancerze, czy związane z cyklem życia aplikacji. Natomiast współczynnik obciążenia przesyłany przez serwer aplikacji będzie miał wartość obliczaną (lub ustalaną odgórnie) tylko dla tego konkretnego serwera. Współczynnik ten będzie wpływał na działanie całego systemu tak samo jak wartość lbfactor w pliku konfiguracyjnym workers.properties modułu mod_jk.

Konfiguracja z klastrowaniem

W tym trybie serwery aplikacji formułują klaster, a dane są wymieniane między węzłami w klastrze.

Konfiguracja z klastrowaniem

Konfiguracja z klastrowaniem

Wymieniane są również informacje o aktualnym stanie serwerów pomiędzy instancjami. Jedna z usług ModClusterService będzie zawierała komponent HASingleton odpowiedzialny za zbieranie informacji o stanie wszystkich węzłów. Następnie dane te są poddawane analizie, a wyniki w postaci czynnika obciążenia dostarczane są wszystkim węzłom w klastrze oraz dostępnym load balancerom.

Instalacja

Należy skonfigurować odpowiednio serwer Apache i aplikacji. Bardzo dobra dokumentacja instalacji znajduje się na stronach mod_cluster, zarówno jeżeli chodzi o konfigurację serwera Apache jak i serwera aplikacji.

Podsumowanie

Pewnie parę osób zarzuci mi, że jestem przesadnie podekscytowany tym rozwiązaniem. Być może, ale nie ulega żadnej wątpliwości, że mod_cluster zmieni całkowicie podejście do programowych load balancerów. Polecam przyjrzenie się temu projektowi! Według mapy wydań możemy się spodziewać w najbliższym czasie pierwszego, stabilnego wydania mod_cluster, a więc nie ma na co czekać, tylko uruchamiać. W końcu każdy chce mieć stabilny i szybki system. Co? Może nie?

Warto odwiedzić

Migracja aplikacji z JBoss AS 4.2.3 do JBoss AS 5

Do celów edukacyjnych posiadam napisaną aplikację we frameworku JBoss Seam. Niedawno pisałem, że ukazała się nowa wersja serwera aplikacji JBoss, dlatego też postanowiłem spróbować uruchomić mój poligon doświadczalny na nowym serwerze. Oto konfiguracja mojej aplikacji:

  • EJB 3.0, JavaBeans, JPA, JSF
  • JBoss Seam 2.1.0.SP1
  • RichFaces 3.2.2.GA
  • Hibernate 3.2.4.sp1 (dialekt PostgreSQL)

Używane przeze mnie IDE to Eclipse Ganymede, wraz z zestawem wtyczek JBoss Tools.  Połączenie to stanowi bardzo dobre środowisko do tworzenia aplikacji we frameworku JBoss Seam. Aplikacje uruchamiam na gorąco na serwerze JBoss AS za pomocą skryptu Anta — nowe pliki są kompilowane i kopiowane do katalogu deploy serwera JBoss. Na tym etapie nie przewiduję żadnych zmian w konfiguracji.

Po podmianie ścieżki do nowego serwera spróbowałem uruchomić aplikację  (a nóż widelec…). Jak można było łatwo przewidzieć — nie uruchomiła się.

Moja aplikacja używa cacheowania encji. Okazało się, że w tym temacie zaszły duże zmiany w nowej wersji serwera. Na szczęście dostępna jest bardzo dobra dokumentacja, która pozwoliła mi się wstępnie rozeznać w temacie. Od wersji 5.0 pamięcią podręczną w serwerze JBoss AS zarządza nowa usługa — CacheManager. Na stronie z dokumentacją nowej usługi znajduje się szczegółowa informacja w jaki sposób skonfigurować aplikację — okazało się to bardzo pomocne :)

Dokonałem zmian zgodnych z sekcją Usage with JPA and Hibernate Entity Clustering dokumentu i uruchomiłem ponownie aplikację. Posunąłem się tylko o mały krok naprzód, ponieważ JBoss przywitał mnie wyjątkiem:

20:03:35,416 ERROR [AbstractKernelController] Error installing to Start: name=persistence.unit:unitName=system.ear/system.jar#system state=Create
javax.persistence.PersistenceException: [PersistenceUnit: system] Unable to build EntityManagerFactory
	at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:677)
	at org.hibernate.ejb.HibernatePersistence.createContainerEntityManagerFactory(HibernatePersistence.java:132)
	at org.jboss.jpa.deployment.PersistenceUnitDeployment.start(PersistenceUnitDeployment.java:311)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
	at java.lang.reflect.Method.invoke(Method.java:597)
	at org.jboss.reflect.plugins.introspection.ReflectionUtils.invoke(ReflectionUtils.java:59)
	at org.jboss.reflect.plugins.introspection.ReflectMethodInfoImpl.invoke(ReflectMethodInfoImpl.java:150)
	at org.jboss.joinpoint.plugins.BasicMethodJoinPoint.dispatch(BasicMethodJoinPoint.java:66)
	at org.jboss.kernel.plugins.dependency.KernelControllerContextAction$JoinpointDispatchWrapper.execute(KernelControllerContextAction.java:241)
	at org.jboss.kernel.plugins.dependency.ExecutionWrapper.execute(ExecutionWrapper.java:47)
	at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.dispatchExecutionWrapper(KernelControllerContextAction.java:109)
	at org.jboss.kernel.plugins.dependency.KernelControllerContextAction.dispatchJoinPoint(KernelControllerContextAction.java:70)
	at org.jboss.kernel.plugins.dependency.LifecycleAction.installActionInternal(LifecycleAction.java:221)
	at org.jboss.kernel.plugins.dependency.InstallsAwareAction.installAction(InstallsAwareAction.java:54)
	at org.jboss.kernel.plugins.dependency.InstallsAwareAction.installAction(InstallsAwareAction.java:42)
	at org.jboss.dependency.plugins.action.SimpleControllerContextAction.simpleInstallAction(SimpleControllerContextAction.java:62)
	at org.jboss.dependency.plugins.action.AccessControllerContextAction.install(AccessControllerContextAction.java:71)
	at org.jboss.dependency.plugins.AbstractControllerContextActions.install(AbstractControllerContextActions.java:51)
	at org.jboss.dependency.plugins.AbstractControllerContext.install(AbstractControllerContext.java:348)
	at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:1598)
	at org.jboss.dependency.plugins.AbstractController.incrementState(AbstractController.java:934)
	at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:1062)
	at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:984)
	at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:774)
	at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:540)
	at org.jboss.deployers.vfs.deployer.kernel.BeanMetaDataDeployer.deploy(BeanMetaDataDeployer.java:121)
	at org.jboss.deployers.vfs.deployer.kernel.BeanMetaDataDeployer.deploy(BeanMetaDataDeployer.java:51)
	at org.jboss.deployers.spi.deployer.helpers.AbstractSimpleRealDeployer.internalDeploy(AbstractSimpleRealDeployer.java:62)
	at org.jboss.deployers.spi.deployer.helpers.AbstractRealDeployer.deploy(AbstractRealDeployer.java:50)
	at org.jboss.deployers.plugins.deployers.DeployerWrapper.deploy(DeployerWrapper.java:171)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doDeploy(DeployersImpl.java:1439)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1157)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1178)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.doInstallParentFirst(DeployersImpl.java:1210)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.install(DeployersImpl.java:1098)
	at org.jboss.dependency.plugins.AbstractControllerContext.install(AbstractControllerContext.java:348)
	at org.jboss.dependency.plugins.AbstractController.install(AbstractController.java:1598)
	at org.jboss.dependency.plugins.AbstractController.incrementState(AbstractController.java:934)
	at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:1062)
	at org.jboss.dependency.plugins.AbstractController.resolveContexts(AbstractController.java:984)
	at org.jboss.dependency.plugins.AbstractController.change(AbstractController.java:822)
	at org.jboss.dependency.plugins.AbstractController.change(AbstractController.java:553)
	at org.jboss.deployers.plugins.deployers.DeployersImpl.process(DeployersImpl.java:781)
	at org.jboss.deployers.plugins.main.MainDeployerImpl.process(MainDeployerImpl.java:545)
	at org.jboss.system.server.profileservice.ProfileServiceBootstrap.loadProfile(ProfileServiceBootstrap.java:304)
	at org.jboss.system.server.profileservice.ProfileServiceBootstrap.start(ProfileServiceBootstrap.java:205)
	at org.jboss.bootstrap.AbstractServerImpl.start(AbstractServerImpl.java:405)
	at org.jboss.Main.boot(Main.java:209)
	at org.jboss.Main$1.run(Main.java:547)
	at java.lang.Thread.run(Thread.java:619)
Caused by: org.hibernate.cache.CacheException: unsupported access type [nonstrict-read-write]
	at org.hibernate.cache.jbc2.entity.EntityRegionImpl.buildAccessStrategy(EntityRegionImpl.java:66)
	at org.hibernate.impl.SessionFactoryImpl.<init>(SessionFactoryImpl.java:256)
	at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1327)
	at org.hibernate.cfg.AnnotationConfiguration.buildSessionFactory(AnnotationConfiguration.java:867)
	at org.hibernate.ejb.Ejb3Configuration.buildEntityManagerFactory(Ejb3Configuration.java:669)
	... 51 more

Moje encje wyglądają mniej więcej tak:

@Name("rolePermission")
@Entity
@Table(name = "role_permissions")
@Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@SequenceGenerator(name = "role_permission_seq", sequenceName = "role_permission_seq")
public class RolePermission implements Serializable, SystemEntity {

	...

}

Zgłupiałem. Czyżby ta strategia dostępu do encji nie była wspierana? Przecież w JBoss AS 4.2.x była! Kod źródłowy klasy org.hibernate.cache.jbc2.entity.EntityRegionImpl okazał się pomocny.

    public EntityRegionAccessStrategy buildAccessStrategy(AccessType accessType) throws CacheException {
        if (AccessType.READ_ONLY.equals(accessType)) {
            return optimistic ? new OptimisticReadOnlyAccess(this) : new ReadOnlyAccess(this);
        }
        if (AccessType.TRANSACTIONAL.equals(accessType)) {
            return optimistic ? new OptimisticTransactionalAccess(this) : new TransactionalAccess(this);
        }

        // todo : add support for READ_WRITE ( + NONSTRICT_READ_WRITE ??? )

        throw new CacheException("unsupported access type [" + accessType.getName() + "]");
    }

Ten TODO wyjaśnia wszystko…

Atrybut usage adnotacji @org.hibernate.annotations.Cache ustala jeden z pięciu typów strategii użycia pamięci podręcznej:

  1. NONE — pamięć nie jest używana,
  2. READ_ONLY — aplikacja czyta obiekty, ale nigdy ich nie modyfikuje,
  3. READ_WRITE — dane mogą być odczytywane jak i modyfikowane,
  4. NONSTRICT_READ_WRITE — dane są głównie odczytywane, modyfikacje mogą wystąpić, ale są rzadkie,
  5. TRANSACTIONAL — używany przy providerach w pełni wspierających transakcje.

Do tej pory używałem CacheConcurrencyStrategy.NONSTRICT_READ_WRITE z uwagi na wydajność. Wygląda na to, że trzeba się pożegnać z tym trybem na rzecz CacheConcurrencyStrategy.TRANSACTIONAL.

Kolejna próba uruchomienia aplikacji zakończyła się sukcesem, jeżeli można tak powiedzieć. Po drodze musiałem poprawić jeden komponent. Przy okzji hot deploy’u nowej wersji odkryłem błąd — aplikacja uruchamia się, po czym chwilę później zamyka się — bez jakichkolwiek błędów, czy ostrzeżeń!

20:43:38,856 INFO  [SeamFilter] Initializing filter: org.jboss.seam.web.identityFilter
20:43:38,864 INFO  [[/system]] org.tuckey.web.filters.urlrewrite.UrlRewriteFilter INFO: conf reload check performed each request
20:43:38,923 INFO  [[/system]] org.tuckey.web.filters.urlrewrite.UrlRewriteFilter INFO: loaded (conf ok)
20:43:44,104 INFO  [TomcatDeployment] undeploy, ctxPath=/system, vfsUrl=system.ear/system.war
20:43:44,108 INFO  [[/system]] org.tuckey.web.filters.urlrewrite.UrlRewriteFilter INFO: destroy called

Bardzo irytujące — nie da się przez to pracować, ponieważ deploy trwa w takim przypadku ok. 10 minut (sic!). Muszę poczytać o hot deploy’u w nowej wersji, bo to powinno działać! Kupiłem sobie książkę JBoss in Action — będzie dobry pretekst aby ją przeczytać :)  Tymczasem wracam do starego, dobrego JBoss AS 4.2.3.

Wrażenia po 4. spotkaniu Silesia JUG – Kryptografia w Javie

Dwa dni temu (10.12.2008) miało miejsce już 4. spotkanie Silesia JUG! Gospodarzem prelekcji był tym razem Zacheusz Siedlecki, który przybliżył nam (na tyle, na ile się dało – w związku z nieubłaganym czasem) zagadnienia z dziedziny kryptografii w Javie.

Temat prostym nie jest – ilość zagadnień, które są z nim związane może przytłoczyć niejednego prelegenta. Zacheusz założył (i słusznie!), że sens wdawania się w szczegóły będzie mijał się z celem. W efekcie końcowym skupił się na pokazaniu mnóstwa przykładów (liczył to ktoś?!) wykorzystania kryptografii.

Oczywiście najciekawszym dla mnie był ten ostatni – na temat autentyfikacji klientów za pomocą certyfikatów w aplikacjach internetowych. Otrzymałem wskazówki w jaki sposób zaimplementować autentyfikację klienta, gdzie wymogiem jest, aby aplikacja pozwalała użytkownikowi wybrać z dwóch metod autentyfikacji: za pomocą karty z certyfikatem oraz zwykłego logowania za pomocą nazwy użytkownika i hasła. Trzeba będzie to spróbować zaimplementować w serwerze JBoss AS :)

Wracając do prezentacji – była kompletna, spójna, mogła się podobać i na pewno się podobała! Chętnie usłyszałbym więcej na ten temat!

Model zdarzeniowy, zdarzenia w JBoss Seam

Na prośbę Jacka postanowiłem napisać kilka słów na temat obsługi zdarzeń w JBoss Seam. Aby jednak można się było zająć się konkretnie implementacją tego modelu w JBoss Seamie należy dowiedzieć się w pierwszej kolejności do czego służą i jak z nich korzystać.

  • Czy komponenty aby się ze sobą komunikować zawsze muszą być ze sobą powiązane?
  • Zazwyczaj tak.
  • Zazwyczaj?! A co, nie muszą?!
  • Nie, nie muszą.

Czasami chcemy, aby wykonało się coś w momencie, gdy tylko nastąpi określona okoliczność. Może zbyt ogólnie – dla przykładu wyobraźmy sobie aplikację służącą do blogowania – coś a’la WordPress. Możemy być zainteresowani wyczyszczeniem pamięci podręcznej zawierającej listę ostatnich postów na naszym blogu po dodaniu nowego wpisu. Jak to można wykonać? Na pierwszy rzut oka – oczywistym jest przywołanie (DI) odpowiedniego komponentu odpowiedzialnego za zarządzanie pamięcią podręczną i wywołanie na nim metody clearLastPostsPageFragment(). Jest to poprawne rozwiązanie. Ale co w przypadku, gdy po umieszczeniu nowego posta chcemy dodatkowo wysłać (automatycznie) maila do osób subskrybujących bloga, oraz wrzucić nowy wpis na Twitterze (ktoś tego używa w Polsce?!) informujący o nowym poście. Można oczywiście wstrzyknąć odpowiednie komponenty (załóżmy: MailService i TwitterService) i wywołać odpowiednie akcje.

Należy się zastanowić, czy zaśmiecanie komponentu odpowiedzialnego za zapisanie posta jest konieczne. Jego zadaniem powinno być zapisanie nowego wpisu i co najwyżej poinformowanie zainteresowanych komponentów o tym facie. Czy komponent musi wiedzieć kto jest w takim razie zainteresowany? Nie! Komponenty chcące być informowane o zdarzeniu muszą „przypisać” się do tego zdarzenia. Różne implementacje tego modelu robią to na różne sposoby.

Zdarzenia w Swingu

Model zdarzeniowy bardzo często wykorzystywany jest w warstwie prezentacji. Jako przykład posłużyć może biblioteka Swing służąca do tworzenia aplikacji okienkowych w Javie – weźmy ją na warsztat:

JButton button = new JButton("Klik");

button.addActionListener(new ActionListener() {

	@Override
	public void actionPerformed(ActionEvent e) {
		System.out.println("Przycisk został naciśnięty");
	}

});

Taka architektura jest typowym przykładem wzorca projektowego Observer opartego na zdarzeniach. JButton jest obserwowany, a implementacja interfejsu java.awt.event.ActionListener jest obserwatorem zmian.

Diagram sekwencji

Diagram sekwencji

Gdy zostanie wykonana akcja (w tym konkretnym przypadku – przyciśnięcie przycisku) – button powiadamia o tym wszystkich obserwatorów (listenerów). Listener jest odpowiedzialny za obsługę akcji. Proszę zauważyć, że klasa Button musi zapamiętać kto chce zostać powiadomiony o przyciśnięciu (linia 3)! Przedstawione zostało to na diagramie sekwencji*.

Oczywiście możemy stworzyć własną klasę implementującą interfejs ActionListener, która będzie odpowiedzialna za obsługę zdarzeń – w przykładzie chciałem ograniczyć ilość kodu.

Należy zwrócić uwagę na fakt, że producent zdarzenia nie jest zainteresowany akcjami, które mają zostaną wykonane po wywołaniu zdarzenia, co więcej, producent nie wie nawet jakie akcje mają zostać wykonane, nie interesuje go to. Jego zadaniem było powiadomienie wszystkich zainteresowanych o fakcie wystąpienia określonych okoliczności.

Dlaczego warto używać zdarzeń?

Model zdarzeniowy pozwala rozdzielić obserwatora od obserwowanego, jeden komponent od drugiego. Na który artykuł czy prezentację Gavina Kinga (twórca Hibernate’a i JBoss Seam’a) bym nie spojrzał mówi: „loose coupling, strong typing!” (oddzielenie producentów od konsumentów zdarzeń to jedna z wielu pozycji na liście Manifestu WebBeans’ów). Do tego dąży Java EE 6 – dlaczego więc nie być trendy? :)

Trend nie należy może do najbardziej przekonywujących argumentów. Chcesz usłyszeć więcej? Czytaj dalej!

Rozdzielenie komponentów pozwala w łatwy sposób na testowanie jednostkowe! Nie mamy zależności, a to oznacza, że nie musimy mockować niepotrzebnie komponentów – skupiamy się tylko i wyłącznie na testowaniu czystego kodu związanego z konkretną akcją (np. zapisywanie posta).

Kolejnym argumentem jest fakt, że w przypadku chęci dodania nowej akcji, która miałaby się wykonywać (w naszym przypadku – po zapisaniu posta) wystarczy tylko ją przypisać do odpowiedniego zdarzenia jako obserwator – bez potrzeby zmiany kodu akcji zapisywania wpisu!

Zdarzenia w JBoss Seam

Aplikacje okienkowe mają to do siebie, że nie mają kontekstów o różnych długościach. Można powiedzieć, że jedynym kontekstem jest kontekst aplikacji – obiekt raz utworzony może „dotrwać” aż do zamknięcia aplikacji. W aplikacjach internetowych mamy do czynienia, standardowo, z trzema kontekstami: żądania, sesji i aplikacji. JBoss Seam rozszerza tę listę do 7 kontekstów:

  • stateless,
  • event (request),
  • page,
  • conversation,
  • session,
  • business process,
  • application.

Mając na uwadze fakt, że możemy mieć obiekty porozrzucane po kontekstach – nasuwa się pytanie – w jakim kontekście zostanie wywołane w takim razie zdarzenie? Odpowiedź jest prostsza niż może się wydawać – zdarzenia wywoływane są w kontekście komponentu, który jest odpowiedzialny za wywołanie akcji, czyli w kontekście producenta.

We frameworku JBoss Seam mamy do czynienia z kilkoma rodzajami zdarzeń:

  • zdarzenia JSF:
    <h:commandButton value="Click me!" action="#{helloWorld.sayHello}"/>
  • zdarzenia przejść jBPM – przejście procesów biznesowych – inny, długi temat…,
  • zdarzenia powiązane ze stronami:
    <pages>
    	<page view-id="/hello/*" action="#{helloWorld.sayHello}"/>
    </pages>
  • zdarzenia komponentowe – te interesują nas szczególnie, na nich się skupię,
  • zdarzenia kontekstowe – wbudowane zdarzenia wywoływane automatycznie przez Seama. Pełną listę można zobaczyć na stronie z dokumentacją Seam’a.

Zdarzenia komponentowe

Zdarzenie komponentowe (dalej: zdarzenie) identyfikowane są za pomocą ciągu znaków. Oznacza to, że każde zdarzenie musi posiadać swoją nazwę. I tyle – nic więcej nie jest potrzebne :)

Podczas wywoływania zdarzeń kontekst wywołania jest propagowany do obserwatora, oznacza to, że obserwator będzie miał dostęp do wszystkich komponentów, do których ma dostęp producent akcji! Dotyczy to w szczególności możliwości wstrzykiwania, czy pobierania instancji komponentu za pomocą Component.getInstance()! Co to oznacza dla developera? Praktycznie wyeliminowana została potrzeba przekazywania jakichkolwiek obiektów pośredniczących między producentem a konsumentem zdarzenia.

Zdarzenie w JBoss Seam można wywołać na kilka sposobów:

  1. za pomocą XML’a,
  2. za pomocą komponentu org.jboss.seam.core.Events,
  3. za pomocą adnotacji.

We wszystkich rodzajach wywołań wykorzystujemy jako parametr – nazwę wybranego zdarzenia. Czas na przykłady!

Wywoływanie zdarzeń za pomocą XML’a

Akcje można wywoływać bezpośrednio w pliku pages.xml za pomocą elementu <raise-event />:

<page view-id="/home.xhtml" login-required="true">
	<rewrite pattern="/home/{cid}" />
	<rewrite pattern="/home" />

	<raise -event type="sayHello" />
</page>

Jedynym wymaganym argumentem jest type będący nazwą zdarzenia, które ma zostać wywołane.

Wywoływanie zdarzeń za pomocą komponentu Events

Jest to chyba najpopularniejszy sposób na wywołanie zdarzeń w JBoss Seam. Są one wywoływane bezpośrednio w kodzie za pomocą komponentu przeznaczonego do obsługi zdarzeń – org.jboss.seam.core.Events. Zawsze należy się odwoływać do niego za pomocą konstrukcji: Events.instance() (ewentualnie można go wstrzyknąć, ale nie widziałem jeszcze żeby ktoś tego używał w ten sposób).

Komponent Events zawiera kilka bardzo ważnych metod. Wszystkie z nich przedstawione zostały na poniższym listingu:

Events.instance().raiseEvent(String type, Object... parameters)
Events.instance().raiseAsynchronousEvent(String type, Object... parameters)
Events.instance().raiseTimedEvent(String type, Schedule schedule, Object... parameters)
Events.instance().raiseTransactionCompletionEvent(String type, Object... parameters)
Events.instance().raiseTransactionSuccessEvent(String type, Object... parameters)
Events.instance().addListener(String type, String methodBindingExpression, Class... argTypes)

Pierwsze dwie metody są najważniejsze – reszty używa się w specyficznych sytuacjach. Różnica między pierwszymi dwoma jest taka, że raiseEvent blokuje wątek do momentu wykonania akcji, a raiseAsynchronousEvent, jak sama nazwa wskazuje – wykonuje się asynchronicznie nie blokując wątku.

Wywoływanie zdarzeń za pomocą adnotacji

Do wywoływania zdarzeń może posłużyć również adnotacja @org.jboss.seam.annotations.RaiseEvent.

@Target(METHOD)
@Retention(RUNTIME)
@Documented
public @interface RaiseEvent
{

   String[] value() default {};

}

Używa się jej w następujący sposób (zakładając, że chcemy wywołać zdarzenie „System.Property.Modified”):

@RaiseEvent("System.Property.Modified")
private void saveProperty(PropertyType type, String value) {
	Property p = settingsList.getProperty(type);

	if (p == null)
		p = new Property(type);

	p.setValue(value);

	em.merge(p);
}

W przypadku adnotacji może nasunąć się pytanie – kiedy nastąpi wywołanie zdarzenia? Zdarzenie zostanie wywołane po zakończeniu metody, gdy zwróci ona wartość różną od null oraz gdy nie wystąpi wyjątek w czasie wykonywania metody.

Możemy pominąć specyfikowanie nazwy zdarzenia które ma się wywołać – wtedy domyślnie zostanie wywołane zdarzenie o nazwie równoznacznej do nazwy metody – w naszym przypadku będzie to „saveProperty”.

Aby wywołać więcej niż jedno zdarzenie należy podać nazwy wszystkich zdarzeń po przecinku.

@RaiseEvent({"System.Property.Modified", "ZdarzenieA", "ZdarzenieB"})

Obsługa zdarzeń

Wiemy już jak możemy wywoływać zdarzenia, ale do tej pory nie wspomniałem nic o obsłudze zdarzeń.

Aby przypisać się do wybranego zdarzenia należy oznaczyć adnotacją @org.jboss.seam.annotations.Observer metodę, która ma zostać wykonana po wywołaniu zdarzenia.

@Observer( { "System.Permission.Removed", "System.Permission.Updated" })
public void onSecurityUpdate() {

	permissions.clear();
}

Tak jak, jak i w przypadku adnotacji @RaiseEvent można wyspecyfikować więcej nazw zdarzeń, których wywołanie ma uruchomić adnotowaną metodę.

Oto źródło adnotacji @Observer:

@Target(METHOD)
@Retention(RUNTIME)
@Documented
public @interface Observer
{
   String[] value() default {};

   boolean create() default true;
}

Drugi argument ustala, czy w przypadku gdy komponent zawierający metodę obsługującą zdarzenie nie istnieje, powinien zostać utworzony czy nie. Jeżeli komponent nie istnieje, a create jest ustawiona na false – zdarzenie nie zostanie obsłużone przez ten komponent (ale przez inny może zostać oczywiście obsłużone).

Podsumowanie

Zdarzenia, szczególnie w JBoss Seam’ie, to bardzo proste, a zarazem ciekawe rozwiązanie! Tak jak z wszystkim – używać, ale z głową i nie przesadzać z ilością, chyba że architektura naszej aplikacji jest kompletnie event-driven.

Osobiście używam zdarzeń w wyjątkowych sytuacjach, korzystając głównie z dobrodziejstw wstrzykiwania zależności. Zdarzenia traktuję jako miły dodatek :)

Mam nadzieję, że post ten rozjaśnił trochę temat zdarzeń ogólnie, jak i ich wykorzystania w aplikacjach napisanych w JBoss Seam. Jeżeli jest coś nie jasne – postaram się wytłumaczyć!

A Wy – jak widzicie przydatność zdarzeń? Używacie ich na co dzień? Jakieś sugestie? Piszcie śmiało – warto wymieniać się wiedzą!

*Nie jestem guru jeżeli chodzi o UML. Jeżeli widzisz błąd – powiadom mnie o tym!

JBoss Application Server 5.0 GA do pobrania!

http://www.jboss.org/

źródło: http://www.jboss.org/

Jak dumnie informuje strona główna community JBossa wersja 5 została oznaczona jako gotowa do produkcyjnych zastosowań i jest już dostępna do pobrania!

Nic tylko ściągać i instalować. Lista zmian dostępna jest na stronie z informacjami o wydaniu – na pierwszy rzut oka wygląda pokaźnie. Oprócz poprawek wielu błędów mamy aktualizacje do najnowszych wersji różnych projektów zależnych, poprawki upiększające, czy takie kwiatki jak wstępne przygotowanie do Java EE 6 (web config – konfiguracja zgodna z założeniami Java EE 6 dla profilu web). Przez weekend muszę się bliżej przyjrzeć zmianom.

Ciekawe jak będzie wyglądała integracja z ich flagowym frameworkiem – JBoss Seam? Dorzucić do TODO: przez weekend zmigrować mój poligon doświadczalny z wersji 4.2.3 na 5.