Beispiel: Message Driven Bean

Inhalt:

Der richtige Server
JMS-Standardkonfiguration
Anlegen der Message Driven Bean "Message"
MDB goes Server
Vorbereiten des Applicationclients
Anlegen des Applicationclients
Ausführen des Applicationclients
Mit Injection
Ohne Annotations
JavaEE7-Features

Für WildFly 31 und JakartaEE 10: Beispiel für eine Message Driven Bean, die ihre Messages per Application Client erhält.
Hier gibt es das Projekt zum Download (dies ist ein EAR-Export, die Importanleitung findet man im Stateless-Beispiel): Message.ear

Aufbau des Beispieles

a) Message Driven Bean, die an einer Queue hängt.
b) Application Client der die Nachrichten abschickt.

Die relevante Doku für dieses Beispiel findet sich: https://docs.wildfly.org/31/Admin_Guide.html#Messaging
Und das Beispiel aus den Quickstart-Guides (diese sind auch über die WildFly-Downloadseite als Gesamtpaket zu finden): https://github.com/wildfly/quickstart/tree/31.x/helloworld-jms

Das Beispiel besteht aus einem "EAR Application Project" mit dem Namen "Message", einem EJB-Projekt mit einer Message Driven Bean und einem Application Client-Projekt.

Der richtige Server

Die Default-Konfiguration von WildFly 31 im Standalone-Modus aus der Datei "standalone.xml" stellt das sogenannte "Web Profile" dar, das keine JMS-Komponenten enthält. Deployed man das Beispiel auf einem Server im Standalone-Modus, erhält man solche Fehler:
20:00:58,381 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-3) MSC000001: Failed to start service jboss.deployment.subunit."Message.ear"."MessageEJB.jar".PARSE: org.jboss.msc.service.StartException in service jboss.deployment.subunit."Message.ear"."MessageEJB.jar".PARSE: WFLYSRV0153: Failed to process phase PARSE of subdeployment "MessageEJB.jar" of deployment "Message.ear"
	at org.jboss.as.server@23.0.1.Final//org.jboss.as.server.deployment.DeploymentUnitPhaseService.start(DeploymentUnitPhaseService.java:179)
	at org.jboss.msc@1.5.2.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1617)
	at org.jboss.msc@1.5.2.Final//org.jboss.msc.service.ServiceControllerImpl$StartTask.execute(ServiceControllerImpl.java:1580)
	at org.jboss.msc@1.5.2.Final//org.jboss.msc.service.ServiceControllerImpl$ControllerTask.run(ServiceControllerImpl.java:1438)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1990)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1486)
	at org.jboss.threads@2.4.0.Final//org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1377)
	at java.base/java.lang.Thread.run(Thread.java:834)
Caused by: org.jboss.msc.service.ServiceNotFoundException: service jboss.ejb.default-resource-adapter-name-service not found
	at org.jboss.msc@1.5.2.Final//org.jboss.msc.service.ServiceContainerImpl.getRequiredService(ServiceContainerImpl.java:673)
	at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.deployment.processors.MessageDrivenComponentDescriptionFactory.getDefaultResourceAdapterName(MessageDrivenComponentDescriptionFactory.java:230)
	at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.deployment.processors.MessageDrivenComponentDescriptionFactory.processMessageBeans(MessageDrivenComponentDescriptionFactory.java:111)
	at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.deployment.processors.MessageDrivenComponentDescriptionFactory.processAnnotations(MessageDrivenComponentDescriptionFactory.java:65)
	at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.deployment.processors.AnnotatedEJBComponentDescriptionDeploymentUnitProcessor.processAnnotations(AnnotatedEJBComponentDescriptionDeploymentUnitProcessor.java:128)
	at org.jboss.as.ejb3@31.0.0.Final//org.jboss.as.ejb3.deployment.processors.AnnotatedEJBComponentDescriptionDeploymentUnitProcessor.deploy(AnnotatedEJBComponentDescriptionDeploymentUnitProcessor.java:65)
	at org.jboss.as.server@23.0.1.Final//org.jboss.as.server.deployment.DeploymentUnitPhaseService.start(DeploymentUnitPhaseService.java:172)
	... 8 more
Man könnte "standalone.xml" um die Konfiguration für JMS erweitern. Einfacher ist es, das "Full Profile" aus "standalone-full.xml" zu verwenden.

Serverstart über Kommandozeile:
standalone.bat -c standalone-full.xml

Server in Eclipse:
Hier muss man eine komplett neue Serverdefinition anlegen:
Serverdefinition mit standalone-full.xml
Auch hier gibt man "standalone-full.xml" als Konfigurationsdatei an.


JMS-Standardkonfiguration

WildFly verwendet als Messaging-System "Apache ActiveMQ Artemis" (http://activemq.apache.org/artemis/).

Im folgenden werden die relevanten Elemente der JMS-Konfiguration in "standalone-full.xml" erklärt.
Im Bereich "extensions" wird die JMS-Erweiterung der Konfigurationsdatei deklariert (ohne diese würde das Parsen der Element des Subsystems "messaging-activemq" fehlschlagen):
    <extensions>
        ...
        <extension module="org.wildfly.extension.messaging-activemq"/>
        ...
    </extensions>
Weiter unten wird diese Konfiguration als Subsystem verwendet. Dort wird ein "server" namens "default" definiert:
        <subsystem xmlns="urn:jboss:domain:messaging-activemq:16.0">
            <server name="default">
              ...
            <server>
        </subsystem>			
Die Schema-Definition für diesen Bereich findet man in der Datei "wildfly-messaging-activemq_16_0.xsd" (in "%WILDFLY_HOME%\docs\schema").

Infos zu diesem Element: https://docs.wildfly.org/31/wildscribe/subsystem/messaging-activemq/server/index.html.

Aus der ActiveMQ-Doku: https://activemq.apache.org/components/artemis/documentation/latest/configuring-transports.html (diese Doku scheint allerdings nicht für das zu gelten, was in "standalone-full.xml" steckt, dort findet sich wohl eine leicht angepasste Variante der Konfiguration)

Die erste relevante Deklaration darin ist ein "connector":
                <http-connector name="http-connector" endpoint="http-acceptor" socket-binding="http"/>
                <http-connector name="http-connector-throughput" endpoint="http-acceptor-throughput" socket-binding="http">
                    <param name="batch-delay" value="50"/>
                </http-connector>
                <in-vm-connector name="in-vm" server-id="0"/>

Ein Connector wird von einem JMS-Client genutzt, um sich mit einem Server zu verbinden.

Der "in-vm-connector" wird verwendet, wenn innerhalb der aktuell laufenden VM eine Verbindung zu ActiveMQ aufgebaut werden soll.
Der "http-connector" führt ein Verbindung zu einem Remote-Server über HTTP durch.
Der "http-connector-throughput" führt ebenfalls eine HTTP-Verbindung zu einem Remote-Server durch und ist für erhöhten Durchsatz gedacht.
Die beiden HTTP-Connectoren verweisen auf ein "socket-binding" namens "http", in dem der Port (8080) des lokalen Hosts deklariert ist. Die Bindings werden weiter unten erklärt.

Will z.B. ein Servlet eine Nachricht an eine Message Queue im gleichen Server schicken, kann es den "in-vm-connector" nutzen, oder den HTTP-Connector, der gemäß obenstehender Konfiguration ebenfalls auf den lokalen Server weiterleitet.

Umgekehrt gibt es die "acceptors":
                <http-acceptor name="http-acceptor" http-listener="default"/>
                <http-acceptor name="http-acceptor-throughput" http-listener="default">
                    <param name="batch-delay" value="50"/>
                    <param name="direct-deliver" value="false"/>
                <in-vm-acceptor name="in-vm" server-id="0"/>
                </http-acceptor>
Hier geht es um den Empfang von Nachrichten: der "in-vm-acceptor" akzeptiert Nachrichten aus der gleichen virtuellen Maschine, die "http-acceptor" und "http-acceptor-throughput" empfangen Nachrichten über das Netzwerk, und zwar horchen sie an den im "http-listener" angegebenen HTTP-Listener.
Dieser wiederum ist im Subsystem "urn:jboss:domain:undertow:14.0" deklariert:
        <subsystem xmlns="urn:jboss:domain:undertow:14.0">
            <server name="default-server">
                <http-listener name="default" redirect-socket="https" socket-binding="http"/>
                <host name="default-host" alias="localhost">
                    ...
                </host>
            </server>
Dieser Listener verweist auf ein "socket-binding" namens "http", das schon oben beim HTTP-Connector gezeigt wurde.

Die folgenden "security-settings" definieren Berechtigungen.
                <security-setting name="#">
                    <role name="guest" 
                        delete-non-durable-queue="true" 
                        create-non-durable-queue="true" 
                        consume="true"
                        send="true"/>
                </security-setting>
Hier sind die Permissions für "send" und "consume" relevant: nur ein Benutzer, der sich in der deklarierten Rolle "guest" befindet, darf Nachrichten abschicken/empfangen. Über das "name"-Attribut wird definiert, für welche Queue oder welches Topic diese Deklaration gilt. Im obigen Ausschnitt ist "#" die Wildcard für "alle".

Jetzt folgende die "connection-factories":
                <connection-factory 
                    name="InVmConnectionFactory" 
                    entries="java:/ConnectionFactory" 
                    connectors="in-vm"/>
                <connection-factory 
                    name="RemoteConnectionFactory" 
                    entries="java:jboss/exported/jms/RemoteConnectionFactory" 
                    connectors="http-connector"/>
                <pooled-connection-factory 
                    name="activemq-ra" 
                    transaction="xa" 
                    entries="java:/JmsXA java:jboss/DefaultJMSConnectionFactory" 
                    connectors="in-vm"/

Für jeden der obigen Connectoren wird eine ConnectionFactory ins JNDI gebunden. Wichtig ist hier die "pooled-connection-factory" namens "activemq-ra". Diese bindet sich an den In-VM-Connector und wird von WildFly 10 unter dem JaveEE7-Standardnamen "java:comp/DefaultJMSConnectionFactory" ins JNDI gebunden! Im folgenden Beispiel wird die In-VM-Connectionfactory "java:/ConnectionFactory" genutzt.

Am Ende von "standalone-full.xml" folgen die Socket Bindings, die Ports benennen.
Im folgenden nur die in der ActiveMQ-Konfiguration erwähnten Ports:
    <socket-binding-group name="standard-sockets" ...>
        ...
        <socket-binding name="http" port="${jboss.http.port:8080}"/>
        ...
    </socket-binding-group>

Wenn man weiten oben einen Connector auf einen Remote Host deklariert hätte, dann müsste man hier wohl ein "outbound-socket-binding" mit Namen und Port des Remote Host deklarieren:
        <outbound-socket-binding name="myremotemessaging">
            <remote-destination host="otherhost" port="5445"/>
        </outbound-socket-binding>
Dieser Connector müsste im "socket-binding"-Attribut den Namen dieses Socket Binding, also "myremotemessaging", stehen haben. Ein Beispiel hierfür gibt es in der Konfiguration des JavaEE-ApplicationClient.

Die Connectoren der Standard-Konfiguration referenzieren alle nur "socket-binding"-Elemente. Ich nehme an, dass sie deshalb per Default mit dem deklarieren Port des Servers "localhost" kommunizieren.

Anlegen der Message Driven Bean "Message"

Über "New" -> "Other..." wählen wir "EJB" -> "Message-Driven Bean (3.x)" aus:
Message Driven Bean (1)
Die Klasse heißt "MessageBean" und liegt im Package "de.hsrm.jakartaee.knauf.mdb".
Als "Destination Name" wird ein Name angegeben, unter dem die zugehörige Queue ins Server-JNDI gebunden wird, hier "jms/queue/MessageBeanQueue". Falls wir keine Queue im Server konfigurieren (siehe weiter unten) wird hier beim Deploy eine neue angelegt.
Der "Destination Type" gibt an, ob es sich bei dieser Bean um eine Queue (Punkt-zu-Punkt-Verbindung, der Client schickt die Nachricht an einen bestimmten Empfänger, wie eine Telefonverbindung) oder ein Topic (es gibt eine unbekannte Anzahl von Empfängern, vergleichbar mit dem Radio-Senden) handelt.
Message Driven Bean
Im nächsten Schritt können wir alles bei den Defaults belassen.


Die generierte Klasse sieht so aus (bereinigt um Kommentare, außerdem wurde bei den "import"-Statements "javax" durch "jakarta" ersetzt, in der Property "destinationType" muss ebenfalls ein Package angepasst werden):
package de.hsrm.jakartaee.knauf.mdb;

import jakarta.ejb.ActivationConfigProperty;
import jakarta.ejb.MessageDriven;
import jakarta.jms.Message;
import jakarta.jms.MessageListener;

@MessageDriven(
		activationConfig = { @ActivationConfigProperty(
				propertyName = "destinationType", propertyValue = "jakarta.jms.Queue"), @ActivationConfigProperty(
				propertyName = "destination", propertyValue = "jms/queue/MessageBeanQueue")
		}, 
		mappedName = "jms/queue/MessageBeanQueue")
public class MessageBean implements MessageListener
{
  public MessageBean()
  {
  }

  public void onMessage(Message message)
  {
  }
}
Anmerkung: das Attribut "mappedName" ist in meinem Beispielcode nicht enthalten, es geht also auch ohne.


In der Methode onMessage steckt die Implementierung der Bean:
  private static final Logger logger = Logger.getLogger ( MessageBean.class.getName() );

  public void onMessage(Message message)
  {
    try
    {
      MessageBean.logger.info("onMessage: Message vom Typ " + message.getClass().toString() + " erhalten");
      if (message instanceof TextMessage)
      {
        TextMessage textMessage = (TextMessage) message;
        MessageBean.logger.info("TextMessage enthält diesen Text: " + textMessage.getText() );
      }
      else
        MessageBean.logger.info("Sonstige Message. toString() = " + message.toString() );
    }
    catch (JMSException jex)
    {
      MessageBean.logger.log( Level.SEVERE, "Fehler beim Verarbeiten der Message: " + jex.getMessage(), jex );
      throw new EJBException ("Fehler beim Verarbeiten der Message: " + jex.getMessage(), jex );
    }
  }

Schon sind wir kurz davor die MDB auf den Server zu packen.


MDB goes Server

Anmerkung: scheinbar generiert JBoss automatisch eine Queue, wenn wir folgende Config-Schritte nicht durchführen!

Also deklarieren wir die Queue. Hierfür gibt es drei Möglichkeiten (sowie die Möglichkeit des automatischen Erzeugens):

Queue-Statistiken:
ActiveMQ bietet nicht viele Möglichkeiten, Informationen über die Queue abzurufen. Vor allem ist es nicht möglich, eine Historie der gesendeten Nachrichten anzuzeigen (auch wenn die WildFly-CLI-Befehle sehr danach klingen, als wäre es möglich).

Für das Abrufen der Anzahl gesendeter Messages müssen in der Serverkonfiguration die "Statistics" aktiviert werden:
        <subsystem xmlns="urn:jboss:domain:messaging-activemq:16.0">
            <server name="default">
                <security elytron-domain="ApplicationDomain"/>
                <statistics enabled="${wildfly.messaging-activemq.statistics-enabled:${wildfly.statistics-enabled:false}}"/>
Für "enabled" wird "true" eingetragen.

Jetzt können wir in der Management Console die Anzahl der über die Queue gesendeten Nachrichten abrufen:
Messages added

Über CLI geht es mit diesem Befehl:
/subsystem=messaging-activemq/server=default/jms-queue=MessageBeanQueue:read-attribute(name=messages-added)

Ausgabe:
{
    "outcome" => "success",
    "result" => 2L
}

Weitere Management-Operationen:

Anzahl der Nachrichten, die aktuell in der Queue stehen (hier kann man eine Ausgabe tricksen, indem in "MessageBean.onMessage" ein Thread.sleep eingebaut wird):
[standalone@localhost:9990 /] jms-queue --queue-address=MessageBeanQueue count-messages
Eine Langform des gleichen Befehls (diese hat den Vorteil, dass man die Operationen wie "count-messages" per Code completion angezeigt bekommt):
[standalone@localhost:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=MessageBeanQueue:count-messages

Liefert leider keine Ergebnisse, auch nicht mit obigem "Thread.sleep"-Trick: Ausgabe der aktuell in der Queue befindlichen, aber noch nicht zugestellten Nachrichten:
[standalone@localhost:9990 /] jms-queue --queue-address=MessageBeanQueue list-messages


Details über die Queue:
[standalone@localhost:9990 /] /subsystem=messaging-activemq/server=default/jms-queue=MessageBeanQueue:read-resource


Vorbereiten des Applicationclients

Schritt 1: Konfiguration
Die Konfiguration des JMS-Framework muss analog zu den Einträgen von "standalone-full.xml" auch in der Configdatei des Client-Container ("appclient.xml"). Tun wir das nicht, führt das zu diesem Fehler:
19:37:33,522 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-1) WFLYSRV0027: Starting deployment of "Message.ear" (runtime-name: "Message.ear")
19:37:33,687 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-1) WFLYSRV0207: Starting subdeployment (runtime-name: "MessageClient.jar")
19:37:33,687 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-4) WFLYSRV0207: Starting subdeployment (runtime-name: "MessageEJB.jar")
19:37:33,931 INFO  [org.jboss.as.ejb3.deployment] (MSC service thread 1-5) WFLYEJB0473: JNDI bindings for session bean named 'MessageBean' in deployment unit 'subdeployment "MessageEJB.jar" of deployment "Message.ear"' are as follows:


