Long Running Task mit JSF/RichFaces
Häufig wird die Anforderung gestellt, Aufgaben im Hintergrund zu verarbeiten. Durch den Trend der Webanwendungen trifft früher oder später diese Anforderung auch auf ein Web-Projekt zu. Erst kürzlich kam dieselbe Frage in der JBoss Community auf.
Webanwendungen sind sehr häufig nach dem Schema der einmaligen und kurzen Ein- und Ausgabe konzipiert. Das heisst: Ein Anwender stellt eine Anfrage an den Server, es folgt eine kurze Verarbeitung und der Anwender erhält seine Antwort. Würde der Anwender länger warten, so klicken über 90% der Anwender erneut bzw. laden die Seite erneut, was zu Mehrfach-Requests führt. Bei Ajax-Anwendungen ist dies eleganter gelöst, dort wird nur ein Teil der Anforderung zum Server gesendet, zudem führen die Bitte-Warten-Meldungen dazu, dass der Anwender länger als 2 oder 3 Sekunden wartet. Was aber tun, wenn die Verarbeitung mehrere Minuten oder sogar Stunden in Anspruch nimmt?
Für diesen Fall eignet sich ein Prozess-Monitor am besten. Mit einem Prozess-Monitor kann ein Prozess getrennt gestartet werden, dieser wird nach der eigentlichen Anfrage weiterhin ausgeführt. Der Prozess-Monitor zeigt nach dem Start den Zustand/Status des oder der Prozesse an.
Als Hintergrundverarbeitung können mehrere Ausführungsarten verwendet werden:
- eigene Threads (in J2EE-Umgebungen nicht empfehlenswert)
- EJB Timer
- JMS Worker
Alle diese Ausführungsarten haben eines gemeinsam: Sie laufen unabhängig vom Benutzer weiter und benötigen eine Art von Status-Synchronisation, damit der Status entsprechend an den Prozess-Monitor weitergegeben werden kann. Dies können z. B. eine Datenbank, ein In-Memory-Model oder noch viele andere Sorten von Inter-Prozess-Kommunukationsmechanismen sein.
Ich habe für diesen Fall ein Beispiel erstellt, welches genau diese Schritte mit einem Thread ausführt. Das Beispiel ist in JSF2 und RichFaces 4 gehalten. Der Code kann am Ende des Beitrags heruntergeladen werden.
Das Beispiel besteht aus einem Controller (Frontend-Controller), einer Model-Klasse zum Transport der Daten (DTO) und einer Thread-Worker-Klasse. Das Frontend selbst ist eine XHTML-Seite.
Controller.java
@ManagedBean @RequestScoped public class Controller implements Serializable { private static final long serialVersionUID = -4160088442207082094L; @ManagedProperty(value = "#{model}") private Model model; /** * Start a long running task. */ public void startLongRunningTask() { // Beware of this in real J2EE envs as Threads are a bit risky. // Use EJB Timers or JMS instead. LongRunningTask task = new LongRunningTask(model); task.start(); } /** * @return the model */ public Model getModel() { return model; } /** * @param model * the model to set */ public void setModel(Model model) { this.model = model; } }
Model.java
@ManagedBean @SessionScoped public class Model implements Serializable { private static final long serialVersionUID = -6712635521042543903L; private boolean running = false; private boolean success = false; private List<String> messages = new ArrayList<String>(); /** * @return the running */ public boolean isRunning() { return running; } /** * @param running * the running to set */ public void setRunning(boolean running) { this.running = running; } /** * @return the success */ public boolean isSuccess() { return success; } /** * @param success * the success to set */ public void setSuccess(boolean success) { this.success = success; } /** * @return the messages */ public List<String> getMessages() { synchronized (messages) { List<String> copy = new ArrayList<String>(); copy.addAll(messages); return copy; } } /** * @return the last Message. */ public String getLastMessage() { synchronized (messages) { if (messages.isEmpty()) { return ""; } return messages.get(messages.size() - 1); } } public void addMessage(String message) { synchronized (messages) { messages.add(message); } } }
LongRunningTask.java
public class LongRunningTask extends Thread { private Model model; /** * @param model */ public LongRunningTask(Model model) { this.model = model; } /** * @see java.lang.Thread#run() */ @Override public void run() { try { model.setRunning(true); Thread.sleep(2000); model.addMessage("first"); Thread.sleep(2000); model.addMessage("second"); Thread.sleep(2000); model.addMessage("third"); Thread.sleep(2000); model.addMessage("last"); model.setSuccess(true); } catch (Exception e) { model.setSuccess(false); } finally { model.setRunning(false); } }
index.xhtml
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xmlns:h="http://java.sun.com/jsf/html" xmlns:f="http://java.sun.com/jsf/core" xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:a4j="http://richfaces.org/a4j" xmlns:rich="http://richfaces.org/rich"> <f:view> <h:head> <title>My LRT Example</title> </h:head> <h:body> <h:form prependid="false"> <h:panelGroup> <h:outputText value="Start LRT" /> <a4j:commandButton value="Start LRT" action="#{controller.startLongRunningTask}" render="panel" /> </h:panelGroup> <a4j:poll interval="1000" render="panel" /> <br /> <br /> <a4j:outputPanel id="panel"> <h:panelGrid columns="2"> <h:outputText value="Running" /> <h:outputText value="#{controller.model.running}" /> <h:outputText value="Success" /> <h:outputText value="#{controller.model.success}" /> <h:outputText value="Last Message" /> <h:outputText value="#{controller.model.lastMessage}" /> </h:panelGrid> <br /> <br /> <rich:dataTable var="var" value="#{controller.model.messages}"> <rich:column> <f:facet name="header"> <h:outputText value="All messages"/> </f:facet> <h:outputText value="#{var}" /> </rich:column> </rich:dataTable> <a4j:commandButton value="Refresh" render="panel"/> </a4j:outputPanel> </h:form> </h:body> </f:view><br /> </html><
Das Beispiel-Projekt liegt als Maven2-Projekt vor und kann hier heruntergeladen werden: richfaces-long-running-task.zip (10KB) oder bei Github

