diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index a0bf5dd411..3c44feef7a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -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 getPropertyAccessors(); + /** * Return a list of resolvers that will be asked in turn to locate a constructor. */ @@ -52,9 +57,10 @@ public interface EvaluationContext { List 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 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 diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java new file mode 100644 index 0000000000..c5db421c6d --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java @@ -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. + * + *

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 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(); + } + +} diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java index df7860a5fd..1d65b310db 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java @@ -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: *

    @@ -112,13 +115,13 @@ public class ReflectiveMethodResolver implements MethodResolver { try { TypeConverter typeConverter = context.getTypeConverter(); Class type = (targetObject instanceof Class ? (Class) targetObject : targetObject.getClass()); - List methods = new ArrayList<>(getMethods(type, targetObject)); + ArrayList 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 filtered = filter.filter(methods); - methods = (filtered instanceof ArrayList ? filtered : new ArrayList<>(filtered)); + methods = (filtered instanceof ArrayList ? (ArrayList) 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 getMethods(Class type, Object targetObject) { + private Set getMethods(Class type, Object targetObject) { if (targetObject instanceof Class) { Set 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 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 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. + *

    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; + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 41eb0ed7bc..21ff6f82d6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java @@ -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. *

    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 diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index 672f103ac0..fa379a47d5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -92,6 +92,8 @@ public class SimpleEvaluationContext implements EvaluationContext { private final List propertyAccessors; + private final List methodResolvers; + private final TypeConverter typeConverter; private final TypeComparator typeComparator = new StandardTypeComparator(); @@ -101,8 +103,11 @@ public class SimpleEvaluationContext implements EvaluationContext { private final Map variables = new HashMap<>(); - private SimpleEvaluationContext(List accessors, @Nullable TypeConverter converter) { + private SimpleEvaluationContext(List accessors, List 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 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 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}. *

    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 propertyAccessors; + private final List accessors; + + private List 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); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java index a2069ee875..26bd762861 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/PropertyAccessTests.java @@ -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)); }