Beispiel: Zugriff auf Stateless Session Bean aus Java-Anwendung


Inhalt:

Anlegen des Projekts
Code des Application Client
Konfiguration
Ausführen des Clients
Verbinden mit Username/Passwort
Troubleshooting
Import


Für WildFly 30 und JakartaEE10: Beispiel für eine "ganz normale" Java-Anwendung, die auf eine Stateless Session Bean zugreift.


Der EJB-Zugriff erfolgt über den "WildFly Naming Client", siehe https://github.com/wildfly/wildfly-naming-client.
Zum Konzept des JNDI-Zugriffs sei auf die WildFly-Doku verwiesen: https://docs.wildfly.org/30/Developer_Guide.html#EJB_invocations_from_a_remote_client_using_JNDI (bezieht sich allerdings auf ein mittlerweile deprecated Verfahren aus JBoss 7/WildFly 8).


Hier gibt es das gepackte Eclipse-Projekt zum Download (die Importanleitung findet man am Ende dieses Dokuments): StatelessStandaloneClient.zip


Vorraussetzungen: das Projekt basiert darauf, dass die EAR-Datei aus dem Stateless-Beispiel in Eclipse vorhanden ist und außerdem auf dem lokalen JBoss deployed wurde.


Anlegen des Projekts

Es wird ein "Java Project" erzeugt:
Java Project
Im ersten Schritt des Assistenten geben wir dem Projekt einen Namen (hier: "StatelessStandaloneClient").
Project 'StatelessStandaloneClient'
Im Rest des Assistenten bleibt alles beim Default.

Nachbearbeitung: Build Path
Wir müssen den Build Path anpassen.
Schritt 1: Das EJB-Projekt "StatelessEJB" wird bekannt gemacht. Dazu in die "Properties" des Projekts gehen. Im Bereich "Java Build Path" gehen wir auf den Karteireiter "Projects",wählen den Eintrag "Classpath" und klicken auf "Add":
StatelessEJB im Build Path (1)
Wir wählen das Projekt "StatelessEJB" aus:
StatelessEJB im Build Path (2)
Anmerkung: eigentlich benötigen wir nur die Klassen "GeometricModelRemote" sowie die in den Interfacemethoden deklarierte Exception "InvalidParameterException".


Schritt 2: eine JBoss-Library wird zugefügt, die die benötigten EJB-Klassen enthält.
Wieder in den "Java Build Path" gehen, diesmal auf den Karteireiter "Libraries". Hier wieder den Eintrag "Classpath" wählen und auf "Add External Jars" klicken:
jboss-client.jar im Build Path (1)
Die Datei "%JBOSS_HOME%\bin\client\jboss-client.jar" auswählen. Das Ergebnis sieht so aus:
jboss-client.jar im Build Path (2)


Code des Application Client

Der volle Code des Client sieht so aus:

public class GeometricModelApplicationClient
{
  public static void main(String[] args)
  {
    try
    {
      GeometricModelRemote geometricModel;

      //InitialContext befüllt sich komplett aus "jndi.properties"
      InitialContext context = new InitialContext();

      //Lookup durchführen:
      geometricModel = (GeometricModelRemote) context.lookup("Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote");
      
      //EJB aufrufen:
      double dblVolume = geometricModel.computeCuboidVolume(10, 5, 7);
      double dblSurface = geometricModel.computeCuboidSurface(10, 5, 7);

      System.out.println("Calculated volume: " + dblVolume + ", surface: " + dblSurface);
    }
    catch (Exception ex)
    {
      ex.printStackTrace();
    }
  }
}

Anmerkung: würde man den Lookup auf eine Stateful Session Bean durchführen, muss an den Lookup-String "?stateful" angehängt werden. Grund ist, dass die EJB-Client-API einen Hinweis darauf braucht, dass eine Stateful Session Bean gesucht wird und deshalb interne Optimierungen, die nur für Stateless Beans möglich sind, nicht durchgeführt werden dürfen.


Konfiguration

