SimpleEvaluationContext builder

Issue: SPR-16588
This commit is contained in:
Rossen Stoyanchev 2018-03-16 10:40:22 -04:00
parent b5511645b8
commit 19293b9847
3 changed files with 128 additions and 48 deletions

View File

@ -17,6 +17,7 @@
package org.springframework.expression.spel.support;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -38,15 +39,26 @@ 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.
* of essential SpEL features and configuration options.
*
* <p>In many cases, the full extent of the SpEL is not
* <p>In many cases, the full extent of the SpEL language 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.
* that effect, {@code SimpleEvaluationContext} is tailored to support only a
* subset of the SpEL language syntax, e.g. excluding references to Java types,
* constructors, and bean references.
*
* <p>When creating {@code SimpleEvaluationContext} you need to choose the level
* of support you need to deal with properties and methods in SpEL expressions.
* By default, {@link SimpleEvaluationContext#create()} enables only read access
* to properties via {@link DataBindingPropertyAccessor}. Alternatively, use
* {@link SimpleEvaluationContext#builder()} to configure the exact level of
* support needed, targeting one of, or some combination of the following:
* <ul>
* <li>Custom {@code PropertyAccessor} only (no reflection).</li>
* <li>Data binding properties for read-only access.</li>
* <li>Data binding properties for read and write.</li>
* </ul>
*
* <p>Note that {@code SimpleEvaluationContext} cannot be configured with a
* default root object. Instead it is meant to be created once and used
@ -56,6 +68,8 @@ import org.springframework.lang.Nullable;
*
* @author Rossen Stoyanchev
* @since 4.3.15
* @see StandardEvaluationContext
* @see DataBindingPropertyAccessor
*/
public class SimpleEvaluationContext implements EvaluationContext {
@ -66,11 +80,9 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final List<PropertyAccessor> propertyAccessors;
private final List<ConstructorResolver> constructorResolvers =
Collections.singletonList(new ReflectiveConstructorResolver());
private final List<ConstructorResolver> constructorResolvers = Collections.emptyList();
private final List<MethodResolver> methodResolvers =
Collections.singletonList(new ReflectiveMethodResolver());
private final List<MethodResolver> methodResolvers = Collections.emptyList();
private final TypeConverter typeConverter;
@ -81,25 +93,12 @@ public class SimpleEvaluationContext implements EvaluationContext {
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);
private SimpleEvaluationContext(List<PropertyAccessor> accessors, @Nullable TypeConverter converter) {
this.propertyAccessors = Collections.unmodifiableList(new ArrayList<>(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
@ -118,7 +117,8 @@ public class SimpleEvaluationContext implements EvaluationContext {
}
/**
* Return a single {@link ReflectiveConstructorResolver}.
* Return an empty list, always, since this context does not support the
* use of type references.
*/
@Override
public List<ConstructorResolver> getConstructorResolvers() {
@ -190,4 +190,76 @@ public class SimpleEvaluationContext implements EvaluationContext {
return this.variables.get(name);
}
/**
* Create a {@code SimpleEvaluationContext} with read-only access to
* public properties via {@link DataBindingPropertyAccessor}.
* <p>Effectively, a shortcut for:
* <pre class="code">
* SimpleEvaluationContext context = SimpleEvaluationContext.builder()
* .dataBindingPropertyAccessor(true)
* .build();
* </pre>
* @see #builder()
*/
public static SimpleEvaluationContext create() {
return new Builder().dataBindingPropertyAccessor(true).build();
}
/**
* Return a builder to create a {@code SimpleEvaluationContext}.
* @see #create()
*/
public static Builder builder() {
return new Builder();
}
/**
* Builder for {@code SimpleEvaluationContext}.
*/
public static class Builder {
private final List<PropertyAccessor> propertyAccessors = new ArrayList<>();
@Nullable
private TypeConverter typeConverter;
/**
* Enable access to public properties for data binding purposes.
* <p>Effectively, a shortcut for
* {@code propertyAccessor(new DataBindingPropertyAccessor(boolean))}.
* @param readOnlyAccess whether to read-only access to properties,
* {@code "true"}, or read and write, {@code "false"}.
*/
public Builder dataBindingPropertyAccessor(boolean readOnlyAccess) {
return propertyAccessor(readOnlyAccess ?
DataBindingPropertyAccessor.forReadOnlyAccess() :
DataBindingPropertyAccessor.forReadWriteAccess());
}
/**
* Register a custom accessor for properties in expressions.
* <p>By default, the builder does not enable property access.
*/
public Builder propertyAccessor(PropertyAccessor... accessors) {
this.propertyAccessors.addAll(Arrays.asList(accessors));
return this;
}
/**
* Register a custom {@link TypeConverter}.
* <p>By default {@link StandardTypeConverter} is used.
*/
public Builder typeConverter(TypeConverter converter) {
this.typeConverter = converter;
return this;
}
public SimpleEvaluationContext build() {
return new SimpleEvaluationContext(this.propertyAccessors, this.typeConverter);
}
}
}

View File

@ -47,6 +47,7 @@ import org.springframework.util.Assert;
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
* @see SimpleEvaluationContext
* @see ReflectivePropertyAccessor
* @see ReflectiveConstructorResolver
* @see ReflectiveMethodResolver

View File

@ -181,23 +181,30 @@ create a boolean condition:
=== `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
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.
properties, methods, fields, and to help perform type conversion. There are two
out-of-the-box implementations.
`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
* `SimpleEvaluationContext` -- exposes a subset of essential SpEL language features and
configuration options, for categories of expressions that do not require the full extent
of the SpEL language syntax and 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.
others.
`StandardEvaluationContext` exposes the full set of SpEL language features and
* `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.
`SimpleEvaluationContext` is designed to support only a subset of the SpEL language syntax.
It excludes Java type references, constructors, and bean references. It also requires
explicit choosing the level of support for properties and methods in expressions.
By default, the `create()` static factory method enables only read access to properties.
You can also obtain a builder to configure the exact level of support needed, targeting
one of, or some combination of the following:
. Custom {@code PropertyAccessor} only (no reflection).
. Data binding properties for read-only access.
. Data binding properties for read and write.
[[expressions-type-conversion]]
==== Type conversion
@ -225,7 +232,7 @@ being placed in it. A simple example:
Simple simple = new Simple();
simple.booleanList.add(true);
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext().create();
// 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
@ -602,7 +609,7 @@ arrays and lists are obtained using square bracket notation.
[subs="verbatim,quotes"]
----
ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
// Inventions Array
@ -885,7 +892,7 @@ done within a call to `setValue` but can also be done inside a call to `getValue
[subs="verbatim,quotes"]
----
Inventor inventor = new Inventor();
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
parser.parseExpression("Name").setValue(context, inventor, "Alexander Seovic2");
@ -953,7 +960,7 @@ are set using the method setVariable on `EvaluationContext` implementations.
[subs="verbatim,quotes"]
----
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("newName", "Mike Tesla");
parser.parseExpression("Name = #newName").getValue(context, tesla);
@ -979,7 +986,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();
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("primes",primes);
// all prime numbers > 10 from the list (using selection ?{...})
@ -1001,7 +1008,7 @@ expression string. The function is registered through the `EvaluationContext`.
----
Method method = ...;
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("myFunction", method);
----
@ -1028,7 +1035,7 @@ The above method can then be registered and used as follows:
[subs="verbatim,quotes"]
----
ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", String.class));
@ -1049,7 +1056,7 @@ lookup beans from an expression using the (@) symbol.
[subs="verbatim,quotes"]
----
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
StandardEvaluationContext context = StandardEvaluationContext.create();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation
@ -1062,7 +1069,7 @@ To access a factory bean itself, the bean name should instead be prefixed with a
[subs="verbatim,quotes"]
----
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
StandardEvaluationContext context = StandardEvaluationContext.create();
context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation
@ -1140,7 +1147,7 @@ Here is a more complex example.
ExpressionParser parser = new SpelExpressionParser();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
String name = parser.parseExpression("Name?:'Elvis Presley'").getValue(context, tesla, String.class);
@ -1172,7 +1179,7 @@ safe navigation operator will simply return null instead of throwing an exceptio
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
SimpleEvaluationContext context = new SimpleEvaluationContext();
SimpleEvaluationContext context = SimpleEvaluationContext.create();
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan