Sunday, November 22, 2009

Enforcing method contracts with AspectJ and Spring EL

After reading a bit about the new Expression Language in Spring 3 I decided to try to use it to implement a very basic version of Design by Contract. I thought it would be nice to be able to specify pre and post conditions for methods in the expression language, e.g.

public class Stack {

@Require("value != null")
@Ensure("top().equals(value)")
public void push(String value) {
values.add(value);
}
}


It turned out that there were two problems with this approach:




  • The Spring EL saw the “value” and tried to find a matching property in the root object of the evaluation context. To access named variables you have to write “#value”. Not quite as aesthetically pleasing and a bit error prone, but easy enough.


  • The other problem was that there is no way to get at the parameter names using Java reflection of course. Luckily someone solved this problem for me. At codehaus there is this library Paranamer which will read the parameter names from the debug info in the class files.



So my annotations then looked like this:



public class Stack {

@Require("#value != null")
@Ensure("top().equals(#value)")
public void push(String value) {
values.add(value);
}
}


Enforcing the Contract



The annotation classes themselves are pretty boring, they only contain a value and specify that they only apply to methods and are retained at runtime.



@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Require {
public String value();
}


The fun part is evaluating these pre and post conditions.  It turned out that AspectJ makes it quite easy with its support for annotation based pointcuts:



public aspect ContractEnforcer {
pointcut methodWithPrecondition() : execution(@Require * *(..));
pointcut methodWithPostcondition() : execution(@Ensure * *(..));
pointcut methodWithContract() : methodWithPrecondition() || methodWithPostcondition();
ThreadLocal inContractCheck = new ThreadLocal();

Object around(): methodWithContract() {
if(Boolean.TRUE.equals(inContractCheck.get())) {
return proceed();
}
inContractCheck.set(Boolean.TRUE);
Object result = null;
try {
Signature sig = thisJoinPointStaticPart.getSignature();
if(sig instanceof MethodSignature) {
Method method = ((MethodSignature) sig).getMethod();
Paranamer paranamer = new BytecodeReadingParanamer();
String[] parameterNames = paranamer.lookupParameterNames(method);
Require precondition = method.getAnnotation(Require.class);
if(precondition != null) {
enforcePrecondition(precondition.value(), sig, parameterNames, thisJoinPoint.getArgs(), thisJoinPoint.getTarget());
}
result = proceed();
Ensure postcondition = method.getAnnotation(Ensure.class);
if(postcondition != null) {
enforcePostcondition(postcondition.value(),sig, parameterNames, thisJoinPoint.getArgs(), thisJoinPoint.getTarget(), result);
}
}
} finally {
inContractCheck.set(Boolean.FALSE);
}
return result;
}

private void enforcePrecondition(String condition, Signature signature, String[] parameterNames, Object[] parameterValues, Object target) {
if(!conditionSatisfied(condition, parameterNames, parameterValues, target, null)) {
throw new PreconditionViolationException(condition, signature.toString(), parameterNames, parameterValues);
}
}

private void enforcePostcondition(String condition, Signature signature, String[] parameterNames, Object[] parameterValues, Object target, Object methodResult) {
if(!conditionSatisfied(condition, parameterNames, parameterValues, target, methodResult)) {
throw new PostconditionViolationException(condition, signature.toString(), parameterNames, parameterValues, methodResult);
}
}
private boolean conditionSatisfied(String condition, String[] parameterNames, Object[] parameterValues, Object target, Object methodResult) {
try {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(condition);
EvaluationContext context = new StandardEvaluationContext(target);
for(int index = 0; index < parameterNames.length; index++) {
context.setVariable(parameterNames[index], parameterValues[index]);
}
context.setVariable("result", methodResult);
Boolean result = exp.getValue(context, Boolean.class);
if(result == null) return false;
return result;
} catch(ParameterNamesNotFoundException e) {
System.out.println("Could not check condition - parameter names not available");
} catch(ParseException e) {
System.out.println("Could not check condition: " + e);
} catch (EvaluationException e) {
System.out.println("Could not check condition: " + e);
}
return false;
}
}


So it turned out to be quite easy to provide this very basic contract enforcement. The ThreadLocal is in there to make it possible to use methods with contract annotations within the contract conditions of other methods. That way only the “top most” conditions are checked.



Of course there is a lot to be done but I think this approach looks quite promising. I expect Spring EL support to arrive pretty soon in most IDEs and then it probably won’t be too difficult to provide code completion and error checking for the pre and post conditions in the IDE.