Browse Source

SimpleEvaluationContext.Builder withMethodResolvers/withInstanceMethods

Includes DataBindingMethodResolver as ReflectiveMethodResolver subclass.

Issue: SPR-16588
pull/1737/merge
Juergen Hoeller 7 years ago
parent
commit
9128226da4
  1. 18
      spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java
  2. 76
      spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java
  3. 46
      spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java
  4. 3
      spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java
  5. 62
      spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java
  6. 60
      spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java

18
spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java

@ -1,5 +1,5 @@ @@ -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.
@ -41,6 +41,11 @@ public interface EvaluationContext { @@ -41,6 +41,11 @@ public interface EvaluationContext {
*/
TypedValue getRootObject();
/**
* Return a list of accessors that will be asked in turn to read/write a property.
*/
List<PropertyAccessor> getPropertyAccessors();
/**
* Return a list of resolvers that will be asked in turn to locate a constructor.
*/
@ -52,9 +57,10 @@ public interface EvaluationContext { @@ -52,9 +57,10 @@ public interface EvaluationContext {
List<MethodResolver> getMethodResolvers();
/**
* Return a list of accessors that will be asked in turn to read/write a property.
* Return a bean resolver that can look up beans by name.
*/
List<PropertyAccessor> getPropertyAccessors();
@Nullable
BeanResolver getBeanResolver();
/**
* Return a type locator that can be used to find types, either by short or
@ -78,12 +84,6 @@ public interface EvaluationContext { @@ -78,12 +84,6 @@ public interface EvaluationContext {
*/
OperatorOverloader getOperatorOverloader();
/**
* Return a bean resolver that can look up beans by name.
*/
@Nullable
BeanResolver getBeanResolver();
/**
* Set a named variable within this evaluation context to a specified value.
* @param name variable to set

76
spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java

@ -0,0 +1,76 @@ @@ -0,0 +1,76 @@
/*
* 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.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodExecutor;
import org.springframework.lang.Nullable;
/**
* A {@link org.springframework.expression.MethodResolver} variant for data binding
* purposes, using reflection to access instance methods on a given target object.
*
* <p>This accessor does not resolve static methods and also no technical methods
* on {@code java.lang.Object} or {@code java.lang.Class}.
* For unrestricted resolution, choose {@link ReflectiveMethodResolver} instead.
*
* @author Juergen Hoeller
* @since 4.3.15
* @see #forInstanceMethodInvocation()
* @see DataBindingPropertyAccessor
*/
public class DataBindingMethodResolver extends ReflectiveMethodResolver {
private DataBindingMethodResolver() {
super();
}
@Override
@Nullable
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name,
List<TypeDescriptor> argumentTypes) throws AccessException {
if (targetObject instanceof Class) {
throw new IllegalArgumentException("DataBindingMethodResolver does not support Class targets");
}
return super.resolve(context, targetObject, name, argumentTypes);
}
@Override
protected boolean isCandidateForInvocation(Method method, Class<?> targetClass) {
if (Modifier.isStatic(method.getModifiers())) {
return false;
}
Class<?> clazz = method.getDeclaringClass();
return (clazz != Object.class && clazz != Class.class && !ClassLoader.class.isAssignableFrom(targetClass));
}
/**
* Create a new data-binding method resolver for instance method resolution.
*/
public static DataBindingMethodResolver forInstanceMethodInvocation() {
return new DataBindingMethodResolver();
}
}

46
spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java

@ -20,8 +20,6 @@ import java.lang.reflect.Method; @@ -20,8 +20,6 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
@ -82,6 +80,12 @@ public class ReflectiveMethodResolver implements MethodResolver { @@ -82,6 +80,12 @@ public class ReflectiveMethodResolver implements MethodResolver {
}
/**
* Register a filter for methods on the given type.
* @param type the type to filter on
* @param filter the corresponding method filter,
* or {@code null} to clear any filter for the given type
*/
public void registerMethodFilter(Class<?> type, @Nullable MethodFilter filter) {
if (this.filters == null) {
this.filters = new HashMap<>();
@ -94,7 +98,6 @@ public class ReflectiveMethodResolver implements MethodResolver { @@ -94,7 +98,6 @@ public class ReflectiveMethodResolver implements MethodResolver {
}
}
/**
* Locate a method on a type. There are three kinds of match that might occur:
* <ol>
@ -112,13 +115,13 @@ public class ReflectiveMethodResolver implements MethodResolver { @@ -112,13 +115,13 @@ public class ReflectiveMethodResolver implements MethodResolver {
try {
TypeConverter typeConverter = context.getTypeConverter();
Class<?> type = (targetObject instanceof Class ? (Class<?>) targetObject : targetObject.getClass());
List<Method> methods = new ArrayList<>(getMethods(type, targetObject));
ArrayList<Method> methods = new ArrayList<>(getMethods(type, targetObject));
// If a filter is registered for this type, call it
MethodFilter filter = (this.filters != null ? this.filters.get(type) : null);
if (filter != null) {
List<Method> filtered = filter.filter(methods);
methods = (filtered instanceof ArrayList ? filtered : new ArrayList<>(filtered));
methods = (filtered instanceof ArrayList ? (ArrayList<Method>) filtered : new ArrayList<>(filtered));
}
// Sort methods into a sensible order
@ -126,7 +129,7 @@ public class ReflectiveMethodResolver implements MethodResolver { @@ -126,7 +129,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
methods.sort((m1, m2) -> {
int m1pl = m1.getParameterCount();
int m2pl = m2.getParameterCount();
// varargs methods go last
// vararg methods go last
if (m1pl == m2pl) {
if (!m1.isVarArgs() && m2.isVarArgs()) {
return -1;
@ -218,7 +221,7 @@ public class ReflectiveMethodResolver implements MethodResolver { @@ -218,7 +221,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
}
}
private Collection<Method> getMethods(Class<?> type, Object targetObject) {
private Set<Method> getMethods(Class<?> type, Object targetObject) {
if (targetObject instanceof Class) {
Set<Method> result = new LinkedHashSet<>();
// Add these so that static methods are invocable on the type: e.g. Float.valueOf(..)
@ -236,12 +239,24 @@ public class ReflectiveMethodResolver implements MethodResolver { @@ -236,12 +239,24 @@ public class ReflectiveMethodResolver implements MethodResolver {
Set<Method> result = new LinkedHashSet<>();
// Expose interface methods (not proxy-declared overrides) for proper vararg introspection
for (Class<?> ifc : type.getInterfaces()) {
Collections.addAll(result, getMethods(ifc));
Method[] methods = getMethods(ifc);
for (Method method : methods) {
if (isCandidateForInvocation(method, type)) {
result.add(method);
}
}
}
return result;
}
else {
return Arrays.asList(getMethods(type));
Set<Method> result = new LinkedHashSet<>();
Method[] methods = getMethods(type);
for (Method method : methods) {
if (isCandidateForInvocation(method, type)) {
result.add(method);
}
}
return result;
}
}
@ -257,4 +272,17 @@ public class ReflectiveMethodResolver implements MethodResolver { @@ -257,4 +272,17 @@ public class ReflectiveMethodResolver implements MethodResolver {
return type.getMethods();
}
/**
* Determine whether the given {@code Method} is a candidate for method resolution
* on an instance of the given target class.
* <p>The default implementation considers any method as a candidate, even for
* static methods sand non-user-declared methods 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 isCandidateForInvocation(Method method, Class<?> targetClass) {
return true;
}
}

3
spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java

@ -421,7 +421,8 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { @@ -421,7 +421,8 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
}
/**
* Determine whether the given {@code Method} is a candidate for property access.
* Determine whether the given {@code Method} is a candidate for property access
* on an instance of the given target class.
* <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

62
spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java

@ -92,6 +92,8 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -92,6 +92,8 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final List<PropertyAccessor> propertyAccessors;
private final List<MethodResolver> methodResolvers;
private final TypeConverter typeConverter;
private final TypeComparator typeComparator = new StandardTypeComparator();
@ -101,8 +103,11 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -101,8 +103,11 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final Map<String, Object> variables = new HashMap<>();
private SimpleEvaluationContext(List<PropertyAccessor> accessors, @Nullable TypeConverter converter) {
private SimpleEvaluationContext(List<PropertyAccessor> accessors, List<MethodResolver> resolvers,
@Nullable TypeConverter converter) {
this.propertyAccessors = accessors;
this.methodResolvers = resolvers;
this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
}
@ -119,6 +124,10 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -119,6 +124,10 @@ public class SimpleEvaluationContext implements EvaluationContext {
return TypedValue.NULL;
}
/**
* Return the specified {@link PropertyAccessor} delegates, if any.
* @see #forPropertyAccessors
*/
@Override
public List<PropertyAccessor> getPropertyAccessors() {
return this.propertyAccessors;
@ -134,16 +143,17 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -134,16 +143,17 @@ public class SimpleEvaluationContext implements EvaluationContext {
}
/**
* Return a single {@link ReflectiveMethodResolver}.
* Return the specified {@link MethodResolver} delegates, if any.
* @see Builder#withMethodResolvers
*/
@Override
public List<MethodResolver> getMethodResolvers() {
return Collections.emptyList();
return this.methodResolvers;
}
/**
* {@code SimpleEvaluationContext} does not support use of bean references.
* @return Always returns {@code null}
* {@code SimpleEvaluationContext} does not support the use of bean references.
* @return always {@code null}
*/
@Override
@Nullable
@ -164,6 +174,8 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -164,6 +174,8 @@ public class SimpleEvaluationContext implements EvaluationContext {
/**
* The configured {@link TypeConverter}.
* <p>By default this is {@link StandardTypeConverter}.
* @see Builder#withTypeConverter
* @see Builder#withConversionService
*/
@Override
public TypeConverter getTypeConverter() {
@ -203,6 +215,7 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -203,6 +215,7 @@ public class SimpleEvaluationContext implements EvaluationContext {
* delegates: typically a custom {@code PropertyAccessor} specific to a use case
* (e.g. attribute resolution in a custom data structure), potentially combined with
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
* @param accessors the accessor delegates to use
* @see DataBindingPropertyAccessor#forReadOnlyAccess()
* @see DataBindingPropertyAccessor#forReadWriteAccess()
*/
@ -242,13 +255,46 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -242,13 +255,46 @@ public class SimpleEvaluationContext implements EvaluationContext {
*/
public static class Builder {
private final List<PropertyAccessor> propertyAccessors;
private final List<PropertyAccessor> accessors;
private List<MethodResolver> resolvers = Collections.emptyList();
@Nullable
private TypeConverter typeConverter;
public Builder(PropertyAccessor... accessors) {
this.propertyAccessors = Arrays.asList(accessors);
this.accessors = Arrays.asList(accessors);
}
/**
* Register the specified {@link MethodResolver} delegates for
* a combination of property access and method resolution.
* @param resolvers the resolver delegates to use
* @see #withInstanceMethods()
* @see SimpleEvaluationContext#forPropertyAccessors
*/
public Builder withMethodResolvers(MethodResolver... resolvers) {
for (MethodResolver resolver : resolvers) {
if (resolver.getClass() == ReflectiveMethodResolver.class) {
throw new IllegalArgumentException("SimpleEvaluationContext is not designed for use with a plain " +
"ReflectiveMethodResolver. Consider using DataBindingMethodResolver or a custom subclass.");
}
}
this.resolvers = Arrays.asList(resolvers);
return this;
}
/**
* Register a {@link DataBindingMethodResolver} for instance method invocation purposes
* (i.e. not supporting static methods) in addition to the specified property accessors,
* typically in combination with a {@link DataBindingPropertyAccessor}.
* @see #withMethodResolvers
* @see SimpleEvaluationContext#forReadOnlyDataBinding()
* @see SimpleEvaluationContext#forReadWriteDataBinding()
*/
public Builder withInstanceMethods() {
this.resolvers = Collections.singletonList(DataBindingMethodResolver.forInstanceMethodInvocation());
return this;
}
/**
@ -276,7 +322,7 @@ public class SimpleEvaluationContext implements EvaluationContext { @@ -276,7 +322,7 @@ public class SimpleEvaluationContext implements EvaluationContext {
}
public SimpleEvaluationContext build() {
return new SimpleEvaluationContext(this.propertyAccessors, this.typeConverter);
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter);
}
}

60
spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java

@ -32,7 +32,7 @@ import org.springframework.expression.PropertyAccessor; @@ -32,7 +32,7 @@ import org.springframework.expression.PropertyAccessor;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.DataBindingPropertyAccessor;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.testresources.Person;
@ -186,22 +186,38 @@ public class PropertyAccessTests extends AbstractExpressionTests { @@ -186,22 +186,38 @@ public class PropertyAccessTests extends AbstractExpressionTests {
@Test
public void standardGetClassAccess() {
Expression expr = parser.parseExpression("'a'.class.getName()");
assertEquals(String.class.getName(), expr.getValue());
assertEquals(String.class.getName(), parser.parseExpression("'a'.class.name").getValue());
}
@Test(expected = SpelEvaluationException.class)
public void noGetClassAccess() {
Expression expr = parser.parseExpression("'a'.class.getName()");
StandardEvaluationContext context = new StandardEvaluationContext();
context.setPropertyAccessors(Collections.singletonList(DataBindingPropertyAccessor.forReadWriteAccess()));
expr.getValue(context);
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
parser.parseExpression("'a'.class.name").getValue(context);
}
@Test
public void propertyReadOnly() {
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression expr = parser.parseExpression("name");
Person target = new Person("p1");
assertEquals("p1", expr.getValue(context, target));
target.setName("p2");
assertEquals("p2", expr.getValue(context, target));
try {
parser.parseExpression("name='p3'").getValue(context, target);
fail("Should have thrown SpelEvaluationException");
}
catch (SpelEvaluationException ex) {
// expected
}
}
@Test
public void propertyReadWrite() {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setPropertyAccessors(Collections.singletonList(DataBindingPropertyAccessor.forReadWriteAccess()));
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
Expression expr = parser.parseExpression("name");
Person target = new Person("p1");
@ -218,18 +234,26 @@ public class PropertyAccessTests extends AbstractExpressionTests { @@ -218,18 +234,26 @@ public class PropertyAccessTests extends AbstractExpressionTests {
assertEquals("p4", expr.getValue(context, target));
}
@Test(expected = SpelEvaluationException.class)
public void propertyReadOnly() {
StandardEvaluationContext context = new StandardEvaluationContext();
context.setPropertyAccessors(Collections.singletonList(DataBindingPropertyAccessor.forReadOnlyAccess()));
@Test
public void propertyAccessWithoutMethodResolver() {
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression expr = parser.parseExpression("name");
Person target = new Person("p1");
assertEquals("p1", expr.getValue(context, target));
target.setName("p2");
assertEquals("p2", expr.getValue(context, target));
try {
parser.parseExpression("name.substring(1)").getValue(context, target);
fail("Should have thrown SpelEvaluationException");
}
catch (SpelEvaluationException ex) {
// expected
}
}
parser.parseExpression("name='p3'").getValue(context, target);
@Test
public void propertyAccessWithInstanceMethodResolver() {
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build();
Person target = new Person("p1");
assertEquals("1", parser.parseExpression("name.substring(1)").getValue(context, target));
}

Loading…
Cancel
Save