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:
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
File dir = (File) session.getServletContext().getAttribute("javax.servlet.context.tempdir");
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
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).
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>