Beispiel: Message Driven Bean auf WildFly 8

Inhalt:

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

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.jboss.org/author/display/WFLY8/Messaging+configuration
Und das Beispiel aus den Quickstart-Guides (diese sind auch über die WildFly-Downloadseite als Gesamtpaket zu finden): https://github.com/wildfly/quickstart/tree/master/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 8 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:42:34,808 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: JBAS018733: Failed to process phase PARSE of subdeployment "MessageEJB.jar" of deployment "Message.ear"
	at org.jboss.as.server.deployment.DeploymentUnitPhaseService.start(DeploymentUnitPhaseService.java:166) [wildfly-server-8.0.0.Beta2-SNAPSHOT.jar:8.0.0.Beta2-SNAPSHOT]
	at org.jboss.msc.service.ServiceControllerImpl$StartTask.startService(ServiceControllerImpl.java:1944) [jboss-msc-1.2.0.Beta2.jar:1.2.0.Beta2]
	at org.jboss.msc.service.ServiceControllerImpl$StartTask.run(ServiceControllerImpl.java:1877) [jboss-msc-1.2.0.Beta2.jar:1.2.0.Beta2]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) [rt.jar:1.7.0_45]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) [rt.jar:1.7.0_45]
	at java.lang.Thread.run(Unknown Source) [rt.jar:1.7.0_45]
Caused by: org.jboss.msc.service.ServiceNotFoundException: Service service jboss.ejb.default-resource-adapter-name-service not found
	at org.jboss.msc.service.ServiceContainerImpl.getRequiredService(ServiceContainerImpl.java:668) [jboss-msc-1.2.0.Beta2.jar:1.2.0.Beta2]
	at org.jboss.as.ejb3.deployment.processors.MessageDrivenComponentDescriptionFactory.getDefaultResourceAdapterName(MessageDrivenComponentDescriptionFactory.java:270)
	at org.jboss.as.ejb3.deployment.processors.MessageDrivenComponentDescriptionFactory.processMessageBeans(MessageDrivenComponentDescriptionFactory.java:152)
	at org.jboss.as.ejb3.deployment.processors.MessageDrivenComponentDescriptionFactory.processAnnotations(MessageDrivenComponentDescriptionFactory.java:80)
	at org.jboss.as.ejb3.deployment.processors.AnnotatedEJBComponentDescriptionDeploymentUnitProcessor.processAnnotations(AnnotatedEJBComponentDescriptionDeploymentUnitProcessor.java:58)
	at org.jboss.as.ejb3.deployment.processors.AbstractDeploymentUnitProcessor.deploy(AbstractDeploymentUnitProcessor.java:81)
	at org.jboss.as.server.deployment.DeploymentUnitPhaseService.start(DeploymentUnitPhaseService.java:159) [wildfly-server-8.0.0.Beta2-SNAPSHOT.jar:8.0.0.Beta2-SNAPSHOT]
	... 5 more
Man könnte "standalone.xml" im 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


JMS-Standardkonfiguration

WildFly verwendet als Messaging-System "HornetQ" (http://www.jboss.org/hornetq/).

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 "hornetq-server" fehlschlagen):
    <extensions>
        ...
        <extension module="org.jboss.as.messaging"/>
        ...
    </extensions>
Weiter unten wird diese Konfiguration als Subsystem verwendet. Dort wird ein "hortneq-server" definiert:
        <subsystem xmlns="urn:jboss:domain:messaging:1.4">
            <hornetq-server>
				...
            </hornetq-server>
        </subsystem>			
Infos zu diesem Element:
http://wildscribe.github.io/Wildfly/8.0.0.CR1/%2Fsubsystem%2Fmessaging%2Fhornetq-server%2Findex.html
Und aus der HornetQ-Doku: http://docs.jboss.org/hornetq/2.4.0.Final/docs/user-manual/html/configuring-transports.html

