Hello!

I just saw the declarative exception handling approach that is implemented by deltaspike and I wanted to share/discuss some ideas and concepts I have implemented.

Before you look through the code I would like to explain the basic concept.
After some time on my new EE6 project I realized that there are a lot catch-blocks in my JSF backing bean methods that simply populate a localized message via FacesMessage to the FacesContext and to be honest, the code really smelled! Soon I played around with an interceptor that should catch these exceptions and populates the messages on it's own. I just wanted to specify the mapping between exception and a type safe keys for internationalized messages. Fortunatelly I used cal10n, the library offers a type safe approach for internationalization, so I decided to map the exceptions to the enum keys. I wanted to separate the keys for the messages modulewise, but unfortunatelly the annotation model does not support constructs like,

Enum<A extends Enum<A>>

for an annotation value.

Therefore I had to write a base interceptor that does the smelling exception handling and mapping to the i18n keys and extend it for every new module that specified another enum type for the keys. This sucks, especially because I am using naming schemes to get values via reflection of the annotation. I still have no clue how I could implement that stuff, so I can still use the i18n keys in a typesafe manner, but also don't have to copy some classes when creating new modules with new message enums. Any suggestions on that?

With the described solution, I had no more, or at least only few, catch blocks in my methods, but like some annotations on my class that are actually necessary to activate the interceptor. First I mapped the messages directly in the exception handling annotations but after some time, I added a default message annotation for exceptions and now the messages are mostly declared on the exceptions. Today I thought of using my base exception class as marker for exceptions to be handeled automatically, the whole code would be reduced to one annotation(for the interceptor), annotated at the class level. Every exception that extends the marker exception would be handeled automatically and the default messages are used for the population of the FacesMessages, but I didn't implement that yet.

In the end I also added cleanup functionallity to the handlings. Methods annotated with a cleanup annotation and having the same name as declared in an exception handler or handling annotation are invoked when an exception occurs. Not very typesafe but better than adding more and more annotations for type safety.

As an example some code fragments on how to use that approach,

// This is the marker exception, subclasses of it will be handled automatically public class ApplicationException extends Exception{/*constructors from base class*/}

@DefaultMessage(UserMessage.USER_NOT_FOUND)
public class UserNotFoundException exteds ApplicationException{/*constructors from base class*/}

@ExceptionHandler(@ExceptionHandling(exception=UserNotFoundException.class))
public class LoginBean{

public void login() throws UserNotFoundException{ // preferre ApplicationException so you don't get a big throws clause /*do login action that maybe throws UserNotFoundException, without having to catch it :)*/
    }
}

What do you think about that approach?

Basically I use interceptors to populate localized messages when an exception occurs. The base annotation looks like this:

@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.METHOD,
        ElementType.TYPE })
public @interface ExceptionHandler {
    @Nonbinding
    ExceptionHandling[] value() default {};

    @Nonbinding
    String cleanupName() default "";
}

This is the container for multiple exception handlings like this:

@Retention(RetentionPolicy.RUNTIME)
@Target({
        ElementType.METHOD,
        ElementType.TYPE })
public @interface ExceptionHandling {
Class<? extends Throwable> exception() default java.lang.Exception.class;

    Message message() default i18n.Message.DEFAULT_MESSAGE;

    String cleanupName() default "";
}

The interceptor that does the handling:

/**
 *
* @description This abstract ExceptionHandlerInterceptor is the base for every
 *              type safe exception handler.<br>
* The ExceptionHandler Annotation may contain a name of a cleanup method. * This cleanup method will be invoked for every defined exception
 *              handling.
* When the ExceptionHandling Annotation defines a cleanupName, then only
 *              this cleanup will be invoked.
* Be aware that if the ExceptionHandling method defines a cleanup method * that cannot be found, but the ExceptionHandler already defined a cleanup * the cleanup handler of the ExceptionHandler will be invoked.
 *
* The ExceptionMessage annotation can be used on an exception type to define a default message. * When a thrown exception has no default message and the exception handling annotation does * not define a default message, the default message of the exception handler will be used.
 *              @param T the concrete exception handler annotation type
* @param V the annotation type for default messages annotated on exceptions
 *              @param M the cal10n annotated enum type for the i18n keys
 */
public abstract class AbstractExceptionHandlerInterceptor<T extends Annotation, V extends Annotation, M extends Enum<M>> {

    private Class<T> clazz;
    private M defaultMessage;
    private Class<V> defaultMessageAnnotation;
    private ICCLoggable log = null;

    public AbstractExceptionHandlerInterceptor() {
        super();
    }

public AbstractExceptionHandlerInterceptor(Class<T> clazz, M defaultMessage, Class<V> defaultMessageAnnotation) {
        this.clazz = clazz;
        this.defaultMessage = defaultMessage;
        this.defaultMessageAnnotation = defaultMessageAnnotation;
    }

