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; package org.springframework.expression.spel.support;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -38,15 +39,26 @@ import org.springframework.lang.Nullable;
/** /**
* A basic implementation of {@link EvaluationContext} that focuses on a subset * A basic implementation of {@link EvaluationContext} that focuses on a subset
* of essential SpEL features and configuration options, and relies on default * of essential SpEL features and configuration options.
* strategies otherwise.
* *
* <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 * required and should be meaningfully restricted. Examples include but are not
* limited to data binding expressions, property-based filters, and others. To * limited to data binding expressions, property-based filters, and others. To
* that effect, {@code SimpleEvaluationContext} supports only a subset of the * that effect, {@code SimpleEvaluationContext} is tailored to support only a
* SpEL language syntax that excludes references to Java types, constructors, * subset of the SpEL language syntax, e.g. excluding references to Java types,
* and bean references. * 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 * <p>Note that {@code SimpleEvaluationContext} cannot be configured with a
* default root object. Instead it is meant to be created once and used * 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 * @author Rossen Stoyanchev
* @since 4.3.15 * @since 4.3.15
* @see StandardEvaluationContext
* @see DataBindingPropertyAccessor
*/ */
public class SimpleEvaluationContext implements EvaluationContext { public class SimpleEvaluationContext implements EvaluationContext {
@ -66,11 +80,9 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final List<PropertyAccessor> propertyAccessors; private final List<PropertyAccessor> propertyAccessors;
private final List<ConstructorResolver> constructorResolvers = private final List<ConstructorResolver> constructorResolvers = Collections.emptyList();
Collections.singletonList(new ReflectiveConstructorResolver());
private final List<MethodResolver> methodResolvers = private final List<MethodResolver> methodResolvers = Collections.emptyList();
Collections.singletonList(new ReflectiveMethodResolver());
private final TypeConverter typeConverter; private final TypeConverter typeConverter;
@ -81,25 +93,12 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final Map<String, Object> variables = new HashMap<>(); private final Map<String, Object> variables = new HashMap<>();
public SimpleEvaluationContext() { private SimpleEvaluationContext(List<PropertyAccessor> accessors, @Nullable TypeConverter converter) {
this(null, null); this.propertyAccessors = Collections.unmodifiableList(new ArrayList<>(accessors));
}
public SimpleEvaluationContext(@Nullable List<PropertyAccessor> accessors, @Nullable TypeConverter converter) {
this.propertyAccessors = initPropertyAccessors(accessors);
this.typeConverter = converter != null ? converter : new StandardTypeConverter(); 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. * {@code SimpleEvaluationContext} cannot be configured with a root object.
* It is meant for repeated use with * 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 @Override
public List<ConstructorResolver> getConstructorResolvers() { public List<ConstructorResolver> getConstructorResolvers() {
@ -190,4 +190,76 @@ public class SimpleEvaluationContext implements EvaluationContext {
return this.variables.get(name); 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 Juergen Hoeller
* @author Sam Brannen * @author Sam Brannen
* @since 3.0 * @since 3.0
* @see SimpleEvaluationContext
* @see ReflectivePropertyAccessor * @see ReflectivePropertyAccessor
* @see ReflectiveConstructorResolver * @see ReflectiveConstructorResolver
* @see ReflectiveMethodResolver * @see ReflectiveMethodResolver

View File

@ -181,23 +181,30 @@ create a boolean condition:
=== `EvaluationContext` === `EvaluationContext`
The interface `EvaluationContext` is used when evaluating an expression to resolve 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 properties, methods, fields, and to help perform type conversion. There are two
implementations, `SimpleEvalutationContext` and `StandardEvaluationContext`, use out-of-the-box implementations.
reflection to manipulate the object, caching `java.lang.reflect.Method`,
`java.lang.reflect.Field`, and `java.lang.reflect.Constructor` instances for increased
performance.
`SimpleEvaluationContext` exposes a subset of essential SpEL language features and * `SimpleEvaluationContext` -- exposes a subset of essential SpEL language features and
configuration options. Certain categories of expressions, do not require the full extent configuration options, for categories of expressions that do not require the full extent
of the SpEL language syntax and arguably should be meaningfully restricted. Examples of the SpEL language syntax and should be meaningfully restricted. Examples
include but are not limited to data binding expressions, property-based filters, and 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 others.
that excludes references to Java types, constructors, and bean references.
`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 configuration options. You may use it to specify a default root object, and to configure
every available evaluation-related strategy. 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]] [[expressions-type-conversion]]
==== Type conversion ==== Type conversion
@ -225,7 +232,7 @@ being placed in it. A simple example:
Simple simple = new Simple(); Simple simple = new Simple();
simple.booleanList.add(true); 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 // 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 // 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"] [subs="verbatim,quotes"]
---- ----
ExpressionParser parser = new SpelExpressionParser(); ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = new SimpleEvaluationContext(); SimpleEvaluationContext context = SimpleEvaluationContext.create();
// Inventions Array // 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"] [subs="verbatim,quotes"]
---- ----
Inventor inventor = new Inventor(); Inventor inventor = new Inventor();
SimpleEvaluationContext context = new SimpleEvaluationContext(); SimpleEvaluationContext context = SimpleEvaluationContext.create();
parser.parseExpression("Name").setValue(context, inventor, "Alexander Seovic2"); parser.parseExpression("Name").setValue(context, inventor, "Alexander Seovic2");
@ -953,7 +960,7 @@ are set using the method setVariable on `EvaluationContext` implementations.
[subs="verbatim,quotes"] [subs="verbatim,quotes"]
---- ----
Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
SimpleEvaluationContext context = new SimpleEvaluationContext(); SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("newName", "Mike Tesla"); context.setVariable("newName", "Mike Tesla");
parser.parseExpression("Name = #newName").getValue(context, 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 // create parser and set variable 'primes' as the array of integers
ExpressionParser parser = new SpelExpressionParser(); ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = new SimpleEvaluationContext(); SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("primes",primes); context.setVariable("primes",primes);
// all prime numbers > 10 from the list (using selection ?{...}) // all prime numbers > 10 from the list (using selection ?{...})
@ -1001,7 +1008,7 @@ expression string. The function is registered through the `EvaluationContext`.
---- ----
Method method = ...; Method method = ...;
SimpleEvaluationContext context = new SimpleEvaluationContext(); SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("myFunction", method); context.setVariable("myFunction", method);
---- ----
@ -1028,7 +1035,7 @@ The above method can then be registered and used as follows:
[subs="verbatim,quotes"] [subs="verbatim,quotes"]
---- ----
ExpressionParser parser = new SpelExpressionParser(); ExpressionParser parser = new SpelExpressionParser();
SimpleEvaluationContext context = new SimpleEvaluationContext(); SimpleEvaluationContext context = SimpleEvaluationContext.create();
context.setVariable("reverseString", context.setVariable("reverseString",
StringUtils.class.getDeclaredMethod("reverseString", String.class)); StringUtils.class.getDeclaredMethod("reverseString", String.class));
@ -1049,7 +1056,7 @@ lookup beans from an expression using the (@) symbol.
[subs="verbatim,quotes"] [subs="verbatim,quotes"]
---- ----
ExpressionParser parser = new SpelExpressionParser(); ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(); StandardEvaluationContext context = StandardEvaluationContext.create();
context.setBeanResolver(new MyBeanResolver()); context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation // 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"] [subs="verbatim,quotes"]
---- ----
ExpressionParser parser = new SpelExpressionParser(); ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(); StandardEvaluationContext context = StandardEvaluationContext.create();
context.setBeanResolver(new MyBeanResolver()); context.setBeanResolver(new MyBeanResolver());
// This will end up calling resolve(context,"&foo") on MyBeanResolver during evaluation // 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(); ExpressionParser parser = new SpelExpressionParser();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); 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); 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"); Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan")); tesla.setPlaceOfBirth(new PlaceOfBirth("Smiljan"));
SimpleEvaluationContext context = new SimpleEvaluationContext(); SimpleEvaluationContext context = SimpleEvaluationContext.create();
String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class); String city = parser.parseExpression("PlaceOfBirth?.City").getValue(context, tesla, String.class);
System.out.println(city); // Smiljan System.out.println(city); // Smiljan