poniedziałek, 12 października 2009

Dostęp do Websphere Remote EJB 2.1 z JAVA SE

Potrzebowałem stworzyć małą aplikację J2SE do wołania zdalnych sesyjnych bezstanowych EJB uruchomionych na Websphere 6.1.0.19. W mojej poprzedniej pracy juz robiłem coś takiego, ale wtedy aplikacja działała na SUN JRE. Tym razem podstawowym wymaganiem było użycie IBM JRE. Skoro serwera aplikacyjny oraz JRE są od tego samego dostawcy to sprawa powinna być prosta.... tymbardziej, że aplikacja była trywialnana a dodatkowo nie miałem potrzeby korzystać z żadnych z enterprise features jak bezpieczeństwo czy transakcyjność.
Dodatkowo EAR zawierający EJB, do którego trzeba się było dobierać nie był mojego autorstwa i nie dostarczono dla niego jara klienckiego - ze stubami, ale te można samemu wygenerować za pomocą ejbdeploy.
Za wszelką cene chciałem uniknąć sytuacji, w której to musiałbym uruchamiać moją aplikację za pomocą client container albo wykonywać jakies inne, dziwne kroki w fazie developmentu/deploymentu.
Po przeczytaniu dokumentacji wyglądało to wszystko extra skomplikowanie. Tymbardziej, że Weblogic już od dawna udostępniał swoim użytkownikom lekkiego client.jar (czy jakoś tak) do takich celów.
Czemu prosta rzecz nie może byc prosta?
Po wielu próbach i błędach udało mi się w końcu przygotować aplikację. Oprócz jara ze stubami, classpath zawiera następujące pozycje:

ecore-2.3.0-v200706262000.jar
emf-2.1.0.jar
com.ibm.ws.webservices.thinclient_6.1.0.jar
com.ibm.ws.runtime_6.1.0.jar


ecore-2.3.0-v200706262000.jar jest do ściągnięcia z mvnrepository, pozostałe 3 są skopiowane z WebSphere.
Dodatkowo bardzo ważna jest kolejność na classpath: com.ibm.ws.webservices.thinclient_6.1.0.jar musi byc przed com.ibm.ws.runtime_6.1.0.jar, ze względu na to, że klasa com.ibm.ejs.ras.Tr znajduję się w tych obu jarach.

Odwołanie do EJB jest realizowane w następujacy sposób:

Context initialContext = new InitialContext();
StringBuilder builder = new StringBuilder("corbaname:iiop:");
builder.append(serverIp);
builder.append(":");
builder.append(serverPort);
builder.append("#");
builder.append(ejbJNDIName);
Object lookUpResult = initialContext.lookup(builder.toString());
HomeInterface home = (HomeInterface) javax.rmi.PortableRemoteObject.narrow(
lookUpResult, HomeInterface.class);

serverIp - adres serwera na którym jest zdeployowany EJB
serverPort - port do komunikacji RMI (domyslnie 2809)
ejbJNDIName - nazwa JNDI zdalnego EJB

Dodatkowo dla serwera, którego EJB będziemy wywoływać należy dodać parę:adres ip, nazwa serwera do etc\hosts.

Jedyne co mi się nie podoba w przygotowanym przeze mnie rozwiązaniu to potrzeba umieszczenia na classpath w/w bibliotek z Websphere (sam com.ibm.ws.runtime_6.1.0.jar zajmuję prawie 60 MB).
Podobno w Websphere 7.0 został przygotowany jar kliencki, ale osobiście nie mam jak na razie dostępu do tej wersji.

UPDATE: przeglądając materiały związane z classloadingiem dla WAS 6.1 natknąłem się na info dotyczące "thin client library" dla WAS 6.1. Składają się na to 2 jary: com.ibm.ws.admin.client_6.1.0.jar (34 Kb) oraz com.ibm.ws.webservices.thinclient_6.1.0.jar (15 Kb) siedzące w <INSTALL_ROOT>/runtimes. Po usunięciu z classpath jarów wymienionych na początku posta (ecore-2.3.0-v200706262000.jar, emf-2.1.0.jar, com.ibm.ws.webservices.thinclient_6.1.0.jar, com.ibm.ws.runtime_6.1.0.jar) i podegraniu na ich miejsce tych z <INSTALL_ROOT>/runtimes wszystko działa bez zmian.

