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*