CodeGym /Java Course /Module 5. Spring /Spring Expression Language (SpEL)

Spring Expression Language (SpEL)

Module 5. Spring
Level 4 , Lesson 12
Available

Spring Expression Language ("SpEL" for short) is a powerful expression language that supports querying and manipulating an object graph at runtime. The language's syntax is similar to Unified EL, but offers additional features, most notably method invocation and basic string templating functionality.

Although there are several other Java expression languages - OGNL, MVEL, JBoss EL and others - the Spring Expression Language was created to give the Spring community a single, well-supported expression language that can be used across all products in the Spring portfolio. Features of the language are determined by the requirements of projects in the Spring portfolio, including requirements for tools to support code completion in Spring Tools for Eclipse. However, SpEL is based on a technology-independent API that allows the integration of other expression language implementations should the need arise.

Although SpEL is the foundational expression evaluation product in the Spring portfolio, it is not directly related to Spring and can be used independently. To demonstrate self-containment, many of the examples in this chapter use SpEL as if it were an independent expression language. This requires creating several boot infrastructure classes, such as a parser. Most Spring users don't need to deal with this framework, so they can instead create just strings of expressions to evaluate. An example of such a typical use is the integration of SpEL in the creation of XML-based or annotation-based bean definitions.

This chapter discusses the features of the expression language, its API, and the language syntax. In some places, the Inventor and Society classes are used as targets for evaluating expressions. The declarations of these classes and the data used to populate them are listed at the end of the chapter.

The expression language supports the following functionality:

  • Literal expressions (literals);

  • Boolean and relational operators;

  • Regular expressions;

  • Class -expressions;

  • Access to properties, arrays, lists and associative arrays;

  • Access to methods;

  • Relationship operators;

  • Assignment;

  • Calling constructors;

  • Links to bins;

  • Constructing an array;

  • Embedded lists;

  • Embedded associative arrays;

  • Ternary operator;

  • Variables;

  • User-defined functions;

  • Projection of collections;

  • Fetching collections ;

  • Template expressions.

Computation

This section introduces the simple use of the SpEL and their expression language.

The following code uses the SpEL API to evaluate the literal string expression Hello World.

Java

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'"); 
String message = (String) exp.getValue();
  1. The value of the message variable is 'Hello World'.
Kotlin
 
val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'") 
val message = exp.value as String
  1. The value of the message variable is 'Hello World'.

The SpEL classes and interfaces you are most likely to use are in the org.springframework.expression package and its subpackages, such as spel.support.

The ExpressionParser interface is responsible for parsing the expression string. In the previous example, the expression string is a string literal, denoted by the single quotes surrounding it. The Expression interface is responsible for evaluating a previously defined expression string. Two exceptions ParseException and EvaluationException can be thrown when calling parser.parseExpression and exp.getValue, respectively.

SpEL supports a wide range of functionality, such as method invocation, property access, and constructor invocation.

In the following method invocation example, we call the concat method on a string literal:

Java

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'.concat('!')"); 
String message = (String) exp.getValue();
  1. The value of message is now 'Hello World!'.
Kotlin

val parser = SpelExpressionParser()
val exp = parser.parseExpression("'Hello World'.concat('!')") 
val message = exp.value as String
  1. Value message is now 'Hello World!'.

The following example JavaBean property call calls the String property Bytes:

Java

ExpressionParser parser = new SpelExpressionParser();
// accesses 'getBytes()'
Expression exp = parser.parseExpression("'Hello World'.bytes"); 
byte[] bytes = (byte[]) exp.getValue();
  1. This line converts a literal to a byte array.
Kotlin

val parser = SpelExpressionParser()
// accesses 'getBytes()'
val exp = parser.parseExpression("'Hello World'.bytes") 
val bytes = exp.value as ByteArray
  1. This line converts a literal to a byte array.

SpEL also supports nested properties using standard dot notation (for example, prop1.prop2.prop3), as well as setting property values accordingly. It is also possible to access public fields.

The following example shows how to use dot notation to get the length of a literal:

Java

ExpressionParser parser = new SpelExpressionParser();
// accesses 'getBytes().length'
Expression exp = parser.parseExpression("'Hello World'.bytes.length"); 
int length = (Integer) exp.getValue();
  1. 'Hello World'.bytes.length specifies the length of the literal.
Kotlin

val parser = SpelExpressionParser()
// accesses 'getBytes().length'
val exp = parser.parseExpression("'Hello World'.bytes.length") 
val length = exp.value as Int
  1. 'Hello World'.bytes.length specifies the length of the literal.

