From 66f708392e463a564ecf99caf057add20f6ff395 Mon Sep 17 00:00:00 2001 From: Andy Clement Date: Tue, 2 Feb 2010 19:38:44 +0000 Subject: [PATCH] SPR-6764: filtering strategy for SpEL method invocation --- .../expression/MethodFilter.java | 46 ++++++++ .../support/ReflectiveMethodResolver.java | 41 +++++++ .../support/StandardEvaluationContext.java | 18 ++- .../spel/MethodInvocationTests.java | 103 +++++++++++++++++- 4 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 org.springframework.expression/src/main/java/org/springframework/expression/MethodFilter.java diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/MethodFilter.java b/org.springframework.expression/src/main/java/org/springframework/expression/MethodFilter.java new file mode 100644 index 00000000000..d81268d226d --- /dev/null +++ b/org.springframework.expression/src/main/java/org/springframework/expression/MethodFilter.java @@ -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 filter(List methods); + +} \ No newline at end of file diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java index 14abc3bea76..6e6eda19dba 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java @@ -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,MethodFilter> filters = null; + + /** * Locate a method on a type. There are three kinds of match that might occur: *
    @@ -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 methodsForFiltering = new ArrayList(); + for (Method method: methods) { + methodsForFiltering.add(method); + } + List 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,MethodFilter>(); + } + if (filter==null) { + filters.remove(type); + } + else { + filters.put(type,filter); + } + } + } diff --git a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index d93a94056c4..2bdd8d77811 100644 --- a/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/org.springframework.expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -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 constructorResolvers; private List methodResolvers; + + private ReflectiveMethodResolver reflectiveMethodResolver; private List propertyAccessors; @@ -120,7 +123,7 @@ public class StandardEvaluationContext implements EvaluationContext { private void ensureMethodResolversInitialized() { if (this.methodResolvers == null) { this.methodResolvers = new ArrayList(); - 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); + } + } diff --git a/org.springframework.expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java b/org.springframework.expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java index 1bae4c43c12..d25bc127ba5 100644 --- a/org.springframework.expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java +++ b/org.springframework.expression/src/test/java/org/springframework/expression/spel/MethodInvocationTests.java @@ -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 filter(List methods) { + filterCalled = true; + List forRemoval = new ArrayList(); + 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() {