29.09.2012
Remote Zugriff auf EJBs mit einem Standalone-Java-Client (JEE6)
In diesem Artikel wird beschrieben, wie man einen Java-Client erstellt, mit dem auf ein EJB zugegriffen werden kann, welches auf einen JBoss oder Glassfish Application-Server deployt ist. Es wird erklärt, welche Probleme auftreten können und wie man diese löst. Außerdem wird gezeigt, welche Anpassungen an dem EJB notwendig sind, damit es über eine Remote-Schnittstelle erreichbar wird.
Im Folgenden wird davon ausgegangen, dass man weiß, was eine Stateless Session Bean ist. Falls die nicht der Fall ist, dann findet man in dem EJB-Tutorial ein sehr einfaches Beispiel mit Erklärungen.
Stateless Session Bean erstellen
Als erstes legen wir ein Interface an, welches die Methode enthält, die wir ansprechen möchten. Wichtig ist die Annotation @Remote
, die angibt, dass es sich um ein Remote-Interface handelt. Außerdem sollte das Interface Serializable
erweitern.
package org.hameister.warehouse; import java.io.Serializable; import javax.ejb.Remote; /** * * @author Hameister */ @Remote public interface RemoteItemService extends Serializable { public String numberOfItems(); }
Als nächstes legen wir eine einfache Stateless Session Bean mit einer Methode an, die einen String
als Rückgabewert hat. Diese Methode muss mit der Signatur, der Methode aus dem Remote-Interface übereinstimmen.
Wichtig ist, dass die Klasse von dem Interface RemoteItemService
erbt. Falls man die Bean weiterhin lokal ansprechen möchte, dann sollte man die Annotation @LocalBean
nicht vergessen. So kann die Bean beispielsweise, weiterhin von eine JSF-Seite aufgerufen werden, die auf dem gleichen Application-Server deployt ist.
package org.hameister.warehouse; import javax.ejb.LocalBean; import javax.ejb.Remote; import javax.ejb.Stateless; import javax.inject.Named; /** * * @author Hameister */ @Stateless @Named @LocalBean public class ItemService implements RemoteItemService { private Integer numberOfItems = 42; public String numberOfItems() { return "42"; } }
Anzumerken ist, dass es auch die Möglichkeit gibt, die Annotation @Remote
im Interface wegzulassen. Falls beispielsweise nicht die Möglichkeit besteht, die Interface-Datei zu ändern, weil es sich um ein Legacy-Interface handelt. Kann in der Klasse ItemService
auch die Annotation @Remote(RemoteItemService.class)
ergänzt werden. Das hat die gleiche Wirkung, wie das @Remote
in der Interface-Datei.
Die Bean ist nun fertig und kann auf dem Application-Server deployt werden.
Im nächsten Schritt wird die Interface-Datei in eine Jar-Datei gepackt. Für das eine Interface sicherlich "overkill", aber bei größeren Projekten sinnvoll. Am einfachsten verwendet man Ant dafür.
In dem folgenden Ant-Target wird einfach die class-Datei aus dem build-Verzeichnis kopiert und in die jar-Datei gepackt. (Die Pfadangaben beziehen sich auf NetBeans. Bei Eclipse sehen sie geringfügig anders aus.)
<target name="create-client" depends="compile"> <jar jarfile="./dist/WarehouseTestClient.jar"> <fileset dir="./build/web/WEB-INF/classes" includes="**/RemoteItemService.class"> </fileset> </jar> </target>
Wem das zu kompliziert ist, der kann die Datei später auch einfach in das neue Projekt kopieren, welches wir gleich anlegen.
Client erstellen
Es wird zwar immer gesagt, dass die Server so wunderbar kompatibel sind, aber das stimmt so leider nicht. Wenn man einen Stand-alone-Client erstellen möchte, dann gibt es Unterschiede zwischen den Application-Servern. Im Folgenden wird beschrieben was beim Glassfish und was beim JBoss beachtet werden muss.
Glassfish
Der Glassfish-Server stellt unter $GLASSFISH_HOME/glassfish/lib/gf-client.jar
eine jar-Datei zur Verfügung, die in den Classpath des Projekts eingebunden werden muss. Außerdem ist es notwendig, die oben erstellte Interface-Datei im Classpath einzutragen. (Entweder in einem Jar verpackt oder als kopierte Klasse.)
Als nächstes wird eine Client-Klasse mit dem Namen EJBRemoteClientGlassfish
erstellt und folgender Quellcode ergänzt.
package ejbremoteclientglassfish; import java.util.logging.Level; import java.util.logging.Logger; import javax.naming.InitialContext; import javax.naming.NamingException; import org.hameister.warehouse.RemoteItemService; /** * * @author Hameister */ public class EJBRemoteClientGlassfish { public static void main(String[] args) { try { InitialContext context = new InitialContext(); RemoteItemService itemService = (RemoteItemService) context.lookup("java:global/WarehouseTest/ItemService!org.hameister.warehouse.RemoteItemService"); System.out.println("response: " + itemService.numberOfItems()); } catch (NamingException ex) { Logger.getLogger(EJBRemoteClientGlassfish.class.getName()).log(Level.SEVERE, null, ex); } } }
Der InitialContext
kann einfach erzeugt werden. Anschließend wird über ein lookup
nach der Bean gesucht. Sehr wichtig ist dabei der JNDI-Name. Dieser wird normalerweise im Logfile oder auf der Konsole ausgegeben, wenn die Session Bean auf dem Application-Server deployt wird. Eine kurze Beschreibung zu JNDI-Namen findet man im Artikel Unit-Tests für EJBs mit einem Embeddable-Container. Der Rückgabewert des lookup
ist ein Objekt, welches das RemoteItemService
-Interface implementiert. Daher kann direkt die Methode numberOfItems()
aufgerufen werden, welche den Wert 42
zurückliefert.
Wenn man nun die Java-Klasse übersetzt und startet, dann sollte man die Ausgabe response: 42
erhalten.
Weitere Informationen zur Client-Programmierung mit dem Glassfish findet man hier: EJB Clients.
JBoss
Auch der JBoss stellt eine Client-Library zur Verfügung, die unter $JBOSS_HOME/bin/client/jboss-client.jar
abgelegt ist und in den Classpath des Projekts eingebunden werden muss. Außerdem ist es notwendig, die oben erstellte Interface-Datei im Classpath einzutragen. (Entweder in einem Jar verpackt oder als kopierte Klasse.)
Da der JBoss von Hause aus secure geschaltet ist, muss für den Zugriff auf das EJB ein Benutzer angelegt werden. Dies kann ganz einfach mit dem Script $JBOSS_HOME/bin/add-user.sh
erledigt werden. Im folgenden wird davon ausgegangen, dass ein Benutzer max
mit dem Passwort max1
existiert.
Als nächstes wird eine Client-Klasse mit dem Namen EJBRemoteClientJBoss
erstellt und folgender Quellcode ergänzt.
package ejbremoteclientjboss; import java.util.Hashtable; import java.util.logging.Level; import java.util.logging.Logger; import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.NamingException; import org.hameister.warehouse.RemoteItemService; /** * * @author Hameister */ public class EJBRemoteClientJBoss { /** * @param args the command line arguments */ public static void main(String[] args) { try { final Hashtable jndiProperties = new Hashtable(); jndiProperties.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming"); jndiProperties.put(Context.PROVIDER_URL,"remote://localhost:4447"); InitialContext context = new InitialContext(jndiProperties); RemoteItemService itemService = (RemoteItemService) context.lookup("ejb:/WarehouseTest/ItemService!org.hameister.warehouse.RemoteItemService"); System.out.println("response: "+itemService.numberOfItems()); } catch (NamingException ex) { Logger.getLogger(EJBRemoteClientJBoss.class.getName()).log(Level.SEVERE, null, ex); } } }
Im Gegensatz zum Glassfish benötigt der JBoss beim Erstellen des InitialContext
ein paar Parameter, die in einer Hashtable
abgelegt sein müssen. Der Naming-Service des JBoss läuft auf dem Port 4447
.
Ein weiterer Unterschied zum Glassfish ist, dass das Prefix ejb:
im JNDI-Name benötigt wird.
Und als letztes benötigt des JBoss-Client noch eine Property-Datei mit dem Namen jboss-ejb-client.properties
, die auch im Classpath liegen muss und folgenden Inhalt hat:
endpoint.name=client-endpoint remote.connectionprovider.create.options.org.xnio.Options.SSL_ENABLED=false remote.connections=default remote.connection.default.host=localhost remote.connection.default.port = 4447
Wenn man nun die Java-Klasse übersetzt und startet, dann sollte man die Ausgabe response: 42
erhalten.
Weitere Informationen zur Client-Programmierung mit dem JBoss findet man hier: EJB invocations from a remote client using JNDI.
Zusätzlich ist noch dieses Dokument empfehlenswert: Remote EJB invocations via JNDI - EJB client API or remote-naming project.
Fehlermeldungen
Wenn man die Fehlermeldung Caused by: java.lang.ClassNotFoundException: org.hameister.warehouse.__EJB31_Generated__ItemService__Intf____Bean__ (no security manager: RMI class loader disabled)
erhält, dann stimmt etwas nicht mit dem JNDI-Namen. Aus eigener Erfahrung muss ich sagen, dass es sehr leicht passiert, dass man java:global/WarehouseTest/RemoteItemService!org.hameister.warehouse.ItemService
schreibt. Na, Fehler gefunden? ;-)
Die folgende Fehlermeldung deutet auf das gleiche Problem hin: WARNUNG: IOP00100006: Class com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate is not Serializable
org.omg.CORBA.BAD_PARAM: WARNUNG: IOP00100006: Class com.sun.ejb.containers.EJBLocalObjectInvocationHandlerDelegate is not Serializable vmcid: SUN minor code: 6 completed:
Weitere Informationen
Die Enterprise JavaBeans 3.1 gehören zum JSR-318. Hier findet man das PDF mit der Spezifikation dazu.
Weitere Informationen zum Thema EJBs findet man hier:
- JEE6-Tutorial
- EJB-Tutorial
- TimerService
- Interceptors
- Bean-Validation
- REST-Schnittstelle
- Unit-Tests für EJBs mit einem Embeddable-Container