Die erste relevante Deklaration darin ist ein "connector":
Ausschnitt aus der Config von WildFly 8 Final:
                <connectors>
                    <http-connector name="http-connector" socket-binding="http">
                        <param key="http-upgrade-endpoint" value="http-acceptor"/>
                    </http-connector>
                    <http-connector name="http-connector-throughput" socket-binding="http">
                        <param key="http-upgrade-endpoint" value="http-acceptor-throughput"/>
                        <param key="batch-delay" value="50"/>
                    </http-connector>
                    <in-vm-connector name="in-vm" server-id="0"/>
                </connectors>

Ausschnitt aus der Config von WildFly 8 CR1 (Community Release 1, ein Release Candidate vor WildFly 8 Final):
                <connectors>
                    <netty-connector name="netty" socket-binding="messaging"/>
                    <netty-connector name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                    </netty-connector>
                    <http-connector name="http" socket-binding="http"/>
                    <in-vm-connector name="in-vm" server-id="0"/>
                </connectors>
Ein Connector wird von einem JMS-Client genutzt, um sich mit einem Server zu verbinden.

Für Remote-Verbindungen verwendet HornetQ das "Netty"-Framework (http://netty.io/), ein Client-Server-Framework für Netzwerkanwendungen. Die beiden "netty-connector"-Elemente aus der WildFly8-CR1-Config deklarieren also Remote-Connectoren, die Verbindungen zu dem im Attribut "socket-binding" deklarierten Ports aufbauen (siehe weiter unten). Der Connector "netty-troughput" ist dabei wohl für höheren Nachrichtendurchsatz gedacht (indem kleine Nachrichten gesammelt und als Gesamtpaket nur alle "batch-delay" Millisekunden verschickt werden).
Der "in-vm-connector" wird verwendet, wenn innerhalb der aktuell laufenden VM eine Verbindung zu HornetQ aufgebaut werden soll.
Der "http-connector" (hieß in Beta-Versionen noch "servlet-connector") führt ein Verbindung zu einem Remote-Server über HTTP durch.
In der WildFly8-Final-Config ist der Netty-Connector entfallen, da in dieser Version jegliche Server-Kommunikation über den HTTP-Port 8080 und das HTTP-Upgrade-Protokoll "getunnelt" werden. Deshalb gibt es hier nur noch den HTTP-Connector sowie den Connector "http-connector-throughput" für erhöhten Durchsatz.

Will z.B. ein Servlet eine Nachricht an eine Message Queue im gleichen Server schicken, würde sie den "in-vm-connector" nutzen. Geht es an einen remote Server, wäre der "netty-connector" relevant.


Umgekehrt gibt es die "acceptors":
Ausschnitt aus der Config von WildFly 8 Final:
                <acceptors>
                    <http-acceptor http-listener="default" name="http-acceptor"/>
                    <http-acceptor http-listener="default" name="http-acceptor-throughput">
                        <param key="batch-delay" value="50"/>
                        <param key="direct-deliver" value="false"/>
                    </http-acceptor>
                    <in-vm-acceptor name="in-vm" server-id="0"/>
                </acceptors>
Ausschnitt aus der Config von WildFly 8 CR1
                <acceptors>
                    <http-acceptor name="http" http-listener="default"/>
                    <netty-acceptor name="netty" socket-binding="messaging"/>
                    <netty-acceptor name="netty-throughput" socket-binding="messaging-throughput">
                        <param key="batch-delay" value="50"/>
                        <param key="direct-deliver" value="false"/>
                    </netty-acceptor>
                    <in-vm-acceptor name="in-vm" server-id="0"/>
                </acceptors>
Hier geht es um den Empfang von Nachrichten: der "in-vm-acceptor" akzeptiert Nachrichten aus der gleichen virtuellen Maschine, die "netty-acceptor" (entfällt in WildFly8 Final wg. HTTP-Upgrade-Umstellung) und "http-acceptor" empfangen Nachrichten über das Netzwerk, und zwar horchen sie an den im "socket-binding" angegebenen Ports.

Die folgenden "security-settings" definieren Berechtigungen.
                <security-settings>
                    <security-setting match="#">
                        <permission type="send" roles="guest"/>
                        <permission type="consume" roles="guest"/>
                        <permission type="createNonDurableQueue" roles="guest"/>
                        <permission type="deleteNonDurableQueue" roles="guest"/>
                    </security-setting>
                </security-settings>
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 "match"-Attribut wird definiert, für welche Queue oder welches Topic diese Deklaration gilt. Im obigen Ausschnitt ist "#" die Wilcard für "alle".

Jetzt folgende die "connection-factories":
                <jms-connection-factories>
                    <connection-factory name="InVmConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/ConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <connection-factory name="RemoteConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="netty"/>
                        </connectors>
                        <entries>
                            <entry name="java:jboss/exported/jms/RemoteConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <connection-factory name="HTTPConnectionFactory">
                        <connectors>
                            <connector-ref connector-name="http"/>
                        </connectors>
                        <entries>
                            <entry name="java:jboss/exported/jms/HTTPConnectionFactory"/>
                        </entries>
                    </connection-factory>
                    <pooled-connection-factory name="hornetq-ra">
                        <transaction mode="xa"/>
                        <connectors>
                            <connector-ref connector-name="in-vm"/>
                        </connectors>
                        <entries>
                            <entry name="java:/JmsXA"/>
                            <!-- Global JNDI entry used to provide a default JMS Connection factory to EE application -->
                            <entry name="java:jboss/DefaultJMSConnectionFactory"/>
                        </entries>
                    </pooled-connection-factory>
Dieser Ausschnitt entstammt wiederum WildFly8 CR1, in der Final-Version verwendet die "RemoteConnectionFactory" den "http"-Connector statt des "netty"-Connector, und die "HTTPConnectionFactory" ist entfallen.

Für jeden der obigen Connectoren wird eine ConnectionFactory ins JNDI gebunden. Wichtig ist hier die "pooled-connection-factory" namens "hornetq-ra". Diese bindet sich an den In-VM-Connector und wird von WildFly 8 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 HornetQ-Konfiguration erwähnten Ports:
	<socket-binding-group name="standard-sockets" default-interface="public" port-offset="${jboss.socket.binding.port-offset:0}">
		...
		<socket-binding name="http" port="${jboss.http.port:8080}"/>
		<socket-binding name="messaging" port="5445"/>
		<socket-binding name="messaging-throughput" port="5455"/>
		..
	</socket-binding-group>
Dieser Ausschnitt entstammt wiederum WildFly8 CR1, in der Final-Version entfallen die beiden "messaging"-Bindings, da alles über den HTTP-Port läuft.

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.
Die Connectoren der Standard-Konfiguration referenzieren alle nur "socket-bindung"-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.fhw.komponentenarchitekturen.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):
package de.fhw.komponentenarchitekturen.knauf.mdb;

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