19:37:34,048 ERROR [org.jboss.as.controller.management-operation] (Thread-45) WFLYCTL0013: Operation ("deploy") failed - address: ([("deployment" => "Message.ear")]) - failure description: {
    "WFLYCTL0412: Required services that are not installed:" => ["jboss.naming.context.java.ConnectionFactory"],
    "WFLYCTL0180: Services with missing/unavailable dependencies" => ["service jboss.naming.context.java.module.Message.MessageClient.env.jms.MBConnectionFactory is missing [jboss.naming.context.java.ConnectionFactory]"]
}
19:37:34,048 ERROR [org.jboss.as.controller.management-operation] (Thread-45) WFLYCTL0013: Operation ("deploy") failed - address: ([("deployment" => "Message.ear")]) - failure description: {
    "WFLYCTL0412: Required services that are not installed:" => ["jboss.naming.context.java.ConnectionFactory"],
    "WFLYCTL0180: Services with missing/unavailable dependencies" => ["service jboss.naming.context.java.module.Message.MessageClient.env.jms.MBConnectionFactory is missing [jboss.naming.context.java.ConnectionFactory]"]
}
19:37:34,049 ERROR [org.jboss.as.server] (Thread-45) WFLYSRV0021: Deploy of deployment "Message.ear" was rolled back with the following failure message:
{
    "WFLYCTL0412: Required services that are not installed:" => ["jboss.naming.context.java.ConnectionFactory"],
    "WFLYCTL0180: Services with missing/unavailable dependencies" => ["service jboss.naming.context.java.module.Message.MessageClient.env.jms.MBConnectionFactory is missing [jboss.naming.context.java.ConnectionFactory]"]
}

Also öffnen wir "%WILDFLY_HOME%\appclient\configuration\appclient.xml" und fügen folgendes hinzu:


Im Element "extensions" steht bereits die Aktivierung der Module "org.wildfly.extension.messaging-activemq" und "org.wildfly.extension.elytron", die wir hier benötigen. In früheren Versionen musste dies noch händisch eingetragen werden.

    <extensions>
		...
		<extension module="org.wildfly.extension.elytron"/>
		<extension module="org.wildfly.extension.messaging-activemq"/>
		...
    </extensions>
Im Element "profile" suchen wir das Subsystem "urn:jboss:domain:messaging-activemq:15.0", das leer ist, und befüllen es so:
        <subsystem xmlns="urn:jboss:domain:messaging-activemq:15.0">
            <server name="default">
                <security elytron-domain="ApplicationDomain"/>
                
                <address-setting name="#" message-counter-history-day-limit="10" page-size-bytes="2097152" max-size-bytes="10485760" expiry-address="jms.queue.ExpiryQueue" dead-letter-address="jms.queue.DLQ"/>
                <http-connector name="http-connector" endpoint="http-acceptor" socket-binding="http"/>

                <!--
                <jms-queue name="MessageBeanQueue" entries="java:/jms/queue/MessageBeanQueue"/>
                -->
				
                <connection-factory name="RemoteConnectionFactory" entries="java:/ConnectionFactory" connectors="http-connector"/>
            </server>
        </subsystem>
Hier wird ein einziger Connector deklariert, da wir in der Anwendung nur eine Verbindung zu einem fernen Server aufbauen wollen. Deshalb nehmen wir als Typ des Connectors einen "http-connector", der das Socket Binding "http" verwendet (siehe nächster Schritt).
Der JNDI-Name der "RemoteConnectionFactory" ("java:/ConnectionFactory") wird so von der Client-Anwendung benötigt.
Die elytron-domain ist nötig, da wir kein anonymes Senden an die Queue durchführen können und stattdessen eine Anmeldung mit Username und Passwort durchführen.

Anmerkung: das Beispiel enthält einen auskommentierten Block, der die Queue deklariert. Das klappt natürlich nicht, wenn wie im Beispielcode nach Variante 3 die Queue durch die Config-Datei "knaufmq-activemq-jms.xml" als Teil des EJB-Deployment erzeugt wird!


Definiert wird die "elytron-domain" im Subsystem urn:wildfly:elytron:17.0, das wir ebenfalls vorfinden. Hier wird der fett markierte Teil zugefügt:
        <subsystem xmlns="urn:wildfly:elytron:17.0" final-providers="elytron" disallowed-providers="OracleUcrypto">
            <providers>
                <provider-loader name="elytron" module="org.wildfly.security.elytron"/>
            </providers>
            <security-domains>
                <security-domain name="ApplicationDomain"/>
            </security-domains>
        </subsystem>


Im Bereich "socket-binding-group" wird deklariert, zu welchem Server/Port sich die Queue verbinden soll. Der Name des Bindings entspricht dem beim Connector angegebenen "socket-binding"
    <socket-binding-group name="standard-sockets" default-interface="public">
      ...
      <outbound-socket-binding name="http">
        <remote-destination host="localhost" port="8080"/>
      </outbound-socket-binding>
    </socket-binding-group>
	

Besonderheit ist hier, dass wir ein "outbound-socket-binding" deklarieren, d.h. ein ausgehender Port inklusive Zielserver wird deklariert. Würden wir hier ein "socket-binding" deklarieren (so wie es die anderen Deklarationen in diesem Bereich tun), dann würde das vom "Connector" zwar auch als ausgehender Port genutzt, dieser würde sich aber nur mit "localhost" verbinden, statt mit einem fernen Server. Siehe z.B.
https://docs.wildfly.org/31/Developer_Guide.html#create-a-outbound-socket-binding-on-the-client-server


Schritt 2: User hinzufügen:
Das Senden an eine Queue (liegt das hier nur daran, dass wir uns außerhalb des Server-Prozesses befinden?) ist verboten, ohne dass der User authentifiziert ist. Starten wir unseren Application Client ohne Anmeldung (also ohne Angabe eines Users beim Erzeugen der Queue-Connection: queueConnectionFactory.createQueueConnection();), kommt eine solche Exception im Client:
2024-02-15 19:54:40,595 ERROR [stderr] (AWT-EventQueue-0) jakarta.jms.JMSSecurityException: AMQ229031: Unable to validate user from /127.0.0.1:51650. Username: tester; SSL certificate subject DN: unavailable
2024-02-15 19:54:40,595 ERROR [stderr] (AWT-EventQueue-0) 	at org.apache.activemq.artemis.client@2.31.2//org.apache.activemq.artemis.core.protocol.core.impl.ChannelImpl.sendBlocking(ChannelImpl.java:560)
2024-02-15 19:54:40,595 ERROR [stderr] (AWT-EventQueue-0) 	at org.apache.activemq.artemis.client@2.31.2//org.apache.activemq.artemis.core.protocol.core.impl.ChannelImpl.sendBlocking(ChannelImpl.java:452)
Also verwenden wir das Script "%JBOSS_HOME%\bin\add-user.bat", um einen neuen User anzulegen:

add user


Anlegen des Applicationclients

Der Applicationclient muss nicht die EJB-JARs referenzieren, da MessageDrivenBeans über keine Remote/Local-Interfaces verfügen.
Es wird ein neue Klasse vom Typ javax.swing.JFrame zugefügt.
Sie muss in "Manifest.mf" als Main-Class eingetragen werden.

Wichtig: es muss für das Beenden des Application Client die gleiche Logik implementiert werden, die im
Stateless-Beispiel implementiert ist ("defaultCloseOperation", Warten auf Schließen des Fensters).

Auf dem JFrame einen Button und ein Textfeld platzieren. Beim Button-Klick soll die Eingabe aus dem Textfeld an die MessageBean geschickt werden.
Der Code dazu sieht so aus:
  private void sendMessage()
  {
    try
    {
      InitialContext initialContext = new InitialContext();
      QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory) initialContext.lookup("java:comp/env/jms/MBConnectionFactory");

      //Die konfigurierte Queue holen:
      Queue queue = (Queue) initialContext.lookup("java:comp/env/jms/MBQueueRef");

      //Verbindung erzeugen:
      //Hier muss der User angegeben werden!
      QueueConnection queueConnection = queueConnectionFactory.createQueueConnection("tester", "test123!");
      QueueSession queueSession = queueConnection.createQueueSession(false, Session.AUTO_ACKNOWLEDGE);
      QueueSender queueSender = queueSession.createSender(queue);

      //Senden der Nachricht:
      TextMessage textMessage = queueSession.createTextMessage();
      textMessage.setText(this.jTextFieldMessage.getText());

      queueSender.send(textMessage);

      //Alles sauber aufräumen:
      queueSender.close();
      queueSession.close();
      queueConnection.close();

      //Fertig.
      JOptionPane.showMessageDialog(this, "Nachricht wurde gesendet und sollte im Serverlog stehen !");
    }
    catch (Exception ex)
    {
      ex.printStackTrace();
    }
  }
An den InitialContext müssen keine Parameter übergeben werden.
Man holt sich die QueueConnectionFactory aus einem Environment Naming Context-Eintrag "java:comp/env/jms/MBConnectionFactory", der im Folgenden beschrieben wird.
Danach wird die Queue über "java:comp/env/jms/MBQueueRef" geholt.
Anschließend erzeugt man sich über die Connection Factory eine Connection, startet eine QueueSession und erzeugt daraus und der Ziel-Queue einen QueueSender. Über diesen schickt man die Textnachricht.

Jetzt kommt die Hauptarbeit: die Resourcen-Referenzen müssen deklariert werden:
In "application-client.xml" wird folgendes eingefügt:
   <resource-ref>
      <res-ref-name>jms/MBConnectionFactory</res-ref-name>
      <res-type>jakarta.jms.QueueConnectionFactory</res-type>
      <res-auth>Container</res-auth>
   </resource-ref>
   <resource-ref>
      <res-ref-name>jms/MBQueueRef</res-ref-name>
      <res-type>jakarta.jms.Queue</res-type>
      <res-auth>Container</res-auth>
   </resource-ref>

Die eigentliche Verknüpfung mit den globalen JNDI-Namen der JBoss-Resourcen erfolgt über die Datei "jboss-client.xml". Wir könnten natürlich (wie in den bisherigen Beispielen) den XML-Inhalt händisch zufügen, aber um den WebTools-Plugin ein bisschen besser kennen zu lernen machen wir das hier über den umständlichen Weg:

