środa, 16 września 2009

Websphere 6.1 remote SLSB między 2 EAR

W aplikacji EAR na websphere 6.1 stanęliśmy przed problemem integracji z pewnym zewnętrznym modułem. Początkowo planowałem owy moduł w postaci zbioru jarów wrzucić bezpośrednio do aplikacji i korzystać z niego poprzez przygotwaną facade. Jednak gdy otrzymałem już moduł to wiedziałem ,że takie podejście może się nie sprawdzić. Moduł ważył ponad 44 MB, a dodatkowo zawierał takie jary jak: servlet.api, jta.jar oraz parę innych z j2ee, ale także pewne biblioteki (m.in stax,jaxb,spring, hibernate, xerces, log4j) w innych wersjach niż te z których do teraz korzystałem... bez OSGI sądzę, że nie ma szans pogodzić ze sobą mojej aplikacji i modułu w ramach pojedynczego EAR.
Jedyne co udało się wykombinować to wrzucić nowy moduł jako osobny EAR, na ten sam serwer, na którym jest zdeployowana aplikacja. Oczywiście trzeba było obmyślić w jaki sposób aplikacja będzie mogła się teraz komunikowac z modułem. Najprostsze wydawało się stworzenie remote stateless session EJB 2.1 (2.1, bo nasz serwer nie ma zainstalowanego packa do EJB 3.0).
Skoro i tak juz został utworzony interfejs POJO, który miał wystawiać wszystkie usługi to wystarczyło stworzyć (wyklikać) EJB, którego remote interfejs pokrywałby się z tym uprzednio przygotowanym (z jedyną modyfikacją, że każda z metod musi deklarowac RemoteException).
Dostawca modułu wszystko ładnie przygotował - otrzymałem client jar z interfejsami oraz stubami, a także dodatkowo dostarczono nawet klienta testowego. Wszystko było super do momentu kiedy spróbowałem wywołać z mojej aplikacji metodę interfejsu modułu. Skoro funkcjonalność modułu jest dostępna przez remote EJB to odwołanie do takiego komponentu wydaję się bezproblemowe z poziomu Spring:

<xml:namespace prefix = jee /><?xml:namespace prefix = jee />
<jee:remote-slsb id="modulFacade" name="ejb/ModulFacade" ref="false" interface="xxx.yyy.zzz.ModuleFacadePojo"/>


Konsumpcja remote stateless session EJB w taki sposób ma parę zasadniczych zalet:

  • nie musimy pisać kodu, który robi lookup, wywołuję create(), cachuje home

  • nasz kod korzystający z modulFacade nie musi zajmować się obsługą RemoteException


Jednak po wywołaniu motody na beanie modulFacade leciały wyjątki: ClassCastException lub NoSuchMethodException. Szczególnie ten drugi wyjątek dał mi do myślenia i zasugerował, że wyjątki te mogą wynikac z moich ulubionych problemów związanych z classloadingiem. Przygotowana przez dostawce modułu aplikacja testowa w postacie war działała poprawnie, jednak tam dostęp do EJB był zrealizowany poprzez wygenerowaną (pewnie z RAD) klasę Util, która zajmowała się lookup(), PortableRemoteObject.narrow(), create(). Na potrzeby testu, spróbowałem wykorzystać tą klasę w mojej aplikacji i okazało się ,że tym razem wszystko działa bez problemu...



W takim razie problem leżał w Spring lub w niewłaściwej konfiguracji elementu jee:remote-slsb.Po chwili googlowania udało mi się znaleźć odpowiedni post na forum, wraz z rozwiązaniem.


Chodzi o ustawienie atrybutu home-interface:


<xml:namespace prefix = jee /><?xml:namespace prefix = jee /><jee:remote-slsb id="modulFacade" name="ejb/ModulFacade" ref="false" interface="xxx.yyy.zzz.ModuleFacadePojo" home-interface="xxx.yyy.zzz.ModulFacadeHome"/>

Pobrany z JNDI objekt będący home remote SLSB aplikacji-modułu jest stubem, który został załadowany przez classloader aplikacji-modułu. Dalsze wywołania na tym obiekcie skutkują tworzeniem obiektów, których klasy są ładowane tym samym classloaderem. W ten sposób dojdziemy do momentu, w którym to dana metod biznesowe zwraca nam obiekt, którego klasa znajduję się w client jar, ale został załadowany przez classloader aplikacji-modułu.
Teraz jeśli w naszej aplikacji zrobiony będzie cast to dostaniemy ClassCastException.
Podobnie to wszystko wygląda gdy nasza metoda biznesowa, będzie brała jako parametr obiekt klasy znajdujący się w client jar. W takim przypadku sygnatura metody nie będzie się zgadzać bo 2 identyczne klasy załadowane przez 2 różne classloadery są niezgodne.
Całość bardzo ładnie widać w trybie debug:

protected Object lookup() throws NamingException {
Object homeObject = super.lookup();
if (this.homeInterface != null) {
try {
homeObject = PortableRemoteObject.narrow(homeObject, this.homeInterface);
}catch (ClassCastException ex) {
throw new RemoteLookupFailureException(
Could not narrow EJB home stub to home interface [" + this.homeInterface.getName() + "]", ex);
}
}
return homeObject;
}

Wywołanie lookup() zwraca nam objekt będacy interfejsem home EJB.
Jednak gdy podejrzymy: homeObject.getClass().getClassLoader() to zobaczymy classloader, który załadował aplikacje modułu. Dopiero wywołanie PortableRemoteObject.narrow spowoduję, że wartością homeObject.getClass().getClassLoader() będzie classloader naszej aplikacji. Z tego co się doczytałem zachowanie takie jest spowodowane tym, że Websphere optymalizuję wywołanie przez zdalne EJB w ramach dwóch aplikacji znajdujących się w tym samym JVM. Jak będę miał chwilę to faktycznie można to sprawdzić: należałoby wrzucic client jar do shared-library i zobaczyć czy faktycznie uda się uzyskać pass-by-reference.
Dodatkowo gdy każda z tych aplikacji byłaby uruchamiana na różnych JVM to podawanie home-interface ma także być zbędne, czyli bez wywołania PortableRemoteObject.narrow wszystko ma działać...to też można by sprawdzić.

Update: przypadkowo trafiłem na całkiem sensowny opis mechanizmu/rozwiązań: http://fixunix.com/websphere/344774-local-interfaces-same-jvm-but-different-ear-issue.html