Remote Zugriff auf EJBs mit einem Standalone-Java-Client - JEE6 - Java Enterprise Edition 6 (JSR-318)

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: