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
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:
- za pomocą XML’a,
- za pomocą komponentu
org.jboss.seam.core.Events,
- 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!