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; das Beispiel verwendet dennoch Threads aus Gründen der Anschaulichkeit)
  • 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. Ein Beispiel mit JMS, EJB-Timern oder asynchronen EJB's wäre weit aus komplizierter, daher bedient sich das Beispiel eines eigenen Threads und ist in JSF2/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>
</html>

Das Beispiel-Projekt liegt als Maven2-Projekt vor und kann hier heruntergeladen werden: richfaces-long-running-task.zip (10KB) oder bei Github