Beispiel: Stateless Session Bean


Inhalt:

Anlegen der Enterprise Application
Anlegen der GeometricModelBean
Anlegen des WebClient
Server einrichten
Anlegen des Application Clients
Ausführen des Application Clients
Export des Workspace
Re-Import
Ohne Annotations

Für WildFly 30 und JakartaEE 10: Beispiel für eine Stateless Session Bean, auf die per Webclient und mittels Application Client zugegriffen wird.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man am Ende dieses Dokuments): Stateless.ear

Aufbau des Beispieles


a) Bean-Klasse mit Local und Remote-Interfaces
b) Webclient: JSP-Seite
c) WebClient: Servlet
d) App-Client


Anlegen der Application

Schritt 1: Über "File" -> "New" -> "Other..." gelangen wir in den Wizard zum Hinzufügen von neuen Komponenten. Wir wählen im Zweig "Java EE" ein "Enterprise Application Project".
Erzeugen einer Application (Schritt 1)
Schritt 2: In dem erscheinenden Dialog geben wir dem Projekt einen Namen und wählen den WildFly 30-Server aus. Die "EAR version" belassen wir auf dem Default "10.0".
Erzeugen einer Application (Schritt 2)
Auf "Next" klicken.
Schritt 3: Hier könnten wir direkt die einzelnen Modulprojekte zufügen. Deshalb klicken wir auf "New Module" (siehe übernächster Screenshot) und wählen im erscheinenden Dialog die Module "Application client module", "EJB module" und "Web module".
Erzeugen einer Application (Schritt 3)
Das Ergebnis sehen wir hier:
Erzeugen einer Application (Schritt 4)
Die Checkbox "Create application.xml Deployment Descriptor" wird nicht gesetzt, das wäre erst für die Variante "
Ohne Annotations" relevant.

Man klickt auf "Finish".

Beim ersten Aufruf erscheint die Abfrage ob wir zur "Java EE perspective" wechseln wollen. Diese bejahen wir (Screenshot aus einer älteren Eclipse-Version).
JavaEE perspective

Jetzt sieht man folgende Hierarchie im Project Explorer:
Erzeugte Projekthierarchie


Anlegen der GeometricModelBean

Schritt 1: Im Project Explorer den Knoten "StatelessEJB" wählen. Rechtsklick, im Contextmenü den Punkt "New" -> "Session Bean" wählen.
Stateless Session Bean
Schritt 2: Wir vergeben einen Namespace und einen Bean-Namen. Da wir Local- und Remote-Interfaces haben wollen, setzen wir die entsprechenden Checkboxen. Die Checkbox "No-interface view" ist per Default an und wird unchecked.

ACHTUNG: Namenskonvention: Die eigentlichen Implementierung der Bean hat den Zusatz "Bean", das Local Interface hat den Zusatz "Local", das Remote Interface den Zusatz "Remote" (teilweise gibt es auch die Konvention, dem Remote Interface keinerlei Namenszusatz zu geben). Eclipse kommt mit der Konvention "Bean-Klasse hat Zusatz Bean" nicht so ganz zurecht und packt diesen Zusatz in die Namen der Interfaces. Deshalb wieder entfernen!

Stateless Session Bean
Im nächsten Schritt wird alles auf den Defaults belassen:
Stateless Session Bean

Schritt 3: Vorbereiten der Business-Methoden:
Die beiden Business-Methoden werfen eine eigene Exception. Diese wird dem Projekt so zugefügt: Das Package "de.hsrm.jakartaee.knauf.stateless" im Folder "StatelessEJB\ejbModule" im Projekt "StatelessEJB" auswählen. Rechtsklick, "New" -> "Class". Eine Klasse namens "InvalidParameterException" zufügen, die von java.lang.Exception abgeleitet ist.
  public class InvalidParameterException extends Exception
  {
    private static final long serialVersionUID = 1L;
    
    public InvalidParameterException(String str_Message)
    {
      super(str_Message);
    }
  } 

Schritt 4: Jetzt legen wir die Business-Methoden "computeCuboidVolume" und "computeCuboidSurface" in der GeometricModelBean an. Die gesamte Klasse sieht so aus:
  package de.hsrm.jakartaee.knauf.stateless;

  import java.util.logging.Logger;
  import javax.ejb.Stateless;

  @Stateless
  public class GeometricModelBean implements GeometricModel, GeometricModelLocal
  {  
    protected static final Logger logger = Logger.getLogger(GeometricModelBean.class.getName());
  
    public GeometricModelBean()
    {
    }
  
    public double computeCuboidVolume(double a, double b, double c) throws InvalidParameterException
    {
      logger.info("computeCuboidVolume with a = " + a + ", b = " + b + ", c = " + c);
      if (a <= 0 || b <= 0 || c <= 0)
        throw new InvalidParameterException("Side length <= 0");
  
      return a * b * c;
    }
  
    public double computeCuboidSurface(double a, double b, double c) throws InvalidParameterException
    {
      logger.info("computeCuboidSurface with a = " + a + ", b = " + b + ", c = " + c);
      if (a <= 0 || b <= 0 || c <= 0)
        throw new InvalidParameterException("Side length <= 0");
  
      return 2 * (a * b + b * c + c * a);
    }
  } 


Schritt 5: Wir fügen die Methoden dem Local- und Remote-Interface der Bean zu. Dazu können wir uns die Refactoring-Möglichkeiten von Eclipse zu Nutze machen. Im Project Explorer die Bean-Klasse wählen, Rechtsklick, "Refactor", "Pull up..." wählen.
Refactor
Zuerst schieben wir die beiden Methoden ins Local Interface, indem wir es auswählen und die zwei Methoden markieren.
Refactor
Im nächsten Schritt gibt es nix zu ändern.
Refactor
Anmerkung: Eclipse generiert uns jetzt in der Bean-Klasse die "@Override"-Annotation, um die Bean-Methode als "aus dem Interface stammend" zu markieren.

Das gleiche wiederholen wir, um die Methoden ins Remote Interface "GeometricModelRemote" zu schieben.


Anlegen des Webclients

