Beispiel: Einfache Entity Bean


Inhalt:

Der Weg des Assistenten: Project Facet "Java Persistence API"
Der Weg für Harte: Anlegen der Entity Bean ohne JPA
Der Weg für Harte: persistence.xml
Der Weg des Assistenten: Anlegen der Entity Bean mit JPA
Anlegen der Session Bean
Application Client
Datenbank
jboss-deployment-structure.xml
Das Ende des ApplicationClient
Ausführen des Clients
Logging der SQL-Parameter
Ohne Annotations
Manueller Primary Key
Zugriff auf MySQL-Datenbank
Troubleshooting

Beispiel für WildFly 8.2 und eine Entity Bean, auf die per Applicationclient zugegriffen wird.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man im Stateless-Beispiel - nach dem Import sollte die JPA-Facet zugefügt werden): KuchenSimple.ear

Falls mit der Project Facet "Java Persistence API" gearbeitet werden soll, muss diese nach jedem Import dem Projekt neu zugefügt werden (identische Schritte wie beim Projekt-Erstellen)

Aufbau des Beispieles


a) Entity Bean-Klasse
b) Zugriff auf die Entity-Bean erfolgt über eine Stateless Session Bean.
c) Ein Application Client greift auf die Session Bean zu.


Das Beispiel besteht aus einem "EAR Application Project" mit dem Namen "KuchenSimple" sowie einem EJB-Projekt und einem Anwendungsclientprojekt.


Der Weg des Assistenten: Project Facet "Java Persistence API"

Der folgende Abschnitt ist optional, man kann seine Entity Beans sowie den nötigen Deployment Deskriptor "persistence.xml" auch händisch erzeugen, und der Aufwand dürfte sogar geringer sein als beim Klicken über Assistenten ;-).

Schritt 1: Rechtsklick auf das EJB-Projekt, in den "Properties" den Punkt "Projekt Facets" auswählen.
Den Haken bei "Java Persistence" setzen, die Version wird auf "2.0" gesetzt.
Java Persistence 2.0 facet (1)
Anschließend auf "Further Configuration available" klicken.
Hier bleiben alle Einstellungen auf ihren Default-Werten (da wir keinen Zugriff auf eine vorhandene Datenbank brauchen, und uns darauf verlassen, dass der JBoss eine JPA-Implementation mitbringt). Eigentlich hätte man sich den Punkt sparen können, aber (weiter nach dem Screenshot)...
Java Persistence 2.0 facet (2)

Anmerkung:
Führt man diesen Schritt nicht durch, führt jede vorhandene EntityBean zu einem nervigen Validierungsfehler wie diesem:

Class "de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean" is managed, but is not listed in the persistence.xml file

Grund ist wohl, dass hier die Option "Discover annotated classes automatically" standardmäßig angeschaltet wird. Ignoriert man diesen Schritt, scheint der Default auf "Annotated classes must be listed in persistence.xml" zu stehen. Bugreport:
https://bugs.eclipse.org/bugs/show_bug.cgi?id=460162

Nachträglich kann man diesen Schritte erreichen über die "Project Properties":
Project Properties (JPA)

Jetzt wird im Projekt eine Datei "META-INF\persistence.xml" erzeugt, und es taucht im "Project Explorer" ein Punkt "JPA Content" auf.
Java Persistence 2.0 facet (3)

Die Datei "persistence.xml" öffnen wir (per Doppelklick, oder Rechtsklick => "Open With" => "Persistence XML Editor"), und ändern auf dem Karteireiter "General" den Name auf "kuchenPersistenceUnit". Der einzige Grund hierfür ist allerdings, dass ich dies im Vorjahresbeispiel genauso gehalten hatte ;-).

Persistence Unit Name

Auf dem Karteireiter "Connection" können wir beim "JTA Data Source Name" den Wert java:jboss/datasources/ExampleDS eintragen (Erklärung im Abschnitt Der Weg für Harte: persistence.xml).
JTA Data Source Name

Auf dem Karteireiter "Properties" werden die Properties "hibernate.hbm2ddl.auto" mit dem Wert "create-drop" und "hibernate.show_sql" mit dem Wert "true" angelegt (Erklärung im Abschnitt Der Weg für Harte: persistence.xml).
Properties
Auf dem Karteireiter "Source" können wir uns das Ergebnis anschauen:
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
	<persistence-unit name="kuchenPersistenceUnit">
		<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
		<properties>
			<property name="hibernate.hbm2ddl.auto" value="create-drop" />
			<property name="hibernate.show_sql" value="true"></property>
		</properties>
	</persistence-unit>
</persistence>

Das Hinzufügen der "Java Persistence"-Facet führt zu einer Warnung "No connection specified for project. No database-specific validation will be performed.".
Der Warnung zu folgen und eine Connection anzulegen, ist nicht sinnvoll, denn wir definieren durch unsere EJBs schließlich die Struktur der Datenbank, nicht umgekehrt. Ein Vergleich der Entity-Beans und ihrer Felder mit einer Datenbankstruktur wäre also nicht sinnvoll.

Wir können diese Warnung abschalten: dazu gehen wir in die Projekt-Properties in den Bereich "JPA" -> "Errors/Warnings", setzen dort die Checkbox "Enable project specific settings" und schalten im Bereich "Project" die Option "No connection specified for project" von "Warning" auf "Ignore".
JPA Validation
Dies könnten wir auch global für alle Projekte abschalten in den "Preferences" unter "Java Persistence" -> "JPA"


JavaEE7 hat eine Default-Datenquelle java:comp/DefaultDataSource definiert, die man statt der oben verwendeten WildFly-spezifischen Datenquelle java:jboss/datasources/ExampleDS verwenden könnte.
Diese wird so eingebunden:

		<jta-data-source>java:comp/DefaultDataSource</jta-data-source>



Der Weg für Harte: Anlegen der Entity Bean ohne JPA

Wir fügen eine Klasse "KuchenSimpleBean" zu. Der Namenszusatz "Simple" kommt daher dass es noch weitere Beispiele kommen in denen Kuchen-Entity-Beans enthalten sind. Deshalb muss in jedem Beispiel ein eindeutiger Name für das Objekt "Kuchen" vergeben sein damit es keine Konflikte beim JNDI-Namen oder bei Tabellennamen gibt.


Es wird eine neue Klasse "KuchenSimpleBean" im Package "de.fhw.komponentenarchitekturen.knauf.kuchen" angelegt. Wichtig ist dass diese Klasse das Interface "java.io.Serializable" implementiert!
Neue Entity Bean (1)

Die Bean-Klasse bekommt die Annotation "@javax.persistence.Entity". Da wir die EJB-QL-Strings zum Finden der Instanzen nicht hartcodiert in der Session-Bean haben wollen deklarieren wir bei der Bean-Klasse eine "@javax.persistence.NamedQuery".
@NamedQuery (name="findAllKuchen", query="select o from KuchenSimpleBean o")
@Entity()
public class KuchenSimpleBean implements Serializable
{
Anmerkung 1:
Wenn mehr als eine Query nötig sind muss man diese in eine Annotation "@NamedQueries" packen:
@NamedQueries({
  @NamedQuery (name="findAllKuchen", query="select o from KuchenSimpleBean o"),
  @NamedQuery (name="findByName", query="select o from KuchenSimpleBean o where o.name like ?1")
  })

Zwei Felder sowie die zugehörigen Getter und Setter werden zugefügt:
  private Integer intId;
  private String strName;
  
  @Column()
  @Id ()
  @GeneratedValue () 
  public Integer getId()
  {
    return this.intId;
  }

  public void setId(Integer int_Id)
  {
    this.intId = int_Id;
  }
  
  @Column()
  public String getName()
  {
    return this.strName;
  }

  public void setName(String str_Name)
  {
    this.strName = str_Name;
  }
  
  @Override
  public String toString()
  {
    return this.strName;
  }
  
Die Properties werden mit der Annotation @javax.persistence.Column als persistente Bean-Felder markiert. Die Property "ID" wird mit der Annotation @javax.persistence.Id als Primary-Key-Feld markiert. Der Wert soll vom Container automatisch generiert werden (@javax.persistence.GeneratedValue). Das Verfahren der Generierung bleibt dem Server überlassen, beim JBoss und der H2-Datenbank wird daraus eine Auto-ID-Spalte (sprich: beim Insert wird die ID erzeugt).

Die Methode toString wird überladen und gibt den Namen des Kuchens zurück. Das hat den Vorteil, dass wir die Kuchen im Client als Objekt in eine Listbox packen und auch wieder auslesen können.


Der Weg für Harte: persistence.xml

Für Entity Beans muss eine Persistence Unit deklariert werden. Dies geschieht über eine Datei "persistence.xml" im Unterverzeichnis "META-INF" des EJB-Projekts.
Sie hat diesen Inhalt:
	<?xml version="1.0" encoding="UTF-8"?>
	<persistence xmlns="http://java.sun.com/xml/ns/persistence"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
		version="2.0">
		<persistence-unit name="kuchenPersistenceUnit">
			<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
			<properties>
				<!-- Setzen dieser Property aktiviert das automatische Tabellen-Generieren und Löschen beim Deploy! -->
				<property name="hibernate.hbm2ddl.auto" value="create-drop" />
				<!-- SQL-Logging einschalten: -->
				<property name="hibernate.show_sql" value="true"></property>
			</properties>
		</persistence-unit>
	</persistence> 
Es wird eine "persistence-unit" namens "kuchenPersistenceUnit" deklariert. Sie ist verbunden mit einer JDBC-Datenquelle des Servers, die im JNDI unter dem Namen "java:jboss/datasources/ExampleDS" abgelegt ist und auf die JBoss-interne H2-Datenbank zeigt.
DefaultDS

Die Property "hibernate.hbm2ddl.auto" ist JBoss-spezifisch und legt fest dass Datenbanktabellen beim Deploy einer Bean erzeugt und beim Undeploy wieder gelöscht werden sollen. Ohne diesen Parameter müssten wir die Datenbanktabellen von Hand anlegen.
Die Property "hibernate.show_sql" gibt an dass SQL-Befehle ins Server-Log geschrieben werden sollen, und als netter Nebeneffekt auch auf die Server-Console in Eclipse. Damit haben wir eine gute Diagnosemöglichkeit falls Datenbankzugriffe Probleme machen.

Anmerkung:
Mit JavaEE7 wurden neue Properties eingeführt, die die JBoss/Hibernate-spezifische Property "hibernate.hbm2ddl.auto" ersetzen:

         <property name="javax.persistence.schema-generation.database.action" value="drop-and-create"/>
         <property name="javax.persistence.schema-generation.create-source" value="metadata"/>
         <property name="javax.persistence.schema-generation.drop-source" value="metadata"/>
Siehe JakartaEE Tutorial, Kapitel "Database Schema Creation":
https://eclipse-ee4j.github.io/jakartaee-tutorial/persistence-intro006.html


Der Weg des Assistenten: Anlegen der Entity Bean mit JPA

Im "Project Explorer" das EJB-Projekt wählen, Rechtsklick, "New" => "JPA Entity" wählen.
JPA Entity (1)
Name ("KuchenSimpleBean") und Package ("de.fhw.komponentenarchitekturen.knauf.kuchen") werden angegeben.
JPA Entity (2)
Im nächsten Dialog werden zwei Felder "id" (vom Typ java.lang.Integer) und "name" (vom Typ java.lang.String) angelegt.
Am Ende sollte der Dialog so aussehen:
-Das Feld "id" ist als Primary Key markiert.
-Der "Access Type" wird von "Field-based" (Annotations werden an die Feldvariablen gesetzt) auf "Property-based" geändert (Zugriff und Annotations nur über getter/setter).
Entity Properties
Hier der Dialog für das Hinzufügen des Felds "id":
Felder
Die so erzeugte Entity taucht jetzt auch im "Project Explorer" auf:
Project Explorer

Nachbearbeitung: Es muss eine Named Query zugefügt werden:
@NamedQuery (name="findAllKuchen", query="select o from KuchenSimpleBean o")
@Entity()
public class KuchenSimpleBean implements Serializable
{
Die Property "id" muss außerdem mit der Annotation javax.persistence.GeneratedValue versehen werden:
	@Id    
	@GeneratedValue()
	public Integer getId()
	{
		return this.id;
	}
Anschließend wird die toString überladen (siehe Abschnitt
Der Weg für Harte: Anlegen der Entity Bean ohne JPA).

Anlegen der Session Bean

Da der Entity-Manager für den Zugriff auf die Entity-Bean nicht in einem Application Client verwendet werden kann, müssen wir alle Zugriffe auf die Bean kapseln. Dazu verwenden wir eine Session Bean "KuchenWorkerBean" mit einem Remote Interface "KuchenWorkerRemote".
KuchenWorkerBean

Der Entity-Manager für den Zugriff auf die Entity Bean wird als vom Container "injected" Variable deklariert:
  @PersistenceContext(unitName="kuchenPersistenceUnit")
  private EntityManager entityManager = null;
Die Implementierung von "saveKuchen" sieht so aus:
  public void saveKuchen (KuchenSimpleBean kuchen)
  {
    this.entityManager.merge(kuchen);
  }
Wichtig ist dass hier "merge" und nicht "persist" genommen wird, siehe Abschnitt
Troubleshooting.

"deleteKuchen":
  public void deleteKuchen(KuchenSimpleBean kuchen)
  {
    kuchen = this.entityManager.find (KuchenSimpleBean.class, kuchen.getId() );
    this.entityManager.remove(kuchen);
  } 
Wichtig ist dass das zu löschende Objekt eventuell "detached" ist und deshalb vorher unter Container-Verwaltung gestellt werden muss, siehe Abschnitt Troubleshooting.

"getKuchen": Diese Methode verwendet die NamedQuery die wir in der Entity-Bean deklariert haben:
  public List<KuchenSimpleBean> getKuchen()
  {
    Query query = this.entityManager.createNamedQuery("findAllKuchen");
    List<KuchenSimpleBean> listKuchen = query.getResultList();
    
    return listKuchen;
  } 
Anmerkung: wir hätten die Query hier auch direkt erzeugen können.

Query query = this.entityManager.createQuery("select o from KuchenSimpleBean o");

Dadurch hätten wir allerdings eine Abhängigkeit vom Namen "KuchenSimpleBean" gebaut die weit entfernt von der eigentlichen Klasse ist!

Anmerkung: die Zuweisung der ResultList an "List<KuchenSimpleBean>" führt zu einer Compilerwarnung (Type safety: The expression of type List needs unchecked conversion to conform to List<KuchenSimpleBean>). Dies könnten wir umgehen indem wir vor die entsprechende Zeile folgende Annotation eintragen:
    @SuppressWarnings("unchecked")
    List<KuchenSimpleBean> listKuchen = query.getResultList(); 


Application Client

Zuallerest einmal werfen die die Klasse "Main" im Default-Package weg.

Der Client muss die EJB-Jars referenzieren (siehe dazu Anleitung in den vorherigen Beispielen).

Folgende GUI-Elemente sind vorhanden:
-de.fhw.komponentenarchitekturen.knauf.kuchen.FrameKuchen ist das Hauptfenster der Anwendung. Er besteht aus einer JList auf einem JScrollPane und drei Buttons "Neu", "Bearbeiten" und "Löschen".
-Ein JDialog KuchenDialog zum Bearbeiten eines Kuchens (Textfeld für den Namen und OK/Abbrechen-Buttons)
Die GUI wurde ursprünglich mit dem Eclipse-Plugin "Jigloo" erstellt, der aber mittlerweile dahingeschieden ist. Da es scheinbar aktuell keinen Swing-GUI-Designer für Eclipse gibt, muss man das wohl in Zukunft händisch weiterverarbeiten. Mein GUI-Code ist aber in einigen Stellen noch Jigloo-generiert, was z.B. die "get"-Methoden für die Komponenten erklärt.

Die Main-Methode soll in einer Klasse FrameKuchen liegen, die gleichzeitig das Hauptfenster der Anwendung ist.

Die SessionBean wird per Injecton in den FrameKuchen gepackt:
public class FrameKuchen extends JFrame
{
  @EJB
  private static KuchenWorkerRemote kuchenWorker;

Jetzt noch die Main class in "MANIFEST.MF" eintragen:
	Manifest-Version: 1.0
	Class-Path: KuchenSimpleEJB.jar
	Main-Class: de.fhw.komponentenarchitekturen.knauf.kuchen.FrameKuchen


Jetzt können wir die Bean auf den Server stellen. Dieser Prozess erzeugt die Datenbanktabelle. Im Server-Log finden sich unter anderem diese Einträge, an der wir erkennen können wie die Tabelle erzeugt wird:
19:59:04,498 INFO  [org.hibernate.tool.hbm2ddl.SchemaExport] (ServerService Thread Pool -- 50) HHH000227: Running hbm2ddl schema export
19:59:04,498 INFO  [stdout] (ServerService Thread Pool -- 50) Hibernate: drop table KuchenSimpleBean if exists
19:59:04,514 INFO  [stdout] (ServerService Thread Pool -- 50) Hibernate: drop sequence hibernate_sequence
19:59:04,514 ERROR [org.hibernate.tool.hbm2ddl.SchemaExport] (ServerService Thread Pool -- 50) HHH000389: Unsuccessful: drop sequence hibernate_sequence
19:59:04,514 ERROR [org.hibernate.tool.hbm2ddl.SchemaExport] (ServerService Thread Pool -- 50) Sequenz "HIBERNATE_SEQUENCE" nicht gefunden
Sequence "HIBERNATE_SEQUENCE" not found; SQL statement:
drop sequence hibernate_sequence [90036-168]
19:59:04,529 INFO  [stdout] (ServerService Thread Pool -- 50) Hibernate: create table KuchenSimpleBean (id integer not null, name varchar(255), primary key (id))
19:59:04,529 INFO  [stdout] (ServerService Thread Pool -- 50) Hibernate: create sequence hibernate_sequence start with 1 increment by 1
19:59:04,529 INFO  [org.hibernate.tool.hbm2ddl.SchemaExport] (ServerService Thread Pool -- 50) HHH000230: Schema export complete


Datenbank-Adminstrations-Tool

Die in JBoss gebündelte und per Default verwendete H2 Database Engine bringt ein rudimentäres Administrationstool mit. Allerdings wird die Datenbank in der JBoss-Default-Datenquelle als "In memory"-Datenbank initialisiert, so dass kein Blick von außen darauf möglich ist. Es gibt zwei Wege, um das zu umgehen:

Variante 1:
Siehe https://issues.jboss.org/browse/WFLY-717
Eine kleine Webanwendung wird deployed, die nichts anderes tut, als eine bestimmte Webadresse an das H2-Servlet ("org.h2.server.web.WebServlet") weiterzuleiten. Die gemäß Anleitung erstellte Webanwendung findet sich auch hier: H2Console.war.
Sie wird einfach in "%WILDFLY_HOME%\standalone\deployments" kopiert.

Danach kann man die Konsole aufrufen unter der URL http://localhost:8080/H2Console/h2.
Wichtig: im Anmeldebildschirm muss man die JDBC-URL angeben, die auch in "standalone.xml" für die Datenbank angegeben ist, also "jdbc:h2:mem:test"! Das Passwort ist seit WildFly 8 "sa", in JBoss 7.x war es leer.
Login für H2Console

Ab WildFly 26 funktioniert das nicht mehr:
19:24:27,534 ERROR [org.jboss.as.controller.management-operation] (Controller Boot Thread) WFLYCTL0013: Operation ("deploy") failed - address: ([("deployment" => "H2Console.war")]) - failure description: 
  {"WFLYCTL0080: Failed services" => {"jboss.deployment.unit.\"H2Console.war\".undertow-deployment.UndertowDeploymentInfoService" => "Failed to start service
    Caused by: java.lang.NoClassDefFoundError: Failed to link org/h2/server/web/WebServlet
Lösung: siehe https://docs.wildfly.org/26/Developer_Guide.html#h2-web-console:

Variante 2: Es wird statt der nur im JBoss-Speicher verfügbaren Datenbank ein echter Prozess gestartet, der über eine Netzwerkverbindung erreichbar ist und eine Administrationskonsole bietet. Siehe http://www.mastertheboss.com/jboss-datasource/h2-database-tutorial

Hier die Schritte aus dem Tutorial:
Und wo liegt diese Datenbank?
Es ist eine Datei "C:\Users\USERNAME\test.h2.db" entstanden.
Anmerkung: die Datenbanktabellen werden bei jedem Deploy (auch im Rahmen eines Server-Neustarts) gelöscht. Um dies zu verhindern, müsste man in "persistence.xml" die Property "hibernate.hbm2ddl.auto" auf einen anderen Wert stellen und die Tabellen händisch anlegen.


"jboss-deployment-structure.xml"

Die "main"-Methode von "FrameKuchen" enthält folgendes Codefragment:
  public static void main(String[] args)
  {
    try
    {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }
    FrameKuchen frameKuchen = new FrameKuchen();
    frameKuchen.setVisible(true);
    ....
  }
Die Zeile "UIManager.setLookAndFeel" löst allerdings folgende Exception aus (hier nur als Fehlermeldung auf der Konsole wegen try/catch):
20:01:58,876 ERROR [stderr] (Thread-37) java.lang.ClassNotFoundException: com.sun.java.swing.plaf.windows.WindowsLookAndFeel from [Module "deployment.KuchenSimple.ear.KuchenSimpleClient.jar:main" from Service Module Loader]
20:01:58,876 ERROR [stderr] (Thread-37)         at org.jboss.modules.ModuleClassLoader.findClass(ModuleClassLoader.java:196)
20:01:58,876 ERROR [stderr] (Thread-37)         at org.jboss.modules.ConcurrentClassLoader.performLoadClassUnchecked(ConcurrentClassLoader.java:444)
20:01:58,876 ERROR [stderr] (Thread-37)         at org.jboss.modules.ConcurrentClassLoader.performLoadClassChecked(ConcurrentClassLoader.java:432)
20:01:58,876 ERROR [stderr] (Thread-37)         at org.jboss.modules.ConcurrentClassLoader.performLoadClass(ConcurrentClassLoader.java:374)
20:01:58,876 ERROR [stderr] (Thread-37)         at org.jboss.modules.ConcurrentClassLoader.loadClass(ConcurrentClassLoader.java:119)
20:01:58,876 ERROR [stderr] (Thread-37)         at java.lang.Class.forName0(Native Method)
20:01:58,876 ERROR [stderr] (Thread-37)         at java.lang.Class.forName(Unknown Source)
20:01:58,892 ERROR [stderr] (Thread-37)         at javax.swing.SwingUtilities.loadSystemClass(Unknown Source)
20:01:58,892 ERROR [stderr] (Thread-37)         at javax.swing.UIManager.setLookAndFeel(Unknown Source)
20:01:58,892 ERROR [stderr] (Thread-37)         at de.fhw.komponentenarchitekturen.knauf.kuchen.FrameKuchen.main(FrameKuchen.java:71)
20:01:58,892 ERROR [stderr] (Thread-37)         at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
20:01:58,892 ERROR [stderr] (Thread-37)         at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
20:01:58,892 ERROR [stderr] (Thread-37)         at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
20:01:58,892 ERROR [stderr] (Thread-37)         at java.lang.reflect.Method.invoke(Unknown Source)
20:01:58,892 ERROR [stderr] (Thread-37)         at org.jboss.as.appclient.service.ApplicationClientStartService$1.run(ApplicationClientStartService.java:122)
20:01:58,892 ERROR [stderr] (Thread-37)         at java.lang.Thread.run(UnknownSource)

Erklärung siehe
https://community.jboss.org/thread/220971:
Die Look&Feel-Implementierung ist eine Klasse, die nur in der Sun/Oracle-Java-Runtime enthalten ist, aber nicht Teil des Java-Standards ist. Solche Klassen "versteckt" JBoss per Default vor den im Server laufenden Anwendungen.

Über eine Datei "jboss-deployment-structure.xml" kann man dieses "Verstecken" steuern. Wir fügen diese Datei deshalb im EAR-Projekt unter "META-INF" zu (siehe https://docs.wildfly.org/26/Developer_Guide.html#jboss-deployment-structure-file)
jboss-deployment-structure.xml
Die Datei hat diesen Inhalt:
<jboss-deployment-structure xmlns="urn:jboss:deployment-structure:1.2"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="urn:jboss:deployment-structure:1.2 http://www.jboss.org/schema/jbossas/jboss-deployment-structure-1_2.xsd">
    <deployment>
        <dependencies>
            <system export="true">
                <paths>
                    <path name="com/sun/java/swing/plaf/windows"/>
                </paths>
            </system>
        </dependencies>
    </deployment>
</jboss-deployment-structure>
Die XSD-Datei hierzu findet sich nicht nur im Web, sondern auch in "%JBOSS_HOME%\docs\schema\jboss-deployment-structure-1_2.xsd".


Das Ende des ApplicationClient

In einer "ganz normalen" Swing-Anwendung könnte die main-Methode (zu finden hier in "FrameKuchen") so aussehen:
  public static void main(String[] args)
  {
    //...hier wird das Look&Feel gesetzt...

    FrameKuchen frameKuchen = new FrameKuchen();
    frameKuchen.setVisible(true);
  }
In der Initialisierung des "FrameKuchen" würde man die "DefaultCloseOperation" auf "Exit on close" setzen, sprich der Javaprozess endet durch das Schließen des Hauptfensters:
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Letzteres ist relativ offensichtlich ein Problem, weil unsere main-Methode hier ja vom JBoss-ApplicationClient-Launcher gestartet wird und ein "System.exit()" kontraproduktiv wäre.
Deshalb setzen wir die DefaultCloseOperation auf "Dispose on close". Der Default "Hide on close" würde einen weiteren Aufruf im weiter unten folgenden WindowListener nötig machen.
    this.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

Ein zweites Problem ist nicht so offensichtlich: die main-Methode ist fertig nach dem Sichtbarschalten des Frame. Dies startet den "AWT Event Dispatch Thread (EDT)", der asynchron weiterläuft und den Javaprozess fortführt. Die main-Methode ist damit beendet. Dies führt dazu, dass der JBoss-ApplicationClient ebenfalls denkt dass die Anwendung fertig ist und sich selbst beendet (Undeploy der Anwendung im ApplicationClient-JBoss). Das Swing-Fenster lebt zwar weiter, aber jeder EJB-Zugriff wird in der Folge fehlschlagen.

Deshalb muss man nach dem Sichtbarschalten warten, warten, warten, und zwar bis das Fenster geschlossen wird.
Die allersimpelste Lösung:
Nach dem Anzeigen des Fensters wird darauf gewartet, dass es wieder unsichtbar wird.
    frameKuchen.setVisible(true);
    while (frameKuchen.isVisible() == true)
    {
      Thread.sleep(5000);
    }
    frameKuchen.dispose();
Meiner Erfahrung nach kostet ein "Thread.sleep" relativ viel Prozessorleistung (vielleicht gilt das in aktuellen Java-Versionen nicht mehr)

Eine hoffentlich bessere Lösung (inspiriert von
http://stackoverflow.com/questions/799631/java-wait-for-jframe-to-finish)
Es wird eine java.util.concurrent.CountDownLatch verwendet. Diese dient als eine Art Freigabezähler: beim Erzeugen der Klasse wird eine Anzahl von Freigabesignalen definiert, die erforderlich ist, damit die Freigabe "erteilt" wird. Nach dem Sichtbarschalten des Fensters wartet die Main-Methode deshalb auf diese Freigabe. Im Schließen des Fensters wird die Freigabe über die Methode CountDownLatch.countDown erteilt. Hierzu wird ein "WindowListener" für das Event "windowClosed" registriert.
Der Code sieht insgesamt so aus:
  private static CountDownLatch frameClosedSignal;

  public static void main(String[] args)
  {
    try
    {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
    }
    catch (Exception e)
    {
      e.printStackTrace();
    }

    //Erzeugen des "warten auf Schließen des Fensters"-Signals.  
    FrameKuchen.frameClosedSignal = new CountDownLatch(1);
    
    FrameKuchen frameKuchen = new FrameKuchen();
    frameKuchen.setVisible(true);
    
    //WindowListener erzeugen, der im "windowClosed" die CountDownLatch anstößt.
    frameKuchen.addWindowListener(new WindowAdapter()
    {
      @Override
      public void windowClosed(WindowEvent windowEvent)
      {
        //Der CountDownLatch die "Freigabe" erteilen.
        FrameKuchen.frameClosedSignal.countDown();
        
        super.windowClosed(windowEvent);
      }
    });
    
    // Jetzt darauf warten, dass das Fenster geschlossen wird (WindowListener reagiert!) 
    // Dieses await() blockiert bis die Methode "countDown" auf der CountDownLatch" aufgerufen wird.
    try
    {
      FrameKuchen.frameClosedSignal.await();
    }
    catch (InterruptedException e)
    {
      e.printStackTrace();
    }
  }
Ein kleiner Hinweis: "windowClosed" wird erst aufgerufen, wenn die Methode "dispose" des JFrame aufgerufen wird. Durch die oben genannte "DefaultCloseOperation = DISPOSE_ON_CLOSE" haben wir das erreicht. Beim Default "HIDE_ON_CLOSE" würde "windowClosed" nicht aufrufen, außer wir führen das "dispose" z.B in einem "windowClosing"-Eventhandler durch.


Ausführen des Clients

Analog zum Stateless-Beispiel:
Schritt 1: als EAR-Datei exportieren.
Schritt 2: "appclient.bat" mit dieser exportierten Datei als Parameter aufrufen (im Beispiel hatte ich sie nach "c:\temp\" exportiert):

%JBOSS_HOME%\bin\appclient.bat c:\temp\KuchenSimple.ear#KuchenSimpleClient.jar

Achtung: die EAR-Dateien, die hier zum Download stehen, wurden mit Java 1.8 erstellt. Mit Java 17 (das erst ab WildFly 25 offiziell unterstützt wird) gab es folgende Fehlermeldung:

Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make field private static final java.lang.Object javax.swing.JFrame.defaultLookAndFeelDecoratedKey accessible: module java.desktop does not \"opens javax.swing\" to unnamed module @f7cd57e"}

Ursache: es fehlt eine "module-info"-Datei in meinem Projekt (da mit Java 1.8 erstellt).
Lösung: Durch Setzen der Umgebungsvariable "JAVA_OPTS", die in "appclient.bat" ausgewertet wurd, kann dieses Problem umgangen werden:
set JAVA_OPTS=--add-opens=java.desktop/javax.swing=ALL-UNNAMED --add-opens=java.desktop/java.awt=ALL-UNNAMED
%WILDFLY_HOME%\bin\appclient.bat c:\temp\KuchenSimple.ear#KuchenSimpleClient.jar

In vorherigen Java-Versionen konnte man die JAVA_OPTS auf --illegal-access=permit setzen, aber dies wurde in Java 17 entfernt.

Hinweis: Der JavaEE-ApplicationClient wird in einem eigenen JBoss-Server gestartet, der eine eigene Config-Datei hat, zu finden in "%JBOSS_HOME%\bin\appclient\configuration\appclient.xml". In dieser Datei muss die Default-Datenquelle ebenfalls deklariert werden (wobei die konkrete Datenbank nicht identisch sein muss, es muss nur der Name der Datenquelle vorhanden sein). Hier ist ein Auszug aus der Default-Konfiguration, der auf die in diesem Beispiel verwendete "ExampleDS" zeigt:
        <subsystem xmlns="urn:jboss:domain:datasources:1.1">
            <datasources>
                    <datasource jndi-name="java:jboss/datasources/ExampleDS" enabled="true" use-java-context="true"
                            pool-name="java:jboss/datasources/ExampleDS">
                    <connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</connection-url>
                    <driver>h2</driver>
                    ...
                </datasource>
                ...
            </datasources>
        </subsystem>


Logging der SQL-Parameter

Leider aktiviert die Property "hibernate.show_sql" in persistence.xml nur das Logging der SQL-Statements. Die Parameter der dabei benutzten Prepared Statements werden nicht ausgegeben. Wir erhalten nur solche Ausgaben:
insert into KuchenSimpleBean (id, name) values (?, ?)

Es gibt allerdings eine Lösung: mittels JBoss Logging können wir die Ausgabe der Parameter konfigurieren.
Dies geht so (die Quelle zeigt ein allgemeingültiges Beispiel für Log4j, aber dies ist einfach an die WildFly-Konfiguration anpassbar):
https://docs.jboss.org/hibernate/stable/orm/userguide/html_single/Hibernate_User_Guide.html#best-practices-logging.

In der Datei "%JBOSS_HOME\standalone\configuration\standalone.xml" sucht man den Bereich subsystem xmlns="urn:jboss:domain:logging:2.0". Hier wird folgendes eingetragen (ACHTUNG: Datei als UTF-8 speichern !):
Das Ergebnis eines Inserts sieht jetzt so aus:
22:01:45,191 INFO  [stdout] (EJB default - 4) Hibernate: call next value for hibernate_sequence

22:01:45,191 INFO  [stdout] (EJB default - 4) Hibernate: insert into KuchenSimpleBean (name, id) values (?, ?)

22:01:45,191 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] (EJB default - 4) binding parameter [1] as [VARCHAR] - Mohnkuchen
22:01:45,191 TRACE [org.hibernate.type.descriptor.sql.BasicBinder] (EJB default - 4) binding parameter [2] as [INTEGER] - 1

Nachteil: jetzt werden auch bei einem Select alle zurückgelieferten Spalten ausgegeben:
22:01:45,191 INFO  [stdout] (EJB default - 5) Hibernate: select kuchensimp0_.id as id0_, kuchensimp0_.name as name0_ from KuchenSimpleBean kuchensimp0_

22:01:45,206 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (EJB default - 3) extracted value ([id1_0_] : [INTEGER]) - [1]
22:01:45,206 TRACE [org.hibernate.type.descriptor.sql.BasicExtractor] (EJB default - 3) extracted value ([name2_0_] : [VARCHAR]) - [Mohnkuchen]


Ohne Annotations

Die Deklaration ohne Annotations erfordert eine ganze Reihe Arbeit.

Deployment-Deskriptoren im EJB-Projekt
ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar version="3.1" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                           http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd">
	<display-name>KuchenSimpleEJB</display-name>
	<enterprise-beans>
		<session>
			<description>
				<![CDATA[Stateless Session Bean für den Zugriff auf die Entitiy Bean
 * "Kuchen". Enthält Methoden zum Speichern und Löschen eines einzelnen
 * Kuchens sowie zum Holen einer Liste aller Kuchen.]]>
			</description>
			<display-name>KuchenWorkerBean</display-name>
			<ejb-name>KuchenWorkerBean</ejb-name>
			<remote>de.fhw.swtvertiefung.knauf.kuchen.KuchenWorker</remote>
			<ejb-class>de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean</ejb-class>
			<session-type>Stateless</session-type>
			<!--EntityManager-Injection -->
			<persistence-context-ref>
				<persistence-context-ref-name>KuchenPersistenceUnitRef</persistence-context-ref-name>
				<persistence-unit-name>kuchenPersistenceUnit</persistence-unit-name>
				<injection-target>
					<injection-target-class>
						de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean
					</injection-target-class>
					<injection-target-name>entityManager</injection-target-name>
				</injection-target>
			</persistence-context-ref>
		</session>
		<entity>
			<description>
				<![CDATA[Entity Bean für einen einzelnen Kuchen.]]>
			</description>
			<display-name>KuchenSimpleBean</display-name>
			<ejb-name>KuchenSimpleBean</ejb-name>
			<ejb-class>de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean</ejb-class>
			<persistence-type>Container</persistence-type>
			<prim-key-class>java.lang.Integer</prim-key-class>
			<reentrant>false</reentrant>
		</entity>
	</enterprise-beans>
</ejb-jar> 
Neu in diesem Beispiel:
Anmerkung:
In "ejb-jar.xml" dürfen KEINE EJB3.0-Entity-Beans (durch das Element "entity") eingetragen werden! Das Element "entity" bezieht sich nur auf EJB-2.1-Entity-Beans!

Neu in diesem Beispiel ist ein weiterer Deployment-Deskriptor: "orm.xml". Hier wird das gesamte Mapping von Bean auf Datenbank erledigt.
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
	version="2.0">
	<named-query name="findAllKuchen">
		<query>select o from KuchenSimpleBean o</query>
	</named-query>

	<entity class="de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean" access="PROPERTY"
		metadata-complete="true">
		<attributes>
			<id name="id">
				<generated-value/>
			</id>
			<basic name="name">
			</basic>
		</attributes>
	</entity>
</entity-mappings> 
Das Element "entity" hat keine in der Schema-Definition angegebenen Pflicht-Unterelemente, trotzdem sind einige nötig damit unser Beispiel funktioniert.

Das Attribut "access" gibt an ob die Fehler per set-Property vom Container befüllt werden sollen, oder ob er sie direkt in die (privaten) Membervariablen schreiben soll.

"metadata-complete" gibt wohl an, ob nach weiteren Annotations auf der Entity-Bean-Klasse gesucht werden soll oder ob der Deployment-Deskriptor alles enthält (was bei mir der Fall ist). Dies war zumindest in JBoss 4.2 sehr wichtig: dort gab es beim Deploy seltsame Fehlermeldungen, wenn es fehlte. Im JBoss 5.0 schien es schon nicht mehr nötig zu sein, ich belasse es trotzdem.

Innerhalb der Entity werden die Properties definiert. Für die ID-Spalte geben wir an dass der Wert automatisch generiert werden soll.

Anmerkung:
Eine erweiterte Version, in der z.B. Tabellen- und Spaltennamen angegeben werden, sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
	version="2.0">
	<named-query name="findAllKuchen">
		<query>select o from KuchenSimpleBean o</query>
	</named-query>

	<entity class="de.fhw.swtvertiefung.knauf.kuchen.KuchenSimpleBean" access="PROPERTY"
		metadata-complete="true">
		<table name="KUCHENSIMPLEBEAN"></table>
		<attributes>
			<id name="id">
				<column name="ID" />
				<generated-value/>
			</id>
			<basic name="name">
				<column name="NAME" />
			</basic>
		</attributes>
	</entity>
</entity-mappings> 
Wir geben hier den Tabellennamen an ("KUCHENSIMPLEBEAN"), außerdem die Attribute und die zugehörigen Datenbankspalten. Für die ID-Spalte geben wir an dass der Wert automatisch generiert werden soll.


Deployment-Deskriptoren im Application-Client-Projekt
Der Standard-Deskriptor "application-client.xml" sieht so aus (Injection der Session-Bean wird gesteuert):
<?xml version="1.0" encoding="UTF-8"?>
<application-client version="6"
	xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application-client_6.xsd">
	<display-name>KuchenSimpleClient</display-name>
	<ejb-ref>
		<ejb-ref-name>ejb/KuchenWorker</ejb-ref-name>
		<ejb-ref-type>Session</ejb-ref-type>
		<remote>de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenWorkerRemote</remote>
		<injection-target>
			<injection-target-class>de.fhw.komponentenarchitekturen.knauf.kuchen.FrameKuchen</injection-target-class>
			<injection-target-name>kuchenWorker</injection-target-name>
		</injection-target>
	</ejb-ref>
</application-client>

Der JBoss-spezifische Deskriptor "jboss-client.xml" aus dem Application-Client-Projekt (scheinbar gibt es noch keine Version der XSD für JBoss 7, deshalb wird hier die 6er-Variante verwendet):
<?xml version="1.0" encoding="UTF-8"?>
<jboss-client xmlns="http://www.jboss.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-client_6_0.xsd"
    version="6.0">
	<jndi-name>KuchenSimpleClient</jndi-name>
	<ejb-ref>
		<ejb-ref-name>ejb/KuchenWorker</ejb-ref-name>
		<jndi-name>java:global/KuchenSimple/KuchenSimpleEJB/KuchenWorkerBean!de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenWorkerRemote</jndi-name>
	</ejb-ref>
</jboss-client> 


Die modifizierte Version des Projekts gibt es hier:
KuchenSimpleNoAnnotation.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenSimple-Beispiel existieren !


Manueller Primary Key


Jetzt wollen wir den Primary Key manuell generieren. Das Verfahren hier ist einfach, aber sehr inperformant: bei jedem Neuanlegen eines Datensatzes wird eine Query "select max(id) from KuchenSimpleBean" ausgeführt. Das Ergebnis dieser Query + 1 wird als ID für den neuen Datensatz verwendet.
Im Code ist folgendes zu ändern:

In KuchenSimpleBean.getId wird die Annotation "@GeneratedValue" entfernt:
  @Column()
  @Id()
  public Integer getId()
  {
    return this.intId;
  } 
KuchenSimpleBean erhält eine weitere @NamedQuery-Annotation (die wir jetzt in eine Annotation @NamedQueries verpacken müssen):
@NamedQueries( { 
  @NamedQuery(name = "findAllKuchen", query = "select o from KuchenSimpleBean o"),
  @NamedQuery(name = "getMaxKuchenId", query = "select max (o.id) from KuchenSimpleBean o")
  })
public class KuchenSimpleBean implements Serializable
{
  ...
KuchenWorkerBean.saveKuchen sieht so aus:
  public void saveKuchen (KuchenSimpleBean kuchen)
  {
    if (kuchen.getId() == null)
    {
      Query query = this.entityManager.createNamedQuery("getMaxKuchenId");
      Integer intMaxId = (Integer) query.getSingleResult();
      //Beim ersten Aufruf kommt hier NULL zurück weil keine ID da ist.
      if (intMaxId == null)
      {
        //Wir starten mit der ID "1"
        kuchen.setId(1);
      }
      else
      {
        //MaxID + 1 verwenden:
        kuchen.setId(intMaxId + 1);
      }
    }
    this.entityManager.merge(kuchen);
  } 
Neu ist der fette Code: wenn der übergebene Kuchen keine ID enthält, dann nehmen wir an dass er neu ist. In diesem Fall führen wir unsere Query aus. Sie liefert beim ersten Aufruf NULL zurück, in diesem Fall setzen wir die ID auf "1". Finden wir eine ID erhöhen wir diese um 1.

Kleiner Kurs in Java: Vor Java5 hätten die Aufrufe zum ID-Setzen so aussehen müssen:
	kuchen.setId( new Integer (intMaxId.intValue() + 1));
Neu in Java 5 kam "Auto-Boxing" hinzu, das es erlaubt einen Basisdatentyp bei Bedarf in die entsprechende Klasse zu konvertieren und zurück (hier: int und java.lang.Integer).

Die modifizierte Version des Projekts gibt es hier:
KuchenSimpleManuellerPK.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen KuchenSimple-Beispiel existieren !



Zugriff auf MySQL-Datenbank

Am Beispiel einer lokalen MySQL-Installation soll gezeigt werden, wie man JBoss auf eine andere Datenbank umschaltet. Hierzu verwende ich eine MySQL-Datenbank (das Beispiel ist mit Version 5.6.11 entstanden).

Vorbereitung

Falls noch keine MySQL verfügbar ist: Download von
http://dev.mysql.com/downloads/mysql/5.6.html. Ich hatte die Zip-Variante (ohne Installation) genutzt.
Und schon sind wir bereit für den nächsten Schritt...

MySQL-Treiber goes JBoss

Die folgenden Schritte folgen dieser Anleitung: https://community.jboss.org/wiki/DataSourceConfigurationInAS7



Troubleshooting

Hier werden ein paar Fehler beschrieben in die ich beim Programmieren gelaufen bin ;-)

Stand 04.01.2022
Historie:
30.05.2013: Erstellt aus 2008-er-Beispiel, angepaßt an Eclipse 4.2 und JBoss 7, Nutzung einer MySQL-Datenbank
11.09.2013: Cleanup, Hinweis zu Eclipse-Validierungsfehler "Class xx is managed, but is not listed in the persistence.xml file"
22.02.2015: Angepaßt an WildFly 8.2 und Eclipse 4.4
02.04.2020: Angepasst an WildFly 19, Eclipse 2019-12 (4.14.0), Hinweise auf JavaEE7-Features
27.12.2021: WildFly26: Workaround für Aufruf der H2Console.
31.12.2021: Link auf die WildFly-Doku aktualisiert, Hinweis zum Start unter Java 17, Workaround für "ExampleDS" in "appclient.xml" ist seit WildFly 8.1 nicht mehr nötig - ersetzt durch Hinweis auf Vorhandensein der DataSource, "jboss-deployment-structure.xml" funktioniert mittlerweile auch mit schemaLocation (
https://issues.redhat.com/browse/WFCORE-568).
04.01.2022: Link auf Konfiguration des Loggings der SQL-Parameter korrigiert.