25 lutego 2008

"Globalizacja" obiektów Wicketa ze Spring Framework

Mimo tak szumnego tytułu zacznę lekko, od wtyczki Wicket Bench. Podczas mojej pracy z Eclipse i projektem wicketowym całkowicie o niej zapomniałem. Podczas tworzenia klasy-strony PrzedstawSie w mojej demonstracyjnej aplikacji nieoczekiwanie pojawiła się podpowiedź odnośnie możliwości utworzenia odpowiadającej jej strony html.


Nie ukrywam, że mile mnie zaskoczyła nadgorliwość wtyczki do skracania czasu potrzebnego do tworzenia kolejnej strony, a że miałem zabrać się za HTML, więc rychło skorzystałem z pomocy. Jakież było moje zaskoczenie, kiedy moim oczom ukazała się strona następującej treści:
<html xmlns:wicket>
<wicket:panel>

</wicket:panel>
</html>
Pomyślałem, że to z powodu, że nic nie było w samej klasie. Szybko uzupełniłem (oprogramowałem) klasę o potrzebne mi komponenty i po skasowaniu strony HTML, wykonałem generacje strony ponownie. Niestety, ale efekt końcowy nie zmienił się, ani na jotę. Spodziewałem się więcej. Na razie odpuszczam ją sobie, chociaż wciąż w tle próbuje mi pomóc.

Wciąż część mojego czasu poświęcam na lekturę książki Pro Wicket z Manning i wciąż ten sam rozdział 3. Developing a Simple Application. Ten rozdział zacznie mi się niedługo śnić. Po ostatnim moim wpisie odnośnie sesji - Sesja i przekierowanie żądania w Wicket - przyszła pora na wzmiankę o zasięgu aplikacyjnego - przestrzeni obiektów globalnych aplikacji. Krótka wizyta na stronie dokumentacji klasy org.apache.wicket.markup.repeater.data.IDataProvider, a tam oto taki przykład pomocniczy:
class UsersProvider implements IDataProvider {

public Iterator iterator(int first, int count) {
((MyApplication)Application.get()).getUserDao().iterator(first, count);
}

public int size() {
((MyApplication)Application.get()).getUserDao().getCount();
}

public IModel model(Object object) {
return new DetachableUserModel((User)object);
}
}
Pomijając wartość płynącą z IDataProvider przyjrzyjmy się konstrukcji
((MyApplication)Application.get()).getUserDao()
, która zwraca pewne DAO (w tym przypadku UserDao)...globalne swoim zasięgiem. Właśnie w ten sposób realizuje się zasięg application znany z JSF czy JSP/Servlets w wykonaniu Wicket.

Przypominając jak działała sesja w Wicket można zauważyć podobieństwo między nimi w sposobie ich pozyskiwania. I sesję, i aplikację pobieramy za pomocą konstrukcji PewnienBytWicketa.get() z rzutowaniem na właściwy typ. Tak było z sesją - Session.get() i tak jest z aplikacją - Application.get(). Jeśli zatem przyjdzie nam umieścić pewnien byt w przestrzeni obiektów o zasięgu aplikacyjnym (application) wystarczy dostarczyć odpowiednie metody do naszej klasy aplikacyjnej (u mnie będzie to pl.jaceklaskowski.wicket.WicketDemoApplication) i na tym sprawa się kończy. Podobnie jak miało to miejsce przy obiektach sesyjnych, mamy dostarczone silne typowanie za darmo, tzn. w czasie proporcjonalnym do naszego nakładu pracy przy utworzeniu atrybutu zadanego typu.
public class WicketDemoApplication extends WebApplication {

private Logger logger = Logger.getLogger(WicketDemoApplication.class.getName());

private SlowoDao slowoDao;

...

public SlowoDao getSlowoDao() {
return slowoDao;
}

public void setSlowoDao(SlowoDao slowoDao) {
this.slowoDao = slowoDao;
}
}
Nie wiedzieć czemu, kiedykolwiek widzę DAO na myśl przychodzi mi Spring Framework. Skoro już przy nim jestem spojrzenie jak to mógłby on nas wesprzeć w tworzeniu aplikacji z Wicket (a wiem, że może, więc pewnie oczekuję tej integracji tak pewnie). Skok do rozdziału 5. Integration with Other Frameworks w Pro Wicket, gdzie odszukałem niezbędne informacje o integracji Wicket-Spring.

