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() + "'");