2007-08-26

Better exception handling

I read Karsten Wagner's Blog: Better exception handling today. The post in essence touches three key elements of exception handling and design.

Exceptions are an equally important part of API design

Equally important as classes, method, and arguments. What exceptions are thrown where and why, and whether they should be checked, has to be carefully thought of. One should also be careful when the class hierarchy of exceptions, if one should be made at all. For instance it isn't always the case that all exceptions should be checked (or unchecked for that matter). Be very specific on which cases which could benefit from a checked exception and thus explicit error handling on the client. The post uses classes from java.io as an example of how not to do it. java.sql and java.remote could also be mentioned with their checked exceptions causing boilerplate code time and time again.

An essential problem of their checked IOException, SQLException and RemoteException is that the exception has no way of informing the executing code whether the failure is transient or permanent, and what the recovery or restart actions could be. For SQLException in JDK 6 this is not longer the cases, as there are now several subclasses of SQLException providing this information. See SQLNonTransientException, SQLRecoverableException, SQLTransientException, and SQLWarning for details.

Nulls considered harmful

I have written about this topic before, as others have. With the ability to declare whether a method accepts or returns nulls, static analysis tools, or even the compiler, could do checks both at compile and execution time giving better error reporting on null references. Static analysis could produce compiler errors when an argument declared not to accept nulls are served a value known to be null. At run time checks could be added like the one added for arrays indexing, for instance in form of assertions: assert someVarName != null : "Invalid null dereference of declared nonnull variable 'someVarName'"; These assertions could potentially be disabled by the -da command line option to the JVM.

Unchecked exceptions are better than their reputation

They are far better than many other alternatives, like SIGSEGV and core dumps, and are actually quite robust and helpful in diagnosing an error. For most cases, a RuntimeException means you have error situations you cannot or will not handle. The default result in Java is that the stack trace is written to the console, and the executing thread stops. Whether or not the execution is restartable depends on your application. If it's a background thread responsible for some kind of periodic check, the thread will need to be rescheduled. If it's a thread responsible for serving some external request, the client could just retry the request.

Aspects to the assistance

One possible way of working around some of these issues is to put AspectJ to work. Save state to add a restartable checkpoint Use this to add a checkpoint before attempting an operation known to be erratic and cause inconsistent state:

@Pointcut("call(* dangerousOperation(..))") 
public void dangerousOperation() {} 

@Before("dangerousOperation() && this(myObject)") 
public void saveState(MyClass myObject) {
   myObject.saveState(); 
} 

@AfterThrowing(pointcut="dangerousOperation() && this(myObject)", throwing="e") 
public void restoreState(MyClass myObject, SomeDangerousCheckedException e) {
  myObject.recoverState();
   log.warning("Recovered " + myObject + " from " + e);
}

Handling a certain checked exception in a common way
When using an API throwing checked exceptions you have to add boilerplate try / catch blocks around each and every call to this API, something that could be very tedious. One solution is to use a wrapper API which catches these exceptions and provides error handling and / or unchecked exceptions. Spring JDBC does this for JDBC access. But you can also use an aspect for this, like this one from ibm.com/developerworks/ :

public class SqlAccess {
   private Connection conn;
   private Statement stmt;
   public void doUpdate(){
     conn = DriverManager.getConnection("url for testing purposes");
     stmt = conn.createStatement();
     stmt.execute("UPDATE ACCOUNTS SET BALANCE = 0");
   }
   public static void main(String[] args) throws Exception{
     new SqlAccess().doUpdate();
   }
 }

private static aspect exceptionHandling{
   declare soft : SQLException : within(SqlAccess);
   pointcut methodCall(SqlAccess accessor) : this(accessor) && call(* * SqlAccess.*(..));
   after(SqlAccess accessor) : methodCall (accessor){
     System.out.println("Closing connections.");
     if(accessor.stmt != null) {
       accessor.stmt.close();
     }
     if(accessor.conn != null) {
       accessor.conn.close();
     }
   }
 }

0 kommentarer:

Legg inn en kommentar