ś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

wtorek, 14 lipca 2009

Websphere 6.1, SWS i MTOM

Websphere w wersji 6.1 obsługuje jedynie SAAJ w wersji 1.2. Aby móc korzystac z MTOM potrzebny jest SAAJ 1.3. Z tego co znalazłem w sieci jest dostępny odpowiedni feature pack, który ma obsługiwac SAAJ 1.3, MTOM oraz wiele więcej. Jednak instalowanie feature packów od IBM często kończy się różnie (tzn czasami nie działają, a jeszcze częściej trzeba instalowac kolejne packi/łaty itd). Dlatego zdecydowałem się na "własną instalację" SAAJ 1.3.
Na podstawie moich wcześniejszych walk z SWS pod Websphere, wszystko okazało się całkiem proste.
Do jarów wchodzących w skład shared-library (definujących biblioteki nowego classloadera)dodajemy dodatkowo:

Dodatkowo pozwalamy SWS na automatyczne wykrycie implementacji javax.xml.soap.MessageFactory - czyli z naszej konfiguracji springowej wywalamy bean "messageFactory".

Alternatywnie, MTOM równie działa, gdy wymienione wyżej biblioteki wrzucimy do WEB-INF/lib oraz ustawimy ładowanie klas z bibliotek aplikacji przed tymi z bibliotek serwera (PARENT-LAST dla modułu webowego).

W przypadku ustawienia PARENT_LAST i używania JAXB 2 pojawiają się jednak pewne problemy, które objawiają się wyjątkiem:


Caused by: java.lang.VerifyError
at com.sun.xml.bind.v2.model.impl.RuntimeBuiltinLeafInfoImpl.<clinit>(RuntimeBuiltinLeafInfoImpl.java:224)
at java.lang.J9VMInternals.initializeImpl(Native Method)
at java.lang.J9VMInternals.initialize(J9VMInternals.java:194)
at com.sun.xml.bind.v2.model.impl.RuntimeTypeInfoSetImpl.<init>(RuntimeTypeInfoSetImpl.java:61)
at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.createTypeInfoSet(RuntimeModelBuilder.java:127)
at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.createTypeInfoSet(RuntimeModelBuilder.java:79)
at com.sun.xml.bind.v2.model.impl.ModelBuilder.<init>(ModelBuilder.java:151)
at com.sun.xml.bind.v2.model.impl.RuntimeModelBuilder.<init>(RuntimeModelBuilder.java:87)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContextImpl.java:422)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl..<init.>(JAXBContextImpl.java:286)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:139)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:117)
at com.sun.xml.bind.v2.ContextFactory.createContext(ContextFactory.java:188)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:79)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:618)
at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:133)
at javax.xml.bind.ContextFinder.find(ContextFinder.java:286)
at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:372)
at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:337)
at org.springframework.oxm.jaxb.Jaxb2Marshaller.createJaxbContextFromContextPath(Jaxb2Marshaller.java:311)