Modul-Abhängigkeiten
Zuerst müssen wir eine Referenz auf das EJB-Projekt zufügen, damit wir die Interfaces der Bean verwenden können:
Auf "StatelessWeb" einen Rechtsklick durchführen und in die "Properties" gehen. Im Punkt "Deployment Assembly" gehen wir auf den den Karteireiter "Manifest Entries" und klicken auf "Add...":
Deployment Assembly - add manifest entry (1)
Hier wählen wir das Projekt "StatelessEJB" aus:
Deployment Assembly - add manifest entry (2)
Das Ergebnis sieht so aus:
Deployment Assembly - add manifest entry (3)
Schauen wir uns die Datei "StatelessWeb/src/main/webapp/META-INF/Manifest.mf", so sieht sie so aus:
Manifest-Version: 1.0
Class-Path: StatelessEJB.jar
Die zweite Zeile haben wir durch den letzten Schritt zugefügt.


JSP anlegen
Den Ordner "StatelessWeb" -> "WebContent" auswählen und per Rechtsklick eine JSP zufügen.
Neue JSP (1)
Im ersten Schritt des Assistenten der JSP den Namen "GeometricModelTest.jsp" geben.
Neue JSP (2)
In Schritt 2 verwenden wir das vorgeschlagene Template.
Neue JSP (3)
Jetzt noch auf "Finish" klicken und den JSP-Code einbauen. Die vollständige JSP-Seite sieht so aus:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"
    import="jakarta.ejb.EJB,de.hsrm.jakartaee.knauf.stateless.*, javax.naming.InitialContext"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>GeometricModelTest</title>
</head>
<body>
<H1>Test for the GeometricModel stateless session bean</H1>
<%
  InitialContext initialContext = new InitialContext();

  GeometricModelRemote geometricModel = (GeometricModelRemote) initialContext.lookup ("java:comp/env/ejb/GeometricModel");
  GeometricModelLocal geometricModelLocal = (GeometricModelLocal) initialContext.lookup ("java:comp/env/ejb/GeometricModelLocal");
%>

<%
//Ist es der erste Aufruf oder wurde auf "Berechnen" geklickt.
if (request.getParameter("submit") == null)
{
  //Erster Aufruf: Nur Formular ausgeben.
}
else
{
  // Zweiter bis n-ter Aufruf: Berechnen.

  double dblA = Double.parseDouble(request.getParameter("a") );
  double dblB = Double.parseDouble(request.getParameter("b") );
  double dblC = Double.parseDouble(request.getParameter("c") );

  ///////////////////////////////////////////////////////////////
  // Berechnung über Remote-Interface.

  double dblVolume = geometricModel.computeCuboidVolume( dblA, dblB, dblC);
  double dblSurface = geometricModel.computeCuboidSurface( dblA, dblB, dblC);

  %>

  Volume = <%=dblVolume%>, Surface= <%=dblSurface%>

  <br>And now with the Local Interface:
  <%
  dblVolume = geometricModelLocal.computeCuboidVolume( dblA, dblB, dblC);
  dblSurface = geometricModelLocal.computeCuboidSurface( dblA, dblB, dblC);

  %>

  Volume = <%=dblVolume%>, Surface = <%=dblSurface%>
  <br><br><br>
<%
}
%>
<form action="GeometricModelTest.jsp" method="post">
  a = <input type="text" name="a" /> <br>
  b = <input type="text" name="b" /> <br>
  c = <input type="text" name="c" /> <br>

  <input type="submit" name="submit" value="submit" />
</form>

</BODY>
</html> 
ACHTUNG: Die Injection in JSP-Seiten ist nicht Teil des JavaEE-Standards:

Zitat aus der JSP-2.1-Spezifikation, Kapitel 7.1.11:
"Finally, note that injection is not supported on JSP pages or tag files. This is because all the information represented by injection annotations needs to be known at deployment time. If an annotation is included in a JSP page or tag file, it won't be seen at deployment time (unless the JSP was being precompiled during deployment)."

"web.xml" anlegen
Wie man im obigen Beispiel sieht, habe ich mir die Remote-/Local-Interfaces der EJB aus dem JNDI geholt. Besonderheit dabei: ich bin den "sauberen" Weg gegangen, der die EJB nicht aus dem globalen JNDI holt, sondern dem Deployer der Anwendung die Möglichkeit läßt, den JNDI-Namen individuell zu konfigurieren und im "Environment Naming Context" bereitzustellen. Der ENC wird mittels des Deployment-Deskriptors "web.xml" konfiguriert.
Wir haben allerdings die Webanwendung ohne Deployment-Deskriptoren angelegt, da diese seit JavaEE6 (Servlet 3.0-Standard) optional sind. Den hier benötigten Deployment-Deskriptor "web.xml" können wir uns generieren lassen: im Projekt "StatelessWeb" einen Rechtsklick durchführen und den Menüpunkt "Generate Deployment Descriptor Stub" aufrufen:
Generate Deployment Descriptor Stub


Jetzt deklariert der Deployer der Anwendung (also wir ;-)) den Eintrag im "Environment Naming Context (ENC)". Schritt 1 davon erfolgt über "web.xml":
	<ejb-ref>
		<ejb-ref-name>ejb/GeometricModel</ejb-ref-name>
		<ejb-ref-type>Session</ejb-ref-type>
		<remote>de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote</remote>
	</ejb-ref>
	<ejb-local-ref>
		<ejb-ref-name>ejb/GeometricModelLocal</ejb-ref-name>
		<ejb-ref-type>Session</ejb-ref-type>
		<local>de.hsrm.jakartaee.knauf.stateless.GeometricModelLocal</local>
	</ejb-local-ref>
Das Binden dieses JNDI-Namens an einen Eintrag im Server-JNDI erfolgt über einen JBoss-spezifischen Deployment-Deskriptor namens "jboss-web.xml", der an der gleichen Stelle wie "web.xml", also in "WebContent\WEB-INF", liegt. Er hat diesen Inhalt:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web xmlns="urn:jboss:jakartaee:1.0"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="urn:jboss:jakartaee:1.0 https://www.jboss.org/schema/jbossas/jboss-web_15_0.xsd"
           version="15.0">
	<ejb-ref>
		<ejb-ref-name>ejb/GeometricModel</ejb-ref-name>
		<jndi-name>java:global/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote</jndi-name>
	</ejb-ref>
	<ejb-local-ref>
		<ejb-ref-name>ejb/GeometricModelLocal</ejb-ref-name>
		<local-jndi-name>java:global/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelLocal</local-jndi-name>
	</ejb-local-ref>

