INT-828, INT-936 Created a FilteringReflectiveMethodResolver for use in the SpEL expression evaluation.

This commit is contained in:
Mark Fisher
2009-12-23 02:09:50 +00:00
parent f84f71f261
commit 886e1dccb2
6 changed files with 377 additions and 7 deletions

View File

@@ -0,0 +1,158 @@
/*
* 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.integration.handler;
import java.lang.reflect.Method;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.AccessException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.MethodExecutor;
import org.springframework.expression.MethodResolver;
import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.support.ReflectionHelper;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils;
/**
* An adaptation of the SpEL ReflectiveMethodResolver with the simple addition of a {@link MethodFilter}.
*
* @author Andy Clement
* @author Mark Fisher
* @since 2.0
*/
class FilteringReflectiveMethodResolver implements MethodResolver {
private final MethodFilter methodFilter;
private final Class<?>[] filteredTypes;
public FilteringReflectiveMethodResolver(MethodFilter methodFilter, Class<?> ... filteredTypes) {
Assert.notNull(methodFilter, "methodFilter must not be null");
this.methodFilter = methodFilter;
this.filteredTypes = filteredTypes;
}
public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, Class<?>[] argumentTypes) throws AccessException {
try {
TypeConverter typeConverter = context.getTypeConverter();
Class<?> type = (targetObject instanceof Class ? (Class<?>) targetObject : targetObject.getClass());
// this is the only addition for this implementation:
Method[] methods = ObjectUtils.containsElement(this.filteredTypes, type) ?
this.methodFilter.filter(type.getMethods()) : type.getMethods();
Method closeMatch = null;
int[] argsToConvert = null;
boolean multipleOptions = false;
Method matchRequiringConversion = null;
for (Method method : methods) {
if (method.isBridge()) {
continue;
}
if (method.getName().equals(name)) {
ReflectionHelper.ArgumentsMatchInfo matchInfo = null;
if (method.isVarArgs() && argumentTypes.length >= (method.getParameterTypes().length - 1)) {
// *sigh* complicated
matchInfo = ReflectionHelper.compareArgumentsVarargs(method.getParameterTypes(), argumentTypes, typeConverter);
} else if (method.getParameterTypes().length == argumentTypes.length) {
// name and parameter number match, check the arguments
matchInfo = ReflectionHelper.compareArguments(method.getParameterTypes(), argumentTypes, typeConverter);
}
if (matchInfo != null) {
if (matchInfo.kind == ReflectionHelper.ArgsMatchKind.EXACT) {
return new ReflectiveMethodExecutor(method, null);
}
else if (matchInfo.kind == ReflectionHelper.ArgsMatchKind.CLOSE) {
closeMatch = method;
}
else if (matchInfo.kind == ReflectionHelper.ArgsMatchKind.REQUIRES_CONVERSION) {
if (matchRequiringConversion != null) {
multipleOptions = true;
}
argsToConvert = matchInfo.argsRequiringConversion;
matchRequiringConversion = method;
}
}
}
}
if (closeMatch != null) {
return new ReflectiveMethodExecutor(closeMatch, null);
}
else if (matchRequiringConversion != null) {
if (multipleOptions) {
throw new SpelEvaluationException(SpelMessage.MULTIPLE_POSSIBLE_METHODS, name);
}
return new ReflectiveMethodExecutor(matchRequiringConversion, argsToConvert);
}
else {
return null;
}
}
catch (EvaluationException ex) {
throw new AccessException("Failed to resolve method", ex);
}
}
/**
* This class is copied also, since the original has package-only access.
*
* @author Andy Clement
*/
private static class ReflectiveMethodExecutor implements MethodExecutor {
private final Method method;
// When the method was found, we will have determined if arguments need to be converted for it
// to be invoked. Conversion won't be cheap so let's only do it if necessary.
private final int[] argsRequiringConversion;
public ReflectiveMethodExecutor(Method theMethod, int[] argumentsRequiringConversion) {
this.method = theMethod;
this.argsRequiringConversion = argumentsRequiringConversion;
}
public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException {
try {
if (this.argsRequiringConversion != null && arguments != null) {
ReflectionHelper.convertArguments(this.method.getParameterTypes(), this.method.isVarArgs(),
context.getTypeConverter(), this.argsRequiringConversion, arguments);
}
if (this.method.isVarArgs()) {
arguments = ReflectionHelper.setupArgumentsForVarargsInvocation(this.method.getParameterTypes(), arguments);
}
ReflectionUtils.makeAccessible(this.method);
return new TypedValue(this.method.invoke(target, arguments), new TypeDescriptor(new MethodParameter(method,-1)));
} catch (Exception ex) {
throw new AccessException("Problem invoking method: " + this.method, ex);
}
}
}
}

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.integration.handler;
import java.lang.reflect.Method;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* @author Mark Fisher
* @since 2.0
*/
class FixedHandlerMethodFilter implements MethodFilter {
private final Method method;
public FixedHandlerMethodFilter(Method method) {
Assert.notNull(method, "method must not be null");
this.method = method;
}
public Method[] filter(Method[] methods) {
if (ObjectUtils.containsElement(methods, this.method)) {
return new Method[] { this.method };
}
return new Method[0];
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.integration.handler;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
/**
* A MethodFilter implementation that enables the following:
* <ol>
* <li>matching on method name, if available</li>
* <li>exclusion of void-returning methods if 'requiresReply' is true</li>
* <li>limiting to annotated methods if at least one is present</li>
* </ol>
* <p>
*
* @author Mark Fisher
* @since 2.0
*/
class HandlerMethodFilter implements MethodFilter {
private final Class<? extends Annotation> annotationType;
private final String methodName;
private final boolean requiresReply;
public HandlerMethodFilter(Class<? extends Annotation> annotationType, String methodName, boolean requiresReply) {
this.annotationType = annotationType;
this.methodName = methodName;
this.requiresReply = requiresReply;
}
public Method[] filter(Method[] methods) {
List<Method> annotatedCandidates = new ArrayList<Method>();
List<Method> fallbackCandidates = new ArrayList<Method>();
for (Method method : methods) {
if (method.isBridge()) {
continue;
}
if (this.requiresReply && method.getReturnType().equals(void.class)) {
continue;
}
if (StringUtils.hasText(this.methodName) && !this.methodName.equals(method.getName())) {
continue;
}
if (this.annotationType != null && AnnotationUtils.findAnnotation(method, this.annotationType) != null) {
annotatedCandidates.add(method);
}
else {
fallbackCandidates.add(method);
}
}
return (!annotatedCandidates.isEmpty() ? annotatedCandidates.toArray(new Method[annotatedCandidates.size()])
: fallbackCandidates.toArray(new Method[fallbackCandidates.size()]));
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.integration.handler;
import java.lang.reflect.Method;
/**
* Strategy interface for filtering the available Methods prior to resolution.
*
* @author Mark Fisher
* @since 2.0
*/
interface MethodFilter {
Method[] filter(Method[] methods);
}

View File

@@ -100,7 +100,7 @@ public class MethodInvokingMessageProcessor implements MessageProcessor {
HandlerMethod handlerMethod = new HandlerMethod(method);
this.targetObject = targetObject;
this.handlerMethods = Collections.<Class<?>, HandlerMethod>singletonMap(handlerMethod.getTargetParameterType(), handlerMethod);
this.evaluationContext = this.createEvaluationContext(targetObject);
this.evaluationContext = this.createEvaluationContext(targetObject, method, annotationType);
}
private MethodInvokingMessageProcessor(Object targetObject, Class<? extends Annotation> annotationType, String methodName) {
@@ -111,15 +111,23 @@ public class MethodInvokingMessageProcessor implements MessageProcessor {
this.targetObject = targetObject;
this.requiresReply = requiresReply;
this.handlerMethods = this.findHandlerMethodsForTarget(targetObject, annotationType, methodName, requiresReply);
this.evaluationContext = this.createEvaluationContext(targetObject);
this.evaluationContext = this.createEvaluationContext(targetObject, methodName, annotationType);
}
private EvaluationContext createEvaluationContext(Object targetObject) {
private EvaluationContext createEvaluationContext(Object targetObject, Object method, Class<? extends Annotation> annotationType) {
StandardEvaluationContext context = new StandardEvaluationContext();
// TODO: Add a filtering MethodResolver (may soon be supported by SpEL) to enable:
// 1) exclusion of void-returning methods if requiresReply is true
// 2) limiting to annotated methods if at least one is present
// TODO: StandardEvaluationContext may soon provide a better way to *replace* the MethodResolver
context.getMethodResolvers().clear();
Class<?> targetType = AopUtils.getTargetClass(this.targetObject);
if (method instanceof Method) {
context.getMethodResolvers().add(new FilteringReflectiveMethodResolver(
new FixedHandlerMethodFilter((Method) method), targetType));
}
else if (method == null || method instanceof String) {
context.getMethodResolvers().add(new FilteringReflectiveMethodResolver(
new HandlerMethodFilter(annotationType, (String) method, this.requiresReply), targetType));
}
context.addPropertyAccessor(new MapAccessor());
// TODO: Enable configuration of an integration ConversionService bean to be used here,
// but then fallback to this same default if no such bean has been defined.

View File

@@ -28,6 +28,7 @@ import java.util.Properties;
import org.junit.Test;
import org.springframework.integration.annotation.Header;
import org.springframework.integration.annotation.ServiceActivator;
import org.springframework.integration.core.Message;
import org.springframework.integration.message.GenericMessage;
import org.springframework.integration.message.MessageBuilder;
@@ -114,7 +115,7 @@ public class MethodInvokingMessageProcessorTests {
processor.processMessage(MessageBuilder.withPayload("Something").build());
fail();
}
catch(IllegalArgumentException ex) {
catch(MessageHandlingException ex) {
exception = ex;
}
assertNotNull(exception);
@@ -178,6 +179,26 @@ public class MethodInvokingMessageProcessorTests {
assertEquals("bar-42", result);
}
@Test
public void filterSelectsAnnotationMethodsOnly() {
AmbiguousMethodBean bean = new AmbiguousMethodBean();
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(bean, ServiceActivator.class);
processor.processMessage(MessageBuilder.withPayload(123).build());
assertNotNull(bean.lastArg);
assertEquals(String.class, bean.lastArg.getClass());
assertEquals("123", bean.lastArg);
}
@Test
public void filterSelectsNonVoidReturningMethodsOnly() {
AmbiguousMethodBean bean = new AmbiguousMethodBean();
MethodInvokingMessageProcessor processor = new MethodInvokingMessageProcessor(bean, "foo", true);
processor.processMessage(MessageBuilder.withPayload(true).build());
assertNotNull(bean.lastArg);
assertEquals(String.class, bean.lastArg.getClass());
assertEquals("true", bean.lastArg);
}
@SuppressWarnings("unused")
private static class TestBean {
@@ -263,4 +284,31 @@ public class MethodInvokingMessageProcessorTests {
}
/**
* Method names create ambiguities, but the MethodResolver implementation
* should filter out based on the annotation or the 'requiresReply' flag.
*/
@SuppressWarnings("unused")
private static class AmbiguousMethodBean {
private volatile Object lastArg = null;
public void foo(boolean b) {
this.lastArg = b;
}
@ServiceActivator
public String foo(String s) {
this.lastArg = s;
return s;
}
public String foo(int i) {
this.lastArg = i;
return Integer.valueOf(i).toString();
}
}
}