Beispiel: Stateless Session Bean und Maven


Inhalt:

Anlegen des Projekts
Konfiguration des Projekts
Code
Plugins für den Build
Ausführen
Debuggen
Import
Maven-KnowHow


Für WildFly 33 und JakartaEE10: hier wird ein EAR-Projekt, bestehend aus EJB-Projekt und Web-Projekt, mittels Maven erstellt und deployed.


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

Das Beispiel basiert auf diesem Blog: https://onehumanunit.github.io/blog/2015/01/16/creating-an-application-with-wildfly-java-ee-rest-and-backbone-dot-js-part-1/

Anlegen des Projekts

Wir erstellen ein neues Projekt "Maven" - "Maven Project":
New Maven Project
Den Catalog "Maven Central" auswählen, dann das Artifact "wildfly-jakartaee-ear-archetype" wählen:
New Maven Project (Schritt 1)

Der Archetype befindet sich hier: https://repo.maven.apache.org/maven2/org/wildfly/archetype/wildfly-jakartaee-ear-archetype/
Der Quellcode liegt hier: https://github.com/wildfly/wildfly-archetypes

Im nächsten Schritt geben wir eine "GroupId" (kann man als Package betrachten) und die "ArtifactId" (Name der Anwendung) an. Im Beispiel sind das "de.hsrm.jakartaee.knauf" und "StatelessMaven". Eclipse schlägt dadurch als Root-Package der Anwendung "de.hsrm.jakartaee.knauf.StatelessMaven" vor - das sollten wir der Java-Konvention zuliebe in LowerCase ändern: "de.hsrm.jakartaee.knauf.statelessmaven".
New Maven Project (Schritt 2)

Da ich im letzten Schritt die Checkbox "run archetype generation interactively" gesetzt hatte (Defaultwert), muss man jetzt ein Konsolenfenster suchen, in dem man einmal "Y" eingeben muss:
New Maven Project (Schritt 3)

Das Ergebnis sieht so aus (vier Projekte, wobei "StatelessMaven" das Wurzelprojekt ist, das wiederum drei Unterprojekte enthält). In jedem Projekt gibt es eine "pom.xml"
Project Explorer
Aus Festplatte ist die Verzeichnisstruktur so, dass das Projekt "StatelessMaven" seine drei Unterprojekte in Unterverzeichnissen enthält.
Verzeichnisstruktur


Cleanup:
Der Archetype erzeugt einige Codefragmente, die nicht relevant sind (siehe Datei "README.txt" im Root-Projekt):


Konfiguration des Projekts

Die Hauptkonfiguration (Java-Version, WildFly-Version) steckt in der Datei "pom.xml" des Wurzelprojekts "StatelessMaven". Um daran etwas zu ändern, führt man einen Doppelklick auf die Datei durch => Es öffnet sich der "Maven POM Editor". In diesem wechseln wir auf den Karteireiter "pom.xml", um direkt die XML-Datei zu bearbeiten:
pom.xml bearbeiten

Die Java-Version ist hier definiert:
    <properties>
        ...
        <!-- maven-compiler-plugin -->
        <maven.compiler.release>11</maven.compiler.release>
    </properties>