</jboss-web>
Diese Datei wird zu einem Validierungsfehler "Downloading external resources is disabled" führen, solange JBossTools die Datei nicht selbst einbindet (https://issues.redhat.com/browse/JBIDE-29144.
jboss-web.xml validation error

Lösung: in den "Preferences" unter "XML (Wild Web Developer)" wird die Option "Download external resources like referenced DTD, XSD" gewählt:
Wild Web Developer settings
In "web.xml" wird der Eintrag für den Environment Naming Context deklariert (z.B. "ejb/GeometricModel"). In "jboss-web.xml" erfolgt das Binden von "ejb/GeometricModel" an den globalen JNDI-Namen, also "java:global/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote".

Hinweis:
Man könnte den JNDI-Lookup auch auf den globalen JNDI-Namen durchführen:
 GeometricModelRemote geometricModel = 
   (GeometricModelRemote) initialContext.lookup ("java:global/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote");
Das hat aber den großen Nachteil, dass bei einer Änderung des globalen Namens alle Stellen, die auf diesen Namen zugreifen, angepaßt werden müssten. Und manchmal ändert sich in einer JBoss-Version die Konvention für den Aufbau des globalen Namens, in JBoss 6 und früher wäre der JNDI-Name "Stateless/GeometricModelBean/remote" gewesen - viel Spaß beim Anpassen von Code bei einer Migration.

Regeln für den Aufbau von JNDI-Namen:
Siehe https://docs.wildfly.org/30/Developer_Guide.html#EJB_invocations_from_a_remote_client_using_JNDI (Abschnitt nach dem großen Codestück im Kapitel "Writing a remote client application for accessing and invoking the EJBs deployed on the server")

Für ein Remote Interface ist die Regel so:
ejb:<app-name>/<module-name>/<distinct-name>/<bean-name>!<fully-qualified-classname-of-the-remote-interface>
Alternativ geht auch der Präfix "java:/global" statt "ejb:" (wie in meinem Beispiel geschehen)

Für local Interfaces muss man "java:/global" nehmen, "ejb:" ist hier nicht unterstützt (https://community.jboss.org/thread/215535).


Servlet anlegen:
Wir gehen auf "StatelessWeb" und erstellen mit Rechtsklick -> "New" -> "Servlet" ein neues Servlet. Package und Name sollten so aussehen:
Neues Servlet (1)
Im nächsten Schritt muss man eine URL angegeben unter der das Servlet später angesprochen werden soll. In diesem Beispiel habe ich das Default-URL-Mapping "/GeometricModelServlet" geändert auf "/servlet/GeometricModelServlet":
Neues Servlet (2)
Anmerkung: In diesem Beispiel will ich das Servlet über Annotations steuern. Leider bietet Eclipse keinen Assistenten, um ein Servlet mit Annotations zu erzeugen, es werden Einträge in "web.xml" erzeugt. Deshalb müssen die Ergebnisse dieses Schritts nachträglich überarbeitet werden.
In Schritt 3 belassen wir alles auf den Defaults.

Nacharbeiten: Die Datei "web.xml" wird mit dem "XML Editor" öffnen und die Elemente "servlet" und "servlet-mapping" werden wieder herausgelöscht, weil im Folgenden Annotations dafür verwendet werden:
<web-app ...>
  ...
  <servlet>
    <description></description>
    <display-name>GeometricModelServlet</display-name>
    <servlet-name>GeometricModelServlet</servlet-name>
    <servlet-class>de.hsrm.jakartaee.knauf.stateless.GeometricModelServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>GeometricModelServlet</servlet-name>
    <url-pattern>/servlet/GeometricModelServlet</url-pattern>
  </servlet-mapping>
</web-app>
Das Servlet GeometricModelServlet wird mit der Annotation @WebServlet versehen. Der gesamte Code des Servlets sieht so aus:
package de.hsrm.jakartaee.knauf.stateless.stateless;

import jakarta.ejb.EJB;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * Client-Servlet for the GeometricModeBean.
 * It does the same actions as the JSP page.
 *
 */
@WebServlet(name="GeometricModelServlet", displayName="GeometricModelServlet",
  urlPatterns={"/servlet/GeometricModelServlet"})
public class GeometricModelServlet extends HttpServlet
{

  /**Remote interface for Bean @see GeometricModelBean is injected by the server.
   */
  @EJB
  private GeometricModelRemote geometricModel;

  /**Local interface for Bean @see GeometricModelBean is injected by the server.
   */
  @EJB
  private GeometricModelLocal geometricModelLocal;

  /**Serialisierungs-ID. Nur zugefügt da Eclipse sonst meckert.
   */
  private static final long serialVersionUID = 1L;

  /**Konstruktor. Hier passiert nichts.
   * @see javax.servlet.http.HttpServlet#HttpServlet()
   */
  public GeometricModelServlet()
  {
    super();
  }

  /**Verarbeiten einer HTTP-Anfrage per GET-Request.
   * Wird direkt weitergeleitet an doPost.
   * GET-Requests erfolgen nur beim ersten Aufrufen der Seite.
   * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse
   *      response)
   */
  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
  {
    this.doPost(request, response);
  }

  /**Process a HTTP-Post-Request.
   * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse
   *      response)
   */
  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
  {
    // This printwriter is used for outputting the response HTML:
    PrintWriter printWriter = response.getWriter();

    try
    {
      // Page header:
      printWriter.write("<HTML>\r\n" + "<HEAD>\r\n" + "<TITLE>GeometricModelTest</TITLE>\r\n" + 
          " <meta http-equiv=\"content-type\" content=\"text/html; charset=Windows-1252\">\r\n" + 
          "</HEAD>\r\n"+
          "<BODY>\r\n" + 
          "<H1>Test for the GeometricModel stateless session bean</H1>\r\n");

      // Should only the form be shown or is this a submitted form and we have
      // to calculate ?
      if (request.getParameter("submit") == null)
      {
        // First call to this page: just create the form.
      }
      else
      {
        // Page was submitted: do some calculations.

        double dblA = Double.parseDouble(request.getParameter("a"));
        double dblB = Double.parseDouble(request.getParameter("b"));
        double dblC = Double.parseDouble(request.getParameter("c"));

        // /////////////////////////////////////////////////////////////
        // Calculate by using the remote interface:
        
        double dblVolume = this.geometricModel.computeCuboidVolume(dblA, dblB, dblC);
        double dblSurface = this.geometricModel.computeCuboidSurface(dblA, dblB, dblC);

        printWriter.write("Volume = " + dblVolume + " Surface= " + dblSurface + "\r\n"
            + "<br>And now with the Local Interface: <br>\r\n");

        // /////////////////////////////////////////////////////////////
        // Calculate by using the local interface:

        dblVolume = this.geometricModelLocal.computeCuboidVolume(dblA, dblB, dblC);
        dblSurface = this.geometricModelLocal.computeCuboidSurface(dblA, dblB, dblC);

        printWriter.write("Volume = " + dblVolume + ", Surface = " + dblSurface + "\r\n" + "<br><br><br>\r\n");
      }

      // Write the form:
      printWriter.write("<form action=\"./GeometricModelServlet\" method=\"post\"> \r\n" +
          " a = <input type=\"text\" name=\"a\" /> <br> \r\n" +
          " b = <input type=\"text\" name=\"b\" /> <br> \r\n" +
          " c = <input type=\"text\" name=\"c\" /> <br> \r\n" +
          " <input type=\"submit\" name=\"submit\" value=\"submit\" /> \r\n" + 
          "</form> \r\n" + 
          "</BODY> \r\n" +
          "</HTML> \r\n");
    }
    catch (Exception ex)
    {
      // Output in Servlet-Log:
      this.log("An error has occured: " + ex.getMessage(), ex);
      printWriter.write("An error has occured: " + ex.getMessage());
    }

    // Aufräumen:
    printWriter.flush();
    printWriter.close();
  }
}
Man beachte die Annotation @WebServlet, die die Informationen enthält, die uns der Assistent ansonsten in "web.xml" packt:

Aufrufbar ist das Servlet jetzt über diese URL: http://localhost:8080/StatelessWeb/servlet/GeometricModelServlet

Das Ergebnis sieht im Project Explorer so aus:
Struktur von StatelessWeb

Hinweis:
Wenn man im Servlet ohne Injection arbeiten will, führt man den JNDI-Lookup genauso aus wie in der JSP.


Server einrichten

Das Webprojekt auswählen, Rechtsklick -> "Run As..." -> "Run on Server" wählen. Im Assistenten wählen wir als "Type" den WildFly-27+-Server.
Falls wir den Server bereits angelegt haben (dies ist gibt es außerdem als Option beim Definieren der Server-Runtime), dann wählen wir hier "Choose an existing server" und wählen den Server aus, ansonsten "Manually define a new server".
Da die JBoss Tools mittlerweile die WildFly-Serverdefinitionen nicht mehr pro Major Version neu erzeugen, sollten wir den Namen "WildFly 27+" in "WildFly 30" ändern:
Server anlegen (1)
In Schritt 2 belassen wir die Defaults - vor allem sollte hier der WildFly 30-Server auftauchen, den wir bei im Kapitel "Allgemein" eingerichtet hatten:
Server anlegen (1a)
In Schritt 3 sollte unsere Anwendung bereits ausgewählt sein.
Server anlegen (2)

Wir klicken auf "Finish". Der WildFly-Server wird gestartet und die Enterprise Application deployed.
Falls das ohne Fehlermeldungen klappt, könnten wir die Webanwendung jetzt aufrufen unter der URL
http://localhost:8080/StatelessWeb/.
Diese zeigt allerdings den Fehler "Forbidden" an. Dies liegt daran dass unser Projekt keine Index-Seite definiert (mehr dazu im nächsten Beispiel).
Webclient testen
Wir erweitern die URL also um unsere Seite "GeometricModelTest.jsp" and und können endlich Berechnungen ausführen: http://localhost:8080/StatelessWeb/GeometricModelTest.jsp

Den neu angelegten Server erreichen wir über die Karteikarte "Servers" im unteren Bereich der Anwendung. Nachdem wir die Anwendung jetzt zum ersten Mal deployed haben können wir sie in Zukunft nach einer Änderung aktualisieren indem wir den Server auswählen, ihn aufklappen, die Anwendung "Stateless" auswählen und im Contextmenü die Option "Full publish" wählen. Kurz nach einem Publish sollte auf der Karteikarte "Console", auf der die JBoss-Ausgabe landet, eine Ausgabe über ein Neu-Laden der Anwendung auftauchen.
Publish
Achtung: Auch auf der Ebene des Servers gibt es die Option "Publish" im Contextmenü. Diese hat bei mir aber nicht immer das gewünschte Ergebnis geliefert. Von einem "Incremental Publish" der Anwendung rate ich ebenfalls ab - lieber ein sauberes "Full Publish".


Automatisches Publish abschalten:
Leider hat Eclipse die Default-Einstellung, die Anwendung regelmäßig automatisch auf zu publishen. Das ist unnötig und kostet nur Laufzeit, deshalb wird es abgeschaltet. Im Fenster "Servers" bei gestopptem Server Rechtsklick auf den Server => "Open" wählen.
Open Server
In der Sektion "Publishing" wird "Never publish automatically" aktiviert. Außerdem können wir links oben den Name des Servers ändern.
Wichtig: Anschließend die Einstellungen speichern!
Never publish automatically


Anlegen des Application Clients

Zuerst wird die generierte Klasse "Main.java" in "StatelessClient\appClientModule" gelöscht.

Dann im Project Explorer "StatelessClient" auswählen. Rechtsklick, und dann "New" -> "Class" wählen.
Application Client, Schritt 1
Wie im Webclient muss hier eine Abhängigkeit zum EJB-Projekt definiert werden (Rechtsklick auf "StatelessClient", "Properties" wählen). Man fügt unter "Deployment Assembly", Karteireiter "Manifest Entries" die JAR-Datei des EJB-Projekts hinzu.
Application Client, Schritt 2


Der Client bekommt die EJB per Injection geschenkt, d.h. der Code ist genauso einfach wie im Web. Im folgenden der gesamte Code des Clients:
package de.hsrm.jakartaee.knauf.stateless;

import jakarta.ejb.EJB;

public class GeometricModelApplicationClient
{
  @EJB()
  public static GeometricModelRemote geometricModel;
  
  public static void main(String[] args)
  {
    try
    {
      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();
    }
  }
}

Es ist keine weitere Konfiguration nötig!

Jetzt noch die Klasse mit der "main"-Methode in "StatelessClient\appClientModule\META-INF\MANIFEST.MF" eintragen:
Manifest-Version: 1.0
Class-Path: StatelessEJB.jar
Main-Class: de.hsrm.jakartaee.knauf.stateless.GeometricModelApplicationClient
Die von Eclipse generierte Dummy-Klasse "Main.java" können wir übrigens gefahrlos löschen.


Ausführen des Application Clients

Mehr Details siehe hier: https://docs.wildfly.org/30/Developer_Guide.html#Application_Client_Reference

Das Ausführen des Application Client geht leider nicht über Eclipse-Bordmittel, da das Ausführen eines Application Client unter JBoss ein Umfeld benötigt, das man grob als "Mini-JBoss-Server" bezeichnen kann.

Hinweis: es gibt einen Feature Request, um JBoss Tools entsprechend zu erweitern: https://issues.jboss.org/browse/JBIDE-11517

Hierzu benötigen wir die in den JBoss deployed Anwendung als ganzes irgendwo auf der Festplatte. Ideal wäre das JBoss-Verzeichnis "deployments", in dem die Daten schon liegen. Allerdings führen die JBoss Tools ein exploded Deployment durch, und ich habe es noch nicht geschafft, den folgenden Aufruf daraufhin anzupassen.

Deshalb müssen wir uns eine EAR-Datei unserer Anwendung exportieren. Dies geht wie im Kapitel Export des Workspace beschrieben, nur dass wir keine Quelldateien benötigen.
Aus dem Verzeichnis der EAR-Datei ruft man dann folgendes auf:
cd C:\temp\wildfly-30.0.0.Final\bin
.\appclient.bat x:\PfadZurEarDatei\Stateless.ear#StatelessClient.jar

Man beachte das "#", das für Java eine Datei innerhalb von "Stateless.ear" angibt.

Hinweis:
Ein auszuführender Application Client muss wohl immer Teil einer EAR-Datei sein, die außer der JAR-Datei mit dem Application Client auch eine EJB-Jar-Datei enthält.

Wenn man alles richtig gemacht hat, führt das zu dieser Ausgabe:
C:\Temp\wildfly-30.0.0.Final\bin>appclient.bat c:\temp\Stateless.ear#StatelessClient.jar
Calling "C:\Temp\wildfly-30.0.0.Final\bin\appclient.conf.bat"
JAVA_HOME is not set. Unexpected results may occur.
Set JAVA_HOME to the directory of your local JDK to avoid this message.
20:50:40,716 INFO  [org.jboss.modules] (main) JBoss Modules version 2.1.2.Final
20:50:41,114 INFO  [org.jboss.msc] (main) JBoss MSC version 1.5.2.Final
20:50:41,118 INFO  [org.jboss.threads] (main) JBoss Threads version 2.4.0.Final
20:50:41,201 INFO  [org.jboss.as] (MSC service thread 1-1) WFLYSRV0049: WildFly Full 30.0.0.Final (WildFly Core 22.0.1.Final) starting
20:50:41,364 INFO  [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0282: Server is starting with graceful startup disabled; external requests may receive failure responses until startup completes.
20:50:41,629 INFO  [org.wildfly.security] (Controller Boot Thread) ELY00001: WildFly Elytron version 2.2.2.Final
20:50:42,738 INFO  [org.jboss.as.controller.management-deprecated] (ServerService Thread Pool -- 13) WFLYCTL0028: Attribute 'use-transaction-setup-provider' in the resource at address '/subsystem=ee/context-service=default' is deprecated, and may be removed in a future version. See the attribute description in the output of the read-resource-description operation to learn more about the deprecation.
20:50:42,776 WARN  [org.jboss.as.txn] (ServerService Thread Pool -- 31) WFLYTX0013: The node-identifier attribute on the /subsystem=transactions is set to the default value. This is a danger for environments running multiple servers. Please make sure the attribute value is unique.
20:50:42,818 INFO  [org.jboss.as.naming] (ServerService Thread Pool -- 29) WFLYNAM0001: Activating Naming Subsystem
20:50:42,834 INFO  [org.jboss.as.webservices] (ServerService Thread Pool -- 32) WFLYWS0002: Activating WebServices Extension
20:50:42,914 INFO  [org.wildfly.iiop.openjdk] (ServerService Thread Pool -- 24) WFLYIIOP0001: Activating IIOP Subsystem
20:50:42,923 INFO  [org.jboss.as.connector] (MSC service thread 1-6) WFLYJCA0009: Starting Jakarta Connectors Subsystem (WildFly/IronJacamar 3.0.4.Final)
20:50:42,941 INFO  [org.jboss.as.naming] (MSC service thread 1-7) WFLYNAM0003: Starting Naming Service
20:50:42,951 INFO  [org.jboss.as.connector.subsystems.datasources] (ServerService Thread Pool -- 20) WFLYJCA0004: Deploying JDBC-compliant driver class org.h2.Driver (version 2.2)
20:50:42,960 INFO  [org.jboss.as.connector.deployers.jdbc] (MSC service thread 1-1) WFLYJCA0018: Started Driver service with driver-name = h2
20:50:43,253 INFO  [org.xnio] (MSC service thread 1-2) XNIO version 3.8.11.Final
20:50:43,270 INFO  [org.xnio.nio] (MSC service thread 1-2) XNIO NIO Implementation Version 3.8.11.Final
20:50:43,347 INFO  [org.jboss.remoting] (MSC service thread 1-2) JBoss Remoting version 5.0.27.Final
20:50:43,353 INFO  [org.jboss.ws.common.management] (MSC service thread 1-7) JBWS022052: Starting JBossWS 7.0.0.Final (Apache CXF 4.0.0)
20:50:43,476 INFO  [org.jboss.as.connector.subsystems.datasources] (MSC service thread 1-3) WFLYJCA0001: Bound data source [java:jboss/datasources/ExampleDS]
20:50:43,518 INFO  [org.wildfly.iiop.openjdk] (MSC service thread 1-8) WFLYIIOP0009: CORBA ORB Service started
20:50:43,591 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 30.0.0.Final (WildFly Core 22.0.1.Final) started in 2610ms - Started 133 of 141 services (16 services are lazy, passive or on-demand) - Server configuration file in use: appclient.xml
20:50:43,604 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-7) WFLYSRV0027: Starting deployment of "Stateless.ear" (runtime-name: "Stateless.ear")
20:50:43,756 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-3) WFLYSRV0207: Starting subdeployment (runtime-name: "StatelessWeb.war")
20:50:43,756 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-5) WFLYSRV0207: Starting subdeployment (runtime-name: "StatelessEJB.jar")
20:50:43,756 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-2) WFLYSRV0207: Starting subdeployment (runtime-name: "StatelessClient.jar")
20:50:43,959 INFO  [org.jboss.as.ejb3.deployment] (MSC service thread 1-5) 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

