From 65a8aa1c092b9259414de47ce3780fcbb4513211 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Wed, 28 Mar 2018 01:22:59 +0200 Subject: [PATCH] Backported DataBindingPropertyAccessor and DataBindingMethodResolver Issue: SPR-16588 --- .../expression/EvaluationContext.java | 24 +- .../support/DataBindingMethodResolver.java | 74 +++++ .../support/DataBindingPropertyAccessor.java | 72 +++++ .../support/ReflectiveMethodResolver.java | 41 ++- .../support/ReflectivePropertyAccessor.java | 140 +++++++--- .../spel/support/SimpleEvaluationContext.java | 253 ++++++++++++++---- .../support/StandardEvaluationContext.java | 18 +- .../expression/spel/PropertyAccessTests.java | 103 ++++++- 8 files changed, 602 insertions(+), 123 deletions(-) create mode 100644 spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java create mode 100644 spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java 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 9d509c6121..417f12586c 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-2013 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. @@ -22,9 +22,9 @@ import java.util.List; * Expressions are executed in an evaluation context. It is in this context that * references are resolved when encountered during expression evaluation. * - *

There is a default implementation of the EvaluationContext, - * {@link org.springframework.expression.spel.support.StandardEvaluationContext} that can - * be extended, rather than having to implement everything. + *

There is a default implementation of this EvaluationContext interface: + * {@link org.springframework.expression.spel.support.StandardEvaluationContext} + * which can be extended, rather than having to implement everything manually. * * @author Andy Clement * @author Juergen Hoeller @@ -39,6 +39,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. */ @@ -50,9 +55,9 @@ 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(); + BeanResolver getBeanResolver(); /** * Return a type locator that can be used to find types, either by short or @@ -76,11 +81,6 @@ public interface EvaluationContext { */ OperatorOverloader getOperatorOverloader(); - /** - * Return a bean resolver that can look up beans by name. - */ - BeanResolver getBeanResolver(); - /** * Set a named variable within this evaluation context to a specified value. * @param name variable to set @@ -91,7 +91,7 @@ public interface EvaluationContext { /** * Look up a named variable within this evaluation context. * @param name variable to lookup - * @return the value of the variable + * @return the value of the variable, or {@code null} if not found */ Object lookupVariable(String name); 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..d63e646839 --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java @@ -0,0 +1,74 @@ +/* + * 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; + +/** + * 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 + 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/DataBindingPropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java new file mode 100644 index 0000000000..8ee7ec946d --- /dev/null +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingPropertyAccessor.java @@ -0,0 +1,72 @@ +/* + * 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; + +/** + * A {@link org.springframework.expression.PropertyAccessor} variant for data binding + * purposes, using reflection to access properties for reading and possibly writing. + * + *

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. + * + *

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 + * @see #forReadOnlyAccess() + * @see #forReadWriteAccess() + * @see SimpleEvaluationContext + * @see StandardEvaluationContext + * @see ReflectivePropertyAccessor + */ +public class DataBindingPropertyAccessor extends ReflectivePropertyAccessor { + + /** + * Create a new property accessor for reading and possibly also writing. + * @param allowWrite whether to also allow for write operations + * @see #canWrite + */ + private DataBindingPropertyAccessor(boolean allowWrite) { + super(allowWrite); + } + + @Override + 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 operations. + */ + public static DataBindingPropertyAccessor forReadOnlyAccess() { + return new DataBindingPropertyAccessor(false); + } + + /** + * Create a new data-binding property accessor for read-write operations. + */ + public static DataBindingPropertyAccessor forReadWriteAccess() { + return new DataBindingPropertyAccessor(true); + } + +} 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 fb512d86a0..b1cffa5d43 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 @@ -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. @@ -21,7 +21,6 @@ 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.Comparator; import java.util.HashMap; @@ -81,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, MethodFilter filter) { if (this.filters == null) { this.filters = new HashMap, MethodFilter>(); @@ -93,7 +98,6 @@ public class ReflectiveMethodResolver implements MethodResolver { } } - /** * Locate a method on a type. There are three kinds of match that might occur: *

    @@ -219,7 +223,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(..) @@ -237,12 +241,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()) { - result.addAll(Arrays.asList(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; } } @@ -258,4 +274,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 8cb54e8f0a..0766dbbe47 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 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. @@ -44,16 +44,19 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** - * Simple {@link PropertyAccessor} that uses reflection to access properties - * for reading and writing. + * A powerful {@link PropertyAccessor} that uses reflection to access properties + * for reading and possibly also for writing. * - *

    A property can be accessed through a public getter method (when being read) + *

    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 * @author Juergen Hoeller * @author Phillip Webb * @since 3.0 + * @see StandardEvaluationContext + * @see SimpleEvaluationContext + * @see DataBindingPropertyAccessor */ public class ReflectivePropertyAccessor implements PropertyAccessor { @@ -62,13 +65,15 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { private static final Set> BOOLEAN_TYPES; static { - Set> booleanTypes = new HashSet>(); + Set> booleanTypes = new HashSet>(4); booleanTypes.add(Boolean.class); booleanTypes.add(Boolean.TYPE); BOOLEAN_TYPES = Collections.unmodifiableSet(booleanTypes); } + private final boolean allowWrite; + private final Map readerCache = new ConcurrentHashMap(64); @@ -81,6 +86,25 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { private InvokerPair lastReadInvokerPair; + /** + * Create a new property accessor for reading as well writing. + * @see #ReflectivePropertyAccessor(boolean) + */ + public ReflectivePropertyAccessor() { + this.allowWrite = true; + } + + /** + * Create a new property accessor for reading and possibly writing. + * @param allowWrite whether to also allow for write operations + * @since 4.3.15 + * @see #canWrite + */ + public ReflectivePropertyAccessor(boolean allowWrite) { + this.allowWrite = allowWrite; + } + + /** * Returns {@code null} which means this is a general purpose accessor. */ @@ -94,14 +118,17 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { if (target == null) { return false; } + Class type = (target instanceof Class ? (Class) target : target.getClass()); if (type.isArray() && name.equals("length")) { return true; } + PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); if (this.readerCache.containsKey(cacheKey)) { return true; } + Method method = findGetterForProperty(name, type, target); if (method != null) { // Treat it like a property... @@ -121,11 +148,8 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { return true; } } - return false; - } - public Member getLastReadInvokerPair() { - return this.lastReadInvokerPair.member; + return false; } @Override @@ -144,20 +168,19 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); InvokerPair invoker = this.readerCache.get(cacheKey); - lastReadInvokerPair = invoker; + this.lastReadInvokerPair = invoker; if (invoker == null || invoker.member instanceof Method) { Method method = (Method) (invoker != null ? invoker.member : null); if (method == null) { method = findGetterForProperty(name, type, target); if (method != null) { - // TODO remove the duplication here between canRead and read // Treat it like a property... // The readerCache will only contain gettable properties (let's not worry about setters for now). Property property = new Property(type, method, null); TypeDescriptor typeDescriptor = new TypeDescriptor(property); invoker = new InvokerPair(method, typeDescriptor); - lastReadInvokerPair = invoker; + this.lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } @@ -179,7 +202,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { field = findField(name, type, target); if (field != null) { invoker = new InvokerPair(field, new TypeDescriptor(field)); - lastReadInvokerPair = invoker; + this.lastReadInvokerPair = invoker; this.readerCache.put(cacheKey, invoker); } } @@ -200,14 +223,16 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { @Override public boolean canWrite(EvaluationContext context, Object target, String name) throws AccessException { - if (target == null) { + if (!this.allowWrite || target == null) { return false; } + Class type = (target instanceof Class ? (Class) target : target.getClass()); PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); if (this.writerCache.containsKey(cacheKey)) { return true; } + Method method = findSetterForProperty(name, type, target); if (method != null) { // Treat it like a property @@ -225,11 +250,17 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { return true; } } + return false; } @Override public void write(EvaluationContext context, Object target, String name, Object newValue) throws AccessException { + if (!this.allowWrite) { + throw new AccessException("PropertyAccessor for property '" + name + + "' on target [" + target + "] does not allow write operations"); + } + if (target == null) { throw new AccessException("Cannot write property on null target"); } @@ -294,6 +325,16 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { throw new AccessException("Neither setter method nor field found for property '" + name + "'"); } + /** + * @deprecated as of 4.3.15 since it is not used within the framework anymore + */ + @Deprecated + public Member getLastReadInvokerPair() { + InvokerPair lastReadInvoker = this.lastReadInvokerPair; + return (lastReadInvoker != null ? lastReadInvoker.member : null); + } + + private TypeDescriptor getTypeDescriptor(EvaluationContext context, Object target, String name) { if (target == null) { return null; @@ -306,7 +347,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { PropertyCacheKey cacheKey = new PropertyCacheKey(type, name, target instanceof Class); TypeDescriptor typeDescriptor = this.typeDescriptorCache.get(cacheKey); if (typeDescriptor == null) { - // attempt to populate the cache entry + // Attempt to populate the cache entry try { if (canRead(context, target, name)) { typeDescriptor = this.typeDescriptorCache.get(cacheKey); @@ -316,7 +357,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { } } catch (AccessException ex) { - // continue with null type descriptor + // Continue with null type descriptor } } return typeDescriptor; @@ -338,14 +379,6 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { return method; } - private Field findField(String name, Class clazz, Object target) { - Field field = findField(name, clazz, target instanceof Class); - if (field == null && target instanceof Class) { - field = findField(name, target.getClass(), false); - } - return field; - } - /** * Find a getter method for the specified property. */ @@ -373,7 +406,7 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { Method[] methods = getSortedClassMethods(clazz); for (String methodSuffix : methodSuffixes) { for (Method method : methods) { - if (method.getName().equals(prefix + methodSuffix) && + if (isCandidateForProperty(method, clazz) && method.getName().equals(prefix + methodSuffix) && method.getParameterTypes().length == numberOfParams && (!mustBeStatic || Modifier.isStatic(method.getModifiers())) && (requiredReturnTypes.isEmpty() || requiredReturnTypes.contains(method.getReturnType()))) { @@ -382,11 +415,23 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { } } return null; - } /** - * Returns class methods ordered with non bridge methods appearing higher. + * 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 + * @param targetClass the concrete target class that is being introspected + * @since 4.3.15 + */ + protected boolean isCandidateForProperty(Method method, Class targetClass) { + return true; + } + + /** + * Return class methods ordered with non bridge methods appearing higher. */ private Method[] getSortedClassMethods(Class clazz) { Method[] methods = clazz.getMethods(); @@ -408,9 +453,9 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { protected String[] getPropertyMethodSuffixes(String propertyName) { String suffix = getPropertyMethodSuffix(propertyName); if (suffix.length() > 0 && Character.isUpperCase(suffix.charAt(0))) { - return new String[] { suffix }; + return new String[] {suffix}; } - return new String[] { suffix, StringUtils.capitalize(suffix) }; + return new String[] {suffix, StringUtils.capitalize(suffix)}; } /** @@ -424,6 +469,14 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { return StringUtils.capitalize(propertyName); } + private Field findField(String name, Class clazz, Object target) { + Field field = findField(name, clazz, target instanceof Class); + if (field == null && target instanceof Class) { + field = findField(name, target.getClass(), false); + } + return field; + } + /** * Find a field of a certain name on a specified class. */ @@ -452,29 +505,33 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { } /** - * Attempt to create an optimized property accessor tailored for a property of a particular name on - * a particular class. The general ReflectivePropertyAccessor will always work but is not optimal - * due to the need to lookup which reflective member (method/field) to use each time read() is called. - * This method will just return the ReflectivePropertyAccessor instance if it is unable to build - * something more optimal. + * Attempt to create an optimized property accessor tailored for a property of a + * particular name on a particular class. The general ReflectivePropertyAccessor + * will always work but is not optimal due to the need to lookup which reflective + * member (method/field) to use each time read() is called. This method will just + * return the ReflectivePropertyAccessor instance if it is unable to build a more + * optimal accessor. + *

    Note: An optimal accessor is currently only usable for read attempts. + * Do not call this method if you need a read-write accessor. + * @see OptimalPropertyAccessor */ - public PropertyAccessor createOptimalAccessor(EvaluationContext evalContext, Object target, String name) { - // Don't be clever for arrays or null target + public PropertyAccessor createOptimalAccessor(EvaluationContext context, Object target, String name) { + // Don't be clever for arrays or a null target... 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); @@ -489,7 +546,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); @@ -604,7 +661,6 @@ public class ReflectivePropertyAccessor implements PropertyAccessor { if (target == null) { return false; } - Class type = (target instanceof Class ? (Class) target : target.getClass()); if (type.isArray()) { return false; 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 06f14cc8f0..f298b081b9 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 @@ -16,12 +16,14 @@ 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.core.convert.TypeDescriptor; import org.springframework.expression.BeanResolver; import org.springframework.expression.ConstructorResolver; import org.springframework.expression.EvaluationContext; @@ -38,29 +40,55 @@ import org.springframework.expression.spel.SpelMessage; /** * A basic implementation of {@link EvaluationContext} that focuses on a subset - * of essential SpEL features and configuration options, and relies on default - * strategies otherwise. + * of essential SpEL features and customization options, targeting simple + * condition evaluation and in particular data binding scenarios. * - *

    In many cases, the full extent of the SpEL 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} supports only a subset of the - * SpEL language syntax that excludes references to Java types, constructors, + *

    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. * - *

    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. + *

    When creating a {@code SimpleEvaluationContext} you need to choose the + * level of support that you need for property access in SpEL expressions: + *

      + *
    • A custom {@code PropertyAccessor} (typically not reflection-based), + * potentially combined with a {@link DataBindingPropertyAccessor}
    • + *
    • Data binding properties for read-only access
    • + *
    • Data binding properties for read and write
    • + *
    + * + *

    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}, and potentially + * activate method resolution and/or a type converter through the builder. + * + *

    Note that {@code SimpleEvaluationContext} is typically not 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: + * {@link org.springframework.expression.Expression#getValue(EvaluationContext, Object)}. + * + *

    For more power and flexibility, in particular for internal configuration + * scenarios, consider using {@link StandardEvaluationContext} instead. * * @author Rossen Stoyanchev + * @author Juergen Hoeller * @since 4.3.15 + * @see #forPropertyAccessors + * @see #forReadOnlyDataBinding() + * @see #forReadWriteDataBinding() + * @see StandardEvaluationContext + * @see StandardTypeConverter + * @see DataBindingPropertyAccessor */ public class SimpleEvaluationContext implements EvaluationContext { private static final TypeLocator typeNotFoundTypeLocator = new TypeLocator() { - @Override public Class findType(String typeName) throws EvaluationException { throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); @@ -68,13 +96,11 @@ public class SimpleEvaluationContext implements EvaluationContext { }; + private final TypedValue rootObject; + private final List propertyAccessors; - private final List constructorResolvers = - Collections.singletonList(new ReflectiveConstructorResolver()); - - private final List methodResolvers = - Collections.singletonList(new ReflectiveMethodResolver()); + private final List methodResolvers; private final TypeConverter typeConverter; @@ -85,52 +111,45 @@ public class SimpleEvaluationContext implements EvaluationContext { private final Map variables = new HashMap(); - public SimpleEvaluationContext() { - this(null, null); - } + private SimpleEvaluationContext(List accessors, List resolvers, + TypeConverter converter, TypedValue rootObject) { - public SimpleEvaluationContext(List accessors, TypeConverter converter) { - this.propertyAccessors = initPropertyAccessors(accessors); - this.typeConverter = converter != null ? converter : new StandardTypeConverter(); - } - - - private static List initPropertyAccessors(List accessors) { - if (accessors == null) { - accessors = new ArrayList(5); - accessors.add(new ReflectivePropertyAccessor()); - } - return accessors; + this.propertyAccessors = accessors; + this.methodResolvers = resolvers; + this.typeConverter = (converter != null ? converter : new StandardTypeConverter()); + this.rootObject = (rootObject != null ? rootObject : TypedValue.NULL); } /** - * {@code SimpleEvaluationContext} cannot be configured with a root object. - * It is meant for repeated use with - * {@link org.springframework.expression.Expression Expression} method - * variants that accept both an {@code EvaluationContext} and a root object. - * @return Always returns {@link TypedValue#NULL}. + * Return the specified root object, if any. */ @Override public TypedValue getRootObject() { - return TypedValue.NULL; + return this.rootObject; } + /** + * Return the specified {@link PropertyAccessor} delegates, if any. + * @see #forPropertyAccessors + */ @Override public List getPropertyAccessors() { return this.propertyAccessors; } /** - * Return a single {@link ReflectiveConstructorResolver}. + * Return an empty list, always, since this context does not support the + * use of type references. */ @Override public List getConstructorResolvers() { - return this.constructorResolvers; + return Collections.emptyList(); } /** - * Return a single {@link ReflectiveMethodResolver}. + * Return the specified {@link MethodResolver} delegates, if any. + * @see Builder#withMethodResolvers */ @Override public List getMethodResolvers() { @@ -138,8 +157,8 @@ public class SimpleEvaluationContext implements EvaluationContext { } /** - * {@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 public BeanResolver getBeanResolver() { @@ -159,6 +178,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() { @@ -173,7 +194,6 @@ public class SimpleEvaluationContext implements EvaluationContext { return this.typeComparator; } - /** * Return an instance of {@link StandardOperatorOverloader}. */ @@ -192,4 +212,145 @@ public class SimpleEvaluationContext implements EvaluationContext { return this.variables.get(name); } + + /** + * Create a {@code SimpleEvaluationContext} for the specified {@link PropertyAccessor} + * 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() + */ + public static Builder forPropertyAccessors(PropertyAccessor... accessors) { + for (PropertyAccessor accessor : accessors) { + if (accessor.getClass() == ReflectivePropertyAccessor.class) { + throw new IllegalArgumentException("SimpleEvaluationContext is not designed for use with a plain " + + "ReflectivePropertyAccessor. Consider using DataBindingPropertyAccessor or a custom subclass."); + } + } + return new Builder(accessors); + } + + /** + * Create a {@code SimpleEvaluationContext} for read-only access to + * public properties via {@link DataBindingPropertyAccessor}. + * @see DataBindingPropertyAccessor#forReadOnlyAccess() + * @see #forPropertyAccessors + */ + 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#forReadWriteAccess() + * @see #forPropertyAccessors + */ + public static Builder forReadWriteDataBinding() { + return new Builder(DataBindingPropertyAccessor.forReadWriteAccess()); + } + + + /** + * Builder for {@code SimpleEvaluationContext}. + */ + public static class Builder { + + private final List accessors; + + private List resolvers = Collections.emptyList(); + + private TypeConverter typeConverter; + + private TypedValue rootObject; + + public Builder(PropertyAccessor... 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( + (MethodResolver) DataBindingMethodResolver.forInstanceMethodInvocation()); + return this; + } + + + /** + * Register a custom {@link ConversionService}. + *

    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; + } + /** + * Register a custom {@link TypeConverter}. + *

    By default a {@link StandardTypeConverter} backed by a + * {@link org.springframework.core.convert.support.DefaultConversionService} is used. + * @see #withConversionService + * @see StandardTypeConverter#StandardTypeConverter() + */ + public Builder withTypeConverter(TypeConverter converter) { + this.typeConverter = converter; + return this; + } + + /** + * Specify a default root object to resolve against. + *

    Default is none, expecting an object argument at evaluation time. + * @see org.springframework.expression.Expression#getValue(EvaluationContext) + * @see org.springframework.expression.Expression#getValue(EvaluationContext, Object) + */ + public Builder withRootObject(Object rootObject) { + this.rootObject = new TypedValue(rootObject); + return this; + } + + /** + * Specify a typed root object to resolve against. + *

    Default is none, expecting an object argument at evaluation time. + * @see org.springframework.expression.Expression#getValue(EvaluationContext) + * @see org.springframework.expression.Expression#getValue(EvaluationContext, Object) + */ + public Builder withTypedRootObject(Object rootObject, TypeDescriptor typeDescriptor) { + this.rootObject = new TypedValue(rootObject, typeDescriptor); + return this; + } + + public SimpleEvaluationContext build() { + return new SimpleEvaluationContext(this.accessors, this.resolvers, this.typeConverter, this.rootObject); + } + } + } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 6e95e7941d..78a7e91f95 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.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. @@ -37,9 +37,13 @@ import org.springframework.expression.TypedValue; import org.springframework.util.Assert; /** - * Provides a default EvaluationContext implementation. + * A powerful and highly configurable {@link EvaluationContext} implementation. + * This context uses standard implementations of all applicable strategies, + * based on reflection to resolve properties, methods and fields. * - *

    To resolve properties/methods/fields this context uses a reflection mechanism. + *

    For a simpler builder-style context variant for data-binding purposes, + * consider using {@link SimpleEvaluationContext} instead which allows for + * opting into several SpEL features as needed by specific evaluation cases. * * @author Andy Clement * @author Juergen Hoeller @@ -71,10 +75,18 @@ public class StandardEvaluationContext implements EvaluationContext { private final Map variables = new HashMap(); + /** + * Create a {@code StandardEvaluationContext} with a null root object. + */ public StandardEvaluationContext() { setRootObject(null); } + /** + * Create a {@code StandardEvaluationContext} with the given root object. + * @param rootObject the root object to use + * @see #setRootObject + */ public StandardEvaluationContext(Object rootObject) { setRootObject(rootObject); } 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 394c6ff687..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 @@ -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. @@ -32,7 +32,9 @@ 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.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.expression.spel.testresources.Person; import static org.junit.Assert.*; @@ -73,7 +75,7 @@ public class PropertyAccessTests extends AbstractExpressionTests { * supplied resolver might be able to - so null shouldn't crash the reflection resolver. */ @Test - public void testAccessingOnNullObject() throws Exception { + public void testAccessingOnNullObject() { SpelExpression expr = (SpelExpression)parser.parseExpression("madeup"); EvaluationContext context = new StandardEvaluationContext(null); try { @@ -85,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) { @@ -93,19 +95,19 @@ 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); } } @Test // Adding a new property accessor just for a particular type - public void testAddingSpecificPropertyAccessor() throws Exception { + public void testAddingSpecificPropertyAccessor() { SpelExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext ctx = new StandardEvaluationContext(); @@ -115,7 +117,7 @@ public class PropertyAccessTests extends AbstractExpressionTests { ctx.addPropertyAccessor(new StringyPropertyAccessor()); Expression expr = parser.parseRaw("new String('hello').flibbles"); Integer i = expr.getValue(ctx, Integer.class); - assertEquals((int) i, 7); + assertEquals(7, (int) i); // The reflection one will be used for other properties... expr = parser.parseRaw("new String('hello').CASE_INSENSITIVE_ORDER"); @@ -125,7 +127,7 @@ public class PropertyAccessTests extends AbstractExpressionTests { expr = parser.parseRaw("new String('hello').flibbles"); expr.setValue(ctx, 99); i = expr.getValue(ctx, Integer.class); - assertEquals((int) i, 99); + assertEquals(99, (int) i); // Cannot set it to a string value try { @@ -162,10 +164,10 @@ public class PropertyAccessTests extends AbstractExpressionTests { } @Test - public void testAccessingPropertyOfClass() throws Exception { + public void testAccessingPropertyOfClass() { Expression expression = parser.parseExpression("name"); Object value = expression.getValue(new StandardEvaluationContext(String.class)); - assertEquals(value, "java.lang.String"); + assertEquals("java.lang.String", value); } @Test @@ -182,6 +184,78 @@ public class PropertyAccessTests extends AbstractExpressionTests { assertEquals("Jens", expression.getValue(context)); } + @Test + public void standardGetClassAccess() { + assertEquals(String.class.getName(), parser.parseExpression("'a'.class.name").getValue()); + } + + @Test(expected = SpelEvaluationException.class) + public void noGetClassAccess() { + 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() { + EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().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)); + + 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 + public void propertyAccessWithoutMethodResolver() { + EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); + + Person target = new Person("p1"); + try { + parser.parseExpression("name.substring(1)").getValue(context, target); + fail("Should have thrown SpelEvaluationException"); + } + catch (SpelEvaluationException ex) { + // expected + } + } + + @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)); + } + // This can resolve the property 'flibbles' on any String (very useful...) private static class StringyPropertyAccessor implements PropertyAccessor { @@ -223,7 +297,8 @@ public class PropertyAccessTests extends AbstractExpressionTests { throw new RuntimeException("Assertion Failed! name should be flibbles"); } try { - flibbles = (Integer) context.getTypeConverter().convertValue(newValue, TypeDescriptor.forObject(newValue), TypeDescriptor.valueOf(Integer.class)); + flibbles = (Integer) context.getTypeConverter().convertValue(newValue, + TypeDescriptor.forObject(newValue), TypeDescriptor.valueOf(Integer.class)); } catch (EvaluationException ex) { throw new AccessException("Cannot set flibbles to an object of type '" + newValue.getClass() + "'");