SimpleEvaluationContext with dedicated factory methods for common cases

Aligned with DataBindingPropertyAccessor and shown in ref doc examples.

Issue: SPR-16588
This commit is contained in:
Juergen Hoeller
2018-03-22 18:09:27 +01:00
parent 025ee83403
commit 51c57d77d9
8 changed files with 154 additions and 152 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -57,13 +57,13 @@ public enum SpelMessage {
"Property or field ''{0}'' cannot be found on null"),
PROPERTY_OR_FIELD_NOT_READABLE(Kind.ERROR, 1008,
"Property or field ''{0}'' cannot be found on object of type ''{1}'' - maybe not public?"),
"Property or field ''{0}'' cannot be found on object of type ''{1}'' - maybe not public or not valid?"),
PROPERTY_OR_FIELD_NOT_WRITABLE_ON_NULL(Kind.ERROR, 1009,
"Property or field ''{0}'' cannot be set on null"),
PROPERTY_OR_FIELD_NOT_WRITABLE(Kind.ERROR, 1010,
"Property or field ''{0}'' cannot be set on object of type ''{1}'' - maybe not public?"),
"Property or field ''{0}'' cannot be set on object of type ''{1}'' - maybe not public or not writable?"),
METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED(Kind.ERROR, 1011,
"Method call: Attempted to call method {0} on null context object"),

View File

