Where The Streets Have No Name

An Exception Handling Framework for J2EE Applications 본문

Developement/Java

An Exception Handling Framework for J2EE Applications

highheat 2007. 6. 10. 15:27
http://www.onjava.com/lpt/a/6415

In most Java projects, a large percentage of the code is boilerplatecode. Exception handling comes under this category. Even though thebusiness logic may be just three or four lines of code, exceptionhandling might go on for ten to 20 lines. This article talks about howto keep exception handling simple and straightforward, keeping thedeveloper's plate clean for him to concentrate on business logic ratherthan devoting time to writing exception-handling boilerplate code. Italso gives the basis and guidelines to create and deal with exceptionsin the J2EE environment and targets some of the business problems, inwhich exceptions could be used to resolve them. This article uses the Struts framework as the presentation implementation, though the approach is applicable to any presentation implementation.

When Do We Need Checked and Unchecked Exceptions?

Have you ever wondered why it is such a pain to put a try-catchblock around a block of code you have written, even if you know thatyou cannot do much about those exceptions and will be content with justlogging them in the catch block? You may wonder why this can't just be logged in a centralized place, which in most cases for a J2EE application is a front controller.In other words, you would like to not be bothered with them, as youdon't have much to do with them. But what if a method signaturecontains a throws clause? You are either forced to catch these exceptions or put them in a throws clause of your own method. That's a pain! Fortunately, the Java APIs have a category of exceptions called unchecked exceptions,which you are not forced to catch. Still, the question is, on whatbasis do you decide which exceptions should be checked and whichunchecked? Here are some guidelines:

  • Exceptions for which the end user cannot take any useful actionshould be made unchecked. For example, exceptions that are fatal andunrecoverable should be made unchecked. There is no point in making XMLParseException(thrown while parsing an XML file) checked, as the only action to betaken may be to fix the root cause based on the exception trace. Byextending java.lang.RuntimeException, one can create custom unchecked exceptions.
  • Exceptions associated with the user's actions in an applicationshould be made checked. Checked exceptions require the client to catchthem. You might ask why we don't make every exception unchecked. Theproblem might be that some of them may not get caught where they shouldbe. It creates a bigger problem as errors are identified at runtimeonly. Examples of checked exceptions are business validationexceptions, security exceptions, etc.

Exception Throwing Strategy

Catch a base application exception (say, BaseAppException) only and declare in your throws clause

In most of J2EE applications, decisions about what error message toshow on which screen against an exception get made in the presentationlayer only. This brings up another question: why shouldn't we put thisdecision making in a common place? In J2EE applications, a front controller is a centralized place to do common handling.

Also, there has to be a common mechanism to propagate theexceptions. Exceptions need to be handled in a generic way, too. Todeal with this, we always need to catch the base application exception BaseAppException at the controller end. This means we need to put the BaseAppException, and only that exception, in the throwsclause of each method that could throw a checked exception. The conceptis to use polymorphism to hide the actual implementation of theexception. We just catch BaseAppException in thecontroller, but the specific exception instance thrown might be any ofseveral derived exception classes. You get a lot of exception-handlingflexibility using this approach:

  • You don't need to put so many checked exceptions in the throws clause. Only one exception is required in throws clause.
  • No more clutter of catch blocks for application exceptions. If we need to deal with them, one catch block (for BaseAppException) is sufficient.
  • You don't need to do exception handling (logging and getting theerror code) yourself. That abstraction will be taken care of by ExceptionHandler, which will be discussed later.
  • Even if you introduce more exceptions into the methodimplementation at later stages, the method signature doesn't change,and hence requires no change in the client code, which otherwise wouldhave to be changed in a chain reaction. However thrown exceptions needto be specified in the Javadoc of the method so that the methodcontract is visible to the client.



Here is a sample of throwing a checked exception.

public void updateUser(UserDTO userDTO) 
throws BaseAppException{
UserDAO userDAO = new UserDAO();
UserDAO.updateUser(userDTO);
...
if(...)
throw new RegionNotActiveException(
"Selected region is not active");
}

Controller Method:
...
try{
User user = new User();
user.updateUser(userDTO);
}catch(BaseAppException ex){
//ExceptionHandler is used to handle
//all exceptions derived from BaseAppException
}
...

So far we have discussed that all methods that could potentially throw checked exceptions and are called by Controller should contain only BaseAppException in their throws clauses. However, this actually implies that we can't have any other application exception in the throws clause. But what if you need to perform some business logic based on a certain type of exception in the catchblock? For handling those cases, a method may also throw a specificexception. Keep in mind that this is a special case and should not betaken for granted by developers. The application exception in questionagain should extend the BaseAppException. Here is an example:

CustomerDAO method:
//throws CustomerNotActiveException along with
//BaseAppException
public CustomerDTO getCustomer(InputDTO inputDTO)
throws BaseAppException,
CustomerNotActiveException {
. . .
//Make a DB call to fetch the customer
//details based on inputDTO
. . .
// if not details found
throw new CustomerNotActiveException(
"Customer is not active");
}

Client method:

//catch CustomerNotActiveException
//and continues its processing
public CustomerDTO getCustomerDetails(
UserDTO userDTO)
throws BaseAppException{
...
CustomerDTO custDTO = null;
try{
//Get customer details
//from local database
customerDAO.getCustomerFromLocalDB();
}catch(CustomerNotActiveException){
...
return customerDAO
.activateCustomerDetails();
}
}

Handle unchecked exceptions at the web application level

All unchecked exceptions should be handled at the web application level. A web page can be configured in web.xml to be displayed to the end user whenever any unchecked exception occurs in the application.



Wrap third-party exceptions into application-specific exceptions

Whenever an exception originates from other external interfaces(components), it should be wrapped in an application-specific exceptionand handled accordingly.

Example:


try {
BeanUtils.copyProperties(areaDTO, areaForm);
} catch (Exception se) {
throw new CopyPropertiesException(
"Exception occurred while using
copy properties", se);
}

Here, CopyPropertiesException extends java.lang.RuntimeException, as we would just like to log it. We are catching Exception instead of catching the specific checked exceptions that the copyProperties method could throw, since for all of those exceptions we're throwing the same unchecked CopyPropertiesException.

Too Many Exceptions

You may be wondering, if we create an exception for each errormessage, could we run into in an overflow of exception classesthemselves? For instance, if "Order not found" were an error messagefor OrderNotFoundException, you certainly wouldn't like to have CustomerNotFoundExceptionfor an error message "Customer not found" for an obvious reason: thetwo exceptions represent the same thing, and differ only in thecontexts in which they are used. So if we could specify the contextwhile handling the exception, we could certainly reduce theseexceptions to just one RecordNotFoundException. Here is an example:

try{
...
}catch(BaseAppException ex){
IExceptionHandler eh =ExceptionHandlerFactory
.getInstance().create();
ExceptionDTO exDto = eh.handleException(
"employee.addEmployee", userId, ex);
}

Here, the employee.addEmployee context will be appendedto the error code of a context-sensitive exception, to make a uniqueresultant error code. For instance, if the error code for RecordNotFoundException is errorcode.recordnotfound, the resultant error code for this context will become errorcode.recordnotfound.employee.addEmployee, which will be a unique error code for this context.

However, there is a caveat: if you are using multiple interfaces in the same client method and they all could throw RecordNotFoundException,it would be really difficult to know for which entity you got thisexception. In cases where business interfaces are public and can beused by various external clients, it's recommended to use specificexceptions only and not a generic exception like RecordNotFoundException.Context-specific exceptions are really useful for DB-based recoverableexceptions where exception classes are always the same and thedifference comes only in the contexts in which they occur.

Exception Hierarchy for a J2EE Application

As discussed earlier, we need to define a base exception class, say BaseAppException, that contains the default behavior of all application exceptions. We'll just put this base class in throwsclause of every method that potentially throws a checked exception. Allchecked exceptions for an application should be the subclasses of thisbase class. There are various ways of defining error-handlingabstractions. However, these differences should have more to do withbusiness cases and not technical compulsions. The abstraction oferror-handling can be divided into the following categories. All ofthese exceptions classes will derive from BaseAppException.



Checked Exceptions

  • Business exceptions: Exceptions that occur while performing business logic. BaseBusinessException is the base class for this category.
  • DB exceptions: Exceptions thrown while interacting with persistence mechanism. BaseDBException is the base class for this category.
  • Security exceptions: The exceptions that come while performing security operations. The base class for this type of exceptions will be BaseSecurityException.
  • Confirmation exceptions: Used for getting a confirmation from the end user for executing a specific task. The base class for this category is BaseConfirmationException.

Unchecked Exceptions

  • System exception: There are times when you would like to useunchecked exceptions. For instance, you may not want to handleexceptions coming from third-party library APIs. Rather, you would liketo wrap them in unchecked exceptions and throw to the controller. Attimes, there are configuration issues, which again cannot be handled bythe client and should be raised as unchecked exceptions. All customunchecked exceptions should extend from the java.lang.RuntimeException class.

Presentation-Level Exception Handling

The presentation layer is uniquely responsible for deciding theaction to be taken for an exception. That decision-making involvesidentifying the error code based on the exception thrown. Also, we needto know to which screen we should redirect after handling the error.

We need an abstraction for getting the error code based on the typeof exception. This should also perform logging when needed. We'll callthis abstraction ExceptionHandler. It's a façade based on the "Gang of Four" (GOF) Facade pattern (which the Design Patterns book says is used to "providea unified interface to a set of interfaces in a subsystem. Facadedefines a higher-level interface that makes the subsystem easier touse.") for the whole exception handling subsystem that handles all of the exceptions derived from BaseAppException. Here is the sample exception handling in a Struts Action method.

 

try{
...
DivisionDTO storeDTO = divisionBusinessDelegate
.getDivisionByNum(fromDivisionNum);
}catch(BaseAppException ex){
IExceptionHandler eh = ExceptionHandlerFactory
.getInstance().create();
String expContext = "divisionAction.searchDivision";
ExceptionDTO exDto = eh.handleException(
expContext , userId, ex);
ActionErrors errors = new ActionErrors();
errors.add(ActionErrors.GLOBAL_ERROR
,new ActionError(
exDto.getMessageCode()));
saveErrors(request, errors);
return actionMapping.findForward(
"SearchAdjustmentPage");
}

If you take a closer look at the exception handling code we justwrote, you might realize that you are writing similar code for each andevery Struts method, which is a pain again. The objective is to removeboilerplate code as much as possible. We need to abstract it out again.

The solution is to use the Template Method design pattern (from GOF: "Itis used to implement invariant parts of an algorithm once, and to leaveit to subclasses to implement parts of the algorithm that can vary."). We need a base class that will contain the algorithm in the form of Template Method. The algorithm will contain the try-catch block for BaseAppException and a call to a dispatchMethod the method implementation (delegated to derived class) as shown below in a Struts based Action:

public abstract class BaseAppDispatchAction 
extends DispatchAction{
...
protected static ThreadLocal
expDisplayDetails = new ThreadLocal();

public ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) throws Exception{
...
try{
String actionMethod = request
.getParameter(mapping.getParameter());
finalDestination =dispatchMethod(mapping,
form, request, response,actionMethod);
}catch (BaseAppException Ex) {
ExceptionDisplayDTO expDTO =
(ExceptionDisplayDTO)expDisplayDetails
.get();
IExceptionHandler expHandler =
ExceptionHandlerFactory
.getInstance().create();
ExceptionDTO exDto = expHandler
.handleException(
expDTO.getContext(), userId, Ex);
ActionErrors errors = new ActionErrors();
errors.add(ActionErrors.GLOBAL_ERROR,
new ActionError(exDto
.getMessageCode()));
saveErrors(request, errors);
return mapping.findForward(expDTO
.getActionForwardName());
} catch(Throwable ex){
//log the throwable
//throw ex;
} finally {
expDisplayDetails.set(null);
}

In Struts, the DispatchAction::dispatchMethod method is used to forward the request to the appropriate Action method, named actionMethod.



Let's say you get searchDivision as the actionMethod from an HTTP request: dispatchMethod will dispatch the request to a searchDivision method in the derived Action class of BaseAppDispatchAction. Here, you can see that exception handling is done only in the base class, and the derived class just implements Actionmethods. It confirms to the Template Method design pattern, where theexception-handling part remains invariant while the actualimplementation (the varied part) of dispatchMethod is deferred to the derived class.

The modified Struts Action method mentioned earlier will look something like this after the above changes.

 
...
String exceptionActionForward =
"SearchAdjustmentPage";
String exceptionContext =
"divisionAction.searchDivision";

ExceptionDisplayDTO expDTO =
new ExceptionDisplayDTO(expActionForward,
exceptionContext);
expDisplayDetails.set(expDTO);
...
DivisionDTO divisionDTO =divisionBusinessDelegate
.getDivisionByNum(fromDivisionNum);
...

Wow! Now it looks clean. As exception handling is being done in one centralized place (BaseAppDispatchAction), the scope of manual errors is also minimized.

However, we need to set the exception context and the name of the ActionForward to which the requests will be forwarded if there's an exception. And we are setting this in a ThreadLocal variable, expDisplayDetails.

Hmm. Fine. But why a java.lang.ThreadLocal variable? The expDisplayDetails is a protected data member in the BaseAppDispatchActiion class, and that's why it needs to be thread-safe too. The java.lang.ThreadLocal object comes to the rescue here.

Exception Handler

We talked about an abstraction for handling exceptions in the last section. Here is the contract it should satisfy.

  • Identify the type of exception and get the corresponding error code, which could be used to display a message to the end user.
  • Log the exception. The underlying logging mechanism is hidden and could be configured based on some environmental properties.

As you might have noticed, the only exception we are catching in the presentation layer is BaseAppException. As all checked exceptions are subclasses of BaseAppException, implicitly, we are catching all of the derived classes of BaseAppException. It's fairly easy to identify the error code based on the name of the class.

//exp is an object of BaseAppException
String className = exp.getClass().getName();

Error codes can be configured in an XML file (named exceptioninfo.xml) based on the name of the exception class. Here is a sample of exception configuration.

<exception name="EmployeeConfirmationException">
<messagecode>messagecode.laborconfirmation</messagecode>
<confirmationind>true</confirmationind>
<loggingtype>nologging</loggingtype>
</exception>

As you can see, we are making it fairly explicit that for this exception, the message code to be used is messagecode.employeeconfirmation. The real message can then be extracted from a ResourceBundlefor internationalization purposes. We are also making it very clearthat we don't need to perform logging for this type of exception, asit's just a confirmation message and not an application error.

Let's look at an example of a context-sensitive exception:

<exception name="RecordNotFoundException">
<messagecode>messagecode.recordnotfound</messagecode>
<confirmationind>false</confirmationind>
<contextind>true</contextind>
<loggingtype>error</loggingtype>
</exception>

Here, contextind is true for this exception. The context you passed in handleException method can be used to make a unique error code. For instance, if we passed order.getOrderas a context, the resultant message code will be a concatenation of theexception's message code and the context passed. Hence, we get a uniquemessage code like messagecode.recordnotfound.order.getOrder.



The data coming from exceptioninfo.xml for each exception can be encapsulated into a data transfer object (DTO) named ExceptionInfoDTO.Now we also need a placeholder where we could cache these objects, aswe wouldn't want to parse the XML file again and again and createobjects each time an exception occurs. This work can be delegated to aclass named ExceptionInfoCache, which will cache all ExceptionInfoDTO objects after reading their information from exceptioninfo.xml.

What's this fuss all about, huh? The core of all this is the ExceptionHandler implementation, which will use data encapsulated in ExceptionInfoDTO for getting the message code, creating ExceptionDTO objects, and then logging it based on the type of logging specified in ExceptionInfoDTO for a given exception.

Here is the handleException method of an ExceptionHandler implementation.

public ExceptionDTO handleException(String userId,
BaseAppException exp) {
ExceptionDTO exDTO = new ExceptionDTO();
ExceptionInfoCache ecache =
ExceptionInfoCache.getInstance();
ExceptionInfo exInfo = ecache
.getExceptionInfo(
ExceptionHelper.getClassName(exp));
String loggingType = null;
if (exInfo != null) {
loggingType = exInfo.getLoggingType();
exDTO.setConfirmation(exInfo
.isConfirmation());
exDTO.setMessageCode(exInfo
.getMessageCode());
}

FileLogger logger = new FileLoggerFactory()
.create();
logger.logException(exp, loggingType);

Depending upon business requirements, there can be multiple implementations of the ExceptionHandler interface. Deciding which implementation to use can be delegated to a Factory, specifically a ExceptionHandlerFactory class.

Conclusion

Without a comprehensive exception-handing strategy, an ad hoccollection of exception-handling blocks can lead to non-standard errorhandling and non-maintainable code. Using the above approach, exceptionhandling can be streamlined in a J2EE application.

Resources

ShriKant Vashishtha currently works as a solution architect for Tata Consultancy Services Limited (TCS), India.