Start - Publikationen - Wissen - TOGAF - Impressum -

Einleitung


Ist nebenläufige Abarbeitung über das Starten eigener Threads im Webcontainer gestattet? Die Antwort lautet: im Prinzip schon, es wird aber kaum Nutzen bringen. Grundlage für diese Einsicht ist eine Randnotiz in der Servlet-Spezifikation:

...container should support this behavior [Threads mit einem Container-Environment zu versorgen] when performed on threads created by the developer, but are not currently required to do so. Such a requirement will be added in the next version of this specification. Developers are cautioned that depending on this capability for application-created threads is not recommended, as it is non-portable.

Es ist also nicht verboten, eigene Threads zu starten. Solche Threads besitzen aber Einschränkungen:

  • eigene Threads haben keinen spezifizierten Zugriff auf die Ressourcen der Containerumgebung - schlimmstenfalls verbietet der Container die Nutzung seiner Ressourcen durch fremde Threads
  • das Request- und Responseobjekt darf nicht an eigene Threads weitergereicht werden
  • es ist nicht klar, was mit eigenen Threads passiert, wenn die destroy Methode des Servlets gerufen wird
Für Implementierung von Fachlogik in eigenen Threads nimmt man am besten Objektinstanzen, die für eine nebenläufige Verarbeitung von vorne herein vorgesehen sind. Das sind die HttpSession im Kontext eines spezifischen Nutzers und der ServletContext im Kontext des Servlets. So sind nebenläufige Threads in der Lage, Informationen für spätere Anfragen in der Session des Nutzers oder im Kontext des Servlets zu hinerlegen.

Beipiel: auf Anfrage soll die Zahl Pi auf 100 Stellen genau berechnet werden. Weil das so lange dauert, wird die Berechnung in einem eigenen Thread gestartet und ein Flag im Servlet gesetzt. Jede weitere Anfrage zeigt dann, dass die Brechnung läuft. Nach der Abarbeitung der Berechnung wird das Ergebnis im ServletContext gesetzt. Jede weitere Anfrage nutzt dann das Ergebnis.

public class AsynchPiServlet extends HttpServlet {
  //
  protected void doGet(HttpServletRequest request, HttpServletResponse response)
    throws ServletException, IOException {
    //
    final PrintWriter out = response.getWriter();
    //
    if (getPi() == null) {
      setPi("");
      out.write("Berechnung gestartet");
      // Starte nebenläufige Verarbeitung
      new Thread() {
        public void run() {
          try {
            Thread.sleep(10000);
          } catch (InterruptedException e) {
            // ignore
          }
          setPi(Double.toString(Math.PI));
        }
      }.start();
    } else if ("".equals(getPi())) {
      out.write("Berechung läuft schon");
    } else {
      out.write("Pi ist " + getPi());
    }
  }
  //
  private synchronized String getPi() {
    return getServletConfig().getServletContext().getAttribute("pi.result");
  }
  //
  private synchronized void setPi(String pi) {
    getServletConfig().getServletContext().setAttribute("pi.result", pi);
  }
}

Sind die nebenläufig erzeugten Inhalte zu groß für die Ablage in der HttpSession oder im ServletContext (also im Speicher des Servers), wird statt der Inhalte ein Handle auf diese Inhalte hinterlegt. Für die Inhalte selber kommen dann in Frage

  1. ein mit dem ServletContext assoziiertes Verzeichnis, auf das Schreibrechte besteht:
  2. File dir = (File) session.getServletContext().getAttribute("javax.servlet.context.tempdir");
    
  3. eine Datenbank oder jede andere transaktionale Ressource, diese können jedoch nicht von der Umgebung als Ressource bereitgestellt werden
Enterprise-tauglich ist das alles jedoch nicht. Wie erwähnt verweigern Webcontainer wahrscheinlich fremden Threads den Zugriff auf ihre Ressourcen und ohne Umgebung und Integration ist sinnvolle Fachlogik nicht zu realisieren.

Message Driven Beans


Für echte asynchrone Verarbeitung im Webcontainer wird eine Message Queue benötigt. Eine solche Queue kann jeder EJB Container zur Verfügung stellen. Das Servlet schickt Nachrichten (Jobs) an die Message Queue an der eine Message Driven Bean als Listener angemeldet ist. Das Servlet muss also lediglich eine Nachricht generieren und verschicken und gegebenenfalls eine Job ID an geeigneter Stelle ablegen, die eigentliche Abarbeitung der Jobs erfogt im EJB-Container. In drei Schritten ist man am Ziel, hier am GlassFish demonstriert:

1. Die JMS Ressourcen aktivieren


Dazu in der Managementconsole eine javax.jms.QueueConnectionFactory...

Resources > JMS Resources > Connection Factories > 
JNDI Name: jms/OpossumQueueConnectionFactory
Type: javax.jms.QueueConnectionFactory


...und eine javax.jms.Queue einrichten

Resources > JMS Resources > Destination Resources >
JNDI Name: jms/OpossumQueue
Resource Type: javax.jms.Queue

2. Eine MDB deployen


Diese MDB empfängt die eingehenden Nachrichten und verarbeitet sie. Dazu ist die Beanklasse und ihr Deploymentdeskriptor zu erstellen

