Prior to Version 1.1, the Struts framework provided very minimal exception handling for applications—it was left to you to extend the framework with your own exception-handling capabilities. This encouraged each development group to approach a solution from a different direction and made it difficult to discuss common solutions.
In Version 1.1, Struts added a small but effective exception-handling framework for your applications. The approach that the Struts designers took follows the EJB and Servlet specifications for handling security, allowing developers to use a declarative and/or a programmatic approach.
Declarative exception handling is accomplished by expressing an application's exception-handling policy, including which exceptions are thrown and how they are to be handled, in a text file (typically using XML) that is completely external to the application code. This approach makes it easier to modify the exception-handling logic without major recompilation of the code.
Programmatic exception handling is the opposite. It is the traditional method, involving writing application-specific, intra-method code to handle the exceptions, rather than simply modifying an external configuration file. However, it is quite a bit more complex within a Struts application.
As with other Struts configuration options, the declarative mappings are done in the Struts configuration file. As you saw in Chapter 4, you can specify the exceptions that may occur and what to do if they occur, both at a global level and for a specific action mapping. For a discussion of the parameters available for the exception-handling elements, refer back to Chapter 4.
Example 10-3 shows a partial Struts configuration file that declares three different exceptions that may be thrown from the login action.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts-config PUBLIC
"-//Apache Software Foundation//DTD Struts Configuration 1.1//EN"
"http://jakarta.apache.org/struts/dtds/struts-config_1_1.dtd">
<struts-config>
<action-mappings>
<action
path="/login"
type="com.oreilly.struts.storefront.security.LoginAction"
name="loginForm"
scope="request"
input="/login.jsp">
<!--The following exceptions can be thrown during the login action -->
<exception
key="security.error.changepassword"
path="/changePassword.jsp"
type="com.oreilly.struts.framework.exceptions.ExpiredPasswordException"/>
<exception
key=" security.error.loginfailed"
type="com.oreilly.struts.framework.exceptions.InvalidLoginException"
path="/login.jsp"/>
<exception
key="security.error.accountlocked"
type="com.oreilly.struts.framework.exceptions.AccountLockedException"
path="/accountLocked.jsp"/>
</action>
</action-mappings>
</struts-config>
The exception element that is defined either in the action mapping or in the global exceptions section specifies the path to which to forward when one of the specified exceptions occurs during the corresponding action invocation. For example, if an ExpiredPasswordException is thrown during the login action, the controller will forward control to the changePassword.jsp page. Likewise, if an AccountLockedException is thrown, control will be forwarded to the accountLocked.jsp page.
Whenever an exception is not programmatically handled in the Action class, the RequestProcessor gets a chance to see if there is an exception element configured for that specific exception type. If there is, control is forwarded to the resource specified in the path attribute of the exception element. Example 10-4 shows the processException( ) method from the RequestProcessor class.
protected ActionForward processException(HttpServletRequest request,
HttpServletResponse response,
Exception exception,
ActionForm form,
ActionMapping mapping)
throws IOException, ServletException {
// Is there a defined handler for this exception?
ExceptionConfig config = mapping.findException(exception.getClass( ));
if (config == null){
if (log.isDebugEnabled( )){
log.debug(getInternal().getMessage("unhandledException",exception.getClass( )));
}
if (exception instanceof IOException){
throw (IOException) exception;
}else if (exception instanceof ServletException){
throw (ServletException) exception;
}else{
throw new ServletException(exception);
}
}
// Use the configured exception handling
try {
Class handlerClass = Class.forName(config.getHandler( ));
ExceptionHandler handler = (ExceptionHandler)handlerClass.newInstance( );
return (handler.execute(exception, config, mapping, form,request, response));
}catch (Exception e){
throw new ServletException(e);
}
}
Notice that an ExceptionConfig object may be returned from the findException( ) method at the beginning of the processException( ) method. The ExceptionConfig object is an in-memory representation of the exception element specified in the configuration file. If the findException( ) method doesn't find an exception element for the specific type of exception that occurred, the exception is thrown back to the client without going through a Struts exception handler. Unless the exception is an IOException or one of its subclasses, the exception will be wrapped by a ServletException instance and rethrown.
If there is an exception element specified in the action mapping for the specific type of exception that occurs, an ExceptionConfig object is returned from the findException( ) method. The getHandler( ) method is then called on the ExceptionConfig object, and the handler retrieved is used to process the exception.
The Struts framework has a default exception-handler class that is used to process the exceptions if you don't configure one of your own. The default handler class is org.apache.struts.action.ExceptionHandler . The execute( ) method of this handler creates an ActionError, stores it into the proper scope, and returns an ActionForward object that is associated with the path attribute specified in the exception element. To summarize, if you declare an exception element inside an action element, the default exception handler will create and store an ActionError into the specified scope and give control to the resource specified in the path attribute.
As you saw back in Chapter 4, the exception element also allows you to override the exception handler's behavior if you want a different behavior when an exception occurs. You can do this by specifying a fully qualified Java class that extends the org.apache.struts.action.ExceptionHandler class in the exception's handler attribute. This class will override the execute( ) method of ExceptionHandler in order to perform the specialized behavior. For example, your application exceptions could extend the BaseException class shown in Example 10-5.
package com.oreilly.struts.framework.exceptions;
import java.util.List;
import java.util.ArrayList;
import java.io.PrintStream;
import java.io.PrintWriter;
/**
* This is the common superclass for all application exceptions. This
* class and its subclasses support the chained exception facility that allows
* a root cause Throwable to be wrapped by this class or one of its
* descendants. This class also supports multiple exceptions via the
* exceptionList field.
*/
public class BaseException extends Exception{
protected Throwable rootCause = null;
private List exceptions = new ArrayList( );
private String messageKey = null;
private Object[] messageArgs = null;
public BaseException( ){
super( );
}
public BaseException( Throwable rootCause ) {
this.rootCause = rootCause;
}
public List getExceptions( ) {
return exceptions;
}
public void addException( BaseException ex ){
exceptions.add( ex );
}
public void setMessageKey( String key ){
this.messageKey = key;
}
public String getMessageKey( ){
return messageKey;
}
public void setMessageArgs( Object[] args ){
this.messageArgs = args;
}
public Object[] getMessageArgs( ){
return messageArgs;
}
public void setRootCause(Throwable anException) {
rootCause = anException;
}
public Throwable getRootCause( ) {
return rootCause;
}
public void printStackTrace( ) {
printStackTrace(System.err);
}
public void printStackTrace(PrintStream outStream) {
printStackTrace(new PrintWriter(outStream));
}
public void printStackTrace(PrintWriter writer) {
super.printStackTrace(writer);
if ( getRootCause( ) != null ) {
getRootCause( ).printStackTrace(writer);
}
writer.flush( );
}
}
The BaseException class in Example 10-5 contains a messageKey that can be used as a key in the Struts resource bundle. This key can be passed into the constructor of the ActionError class, and the Struts framework will match it to a message in the Struts resource bundle. This class also contains an object array that the creator of the exception can populate. These objects can then be used to substitute into a message from the bundle that contains substitution parameters based on the MessageFormat class. A message in the bundle might look like this:
global.error.invalid.price=The price must be between {0} and {1}.
When creating an ActionError object, you can pass an array of objects as the second parameter, and each object will be substituted into the parameters enclosed by the braces. The 0th element in the array will be inserted into the {0} position, the object at index 1 will be inserted into the {1} position, and so on. Chapter 12 covers this topic in more detail.
Example 10-6 illustrates how to extend the default exception-handler class and provide specialized behavior for substituting the arguments from the exception into the ActionError constructor.
package com.oreilly.struts.chapter10examples;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.struts.action.ExceptionHandler;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionError;
import org.apache.struts.util.AppException;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.config.ExceptionConfig;
import com.oreilly.struts.framework.exceptions.BaseException;
public class SpecialExceptionHandler extends ExceptionHandler {
protected ActionForward execute(Exception ex,
ExceptionConfig config,
ActionMapping mapping,
ActionForm formInstance,
HttpServletRequest request,
HttpServletResponse response)
throws ServletException {
ActionForward forward = null;
ActionError error = null;
String property = null;
/* Get the path for the forward either from the exception element
* or from the input attribute.
*/
String path = null;
if (config.getPath( ) != null) {
path = config.getPath( );
}else{
path = mapping.getInput( );
}
// Construct the forward object
forward = new ActionForward(path);
/* Figure out what type of exception has been thrown. The Struts
* AppException is not being used in this example.
*/
if( ex instanceof BaseException) {
// This is the specialized behavior
BaseException baseException = (BaseException)ex;
String messageKey = baseException.getMessageKey( );
Object[] exArgs = baseException.getMessageArgs( );
if ( exArgs != null && exArgs.length > 0 ){
// If there were args provided, use them in the ActionError
error = new ActionError( messageKey, exArgs );
}else{
// Create an ActionError without any arguments
error = new ActionError( messageKey );
}
}else{
error = new ActionError(config.getKey( ));
property = error.getKey( );
}
// Store the ActionError into the proper scope
// The storeException method is defined in the parent class
storeException(request, property, error, forward, config.getScope( ));
return forward;
}
}
The specialized behavior that you perform in your handler class is up to you. The behavior shown in Example 10-6 makes the error messages more informative by inserting arguments into the ActionError.
|
There are plenty of other instances where you might need to override the default behavior. The default exception handler provided by the Struts framework doesn't support an exception object that stores multiple exceptions. If your application needs to support this behavior, you'll need to create your own ExceptionHandler class.
In most cases, the Struts exception handler is sufficient. Only when you need specialized exception handling that can't be obtained from the Struts exception handler should you bother to create your own. Figure 10-3 illustrates a sequence diagram for the Struts default exception-handling mechanism.
Using the Struts declarative exception-handling mechanism does not preclude you from also using a programmatic approach. In fact, they can work quite well together. Your Action classes will get the first opportunity to handle any specific exceptions and only if an exception is not caught and handled by the Action instance will it be caught by the processActionPerform( ) method in the RequestProcessor class. The RequestProcessor will then use the declarative exception-handling mechanism to process the error. The next section discusses how to handle exceptions using a programmatic approach.
The alternate approach to the declarative exception handling provided by Struts is to build the application-specific exception handling into the code itself. This means that you will have to extend the framework with behavior specific to your application.
As mentioned earlier in this chapter, there are two basic courses of action when an exception is thrown within an Action class. If the exception is an application exception, the course of action is to log the exception, create and store an ActionError into the appropriate scope, and forward control to the appropriate ActionForward. Recall from the discussion of declarative exception handling that this is the same behavior that the Struts default exception handler performs, minus the logging.
In the case of the Storefront application, application exceptions would all be descendants of BaseException; it's easy to detect when an application exception occurs because you can simply use a catch block for BaseException. If the exception is not an instance of BaseException, you can assume that it's a system exception and should be treated as such. The course of action for system exceptions is normally to log the exception and return an ActionForward for the system error page.
At first, you might be tempted to add try/catch blocks in your Action classes and perform the exception handling like this:
try{
// Peform some work that may cause an application or system exception
}catch( BaseException ex ){
// Log the exception
// Create and store the action error
ActionErrors errors = new ActionErrors( );
ActionError newError = new ActionError( ex.getErrorCode(), ex.getArgs( ) );
errors.add( ActionErrors.GLOBAL_ERROR, newError );
saveErrors( request, errors );
// Return an ActionForward for the Failure resource
return mapping.findForward( "Failure" )
}catch( Throwable ex ){
// Log the exception
// Create and store the action error
ActionError newError = new ActionError( "error.systemfailure" );
ActionErrors errors = new ActionErrors( );
errors.add( ActionErrors.GLOBAL_ERROR, newError );
saveErrors( request, errors );
// Return an ActionForward for the system error resource
return mapping.findForward( IConstants.SYSTEM_FAILURE_PAGE );
}
The problem with this approach is that you end up having the same redundant code inside almost every Action class. Eliminating this redundancy is one of the benefits of using the declarative approach. However, if you don't want to use the declarative approach, or if you can't because you're using an earlier version of Struts, there's an alternate approach that doesn't involve so much redundancy.
In Chapter 5, you saw how the use of an abstract base Action like the StorefrontBaseAction class can reduce the redundancy inside the Action classes for other issues. You can also push the programmatic exception-handling functionality up to the abstract class, so you don't need to have it in all of your Action classes. To do this, you will need to implement the additional executeAction( ) method. The executeAction( ) method is introduced by the StorefrontBaseAction class and is an implementation of the Template design pattern. Example 10-7 shows the execute( ) method of the StorefrontBaseAction class.
public ActionForward execute(ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response)
throws Exception {
ActionForward forwardPage = null;
try{
UserContainer userContainer = getUserContainer( request );
// Inform the specific action instance to do its thing
forwardPage = executeAction(mapping, form, request, response, userContainer);
}catch (BaseException ex){
// Log the application exception using your logging framework
// Call the generic exception handler routine
forwardPage = processExceptions( request, mapping, ex );
}catch (Throwable ex){
// Log the system exception using your logging framework
// Make the exception available to the system error page
request.setAttribute( Action.EXCEPTION_KEY, ex );
// Treat all other exceptions as system errors
forwardPage = mapping.findForward( IConstants.SYSTEM_FAILURE_KEY );
}
return forwardPage;
}
The execute( ) method invokes the executeAction( ) method, which all Action subclasses must override, and wraps the invocation with the appropriate try/catch blocks.
The StorefrontBaseAction class is abstract and provides an abstract version of the executeAction( ) method, which is shown here:
abstract public ActionForward executeAction( ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response,
UserContainer userContainer )
throws BaseException;
When any application exception occurs, as long as it extends StorefrontBaseAction, it will be caught in the try/catch block inside the execute( ) method. The subclasses don't have to worry about providing a catch block unless they plan to provide further specialized behavior for the exception.
The execute( ) method passes the exception, along with the request and mapping objects, to the processExceptions( ) method shown in Example 10-8.
protected ActionForward processExceptions( HttpServletRequest request,
ActionMapping mapping,
BaseException ex ){
ActionErrors errors = new ActionErrors( );
ActionForward forward = null;
// Get the locale for the user
Locale locale = getUserContainer( request ).getLocale( );
if (locale == null){
// If it hasn't been configured, get the default for the environment
locale = Locale.getDefault( );
}
processBaseException(errors, (FieldException) ex, locale);
// Either return to the input resource or a configured failure forward
String inputStr = mapping.getInput( );
String failureForward = mapping.findForward(IConstants.FAILURE_KEY);
if ( inputStr != null) {
forward = new ActionForward( inputStr );
}else if (failureForward != null){
forward = failureForward;
}
// See if this exception contains a list of subexceptions
List exceptions = ex.getExceptions( );
if (exceptions != null && !exceptions.isEmpty( ) ){
int size = exceptions.size( );
Iterator iter = exceptions.iterator( );
while( iter.hasNext( ) ){
// All subexceptions must be BaseExceptions
BaseException subException = (BaseException)iter.next( );
processBaseException(errors, subException, locale);
}
}
// Tell the Struts framework to save the errors into the request
saveErrors( request, errors );
// Return the ActionForward
return forward;
}
The processExceptions( ) method seems quite complex, but it's really not that bad. Here are the steps that the method performs:
1. Obtain the locale for the user.
2. Call the processBaseException( ) method to process the top-level exception.
3. If there are any subexceptions, process each one.
4. Save all of the ActionErrors that were created.
5. Return control back to either the resource identified in the input attribute of the action or a "Failure" ActionForward that has been configured for the action.
The processBaseException( ) method is where the ActionError objects are created. This method is shown in Example 10-9.
protected void processBaseException( ActionErrors errors,
BaseException ex,
Locale locale) {
// Holds the reference to the ActionError to be added
ActionError newActionError = null;
// The errorCode is the key to the resource bundle
String errorCode = ex.getMessageKey( );
/**
* If there are extra arguments to be used by the MessageFormat object,
* insert them into the argList. The arguments are context sensitive
* arguments for the exception; there may be 0 or more.
*/
Object[] args = ex.getMessageArgs( );
/**
* In an application that had to support I18N, you might want to
* format each value in the argument array based on its type and the
* user locale. For example, if there is a Date object in the array, it
* would need to be formatted for each locale.
*/
// Now construct an instance of the ActionError class
if ( args != null && args.length > 0 ){
// Use the arguments that were provided in the exception
newActionError = new ActionError( errorCode, args );
}else{
newActionError = new ActionError( errorCode );
}
errors.add( ActionErrors.GLOBAL_ERROR, newActionError );
}
The processBaseException( ) method is responsible for creating the ActionError object. It uses the messageKey field to look up a bundle message, and if any arguments are included, it includes those in the ActionError constructor as well.
As you can see, adding programmatic exception handling to your applications definitely requires more work than using the default behavior provided by the Struts framework. It also makes maintenance more difficult if you drastically change your exception hierarchy or change how you want to handle certain exceptions. However, if you are using an earlier version of Struts, this may be your only choice. You may have to extend these examples for your own applications, but they show a well-designed approach that you can build upon.
Within the EJB and Servlet specifications, programmatic security is frowned upon because it's too easy to couple your application to the physical security environment. With exception handling, it's unlikely that you'll need to change the exceptions that are thrown based on the target environment. Therefore, there isn't the same stigma associated with programmatic exception handling as there is with programmatic security. It is true, though, that if you can take advantage of declarative exception handling, your application will be easier to maintain than if you have the same functionality in your source code. An application will almost always be modified over time, and new exceptions will need to be thrown and caught. The more you can specify declaratively, the easier time you'll have maintaining it.