SimpleEvaluationContext.Builder withMethodResolvers/withInstanceMethods

Includes DataBindingMethodResolver as ReflectiveMethodResolver subclass.

Issue: SPR-16588
This commit is contained in:
Juergen Hoeller 2018-03-24 16:30:37 +01:00
parent a989ea0867
commit 9128226da4
6 changed files with 220 additions and 45 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -41,6 +41,11 @@ public interface EvaluationContext {
*/ */
TypedValue getRootObject(); 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. * Return a list of resolvers that will be asked in turn to locate a constructor.
*/ */
@ -52,9 +57,10 @@ public interface EvaluationContext {
List<MethodResolver> getMethodResolvers(); 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 * Return a type locator that can be used to find types, either by short or
@ -78,12 +84,6 @@ public interface EvaluationContext {
*/ */
OperatorOverloader getOperatorOverloader(); 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. * Set a named variable within this evaluation context to a specified value.
* @param name variable to set * @param name variable to set

View File

@ -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();
}
}

View File

@ -20,8 +20,6 @@ import java.lang.reflect.Method;
import java.lang.reflect.Modifier; import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy; import java.lang.reflect.Proxy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashSet; import java.util.LinkedHashSet;
@ -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) { public void registerMethodFilter(Class<?> type, @Nullable MethodFilter filter) {
if (this.filters == null) { if (this.filters == null) {
this.filters = new HashMap<>(); this.filters = new HashMap<>();
@ -94,7 +98,6 @@ public class ReflectiveMethodResolver implements MethodResolver {
} }
} }
/** /**
* Locate a method on a type. There are three kinds of match that might occur: * Locate a method on a type. There are three kinds of match that might occur:
* <ol> * <ol>
@ -112,13 +115,13 @@ public class ReflectiveMethodResolver implements MethodResolver {
try { try {
TypeConverter typeConverter = context.getTypeConverter(); TypeConverter typeConverter = context.getTypeConverter();
Class<?> type = (targetObject instanceof Class ? (Class<?>) targetObject : targetObject.getClass()); 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 // If a filter is registered for this type, call it
MethodFilter filter = (this.filters != null ? this.filters.get(type) : null); MethodFilter filter = (this.filters != null ? this.filters.get(type) : null);
if (filter != null) { if (filter != null) {
List<Method> filtered = filter.filter(methods); 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 // Sort methods into a sensible order
@ -126,7 +129,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
methods.sort((m1, m2) -> { methods.sort((m1, m2) -> {
int m1pl = m1.getParameterCount(); int m1pl = m1.getParameterCount();
int m2pl = m2.getParameterCount(); int m2pl = m2.getParameterCount();
// varargs methods go last // vararg methods go last
if (m1pl == m2pl) { if (m1pl == m2pl) {
if (!m1.isVarArgs() && m2.isVarArgs()) { if (!m1.isVarArgs() && m2.isVarArgs()) {
return -1; return -1;
@ -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) { if (targetObject instanceof Class) {
Set<Method> result = new LinkedHashSet<>(); Set<Method> result = new LinkedHashSet<>();
// Add these so that static methods are invocable on the type: e.g. Float.valueOf(..) // Add these so that static methods are invocable on the type: e.g. Float.valueOf(..)
@ -236,12 +239,24 @@ public class ReflectiveMethodResolver implements MethodResolver {
Set<Method> result = new LinkedHashSet<>(); Set<Method> result = new LinkedHashSet<>();
// Expose interface methods (not proxy-declared overrides) for proper vararg introspection // Expose interface methods (not proxy-declared overrides) for proper vararg introspection
for (Class<?> ifc : type.getInterfaces()) { 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; return result;
} }
else { 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 {
return type.getMethods(); 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;
}
} }

View File

@ -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 * <p>The default implementation considers any method as a candidate, even for
* non-user-declared properties on the {@link Object} base class. * non-user-declared properties on the {@link Object} base class.
* @param method the Method to evaluate * @param method the Method to evaluate

View File

@ -92,6 +92,8 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final List<PropertyAccessor> propertyAccessors; private final List<PropertyAccessor> propertyAccessors;
private final List<MethodResolver> methodResolvers;
private final TypeConverter typeConverter; private final TypeConverter typeConverter;
private final TypeComparator typeComparator = new StandardTypeComparator(); private final TypeComparator typeComparator = new StandardTypeComparator();
@ -101,8 +103,11 @@ public class SimpleEvaluationContext implements EvaluationContext {
private final Map<String, Object> variables = new HashMap<>(); 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.propertyAccessors = accessors;
this.methodResolvers = resolvers;
this.typeConverter = (converter != null ? converter : new StandardTypeConverter()); this.typeConverter = (converter != null ? converter : new StandardTypeConverter());
} }
@ -119,6 +124,10 @@ public class SimpleEvaluationContext implements EvaluationContext {
return TypedValue.NULL; return TypedValue.NULL;
} }
/**
* Return the specified {@link PropertyAccessor} delegates, if any.
* @see #forPropertyAccessors
*/
@Override @Override
public List<PropertyAccessor> getPropertyAccessors() { public List<PropertyAccessor> getPropertyAccessors() {
return this.propertyAccessors; return this.propertyAccessors;
@ -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 @Override
public List<MethodResolver> getMethodResolvers() { public List<MethodResolver> getMethodResolvers() {
return Collections.emptyList(); return this.methodResolvers;
} }
/** /**
* {@code SimpleEvaluationContext} does not support use of bean references. * {@code SimpleEvaluationContext} does not support the use of bean references.
* @return Always returns {@code null} * @return always {@code null}
*/ */
@Override @Override
@Nullable @Nullable
@ -164,6 +174,8 @@ public class SimpleEvaluationContext implements EvaluationContext {
/** /**
* The configured {@link TypeConverter}. * The configured {@link TypeConverter}.
* <p>By default this is {@link StandardTypeConverter}. * <p>By default this is {@link StandardTypeConverter}.
* @see Builder#withTypeConverter
* @see Builder#withConversionService
*/ */
@Override @Override
public TypeConverter getTypeConverter() { public TypeConverter getTypeConverter() {
@ -203,6 +215,7 @@ public class SimpleEvaluationContext implements EvaluationContext {
* delegates: typically a custom {@code PropertyAccessor} specific to a use case * delegates: typically a custom {@code PropertyAccessor} specific to a use case
* (e.g. attribute resolution in a custom data structure), potentially combined with * (e.g. attribute resolution in a custom data structure), potentially combined with
* a {@link DataBindingPropertyAccessor} if property dereferences are needed as well. * a {@link DataBindingPropertyAccessor} if property dereferences are needed as well.
* @param accessors the accessor delegates to use
* @see DataBindingPropertyAccessor#forReadOnlyAccess() * @see DataBindingPropertyAccessor#forReadOnlyAccess()
* @see DataBindingPropertyAccessor#forReadWriteAccess() * @see DataBindingPropertyAccessor#forReadWriteAccess()
*/ */
@ -242,13 +255,46 @@ public class SimpleEvaluationContext implements EvaluationContext {
*/ */
public static class Builder { public static class Builder {
private final List<PropertyAccessor> propertyAccessors; private final List<PropertyAccessor> accessors;
private List<MethodResolver> resolvers = Collections.emptyList();
@Nullable @Nullable
private TypeConverter typeConverter; private TypeConverter typeConverter;
public Builder(PropertyAccessor... accessors) { 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 {
} }
public SimpleEvaluationContext build() { public SimpleEvaluationContext build() {
return new SimpleEvaluationContext(this.propertyAccessors, this.typeConverter); return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter);
} }
} }

View File

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