Index
- Introduction
- The Problems
- Consolidating Duplicate Exception Handling in Aspect
- Refactoring to Better Exception Handling
- Conclusion
- References
The concept of using AOP for exception handling has been around for a while. The two papers listed in the Reference section list advantages of this approach such as reducing lines of code for exception handling by a factor of 4, better support for different configurations of exceptional behaviors, etc. Most articles on this topic are based on AspectJ for Java. This article tries to give you a concrete example using C# and Spring.Net AOP.
Assume we are writing a data access object(DAO) OrderDAO
, as shown below. Imaging the Logger
is a Log4Net's ILog
object. When a DbException
occurs, we want to log some debugging information. In this trivial case, we log the ToString
of the argument(s).
public class OrderDAO: IOrderDAO
{
...
public void UpdateOrder(Order order){
try{
Foo();
}catch(DbException e){
Logger.Debug(""+order);
throw;
}
}
public void GetOrder(int OrderID)
{
try{
Foo2();
}catch(DbException e){
Logger.Debug(""+OrderID);
throw;
}
}
private void Foo()
{
}
private void Foo2()
{
}
}
With today's IDEs, we can churn out exception handling code like this in no time. We feel good at the end of the day because we have handled lots of exceptions.
But wait. There are several issues with this code. The code in all catch
blocks is similar. It is the incarnation of the concern "Log debugging information and rethrow the exception". Duplication is rarely good. In this traditional approach, the ToString
may not return all information about the argument objects. It is hard to log all information if the object is huge. If we add more properties to the Order
class, it is likely that we forget to update the catch
block(s). Then we will not have the new properties for debugging. Is there a way to consolidate the duplicate exception handling code?
Let's look at how AOP removes duplication and provides us with more thorough debugging information.
IThrowsAdvice
In Spring.Net AOP module, an IThrowsAdvice
's AfterThrowing
is invoked after an intercepted method throws an exception. The AfterThrowing
method gives us access to the target
, which throws the exception, and method arguments. Our advices will implement this interface. The thrown exception keeps propagating to upper call stack after AfterThrowing
.
LogArgumentsThrowsAdvice
What this advice will do is iterate through the arguments and call the ILog.Debug(object)
. It is too simple. The code speaks better for itself.
class LogArgumentsThrowsAdvice : IThrowsAdvice
{
private ILog Logger;
public LogArgumentsThrowsAdvice()
{
log4net.Config.XmlConfigurator.Configure();
Logger = LogManager.GetLogger(this.GetType().Name);
}
public void AfterThrowing(MethodInfo method, Object[] args,
Object target, Exception exception)
{
if (!(exception is DbException))
return;
if (args != null && args.Length > 0)
{
foreach (Object arg in args)
{
Logger.Debug(""+arg);
}
}
}
}
Binding Aspect and Target
As usual, we use an aspect, ExceptionHandlingAspect
to organize the advice and pointcut. Note that Spring.Net doesn't have the notion of Aspect
class. This is kind of a personal way to organize aspect, advices and pointcuts. ExceptionHandlingAspect
has an instance variable of LogArgumentsThrowsAdvice
type. The following class diagram shows the relationship.
The GetProxy
method is where interception is done. The argument methodRE
is a regular expression and used to create SdkRegularExpressionMethodPointcut
, which Spring.Net uses to select the methods in the target
to intercept. The pointcut and advice are grouped into an advisor of type DefaultPointcutAdvisor
. Note that an advisor expresses a module of an aspect. The advisor is added to the ProxyFactory
. A proxy object is obtained by calling the ProxyFactory
's GetProxy()
method.
private LogArgumentsThrowsAdvice logArgumentsThrowsAdvice;
public static object GetProxy(object target, string methodRE)
{
SdkRegularExpressionMethodPointcut reMethodPointcut =
new SdkRegularExpressionMethodPointcut(methodRE);
ProxyFactory proxyFactory = new ProxyFactory(target);
DefaultPointcutAdvisor exceptionHandlingAdvisor =
new DefaultPointcutAdvisor(reMethodPointcut, logArgumentsThrowsAdvice);
proxyFactory.AddAdvisor(exceptionHandlingAdvisor);
return proxyFactory.GetProxy();
}
If you are familiar with Spring.Net configuration file, you can abstract the above logic to the configuration file and let the Spring.Net IOC container create the proxy for you. This way, you can configure how exceptions are handled at deployment time.
Aspect in Action
The following diagram shows the aspect in action. Note the stereotype advisor
. It intercepts all calls(step 1.1) to the adviced object, e.g. IOrderDAO
. If the method call meets the specification of pointcut inside the advisor, it also redirects the call to its advice(step 1.2).
The New OrderDAO
Now the exception handling code is consolidated in on advice and OrderDAO
is cleaner.
public class OrderDAO: IOrderDAO
{
...
public void UpdateOrder(Order o)
{
Foo();
}
public void GetOrder(int OrderID)
{
Foo2();
}
}
Now we are in a position to easily improve exception handling in our application. Let's start by looking at how we can improve the support for debugging.
What ToString
returns usually doesn't have enough information for debugging. Most often than not, the important debugging information is among what doesn't get logged. OK, what if we persist the whole argument objects, instead? Since we consolidated the exception handling code in one advice, what we need to do is to create one new advice and apply the new advice. Cool, eh?
Introducing SerializeArgumentsThrowsAdvice
We all have special feeling for code. Let's get down to the code listing.
class SerializeArgumentsThrowsAdvice : IThrowsAdvice
{
private BinaryFormatter binaryFormatter = new BinaryFormatter();
public void AfterThrowing(MethodInfo method, Object[] args,
Object target, Exception exception)
{
if(!(exception is DbException))
return;
if (args != null && args.Length > 0)
{
foreach (Object arg in args)
{
Serialize(arg);
}
}
}
protected virtual void Serialize(object obj)
{
if (obj == null)
return;
FileStream fileStream = null;
try
{
String fileName = "" + obj.GetType() + "_" + obj.GetHashCode() + ".dat";
fileStream = File.Create(fileName);
binaryFormatter.Serialize(fileStream, obj);
}
catch (Exception e)
{
System.Console.WriteLine(e.StackTrace);
}
finally
{
if (fileStream != null)
{
fileStream.Close();
}
}
}
}
It is very similar to LogArgumentsThrowsAdvice
except that we call Serialize
instead. Assuming arguments are marked as Serializable, Serialize(object)
uses the BinaryFormatter
in the namespace System.Runtime.Serialization.Formatters.Binary
to persist the object onto file system.
To apply this new advice, modify the ExceptionHandlingAspect
's GetProxy
in the example project. Replace the logArgumentsException
with serializeArgumentsThrowsAdvice
. Or you can rewrite the GetProxy
to make it configurable from Spring.Net configuration file.
The serialized Order objects shown in the following figure were created by the SerializeArgumentsThrowsAdvice
object, which intercepts Foo.UpdateOrder(Order o)
method. Now it is a matter of deserializing and bringing them back to live .NET CLR so we can investigate the internal of the Order
arguments to debug the problem.
Serializing Arguments
You may argue that what if the arguments can't be serialized. As the advice's name suggests, it serializes arguments. You may want to develop and use appropriate advice as per your needs. For example, if the properties of the arguments and the target
meet your needs, you can use .NET reflection to persist the properties or use an alternative serializer such as AltSerializer. Or you can enforce implementing ToString
in your design and implement an advice that persists the ToString
s.
Other Exception Handling Strategy
The Enterprise Library has an Exception Handling Block, in which you have a nice user interface to configure exception handling policy. So if you like, you can have an advice that delegates exception handling to the Exception Handling Block.
Exception inside ThrowsAdvice
Be careful when handling exception inside advice. You don't want to throw exceptions originating from within advices. They overshadow the original exceptions thrown by the target
. In the example code, I simply swallowed the exception and printed out the information to console.
Exception handling with AOP can eliminate duplicate exception handling code, thus reducing the lines of code for exception handling and improving code maintenance. The unified exception handling increases the agility of your design. We can easily apply different exception handling with a few lines of code and configuration changes. We also showed how we can easily refactor the exception handling aspect to provide more thorough debugging information, such as the serialized argument objects of the method that throws the exception!