Po dłuższej chwili debugowania problemem okazała się klasa javax.xml.namespace.QName.
JAXB 2 przy starcie dobiera się do stałych klasy javax.xml.datatype.DatatypeConstants.
Klasa ta sama w sobie jest ładowana z Websphere(../runtimes/base_v61/java/jre/lib/xml.jar), a typem tych stałych jest klasa javax.xml.namespace.QName, która też znajdują się w Webpshere (../runtimes/base_v61/lib/j2ee.jar). Czyli przy odwołaniu się do do klasy javax.xml.datatype.DatatypeConstants, zostanie ona wraz ze swoimi zależnosciami ( m.in. klasą javax.xml.namespace.QName ) załadowana przez classloader Websphere.
Dodatkowo klasa javax.xml.namespace.QName znajduje się w Jsr173_api-1.0.jar/stax-api-1.0.1.jar, a przy ustawieniu Parent-Last nasza aplikacja najpierw załaduje ją właśnie stamtąd. Dojdzie do sytuacji, w której "skontatkują" się ze sobą 2 klasy (javax.xml.namespace.QName)o tej samej nazwie, o potencjalnie różnych definicjach, załadowane przez 2 różne classloadery.
Rozwiązaniem okazuję się wywalenie z naszej aplikacji biblioteki Jsr173_api-1.0.jar/stax-api-1.0.1.jar i podegranie na jej miejsce stax-api-1.0-2.jar(ta nie zawiera w sobie żadnych klas z pakietu jaxax.xml.namespace)

UPDATE: na jednym ze środowisk pomimo w/w ustawień leciały wyjątki:

Caused by:
java.lang.NoClassDefFoundError: javax.xml.transform.stax.StAXResult
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.getOutputHandler(TransformerImpl.java:416)
at com.sun.org.apache.xalan.internal.xsltc.trax.TransformerImpl.transform(TransformerImpl.java:334)
at org.springframework.xml.transform.TransformerObjectSupport.transform(TransformerObjectSupport.java:71)
at org.springframework.ws.wsdl.wsdl11.provider.InliningXsdSchemaTypesProvider.getSchemaElement(InliningXsdSchemaTypesProvider.java:113)
at org.springframework.ws.wsdl.wsdl11.provider.InliningXsdSchemaTypesProvider.addTypes(InliningXsdSchemaTypesProvider.java:101)
at org.springframework.ws.wsdl.wsdl11.ProviderBasedWsdl4jDefinition.afterPropertiesSet(ProviderBasedWsdl4jDefinition.java:233)
at org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition.afterPropertiesSet(DefaultWsdl11Definition.java:170)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1369)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1335)
... 39 more
Caused by:
java.lang.ClassNotFoundException: javax.xml.transform.stax.StAXResult
at java.net.URLClassLoader.findClass(URLClassLoader.java:496)
at com.ibm.ws.bootstrap.ExtClassLoader.findClass(ExtClassLoader.java:132)
at java.lang.ClassLoader.loadClass(ClassLoader.java:631)
at com.ibm.ws.bootstrap.ExtClassLoader.loadClass(ExtClassLoader.java:87)
at java.lang.ClassLoader.loadClass(ClassLoader.java:597)
at com.ibm.ws.classloader.ProtectionClassLoader.loadClass(ProtectionClassLoader.java:58)
at com.ibm.ws.classloader.ProtectionClassLoader.loadClass(ProtectionClassLoader.java:54)
at com.ibm.ws.classloader.CompoundClassLoader.loadClass(CompoundClassLoader.java:394)
at java.lang.ClassLoader.loadClass(ClassLoader.java:597)
... 48 more

Klasa org.springframework.xml.transform.TransformerObjectSupport korzysta z wywołania javax.xml.transform.TransformerFactory.newInstance(), przy czym faktyczna implementacja jest określona za pomocą mechanizmu ServiceLoader API. Okazało się, że w obu jarach: jaxp-ri-1.4.2.jar oraz xalan-2.7.0.jar, istnieją wpisy definiujące implementacje dla interfejsu javax.xml.transform.TransformerFactory. Dotychczas "szczęśliwie" wczytywana była implementacja org.apache.xalan.processor.TransformerFactoryImpl z xalan-2.7.0.jar, ale na jednym ze środowisk zaczytano com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl z jaxp-ri-1.4.2.jar. Nie miałem zbytnio ochoty wgłębiać się w ten temat (można poprzez system property wymusić konkretną implementację , może można określić kolejność ładowania przez ServiceLoader API) usunąłem plik META-INF\services\javax.xml.transform.TransformerFactory z jaxp-ri-1.4.2.jar i po kłopocie :)