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.
Eine aktuelle Version für JakartaEE10 und WildFly 30 findet sich
hier.
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.
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)...
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":
Jetzt wird im Projekt eine Datei "META-INF\persistence.xml" erzeugt, und es taucht im "Project Explorer" ein Punkt "JPA Content" auf.
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 ;-).
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).
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).
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".
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!
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.
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
- Die Property
javax.persistence.schema-generation.database.action
gibt an, ob und wie das Schema erzeugt wird. Der Wert "drop-and-create" löscht beim Deploy das vorhandene Schema und erzeugt es neu.
Im Unterschied zur alten Property hibernate.hbm2ddl.auto
wird hier beim Undeploy der Anwendung nichts gelöscht! Das Löschen erfolgt erst vor einem Redeploy. D.h. die neuen Properties bereinigen
Datenbanken im Testumfeld nicht.
- Die Properties
javax.persistence.schema-generation.create-source
und ....drop-source
geben an, wie die Tabellenerzeugung erfolgen soll: der Wert "metadata" bewirkt, dass die SQL-Befehle
auf Basis der Entity-Bean-Annotations (bzw. der Deklarationen in "ejb-jar.xml") erzeugt werden.
Der Weg des Assistenten: Anlegen der Entity Bean mit JPA
Im "Project Explorer" das EJB-Projekt wählen, Rechtsklick, "New" => "JPA Entity" wählen.
Name ("KuchenSimpleBean") und Package ("de.fhw.komponentenarchitekturen.knauf.kuchen") werden angegeben.
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).
Hier der Dialog für das Hinzufügen des Felds "id":
Die so erzeugte Entity taucht jetzt auch im "Project Explorer" auf:
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".
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.
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:
- Schritt 1: Datenbank starten
Ins Verzeichnis "%WILDFLY_HOME%\modules\system\layers\base\com\h2database\h2\main"
(in JBoss 7.2 war das noch: "%JBOSS_HOME%\modules\com\h2database\h2\main") wechseln. Dort folgenden Befehl ausführen:
java -jar h2-1.3.173.jar
(Die Version von "h2-....jar" wird sich natürlich ändern)
Die Datenbank wird gestartet (erkennbar an dem Systray-Icon) - man wird unter Windows von der Firewall gefragt ob man diese Anwendung freischalten will -
und es öffnet sich ein Webbrowser mit einem Anmeldefenster:
Beim allerersten Start muss man als Passwort sa angeben (in JBoss 7.2 war das Passwort per Default leer)!
Jetzt sieht man die Adminkonsole vor sich und kann sich die Tabellenstruktur anschauen oder Befehle abfeuern.
Unter JBoss 7.2. (bei WildFly 8 nicht mehr nötig!) musste der allererste Befehl sein, dem Admin-User "sa" ein Passwort zu geben (oder alternativ einen neuen User anzulegen und diesen in
der JBoss-Config einzutragen). Dazu wird dieser Befehl ausgeführt:
ALTER USER SA SET PASSWORD 'sa';
- Schritt 2: die integrierte JBoss-Datenbankverbindung (an den JNDI-Namen "java:jboss/datasources/ExampleDS" gebunden)
von einer In-Memory-Datenbank in eine per TCP/IP erreichbare Datenbank umkonfigurieren:
Original:
<subsystem xmlns="urn:jboss:domain:datasources:2.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1</connection-url>
<driver>h2</driver>
...
</datasource>
...
</datasources>
</subsystem>
Neu:
<subsystem xmlns="urn:jboss:domain:datasources:2.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
<connection-url>jdbc:h2:tcp://localhost/~/test</connection-url>
<driver>h2</driver>
<security>
<user-name>sa</user-name>
<password>sa</password>
</security>
</datasource>
...
</datasources>
</subsystem>
Es wird also nur die "connection-url" geändert.
- Schritt 3: JBoss starten
- Schritt 4: Anwendung deployen.
Achtung: das klappt erst nach dem Teilschritt "Setzen des Passworts für den User sa" in Schritt 1, weil die neu erzeugte Datenbank vorher kein Passwort hat,
in der JBoss-Config aber ein solches hinterlegt ist!
- Jetzt können wir unsere Tabellen sehen:
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)
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 !):
- Es wird neuer "console-handler" namens "CONSOLE.HIBERNATE_PARAMETERS" angehängt.
<console-handler name="CONSOLE.HIBERNATE_PARAMETERS">
<level name="TRACE"/>
<formatter>
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n"/>
</formatter>
</console-handler>
Es wird ein neuer Handler definiert, der auf die Konsole (System.out) loggt. Einziger Unterschied zum vorhandenen Konsolen-Appender: sein
"Level" also das Loglevel, ab dem Meldungen durchgelassen wurde, wurde auf die maximale Detailstufe "TRACE" erhöht.
Anmerkung: es würde eigentlich reichen, den Standard-Konsolenappender auf "TRACE" umzustellen, allerdings würden wir dann in JBoss-Meldungen ertrinken.
- Es wird eine neuer Logger definiert. Dieser referenziert unseren Handler (dadurch werden NUR Logausgaben
der Kategorie "org.hibernate.type" an diesen Appender gegeben, und zwar mit TRACE-Level und höher).
<logger category="org.hibernate.type">
<level name="TRACE"/>
<handlers>
<handler name="CONSOLE.HIBERNATE_PARAMETERS"/>
</handlers>
</logger>
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:
- Injection des Persistence Context in die Session Bean (wobei das Pflichtelement "persistence-context-ref-name"
für uns keine Bedeutung hat, das ist nur relevant wenn der Persistence Context per JNDI-Lookup geholt werden muss).
- Bei der EntityBean sind die oben markierten Angaben Pflicht. "persistence-type" gibt an ob der Container oder die Bean
selbst sich um die Datenbankzugriffe kümmert. Bei einer EntityBean gemäß EJB3-Spezifikation ist hier natürlich "Container"
die einzig sinnvolle Angabe. Die "prim-key-class" gibt den Datentyp des Primary Keys an, hier "java.lang.Integer".
"reentrant" gibt an ob man ausgehend von einer Methode der Bean zu einer anderen Bean und wieder zurück laufen kann. Für
unsere Bedürfnisse können wir hier immer "true" nehmen.
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.
- Schritt 1: Entpacken
- Schritt 2: Server starten:
mysqld --console
- Schritt 3: es gibt einen Standarduser "root" mit leerem Passwort. Dies macht für JBoss Probleme, weil man das Passwort in der Config
angeben muss. Also Passwort des root-Users setzen (hier auf "jbosstest"):
mysqladmin.exe -u root password jbosstest
- Schritt 4: MySQL-Client starten:
mysql -u root -p
Danach wird man nach dem Passwort gefragt.
- Schritt 5: eine eigene Datenbank anlegen, mit der JBoss arbeiten darf:
create database jbosstest;
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
- Schritt 0: MySQL-JDBC-Treiber ("MySQL Connector/J") herunterladen von http://dev.mysql.com/downloads/connector/j/,
und daraus die JAR-Datei "mysql-connector-java-5.1.25-bin.jar" entpacken.
- Schritt 1: im JBoss-Verzeichnis "modules\system\layers" (unter JBoss 7.2: "modules") eine Unterverzeichnisstruktur "com\mysql\main" anlegen
- Schritt 2: in diesem Verzeichnis eine Datei "module.xml" mit folgendem Inhalt anlegen:
<?xml version="1.0" encoding="UTF-8"?>
<module xmlns="urn:jboss:module:1.3" name="com.mysql">
<resources>
<resource-root path="mysql-connector-java-5.1.25-bin.jar"/>
</resources>
<dependencies>
<module name="javax.api"/>
</dependencies>
</module>
Anmerkung:
Gemäß oben genanntem JBoss-Wiki-Artikel würde es bei einem JDBC4-Compliant Driver ausreichen, wenn man die JAR-Datei ins Deploy-Verzeichnis kopiert.
Aber der MySQL-Treiber erfüllt diese Anforderung nicht (siehe
http://bugs.mysql.com/bug.php?id=62038.
- Schritt 3: in "%JBOSS_HOME%\standalone\configuration\standalone.xml" sucht man die Sektion
<subsystem xmlns="urn:jboss:domain:datasources:2.0">
,
in der bereits der Eintrag für die H2-Datenbank steckt.
Dort werden folgende zwei fett markierten Fragmente in das Element "datasources" bzw. das Unterelement "drivers" eingefügt:
<subsystem xmlns="urn:jboss:domain:datasources:2.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
...
</datasource>
<datasource jndi-name="java:jboss/datasources/MySqlDS" pool-name="MySqlDS" enabled="true">
<connection-url>jdbc:mysql://localhost:3306/JBOSSTEST</connection-url>
<driver>com.mysql</driver>
<transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>
<pool>
<min-pool-size>10</min-pool-size>
<max-pool-size>100</max-pool-size>
<prefill>true</prefill>
</pool>
<security>
<user-name>root</user-name>
<password>jbosstest</password>
</security>
<statement>
<prepared-statement-cache-size>32</prepared-statement-cache-size>
<share-prepared-statements>true</share-prepared-statements>
</statement>
</datasource>
<drivers>
<driver name="h2" module="com.h2database.h2">
...
</driver>
<driver name="com.mysql" module="com.mysql">
<xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
</driver>
</drivers>
</datasources>
Den Inhalt der Elemente "pool" und "statement" habe ich 1:1 aus dem JBoss-Wiki kopiert, keine Ahnung ob das in dieser Form nötig ist
oder was passieren würde, wenn man es wegläßt ;-)
Das ganze geht auch über die CLI:
Folgender Block deklariert die DataSource exakt mit den Parametern wie oben (nur mit dem anderen Namen "MySqlDS2")
(grundlegend basierend auf https://community.jboss.org/wiki/GenericTypeCLICommands)
[standalone@localhost:9999 /] data-source add --name=MySqlDS2 --jndi-name=java:jboss/datasources/MySQLDS2
--driver-name=com.mysql --enabled=true --connection-url=jdbc:mysql://localhost:3306/JBOSSTEST
--transaction-isolation=TRANSACTION_READ_COMMITTED --min-pool-size=10 --max-pool-size=100
--pool-prefill=true --user-name=root --password=jbosstest --share-prepared-statements=true
--prepared-statements-cache-size=32
(Natürlich ohne die Zeilenumbrüche, die ich hier der Lesbarkeit zuliebe eingefügt hatte)
Danach muss man die Datenquelle noch aktivieren:
[standalone@localhost:9999 /] data-source enable --name=MySqlDS2
Und um das Ding wieder zu entsorgen:
[standalone@localhost:9999 /] data-source remove --name=MySqlDS2
- Schritt 4: jetzt können wir den Server starten. Das erfolgreiche Laden unseres Treibers können wir auf der Konsole sehen:
19:48:13,464 INFO [org.jboss.as.connector.subsystems.datasources] (ServerService Thread Pool -- 27) JBAS010403: Deploying JDBC-compliant driver class org.h2.Driver (version 1.3)
19:48:13,811 INFO [org.jboss.as.connector.subsystems.datasources] (ServerService Thread Pool -- 27) JBAS010404: Deploying non-JDBC-compliant driver class com.mysql.jdbc.Driver (version 5.1)
oder auch über diesen CLI-Befehl: /subsystem=datasources:installed-drivers-list
[standalone@localhost:9999 /] /subsystem=datasources:installed-drivers-list
{
"outcome" => "success",
"result" => [
{
"driver-name" => "h2",
"deployment-name" => undefined,
"driver-module-name" => "com.h2database.h2",
"module-slot" => "main",
"driver-datasource-class-name" => "",
"driver-xa-datasource-class-name" => "org.h2.jdbcx.JdbcDataSource",
"driver-class-name" => "org.h2.Driver",
"driver-major-version" => 1,
"driver-minor-version" => 3,
"jdbc-compliant" => true
},
{
"driver-name" => "com.mysql",
"deployment-name" => undefined,
"driver-module-name" => "com.mysql",
"module-slot" => "main",
"driver-datasource-class-name" => "",
"driver-xa-datasource-class-name" => "com.mysql.jdbc.jdbc2.optional.MysqlXADataSource",
"driver-class-name" => "com.mysql.jdbc.Driver",
"driver-major-version" => 5,
"driver-minor-version" => 1,
"jdbc-compliant" => false
}
]
}
- Schritt 5: In "persistence.xml" können wir die Datenquelle jetzt verwenden:
<?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" transaction-type="JTA">
<jta-data-source>java:jboss/datasources/MySqlDS</jta-data-source>
<properties>
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<property name="hibernate.show_sql" value="true"></property>
</properties>
</persistence-unit>
</persistence>
Troubleshooting
Hier werden ein paar Fehler beschrieben in die ich beim Programmieren gelaufen bin ;-)
-
Beim Speichern eines bereits in der Datenbank vorhandenen Kuchens gibt es folgende Fehlermeldung:
javax.ejb.EJBException: javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean
at org.jboss.ejb3.tx.Ejb3TxPolicy.handleExceptionInOurTx(Ejb3TxPolicy.java:69)
at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:83)
at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:197)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:76)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:62)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:78)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:47)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:106)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessContainer.dynamicInvoke(StatelessContainer.java:225)
at org.jboss.aop.Dispatcher.invoke(Dispatcher.java:106)
at org.jboss.aspects.remoting.AOPRemotingInvocationHandler.invoke(AOPRemotingInvocationHandler.java:82)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:828)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:681)
at org.jboss.remoting.transport.socket.ServerThread.processInvocation(ServerThread.java:358)
at org.jboss.remoting.transport.socket.ServerThread.dorun(ServerThread.java:412)
at org.jboss.remoting.transport.socket.ServerThread.run(ServerThread.java:239)
Caused by: javax.persistence.PersistenceException: org.hibernate.PersistentObjectException: detached entity passed to persist: de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean
at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:567)
at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:192)
at org.jboss.ejb3.entity.TransactionScopedEntityManager.persist(TransactionScopedEntityManager.java:175)
at de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean.saveKuchen(KuchenWorkerBean.java:42)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
Lösung: die zu speichernde, bereits in der Datenbank vorhandene Entity wurde in der KuchenWorkerBean nicht mehr "entityManager.merge()"
gespeichert sondern mittels "entityManager.persist()". Dies klappt aber NUR bei neuen Objekten, nicht bei bereits vorhandenen Daten !
-
Beim Löschen einer Entity kommt es zu folgender Fehlermeldung:
javax.ejb.EJBException: java.lang.IllegalArgumentException: Removing a detached instance de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean#1
at org.jboss.ejb3.tx.Ejb3TxPolicy.handleExceptionInOurTx(Ejb3TxPolicy.java:69)
at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:83)
at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:197)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:76)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:62)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:78)
at org.jboss.ejb3.security.Ejb3AuthenticationInterceptor.invoke(Ejb3AuthenticationInterceptor.java:131)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:47)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:106)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessContainer.dynamicInvoke(StatelessContainer.java:263)
at org.jboss.aop.Dispatcher.invoke(Dispatcher.java:106)
at org.jboss.aspects.remoting.AOPRemotingInvocationHandler.invoke(AOPRemotingInvocationHandler.java:82)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:828)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:681)
at org.jboss.remoting.transport.socket.ServerThread.processInvocation(ServerThread.java:358)
at org.jboss.remoting.transport.socket.ServerThread.dorun(ServerThread.java:412)
at org.jboss.remoting.transport.socket.ServerThread.run(ServerThread.java:239)
Caused by: java.lang.IllegalArgumentException: Removing a detached instance de.fhw.komponentenarchitekturen.knauf.kuchen.KuchenSimpleBean#1
at org.hibernate.ejb.event.EJB3DeleteEventListener.performDetachedEntityDeletionCheck(EJB3DeleteEventListener.java:47)
at org.hibernate.event.def.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:75)
at org.hibernate.event.def.DefaultDeleteEventListener.onDelete(DefaultDeleteEventListener.java:49)
at org.hibernate.impl.SessionImpl.fireDelete(SessionImpl.java:766)
at org.hibernate.impl.SessionImpl.delete(SessionImpl.java:744)
at org.hibernate.ejb.AbstractEntityManagerImpl.remove(AbstractEntityManagerImpl.java:245)
at org.jboss.ejb3.entity.TransactionScopedEntityManager.remove(TransactionScopedEntityManager.java:187)
at de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean.deleteKuchen(KuchenWorkerBean.java:36)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...
Lösung:die Entities vorher müssen "attached" werden, d.h. in eine vom Container verwaltete Entity umgewandelt
werden:
kuchen = this.entityManager.find (KuchenSimpleBean.class, kuchen.getId() );
this.entityManager.remove(kuchen);
Vor dem Aufruf von "remove" müßte eigentlich noch eine Prüfung erfolgen ob der Datensatz in der Datenbank gefunden wurde.
- Beim Aufrufen einer NamedQuery kommt diese Fehlermeldung:
javax.ejb.EJBException: javax.persistence.PersistenceException: org.hibernate.MappingException: Named query not known: findAllKuchen
at org.jboss.ejb3.tx.Ejb3TxPolicy.handleExceptionInOurTx(Ejb3TxPolicy.java:69)
at org.jboss.aspects.tx.TxPolicy.invokeInOurTx(TxPolicy.java:83)
at org.jboss.aspects.tx.TxInterceptor$Required.invoke(TxInterceptor.java:197)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.tx.TxPropagationInterceptor.invoke(TxPropagationInterceptor.java:76)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessInstanceInterceptor.invoke(StatelessInstanceInterceptor.java:62)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.aspects.security.AuthenticationInterceptor.invoke(AuthenticationInterceptor.java:78)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.ENCPropagationInterceptor.invoke(ENCPropagationInterceptor.java:47)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.asynchronous.AsynchronousInterceptor.invoke(AsynchronousInterceptor.java:106)
at org.jboss.aop.joinpoint.MethodInvocation.invokeNext(MethodInvocation.java:101)
at org.jboss.ejb3.stateless.StatelessContainer.dynamicInvoke(StatelessContainer.java:225)
at org.jboss.aop.Dispatcher.invoke(Dispatcher.java:106)
at org.jboss.aspects.remoting.AOPRemotingInvocationHandler.invoke(AOPRemotingInvocationHandler.java:82)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:828)
at org.jboss.remoting.ServerInvoker.invoke(ServerInvoker.java:681)
at org.jboss.remoting.transport.socket.ServerThread.processInvocation(ServerThread.java:358)
at org.jboss.remoting.transport.socket.ServerThread.dorun(ServerThread.java:398)
at org.jboss.remoting.transport.socket.ServerThread.run(ServerThread.java:239)
Caused by: javax.persistence.PersistenceException: org.hibernate.MappingException: Named query not known: findAllKuchen
at org.hibernate.ejb.AbstractEntityManagerImpl.throwPersistenceException(AbstractEntityManagerImpl.java:567)
at org.hibernate.ejb.AbstractEntityManagerImpl.createNamedQuery(AbstractEntityManagerImpl.java:90)
at org.jboss.ejb3.entity.TransactionScopedEntityManager.createNamedQuery(TransactionScopedEntityManager.java:134)
at de.fhw.swtvertiefung.knauf.kuchen.KuchenWorkerBean.getKuchen(KuchenWorkerBean.java:58)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
Die NamedQuery ist aber scheinbar korrekt deklariert.
Lösungsansatz: Ist die NamedQuery auf der Entity-Klasse deklariert deren Instanzen sie finden soll? Bei mir trat
dieser Fehler auf als ich die NamedQuery in der SessionBean deklarierte die den EntityManager verwendete.
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.