Add SimpleEvaluationContext

Issue: SPR-16588
This commit is contained in:
Rossen Stoyanchev 2018-03-13 15:09:24 -04:00
parent 58011f71e9
commit cb8c6e3251
2 changed files with 249 additions and 114 deletions

View File

@ -0,0 +1,193 @@
/*
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.expression.spel.support;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.OperatorOverloader;
import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypeComparator;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypeLocator;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.lang.Nullable;
/**
* A basic implementation of {@link EvaluationContext} that focuses on a subset
* of essential SpEL features and configuration options, and relies on default
* strategies otherwise.
*
* <p>In many cases, the full extent of the SpEL is not
* required and should be meaningfully restricted. Examples include but are not
* limited to data binding expressions, property-based filters, and others. To
* that effect, {@code SimpleEvaluationContext} supports only a subset of the
* SpEL language syntax that excludes references to Java types, constructors,
* and bean references.
*
* <p>Note that {@code SimpleEvaluationContext} cannot be configured with a
* default root object. Instead it is meant to be created once and used
* repeatedly through method variants on
* {@link org.springframework.expression.Expression Expression} that accept
* both an {@code EvaluationContext} and a root object.
*
* @author Rossen Stoyanchev
* @since 4.3.15
*/
public class SimpleEvaluationContext implements EvaluationContext {
private static final TypeLocator typeNotFoundTypeLocator = typeName -> {
throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName);
};
private final List<PropertyAccessor> propertyAccessors;
private final List<ConstructorResolver> constructorResolvers =
Collections.singletonList(new ReflectiveConstructorResolver());
private final List<MethodResolver> methodResolvers =
Collections.singletonList(new ReflectiveMethodResolver());
private final TypeConverter typeConverter;
private final TypeComparator typeComparator = new StandardTypeComparator();
private final OperatorOverloader operatorOverloader = new StandardOperatorOverloader();
private final Map<String, Object> variables = new HashMap<>();
public SimpleEvaluationContext() {
this(null, null);
}
public SimpleEvaluationContext(@Nullable List<PropertyAccessor> accessors, @Nullable TypeConverter converter) {
this.propertyAccessors = initPropertyAccessors(accessors);
this.typeConverter = converter != null ? converter : new StandardTypeConverter();
}
private static List<PropertyAccessor> initPropertyAccessors(@Nullable List<PropertyAccessor> accessors) {
if (accessors == null) {
accessors = new ArrayList<>(5);
accessors.add(new ReflectivePropertyAccessor());
}
return accessors;
}
/**
* {@code SimpleEvaluationContext} cannot be configured with a root object.
* It is meant for repeated use with
* {@link org.springframework.expression.Expression Expression} method
* variants that accept both an {@code EvaluationContext} and a root object.
* @return Always returns {@link TypedValue#NULL}.
*/
@Override
public TypedValue getRootObject() {
return TypedValue.NULL;
}
@Override
public List<PropertyAccessor> getPropertyAccessors() {
return this.propertyAccessors;
}
/**
* Return a single {@link ReflectiveConstructorResolver}.
*/
@Override
public List<ConstructorResolver> getConstructorResolvers() {
return this.constructorResolvers;
}
/**
* Return a single {@link ReflectiveMethodResolver}.
*/
@Override
public List<MethodResolver> getMethodResolvers() {
return this.methodResolvers;
}
/**
* {@code SimpleEvaluationContext} does not support use of bean references.
* @return Always returns {@code null}
*/
@Override
@Nullable
public BeanResolver getBeanResolver() {
return null;
}
/**
* {@code SimpleEvaluationContext} does not support use of type references.
* @return {@code TypeLocator} implementation that raises a
* {@link SpelEvaluationException} with {@link SpelMessage#TYPE_NOT_FOUND}.
*/
@Override
public TypeLocator getTypeLocator() {
return typeNotFoundTypeLocator;
}
/**
* The configured {@link TypeConverter}.
* <p>By default this is {@link StandardTypeConverter}.
*/
@Override
public TypeConverter getTypeConverter() {
return this.typeConverter;
}
/**
* Return an instance of {@link StandardTypeComparator}.
*/
@Override
public TypeComparator getTypeComparator() {
return this.typeComparator;
}
/**
* Return an instance of {@link StandardOperatorOverloader}.
*/
@Override
public OperatorOverloader getOperatorOverloader() {
return this.operatorOverloader;
}
@Override
public void setVariable(String name, @Nullable Object value) {
this.variables.put(name, value);
}
@Override
@Nullable
public Object lookupVariable(String name) {
return this.variables.get(name);
}
}

