SPR-6764: filtering strategy for SpEL method invocation

This commit is contained in:
Andy Clement 2010-02-02 19:38:44 +00:00
parent 4343714c6d
commit 66f708392e
4 changed files with 206 additions and 2 deletions

View File

@ -0,0 +1,46 @@
/*
* Copyright 2002-2009 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;
import java.lang.reflect.Method;
import java.util.List;
/**
* MethodFilter instances allow SpEL users to fine tune the behaviour of the method resolution
* process. Method resolution (which translates from a method name in an expression to a real
* method to invoke) will normally retrieve candidate methods for invocation via a simple call
* to 'Class.getMethods()' and will choose the first one that is suitable for the
* input parameters. By registering a MethodFilter the user can receive a callback
* and change the methods that will be considered suitable.
*
* @author Andy Clement
* @since 3.0.1
*/
public interface MethodFilter {
/**
* Called by the method resolver to allow the SpEL user to organize the list of candidate
* methods that may be invoked. The filter can remove methods that should not be
* considered candidates and it may sort the results. The resolver will then search
* through the methods as returned from the filter when looking for a suitable
* candidate to invoke.
*
* @param methods the full list of methods the resolver was going to choose from
* @return a possible subset of input methods that may be sorted by order of relevance
*/
List<Method> filter(List<Method> methods);
}

View File

@ -17,16 +17,23 @@
package org.springframework.expression.spel.support;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodFilter;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
/**
* A method resolver that uses reflection to locate the method that should be invoked
*
@ -35,6 +42,11 @@ import org.springframework.expression.spel.SpelMessage;
*/
public class ReflectiveMethodResolver implements MethodResolver {
private static Method[] NO_METHODS = new Method[0];
private Map<Class<?>,MethodFilter> filters = null;
/**
* Locate a method on a type. There are three kinds of match that might occur:
* <ol>
@ -49,6 +61,23 @@ public class ReflectiveMethodResolver implements MethodResolver {
TypeConverter typeConverter = context.getTypeConverter();
Class<?> type = (targetObject instanceof Class ? (Class<?>) targetObject : targetObject.getClass());
Method[] methods = type.getMethods();
// If a filter is registered for this type, call it
MethodFilter methodfilter = (filters==null?null:filters.get(type));
if (methodfilter!=null) {
List<Method> methodsForFiltering = new ArrayList<Method>();
for (Method method: methods) {
methodsForFiltering.add(method);
}
List<Method> methodsFiltered = methodfilter.filter(methodsForFiltering);
if (methodsFiltered == null || methodsFiltered.size()==0) {
methods = NO_METHODS;
}
else {
methods = methodsFiltered.toArray(new Method[methodsFiltered.size()]);
}
}
Method closeMatch = null;
int[] argsToConvert = null;
boolean multipleOptions = false;
@ -101,4 +130,16 @@ public class ReflectiveMethodResolver implements MethodResolver {
}
}
public void registerMethodFilter(Class<?> type, MethodFilter filter) {
if (filters==null) {
filters = new HashMap<Class<?>,MethodFilter>();
}
if (filter==null) {
filters.remove(type);
}
else {
filters.put(type,filter);
}
}
}

View File

@ -25,6 +25,7 @@ import java.util.Map;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.MethodFilter;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.OperatorOverloader;
import org.springframework.expression.PropertyAccessor;
@ -50,6 +51,8 @@ public class StandardEvaluationContext implements EvaluationContext {
private List<ConstructorResolver> constructorResolvers;
private List<MethodResolver> methodResolvers;
private ReflectiveMethodResolver reflectiveMethodResolver;
private List<PropertyAccessor> propertyAccessors;
@ -120,7 +123,7 @@ public class StandardEvaluationContext implements EvaluationContext {
private void ensureMethodResolversInitialized() {
if (this.methodResolvers == null) {
this.methodResolvers = new ArrayList<MethodResolver>();
this.methodResolvers.add(new ReflectiveMethodResolver());
this.methodResolvers.add(reflectiveMethodResolver=new ReflectiveMethodResolver());
}
}
@ -199,4 +202,17 @@ public class StandardEvaluationContext implements EvaluationContext {
return this.variables.get(name);
}
/**
* Register a MethodFilter which will be called during method resolution for the
* specified type. The MethodFilter may remove methods and/or sort the methods
* which will then be used by SpEL as the candidates to look through for a match.
*
* @param type the type for which the filter should be called
* @param filter a MethodFilter, or NULL to deregister a filter for the type
*/
public void registerMethodFilter(Class<?> type, MethodFilter filter) {
ensureMethodResolversInitialized();
reflectiveMethodResolver.registerMethodFilter(type,filter);
}
}

View File

@ -16,9 +16,18 @@
package org.springframework.expression.spel;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.junit.Assert;
import org.junit.Test;
import org.springframework.expression.Expression;
import org.springframework.expression.MethodFilter;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.testresources.PlaceOfBirth;
@ -141,7 +150,99 @@ public class MethodInvocationTests extends ExpressionTestCase {
}
// If counter is 5 then the method got called twice!
Assert.assertEquals(4,parser.parseExpression("counter").getValue(eContext));
}
}
@Test
public void testMethodFiltering_SPR6764() {
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(new TestObject());
LocalFilter filter = new LocalFilter();
context.registerMethodFilter(TestObject.class,filter);
// Filter will be called but not do anything, so first doit() will be invoked
SpelExpression expr = (SpelExpression) parser.parseExpression("doit(1)");
String result = expr.getValue(context,String.class);
Assert.assertEquals("1",result);
Assert.assertTrue(filter.filterCalled);
// Filter will now remove non @Anno annotated methods
filter.removeIfNotAnnotated = true;
filter.filterCalled = false;
expr = (SpelExpression) parser.parseExpression("doit(1)");
result = expr.getValue(context,String.class);
Assert.assertEquals("double 1.0",result);
Assert.assertTrue(filter.filterCalled);
// check not called for other types
filter.filterCalled=false;
context.setRootObject(new String("abc"));
expr = (SpelExpression) parser.parseExpression("charAt(0)");
result = expr.getValue(context,String.class);
Assert.assertEquals("a",result);
Assert.assertFalse(filter.filterCalled);
// check de-registration works
filter.filterCalled = false;
context.registerMethodFilter(TestObject.class,null);//clear filter
context.setRootObject(new TestObject());
expr = (SpelExpression) parser.parseExpression("doit(1)");
result = expr.getValue(context,String.class);
Assert.assertEquals("1",result);
Assert.assertFalse(filter.filterCalled);
}
// Simple filter
static class LocalFilter implements MethodFilter {
public boolean removeIfNotAnnotated = false;
public boolean filterCalled = false;
private boolean isAnnotated(Method m) {
Annotation[] annos = m.getAnnotations();
if (annos==null) {
return false;
}
for (Annotation anno: annos) {
String s = anno.annotationType().getName();
if (s.endsWith("Anno")) {
return true;
}
}
return false;
}
public List<Method> filter(List<Method> methods) {
filterCalled = true;
List<Method> forRemoval = new ArrayList<Method>();
for (Method m: methods) {
if (removeIfNotAnnotated && !isAnnotated(m)) {
forRemoval.add(m);
}
}
for (Method m: forRemoval) {
methods.remove(m);
}
return methods;
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface Anno {}
class TestObject {
public int doit(int i) {
return i;
}
@Anno
public String doit(double d) {
return "double "+d;
}
}
@Test
public void testVarargsInvocation01() {