<exception-handler> in SWF1.0.5 drops stack trace of original exception???
I am using SWF 1.0.5 on a large critical project that is near completion and upgrading to 2.0 is, unfortunately, not feasible (I wish it were backwards-compatible.) I have implemented a very well-working error-handling strategy (almost!) using SWF's <exception-handler>. Each of my flows has the following definition:
Code:
<exception-handler bean="flowErrorHandler"/>
The "flowErrorHandler" bean is defined as follows:
Code:
<bean id="flowErrorHandler" class="com.xyz.myapp.web.webflow.FlowExceptionHandler">
<property name="targetStates">
<map>
<entry key="com.xyz.myapp.web.BusinessException1" value="targetState1"/>
<entry key="com.xyz.myapp.web.BackNavigationException" value="expiredMsgState" />
<entry key="java.lang.Throwable" value="systemerror"/>
</map>
</property>
</bean>
My FlowExceptionHandler extends TransitionExecutingStateExceptionHandler adds the ability to inject the target states via a setter and some custom processing:
Code:
import com.xyz.common.config.InvalidConfigurationException;
import org.apache.log4j.Logger;
import org.springframework.webflow.engine.RequestControlContext;
import org.springframework.webflow.engine.support.TransitionExecutingStateExceptionHandler;
import org.springframework.webflow.execution.FlowExecutionException;
import org.springframework.webflow.execution.ViewSelection;
import java.util.Map;
import java.text.MessageFormat;
public class FlowExceptionHandler extends TransitionExecutingStateExceptionHandler {
private static final String MSG_ERR_EXCEPTION_CLASS =
"Error configuring FlowExceptionHandler: ''{0}'' is not a recognized exception class.";
private static final String MSG_REGISTERING_HANDLER =
"Registered error handler for ''{0}'': on error will forward to stateId=''{1}''.";
private static final String MSG_ERROR_INFO =
"{0} was caught and handled by global flow error handler in flow ''{1}'', state ''{2}''. Proceeding to designated state ''{3}''.";
protected static final Logger logger = Logger.getLogger(FlowExceptionHandler.class);
/**
* Handles the incoming flow exception and redirects the flow to the designated state associated with this exception
* type. The exception-type-to-state mappings must be loaded via the {@link #setTargetStates} method.
*
* @param e the FlowExecutionException object generated by the WF engine that wraps the original exception
* @param context context of the current client request
* @return signaled event to proceed to the designated view that is determined by the configuration of this handler
*/
@Override
public ViewSelection handle(FlowExecutionException e, RequestControlContext context) {
logger.error("Some message", e); // want to log the complete stack trace!!!
// do custom processing here...
return super.handle(e, context);
}
/**
* Checks whether this handler should handle the incoming exception. Overrides the default behavior on the SWF
* (v1.0.5) framework's class to avoid the infinite loop in the case of the {@link #handle} method
* throwing an IllegalArgumentException due to a handler being incorrectly configured to forward to a non-existing
* state as well as to handle any Throwable or Exception.
*
* @param e the FlowExecutionException object generated by the WF engine that wraps the original exception
* @return boolean true or false.
*/
@Override
public boolean handles(FlowExecutionException e) {
return e.getCause() != null && !(e.getCause() instanceof IllegalArgumentException) && super.handles(e);
}
/**
* Sets the mappings between the exception types (class names) and target flow states to which control must be
* directed after the exception is handled. This <tt>set</tt> method is introduced to allow <i>setter-based
* injection</i> of the mappings into this handler, and internally uses the {@link #add} method on the superclass to
* insert the mappings.
*
* @param states map of key/value pairs where each key is an exception class name and each value is a flow state ID
* @throws InvalidConfigurationException if invalid exception to state configuration is being loaded, e.g. one of
* the specified exception classes is not recognized, etc.
*/
public void setTargetStates(Map<String, String> states) throws InvalidConfigurationException {
for (String className : states.keySet()) {
try {
String stateId = states.get(className);
add(Class.forName(className), stateId);
logger.debug(MessageFormat.format(MSG_REGISTERING_HANDLER, className, stateId));
}
catch (ClassNotFoundException ex) {
throw new InvalidConfigurationException(MessageFormat.format(MSG_ERR_EXCEPTION_CLASS, className), ex);
}
}
}
}
All my exceptions within the application are runtime exceptions, of course, which allows them to freely propagate to the designated handlers without being altered or mishandled. Everything works beautifully, except for one thing: the FlowExecutionException object created by the webflow engine does not have the stack trace of the cause, i.e the actual application exception. Was this done on purpose? I doubt that such a thing (chopping off the stack trace) could be deliberately done by such expert programmers as the WF authors. No good framework developer will ever assume that they could replace - not complement! - the original complete history of an application-specific error with their own generic one-liner? I see bad programmers do that every day, but not the Spring guys! What am I missing? Must be something obvious.
If I disable the <exception-handler> thing and let the exceptions propagate to my global Spring MVC exception resolver, everything is fine, the stack trace is not lost. But using <exception-handler> gives me FlowExecutionException that just doesn't contain the complete error information, which is unbelievable, and unacceptable to me. If it's a bug, I am sure it is fixed in 2.0, but is there a way to make it work in 1.0.5? I would really love to make it work b/c it is such a slick way to implement a one-stop error-handling solution.
Thanks for any advice. Please help!!!
Spoke too soon. Stack trace disappears again!
Now I am totally confused! For a moment there I thought it was working... I am setting a breakpoint in the handles() method on my class that extends TransitionExecutingStateExceptionHandler (see the code above.) The FlowExecutionException object that is passed to the method contains a non-null cause, including the nested causes, but... all stack traces (for each and every cause exception object) are null!
If "e" is the FlowExecutionException object passed to the handles() method, Calling e.getCause() and this.findRootCause(e), as expected, returns the same non-null cause object. However, this.findRootCause(e).getStackTrace() returns null! Does anyone have any idea what may be causing it? Note that all the proper nested causes themselves are not null. What could possibly be altering the original exception. The original exception is always a runtime exception thrown by one of my services that basically wraps any underlying exception within the service method, adding a clarifying message, or any uncaught 3rd-party RTE. For example:
Code:
public void sendEmail(...)
try {
// sends confirmation email
} catch (Throwable t) {
// all we need to know is that this operation failed: wrap into RTE,
// add msg and propagate to designated handler configured to handle this error specifically
// never worry about this exception anywhere else
throw new SendEmailException("some msg", t);
}
The SendEmailException extends RuntimeException and its constructor looks like this:
Code:
public SendEmailException (String msg, Throwable cause) {
super(msg, cause); // note: we are not discarding the cause!
}
As soon as this exception is thrown, it gets correctly redirected by the Webflow engine to my error handler as I described in the original posting. In the case of this example, the configuration map entry in the exception handler definition would look like this:
Code:
<entry key="com.xyz.myapp.web.SendEmailException" value="myNextSuccessState"/>
The handler would process the error (log it, save some info in the database for later re-sending of the email), and direct the flow to the next state as if nothing bad happened (since in this particular case, we don't want to interrupt the order process and scare off the user.) The webflow engine packages my exception inside a FlowExecutionException object and passes it to my implementation of TransitionExecutingStateExceptionHandler. Everything works as expected, except for one thing. As that FlowExecutionException exception arrives in the handles() method that checks whether it should be handled by this class, the exception is missing the stack trace and stack traces for all nested causes.
Can anyone from the SWF team please shed some light on this? Am I doing something wrong somewhere?
Many thanks in advance!
Constantine