@MessageDriven(
		activationConfig = { @ActivationConfigProperty(
				propertyName = "destinationType", propertyValue = "javax.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):

Weitere Management-Optionen:
Ausgabe aller Nachrichten, die die Queue abgearbeitet hat (leider ohne Ausgabe der Textinhalte):
[standalone@localhost:9990 /] jms-queue --queue-address=MessageBeanQueue list-messages
Eine Langform des gleichen Befehls:
[standalone@localhost:9990 /] /subsystem=messaging/hornetq-server=default/jms-queue=MessageBeanQueue:list-messages

Anzahl der Nachrichten, die die Queue verarbeitet hat:
[standalone@localhost:9990 /] jms-queue --queue-address=MessageBeanQueue count-messages

Details über die Queue:
[standalone@localhost:9990 /] /subsystem=messaging/hornetq-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:
21:54:34,962 ERROR [org.jboss.as.controller.management-operation] (Thread-30) JBAS014613: Operation ("deploy") failed - address: 
    ([("deployment" => "Message.ear")]) - failure description: {"JBAS014771: Services with missing/unavailable dependencies" => [
    "jboss.naming.context.java.module.Message.MessageClient.env.jms.MBQueueRef is missing [jboss.naming.context.java.jms.queue.MessageBeanQueue]",
    "jboss.naming.context.java.module.Message.MessageClient.env.jms.MBConnectionFactory is missing [jboss.naming.context.java.ConnectionFactory]"
]}
21:54:34,962 ERROR [org.jboss.as.server] (Thread-30) JBAS015870: Deploy of deployment "Message.ear" was rolled back with the following failure message: 
{"JBAS014771: Services with missing/unavailable dependencies" => [
    "jboss.naming.context.java.module.Message.MessageClient.env.jms.MBQueueRef is missing [jboss.naming.context.java.jms.queue.MessageBeanQueue]",
    "jboss.naming.context.java.module.Message.MessageClient.env.jms.MBConnectionFactory is missing [jboss.naming.context.java.ConnectionFactory]"
]}
21:54:34,978 ERROR [org.jboss.as.appclient.logging] (Thread-37) JBAS013201: InterruptedException running app client main: java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedInterruptibly(Unknown Source) [rt.jar:1.7.0_45]
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireSharedInterruptibly(Unknown Source) [rt.jar:1.7.0_45]
	at java.util.concurrent.CountDownLatch.await(Unknown Source) [rt.jar:1.7.0_45]
	at org.jboss.as.appclient.service.ApplicationClientStartService$1.run(ApplicationClientStartService.java:108)
	at java.lang.Thread.run(Unknown Source) [rt.jar:1.7.0_45]

21:54:35,024 INFO  [org.jboss.as.connector.deployers.jdbc] (MSC service thread 1-1) JBAS010418: Stopped Driver service with driver-name = h2
21:54:35,040 INFO  [org.hibernate.validator.internal.util.Version] (MSC service thread 1-4) HV000001: Hibernate Validator 5.0.2.Final
21:54:35,118 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-2) JBAS015877: Stopped deployment null (runtime-name: MessageEJB.jar) in 139ms
21:54:35,118 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-4) JBAS015877: Stopped deployment null (runtime-name: MessageClient.jar) in 139ms
21:54:35,118 INFO  [org.jboss.as.server.deployment] (MSC service thread 1-3) JBAS015877: Stopped deployment Message.ear (runtime-name: Message.ear) in 152ms
21:54:35,149 INFO  [org.jboss.as.controller] (Thread-30) JBAS014774: Service status report
JBAS014775:    New missing/unsatisfied dependencies:
      service jboss.deployment.subunit."Message.ear"."MessageClient.jar".component.AppClientComponent.CREATE (missing) dependents: [service jboss.deployment.subunit."Message.ear"."MessageClient.jar".component.AppClientComponent.START] 
      service jboss.deployment.subunit."Message.ear"."MessageClient.jar".component.AppClientComponent.JndiBindingsService (missing) dependents: [service jboss.deployment.subunit."Message.ear"."MessageClient.jar".jndiDependencyService] 
      service jboss.deployment.subunit."Message.ear"."MessageClient.jar".component.AppClientComponent.START (missing) dependents: [service jboss.deployment.subunit."Message.ear"."MessageClient.jar".deploymentCompleteService] 
      service jboss.deployment.subunit."Message.ear"."MessageClient.jar".deploymentCompleteService (missing) dependents: [service jboss.deployment.unit."Message.ear".deploymentCompleteService] 
      service jboss.deployment.subunit."Message.ear"."MessageClient.jar".jndiDependencyService (missing) dependents: [service jboss.deployment.subunit."Message.ear"."MessageClient.jar".component.AppClientComponent.START] 
      service jboss.deployment.subunit."Message.ear"."MessageEJB.jar".deploymentCompleteService (missing) dependents: [service jboss.deployment.unit."Message.ear".deploymentCompleteService] 
      service jboss.naming.context.java.ConnectionFactory (missing) dependents: [service jboss.naming.context.java.module.Message.MessageClient.env.jms.MBConnectionFactory] 
      service jboss.naming.context.java.jms.queue.MessageBeanQueue (missing) dependents: [service jboss.naming.context.java.module.Message.MessageClient.env.jms.MBQueueRef] 
      service jboss.naming.context.java.module.Message.MessageClient.env (missing) dependents: [service jboss.deployment.subunit."Message.ear"."MessageClient.jar".jndiDependencyService] 
      service jboss.naming.context.java.module.Message.MessageClient.env.jms.MBConnectionFactory (missing) dependents: [service jboss.deployment.subunit."Message.ear"."MessageClient.jar".jndiDependencyService] 
      service jboss.naming.context.java.module.Message.MessageClient.env.jms.MBQueueRef (missing) dependents: [service jboss.deployment.subunit."Message.ear"."MessageClient.jar".jndiDependencyService] 
Also öffnen wir "%JBOSS_HOME%\appclient\configuration\appclient.xml" und fügen folgendes hinzu:
Im Element "extensions" wird folgendes eingetragen:
    <extensions>
		...
		<extension module="org.jboss.as.messaging"/>
		...
    </extensions>
Im Element "profile" wird ein neues "subsystem" zugefügt:
Variante für WildFly 8 Final (Verbindung läuft über den HTTP-Port und HTTP Upgrade)
		<subsystem xmlns="urn:jboss:domain:messaging:2.0">
			<hornetq-server>
				<connectors>
					<http-connector name="http-connector" socket-binding="http">
						<param key="http-upgrade-endpoint" value="http-acceptor"/>
					</http-connector>
				</connectors>

				<security-settings>
					<security-setting match="#">
						<permission type="createNonDurableQueue" roles="guest"/>
						<permission type="deleteNonDurableQueue" roles="guest"/>
						<permission type="consume" roles="guest"/>
						<permission type="send" roles="guest"/>
					</security-setting>
				</security-settings>

				<address-settings>
					<!--default for catch all-->
					<address-setting match="#">
						<dead-letter-address>jms.queue.DLQ</dead-letter-address>
						<expiry-address>jms.queue.ExpiryQueue</expiry-address>
						<redelivery-delay>0</redelivery-delay>
						<max-size-bytes>10485760</max-size-bytes>
						<message-counter-history-day-limit>10</message-counter-history-day-limit>
						<address-full-policy>BLOCK</address-full-policy>
					</address-setting>
				</address-settings>

				<!--JMS Stuff-->
				<jms-connection-factories>
					<connection-factory name="RemoteConnectionFactory">
						<connectors>
							<!--<connector-ref connector-name="netty"/>-->
							<connector-ref connector-name="http-connector"/>
						</connectors>
						<entries>
							<entry name="java:/ConnectionFactory"/>
						</entries>
					</connection-factory>
				</jms-connection-factories>
				<!--
				<jms-destinations>
					<jms-queue name="MessageBeanQueue">
						<entry name="queue/MessageBeanQueue"/>
					</jms-queue>
				</jms-destinations>
				-->
			</hornetq-server>
		</subsystem>
Variante für WildFly 8 CR1 - in diesem war auf Serverseite noch der Netty-Connector aktiviert)
		<subsystem xmlns="urn:jboss:domain:messaging:1.4">
			<hornetq-server>
				<connectors>
					<netty-connector name="netty" socket-binding="messaging" >
					</netty-connector>
				</connectors>

				...Rest identisch...
			</hornetq-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 "netty-connector", der das Socket Binding "messaging" verwendet (siehe nächster Schritt).
