SimpleEvaluationContext.Builder withMethodResolvers/withInstanceMethods
Includes DataBindingMethodResolver as ReflectiveMethodResolver subclass. Issue: SPR-16588
This commit is contained in:
parent
a989ea0867
commit
9128226da4
|
@ -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 {
|
|||
*/
|
||||
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 {
|
|||
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 {
|
|||
*/
|
||||
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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
|||
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 {
|
|||
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 {
|
|||
}
|
||||
}
|
||||
|
||||
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 {
|
|||
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 {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
|||
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 {
|
|||
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 {
|
|||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
|||
/**
|
||||
* 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 {
|
|||
* 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 {
|
|||
*/
|
||||
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 {
|
|||
}
|
||||
|
||||
public SimpleEvaluationContext build() {
|
||||
return new SimpleEvaluationContext(this.propertyAccessors, this.typeConverter);
|
||||
return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
|||
|
||||
@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 {
|
|||
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…
Reference in New Issue