4/27/2012

Avoid Null Parameters With Annotation


In JEE6 there is a NotNull annotation which is of very limitted functionality. In my recent project I came across a requirement to check methods parameters for null. Writing if blocks in all the methods is not a good solution. So I decided to go with an AOP solution. I used AspectJ for all my AOP stuff in this blog. Here is what I did, I created an annotation called NotNull and an Aspect with Around pointcut to intercept method calls which has its parameters annotation with NotNull annotation.

Now I will walk through the code.
Code Snippet for NotNull Annotation:
 package org.naveen.maven.research.annotations;

 import java.lang.annotation.ElementType;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.lang.annotation.Target;

 @Retention(RetentionPolicy.RUNTIME)
 @Target(ElementType.PARAMETER)
 public @interface NotNull {
  /**
   * Message set in the exception thrown when parameter is null.
   */
  String message() default "Parameter value is not nullable.";
  
  /**
   * Will specify how null values have to be treated. 
   */
  NotNullPolicy policy() default NotNullPolicy.THROW_EXCEPTION;
  
  /**
   * If you want to throw a custom exception. 
   * By default this will throw an IllegalArgument Exception.
   */
  Class<? extends Throwable> exception() default IllegalArgumentException.class;
 }
 

Here is the NotNullPolicy enumeration:
 package org.naveen.maven.research.annotations;

 public enum NotNullPolicy {
  /** Irrespective of method body, method will return null if all arguments with this policy is null. */
  RETURN_NULL, 
  /** Will throw an exception. [Default] */
  THROW_EXCEPTION, 
  /** Don't do anything and will continue with method body */
  CONTINUE,
  /** Irrespective of method body, method will return null. */
  RETURN_NULL_IMMEDIATE
 }
 

And here comes the backbone, the Aspect.
 package org.naveen.maven.research.aspects;

 import java.lang.annotation.Annotation;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;

 import org.aspectj.lang.ProceedingJoinPoint;
 import org.aspectj.lang.Signature;
 import org.aspectj.lang.annotation.Around;
 import org.aspectj.lang.annotation.Aspect;
 import org.aspectj.lang.reflect.MethodSignature;
 import org.naveen.maven.research.annotations.NotNull;
 import org.naveen.maven.research.annotations.NotNullPolicy;

 @Aspect
 public class NotNullAspect {
  
  private static final Logger logger = Logger.getLogger(NotNullAspect.class.getName());

  @Around("execution(* org.naveen..*(..,@NotNull (*),..)) && " +
    "(!within(org.naveen.maven.research.annotations.*))")
  public Object doNullParamCheck(ProceedingJoinPoint jp) throws Throwable {
   List<Boolean> lazyNullRtValue = new LinkedList<Boolean>();
   boolean hasToContd = false;
   Object[] args = jp.getArgs();
   Signature sig = jp.getSignature();
   if(sig instanceof MethodSignature) {
    MethodSignature mtdSig = MethodSignature.class.cast(sig);
    Method mtd = mtdSig.getMethod();
    logger.log(Level.INFO, String.format("Checking parameters for method --> %s", mtd.getName()));
    Annotation[][] anns = mtd.getParameterAnnotations();
    
    for(int i=0;i<anns.length;++i) {
     if(anns[i].length > 0) {
      if(anns[i][0] instanceof NotNull) {
       NotNull ntnAnn = (NotNull) anns[i][0];
       logger.log(Level.INFO, String.format("NotNull handling policy is set to %s", ntnAnn.policy()));
       if(args[i] == null) {
        switch(ntnAnn.policy()) {
        case THROW_EXCEPTION:
         Class<? extends Throwable> expCls = ntnAnn.exception();
         logger.log(Level.INFO, String.format("Exception class set is %s", expCls.getName()));
         if(expCls == IllegalArgumentException.class) {
          throw new IllegalArgumentException(
           String.format("In Method %s --> %s", 
             mtd.getName(), ntnAnn.message()));
         } else {
          Constructor<? extends Throwable> ct = expCls.getConstructor(String.class);
          Throwable t = ct.newInstance(String.format("In Method %s --> %s", 
                  mtd.getName(), ntnAnn.message()));
          throw t;
         }
        case RETURN_NULL_IMMEDIATE:
         return null;
        case RETURN_NULL:
         logger.log(Level.WARNING, "One NotNull annotated argument has null value. Continuing with lazy checking.");
         lazyNullRtValue.add(false);
         break;
        case CONTINUE:
         hasToContd = true;
         lazyNullRtValue.add(true);
         break;
        }
       } else {
        hasToContd = true;
        if(ntnAnn.policy() == NotNullPolicy.RETURN_NULL) 
         lazyNullRtValue.add(true);
       }
      }
     }
    }
   }
   logger.log(Level.INFO, String.format("Argument length is %d and LazyReturn list is %s and hasToContd %s", 
            args.length, lazyNullRtValue, hasToContd));
   if(hasToContd && checkLazyNull(lazyNullRtValue)) 
    return jp.proceed();
   return null;
  }
  
  private boolean checkLazyNull(List<Boolean> list) {
   if(list.size() == 0) return true; 
   for(Boolean b : list)
    if(b) return b;
   return false;
  }
 }
 

 @Around("execution(* org.naveen..*(..,@NotNull (*),..)) && " +
   "(!within(org.naveen.maven.research.annotations.*))")
 
Above code snippet will instruct AOP weaver to weave classes which satifies following conditions:
  • Which has methods which is declared inside classes which are declared in org.naveen package or sub package of it.
  • Which has methods whoose paramters are annotated with NotNull annotation.
  • Dont weave classes inside org.naveen.maven.research.annotations package or sub package of it.

Different Usage examples:
Throws custom exception:
 public String test(String message, @NotNull(exception=ParameterException.class) String name) {
  return message + name;
 }
 

Throws default exception with custom message:
 private String privateTest(@NotNull String message, @NotNull(message="Name cannot be null.") String name) {
  return message + name;
 }
 
Method exists with Null:
 private String privateTest(@NotNull(policy=NotNullPolicy.RETURN_NULL_IMMEDIATE) String greet) {
  System.out.println("Message received is " + greet);
  return "Hello World " + greet;
 }
 
If you call above privateTest() method with a null argument, you don't see that System out getting executed.

Suppose if you have a scenario like this:
  • You have a method which will accept 2 arguments.
  • Method returns null, if both arguments are null.
  • Execute method, if one of the argument is not null.
You can use NotNullPolicy.RETURN_NULL policy to instruct NotNull aspect to do a lazy null checking.
 private String privateTest(@NotNull(policy=NotNullPolicy.RETURN_NULL) String message, 
          @NotNull(policy=NotNullPolicy.RETURN_NULL, message="Name cannot be null.") String name) {
  System.out.println("Message received is " + message);
  return "Hello World " + name;
 }
 

You can combine both NotNullPolicy.RETURN_NULL and NotNullPolicy.RETURN_NULL_IMMEDIATE together.
 private String privateTest(@NotNull(policy=NotNullPolicy.RETURN_NULL) String message, 
          @NotNull(policy=NotNullPolicy.RETURN_NULL_IMMEDIATE) String greet,
                      @NotNull(policy=NotNullPolicy.RETURN_NULL, message="Name cannot be null.") String name) {
  System.out.println("Message received is " + message);
  return "Hello World " + name;
 }
 

In the above example, we are saying :
  • Execute method body, if either of the arguments 'message' or 'name' is not null AND greet is not null.
  • Return Null, if both arguments 'message' and 'name' is null
  • Return Null, if argument 'greet' is Null


All Resources used in this project are available in My GitHub Repo.