The String constructor can be called instead of using a string literal, as shown in the following example:

Java

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); 
String message = exp.getValue(String.class);
  1. Create a new String from a literal and convert it to uppercase.
Kotlin

val parser = SpelExpressionParser()
val exp = parser.parseExpression("new String('hello world').toUpperCase()") 
val message = exp.getValue(String::class.java)
  1. Create a new String from a literal and convert it to uppercase.

Note the use of the generic method: public <T> T getValue(Class<T> desiredResultType). Using this method eliminates the need to cast the expression value to the desired result type. If the value cannot be cast to type T or converted using a registered type converter, then an EvaluationException is thrown.

A more common use of SpEL is to provide an expression string that is evaluated against a specific object instance (called the root object). The following example shows how to get the name property from an instance of the Inventor class or create a Boolean Boolean condition:

Java

// Create and configure the calendar
GregorianCalendar c = new GregorianCalendar();
c.set(1856, 7, 9);
// The constructor arguments are name, birthday, and nationality.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("name");
// Parse the name as an expression String name = (String) exp.getValue(tesla);
// name == "Nikola Tesla" exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
Kotlin

// Create and configure the calendar
val c = GregorianCalendar() c .set(1856, 7, 9)
// The constructor arguments are name, birthday, and nationality.
val tesla = Inventor("Nikola Tesla", c.time, "Serbian")
val parser = SpelExpressionParser()
var exp = parser.parseExpression("name")
// Parse the name as an expression
val name = exp.getValue( tesla) as String
// name == "Nikola Tesla"
exp = parser.parseExpression("name == 'Nikola Tesla'")
val result = exp.getValue(tesla, Boolean::class.java)
// result == true

Basic information about EvaluationContext

The EvaluationContext interface is used in evaluation expressions to resolve properties, methods, or fields, and to perform type conversions. Spring provides two implementations.

  • SimpleEvaluationContext: Exposes a subset of the core SpEL language features and configuration options for categories of expressions that do not require full SpEL syntax and must be are significantly limited. Examples include, but are not limited to, data binding expressions and property-based filters.

  • StandardEvaluationContext: Opens the full range of SpEL language capabilities and configuration options. You can use it to specify the default root object and to configure all available evaluation-related strategies.

SimpleEvaluationContext is designed to provide partial syntax support only SpEL language. It excludes references to Java types, constructors, and bean references. It also requires an explicit choice of the level of support for properties and methods in expressions. By default, the static factory method create() allows read-only access to properties. You can also get a constructor to fine-tune the required level of support, focusing on one specific parameter or some combination of the following parameters:

  • Only a special PropertyAccessor (no reflection)

  • Data binding properties for read-only access

  • Data binding properties for read-write access

Type conversion

By default, SpEL uses the conversion service available in the Spring core (org.springframework.core.convert.ConversionService). This conversion service comes with many built-in converters to perform general conversions, but is also fully extensible to allow custom conversions between types to be added. In addition, it can work with generics. This means that if you work with typed types in expressions, SpEL attempts to perform conversions to preserve type correctness for any objects it encounters.

What does this actually mean? Suppose an assignment with setValue() is used to set the List property. The type of the property is actually List<Boolean>. SpEL recognizes that list items must be converted to Boolean before being placed into it. The following example shows how to do this:

Java

class Simple {
public List<Boolean> booleanList = new ArrayList<Boolean>();
}
Simple simple = new Simple();
simple.booleanList.add(true);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
// "false" is passed here as a String. SpEL and the conversion service
// recognizes that it should be Boolean and converts it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b - false
Boolean b = simple.booleanList.get(0);
Kotlin

class Simple {
    var booleanList: MutableList<Boolean> = ArrayList()
}
val simple = Simple()
simple.booleanList.add(true)
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
// "false" is passed as a String here. SpEL and the conversion service
// recognizes that it should be Boolean and converts it accordingly.
parser.parseExpression("booleanList[0]").setValue(context, simple, "false")
// b - false
val b = simple.booleanList[0]

Parser Configuration