"jndi.properties"
Es wird eine Datei "jndi.properties" im Verzeichnis "src" des Projekts angelegt. Beim Erzeugen des "InitialContext" (in der Variante ohne Konstruktorparameter) wird diese Datei eingelesen. Sie hat diesen Inhalt:
java.naming.factory.initial=org.wildfly.naming.client.WildFlyInitialContextFactory
#java.naming.factory.url.pkgs=org.jboss.ejb.client.naming
java.naming.provider.url=remote+http://localhost:8080
In dieser Datei wird für das JNDI-Framework (das ein Teil von JavaEE ist und von den Implementieren der Anwendungsserver um eine konkrete Implementierung erweitert) angegeben, welche JBoss-Klassen die JNDI-Implementierung enthalten. Außerdem ist hier die Adresse des Zielservers angegeben. Der Präfix "remote+http://" gibt das zu verwendende Protokoll an.

Der Schlüssel "java.naming.factory.url.pkgs" findet sich in den Beispielen immer mal wieder, aber er scheint nicht unbedingt nötig zu sein.

Anmerkung: in den Beispielen werden immer Username und Passwort übergeben, beim Zugriff auf einen lokal laufenden JBoss ist das allerdings nicht nötig. Siehe dazu der Abschnitt
Verbinden mit Username/Passwort

Alternativ könnte man diese Datei weglassen und die Konfiguration komplett im Code machen:
Properties jndiProps = new Properties();
jndiProps.put(Context.INITIAL_CONTEXT_FACTORY, "org.wildfly.naming.client.WildFlyInitialContextFactory");
//jndiProps.put(Context.URL_PKG_PREFIXES, "org.jboss.ejb.client.naming");
jndiProps.put(Context.PROVIDER_URL,"remote+http://localhost:8080");
// create a context passing these properties
InitialContext context = new InitialContext(jndiProps);

Im Project Explorer sollte es so aussehen:
Config-Dateien im Project Explorer


Anmerkung zum JNDI-Namen:
Der Lookup geht auf eine EJB, die auf Server-Seite unter dem Namen "java:jboss/exported/" ins JNDI eingebunden wurde. Genau solche Einträge finden wir in der Server-Konsole:
13:04:03,321 INFO  [org.jboss.as.ejb3.deployment] (MSC service thread 1-7) WFLYEJB0473: JNDI bindings for 
  session bean named 'GeometricModelBean' in deployment unit 'subdeployment "StatelessEJB.jar" of deployment "Stateless.ear"' are as follows:

	java:global/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote
	java:app/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote
	java:module/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote
	java:jboss/exported/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote
	ejb:Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote
	java:global/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelLocal
	java:app/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelLocal
	java:module/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelLocal
Beim Lookup wird automatisch vor den Lookup-String "java:jboss/exported/" vorangestellt, so dass folgendes funktioniert:
geometricModel = (GeometricModelRemote) context.lookup("Stateless/StatelessEJB//GeometricModelBean!de.fhw.komponentenarchitekturen.knauf.stateless.GeometricModelRemote");
Der JNDI-Name ist so aufgebaut:
Für Stateless Beans:

<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>
Für Stateful Beans:

<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>?stateful
Dabei sind:

Ausführen des Clients

Einfach Rechtsklick auf "GeometricModelApplicationClient" und "Run" wählen. Wenn man alles richtig gemacht hat, kommt diese Ausgabe:

Okt 08, 2017 12:21:42 PM org.wildfly.naming.client.Version 
INFO: WildFly Naming version 1.0.1.Final
Okt 08, 2017 12:21:42 PM org.xnio.Xnio 
INFO: XNIO version 3.5.1.Final
Okt 08, 2017 12:21:42 PM org.xnio.nio.NioXnio 
INFO: XNIO NIO Implementation Version 3.5.1.Final
Okt 08, 2017 12:21:42 PM org.jboss.remoting3.EndpointImpl 
INFO: JBoss Remoting version 5.0.0.Final
Okt 08, 2017 12:21:42 PM org.wildfly.security.Version 
INFO: ELY00001: WildFly Elytron version 1.1.1.Final
Calculated volume: 350.0, surface: 310.0
Am Ende stehen die aus der EJB abgerufenen Werte.