Dazu Rechtsklick auf "META-INF" im Client-Projekt und "New" -> "Other..." wählen. Wir wählen "XML" -> "XML" aus.
jboss-client.xml (1)
Im ersten Schritt wird der Dateiname "jboss-client.xml" angegeben und des META-INF-Verzeichnis als Ziel gewählt.
jboss-client.xml (2)
Im nächsten Schritt wird die Option "Create file using a DTD or XML schema file" gewählt.
jboss-client.xml (3)
Die DTD-Datei steckt im XML-Katalog, wir wählen sie aus - allerdings finden wir hier nicht aktuelle Version 9.0, sondern nur eine ältere Version 8.1 (https://issues.redhat.com/browse/JBIDE-29144):
jboss-client.xml (4)
Im letzten Schritt würde ich empfehlen, den Prefix für den Default-Namespace "jboss" über "Edit" zu kicken (sprich im Feld "Prefix" den Wert zu löschen"). Grund: ansonsten hätten wir vor jedem Element "jboss:" stehen, und das verringert die Lesbarkeit der "jboss-client.xml" eher:
jboss-client.xml (5)
Das erzeugt eine Rumpf-XML-Datei mit sauberer XSD-Deklaration. Diese ändern wir auf "9.0" (Achtung: geänderter Namespace) und noch das Attribut version="9.0" zu (optional)

Jetzt fügen wir die Resourcen-Referenzen zu. Wichtig ist dass die Elemente res-ref-name in "jboss-client.xml" den gleichen Wert haben wie die zugehörigen Elemente in "application-client.xml"!
<?xml version="1.0" encoding="UTF-8"?>
<jboss-client xmlns="urn:jboss:jakartaee:1.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="urn:jboss:jakartaee:1.0 https://www.jboss.org/schema/jbossas/jboss-client_9_0.xsd"
	version="9.0">
	<jndi-name>MessageClient</jndi-name>
	<resource-ref>
		<res-ref-name>jms/MBConnectionFactory</res-ref-name>
		<jndi-name>java:/ConnectionFactory</jndi-name>
	</resource-ref>
	<resource-ref>
		<res-ref-name>jms/MBQueueRef</res-ref-name>
		<jndi-name>java:/jms/queue/MessageBeanQueue</jndi-name>
	</resource-ref>
</jboss-client>
Die ConnectionFactory wird an den Namen "java:/ConnectionFactory" gebunden. Wie dieser konfiguriert wird, sehen wir im nächsten Abschnitt.

Die Queue wird (siehe ebenfalls nächster Abschnitt) an "java:/jms/queue/MessageBeanQueue" gebunden. In der dort angegebenen Konfiguration ist der Queue-Name als "queue/MessageBeanQueue" deklariert, aber hier in "jboss-client.xml" müssen wir "java:/jms" voranstellen, weil alle Queues unter diesem Namen gebunden werden.


Ausführen des Applicationclients

Gestart wird die Anwendung durch diesen Aufruf:
%WILDFLY_HOME%\bin\appclient.bat Message.ear#MessageClient.jar


Achtung: die EAR-Dateien, die hier zum Download stehen, wurden mit Java 11 erstellt. Mit Java 17 gibt 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 11 ohne Verwendung des Modulsystems 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 Message.ear#MessageClient.jar 

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


Mit Injection

Hier gibt es ein EAR-Projekt mit für die Injection vorbereitetem Client: MessageInjection.ear

Der Code des Clients sieht so aus (die injizierten Variablen müssen static sein und in der Main Class stecken):
  @Resource(name="jms/MBConnectionFactory")
  private static QueueConnectionFactory queueConnectionFactory;
  
  @Resource(name="jms/MBQueueRef")
  private static Queue queue;
Für Queue und QueueConnectionFactory greife ich auf einen Eintrag aus dem ENC zu (siehe letzter Abschnitt).

Hier hätte auch ein Binden an den globalen JNDI-Namen ohne Verwendung des ENC geklappt:
  @Resource(mappedName="ConnectionFactory")
  private static QueueConnectionFactory queueConnectionFactory;
  
  @Resource(mappedName="queue/MessageBeanQueue")  
  private static Queue queue;

Die Konfiguration des Environment Naming Context sieht so aus:

application-client.xml:
<?xml version="1.0" encoding="UTF-8"?>
<application-client version="10"
      xmlns="https://jakarta.ee/xml/ns/jakartaee"
      xmlns:xml="http://www.w3.org/XML/1998/namespace"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/application-client_10.xsd">
   <display-name>MessageClient</display-name>
   <resource-ref>
      <res-ref-name>jms/MBConnectionFactory</res-ref-name>
      <res-type>jakarta.jms.QueueConnectionFactory</res-type>
      <res-auth>Container</res-auth>
   </resource-ref>
   
   <message-destination-ref>
      <message-destination-ref-name>jms/MBQueueRef</message-destination-ref-name>
      <message-destination-type>jakarta.jms.Queue</message-destination-type>
   </message-destination-ref>
   
</application-client>
jboss-client.xml:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-client xmlns="urn:jboss:jakartaee:1.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="urn:jboss:jakartaee:1.0 https://www.jboss.org/schema/jbossas/jboss-client_9_0.xsd"
	version="9.0">
  <jndi-name>MessageClient</jndi-name>
  <resource-ref>
      <res-ref-name>jms/MBConnectionFactory</res-ref-name>
      <jndi-name>java:/ConnectionFactory</jndi-name>
  </resource-ref>
  
  <message-destination-ref>
      <message-destination-ref-name>jms/MBQueueRef</message-destination-ref-name>
      <jndi-name>java:/jms/queue/MessageBeanQueue</jndi-name>
  </message-destination-ref>
</jboss-client>
Wichtig ist, dass die Queue nicht als resource-ref eingetragen wird, sondern als message-destination-ref!


Ohne Annotations

"ejb-jar.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar xmlns="https://jakarta.ee/xml/ns/jakartaee"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/ejb-jar_4_0.xsd"
      version="4.0">
	<display-name>MessageEJB</display-name>
	<enterprise-beans>
	
		<message-driven>
			<display-name>MessageBean</display-name>
			<ejb-name>MessageBean</ejb-name>
			<ejb-class>de.hsrm.jakartaee.knauf.mdb.MessageBean</ejb-class>
			<messaging-type>jakarta.jms.MessageListener</messaging-type> 
			<transaction-type>Container</transaction-type>
			<message-destination-type>jakarta.jms.Queue</message-destination-type>
			<activation-config>
				<activation-config-property>
					<activation-config-property-name>destinationType</activation-config-property-name>
					<activation-config-property-value>jakarta.jms.Queue</activation-config-property-value>
				</activation-config-property>
				<activation-config-property>
					<activation-config-property-name>destination</activation-config-property-name>
					<activation-config-property-value>queue/MessageBeanQueue</activation-config-property-value>
				</activation-config-property>
			</activation-config>
		</message-driven>
	</enterprise-beans>
</ejb-jar> 
Die Elemente "messaging-type", "transaction-type" und "message-destination-type" sind scheinbar optional, die Anwendung hat jedenfalls auch ohne funktioniert.


Auf Injection im Application Client habe ich hier verzichtet.

Die modifizierte Version des Projekts gibt es hier:
MessageNoAnnotation.ear (es sollte beim Import in "Message" umbenannt werden)
ACHTUNG: Dieses Projekt kann nicht neben dem obigen Message-Beispiel existieren !





WEITERMACHEN WEITERMACHEN WEITERMACHENWEITERMACHEN


JavaEE7-Features

In JavaEE7 wurde die JMS-Spezifikation mit Version 2 deutlich erweitert.
Auf Seiten der EJB ist es jetzt möglich, die Queue per Annotation zu deklarieren, ohne Container-spezifische Deploymentdeskriptoren wie "xxx-activemq-jms.xml" oder ein Eintrag in die Server-Config ("standalone-full.xml").
Hierzu wird die Annotation jakarta.jms.JMSDestinationDefinition verwendet:
@MessageDriven (activationConfig=
  {
    @ActivationConfigProperty(propertyName="destinationType", propertyValue="jakarta.jms.Queue"),
    @ActivationConfigProperty(propertyName="destination", propertyValue="java:app/jms/queue/MessageBeanQueue")
  })
@JMSDestinationDefinition(
    name = "java:app/jms/queue/MessageBeanQueue",
    interfaceName = "jakarta.jms.Queue",
    destinationName = "MessageBeanQueue"
  )
public class MessageBean implements MessageListener
}
Man beachte, dass die Queue hier ins private JNDI der Anwendung gebunden wird (siehe
https://community.jboss.org/thread/235447)

In der JNDIView findet man die Queue jetzt im privaten Bereich der Anwendung:
JNDIView
Entsprechend kann man sie über CLI auch nur ansprechen, wenn man die Anwendung angibt.
[standalone@localhost:9990 /] /deployment=Message.ear/subdeployment=MessageEJB.jar/subsystem=messaging-activemq/server=default/jms-queue=MessageBeanQueue:list-messages
{
    "outcome" => "success",
    "result" => []
}

Auch auf Client-Seite ergeben sich Vereinfachungen: man kann mit dem jakarta.jms.JMSContext arbeiten, der das Senden der Nachricht vereinfacht. Die ConnectionFactory und die Queue muss man allerdings wie gehabt aus dem JNDI holen.
      QueueConnectionFactory queueConnectionFactory = (QueueConnectionFactory) initialContext.lookup("java:comp/env/jms/MBConnectionFactory");

      Queue queue = (Queue) initialContext.lookup("java:comp/env/jms/MBQueueRef");
      
      JMSContext context = queueConnectionFactory.createContext("tester", "test123!");
      TextMessage textMessage = context.createTextMessage();
      textMessage.setText(this.jTextFieldMessage.getText());
      context.createProducer().send(queue, textMessage);
      
      context.close();
Der JMSContext wird hier über die jakarta.jms.ConnectionFactory geholt, dabei wird das Passwort übergeben.

Anmerkung: in einer serverseitigen Anwendung (Web oder EJB) könnte man sich den Context auch injizieren lassen, aber das geht in einem Application Client nicht (siehe https://jakarta.ee/specifications/messaging/3.1/apidocs/jakarta.messaging/jakarta/jms/jmscontext: Applications running in the Jakarta EE web and EJB containers may alternatively inject a JMSContext into their application using the @Inject annotation).

Anmerkung 2: JavaEE 7 definiert eine Connectionfactory unter dem Namen "java:comp/DefaultJMSConnectionFactory". Dies gilt aber nur für Anwendungen, die innerhalb des Server laufen. Im ApplicationClient muss man die JMS-Konfiguration über "appclient" selbst definieren, und dabei kann man die ConnectionFactory beliebig benennen.

In "jboss-client.xml" habe ich die Environment Naming Context-Eintrag an den "java:"-Bereich gebunden.
	<resource-ref>
		<res-ref-name>jms/MBQueueRef</res-ref-name>
		<jndi-name>java:/jms/queue/MessageBeanQueue</jndi-name>
	</resource-ref>


In "%WILDFLY_HOME%\appclient\configuration\appclient.xml" muss die MDB "MessageBeanQueue" ebenfalls an den Namen "java:/jms/queue/MessageBeanQueue" gebunden werden (einziger Unterschied zu obiger Konfiguration):
        <subsystem xmlns="urn:jboss:domain:messaging-activemq:1.0">
            <server name="default">
                <security-setting name="#">
                    <role name="guest" delete-non-durable-queue="true" create-non-durable-queue="true" consume="true" send="true"/>
                </security-setting>
                <address-setting name="#" message-counter-history-day-limit="10" page-size-bytes="2097152" max-size-bytes="10485760" expiry-address="jms.queue.ExpiryQueue" dead-letter-address="jms.queue.DLQ"/>
                <http-connector name="http-connector" endpoint="http-acceptor" socket-binding="http"/>
				
                <jms-queue name="MessageBeanQueue" entries="java:/jms/queue/MessageBeanQueue"/>
                
                <connection-factory name="RemoteConnectionFactory" entries="java:/ConnectionFactory" connectors="http-connector"/>
            </server>
        </subsystem>
Mein erster Ansatz war, auch im Client die Queue an "java:app/jms/queue/MessageBeanQueue" zu binden:
	<resource-ref>
		<res-ref-name>jms/MBQueueRef</res-ref-name>
		<jndi-name>java:app/jms/queue/MessageBeanQueue</jndi-name>
	</resource-ref>

Aber diese Variante funktioniert nicht (getestet bis WildFly 31). Siehe https://community.jboss.org/message/863696 und https://issues.jboss.org/browse/WFLY-3211
Es gibt diese Fehlermeldung:

20:23:10,912 ERROR [org.jboss.as.controller.management-operation] (Thread-42) WFLYCTL0013: Operation ("deploy") failed - address: ([("deployment" => "Message.ear")]) - failure description: {
    "WFLYCTL0412: Required services that are not installed:" => ["jboss.naming.context.java.app.Message.jms.queue.MessageBeanQueue"],
    "WFLYCTL0180: Services with missing/unavailable dependencies" => ["jboss.naming.context.java.module.Message.MessageClient.env.jms.MBQueueRef is missing [jboss.naming.context.java.app.Message.jms.queue.MessageBeanQueue]"]
}
20:23:10,913 ERROR [org.jboss.as.server] (Thread-42) WFLYSRV0021: Deploy of deployment "Message.ear" was rolled back with the following failure message:
{
    "WFLYCTL0412: Required services that are not installed:" => ["jboss.naming.context.java.app.Message.jms.queue.MessageBeanQueue"],
    "WFLYCTL0180: Services with missing/unavailable dependencies" => ["jboss.naming.context.java.module.Message.MessageClient.env.jms.MBQueueRef is missing [jboss.naming.context.java.app.Message.jms.queue.MessageBeanQueue]"]
}


Mehr zu den JMS 2.0-Features (wobei ein Teil davon nicht für Application Clients zutrifft): http://www.mastertheboss.com/jboss-jms/jms-20-tutorial-on-wildfly-as

Die modifizierte Version des Projekts gibt es hier: MessageJavaEE7.ear (es sollte beim Import in "Message" umbenannt werden)
ACHTUNG: Dieses Projekt kann nicht neben dem obigen Message-Beispiel existieren !


Stand 03.03.2024
Historie:
03.03.2024: Erstellt aus JavaEE8-Beispiel und angepasst an JakartaEE10 und WildFly 31