    /**
* Handles the error. It will be invoked by the concrete implementation.
     *
     * @param InvocationContext
     *            ic the invocation context of the intercepted method
     */
    public Object handleError(InvocationContext ic) throws Exception {
        log = LoggerInitializer.getLogger(ic.getTarget().getClass());
        Object targetObj = ic.getTarget();
        Object ret = null;

// Avoid ExceptionHandling for @Cleanup annotated methods because this could
        // result in a endless loop
        if (ic.getMethod().isAnnotationPresent(Cleanup.class))
            return ic.proceed();

        try {
            ret = ic.proceed();
        } catch (Throwable t) {

// Unwrap Exception if t is instanceof InvocationTargetException
            if (t instanceof InvocationTargetException)
t = ExceptionUtil.unwrapInvocationTargetException((InvocationTargetException) t);

            // Method level exception handling is preferred
if (!handleException(ic.getMethod().getAnnotation(clazz), ic, targetObj, t)) { // Class level exception handling if not handled by method level if (!handleException(AnnotationUtil.findAnnotation(targetObj.getClass(), clazz), ic, targetObj, t)) {
                    if (t instanceof Exception)
                        throw (Exception) t;
                    throw new Exception(t);
                }
            }
        }
        return ret;
    }

    /**
     * Handles the exception.
     *
     * @param handler
     *            the handler, class or method level
     * @param ic
     *            the InvocationContext
     * @param t
     *            the thrown throwable
     * @throws Exception
     *             if an error occurs the handling
     */
    @SuppressWarnings("unchecked")
private boolean handleException(T handler, InvocationContext ic, Object targetObj, Throwable t) throws Exception {
        Object[] handlings = null;

        if (handler != null) {

handlings = (Object[]) clazz.getMethod("value").invoke(handler);

// Unwrap Exception if t is instanceof InvocationTargetException
            if (t instanceof InvocationTargetException)
t = ExceptionUtil.unwrapInvocationTargetException((InvocationTargetException) t);

            for (Object exHandle : handlings) {
if (t.getClass().equals((Class<? extends Exception>) exHandle.getClass().getMethod("exception").invoke(exHandle))) { logError(log, ic.getMethod().getDeclaringClass().getSimpleName(), ic.getMethod().getName(), t); Enum<M> message = (Enum<M>) exHandle.getClass().getMethod("message").invoke(exHandle);

                    if (message == defaultMessage) {
Annotation messageAnnotaion = t.getClass().getAnnotation(defaultMessageAnnotation);
                        if (messageAnnotaion != null) {
message = (Enum<M>) messageAnnotaion.getClass().getMethod("value").invoke(messageAnnotaion);
                        }
                    }

boolean invokedByException = invokeCleanups(ic.getMethod().getDeclaringClass(), targetObj, (String) exHandle.getClass().getMethod("cleanupName").invoke(exHandle));

                    if (!invokedByException)
invokeCleanups(ic.getMethod().getDeclaringClass(), targetObj, (String) handler.getClass().getMethod("cleanupName").invoke(handler));

                    handleException(t, message);
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * Invokes the cleanup methods of the exception handler or exception
     * handling.
     *
     * @param clazz
     *            the class of the bean to get the methods from
     * @param target
     *            the target on which the method is invoked
     * @param cleanupName
     *            the name of the cleanup method
     * @return true if the cleanup method were found and invoked
     * @throws Exception
     *             if an reflection specific exception occurs
     */
private boolean invokeCleanups(Class<?> clazz, Object target, String cleanupName) throws Exception {
        boolean invoked = false;

        if (!cleanupName.isEmpty()) {
            for (Method m : clazz.getMethods()) {
                Cleanup cleanup = m.getAnnotation(Cleanup.class);

if (cleanup != null && cleanup.value().equals(cleanupName)) {
                    m.invoke(target);
                    invoked = true;
                }
            }
        }
        return invoked;
    }

    /**
     * Logs the handled exception.
     *
     * @param log
     *            the Logger of the concrete implementation
     * @param className
     *            the bean classname where the exception occurred
     * @param methodName
     *            the method in which the exception was thrown
     * @param e
     *            the throw Throwable
     */
protected void logError(Logger log, String className, String methodName, Throwable e) { StringBuilder sb = new StringBuilder().append(e.getClass().toString()).append(" in ").append(className).append(".").append(methodName).append("(): ").append((e.getMessage() != null) ? e.getMessage() : "no message in Excepton found !!!");

        if (e.getClass().isAnnotationPresent(Error.class))
            log.error(sb.toString(), new Exception(e));
        else
            log.warn(sb.toString());
    }

    /**
     * Handles the message in case of an thrown exception.
     *
     * @param ex
     *            the thrown Throwable
     * @param message
     *            the message corresponding to the Throwable
     */
    protected abstract void handleException(Throwable ex, Enum<M> message);
}

--
Mit freundlichen Grüßen,
------------------------------------------------------------------------
*Christian Beikov*

Reply via email to