Die WildFly-Abhängigkeiten werden an dieser Stelle definiert:
    <dependencyManagement>
        <dependencies>
            ....
            <!-- JBoss distributes a complete set of Jakarta EE 8 APIs including
                a Bill of Materials (BOM). A BOM specifies the versions of a "stack" (or 
                a collection) of artifacts. We use this here so that we always get the correct 
                versions of artifacts. Here we use the wildfly-jakartaee-8.0-with-tools stack
                (you can read this as the WildFly stack of the Jakarta EE 8 APIs -->
            <dependency>
                <groupId>org.wildfly.bom</groupId>
                <artifactId>wildfly-ee-with-tools</artifactId>
                <version>${version.jboss.bom}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
Diese BOM (="Bill of Materials") befindet sich im Maven-Repository unter
https://repo.maven.apache.org/maven2/org/wildfly/bom/wildfly-ee-with-tools/. Schaut man in dessen pom.xml, findet man die weiteren Abhängigkeiten - das wird über mehrere Schritte gehen.

Die konkrete WildFly-Version steht in den Properties:
    <properties>
        ...
        <!-- Define the version of the JBoss BOMs we want to import to specify tested stacks. -->
        <version.jboss.bom>34.0.1.Final</version.jboss.bom>

    </properties>

Diese WildFly-BOM stellt uns eine Sammlung aller JARs zur Verfügung, die zur Public API von WildFly gehören, darunter auch die Libraries des JavaEE-Standard (EJB, Web, ...). Wird in einer der "pom.xml"-Dateien der Untermodule dieses Projekts eine solche JAR-Datei referenziert, muss man keine Version deklarieren, da die Dependency im WildFly-BOM gefunden und automatisch die von WildFly angebotene Version verwendet wird. Man bindet also nur die Dependency unter ihrer GroupId/ArtifactId ein, und Maven kann sie automatisch auflösen.

Beim Wechsel auf eine neue WildFly-Version muss man theoretisch nur diese Versionsnummer ändern. Es kann aber sein, dass dadurch Dependencies in den Untermodulen ungültig werden, weil sie z.B. umbenannt wurden. Hier hat man also vermutlich weiteren Anpassungsaufwand.


Von Änderungen an "pom.xml" kriegt Eclipse nichts mit. Deshalb muss im Contextmenü von "StatelessMaven" der Punkt "Maven" => "Update Project" aufgerufen werden:
Update Project (1)
Im folgenden Fenster belassen wir alles auf den Defaults (alle Projekte gewählt!) und klicken auf "OK":
Update Project (2)


Code

Im EJB-Projekt werden im Package de.hsrm.jakartaee.knauf.statelessmaven.ejb zwei Klassen angelegt:
EJB-Projekt

Die eigentliche EJB:

package de.hsrm.jakartaee.knauf.statelessmaven.ejb;

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

@Stateless()
public class GeometricModelBean implements GeometricModelLocal
{
  protected static final Logger logger = Logger.getLogger(GeometricModelBean.class.getName());

  public GeometricModelBean()
  {
  }

  public double computeCuboidVolume(double a, double b, double c) throws IllegalArgumentException
  {
    logger.info("computeCuboidVolume with a = " + a + ", b = " + b + ", c = " + c);
    if (a <= 0 || b <= 0 || c <= 0)
      throw new IllegalArgumentException("Side length <= 0");

    return a * b * c;
  }

  public double computeCuboidSurface(double a, double b, double c) throws IllegalArgumentException
  {
    logger.info("computeCuboidSurface with a = " + a + ", b = " + b + ", c = " + c);
    if (a <= 0 || b <= 0 || c <= 0)
      throw new IllegalArgumentException("Side length <= 0");

    return 2 * (a * b + b * c + c * a);
  }
}
Local Interface

package de.hsrm.jakartaee.knauf.statelessmaven.ejb;
import jakarta.ejb.Local;

@Local
public interface GeometricModelLocal
{

  public double computeCuboidVolume(double a, double b, double c) throws IllegalArgumentException;

  public double computeCuboidSurface(double a, double b, double c) throws IllegalArgumentException;
}
Auf ein Remote Interface wird hier verzichtet. Auch die aus den früheren Beispielen bekannte InvalidParameterException wird hier durch die Standard-Exception IllegalArgumentException ersetzt.


Im Web-Projekt wird im Package de.hsrm.jakartaee.knauf.statelessmaven.web das Servlet zugefügt. Der Deployment-Deskriptor "web.xml" ist nicht nötig, da das Servlet die EJB per Injection lädt.
Web-Projekt

Servlet:

package de.hsrm.jakartaee.knauf.statelessmaven.web;

import java.io.IOException;
import java.io.PrintWriter;

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 de.hsrm.jakartaee.knauf.statelessmaven.ejb.GeometricModelLocal;

@WebServlet(name="GeometricModelServlet", displayName="GeometricModelServlet",
urlPatterns={"/servlet/GeometricModelServlet"})
public class GeometricModelServlet extends HttpServlet
{
  @EJB
  private GeometricModelLocal geometricModelLocal;

  private static final long serialVersionUID = 1L;

  public GeometricModelServlet()
  {
    super();
  }

  @Override
  protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
  {
    this.doPost(request, 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 local interface:

        double dblVolume = this.geometricModelLocal.computeCuboidVolume(dblA, dblB, dblC);
        double 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();
  }
}


Plugins für den Build

Im Folgenden werden die Plugins beschrieben, die über "pom.xml" eingebunden werden.

Der WildFly Maven Plugin:
Für das Ausführen wird ein Wildfly-spezifischer Plugin verwendet, der die EAR-Datei erzeugt und sie in "standalone.xml" einträgt.
Es handelt sich um den "WildFly Maven Plugin":
https://repository.jboss.org/nexus/content/groups/public/org/wildfly/plugins/wildfly-maven-plugin/.

Die Doku findet sich hier: https://docs.jboss.org/wildfly/plugins/maven/latest/. Der Quellcode ist unter https://github.com/wildfly/wildfly-maven-plugin zu finden.

In "pom.xml" des Wurzelprojekts ist er deklariert:
    <properties>
        ...
        <version.wildfly.maven.plugin>5.0.1.Final</version.wildfly.maven.plugin>
        ...
    </properties>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.wildfly.plugins</groupId>
                    <artifactId>wildfly-maven-plugin</artifactId>
                    <version>${version.wildfly.maven.plugin}</version>
                    <inherited>true</inherited>
                    <configuration>
                        <skip>true</skip>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

Es werden außerdem weitere Maven-Plugins verwendet.
In "pom.xml" des Wurzelprojekts sind ihre Versionen deklariert:

        <version.compiler.plugin>3.13.0</version.compiler.plugin>
        <version.ear.plugin>3.3.0</version.ear.plugin>
        <version.ejb.plugin>3.2.1</version.ejb.plugin>
        <version.surefire.plugin>3.0.0</version.surefire.plugin>
        <version.failsafe.plugin>3.5.2</version.failsafe.plugin>
        <version.war.plugin>3.4.0</version.war.plugin>
Der Maven EAR Plugin ist in "pom.xml" des EAR-Projekts definiert und generiert die EAR-Datei:

    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
			<!--EAR plugin: format of output file -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-ear-plugin</artifactId>
                <version>${version.ear.plugin}</version>
                <configuration>
                    <!-- Tell Maven we are using Jakarta EE -->
                    <version>8</version>
                    <!-- Use Jakarta EE ear libraries as needed. Jakarta EE ear libraries
                        are in easy way to package any libraries needed in the ear, and automatically
                        have any modules (EJB-JARs and WARs) use them -->
                    <defaultLibBundleDir>lib</defaultLibBundleDir>
                    <modules>
                    <!-- Default context root of the web app is /StatelessMaven-web.
                        If a custom context root is needed, uncomment the following snippet to
                        register our War as a web module and set the contextRoot property -->
                    <!--
                    <webModule>
                        <groupId>${project.groupId}</groupId>
                        <artifactId>StatelessMaven-web</artifactId>
                        <contextRoot>/StatelessMaven</contextRoot>
                    </webModule>
                    -->
                    </modules>
                    <outputFileNameMapping>@{artifactId}@@{dashClassifier?}@.@{extension}@</outputFileNameMapping>
                </configuration>
            </plugin>
            ...
        </plugins>
    </build>
Webseite des Plugins: https://maven.apache.org/plugins/maven-ear-plugin/.
Im Maven-Repository ist er zu finden unter https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-ear-plugin/

Die Definition des "outputFileName" bewirkt, dass das WAR-Modul "StatelessMaven-web.war" und das EJB-Modul "StatelessMaven-ejb.jar" heißen. Normalerweise würden sie so heißen: "de.hsrm.cs.javaee8-StatelessMaven-ejb-0.0.1-SNAPSHOT.jar" (also "groupId-artifactId-version.jar"), und das ist unhandlich, wenn man z.B. im Web-Projekt EJB-References in "web.xml" definiert.
<outputFileNameMapping>@{artifactId}@.@{extension}@</outputFileNameMapping>
Siehe dazu: http://maven.apache.org/plugins/maven-ear-plugin/ear-mojo.html#outputFileNameMapping.


Der Maven EJB Plugin ist in "pom.xml" des EJB-Projekts definiert und kümmert sich um das Erstellen des EJB-Jar:
    <build>
        <finalName>${project.artifactId}</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-ejb-plugin</artifactId>
                <version>${version.ejb.plugin}</version>
                <configuration>
                    <!-- Tell Maven we are using EJB 4.0 -->
                    <ejbVersion>4.0</ejbVersion>
                </configuration>
            </plugin>
        </plugins>
    </build>
Webseite des Plugins: https://maven.apache.org/plugins/maven-ejb-plugin/.
Im Maven-Repository ist er zu finden unter https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-ejb-plugin/


Der Maven War Plugin ist in "pom.xml" des Web-Projekts definiert und erstellt die WAR-Datei:
	<build>
		<finalName>${project.artifactId}</finalName>
		<plugins>
			<plugin>
				<artifactId>maven-war-plugin</artifactId>
				<version>${version.war.plugin}</version>
				<configuration>
					<!-- Jakarta EE doesn't require web.xml, Maven needs to catch up! -->
					<failOnMissingWebXml>false</failOnMissingWebXml>
				</configuration>
			</plugin>
		</plugins>
	</build>
Webseite des Plugins: https://maven.apache.org/plugins/maven-war-plugin/.
Im Maven-Repository ist er zu finden unter https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-war-plugin/


Der Maven Compiler Plugin übernimmt die eigentliche Compilierung.

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${version.compiler.plugin}</version>
                <configuration>
                    <!-- put your configurations here -->
                </configuration>
            </plugin>
            ...
        </plugins>
    </build>
Webseite des Plugins: https://maven.apache.org/plugins/maven-compiler-plugin/.
Im Maven-Repository ist er zu finden unter https://repo.maven.apache.org/maven2/org/apache/maven/plugins/maven-compiler-plugin/

Der Maven Surefire Plugin und der Maven Failsafe Plugin dienen zum Ausführen von Unit-Tests und sind erst in folgenden Beispielen relevant.

Ausführen

Jetzt wird eine "Run Configuration" angelegt, die die entsprechenden Maven-Aufrufe enthält. Dazu klingt man in der Toolbar auf das DropDown des "Run"-Buttons und wählt "Run Configurations" aus:
Run Configuration (1)
Links in der Liste wird unter "Maven Build" ein neuer Eintrag zugefügt:
Run Configuration (1a)
Es werden diese Einstellungen vorgenommen:
Das Ergebnis sieht so aus:
Run Configuration (2)

Jetzt können wir auf "Run" klicken, und das Projekt wird erzeugt und deployed. Achtung: der WildFly-Server muss außerhalb (über Eclipse oder durch Aufruf der "standalone.bat") gestartet worden sein!
Die Ergebnisse des Build (EJB-JAR, Web-WAR, EAR) liegen pro Projekt im Unterverzeichnis "target". Hier ein Screenshot aus dem EAR-Projekt:
Inhalt von target

Die Java-Version in dem Maven-"Run Configuration" sollte der Java-Version entsprechen, die in "pom.xml" des Root-Projekts in den Properties eingetragen ist:
    <properties>
        ...
        <!-- maven-compiler-plugin -->
        <maven.compiler.release>11</maven.compiler.release>
    </properties>

Aber es würde auch mit einer neueren Java-Version funktionieren.

Die Java-Version kann man in der Run Configuration auf dem Karteireiter "JRE" einstellen:
Run Configuration (JRE)


Sofern wir uns Apache Maven herunterladen, können wir die Maven-Aufrufe auch außerhalb von Eclipse ausführen. Dazu wechseln wir ins Verzeichnis des Projekts und rufen "mvn.cmd" auf, wobei die auszuführenden Goals als Parameter übergeben werden.
Im Beispiel musste ich "JAVA_HOME" noch auf das korrekte JDK stellen, da per Default nur eine Java Runtime registriert ist.
C:\Temp\workspace\StatelessMaven>set JAVA_HOME=C:\Temp\jdk-11.0.25\

C:\Temp\workspace\StatelessMaven>c:\temp\apache-maven-3.9.9\bin\mvn clean install wildfly:deploy


Wir erreichen das Servlet unter folgender Adresse:
http://localhost:8080/StatelessMaven-web/servlet/GeometricModelServlet


Die Anwendung wird vom Maven-WildFly-Plugin in "standalone.xml" eingetragen:
    <deployments>
        <deployment name="StatelessMaven-ear.ear" runtime-name="StatelessMaven-ear.ear">
            <content sha1="6c0fbe72e2b7c6acc4b177e09a8c4bc7b001b086"/>
        </deployment>
    </deployments>
Diese Datei verweist auf das Unterverzeichnis "%WILDFLY_HOME%\standalone\data\content\6c\0fbe72e2b7c6acc4b177e09a8c4bc7b001b086". Dort liegt die Datei "content", und das ist die umbenannte EAR-Datei.

Damit die Anwendung bei zukünftigen Serverstarts nicht geladen wird, wenn man nicht daran entwickelt, legen wir uns eine zweite Run Configuration zum Aufräumen an.
Hier wird folgendes Goal eingetragen:
wildfly:undeploy
Run Configuration (undeploy)

Debuggen

Faszinierenderweise ist es ohne Probleme möglich, die per Maven erzeugte Anwendung zu debuggen. Dazu muss man nur seine Breakpoints in Eclipse setzen und den WildFly-Server, auf den im Rahmen des Maven-Goal "wildfly:deploy" deployed wird, über Eclipse und die "Servers"-View im Deploy-Modus starten:
Debug
Vermutlich wird beim ersten Erreichen eines Breakpoints statt des Quellcodes ein Fenster mit der Meldung "Source not found" geöffnet:
Source not found
Dann klickt man auf "Edit Source Lookup Path". Es öffnet sich dieser Dialog:
Source Lookup Path
Man klickt auf "Add" und wählt in dem erscheinenden Dialog "Java Project":
Add Source
Jetzt wählt man das Projekt aus, in dem die Klasse liegt, in der der Breakpoint gesetzt wurde:
Project Selection


Import

Um das Beispielprojekt in Eclipse zu importieren, sind folgende Schritte nötig:
1) Verlinkte zip-Datei in den Workspace entpacken.
2) In Eclipse: Menü "File" -> "Import" wählen. Dort unter "Maven" die Option "Existing Maven Projects" wählen:
Import existing Maven Projects
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 "StatelessMaven" und dessen Unterprojekte. Diese werden abgehakt.
Import StatelessMaven