Krok 1. Zmieniamy konfigurację aplikacji webowej celem zarejestrowania obiektu nasłuchującego realizującego integrację między Wicketem a Springiem.
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
oraz dodaję parametr konfiguracyjny dla wicketowego servletu lub (jak w mojej demonstracyjnej aplikacji) filtra.
<init-param>
<param-name>applicationFactoryClassName</param-name>
<param-value>wicket.spring.SpringWebApplicationFactory</param-value>
</init-param>
Dodatkowo usuwam parametr
<init-param>
<param-name>applicationClassName</param-name>
<param-value>pl.jaceklaskowski.wicket.WicketDemoApplication</param-value>
</init-param>
który będzie wskazany przez konfigurację Springa w jego domyślnie poszukiwanym pliku konfiguracyjnym /WEB-INF/applicationContext.xml lub dowolnym innym pliku konfiguracyjnym wskazanym przez parametr konfiguracyjny wicketowego servletu/filtra przez parametr contextConfigLocation.

Ostatecznie kończę zmiany z następującym plikiem /WEB-INF/web.xml:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">
<display-name>wicket-demo</display-name>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<filter>
<filter-name>wicket.wicket-demo</filter-name>
<filter-class>org.apache.wicket.protocol.http.WicketFilter</filter-class>
<init-param>
<param-name>applicationFactoryClassName</param-name>
<param-value>org.apache.wicket.spring.SpringWebApplicationFactory</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>wicket.wicket-demo</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
Krok 2: Utworzenie konfiguracji dla Spring Framework z Wicketem

Krótka wizyta na stronie dokumentacji Spring Framework - Chapter 3. The IoC container i mamy gotowy plik /WEB-INF/applicationContext.xml o następującej treści:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="slowoDao" class="pl.jaceklaskowski.wicket.model.SlowoDao" />
<bean id="wicketApplication" class="pl.jaceklaskowski.wicket.WicketDemoApplication">
<property name="slowoDao" ref="slowoDao" />
</bean>
</beans>
I tutaj olśnienie - spostrzegłem, że do tej pory wszystkie moje encje były zawarte w pakiecie pl.jaceklaskowski.wicket.entities, podczas gdy trafniejszym byłby pl.jaceklaskowski.wicket.model, który nie wskazywałby na technologię, ale na rolę klas w nim zawartych. Zmieniłem i wdrażam do codziennego użycia.

Krok 3. (opcjonalny) Aktualizacja pom.xml dla projektu mavenowego

Kolejny raz dostrzegam zalety stosowania Maven do zarządzania moim projektem, tak że dodanie wymaganych bibliotek Spring Framework sprowadza się do dodania następującej sekcji dependency do pom.xml:
<dependency>
<groupId>org.apache.wicket</groupId>
<artifactId>wicket-spring</artifactId>
<version>${wicket.version}</version>
</dependency>
Wystarczy, więc dodanie zależności org.apache.wicket.wicket-spring i potrzebne zależności, w tym i sam Spring Framework, zostaną pobrane dzięki uprzejmości Mavena.

Parametr ${wicket.version} to już zadanie dla Maven, który rozwiązuje mi ją do wersji 1.3.1 (co, jak i dlaczego w tym temacie było na samym początku moich zmagań z Wicket - Pierwsze kroki z Apache Wicket 1.3).

Podczas uruchomienia tak zmodyfikowanej aplikacji webowej obiekt nasłuchujący uruchomienia aplikacji zarejestrowany w jej deskryptorze - org.springframework.web.context.ContextLoaderListener zainicjuje infrastrukturę Spring Framework. Korzystamy wyłącznie z funkcjonalności IoC Springa, więc ziarna (wskazane przez elementy beans w pliku applicationContext.xml) zostaną zainicjowane i wstrzelone, gdzie wskazano, np. WicketDemoApplication zostanie "zasilone" egzemplarzem SlowoDao poprzez publiczną metodę zapisu setSlowoDao(SlowoDao slowoDao). Następnie podczas inicjowania Wicket (w naszej konfiguracji podczas uruchomienia filtra) nastąpi uruchomienie fabryki klasy aplikacji wskazanej przez parametr applicationFactoryClassName, tj. org.apache.wicket.spring.SpringWebApplicationFactory. Fabryka SpringWebApplicationFactory już wie, czego należy szukać w "przestrzeni" springowej, aby uruchomić aplikację. Już wiadomo, że potrzebna jest klasa rozszerzająca (wprost bądź niewprost) klasę org.apache.wicket.protocol.http.WebApplication, co w tym przypadku jest jednoznacznym wskazaniem na pl.jaceklaskowski.wicket.WicketDemoApplication.