View File

@ -36,13 +36,7 @@ syntax. In several places an Inventor and Inventor's Society class are used as t
target objects for expression evaluation. These class declarations and the data used to
populate them are listed at the end of the chapter.
[[expressions-features]]
== Feature overview
The expression language supports the following functionality
The expression language supports the following functionality:
* Literal expressions
* Boolean and relational operators
@ -68,7 +62,7 @@ The expression language supports the following functionality
[[expressions-evaluation]]
== Expression evaluation using Spring's Expression interface
== Evaluation
This section introduces the simple use of SpEL interfaces and its expression language.
The complete language reference can be found in the section
@ -156,10 +150,9 @@ result type. An `EvaluationException` will be thrown if the value cannot be cast
type `T` or converted using the registered type converter.
The more common usage of SpEL is to provide an expression string that is evaluated
against a specific object instance (called the root object). There are two options here
and which to choose depends on whether the object against which the expression is being
evaluated will be changing with each call to evaluate the expression. In the following
example we retrieve the `name` property from an instance of the Inventor class.
against a specific object instance (called the root object). The example shows
how to retrieve the `name` property from an instance of the `Inventor` class or
create a boolean condition:
[source,java,indent=0]
[subs="verbatim,quotes"]
@ -172,88 +165,38 @@ example we retrieve the `name` property from an instance of the Inventor class.
Inventor tesla = new Inventor("Nikola Tesla", c.getTime(), "Serbian");
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("**name**");
EvaluationContext context = new StandardEvaluationContext(tesla);
String name = (String) exp.getValue(context);
----
In the last line, the value of the string variable `name` will be set to "Nikola Tesla".
The class StandardEvaluationContext is where you can specify which object the "name"
property will be evaluated against. This is the mechanism to use if the root object is
unlikely to change, it can simply be set once in the evaluation context. If the root
object is likely to change repeatedly, it can be supplied on each call to `getValue`, as
this next example shows:
[source,java,indent=0]
[subs="verbatim,quotes"]
----
/ Create and set a 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**");
String name = (String) exp.getValue(tesla);
----
// name == "Nikola Tesla"
In this case the inventor `tesla` has been supplied directly to `getValue` and the
expression evaluation infrastructure creates and manages a default evaluation context
internally - it did not require one to be supplied.
The StandardEvaluationContext is relatively expensive to construct and during repeated
usage it builds up cached state that enables subsequent expression evaluations to be
performed more quickly. For this reason it is better to cache and reuse them where
possible, rather than construct a new one for each expression evaluation.
In some cases it can be desirable to use a configured evaluation context and yet still
supply a different root object on each call to `getValue`. `getValue` allows both to be
specified on the same call. In these situations the root object passed on the call is
considered to override any (which maybe null) specified on the evaluation context.
[NOTE]
====
In standalone usage of SpEL there is a need to create the parser, parse expressions and
perhaps provide evaluation contexts and a root context object. However, more common
usage is to provide only the SpEL expression string as part of a configuration file, for
example for Spring bean or Spring Web Flow definitions. In this case, the parser,
evaluation context, root object and any predefined variables are all set up implicitly,
requiring the user to specify nothing other than the expressions.
====
As a final introductory example, the use of a boolean operator is shown using the
Inventor object in the previous example.
[source,java,indent=0]
[subs="verbatim,quotes"]
----
Expression exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(context, Boolean.class); // evaluates to true
exp = parser.parseExpression("name == 'Nikola Tesla'");
boolean result = exp.getValue(tesla, Boolean.class);
// result == true
----
[[expressions-evaluation-context]]
=== The EvaluationContext interface
=== `EvaluationContext`
The interface `EvaluationContext` is used when evaluating an expression to resolve
properties, methods, fields, and to help perform type conversion. The out-of-the-box
implementation, `StandardEvaluationContext`, uses reflection to manipulate the object,
caching `java.lang.reflect.Method`, `java.lang.reflect.Field`, and
`java.lang.reflect.Constructor` instances for increased performance.
implementations, `SimpleEvalutationContext` and `StandardEvaluationContext`, use
reflection to manipulate the object, caching `java.lang.reflect.Method`,
`java.lang.reflect.Field`, and `java.lang.reflect.Constructor` instances for increased
performance.
The `StandardEvaluationContext` is where you may specify the root object to evaluate
against via the method `setRootObject()` or passing the root object into the
constructor. You can also specify variables and functions that will be used in the
expression using the methods `setVariable()` and `registerFunction()`. The use of
variables and functions are described in the language reference sections
<<expressions-ref-variables,Variables>> and <<expressions-ref-functions,Functions>>. The
`StandardEvaluationContext` is also where you can register custom
``ConstructorResolver``s, ``MethodResolver``s, and ``PropertyAccessor``s to extend how SpEL
evaluates expressions. Please refer to the javadoc of these classes for more details.
`SimpleEvaluationContext` exposes a subset of essential SpEL language features and
configuration options. Certain categories of expressions, do not require the full extent
of the SpEL language syntax and arguably should be meaningfully restricted. Examples
include but are not limited to data binding expressions, property-based filters, and
others. To effect, `SimpleEvaluationContext` supports a subset of the SpEL language syntax
that excludes references to Java types, constructors, and bean references.
`StandardEvaluationContext` exposes the full set of SpEL language features and
configuration options. You may use it to specify a default root object, and to configure
every available evaluation-related strategy.
[[expressions-type-conversion]]
@ -280,14 +223,14 @@ being placed in it. A simple example:
}
Simple simple = new Simple();
simple.booleanList.add(true);
StandardEvaluationContext simpleContext = new StandardEvaluationContext(simple);
SimpleEvaluationContext context = new SimpleEvaluationContext();
// false is passed in here as a string. SpEL and the conversion service will
// correctly recognize that it needs to be a Boolean and convert it
parser.parseExpression("booleanList[0]").setValue(simpleContext, "false");
parser.parseExpression("booleanList[0]").setValue(context, simple, "false");
// b will be false
Boolean b = simple.booleanList.get(0);
@ -446,7 +389,7 @@ More and more types of expression will be compilable in the future.
[[expressions-beandef]]
== Expression support for defining bean definitions
== Expressions in bean definitions
SpEL expressions can be used with XML or annotation-based configuration metadata for
defining ``BeanDefinition``s. In both cases the syntax to define the expression is of the
@ -455,7 +398,7 @@ form `#{ <expression string> }`.
[[expressions-beandef-xml-based]]
=== XML based configuration
=== XML configuration
A property or constructor-arg value can be set using expressions as shown below.
@ -504,7 +447,7 @@ You can also refer to other bean properties by name, for example.
[[expressions-beandef-annotation-based]]
=== Annotation-based configuration
=== Annotation config
The `@Value` annotation can be placed on fields, methods and method/constructor
parameters to specify a default value.
@ -659,25 +602,24 @@ arrays and lists are obtained using square bracket notation.
[subs="verbatim,quotes"]
----
ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = new SimpleEvaluationContext();
// Inventions Array
StandardEvaluationContext teslaContext = new StandardEvaluationContext(tesla);
// evaluates to "Induction motor"
String invention = parser.parseExpression("inventions[3]").getValue(
teslaContext, String.class);
context, tesla, String.class);
// Members List
StandardEvaluationContext societyContext = new StandardEvaluationContext(ieee);
// evaluates to "Nikola Tesla"
String name = parser.parseExpression("Members[0].Name").getValue(
societyContext, String.class);
context, ieee, String.class);
// List and Array navigation
// evaluates to "Wireless communication"
String invention = parser.parseExpression("Members[0].Inventions[6]").getValue(
societyContext, String.class);
context, ieee, String.class);
----
The contents of maps are obtained by specifying the literal key value within the
@ -943,14 +885,14 @@ done within a call to `setValue` but can also be done inside a call to `getValue
[subs="verbatim,quotes"]
----
Inventor inventor = new Inventor();
StandardEvaluationContext inventorContext = new StandardEvaluationContext(inventor);
SimpleEvaluationContext context = new SimpleEvaluationContext();
parser.parseExpression("Name").setValue(inventorContext, "Alexander Seovic2");
parser.parseExpression("Name").setValue(context, inventor, "Alexander Seovic2");
// alternatively
String aleks = parser.parseExpression(
"Name = 'Alexandar Seovic'").getValue(inventorContext, String.class);
"Name = 'Alexandar Seovic'").getValue(context, inventor, String.class);
----
@ -1005,16 +947,16 @@ used).
=== Variables
Variables can be referenced in the expression using the syntax `#variableName`. Variables
are set using the method setVariable on the `StandardEvaluationContext`.
are set using the method setVariable on `EvaluationContext` implementations.
[source,java,indent=0]
[subs="verbatim,quotes"]
----
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
SimpleEvaluationContext context = new SimpleEvaluationContext();
context.setVariable("newName", "Mike Tesla");
parser.parseExpression("Name = #newName").getValue(context);
parser.parseExpression("Name = #newName").getValue(context, tesla);
System.out.println(tesla.getName()) // "Mike Tesla"
----
@ -1037,7 +979,7 @@ an expression are evaluated, #root always refers to the root.
// create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
SimpleEvaluationContext context = new SimpleEvaluationContext();
context.setVariable("primes",primes);
// all prime numbers > 10 from the list (using selection ?{...})
@ -1052,17 +994,18 @@ an expression are evaluated, #root always refers to the root.
=== Functions
You can extend SpEL by registering user defined functions that can be called within the
expression string. The function is registered with the `StandardEvaluationContext` using
the method.
expression string. The function is registered through the `EvaluationContext`.
[source,java,indent=0]
[subs="verbatim,quotes"]
----
public void registerFunction(String name, Method m)
Method method = ...;
SimpleEvaluationContext context = new SimpleEvaluationContext();
context.setVariable("myFunction", method);
----
A reference to a Java Method provides the implementation of the function. For example, a
utility method to reverse a string is shown below.
For example, given a utility method to reverse a string is shown below:
[source,java,indent=0]
[subs="verbatim,quotes"]
@ -1079,16 +1022,15 @@ utility method to reverse a string is shown below.
}
----
This method is then registered with the evaluation context and can be used within an
expression string.
The above method can then be registered and used as follows:
[source,java,indent=0]
[subs="verbatim,quotes"]
----
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
SimpleEvaluationContext context = new SimpleEvaluationContext();
context.registerFunction("reverseString",
context.setVariable("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", String.class));
String helloWorldReversed = parser.parseExpression(
@ -1198,15 +1140,15 @@ Here is a more complex example.
ExpressionParser parser = new SpelExpressionParser();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
SimpleEvaluationContext context = new SimpleEvaluationContext();
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Nikola Tesla
tesla.setName(null);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, String.class);
name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Elvis Presley
----
@ -1230,14 +1172,14 @@ safe navigation operator will simply return null instead of throwing an exceptio
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
StandardEvaluationContext context = new StandardEvaluationContext(tesla);
SimpleEvaluationContext context = new SimpleEvaluationContext();
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan
tesla.setPlaceOfBirth(null);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, String.class);
city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // null - does not throw NullPointerException!!!
----