@@ -22,12 +22,12 @@ import java.lang.reflect.Method;
* A {@link org.springframework.expression.PropertyAccessor} variant for data binding
* purposes, using reflection to access properties for reading and possibly writing.
*
* <p>A property can be accessed through a public getter method (when being read)
* <p>A property can be referenced through a public getter method (when being read)
* or a public setter method (when being written), and also as a public field.
*
* <p>This accessor is explicitly designed for user-level property evaluation
* and does not resolve technical properties on {@code java.lang.Object}.
* For more resolution power, choose {@link ReflectivePropertyAccessor} instead.
* <p>This accessor is explicitly designed for user-declared properties and does not
* resolve technical properties on {@code java.lang.Object} or {@code java.lang.Class}.
* For unrestricted resolution, choose {@link ReflectivePropertyAccessor} instead.
*
* @author Juergen Hoeller
* @since 4.3.15
@@ -49,20 +49,21 @@ public class DataBindingPropertyAccessor extends ReflectivePropertyAccessor {
}
@Override
protected boolean isCandidateForProperty(Method method) {
return (method.getDeclaringClass() != Object.class);
protected boolean isCandidateForProperty(Method method, Class<?> targetClass) {
Class<?> clazz = method.getDeclaringClass();
return (clazz != Object.class && clazz != Class.class && !ClassLoader.class.isAssignableFrom(targetClass));
}
/**
* Create a new data-binding property accessor for read-only access.
* Create a new data-binding property accessor for read-only operations.
*/
public static DataBindingPropertyAccessor forReadOnlyAccess() {
return new DataBindingPropertyAccessor(false);
}
/**
* Create a new data-binding property accessor for read-write access.
* Create a new data-binding property accessor for read-write operations.
*/
public static DataBindingPropertyAccessor forReadWriteAccess() {
return new DataBindingPropertyAccessor(true);

View File

@@ -48,7 +48,7 @@ import org.springframework.util.StringUtils;
* A powerful {@link PropertyAccessor} that uses reflection to access properties
* for reading and possibly also for writing.
*
* <p>A property can be accessed through a public getter method (when being read)
* <p>A property can be referenced through a public getter method (when being read)
* or a public setter method (when being written), and also as a public field.
*
* @author Andy Clement
@@ -409,7 +409,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
Method[] methods = getSortedClassMethods(clazz);
for (String methodSuffix : methodSuffixes) {
for (Method method : methods) {
if (isCandidateForProperty(method) && method.getName().equals(prefix + methodSuffix) &&
if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) &&
method.getParameterCount() == numberOfParams &&
(!mustBeStatic || Modifier.isStatic(method.getModifiers())) &&
(requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) {
@@ -425,9 +425,10 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
* <p>The default implementation considers any method as a candidate, even for
* non-user-declared properties on the {@link Object} base class.
* @param method the Method to evaluate
* @param targetClass the concrete target class that is being introspected
* @since 4.3.15
*/
protected boolean isCandidateForProperty(Method method) {
protected boolean isCandidateForProperty(Method method, Class<?> targetClass) {
return true;
}
@@ -518,25 +519,25 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
if (target == null) {
return this;
}
Class<?> type = (target instanceof Class ? (Class<?>) target : target.getClass());
if (type.isArray()) {
Class<?> clazz = (target instanceof Class ? (Class<?>) target : target.getClass());
if (clazz.isArray()) {
return this;
}
PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class);
PropertyCacheKey cacheKey = new PropertyCacheKey(clazz, name, target instanceof Class);
InvokerPair invocationTarget = this.readerCache.get(cacheKey);
if (invocationTarget == null || invocationTarget.member instanceof Method) {
Method method = (Method) (invocationTarget != null ? invocationTarget.member : null);
if (method == null) {
method = findGetterForProperty(name, type, target);
method = findGetterForProperty(name, clazz, target);
if (method != null) {
invocationTarget = new InvokerPair(method, new TypeDescriptor(new MethodParameter(method, -1)));
ReflectionUtils.makeAccessible(method);
this.readerCache.put(cacheKey, invocationTarget);
}
}
if (method != null && isCandidateForProperty(method)) {
if (method != null) {
return new OptimalPropertyAccessor(invocationTarget);
}
}
@@ -544,7 +545,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
if (invocationTarget == null || invocationTarget.member instanceof Field) {
Field field = (invocationTarget != null ? (Field) invocationTarget.member : null);
if (field == null) {
field = findField(name, type, target instanceof Class);
field = findField(name, clazz, target instanceof Class);
if (field != null) {
invocationTarget = new InvokerPair(field, new TypeDescriptor(field));
ReflectionUtils.makeAccessible(field);

View File

@@ -16,13 +16,13 @@
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;
import java.util.Map;
import org.springframework.core.convert.ConversionService;
import org.springframework.expression.BeanResolver;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
@@ -41,34 +41,42 @@ import org.springframework.lang.Nullable;
* A basic implementation of {@link EvaluationContext} that focuses on a subset
* of essential SpEL features and configuration options.
*
* <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} is tailored to support only a
* subset of the SpEL language syntax, e.g. excluding references to Java types,
* constructors, and bean references.
* <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} 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:
* of support you need to deal with properties and methods in SpEL expressions:
* <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>
* <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
* repeatedly through method variants on
* {@link org.springframework.expression.Expression Expression} that accept
* both an {@code EvaluationContext} and a root object.
* <p>Conveniently, {@link SimpleEvaluationContext#forReadOnlyDataBinding()}
* enables read access to properties via {@link DataBindingPropertyAccessor};
* same for {@link SimpleEvaluationContext#forReadWriteDataBinding()} when
* write access is needed as well. Alternatively, configure custom accessors
* via {@link SimpleEvaluationContext#forPropertyAccessors}.
*
* <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 {@code getValue} calls on a pre-compiled
* {@link org.springframework.expression.Expression} with both an
* {@code EvaluationContext} and a root object as arguments
*
* <p>For more flexibility, consider {@link StandardEvaluationContext} instead.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 4.3.15
* @see #forReadOnlyDataBinding()
* @see #forReadWriteDataBinding()
* @see StandardEvaluationContext
* @see StandardTypeConverter
* @see DataBindingPropertyAccessor
*/
public class SimpleEvaluationContext implements EvaluationContext {
@@ -80,10 +88,6 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final List<PropertyAccessor> propertyAccessors;
private final List<ConstructorResolver> constructorResolvers = Collections.emptyList();
private final List<MethodResolver> methodResolvers = Collections.emptyList();
private final TypeConverter typeConverter;
private final TypeComparator typeComparator = new StandardTypeComparator();
@@ -94,8 +98,8 @@ public class SimpleEvaluationContext implements EvaluationContext {
private SimpleEvaluationContext(List<PropertyAccessor> accessors, @Nullable TypeConverter converter) {
this.propertyAccessors = Collections.unmodifiableList(new ArrayList<>(accessors));
this.typeConverter = converter != null ? converter : new StandardTypeConverter();
this.propertyAccessors = accessors;
this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
}
@@ -122,7 +126,7 @@ public class SimpleEvaluationContext implements EvaluationContext {
*/
@Override
public List<ConstructorResolver> getConstructorResolvers() {
return this.constructorResolvers;
return Collections.emptyList();
}
/**
@@ -130,7 +134,7 @@ public class SimpleEvaluationContext implements EvaluationContext {
*/
@Override
public List<MethodResolver> getMethodResolvers() {
return this.methodResolvers;
return Collections.emptyList();
}
/**
@@ -170,7 +174,6 @@ public class SimpleEvaluationContext implements EvaluationContext {
return this.typeComparator;
}
/**
* Return an instance of {@link StandardOperatorOverloader}.
*/
@@ -192,26 +195,31 @@ public class SimpleEvaluationContext implements EvaluationContext {
/**
* 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()
* Create a {@code SimpleEvaluationContext} for the specified
* {@link PropertyAccessor} delegates.
* @see ReflectivePropertyAccessor
* @see DataBindingPropertyAccessor
*/
public static SimpleEvaluationContext create() {
return new Builder().dataBindingPropertyAccessor(true).build();
public static Builder forPropertyAccessors(PropertyAccessor... accessors) {
return new Builder(accessors);
}
/**
* Return a builder to create a {@code SimpleEvaluationContext}.
* @see #create()
* Create a {@code SimpleEvaluationContext} for read-only access to
* public properties via {@link DataBindingPropertyAccessor}.
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
*/
public static Builder builder() {
return new Builder();
public static Builder forReadOnlyDataBinding() {
return new Builder(DataBindingPropertyAccessor.forReadOnlyAccess());
}
/**
* Create a {@code SimpleEvaluationContext} for read-write access to
* public properties via {@link DataBindingPropertyAccessor}.
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
*/
public static Builder forReadWriteDataBinding() {
return new Builder(DataBindingPropertyAccessor.forReadWriteAccess());
}
@@ -220,43 +228,41 @@ public class SimpleEvaluationContext implements EvaluationContext {
*/
public static class Builder {
private final List<PropertyAccessor> propertyAccessors = new ArrayList<>();
private final List<PropertyAccessor> propertyAccessors;
@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;
public Builder(PropertyAccessor... accessors) {
this.propertyAccessors = Arrays.asList(accessors);
}
/**
* Register a custom {@link TypeConverter}.
* <p>By default {@link StandardTypeConverter} is used.
* <p>By default a {@link StandardTypeConverter} backed by a
* {@link org.springframework.core.convert.support.DefaultConversionService}
* is used.
* @see #withConversionService
* @see StandardTypeConverter#StandardTypeConverter()
*/
public Builder typeConverter(TypeConverter converter) {
public Builder withTypeConverter(TypeConverter converter) {
this.typeConverter = converter;
return this;
}
/**
* Register a custom {@link ConversionService}.
* <p>By default a {@link StandardTypeConverter} backed by a
* {@link org.springframework.core.convert.support.DefaultConversionService}
* is used.
* @see #withTypeConverter
* @see StandardTypeConverter#StandardTypeConverter(ConversionService)
*/
public Builder withConversionService(ConversionService conversionService) {
this.typeConverter = new StandardTypeConverter(conversionService);
return this;
}
public SimpleEvaluationContext build() {
return new SimpleEvaluationContext(this.propertyAccessors, this.typeConverter);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -42,6 +42,7 @@ public class StandardTypeConverter implements TypeConverter {
/**
* Create a StandardTypeConverter for the default ConversionService.
* @see DefaultConversionService#getSharedInstance()
*/
public StandardTypeConverter() {
this.conversionService = DefaultConversionService.getSharedInstance();

View File

@@ -87,7 +87,7 @@ public class PropertyAccessTests extends AbstractExpressionTests {
}
assertFalse(expr.isWritable(context));
try {
expr.setValue(context,"abc");
expr.setValue(context, "abc");
fail("Should have failed - default property resolver cannot resolve on null");
}
catch (Exception ex) {
@@ -95,13 +95,13 @@ public class PropertyAccessTests extends AbstractExpressionTests {
}
}
private void checkException(Exception e, SpelMessage expectedMessage) {
if (e instanceof SpelEvaluationException) {
SpelMessage sm = ((SpelEvaluationException)e).getMessageCode();
assertEquals("Expected exception type did not occur",expectedMessage,sm);
private void checkException(Exception ex, SpelMessage expectedMessage) {
if (ex instanceof SpelEvaluationException) {
SpelMessage sm = ((SpelEvaluationException) ex).getMessageCode();
assertEquals("Expected exception type did not occur", expectedMessage, sm);
}
else {
fail("Should be a SpelException "+e);
fail("Should be a SpelException " + ex);
}
}
@@ -210,7 +210,12 @@ public class PropertyAccessTests extends AbstractExpressionTests {
assertEquals("p2", expr.getValue(context, target));
parser.parseExpression("name='p3'").getValue(context, target);
assertEquals("p3", target.getName());
assertEquals("p3", expr.getValue(context, target));
expr.setValue(context, target, "p4");
assertEquals("p4", target.getName());
assertEquals("p4", expr.getValue(context, target));
}
@Test(expected = SpelEvaluationException.class)