20:50:43,984 INFO  [org.jboss.as.server] (Thread-44) WFLYSRV0010: Deployed "Stateless.ear" (runtime-name : "Stateless.ear")
20:50:43,993 INFO  [org.jboss.ejb.client] (Thread-51) JBoss EJB Client version 5.0.5.Final
20:50:44,446 INFO  [stdout] (Thread-51) Calculated volume: 350.0, surface: 310.0
20:50:44,455 INFO  [org.jboss.as.connector.subsystems.datasources] (MSC service thread 1-5) WFLYJCA0010: Unbound data source [java:jboss/datasources/ExampleDS]
20:50:44,458 INFO  [org.jboss.as.connector.deployers.jdbc] (MSC service thread 1-5) WFLYJCA0019: Stopped Driver service with driver-name = h2
20:50:44,480 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-5) WFLYSRV0208: Stopped subdeployment (runtime-name: StatelessEJB.jar) in 31ms
20:50:44,480 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-3) WFLYSRV0208: Stopped subdeployment (runtime-name: StatelessClient.jar) in 31ms
20:50:44,482 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-2) WFLYSRV0208: Stopped subdeployment (runtime-name: StatelessWeb.war) in 32ms
20:50:44,485 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-1) WFLYSRV0028: Stopped deployment Stateless.ear (runtime-name: Stateless.ear) in 36ms
20:50:44,489 INFO  [org.jboss.as] (MSC service thread 1-3) WFLYSRV0050: WildFly Full 30.0.0.Final (WildFly Core 22.0.1.Final) stopped in 40ms
Und mittendrin steht die Konsolenausgabe unserer Anwendung:
Calculated volume: 350.0, surface: 310.0

Auf der Serverkonsole findet sich die EJB-seitige Debuggingausgabe.
Hinweis:
Im Beispiel hat der Application Client auf eine EJB im lokalen Server zugegriffen. Wenn man auf einen remote Server zugreifen möchte, geht das gemäß der Anleitung, die ich zu Beginn dieses Kapitels verlinkt hatte.
Allerdings führt der dort beschriebene Aufruf "./appclient.sh --host=hostname myear.ear#appClient.jar" zu einer Fehlermeldung, da die Anleitung heillos veraltet ist.

java.lang.NullPointerException: Cannot invoke "String.hashCode()" because "" is null
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.getResult(EJBClientInvocationContext.java:620)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.getResult(EJBClientInvocationContext.java:551)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.protocol.remote.RemotingEJBClientInterceptor.handleInvocationResult(RemotingEJBClientInterceptor.java:57)
        ...
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.getResult(EJBClientInvocationContext.java:551)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.awaitResponse(EJBClientInvocationContext.java:1003)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBInvocationHandler.invoke(EJBInvocationHandler.java:182)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBInvocationHandler.invoke(EJBInvocationHandler.java:116)
        at deployment.Stateless.ear.StatelessEJB.jar/jdk.proxy6/jdk.proxy6.$Proxy14.computeCuboidVolume(Unknown Source)
        at deployment.Stateless.ear.StatelessClient.jar//de.hsrm.jakartaee.knauf.stateless.GeometricModelApplicationClient.main(GeometricModelApplicationClient.java:29)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.jboss.as.appclient@30.0.0.Final//org.jboss.as.appclient.service.ApplicationClientStartService$1.run(ApplicationClientStartService.java:89)
        at java.base/java.lang.Thread.run(Thread.java:842)
