Tag Archive: jboss seam


Wczoraj na 9. już spotkaniu Silesia JUG zaprezentowałem framework JBoss Seam. Muszę powiedzieć, że frekwencja dopisała (na spotkanie przyszło ok. 25 osób, za co jestem wdzięczny). Mam nadzieję, że prezentacja się mogła podobać, możecie swoje uwagi umieścić w ankiecie, do której wypełnienia gorąco zapraszam! Prezentacja dostępna jest na stronie z materiałami Silesia JUG.

Prezentacja trwała dość długo (trochę ponad 1.5h), ale jest to naprawdę za mało czasu, aby zaprezentować cały wachlarz funkcjonalności oferowanych przez JBoss Seama. Jeżeli będzie zainteresowanie ze strony słuchaczy, mogę oczywiście poprowadzić kolejne prelekcje, tym razem już krótsze, ale skupione na konkretnym, mniej obszernym temacie związanym z JBoss Seam. Oczywiście cieszyłbym się, gdyby ktoś mógł mnie wspomóc i zaprezentować wybrany przez siebie temat.

Za niedługo rusza pierwsza w Polsce grupa użytkowników produktów firmy JBoss: Silesia JBoss User Group (o czym będzie więcej w innym poście). Proponuję, aby kolejne tematy związane z Seamem (jak i innymi projektami JBossa) były omawiane na spotkaniach Silesia JBUG będącej SIG Silesia JUG. Wszystkie informacje na temat Silesia JBUG dostępne są na grupie dyskusyjnej, możesz również śledzić mnie na Twitterze.

24.03.2009, o godz. 18:30 odbędzie się kolejne (a właściwie zaległe) spotkanie Silesia JUG. Tym razem przedmiotem prelekcji będzie JBoss Seam. Spotkanie zostanie poprowadzone przeze mnie. Pragnę skupić się na najważniejszych  funkcjach tego frameworka, ale powiem również o “fajnych” funkcjach, których po prostu nie można ominąć.

Wszystkie osoby zachęcam do przyjścia na spotkanie, które odbędzie się w budynku COIGu, w  Katowicach, przy ul. Mikołowskiej 100, sala 136. Wstęp wolny.

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 Seam w swojej najnowszej odsłonie (2.1.0.SP1) w porównaniu z wersją 2.0 posiada bardzo wiele zmian związanych z bezpieczeństwem. Omówione one zostały na przykładzie aplikacji SeamSpace w poście na blogu Shane’a Bryzaka – głównego deweolpera kwestii bezpieczeństwa w JBoss Seam. Osobom zainteresowanym polecam również przestudiować rozdział poświęcony bezpieczeństwu w oficjalnej dokumentacji Seama.

Nowością w Seam 2.1 jest możliwość definiowania uprawnień i przypisywania ich określonym rolom. Jest to rozwiązanie bardzo elastyczne, jak również bezpieczne, a co najważniejsze – wygodne! Postanowiłem sprawdzić czy sprawdza się to równiez w praktyce.

W swojej aplikacji postawiłem wykorzystać JpaPermissionStore, jak również JpaIdentityStore aby móc zapisywać uprawnienia i użytkowników w relacyjnej bazie danych. Po odpowiedniej konfiguracji (więcej info w poście na blogu Shane’a) oraz zabezpieczeniu odpowiednich stron okazało się, że każdorazowo (sic!) gdy jest sprawdzane jakieś uprawnienie – wykonywane jest zapytanie do bazy danych! W przypadku wielokrotnego sprawdzania uprawnień na jednej stronie (a ma to miejsce praktycznie zawsze) dochodziło do wykonywania nawet kilkudziesięciu zapytań – bardzo często takich samych.

Oczywiście trzeba było to zneutralizować. Jak? Nadpisując komponent org.jboss.seam.security.permission.PersistentPermissionResolver w ten sposób:

package pl.atlone.firma.auth;

import java.util.HashMap;
import java.util.Map;

import org.jboss.seam.ScopeType;
import org.jboss.seam.annotations.Install;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.Observer;
import org.jboss.seam.annotations.Scope;
import org.jboss.seam.annotations.Startup;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
import org.jboss.seam.log.Log;
import org.jboss.seam.security.Identity;
import org.jboss.seam.security.permission.PersistentPermissionResolver;

@Scope(ScopeType.APPLICATION)
@BypassInterceptors
@Name("org.jboss.seam.security.persistentPermissionResolver")
@Install(precedence = Install.APPLICATION)
@Startup
public class SystemPermissionResolver extends PersistentPermissionResolver {

	private static final long serialVersionUID = -9063212676529968783L;

	@Logger
	private Log log;

	private Map<string , Map<String, String>> permissions = new HashMap</string><string , Map<String, String>>();

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

	@Override
	public boolean hasPermission(Object target, String action) {

		log.trace("Checking permissions: [identity = "
				+ Identity.instance().getCredentials().getUsername()
				+ "], [target = " + target + "]. [action = " + action + "]");

		Map</string><string , String> identityPermissions = permissions.get(Identity
				.instance().getCredentials().getUsername());

		if (identityPermissions == null) {
			identityPermissions = new HashMap</string><string , String>();
			permissions.put(Identity.instance().getCredentials().getUsername(),
					identityPermissions);
		}

		String permission = identityPermissions.get(target.toString());

		if (permission != null && permission.equals(action)) {
			return true;
		} else {
			boolean hasPermission = super.hasPermission(target, action);

			if (hasPermission) {
				identityPermissions.put(target.toString(), action);
				return true;
			}
		}

		return false;
	}
}

Otrzymujemy w ten sposób komponent pobierający informacje z bazy danych tylko wtedy, gdy żądana informacja nie została już wcześniej pobrana. Komponent bardzo prosty i przyjemny. Należy pamiętać, aby po usunięciu lub uaktualnieniu uprawnienia wywołać odpowiednio zdarzenia System.Permission.Removed lub System.Permission.Removed, co spowoduje wyczyszczenie listy uprawnień znajdujących się w pamięci, a zarazem wymusi załadowanie nowych, uaktualnionych uprawnień.

Aby nie być gołosłownym, zamieszczę krótkie info o problemie, który do niedawana nie pozwalał na uruchomienie testów integracyjnych dla frameworku JBoss Seam używając Javy w wersji 6.0.

Testy integracyjne mają to do siebie, że aby można było je wykonać należy uruchomić kontener (najczęściej serwer Java EE). Uruchomienie całego serwera JBoss do wykonania tylko i wyłącznie testów mijałoby się z celem, ponieważ taka kobyła jak JBoss uruchamia się trochę (konfiguracja default – powiedzmy, że 40 sek.). Z tego powodu deweloperzy Seama postanowili użyć JBoss Embedded jako kontenera do uruchamiania testów integracyjnych. JBoss Embedded to nic innego jak esencja serwera JBoss.

Do tej pory JBoss Embedded można było uruchomić wyłącznie za pomocą Javy w wersji 5. Od dwóch tygodni na stronie FAQ JBoss Seam pojawiła się informacja w jaki sposób uruchmić JBoss Embedded na Javie 6.0.

W skrócie – wystarczy dodać parametr

-Dsun.lang.ClassLoader.allowArraySyntax=true

do wirtualnej maszyny Javy i po krzyku :) Więcej informacji o konfiguracji w stosownym wpisie w FAQ.

Swoją drogą – testy integracyjne wykonuje się bardzo łatwo w Seamie – wystarczy spojrzeć na dokumentację.

Follow

Otrzymuj każdy nowy wpis na swoją skrzynkę e-mail.