W dokumentacji klasy org.apache.wicket.spring.SpringWebApplicationFactory napisano, że w przypadku wielu aplikacji wicketowych zdefiniowanych w "przestrzeni" Springa można wskazać tę jedną, wybraną przez parametr beanName. Trudno mi teraz to wytłumaczyć, ale natchnęło mnie na przejrzenie źródeł i...strzał w plecy - dokumentacja klasy jest niepoprawna (!) Okazuje się, że należy skorzystać z parametru applicationBean (kolejny raz, kiedy potwierdza się stara dobra maksyma, że warto zaglądać do kodu źródłowego, jeśli się go ma i ma się czas na jego lekturę):
<init-param>
<param-name>applicationBean</param-name>
<param-value>wicketApplication</param-value>
</init-param>
To może skłaniać do pytania, czy nazwa aplikacji wicketowej w applicationContext (poprzez atrybut id) ma znaczenie. Otóż nie. Poszukiwanie tej jednej wybranej aplikacji wicketowej bez określenia jej przez beanName jest realizowane przez odszukanie ziaren (springowych) rozszerzających org.apache.wicket.protocol.http.WebApplication.

I tutaj uwaga. Jeśli ktokolwiek pomyślałby o zadeklarowaniu zależności Spring Framework 2.5.1 w aplikacji opartej o Wicket 1.3.1 może na starcie spodziewać się poniższego komunikatu błędu:
2008-02-24 20:40:23.447::INFO:  jetty-6.1.7
2008-02-24 20:40:23.588::INFO: No Transaction manager found - if your webapp requires one,
please configure one.
2008-02-24 20:40:24.947::WARN:
failed org.mortbay.jetty.plugin.Jetty6PluginWebAppContext@1083717
{/wicket-demo,C:\projs\sandbox\wicket-demo\src\main\webapp}
java.lang.NoSuchMethodError:
org.springframework.core.CollectionFactory.createConcurrentMapIfPossible(I)Ljava/util/Map;
at org.springframework.web.context.ContextLoader.(ContextLoader.java:153)
at org.springframework.web.context.ContextLoaderListener.createContextLoader
(ContextLoaderListener.java:53)
at org.springframework.web.context.ContextLoaderListener.contextInitialized
(ContextLoaderListener.java:44)
at org.mortbay.jetty.handler.ContextHandler.startContext(ContextHandler.java:540)
at org.mortbay.jetty.servlet.Context.startContext(Context.java:135)
at org.mortbay.jetty.webapp.WebAppContext.startContext(WebAppContext.java:1220)
at org.mortbay.jetty.handler.ContextHandler.doStart(ContextHandler.java:510)
at org.mortbay.jetty.webapp.WebAppContext.doStart(WebAppContext.java:448)
at org.mortbay.jetty.plugin.Jetty6PluginWebAppContext.doStart
(Jetty6PluginWebAppContext.java:110)
at org.mortbay.component.AbstractLifeCycle.start(AbstractLifeCycle.java:40)
at org.mortbay.jetty.handler.HandlerCollection.doStart(HandlerCollection.java:152)
Wicketowy org.apache.wicket.wicket-spring.1.3.1 deklaruje już zależność od Spring Framework, więc należy usunąć własną, bo...za nowa. Wersja Spring Framework zadeklarowana jako zależność dla modułu wicket-spring (org.apache.wicket.wicket-parent.1.3.1) to 2.0 i jak widać jest pewna rozbieżność między nimi w publicznym interfejsie Springa, z którego korzysta Wicket.