You can configure the SpEL expression parser using the parser configuration object (org.springframework.expression.spel.SpelParserConfiguration). The configuration object controls the operating logic of some of the components of the expression. For example, if you are indexing an array or collection and the element at the specified index is null, SpEL can automatically create that element. This makes sense when using expressions that consist of a chain of property references. If you index an array or list and specify an index that is beyond the current size of the array or list, SpEL can automatically grow the array or list to accommodate that index. To add an element at the specified index, SpEL will attempt to create the element using the default element type constructor before setting the specified value. If the element type does not have a default constructor, null will be added to the array or list. If there is no built-in or special resolver that knows how to set the value, null will remain in the array or list at the specified index. The following example demonstrates automatic list expansion:

Java

class Demo {
    public List<String> list;
}
// Enable:
// - automatic initialization of an empty (null) link
// - the autocollection is expanded
SpelParserConfiguration config = new SpelParserConfiguration(true, true);
ExpressionParser parser = new SpelExpressionParser(config);
Expression expression = parser.parseExpression("list[3]");
Demo demo = new Demo(); Object o = expression.getValue(demo);
// demo.list will now be an actual collection of 4 entries
// Each entry is a new empty line 
Kotlin

class Demo {
    var list: List<String>? = null
}
// Enable:
// - automatic initialization of an empty (null) link
// - the autocollection is expanded
val config = SpelParserConfiguration(true, true)
val parser = SpelExpressionParser(config)
val expression = parser.parseExpression("list[3] ")
val demo = Demo() val o = expression.getValue(demo)
// demo.list will now be an actual collection of 4 entries
// Each entry is a new empty string 

Compiling with SpEL

Spring Framework 4.1 includes a basic expression compiler. Typically, expressions are interpreted, which provides greater dynamic flexibility in evaluation, but does not provide optimal performance. For occasional use of expressions this is fine, but when using other components such as Spring Integration, performance may be a factor and there is no real need for dynamism.

The SpEL compiler is designed to meet this need. At evaluation time, the compiler generates a Java class that embodies the execution logic of the expression at runtime, and uses this class to perform much faster expression evaluation. Because there is no typing for expressions, the compiler compiles using the information collected during interpreted evaluations of the expression. For example, it does not recognize the type of a property reference purely from the expression, but during the first interpreted calculation it begins to understand what it is. Naturally, compiling from such derived information can lead to problems down the line if the types of the various elements of the expression change over time. For this reason, compilation is best for expressions whose type information will not change when evaluated repeatedly.

Consider the following basic expression:

someArray[0].someProperty.someOtherProperty < 0.1

Because the previous expression involves array access, some property dereferencing, and numeric operations, the performance benefit can be very noticeable. In the example microbenchmark test run of 50,000 iterations, it took 75 ms to evaluate using the interpreter, but only 3 ms using the compiled version of the expression.

Compiler configuration

The compiler is not enabled by default, but you can activate it in one of two different ways. You can enable it through the parser configuration procedure, or through a Spring property if the use of SpEL is embedded in another bean. This section discusses both of these options.

The compiler can operate in one of three modes, which are reflected in the enum type org.springframework.expression.spel.SpelCompilerMode. The modes are as follows:

  • OFF (default): The compiler is turned off.

  • IMMEDIATE : In immediate mode, expressions are compiled as quickly as possible. Typically this occurs after the first interpreted calculation. If the compiled expression fails (usually due to a type change, as described earlier), the caller to the expression evaluation code receives an exception.

  • MIXED: In In mixed mode, expressions switch seamlessly between interpreted and compiled mode over time. After a certain number of interpreted executions, they are switched to the compiled form, but if something goes wrong in the compiled form (for example, the type changes, as described earlier), the expression is automatically switched back to the interpreted form. Some time later, the expression may generate another compiled form and switch to that. Basically, an exception that the user receives in IMMEDIATE mode is instead handled internally.

The IMMEDIATE mode exists because that the MIXED mode may cause problems with expressions that have side effects. If a compiled expression produces a catastrophic error after being partially successful, it may have already performed some action that affected the state of the system. If this happens, the calling code may not want it to be silently re-executed in interpreted mode, since part of the expression might be executed twice.

After choosing a mode, use SpelParserConfiguration to configure the parser. The following example shows how to do this:

Java

SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
        this.getClass().getClassLoader());
SpelExpressionParser parser = new SpelExpressionParser(config);
Expression expr = parser.parseExpression("payload");
MyMessage message = new MyMessage();
Object payload = expr.getValue(message);
Kotlin

val config = SpelParserConfiguration(SpelCompilerMode.IMMEDIATE,
        this.javaClass.classLoader)
val parser = SpelExpressionParser(config)
val expr = parser.parseExpression("payload")
val message = MyMessage()
val payload = expr.getValue(message)

