After being exposed to Spring Expression Language, I thought it could be useful to harness its power to describe condition constraints on methods, as part of a growing focus on test driven development.
The idea is to describe expectations from the object’s state prior to the method’s invocation, expectation from the input and expectation from state following the method’s invocation.
Since spring expression language exposes public methods, its interesting to see how we can verify correctness using other public methods.
For this purpose I will introduce 3 simple annotations:
@ConditionContext- a spring expression to be evaluated prior to any condition checks
@Precondition – a spring expression to be evaluated prior to the method invocation and after the latter
@Postcondition – a spring expression to be evaluated after the method invocation
All of these annotations are of the same code structure:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Postcondition {
String value();
}
I’ve written a very simple service that just manages a list of products.
The service supports the following operations:
public interface FooService {
public Set<Product> getProducts();
public Product getProduct(int id);
public void insertProduct(Product product);
public void updateProduct(Product product);
public void deleteProduct(Product product);
}
Where a Product just contains an ID and price.
Before I show the behind the scenes, I would like to show the final outcome which is what I described above:
@ConditionContext("#ctx.put('sizeBefore', #self.Products.size())")
@Precondition("#arg0 != null and #arg0.Id > 0")
@Postcondition("#sizeBefore + 1 == #self.Products.size() and " +
"#self.Products.?[Id == 1].size() == 1 and " +
"#self.getProduct(#arg0.Id).equals(#arg0)")
public voidinsertProduct(Product product) {
products.add(product);
}
Here are my expectations from the insertProduct method as described in the constraint (just as an example, i know there many other possible constraints which can be expressed):
As a precondition I want the input to be valid which means the product argument (arg0) is not and has a positive ID.
I can also express conditions about the service’s state prior to insert, I will show that in the post condition.
Following the insert the number of products should be incremented by 1, and I shall be able to find the added product.
The latter is expressed in two different ways:
1) the obvious one of invoking getProduct() on the Id and making sure the objects are equal.
2) getting the entire collection and finding it in the collection. I have also wanted to demonstrate SpEL, so I used the collection selection syntax it provides.
The condition context is simply a way to store information prior to method invocation.
Since you cant set variables outside the SpEL context, I used a map to store the data and I later introduce the map’s keys as variable in the SpEL context.
This is why in @Postcondition you see #sizeBefore.
How does this work?
I started by creating a small driver code:
ApplicationContext ctx = newClassPathXmlApplicationContext(newString[]{"beans.xml"});
FooService fooService = (FooService) ctx.getBean("fooService");
Product product1 = new Product();
product1.setId(1);
product1.setPrice(1);
fooService.insertProduct(product1);
Adding my AOP and bean definitions in beans.xml file:
<!-- Enabling aspect support --> <aop:aspectj-autoproxy/> <bean id="product" class="com.lab49.spelaspecttest.domain.Product"/> <bean id="fooService" class="com.lab49.spelaspecttest.service.DefaultFooService"/> <bean id="aspectBean" class="com.lab49.spelaspecttest.aspect.PointCuts"/>
I created an aspect with the following:
- A Pointcut for any public method.
- An Around advice (such that I can control method invocation) to handle the pre and post condition logic.
The logic is quite simple, evaluate the preconditions, if they are met execute the method, if not throw an IllegalStateException.
Following the method execution, evaluate the post conditions and do the same.
The real magic occurs in the aspect code as follows (see documentation in code):
@Aspect
public class PointCuts {
@Pointcut("execution(public * *(..))")
private void anyPublicOperation() {}
@Pointcut("within(com.lab49.spelaspecttest..*)")
private void inServiceLayer() { }
/**
* Just for debugging, print methods executed in service layer
* @param jp
*/
@Before("inServiceLayer()")
public void logServiceMethodExecution(JoinPoint jp) {
System.out.println("Executing: " + jp.getSignature().getName());
}
/**
* I use the public pointcut and add target and annotation to statically type the annotation variables and targen object (the service).
*
*/
@Around(value = "anyPublicOperation() && target(bean) && @annotation(conditionContext) && @annotation(precondition) && @annotation(postcondition)", argNames="bean, conditionContext, precondition, postcondition")
public Object checkConditionConstraints(ProceedingJoinPoint pjp, Object bean, ConditionContext conditionContext, Precondition precondition, Postcondition postcondition) throws Throwable {
System.out.println("Condition context: " + conditionContext.value());
System.out.println("Precondition: " + precondition.value());
System.out.println("Postcondition: " + postcondition.value());
// SPeL action begins
EvaluationContext context = new StandardEvaluationContext(bean);
// I add a #self variable because the implicit this SPeL evaluates properties
// with no object against can be confusing or inconsistent.
context.setVariable("self", bean);
// Add all the #arg* variables to the context, the method inputs.
addArgumentsToContext(pjp, context);
ExpressionParser parser = new SpelExpressionParser();
Expression preConditionExp = parser.parseExpression(precondition.value());
// a little syntactic sugar - we turn the keys in the map in the condition context
// to variables in evaluating the precondition and postcondition expressions
updateConditionContext(conditionContext, parser, context);
// SPeL uses the class<T> generics to ease on casting return values
boolean preconditionValue = preConditionExp.getValue(context, Boolean.class);
System.out.println("Precondition value: " + preconditionValue);
if(!preconditionValue) {
throw new IllegalStateException("Precondition " + precondition.value() + " failed");
}
// use the JoinPoint to actually execute the underlying method
Object retval = pjp.proceed();
Expression postConditionExp = parser.parseExpression(postcondition.value());
boolean postConditionValue = postConditionExp.getValue(context, Boolean.class);
System.out.println("Postcondition value: " + postConditionValue);
if(!postConditionValue) {
throw new IllegalStateException("Postcondition " + postcondition.value() + " failed");
}
//if the postcondition is met, return the method's original return value
return retval;
}
private void updateConditionContext(ConditionContext conditionContext, ExpressionParser parser, EvaluationContext context) {
Map<String,Object> map = new HashMap<String, Object>();
context.setVariable("ctx", map);
Expression contextExpression = parser.parseExpression(conditionContext.value());
contextExpression.getValue(context);
for(Entry<String, Object> entry : map.entrySet()) {
context.setVariable(entry.getKey(), entry.getValue());
}
}
private void addArgumentsToContext(ProceedingJoinPoint pjp, EvaluationContext context) {
Object[] args = pjp.getArgs();
for(int i = 0; i < args.length; i++) {
context.setVariable("arg" + i, args[i]);
}
}
}