public class DigesterBean implements MessageDrivenBean, MessageListener {
  //
  private MessageDrivenContext context = null;
  //	
  public void ejbCreate() {}
  //
  public void ejbRemove() {}
  //
  public void setMessageDrivenContext(final MessageDrivenContext context) {
    this.context = context;
  }
  //
  public void onMessage(Message inMessage) {
    TextMessage msg = null;
    try {
      if (inMessage instanceof TextMessage) {
        msg = (TextMessage) inMessage;
        System.out.println("Message received: "  + msg.getText());
      } else {
        System.out.println("Message of wrong type: " + inMessage.getClass().getName());
      }
    } catch (JMSException e) {
      e.printStackTrace();
      this.context.setRollbackOnly();
    } catch (Throwable te) {
      te.printStackTrace();
    }
  }
}
ejb-jar.xml:
:
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar version="2.1" xmlns="http://java.sun.com/xml/ns/j2ee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
  http://java.sun.com/xml/ns/j2ee/ejb-jar_2_1.xsd">
  <display-name>MDB Modul</display-name>
  <enterprise-beans>
    <message-driven>
      <display-name>AsynchDestinationBean</display-name>
      <ejb-name>AsynchDestinationBean</ejb-name>
      <ejb-class>com.mdb.DigesterBean</ejb-class>
      <transaction-type>Container</transaction-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>acknowledgeMode</activation-config-property-name>
          <activation-config-property-value>Auto-acknowledge</activation-config-property-value>
        </activation-config-property>
      </activation-config>   
    </message-driven>
  </enterprise-beans>
</ejb-jar>

Hinweis: In der activation-config kann man dabei mit der activation-config-property 'selector' gezielt regelbasiert Nachrichten selektieren. Nun muss noch die Verknüpfung der JNDI Namen der JMS Ressourcen mit den logischen Namen des Deploymentdeskriptors erfolgen. Das ist serverspezifisch und sieht für den Glassfish in der sun-ejb-jar.xml so aus

<sun-ejb-jar>
  <enterprise-beans>
    <ejb>
      <ejb-name>AsynchDestinationBean</ejb-name>
      <jndi-name>jms/OpossumQueue</jndi-name>
    </ejb>
  </enterprise-beans>
</sun-ejb-jar>

Die MDB sollte nun deploybar sein und laufen. Stehen die EJB3.0 Features zur Verfügung, wird die Implementierung noch einfacher und auf Deploymentdeskriptoren kann sogar ganz verzichtet werden:

@MessageDriven(name="AsynchDestinationBean", mappedName="jms/OpossumQueue")
public class DigesterBean3 implements MessageListener {
  //
  @Resource
  private MessageDrivenContext context;
  //
  public void onMessage(Message inMessage) {
    // Implementierung wie im Beispiel davor
  }
}

Nachteil dieses Vorgehens: die Angabe in mappedName ist ein JNDI Name und diese Bean ist nicht portabel (JavaDoc: "The mapped name is product-dependent and often installation-dependent. No use of a mapped name is portable."). Portabilität wird erreicht, indem man die Angabe für mappedName weglässt und das Mappen im Deploymentdeskriptor durchführt (siehe sun-ejb-jar.xml im letzten Abschnitt).

3. Senden von Nachrichten


Das Senden von Nachrichten an die JMS Queue des Servers erfolgt in gewohnter Weise:

:
Connection connection = connectionFactory.createConnection();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
MessageProducer messageProducer = session.createProducer(queue);
TextMessage message = session.createTextMessage();
message.setText("Hello World");
messageProducer.send(message);
messageProducer.close();
connection.close();
:

Die connectionFactory- und queue-Instanzen werden dabei einmalig initialisiert und immer wieder verwendet (ConnectionFactory und Destination unterstützten laut JMS Spec 1.1: 2.8 Multithreading concurrent access):

:
Context ctx = new InitialContext();
Object qcf = ctx.lookup("java:comp/env/jms/AsynchSupportConnectionFactory");
ConnectionFactory connectionFactory = 
  (ConnectionFactory) PortableRemoteObject.narrow(qcf, ConnectionFactory.class);
Object queueObject = ctx.lookup("java:comp/env/jms/AsynchSupportQueue");
Queue queue = (Queue) PortableRemoteObject.narrow(queueObject, Queue.class);
:

Damit dem Webcontainer die beiden Ressourcen zur Verfügung stehen, müssen sie in der web.xml deklariert sein...

<web-app>
  :
  <resource-ref>
    <description>MQConnection Support for Asynch Services</description>
    <res-ref-name>jms/AsynchSupportConnectionFactory</res-ref-name>
    <res-type>javax.jms.QueueConnectionFactory</res-type>
    <res-auth>Container</res-auth>
    <res-sharing-scope>Shareable</res-sharing-scope>
  </resource-ref>
  :
  <resource-env-ref>
    <description>Queue for Asynch Services</description>
    <resource-env-ref-name>jms/AsynchSupportQueue</resource-env-ref-name>
    <resource-env-ref-type>javax.jms.Queue</resource-env-ref-type>
  </resource-env-ref>
  :
</web-app>

... und mit den JNDI Namen der JMS Ressourcen verknüpft werden. Das erfolgt wie immer serverspezifisch und sieht für den GlassFish in der sun-web.xml so aus:

<sun-web-app>
  :
  <resource-ref>
    <res-ref-name>jms/AsynchSupportConnectionFactory</res-ref-name>
    <jndi-name>jms/OpossumQueueConnectionFactory</jndi-name>
  </resource-ref>
  :
  <resource-env-ref>
    <resource-env-ref-name>jms/AsynchSupportQueue</resource-env-ref-name>
    <jndi-name>jms/OpossumQueue</jndi-name>
  </resource-env-ref>
  :
</sun-web-app>
copyright © 2003-2021 | Dr. Christian Dürr | prozesse-und-systeme.de | all rights reserved