Beispiel: Message Driven Bean

Inhalt:

Anlegen der Message Driven Bean "Message"
MDB goes Server
Anlegen des Applicationclients
Mit Injection
Ohne Annotations

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

Diese Beispiel gilt für JBoss 6.0, der über ein anderes Messaging-Framework ("HornetQ") als der Vorgänger 5.1 verfügt!

Ein Beispiel für JBoss 5.x findet sich hier.

Aufbau des Beispieles

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


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.

Anlegen der Message Driven Bean "Message"

Über "New" -> "Other..." wählen wir "EJB" -> "Message-Driven Bean" 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. 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")
    },
    mappedName = "queue/MessageBeanQueue")
public class MessageBean implements MessageListener
{
  public MessageBean()
  {
  }

  public void onMessage(Message message)
  {
  }
}
Leider müssen wir hier eine Anpassung vornehmen: das Attribut "mappedName" muss ersetzt werden durch eine weitere Property der "actionConfig", sonst führt dies zu folgender Exception:
org.jboss.deployers.spi.DeploymentException: Required config property RequiredConfigPropertyMetaData@1aec43[name=destination descriptions=[DescriptionMetaData@7f923d[language=de]]] for messagingType 'javax.jms.MessageListener' not found in activation config [ActivationConfigProperty(destinationType=javax.jms.Queue)] ra=jboss.jca:service=RARDeployment,name='jms-ra.rar'
	at org.jboss.resource.deployment.ActivationSpecFactory.createActivationSpec(ActivationSpecFactory.java:95)
	at org.jboss.resource.deployers.RARDeployment.createActivationSpec(RARDeployment.java:313)
	at org.jboss.resource.deployers.RARDeployment.internalInvoke(RARDeployment.java:276)
	at org.jboss.system.ServiceDynamicMBeanSupport.invoke(ServiceDynamicMBeanSupport.java:156)
	at org.jboss.mx.server.RawDynamicInvoker.invoke(RawDynamicInvoker.java:164)
	at org.jboss.mx.server.MBeanServerImpl.invoke(MBeanServerImpl.java:668)
	at org.jboss.ejb3.JmxClientKernelAbstraction.invoke(JmxClientKernelAbstraction.java:58)
	at org.jboss.ejb3.mdb.inflow.JBossMessageEndpointFactory.createActivationSpec(JBossMessageEndpointFactory.java:287)
	at org.jboss.ejb3.mdb.inflow.JBossMessageEndpointFactory.start(JBossMessageEndpointFactory.java:185)
	...
Der Header sieht jetzt so aus:
@MessageDriven (activationConfig=
  {
    @ActivationConfigProperty(propertyName="destinationType", propertyValue="javax.jms.Queue"),
    @ActivationConfigProperty(propertyName="destination", propertyValue="queue/MessageBeanQueue")
  })


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

Wenn wir jetzt das Projekt auf den Server legen würden, würde das zu folgender Meldung führten (wobei das Deploy trotzdem erfolgreich durchläuft):
20:18:13,312 INFO  [org.hornetq.ra.inflow.HornetQActivation] Attempting to reconnect org.hornetq.ra.inflow.HornetQActivationSpec(ra=org.hornetq.ra.HornetQResourceAdapter@18a1063 destination=queue/MessageBeanQueue destinationType=javax.jms.Queue ack=Auto-acknowledge durable=false clientID=null user=null maxSession=15)
20:18:13,312 INFO  [org.hornetq.ra.inflow.HornetQActivation] awaiting topic/queue creation queue/MessageBeanQueue
Diese Meldung wird im Abstand von wenigen Sekunden ein paarmal kommen, und dann scheint ein Retry-Counter zu greifen und sie kommt nicht mehr. Die Anwendung wird allerdings nicht funktionieren, weil die Queue nicht verfügbar ist.
Also deklarieren wir die Queue. Hierfür gibt es drei Möglichkeiten (sowie die Möglichkeit des automatischen Erzeugens):

Quelle:
http://community.jboss.org/wiki/HowtocreateJMSqueuetopicinAS6


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.
Auf ihr 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
    {
      Properties props = new Properties();
      props.setProperty(Context.INITIAL_CONTEXT_FACTORY,"org.jnp.interfaces.NamingContextFactory");
      props.setProperty(Context.URL_PKG_PREFIXES, "org.jboss.naming:org.jnp.interfaces");
      props.setProperty(Context.PROVIDER_URL, "jnp://localhost:1099");
      props.setProperty("j2ee.clientName", "MessageClient");
      
      //JMS initialisieren. Die ConnectionFactory aus dem JNDI holen:
      InitialContext initialContext = new InitialContext(props);
      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:
      QueueConnection queueConnection = queueConnectionFactory.createQueueConnection();
      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);

      //Fertig.
      JOptionPane.showMessageDialog(this, "Nachricht wurde gesendet und sollte im Serverlog stehen !");
    }
    catch (Exception ex)
    {
      ex.printStackTrace();
    }
  }

-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 direkt zufügen, aber um den WebTools-Plugin ein bißchen besser kennen zu lernen machen wir das hier über den umständlichen Weg:

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>ConnectionFactory</jndi-name>
	</resource-ref>
	<resource-ref>
		<res-ref-name>jms/MBQueueRef</res-ref-name>
		<jndi-name>queue/MessageBeanQueue</jndi-name>
	</resource-ref>
</jboss-client>


Jetzt die Anwendung per Rechtsklick -> "Run as" -> "Run as Java Application" ausführen. Nach dem Klick auf den Button sollte die Message im Serverlog bzw. auf der Console erscheinen.


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>ConnectionFactory</jndi-name>
  </resource-ref>
  
  <message-destination-ref>
      <message-destination-ref-name>jms/MBQueueRef</message-destination-ref-name>
      <jndi-name>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!


Zum Starten des Client muss der mühsame Weg über den org.jboss.ejb3.client.ClientLauncher gegangen werden:

Run Configuration

Um Fehler bei der Injection zu erkennen, habe ich außerdem folgende "log4j.properties" im Verzeichnis "appClientModule" abgelegt (die zu ziemlich viel Konsolenausgabe beim Starten des Clients führt). Log4j findet diese Datei automatisch, da sie auf dem Classpath liegt:
log4j.rootLogger=DEBUG, stdout

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%t][%c] - <%m>%n
Anmerkung: wenn die Logausgabe über Log4j nicht funktioniert, kann man die obigen Startparameter erweitern um -Dlog4j.debug=true und sieht dadurch hoffentlich, warum Log4j z.B. die Konfiguration nicht findet.


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.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen Message-Beispiel existieren !



Stand 24.04.2011
Historie:
27.03.2011: Übernommen aus 2009er-Beispiel und angepaßt an JavaEE6, Eclipse 3.6 sowie JBoss 6.0 (neues Messaging-Framework)
24.04.2011: Da "jboss-common_6_0.xsd" jetzt im Web liegt, kann der XML-Catalog-Workaround entfallen