Die Elemente "security-settings" und "address-settings" habe ich aus einer Beispielconfig verwendet, ich weiß nicht, ob sie nötig sind.

Anmerkung: hier ist ein auskommentierter Block, der die Queue deklariert. Das klappt natürlich nicht, wenn wie im Beispielcode nach Variante 3 die Queue durch die Config-Datei "knaufmq-hornetq-jms.xml" als Teil des EJB-Deployment erzeugt wird!

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"
Variante für WildFly 8 Final
    <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>

Variante für WildFly 8 CR1
    <socket-binding-group name="standard-sockets" default-interface="public">
		...
		<outbound-socket-binding name="messaging">
			<remote-destination host="localhost" port="5445"/>
		</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! Siehe z.B.
https://docs.jboss.org/author/display/WFLY8/EJB+invocations+from+a+remote+server+instance


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:
22:20:02,003 ERROR [stderr] (AWT-EventQueue-0) javax.jms.JMSSecurityException: HQ119031: Unable to validate user: null
22:20:02,051 ERROR [stderr] (AWT-EventQueue-0) 	at org.hornetq.core.protocol.core.impl.ChannelImpl.sendBlocking(ChannelImpl.java:390)
22:20:02,051 ERROR [stderr] (AWT-EventQueue-0) 	at org.hornetq.core.client.impl.ClientSessionFactoryImpl.createSessionInternal(ClientSessionFactoryImpl.java:842)
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 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>javax.jms.QueueConnectionFactory</res-type>
      <res-auth>Container</res-auth>
   </resource-ref>
   <resource-ref>
      <res-ref-name>jms/MBQueueRef</res-ref-name>
      <res-type>javax.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:

Anmerkung: die JBoss-Schemadateien sind eigentlich im JBossTools-Plugin enthalten. Allerdings fehlt dort scheinbar "jboss-client_6_0.xsd" (https://issues.jboss.org/browse/JBIDE-16358)! Ansonsten wären folgende Schritte nicht nötig.

Zuerst einmal (dies muss nur einmalig geschehen!) fügen wir die XSD "jboss-client_6_0.xsd" in den XML-Katalog ein. Hierzu sollte eine Internetverbindung bestehen! Unter "Window" -> "Preferences" wird "XML" -> "XML Catalog" gewählt. Hier einen neuen Eintrag in der Kategorie "User Specified Entries" zufügen. Nebenbei: die XSD liegt auch im JBoss-VErzeichnis unter "\docs\schema\jboss-client_6_0.xsd". Aber wir referenzieren trotzdem die aus dem Internet.
Als "Location" wird die Adresse auf der JBoss-Homepage angegeben: http://www.jboss.org/j2ee/schema/jboss-client_6_0.xsd
"Key Type" muss auf "Schema Location" stehen, da es für unterschiedliche JBoss-Versionen unterschiedliche XSD-Dateien gibt, die alle den gleichen "Namespace" haben, d.h. dieser eignet sich nicht für die Unterscheidung.
"Key" ist dementsprechend http://www.jboss.org/j2ee/schema/jboss-client_6_0.xsd.
XML-Katalog (1)
Das Ergebnis sieht so aus:
XML-Katalog (2)
Jetzt können wir "jboss-client.xml" anlegen. 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 XML file from a DTD file" gewählt.
jboss-client.xml (3)
Die DTD-Datei steckt im XML-Katalog, wir wählen sie aus:
jboss-client.xml (4)
Im letzten Schritt würde ich empfehlen, den Prefix für den Default-Namespace "jboss-client" über "Edit" zu kicken (sprich im Feld "Prefix" den Wert zu löschen"). Grund: ansonsten hätten wir vor jedem Element "jboss-client:" 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. Dieser fügen wir noch das Attribut version="6.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="http://www.jboss.com/xml/ns/javaee"
	xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:xml="http://www.w3.org/XML/1998/namespace"
	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>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.


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


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 id="Application-client_ID" 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>MessageClient</display-name>
   <resource-ref>
      <res-ref-name>jms/MBConnectionFactory</res-ref-name>
      <res-type>javax.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>javax.jms.Queue</message-destination-type>
   </message-destination-ref>
   
</application-client>
jboss-client.xml:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-client xmlns="http://www.jboss.com/xml/ns/javaee"
  xmlns:javaee="http://java.sun.com/xml/ns/javaee" xmlns:xml="http://www.w3.org/XML/1998/namespace"
  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>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 id="ejb-jar_ID" 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>MessageEJB</display-name>
	<enterprise-beans>
	
		<message-driven>
			<display-name>MessageBean</display-name>
			<ejb-name>MessageBean</ejb-name>
			<ejb-class>de.fhw.komponentenarchitekturen.knauf.mdb.MessageBean</ejb-class>
			<messaging-type>javax.jms.MessageListener</messaging-type> 
			<transaction-type>Container</transaction-type>
			<message-destination-type>javax.jms.Queue</message-destination-type>
			<activation-config>
				<activation-config-property>
					<activation-config-property-name>destinationType</activation-config-property-name>
					<activation-config-property-value>javax.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.

Außerdem fügen wir eine Datei "jboss.xml" zu (obwohl dies für das Funktionieren der Anwendung nicht nötig wäre, hier nur der Vollständigkeit halber):
<?xml version="1.0" encoding="UTF-8"?>
<jboss 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_5_1.xsd"
	version="5.1">
	<enterprise-beans>
		<message-driven>
			<ejb-name>MessageBean</ejb-name>
		</message-driven>
	</enterprise-beans>
</jboss>

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 !


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-hornetq-jms.xml" oder ein Eintrag in die Server-Config ("standalone-full.xml").
Hierzu wird die Annotation javax.jms.JMSDestinationDefinition verwendet:
@MessageDriven (activationConfig=
  {
    @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
    @ActivationConfigProperty(propertyName="destination", propertyValue="java:app/jms/queue/MessageBeanQueue")
  })
@JMSDestinationDefinition(
    name = "java:app/jms/queue/MessageBeanQueue",
    interfaceName = "javax.jms.Queue",
    destinationName = "MessageBeanQueue"
  )
public class MessageBean implements MessageListener
}
Man beachte, dass die Queue hier ins private JNDI der Anwendung gebunden wird.
Da ich das bisher nicht erfolgreich testen konnte (siehe unten), ist noch nicht ganz geklärt, ob der JNDI-Name korrekt definiert ist (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/hornetq-server=default/jms-queue=MessageBeanQueue:list-messages
{
    "outcome" => "success",
    "result" => []
}

Auch auf Client-Seite ergeben sich Vereinfachungen: man kann mit dem javax.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 javax.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://jms-spec.java.net/2.0/apidocs/javax/jms/JMSContext.html: Applications running in the Java EE web and EJB containers may alternatively inject a JMSContext into their application using the @Inject annotation).

In "jboss-client.xml" habe ich die Environment Naming Context-Eintrag an den "java:app"-Bereich gebunden. Aber auch hier gilt: ich weiß nicht, ob das korrekt wäre.
	<resource-ref>
		<res-ref-name>jms/MBQueueRef</res-ref-name>
		<jndi-name>java:app/jms/queue/MessageBeanQueue</jndi-name>
	</resource-ref>

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 !

Achtung:
Dieses Beispiel funktioniert mit dem aktuellen WildFly 8.0-Server noch nicht. Siehe https://community.jboss.org/message/863696 und https://issues.jboss.org/browse/WFLY-3211

Stand 14.04.2014
Historie:
30.01.2014: Erstellt aus JBoss6-Beispiel und angepaßt an WildFly 8
24.03.2014: Angepasst an Umstellung auf HTTP Upgrade in WildFly 8 Final
14.04.2014: Kapitel "JavaEE7-Features"