Auf das Anhängen des Port "8080" half nicht: appclient.bat --host=localhost:8080 c:\temp\Stateless.ear#StatelessClient.jar. Hier kam ein anderer Fehler:
jakarta.ejb.NoSuchEJBException: EJBCLIENT000079: Unable to discover destination for request for EJB StatelessEJBLocator for "Stateless/StatelessEJB/GeometricModelBean", view is interface de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote, affinity is None
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.getResult(EJBClientInvocationContext.java:620)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.getResult(EJBClientInvocationContext.java:551)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.protocol.remote.RemotingEJBClientInterceptor.handleInvocationResult(RemotingEJBClientInterceptor.java:57)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.getResult(EJBClientInvocationContext.java:622)
        ...
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.getResult(EJBClientInvocationContext.java:551)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBClientInvocationContext.awaitResponse(EJBClientInvocationContext.java:1003)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBInvocationHandler.invoke(EJBInvocationHandler.java:182)
        at org.jboss.ejb-client@5.0.5.Final//org.jboss.ejb.client.EJBInvocationHandler.invoke(EJBInvocationHandler.java:116)
        at deployment.Stateless.ear.StatelessEJB.jar/jdk.proxy6/jdk.proxy6.$Proxy14.computeCuboidVolume(Unknown Source)
        at deployment.Stateless.ear.StatelessClient.jar//de.hsrm.jakartaee.knauf.stateless.GeometricModelApplicationClient.main(GeometricModelApplicationClient.java:29)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.base/java.lang.reflect.Method.invoke(Method.java:568)
        at org.jboss.as.appclient@30.0.0.Final//org.jboss.as.appclient.service.ApplicationClientStartService$1.run(ApplicationClientStartService.java:89)
        at java.base/java.lang.Thread.run(Thread.java:842)
Es klappt mit "appclient.bat --host=http-remoting://localhost:8080 myear.ear#appClient.jar"
(oder alternativ: "-H=http-remoting://localhost:8080")

