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:
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:
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.
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):
- Variante 1: Global
Bei gestopptem Server in der Datei "%WILDFLY_HOME%\standalone\configuration\standalone-full.xml" im Element "hornetq-server"
folgendes nach den "jms-connection-factories" einfügen:
<jms-destinations>
<jms-queue name="MessageBeanQueue">
<entry name="queue/MessageBeanQueue"/>
</jms-queue>
</jms-destinations>
Jetzt können wir den Server starten und unsere Bean publizieren.
Wir starten die Management-Console (http://localhost:9990/console/App.html#naming).
In der JNDIView finden wir die MBean für unsere Queue wieder:
- Variante 2: Zur Laufzeit
Mit folgendem Befehl kann man in der CLI eine Queue hinzufügen:
[standalone@localhost:9990 /] jms-queue add --queue-address=MessageBeanQueue --entries=queue/MessageBeanQueue
Der JNDI-Name wird hier unter "--entries" angegeben, der Queue-Name bei "--queue-address".
Anmerkung: der oben genannten Doku nach scheint der Befehl bei JBoss 7 noch so ausgesehen zu haben: jms-queue add --name=MessageBeanQueue --entries=queue/MessageBeanQueue
Wollen wir die Queue wieder loswerden, können wir die Operation "destroyQueue" aufrufen.
[standalone@localhost:9990 /] jms-queue remove --queue-address=MessageBeanQueue
- Variante 3: Im EJB-Projekt
Jetzt kommt die für Entwickler eleganteste Variante, die allerdings nicht für den Produktiveinsatz gedacht ist:
wir fügen die Queue durch eine Konfigurations-Datei direkt im EJB-Projekt zu.
Die Config-Datei kommt in das Verzeichnis "MessageEJB\ejbModule\META-INF".
Der Name muss mit "-jms.xml" enden, der Teil davor ist egal, im Beispiel heißt sie "knaufmq-hornetq-jms.xml":
<?xml version="1.0" encoding="UTF-8"?>
<messaging-deployment xmlns="urn:jboss:messaging-deployment:1.0">
<hornetq-server>
<jms-destinations>
<jms-queue name="MessageBeanQueue">
<entry name="jms/queue/MessageBeanQueue"/>
</jms-queue>
</jms-destinations>
</hornetq-server>
</messaging-deployment>
Eine "schemaLocation" können wir hier nicht angeben, da die Datei wohl nicht im Web verfügbar ist. Sie steckt im WildFly-Verzeichnis unter
"%JBOSS_HOME%\docs\schema", und der JBossTools-Plugin fügt automatisch einen Eintrag unter "urn:jboss:messaging-deployment:1.0" in den
XML Catalog ein, so dass wir in Eclipse eine Validierung haben.
- Variante 4: per
@javax.jms.JMSDestinationDefinition
Siehe Abschnitt JavaEE7-Features
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:
- Er ist ein "b) Application User"
- Username ist hier (in meinem Client hartcodiert!) "tester", Passwort "test123!"
- Gruppe muss "guest" sein (da das in "appclient.xml" und "standalone-full.xml" so in den "security-settings" deklariert ist)
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
.
Das Ergebnis sieht so aus:
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.
Im ersten Schritt wird der Dateiname "jboss-client.xml" angegeben und des META-INF-Verzeichnis als
Ziel gewählt.
Im nächsten Schritt wird die Option "Create XML file from a DTD file" gewählt.
Die DTD-Datei steckt im XML-Katalog, wir wählen sie aus:
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:
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:
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 !
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"