Verbinden mit Username/Passwort

Wenn der WildFly-Server lokal läuft, dann kann der Client sich ohne Angabe von User/Passwort anmelden - siehe https://web.archive.org/web/20200426115123/https://developer.jboss.org/wiki/AS710Beta1-SecurityEnabledByDefault, Abschnitt "Local Clients".

Man kann die Eingabe eines Passworts auch bei lokalem Zugriff scheinbar "erzwingen": in "standalone.xml" sucht man folgenden Abschnitt:
            <sasl>
                <sasl-authentication-factory name="application-sasl-authentication" sasl-server-factory="configured" security-domain="ApplicationDomain">
                    <mechanism-configuration>
                        <mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
                        <mechanism mechanism-name="DIGEST-MD5">
                            <mechanism-realm realm-name="ApplicationRealm"/>
                        </mechanism>
                    </mechanism-configuration>
                </sasl-authentication-factory>
                <sasl-authentication-factory name="management-sasl-authentication" sasl-server-factory="configured" security-domain="ManagementDomain">
                    <mechanism-configuration>
                        <mechanism mechanism-name="JBOSS-LOCAL-USER" realm-mapper="local"/>
                        <mechanism mechanism-name="DIGEST-MD5">
                            <mechanism-realm realm-name="ManagementRealm"/>
                        </mechanism>
                    </mechanism-configuration>
                </sasl-authentication-factory>
                ...
            </sasl>
In der SASL Authentication Factory "application-sasl-authentication" entfernt man den Mechanism JBOSS-LOCAL-USER und startet den Server neu.
Die Doku dazu steckt im "WildFly Admin guide", Kapitel "4.2.2. Silent Authentication": https://docs.wildfly.org/30/WildFly_Elytron_Security.html#silent-authentication (das Kapitel bezieht sich auf das Management Realm und das Hinzufügen der Silent Authentication, gilt aber wohl genauso für das Application Realm)


Jetzt erhält man eine Exception:
javax.naming.CommunicationException: WFNAM00018: Failed to connect to remote host [Root exception is javax.security.sasl.SaslException: Authentication failed: none of the mechanisms presented by the server (DIGEST-MD5) are supported]
	at org.wildfly.naming.client.remote.RemoteNamingProvider.getPeerIdentityForNaming(RemoteNamingProvider.java:110)
	at org.wildfly.naming.client.remote.RemoteNamingProvider.getPeerIdentityForNaming(RemoteNamingProvider.java:53)
	at org.wildfly.naming.client.NamingProvider.getPeerIdentityForNamingUsingRetry(NamingProvider.java:105)
	at org.wildfly.naming.client.remote.RemoteNamingProvider.getPeerIdentityForNamingUsingRetry(RemoteNamingProvider.java:91)
	at org.wildfly.naming.client.remote.RemoteContext.lambda$lookupNative$0(RemoteContext.java:189)
	at org.wildfly.naming.client.NamingProvider.performExceptionAction(NamingProvider.java:222)
	at org.wildfly.naming.client.remote.RemoteContext.performWithRetry(RemoteContext.java:100)
	at org.wildfly.naming.client.remote.RemoteContext.lookupNative(RemoteContext.java:188)
	at org.wildfly.naming.client.AbstractFederatingContext.lookup(AbstractFederatingContext.java:74)
	at org.wildfly.naming.client.AbstractFederatingContext.lookup(AbstractFederatingContext.java:60)
	at org.wildfly.naming.client.WildFlyRootContext.lookup(WildFlyRootContext.java:144)
	at java.naming/javax.naming.InitialContext.lookup(InitialContext.java:409)
	at de.hsrm.jakartaee.knauf.stateless.standaloneclient.GeometricModelApplicationClient.main(GeometricModelApplicationClient.java:31)