If you specify a compiler mode, you can also specify a class loader (passing null is allowed). Compiled expressions are defined in a child classloader created under any specified class. It is important to ensure that if a class loader is specified, it can recognize all types involved in the expression evaluation process. If you do not specify a class loader, the default class loader will be used (usually the context class loader for the thread that is executed while expressions are evaluated).

The second method of configuring the compiler is intended to be used if SpEL is implemented in any - another component, and it is not possible to configure it through a configuration object. In these cases, you can set the spring.expression.compiler.mode property via a system property from the JVM (or via the SpringProperties mechanism) on one of the enum type values SpelCompilerMode(off, immediate or mixed).

Compiler limitations

Starting with Spring Framework 4.1, a basic compilation framework is used. However, the framework does not yet support the compilation of all types of expressions. The initial focus was on common expressions that are likely to be used in performance-critical contexts. The following types of expressions cannot currently be compiled:

  • Assignment-related expressions;

  • Expressions that use the conversion service;

  • Expressions that use special recognizers or accessors;

  • Expressions that use selection or projection.

More types of expressions will be able to be compiled in the future.

Expressions in bean definitions

SpEL expressions can be used to define instances of BeanDefinition with XML-based or annotation-based configuration metadata. In both cases, the syntax for defining an expression is #{ <expression string> }.

4.2.1. XML Configuration

The value of a property or constructor argument can be specified using expressions, as shown in the following example:


<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
    <!-- other properties -->
</bean>

All beans in the application context are available as predefined variables with their normal bean name. This includes standard context beans such as environment (like org.springframework.core.env.Environment), as well as systemProperties and systemEnvironment (type Map<String, Object>) to gain access to the runtime environment.

The following example demonstrates access to the systemProperties bean as for the SpEL variable:


<bean id="taxCalculator" class="org.spring.samples.TaxCalculator">
    <property name="defaultLocale" value="#{ systemProperties['user.region'] }"/>
<!-- other properties -->
</bean>

Note that in this case you do not need to prefix the predefined variable with the symbol #.

You can also reference other bean properties by name, as shown in the following example:


<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
    <!-- other properties -->
</bean>
<bean id="shapeGuess" class="org.spring.samples.ShapeGuess">
    <property name="initialShapeSeed" value="#{ numberGuess.randomNumber }"/>
    <!-- other properties -->
</bean>

Annotation configuration

To set a default value, you can place the annotation @Value to fields, methods, method parameters, or constructor parameters.

In the following example, a default value is set for a field:

Java

public class FieldValueTestBean {
    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }
    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
Kotlin

class FieldValueTestBean {
    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

The following example shows the equivalent, but for a property setter:

Java

public class PropertyValueTestBean {
    private String defaultLocale;
    @Value("#{ systemProperties['user.region'] }")
    public void setDefaultLocale(String defaultLocale) {
        this.defaultLocale = defaultLocale;
    }
    public String getDefaultLocale() {
        return this.defaultLocale;
    }
}
Kotlin

class PropertyValueTestBean {
    @Value("#{ systemProperties['user.region'] }")
    var defaultLocale: String? = null
}

Automatically discovered and associated methods and constructors can also use the @Value annotation , as shown in the following examples:

Java

public class SimpleMovieLister {
    private MovieFinder movieFinder;
    private String defaultLocale;
    @Autowired
    public void configure(MovieFinder movieFinder,
            @Value("#{ systemProperties['user.region'] }") String defaultLocale) {
        this.movieFinder = movieFinder;
        this.defaultLocale = defaultLocale;
    }
    // ...
}
Kotlin

class SimpleMovieLister {
    private lateinit var movieFinder: MovieFinder
    private lateinit var defaultLocale: String
    @Autowired
    fun configure(movieFinder: MovieFinder,
                @Value("#{ systemProperties['user.region'] }") defaultLocale: String) {
        this.movieFinder = movieFinder
        this.defaultLocale = defaultLocale
    }
    // ...
}
Java

public class MovieRecommender {
    private String defaultLocale;
    private CustomerPreferenceDao customerPreferenceDao;
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao,
            @Value("#{systemProperties['user.country']}") String defaultLocale) {
        this.customerPreferenceDao = customerPreferenceDao;
        this.defaultLocale = defaultLocale;
    }
    // ...
}
Kotlin

class MovieRecommender(private val customerPreferenceDao: CustomerPreferenceDao,
            @Value("#{systemProperties['user.country']}") private val defaultLocale: String) {
    // ...
}
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION