Monday, August 6, 2012

using Spring Expression Language (SpEL) in a blueprint container

Spring Expression Language (SpEL) can be used to configure beans in a spring application context. For example the Spring-batch framework makes heavy use of it for what they call late binding of properties. To get a feel of what SpEL can do, some examples from the spring documentation:

"#{ T(java.lang.Math).random() * 100.0 }" --> return random value [0, 100)
"#{'5.00' matches '^-?\\d+(\\.\\d{2})?$'}" --> return true this regex matches
"#{8 / 5 % 2}" --> return 1

The prefix and suffix #{ and } in this case are just a convention and can be configured differently if you want or just leave them out


In this blog I will show how it can be used in an osgi-blueprint container. The idea is quite simple, we will use the type-converter feature from the blueprint spec to convert our SpEL expression we want to inject. If the expression is correct the result type should match the type expected by the bean. With the code given in this simple example it is not possible to access the beans from the blueprint container. That for a next post.
Here is the code:
package be.i8c.spel;


import org.osgi.service.blueprint.container.Converter;
import org.osgi.service.blueprint.container.ReifiedType;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;

/**
 * 
 * tries to 'convert' string to targetType by evaluating it as a SpEL expression
 *
 */
public class ExpressionLanguageConverter implements Converter {
 
 private static final String EXPRESSION_PREFIX = "#{";
 
 private static final String EXPRESSION_SUFFIX = "}";
 
 private final ParserContext parserContext;
 
 private final EvaluationContext evaluationContext;
 


 public ExpressionLanguageConverter(EvaluationContext evaluationContext){
  
  this.evaluationContext = evaluationContext;
  parserContext = new ParserContext() {
   
   public boolean isTemplate() {
    return true;
   }
   
   public String getExpressionSuffix() {
    return EXPRESSION_SUFFIX;
   }
   
   public String getExpressionPrefix() {
    return EXPRESSION_PREFIX;
   }
  };
 }
 
 /**
  * @return true if the sourceObject is a string expression 
  *      that can be parsed as a Spring Expression Language 
  *      this does not guarantee that the evaluating the expression gives a result of type targetType
  */
 public boolean canConvert(Object sourceObject, ReifiedType targetType) {
  
  if (String.class.isInstance(sourceObject) && ((String)sourceObject).startsWith(EXPRESSION_PREFIX)) {
   try {
    parse(sourceObject);
    return true;
   } catch (ParseException e) {
    return false;
   }
  }
  return false;
 }

 
 @SuppressWarnings("unchecked")
 public Object convert(Object sourceObject, ReifiedType targetType) throws Exception {
  if (String.class.isInstance(sourceObject)) {
   Expression expression = parse(sourceObject);
   return expression.getValue(evaluationContext, targetType.getRawClass());
  }
    
  throw new RuntimeException("could not convert/evaluate source:"+sourceObject +" to target:"+targetType);
 }

 private Expression parse(Object sourceObject) {
  SpelExpressionParser parser = new SpelExpressionParser();
  Expression expression = parser.parseExpression((String)sourceObject, parserContext);
  return expression;
 }

}
OSGI is all about class loading so we have to configure the EvaluationContext to use the classloader associated with this bundle, this is done with a custom TypeLocator:


package be.i8c.spel.impl;

import org.osgi.framework.Bundle;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypeLocator;

public class BundleTypeLocator implements TypeLocator {
 
 private final Bundle bundle;

 public BundleTypeLocator(Bundle bundle){
  this.bundle = bundle;
 }

 public Class<?> findType(String typename) throws EvaluationException {
  Class<?> clazz;
  try {
   clazz = bundle.loadClass(typename);
   return clazz;
  } catch (ClassNotFoundException e) {
   throw new EvaluationException("could not find class in bundle:"+bundle.getBundleId(), e);
  }
  
 }

}
All this code is wired together in the blueprint.xml file:
<?xml version="1.0" encoding="UTF-8"?>
<blueprint xmlns="http://www.osgi.org/xmlns/blueprint/v1.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="
    http://www.osgi.org/xmlns/blueprint/v1.0.0 http://www.osgi.org/xmlns/blueprint/v1.0.0/blueprint.xsd default-activation="lazy">
    
    <type-converters>
     <bean id="spelConverter" class="be.i8c.spel.ExpressionLanguageConverter">
      <argument ref="evaluationContext"/>
     </bean>
    </type-converters>
    
    <bean id="evaluationContext" class="org.springframework.expression.spel.support.StandardEvaluationContext">
     <property name="typeLocator">
      <bean class="be.i8c.spel.impl.BundleTypeLocator">
       <argument ref="blueprintBundle"/>
      </bean>
     </property>
    </bean>
    
    <bean id="numberGuess" class="be.i8c.spel.impl.DummyService" activation="eager" scope="singleton" >
     <argument type="java.lang.Double" value="#{ T(java.lang.Math).random() * 100.0 }"/>
 </bean>
</blueprint>
Note the type-converters tags to register our type converter, our custom typeLocator and the SpEL expression #{ T(java.lang.Math).random() *100.0}.

Last but not least don't forget to install the spring-expression and spring-core bundle in your osgi container when you want to deploy.

ref:
http://static.springsource.org/spring/docs/3.1.x/spring-framework-reference/html/expressions.html


author: Johan Huylebroeck

1 comment:

  1. Hi Johan,

    Thank you for the nice post. Does it work if bean argument or bean property is of type "String"?

    Kind regards,
    Vadym Korol

    ReplyDelete