diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/handler/FilteringReflectiveMethodResolver.java b/org.springframework.integration/src/main/java/org/springframework/integration/handler/FilteringReflectiveMethodResolver.java new file mode 100644 index 0000000000..2394e73856 --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/handler/FilteringReflectiveMethodResolver.java @@ -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); + } + } + + } + +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/handler/FixedHandlerMethodFilter.java b/org.springframework.integration/src/main/java/org/springframework/integration/handler/FixedHandlerMethodFilter.java new file mode 100644 index 0000000000..476f47ae9a --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/handler/FixedHandlerMethodFilter.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.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]; + } + +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/handler/HandlerMethodFilter.java b/org.springframework.integration/src/main/java/org/springframework/integration/handler/HandlerMethodFilter.java new file mode 100644 index 0000000000..74a4323438 --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/handler/HandlerMethodFilter.java @@ -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: + *
    + *
  1. matching on method name, if available
  2. + *
  3. exclusion of void-returning methods if 'requiresReply' is true
  4. + *
  5. limiting to annotated methods if at least one is present
  6. + *
+ *

+ * + * @author Mark Fisher + * @since 2.0 + */ +class HandlerMethodFilter implements MethodFilter { + + private final Class annotationType; + + private final String methodName; + + private final boolean requiresReply; + + + public HandlerMethodFilter(Class annotationType, String methodName, boolean requiresReply) { + this.annotationType = annotationType; + this.methodName = methodName; + this.requiresReply = requiresReply; + } + + + public Method[] filter(Method[] methods) { + List annotatedCandidates = new ArrayList(); + List fallbackCandidates = new ArrayList(); + 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()])); + } + +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/handler/MethodFilter.java b/org.springframework.integration/src/main/java/org/springframework/integration/handler/MethodFilter.java new file mode 100644 index 0000000000..393e93d63c --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/handler/MethodFilter.java @@ -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); + +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/handler/MethodInvokingMessageProcessor.java b/org.springframework.integration/src/main/java/org/springframework/integration/handler/MethodInvokingMessageProcessor.java index 722c20734e..3f6c09def6 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/handler/MethodInvokingMessageProcessor.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/handler/MethodInvokingMessageProcessor.java @@ -100,7 +100,7 @@ public class MethodInvokingMessageProcessor implements MessageProcessor { HandlerMethod handlerMethod = new HandlerMethod(method); this.targetObject = targetObject; this.handlerMethods = Collections., HandlerMethod>singletonMap(handlerMethod.getTargetParameterType(), handlerMethod); - this.evaluationContext = this.createEvaluationContext(targetObject); + this.evaluationContext = this.createEvaluationContext(targetObject, method, annotationType); } private MethodInvokingMessageProcessor(Object targetObject, Class 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 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. diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java index 42a0d5fee8..cfd06af6b3 100644 --- a/org.springframework.integration/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java +++ b/org.springframework.integration/src/test/java/org/springframework/integration/handler/MethodInvokingMessageProcessorTests.java @@ -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(); + } + + } + }