Pora uruchomić aplikację.
2008-02-24 20:44:18.036::INFO:  jetty-6.1.7
2008-02-24 20:44:18.176::INFO: No Transaction manager found - if your webapp requires one,
please configure one.
INFO - ContextLoader - Root WebApplicationContext: initialization started
2008-02-24 20:44:19.520:/wicket-demo:INFO: Loading Spring root WebApplicationContext
INFO - CollectionFactory - JDK 1.4+ collections available
INFO - XmlBeanDefinitionReader - Loading XML bean definitions from ServletContext resource
[/WEB-INF/applicationContext.xml]
INFO - XmlWebApplicationContext - Bean factory for application context [Root WebApplicationContext]:
org.springframework.beans.factory.support.DefaultListableBeanFactory defining
beans [slowoDao,wicketApplication]; root of BeanFactory hierarchy
INFO - XmlWebApplicationContext - 2 beans defined in application context [Root WebApplicationContext]
INFO - XmlWebApplicationContext - Unable to locate MessageSource with name 'messageSource':
using default [org.springframework.context.support.DelegatingMessageSource@5ead9d]
INFO - XmlWebApplicationContext - Unable to locate ApplicationEventMulticaster with name
'applicationEventMulticaster': using default
[org.springframework.context.event.SimpleApplicationEventMulticaster@1a93f38]
INFO - UiApplicationContextUtils - Unable to locate ThemeSource with name 'themeSource':
using default [org.springframework.ui.context.support.ResourceBundleThemeSource@29d75]
INFO - DefaultListableBeanFactory - Pre-instantiating singletons in factory
[org.springframework.beans.factory.support.DefaultListableBeanFactory defining beans
[slowoDao,wicketApplication]; root of BeanFactory hierarchy]
INFO - ContextLoader - Using context class
[org.springframework.web.context.support.XmlWebApplicationContext] for root WebApplicationContext
INFO - ContextLoader - Root WebApplicationContext: initialization completed in 1391 ms
INFO - Application - [WicketDemoApplication] init: Wicket core library initializer
...
INFO - WebApplication - [WicketDemoApplication] Started Wicket version 1.3.1 in development mode
********************************************************************
*** WARNING: Wicket is running in DEVELOPMENT mode. ***
*** ^^^^^^^^^^^ ***
*** Do NOT deploy to your live server(s) without changing this. ***
*** See Application#getConfigurationType() for more information. ***
********************************************************************
2008-02-24 20:44:21.254::INFO: Started SelectChannelConnector@0.0.0.0:8080
[INFO] Started Jetty Server
Aplikacja wystartowała poprawnie! Sprawdzenie w przeglądarce i po chwili już wiem, że wszystko w porządku (tutaj kolejny raz przydałoby się wsparcie Selenium czy podobnie do wykonania testów za mnie - książka An Introduction to Testing Web Applications with twill and Selenium z OReilly już czeka na mnie, jak i Tomasz Kaczanowski z prezentacją na marcowym spotkaniu grupy Warszawa JUG, na którym mnie jeszcze nie będzie).

Na zakończenie kilka ciekawostek, na które dzisiaj natrafiłem w tak zwanym międzyczasie. W trakcie przeglądania mojego zbioru blogów zacząłem od naszych, lokalnych i najciekawszym wydał mi się wpis Jak nie należy programować w polskim The Daily WTF, w którym kluczowym był fragment:

Tydzień z grubsza zajęło mi doprowadzenie do używalności kodu, który otrzymałem w spadku po pewnym urlopowiczu. Dobrze, że nie mam włosów, bo z pewnością bym je dawno wyrwał, choć z — drugiej strony — na programowaniu cierpi moja broda. Kolega wypoczywa w Iranie, a my próbujemy dojść, co też podmiot liryczny miał na myśli. Kiedy czytanie źródeł dłuży się ponad lekturę Nad Niemnem, znak to, że czas na notkę.

Szczególnie ten moment z "podmiot liryczny" jest na 5+. Dobrze, że nic nie piłem, bo parsknąłem, kiedy na niego trafiłem. Ładnie wyglądałby mój laptop. Gratuluję poczucia humoru nawet w tak trudnych chwilach jak analiza czyjegoś kodu źródłowego! Czy ja dzisiaj nie miałem podobnej eskapady?! Szczęśliwie moja lektura obyła się bez wyrywania włosów (wciąż je mam gotowe do przycinki) i wyrywania brody (tego nie mam i nie miałem).

