Jul 022007
 

Dieser Artikel beleuchtet eine Lösung um Exceptions mittels AspectJ abzufangen und als Benutzermeldungen anzuzeigen. Das betrachtete Web-Framework ist Java Server Faces, allerdings lässt sich die Lösung auch auf andere Web-Frameworks adaptieren.

Das gängige Layering einer Server-Applikation mit Java Server Faces sieht vor, dass Backing Beans alle Präsentations-nahen Aufgaben wie Validierung, Dialog-Steuerung (Controller), usw. übernehmen. Desweiteren werden andere Komponenten umgesetzt, welche die Geschäftslogik enthalten. Diese Komponenten werden wiederum von den JSF Backing Beans aufgerufen. Durch diese Trennung wird eine abgekoppelter Test der Business Logic ermöglicht, die Wiederverwendung innerhalb einer Applikation begünstigt und Verantwortlichkeiten klar zwischen Layern (“Presentation Layer” und “Business Layer”) aufgeteilt.Unabhängig von der Validierung durch JSF-Standardvalidatoren und eigene Validator-Implementierungen (zum Beispiel eine Backing-Bean-Methode um ein Feld des Backing-Beans zu validieren) werden oft in der Geschäftslogik weitere Prüfungen durchgeführt. Zudem können bei der Ausführung der Business Methoden benutzerbedingte Fehler (Eingabe, ungültiger Ausgangszustand) auftreten. Schlägt eine Prüfung fehl bzw. tritt ein benutzerbedingter oder ein weicher Systemfehler auf, so besteht oft die Anforderung, dem Benutzer eine Fehlermeldung zusammen mit dem letzten Dialog anzuzeigen. (Weiche Systemfehler sind zum Bsp. “E-Mail kann nicht versendet werden, da der SMTP-Server nicht erreichbar ist. Versuchen Sie es später erneut.”)

Eine, wie ich finde, elegante und effiziente Lösung für die Anforderung “Fehlerdarstellung” in Java Server Faces erfolgt mittels AspectJ, eine dedizierte Exception-Klassen und ein Resource-Bundle (das ist der Anknüpfpunkt zu meinem letzen Blog-Beitrag).

Zunächst einmal die Exception-Klasse: Für mich macht der Name UserException Sinn, da es sich um eine Exception handeln soll, die in einer Meldung an den Benutzter (User) münden soll.

 1 package com.openwishes.business.exc;
 2 
 3 import java.text.MessageFormat;
 4 import java.util.Locale;
 5 import java.util.ResourceBundle;
 6 
 7 // see last blog entry on topic UTC-8 and resource bundles
 8 import com.yourpackage.ResourceBundleUtf8;
 9 
10 /**
11  * An exception which signals that the user has done something wrong.
12  * This will result in some message for the user in the GUI.
13  *
14  * @author Marc Neumann
15  */
16 public class UserException extends Exception {
17 
18   private static final long serialVersionUID = 1L;
19 
20   private static final String RES_BUNDLE_NAME = "com.yourpackage.UserExceptionMessages";
21   private String key;
22   private Object[] msgArgs;
23   private Locale locale;
24 
25   /**
26    * Creates a new user exception.
27    *
28    * @param pKey Message key for UserException resource bundle.
29    * @param pMsgArgs Message args.
30    */
31   public UserException(String pKey, Object... pMsgArgs) {
32     key = pKey;
33     msgArgs = pMsgArgs;
34   }
35 
36   /**
37    * Locale will be used for text formatting when calling getLocalizedMessage().
38    * @param pLocale
39    */
40   public void setLocale(Locale pLocale) {
41     locale = pLocale;
42   }
43 
44   /**
45    * {@inheritDoc}
46    */
47   @Override
48   public String getLocalizedMessage() {
49     ResourceBundle bundle;
50     if (locale != null) {
51       bundle = ResourceBundleUtf8.getBundle(RES_BUNDLE_NAME, locale);
52     }
53     else {
54       bundle = ResourceBundleUtf8.getBundle(RES_BUNDLE_NAME);
55     }
56 
57     return MessageFormat.format(bundle.getString(key), msgArgs);
58   }
59 
60   /**
61    * {@inheritDoc}
62    */
63   @Override
64   public String getMessage() {
65     ResourceBundle bundle = ResourceBundleUtf8.getBundle(RES_BUNDLE_NAME, locale);
66     return MessageFormat.format(bundle.getString(key), msgArgs);
67   }
68 
69 }
70 

Der Konstruktor der UserException nimmt einfach einen Schlüssel-String und optionale, variable Parameter für die Formatierung der Nachricht an den Benutzer (Java 5 sei Dank). Die Nachrichtentexte sind in Property-Dateien hinterlegt, die vom ResourceBundle ausgelesen werden.

Wird eine UserException aus einer Business-Methode herausgeworfen, so muss die UserException natürlich an der Business-Methode deklariert werden. In der Backing-Bean-Method von wo aus der Aufruf an die Business-Methode erfolgt ist, wird die UserException weitergeworfen. Die UserException wird dort einfach nicht behandelt. Der folgende Code-Auschnitt zeigt eine Beispiel einer Backing-Bean-Methode, welche mittels Ajax verwendet wird und keine JSF-Outcome Rückgabe verwendet:

// ...
public void deleteUserEmail() throws UserException {
  long userEmailId = getReqParamUserEmailId();
  new UserEmailDeletionBM().delete(userEmailId);
  removeUserEmailFromListIfPresent(userEmailId);
}
// ...

Die Methode “deleteUserEmail” wird von einem Button in der Web-GUI mittels Ajax aufgerufen und soll eine E-Mailadresse des Benutzers entfernen. Nach Ausführung der Methode werden Bereiche der GUI neu gerendert (mittels Ajax4JSF), d.h. die Ausgabe von JSF-Nachrichten als Feedback an den Benutzer kann erfolgen.

Was passiert mit der UserException, wenn diese aus dem Aufruf der Backin-Bean-Methode “deleteUserEmail” herausschlägt? Regulär würde die Fehlerseite der JSF-Implementierung aufpoppen. Durch Magie in Form von Byte Code Instrumentation mittels AspectJ kann generell für alle Backing-Bean-Methoden, die UserExceptions werfen können, erreicht werden, dass der letzte Dialog mitsamt Fehlermeldung angezeigt wird. Hier der Code für den Aspect:

 1 import java.util.Locale;
 2 
 3 import javax.faces.application.FacesMessage;
 4 import javax.faces.component.UIViewRoot;
 5 import javax.faces.context.FacesContext;
 6 
 7 import com.yourpackage.UserException;
 8 import com.yourpackage.AbstractBaseBB;
 9 import com.yourpackage.ITransaction;
10 import com.yourpackage.TransactionRegistry;
11 
12 /**
13  * Transforms user exception as a jsf message and sets transaction to rollback.
14  * Exception will not be propagated further.
15  */
16 public aspect AspectUserExceptionAsJsfMessage {
17 
18   /**
19    * Combined pointcut for all backing bean methods throwing user exceptions.
20    */
21   private pointcut execBackingBeanMethodThrowingUserExc() : execBackingBeanMethod();
22 
23   /**
24    * Match every real backing method - not getters / setters.
25    */
26   private pointcut execBackingBeanMethod() :
27     execution(public * AbstractBaseBB+.*(..) throws UserException)
28     && !execution(public * AbstractBaseBB+.get*())
29     && !execution(public * AbstractBaseBB+.is*())
30     && !execution(public * AbstractBaseBB+.set*(*));
31 
32 
33   Object around() : execBackingBeanMethodThrowingUserExc() {
34     Object result;
35 
36     try {
37       result = proceed();
38     }
39     catch (UserException e) {
40       ITransaction tx = TransactionRegistry.getTransaction();
41       tx.markForRollback();
42 
43       UIViewRoot viewRoot = FacesContext.getCurrentInstance().getViewRoot();
44       Locale locale = viewRoot.getLocale();
45       e.setLocale(locale);
46 
47       FacesContext.getCurrentInstance().addMessage(
48         null,
49         new FacesMessage(FacesMessage.SEVERITY_ERROR, e.getLocalizedMessage(), ""));
50 
51       return AbstractBaseBB.FAILURE;
52     }
53 
54     return result;
55   }
56 
57 }
58 

Der Aspect wickelt sich um die Ausführung von Backing-Bean-Methoden. Da Backing-Bean-Methoden mittels Reflection und “Method.invoke(..)” aufgerufen werden, ist das Setzen eines AspectJ-Pointcuts für Aufrufe an die Methoden nicht möglich, nur für deren Ausführung (ja, da ist ein Unterschied). Der Pointcut bindet sich an alle abgeleiten Klassen der Basisklasse “AbstractBB” von der alle Backing-Bean-Klassen ableiten. Der Pointcut vermeidet es (aus Performance-Gründen) nicht auf Getter und Setter in Backing-Beans angewendet zu werden.

Tritt eine UserException auf, so wird die UserException zunächst gefangen, die aktuelle Transaktion zurückgesetzt und eine JSF-Message erzeugt. Das Rücksetzten der Transaktion erfolgt hier gegeBn eigene Klassen – zum Thema “hausgemachtes Transaktionsmanagement mit AspectJ” werde ich demnächst einen Blog-Artikel schreiben.

Die UserException wird verschluckt und nicht weiter geworfen. Das ist wichtig – sonst wären wir wieder bei der Standard-JSF-Fehlerseite.

Wurde die Backing-Bean-Methode mittels Ajax4JSF aufgerufen, so hat dieBacking-Bean-Methode möglicherweise keinen Rückgabewert, da keine Navigation mittels JSF-Outcomes gesteuert werden soll. Hat sie jedoch einen String-Rückgabetyp, so geht der Aspect davon aus, dass der Rückgabewert “failure” als JSF-Outcome interpretiert wird und die letzte Dialogseite angezeigt wird. Denkbar wäre hier noch der Ausbau mittels Annotation um den Outcome-String bei UserExceptions festzulegen.

Abschließend betrachtet eine recht simple (wenig Code) und generische Lösung. Durch Lösungen dieser Art wird das Verschmutzen der Backing-Beans vermieden und das Cross-Cutting-Concern “Fehlerdarstellung” an zentraler Stelle (Aspect) behandelt. Ich könnte mir vorstellen, dass diese Lösung auch auf andere Web-Frameworks wie Struts, etc. anwendbar ist.

Wie immer begrüße ich Kommentare, Kritik und vor allem Verbesserungsvorschläge.

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>