Caused by: javax.security.sasl.SaslException: Authentication failed: none of the mechanisms presented by the server (DIGEST-MD5) are supported
	at org.jboss.remoting3.remote.ClientConnectionOpenListener$Capabilities.handleEvent(ClientConnectionOpenListener.java:443)
	at org.jboss.remoting3.remote.ClientConnectionOpenListener$Capabilities.handleEvent(ClientConnectionOpenListener.java:244)
	at org.xnio.ChannelListeners.invokeChannelListener(ChannelListeners.java:92)
	at org.xnio.conduits.ReadReadyHandler$ChannelListenerHandler.readReady(ReadReadyHandler.java:66)
	at org.xnio.nio.NioSocketConduit.handleReady(NioSocketConduit.java:89)
	at org.xnio.nio.WorkerThread.run(WorkerThread.java:603)
	at ...asynchronous invocation...(Unknown Source)
	at org.jboss.remoting3.EndpointImpl.connect(EndpointImpl.java:600)
	at org.jboss.remoting3.EndpointImpl.connect(EndpointImpl.java:565)
	at org.jboss.remoting3.ConnectionInfo$None.getConnection(ConnectionInfo.java:82)
	at org.jboss.remoting3.ConnectionInfo.getConnection(ConnectionInfo.java:55)
	at org.jboss.remoting3.EndpointImpl.doGetConnection(EndpointImpl.java:499)
	at org.jboss.remoting3.EndpointImpl.getConnectedIdentity(EndpointImpl.java:445)
	at org.jboss.remoting3.UncloseableEndpoint.getConnectedIdentity(UncloseableEndpoint.java:52)
	at org.wildfly.naming.client.remote.RemoteNamingProvider.getFuturePeerIdentityPrivileged(RemoteNamingProvider.java:151)
	at org.wildfly.naming.client.remote.RemoteNamingProvider.lambda$getFuturePeerIdentity$0(RemoteNamingProvider.java:138)
	at java.base/java.security.AccessController.doPrivileged(AccessController.java:318)
	at org.wildfly.naming.client.remote.RemoteNamingProvider.getFuturePeerIdentity(RemoteNamingProvider.java:138)
	at org.wildfly.naming.client.remote.RemoteNamingProvider.getPeerIdentity(RemoteNamingProvider.java:126)
	at org.wildfly.naming.client.remote.RemoteNamingProvider.getPeerIdentityForNaming(RemoteNamingProvider.java:106)
	... 12 more
Also fügt man über das Script "add-users.bat" einen Benutzer hinzu, siehe auch "Zugriff auf Web-Console" im Abschnitt "Allgemein"
Wichtig ist hier allerdings, dass man einen "Application User" zugefügt, nicht einen "Management User".
add-user.bat

Jetzt trägt man den User in "jndi.properties" ein:
java.naming.factory.initial=org.wildfly.naming.client.WildFlyInitialContextFactory
#java.naming.factory.url.pkgs=org.jboss.ejb.client.naming
java.naming.provider.url=remote+http://localhost:8080

java.naming.security.principal=theuser
java.naming.security.credentials=thepassword


Troubleshooting

Bei Problemen hilft es eventuell, die Logausgabe der WildFly-Clientschicht hochzudrehen. Dazu stelle ich hier drei Verfahren vor (wobei die Ausgabe sich merkwürdigerweise im Detail unterscheidet...):

Log4J 1.x
Dieses Framework ist eigentlich veraltet, funktioniert aber trotzdem.
Man kann es von
http://logging.apache.org/log4j/1.2/ herunterladen und fügt die Datei "log4j-1.2.17.jar" dem ClassPath zu.

Im Verzeichnis "src" legt man eine Datei "log4j.properties" an mit diesem Inhalt:
log4j.rootLogger=DEBUG, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%t] [%c] - %m%n
Durch das Loglevel "DEBUG" sieht man jetzt ein paar mehr Ausgaben. Das Level "INFO" entspricht wohl der Ausgabe ohne diese Datei. Man könnte auch das Level "TRACE" setzen, das führt zu noch mehr Ausgaben.