Maven-KnowHow

Allgemein
Maven lädt beim Compile/Deploy/... einen Haufen Dateien herunter. Diese landen im Benutzer-Profil im Verzeichnis "%USERPROFILE%\.m2\repository\".


Eclipse: Maven Console
Es gibt eine Konsole, die es ausgibt wenn Maven z.B. Dependencies ins lokale Repository lädt:
Maven Console


Eclipse: Repository Index
Dieser Abschnitt ist nur relevant, wenn man Dependencies über die GUI hinzufügen will. Eigentlich ist dies nicht nötig, da man die Einträge auch händisch in "pom.xml" eintragen kann ;-).
Es scheint so, dass sogar die m2e-Entwickler von der Verwendung der im Folgenden beschriebene Option aufgrund der Verschwendung von Festplattenplatz abraten, siehe Diskussion z.B. in
https://bugs.eclipse.org/bugs/show_bug.cgi?id=403464.

Damit das Maven-Repository beim Hinzufügen von Dependencies durchsucht werden kann, muss man in den "Preferences" unter "Maven" die Option "Download repository index updates on startup" einschalten:
Download repository index
Das führt dazu, dass beim nächsten Eclipse-Start der zentrale Index heruntergeladen wird:
Download repository index
Den findet man hier: "%USERPROFILE%\.m2\repository\.cache\m2e\1.14.0\".
Es werden dabei (Stand: Jan 2020) ca. 800 MB heruntergeladen, danach arbeitet der Rechner über 5 Minuten (trotz SSD) und es wurden 4,5 GB in ein Unterverzeichnis gelegt. Spätere Updates des Index sollten inkrementell sein und schneller gehen.
Auch im Workspace gibt es eine Kopie (?) des Repository-Index in "\.metadata\.plugins\org.eclipse.m2e.core", siehe Erklärungen in https://bugs.eclipse.org/bugs/show_bug.cgi?id=403464#c3.

Jetzt können wir Dependencies zufügen: Dazu die Datei "pom.xml" mit dem "Maven POM Editor" öffnen. Dort auf den Karteireiter "Dependencies" gehen und auf "Add..." klicken:
Dependencies

Jetzt tippt man im Suchfeld "Enter groupId, artifactId or sha1 prefix or pattern (*)" den Suchtext ein. Achtung: die folgende Suche kann einzige Zeit dauern - siehe Eclipse-Progress-Anzeige ganz rechts unten. Im Beispiel wird "commons-lang3" zugefügt - eine sehr nützliche Bibliothek mit z.B. String-Funktionen:
Dependency commons-lang3





Stand 21.12.2024
Historie:
21.12.2024: erstellt aus JavaEE8-Beispiel