Przy okazji analizy blogów natrafiłem na wpis JSF vs Wicket, Job Opportunities. Znowu pod temat dzisiejszego mojego wpisu o Wicket (czy oni się przypadkiem nie skrzyknęli? ;-)). Na razie Wicket daleko w tyle, ale czuję, że podobnie jak w moim przypadku, wszystko za sprawą owego ciała standaryzującego Korporacyjną Javę, gdzie JSF jest jednym z kluczowych graczy i w ogóle całego nagłaśniania ważności technologii Java EE (również i przeze mnie). Wielu z nas, nie ma czasu, chęci, wpisz dowolny powód na brak własnego rozwoju technologicznego i kiedy pozna jedną technologię przykuwa się do niej grubym łańcuchem, aby albo ona wyniosła nas na wyżyny intelektualne, albo my ją. Tak, czy owak nie spotykam się często z przeniesieniem swoich uczuć na inne technologie w ramach samej dżawki (i nie piszę tu o migracji do całkowicie innej platformy jak np. .Net). To wydaje mi się głównym powodem dla popularności JSF. Wierzę, że w Polsce już tą kwestię mamy za sobą ;-)

5 komentarzy:

  1. Cześć Jacek,

    Drobny komentarz a propos (oczywiście :-)) Spring-a. A dokładniej wersji. Ta zależność do 2.0 to jest ustawione przez Wicket-a, w jego maven'owym pom.xml. Tzn. Wicket jest skompilowany ze Spring 2.0, a potem w aplikacji dodajesz Spring 2.5. No i classloadery ładują niepoprawną wersję (2.0 zamiast 2.5). Ja zrobiłem szybki teścik i zmieniłem w pom.xml Wicket-a (dokładnie w wicket-parent-1.3.1.pom znajdującym się w folderze \.m2\repository\org\apache\wicket\wicket-parent\1.3.1) wersję zależności z 2.0 na 2.5.1 i wszystko działa OK. Tzn. zarówno start aplikacji wicketowej, wstrzelenie zależności, jak i jej użycie są OK.

    Jeszcze się nie spotkałem z tym, żeby Spring złamał jakiś publiczny interfejs - dla przykładu, zwykle podmiana wersji Spring odbywa się po prostu przez nadgranie jego plików JAR (pomijam OSGi, gdzie to się ładniej dobywa)...

    Swoją drogą, to ten problem, który napotkałeś, to jest bardzo dobry punkt wyjścia do nauki dla kogoś, kto chce rozpracować (czarną) sztukę classloader'ów w Java ;-)...

    Pozdrawiam,
    Waldek Kot

    OdpowiedzUsuń
  2. tak sobie myślę, czy taki automatyzm to dobre rozwiązanie w mavenie?
    z jednej strony wszystko się robi samo, a z drugiej programiści to nie idioci i sami mogli by dodawać wszystkie inne potrzebne zależności, gdyż jak widać trzeba o wiele więcej pracy (linijek XMLa) aby nadpisać zależność na nowszą wersję ;-)

    OdpowiedzUsuń
  3. Chyba jem za dużo masła i dopada mnie skleroza :P
    Nie napisałeś o tym wcześniej, a jestem ciekawy jak wygląda kwestia bezpieczeństwa wątkowego (thread-safe), czy takie (MyApplication)Application.get() nie będzie powodowało błędów synchronizacji między wątkami?
    Wicket używa filtra, więc przypuszczam, że dla każdego żadania tworzony jest nowy obiekt WebApplication, a może dla każdej sesji i może WebApplication obiekt jest trzymany w sesji???

    Chyba sam poszukam w źródłach ;-)

    OdpowiedzUsuń
  4. Chcieć to móc, dzięki Google proponuję wszystkim zainteresowanym zapoznanie się z tym wątkiem na grupie wicket-user, jest ładnie opisane co i jak z wątkami i gdzie należy uważać. Szkoda, że nie zostało to napisane w książce, jednak polecam oficjalne forum książki Wicket in Action

    OdpowiedzUsuń
  5. Wszystko fajnie tylko taka integracja powoduje niemożliwość przetestowania UsersProvider'a bez ogromnego setUp'u. UsersProvider powinien mieć wstrzyknięte dao a nie pobierać je z singletona.
    Pozdr

    OdpowiedzUsuń