Log4J2
Dies ist die aktuelle Log4J-Variante. Man kann es von https://logging.apache.org/log4j/2.x/ herunterladen, und fügt die Dateien "log4j-api-2.22.1.jar" und "log4j-core-2.22.1.jar" dem ClassPath zu.

Im Verzeichnis "src" legt man eine Datei "log4j2.properties" an mit diesem Inhalt:
appenders = console
appender.console.type = Console
appender.console.name = STDOUT
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %d %p [%t] [%c] - %m%n

rootLogger.level = debug
rootLogger.appenderRefs = stdout
rootLogger.appenderRef.stdout.ref = STDOUT


java.util.Logging
Alternativ kann man den in Java integrierten Logging-Mechanismus verwenden.
Am einfachsten ist, wenn man den Logger der Runtime umkonfiguriert. Dazu dreht man das Loglevel in der Datei "C:\Program Files\Java\jre1.8.0_251\lib\logging.properties" hoch, indem man .level= INFO durch z.B. .level= ALL ersetzt, dito für java.util.logging.ConsoleHandler.level = INFO.

Besser ist es, eine eigene Datei "logging.properties" im Projekt anzulegen:
handlers= java.util.logging.ConsoleHandler
.level= FINE

# Limit the message that are printed on the console to INFO and above.
java.util.logging.ConsoleHandler.level = FINE
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
Diese Datei ersetzt die globale Konfiguration komplett, d.h. man kann nicht den Default-ConsoleHandler aus der globalen Datei verwenden und nur dessen Level umkonfigurieren, sondern man muss ihn tatsächlich komplett neu konfigurieren.
Den Pfad zur Datei gibt man im VM-Parameter "java.util.logging.config.file" an. In Eclipse kann man den Projektpfad durch eine Variable abrufen: -Djava.util.logging.config.file=${project_loc}/logging.properties. Beim Aufruf per Kommandozeile muss man den vollen Pfad angegeben. logging.properties

Gibt man es in der Sektion "Program Arguments" an, dann wird der Parameter ignoriert!

Auch beim Aufruf per Kommandozeile ist die Position wichtig: sie muss VOR dem Namen der auszuführenden "Main Class" stehen:

java "-Djava.util.logging.config.file=c:\Pfad\Zu\StatelessStandaloneClient\logging.properties" -classpath C:\Pfad\Zu\wildfly-30.0.0.Final\bin\client\jboss-client.jar;C:\Pfad\Zu\workspace\StatelessStandaloneClient\bin;C:\Pfad\Zu\workspace\StatelessEJB\build\classes de.hsrm.jakartaee.knauf.stateless.standaloneclient.GeometricModelApplicationClient

Stellt man es hinter den Klassennamen, zählt das wohl ebenfalls nicht als "VM Argument".

Import

Um das Beispielprojekt in Eclipse zu importieren, sind folgende Schritte nötig:
1) Verlinkte zip-Datei in den Workspace entpacken (so dass ein Unterverzeichnis "StatelessStandaloneClient" entsteht).
2) In Eclipse: Menü "File" -> "Import" wählen. Dort die Option "Existing Projects into Workspace" wählen:
Import existing Projects into Workspace
Im nächsten Schritt wählt man als "Root directory" den Workspace selbst aus. Jetzt sollten alle vorhandenen Projekte auftauchen, und außerdem das Projekt "StatelessStandaloneClient". Dieses wird abgehakt.
Import StatelessStandaloneClient
3) Jetzt muss man vermutlich den "Build Path" (bzw. den Pfad zu "jboss-client.jar") anpassen, siehe Abschnitt
Anlegen des Projekts



Stand 31.12.2023
Historie:
31.12.2023: erstellt aus WildFly 11-Beispiel und angepasst an JakartaEE10