WildFly öffnet nur den Http-Port, für die eigentliche Kommunikation wird das "Http upgrade"-Verfahren (http://en.wikipedia.org/wiki/HTTP/1.1_Upgrade_header) verwendet, bei dem ein Http-Request an den Server geschickt wird, in dem ein Wechsel auf ein anderes Protokoll (über den gleichen Port) angefragt wird.




Export des Workspace

Um den kompletten Workspace so zu exportieren, dass er auf einem anderen Rechner wieder importiert werden kann, im Menü "File" den Punkt "Export..." wählen. Als "export destination" wählen wir "EAR file".
Export, Schritt 1
Im nächsten Schritt die zu exportierende EAR-Anwendung "Stateless" auswählen und eine Zieldatei für den Export angeben. Ich würde empfehlen diese Datei genauso zu benennen wie die EAR-Anwendung da der Dateiname beim Import als Vorschlag für die neu zu erstellen EAR-Anwendung verwendet wird.
Wichtig: den Haken bei "Export source files" setzen!
Die Bedeutung der Option "Optimize for a specific server runtime" hat sich mir bisher nicht erschlossen, scheinbar hat sie zumindest für WildFly keine Auswirkung.
Export, Schritt 2
Fertig!


Re-Import:

Falls wir das zu importierende Projekt bereits im Workspace haben dann sollten wir es vorher löschen.

Im Menü "File" ruft man "Import..." auf. Man wählt wiederum die Quelle "EAR file".
Import, Schritt 1
Im nächsten Schritt wählt man die Quelldatei (unsere eben exportierte EAR-Datei), den Namen der Ziel-EAR-Anwendung ("Stateless"), und den WildFly-Server.
Import, Schritt 2
Unsere Anwendung verfügt über keine Hilfsprojekte.
Import, Schritt 3
Im letzten Schritt werden die zu importierenden Module angezeigt - es sollten per Default alle abgehakt sein.
Import, Schritt 4


Jetzt muss leider ein Haufen Nacharbeiten durchgeführt werden, da das importierte Projekt vermutlich die falsche Java-Version definiert. Außerdem sollten die JakartaEE-Versionen geprüft werden.



Ohne Annotations

Alle Annotations des JakartaEE-Standards lassen sich komplett durch Deployment-Deskriptoren ersetzen.

Die Deployment-Deskriptoren gemäß JakartaEE-Standard (also ohne WildFly-spezifisches) kann man sich durch Eclipse generieren lassen.

Schritt 1: Enterprise Application
Beim Erzeugen der Enterprise Application müssen wir den Haken "Generate application.xml deployment descriptor" setzen. Achtung: trotz Setzen dieses Hakens erhalten EJB- und ApplicationClient-Modul keinen Deployment Deskriptor, hier ist die Web Tools Platform unvollständig.
New EAR application project

Im aktuellen Eclipse 2023-12 funktioniert diese Funktion nicht vollständig: die generierte Datei "application.xml" wird einen Validierungsfehler anzeigen, weil die "module"-Einträge fehlen:
<?xml version="1.0" encoding="UTF-8"?>
<application id="Application_ID" version="10"
  xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/application_10.xsd">
  <display-name>Stateless</display-name>
  <module>
    <web>
      <web-uri>StatelessWeb.war</web-uri>
      <context-root>StatelessWeb</context-root>
    </web>
  </module>
  <module>
    <java>StatelessClient.jar</java>
  </module>
  <module>
    <ejb>StatelessEJB.jar</ejb>
  </module>
</application> 
Deshalb händisch nacharbeiten und den fett markierten Teil nachtragen.


Schritt 2: Deployment-Deskriptoren
Jetzt fügen wir die Standard-Deskriptoren für EJB-Projekt und ApplicationClient zu.

Das Generieren der JakartaEE10-Deployment-Deskriptoren ist von der im Folgenden beschriebenen Funktionalität nicht mehr unterstützt.

Mit früheren Eclipse-Versionen hätte das so funktioniert: Rechtsklick auf das EJB-Projekt, "Java EE Tools" => "Generate Deployment Descriptor Stub" aufrufen. Das erzeugte eine Datei "ejb-jar.xml".
Generate Deployment Descriptor Stub
Analog wären wir für den Application Client vorgegangen.
Bugreport:
https://bugs.eclipse.org/bugs/show_bug.cgi?id=582797


Deployment-Deskriptoren im EJB-Projekt
Rechtsklick auf das Projekt im Verzeichnis "StatelessEJB\ejbModule\META-INF", dann im Contextmenü "New" => "Other..." wählen und unter "XML" den Typ "XML file" wählen. Im nächsten Schritt den Namen "ejb-jar.xml" angeben:
ejb-jar.xml (1)
Wir vereinfachen uns die Arbeit und lassen den Header der XML-Datei von Eclipse generieren (statt uns den Header selbst zusammenzusuchen). Dazu im nächsten Schritt "Create file using a DTD or XML schema file" wählen:
ejb-jar.xml (2)
Im nächsten Schritt nutzen wir die Tatsache, dass der JBoss Tools-Plugin alle Schemadateien in einen lokalen Katalog einfügt, und wählen die Option "Select XML Catalog entry". Dort tippen wir "ejb-jar" ins Suchfeld und wählen den Eintrag, der auf die Datei "ejb-jar_4-0.xsd" zeigt:
ejb-jar.xml (3)
Jetzt wird die Generierung so umgestellt, dass der Namespace "jakartaee" zum Default-Namespace wird und keinen Präfix erhält. Dazu in der Tabelle "Namespace information" die erste Zeile bearbeiten:
ejb-jar.xml (4)
Den Prefix leer löschen:
ejb-jar.xml (5)
Jetzt können wir die Datei generieren lassen.


Jetzt noch die Deklaration der Session Bean einfügen:
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar version="4.0"
  xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xml="http://www.w3.org/XML/1998/namespace"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/ejb-jar_4_0.xsd ">
  <display-name>StatelessEJB</display-name>
  <enterprise-beans>
    <session>
      <description><![CDATA[This is the Stateless SessionBean "GeometricModel",
        which might be used for simple geometric calculations.]]>
      </description>
      <display-name>GeometricModelBean</display-name>
      <ejb-name>GeometricModelBean</ejb-name>
      <business-local>de.hsrm.jakartaee.knauf.stateless.GeometricModelLocal</business-local>
      <business-remote>de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote</business-remote>
      <ejb-class>de.hsrm.jakartaee.knauf.stateless.GeometricModelBean</ejb-class>
      <session-type>Stateless</session-type>
    </session>
  </enterprise-beans>
</ejb-jar>
Pflichtangaben gibt es eigentlich keine mehr, da alles durch Annotations gesteuert werden kann. Wird ein Element "session" eingefügt dann muss allerdings zumindest ein "ejb-name" angegeben werden.


Durch Angabe des "ejb-name" können wir steuern unter welchem Namen JBoss die Bean im Default ins JNDI hängt ("Stateless/GeometricModelBean/local" und "Stateless/GeometricModelBean/remote", fett markiert ist der "ejb-name"). Der Name der Bean-Klasse, also "GeometricModelBean", ist gleichzeitig der Default wenn kein EJB-Name angegeben ist.


Deployment-Deskriptoren im Web-Projekt
Im Servlet können EJBs auch in der Annotation-freien Variante durch Injection gesetzt werden. Für JSPs ist dies leider nicht unterstützt (siehe Einschub im Annotation-Beispiel: kein Bestandteil des offiziellen Standards). Deshalb muss auf der JSP ein JNDI-Lookup durchgeführt werden.

"web.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns="https://jakarta.ee/xml/ns/jakartaee"
	xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
	version="6.0">
	<display-name>StatelessWeb</display-name>
	<servlet>
		<description>
		</description>
		<display-name>GeometricModelServlet</display-name>
		<servlet-name>GeometricModelServlet</servlet-name>
		<servlet-class>de.hsrm.jakartaee.knauf.stateless.GeometricModelServlet</servlet-class>
	</servlet>
	<servlet-mapping>
		<servlet-name>GeometricModelServlet</servlet-name>
		<url-pattern>/servlet/GeometricModelServlet</url-pattern>
	</servlet-mapping>
	<welcome-file-list>
		<welcome-file>index.html</welcome-file>
		<welcome-file>index.htm</welcome-file>
		<welcome-file>index.jsp</welcome-file>
		<welcome-file>default.html</welcome-file>
		<welcome-file>default.htm</welcome-file>
		<welcome-file>default.jsp</welcome-file>
	</welcome-file-list>
	<ejb-ref>
		<ejb-ref-name>ejb/GeometricModel</ejb-ref-name>
		<ejb-ref-type>Session</ejb-ref-type>
		<remote>de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote</remote>
		<injection-target>
			<injection-target-class>de.hsrm.jakartaee.knauf.stateless.GeometricModelServlet</injection-target-class>
			<injection-target-name>geometricModel</injection-target-name>
		</injection-target>
	</ejb-ref>
	<ejb-local-ref>
		<ejb-ref-name>ejb/GeometricModelLocal</ejb-ref-name>
		<ejb-ref-type>Session</ejb-ref-type>
		<local>de.hsrm.jakartaee.knauf.stateless.GeometricModelLocal</local>
		<injection-target>
			<injection-target-class>de.hsrm.jakartaee.knauf.stateless.GeometricModelServlet</injection-target-class>
			<injection-target-name>geometricModelLocal</injection-target-name>
		</injection-target>
	</ejb-local-ref>
</web-app>
Die Datei "jboss-web.xml" ist identisch zur obigen Variante mit Annotations und wird hier nicht nochmal gezeigt.

Deployment-Deskriptoren im Application-Client-Projekt
Auch hier müssen wir die Datei "application-client.xml" selbst erzeugen, wie oben bei "ejb-jar.xml":
application-client.xml (1)
Als Schema müssten wir eigentlich "application-client_10.xsd" angeben, aber aktuell fehlt diese Datei noch im JBoss Tools-Cache. Also nehmen wir die Version 9:
application-client.xml (2)
Natürlich auch hier den Default-Namespacepräfix für "jakartaee" leer löschen, bevor generiert wird.


Danach die Version auf "10" ändern und die Injection in den Client definieren:
<?xml version="1.0" encoding="UTF-8"?>
<application-client version="10"
  xmlns="https://jakarta.ee/xml/ns/jakartaee"
  xmlns:xml="http://www.w3.org/XML/1998/namespace"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/application-client_10.xsd ">
  <display-name>StatelessClient</display-name>
  <ejb-ref>
    <ejb-ref-name>ejb/GeometricModel</ejb-ref-name>
    <ejb-ref-type>Session</ejb-ref-type>
    <remote>de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote</remote>
    <injection-target>
      <injection-target-class>de.hsrm.jakartaee.knauf.stateless.GeometricModelApplicationClient</injection-target-class>
      <injection-target-name>geometricModel</injection-target-name>
    </injection-target>
  </ejb-ref>
</application-client>


Analog zur "jboss-web.xml" wird auch hier das Remote-Interface an einen JNDI-Namen gebunden. Diese geschieht über den JBoss-spezifischen Deskriptor "jboss-client.xml":
<?xml version="1.0" encoding="UTF-8"?>
<jboss-client xmlns="urn:jboss:jakartaee:1.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="urn:jboss:jakartaee:1.0
	                    https://www.jboss.org/schema/jbossas/jboss-client_9_0.xsd"
	version="9.0">
	<jndi-name>StatelessClient</jndi-name>
	<ejb-ref>
		<ejb-ref-name>ejb/GeometricModel</ejb-ref-name>
		<jndi-name>java:global/Stateless/StatelessEJB/GeometricModelBean!de.hsrm.jakartaee.knauf.stateless.GeometricModelRemote</jndi-name>
	</ejb-ref>
</jboss-client> 


Die modifizierte Version des Projekts gibt es hier: StatelessNoAnnotation.ear.
Beim Import sollte der "EAR name" von "StatelessNoAnnotation" auf "Stateless" geändert werden.

ACHTUNG: Dieses Projekt kann nicht neben dem obigen Stateless-Beispiel existieren!



Stand 17.06.2024
Historie:
20.12.2023: Erstellt aus JavaEE6-Version, angepaßt an JakartaEE10 und WildFly 30
01.05.2024: Importanleitung angepasst nach Eclipse-Bugfix
17.06.2024: Importanleitung erneut angepasst nach weiterem Eclipse-Bugfix.