INT-828, INT-936 Created a FilteringReflectiveMethodResolver for use in the SpEL expression evaluation.
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()]));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user