diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/endpoint/ServiceActivatorEndpoint.java b/org.springframework.integration/src/main/java/org/springframework/integration/endpoint/ServiceActivatorEndpoint.java index d0218c584e..7e7ab5fea0 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/endpoint/ServiceActivatorEndpoint.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/endpoint/ServiceActivatorEndpoint.java @@ -23,7 +23,8 @@ import org.springframework.integration.annotation.ServiceActivator; import org.springframework.integration.message.Message; import org.springframework.integration.message.MessageHandlingException; import org.springframework.integration.message.MessageMappingMethodInvoker; -import org.springframework.integration.util.MethodUtils; +import org.springframework.integration.util.DefaultMethodResolver; +import org.springframework.integration.util.MethodResolver; import org.springframework.integration.util.MethodInvoker; import org.springframework.util.Assert; @@ -32,8 +33,7 @@ import org.springframework.util.Assert; */ public class ServiceActivatorEndpoint extends AbstractMessageHandlingEndpoint { - public static final String DEFAULT_LISTENER_METHOD = "handle"; - + private final MethodResolver methodResolver = new DefaultMethodResolver(ServiceActivator.class); private final MethodInvoker invoker; @@ -45,13 +45,7 @@ public class ServiceActivatorEndpoint extends AbstractMessageHandlingEndpoint { public ServiceActivatorEndpoint(final Object object) { Assert.notNull(object, "object must not be null"); - Method method = MethodUtils.findMethodWithAnnotation(object.getClass(), ServiceActivator.class); - if (method == null) { - Method[] methods = MethodUtils.findPublicMethods(object.getClass(), false); - if (methods.length == 1) { - method = methods[0]; - } - } + Method method = this.methodResolver.findMethod(object.getClass()); Assert.notNull(method, "unable to resolve ServiceActivator method on target class [" + object.getClass() + "]"); this.invoker = new MessageMappingMethodInvoker(object, method); diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/message/MessageMappingMethodInvoker.java b/org.springframework.integration/src/main/java/org/springframework/integration/message/MessageMappingMethodInvoker.java index e7a6664d26..773147c0ee 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/message/MessageMappingMethodInvoker.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/message/MessageMappingMethodInvoker.java @@ -16,7 +16,6 @@ package org.springframework.integration.message; -import java.lang.annotation.Annotation; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; @@ -26,7 +25,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.integration.util.DefaultMethodInvoker; import org.springframework.integration.util.MethodInvoker; import org.springframework.integration.util.NameResolvingMethodInvoker; @@ -55,8 +53,6 @@ public class MessageMappingMethodInvoker implements MethodInvoker, InitializingB private volatile String methodName; - private volatile Class annotationType; - private volatile OutboundMessageMapper messageMapper; private volatile MethodInvoker invoker; @@ -81,12 +77,6 @@ public class MessageMappingMethodInvoker implements MethodInvoker, InitializingB this.methodName = methodName; } - public MessageMappingMethodInvoker(Object object, Class annotationType) { - Assert.notNull(object, "object must not be null"); - this.object = object; - this.annotationType = annotationType; - } - public void afterPropertiesSet() { synchronized (this.initializationMonitor) { @@ -97,38 +87,18 @@ public class MessageMappingMethodInvoker implements MethodInvoker, InitializingB final List candidates = new ArrayList(); ReflectionUtils.doWithMethods(this.object.getClass(), new ReflectionUtils.MethodCallback() { public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - if (MessageMappingMethodInvoker.this.methodName != null) { - if (method.getName().equals(MessageMappingMethodInvoker.this.methodName)) { - candidates.add(method); - } - } - else if (MessageMappingMethodInvoker.this.annotationType != null) { - if (AnnotationUtils.findAnnotation(method, annotationType) != null) { - candidates.add(method); - } + if (method.getName().equals(MessageMappingMethodInvoker.this.methodName)) { + candidates.add(method); } } }); if (candidates.size() == 0) { - String clause = ""; - if (this.methodName != null) { - clause = " matching method name '" + this.methodName + "'"; - } - else if (this.annotationType != null) { - clause = " matching annotation type '" + this.annotationType + "'"; - } - throw new IllegalArgumentException("unable to find a candidate method" - + clause + " on target class [" + this.object.getClass() + "]"); + throw new IllegalArgumentException("unable to find a candidate method named '" + + this.methodName + "'" + " on target class [" + this.object.getClass() + "]"); } else if (candidates.size() == 1) { this.method = candidates.get(0); } - else if (this.annotationType != null) { - throw new IllegalArgumentException("unable to resolve method for annotation [" - + this.annotationType + "], found " + candidates.size() - + " candidates on target class [" + this.object.getClass() + "]: " - + candidates); - } } if (this.method != null) { Class[] parameterTypes = this.method.getParameterTypes(); diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/util/MethodUtils.java b/org.springframework.integration/src/main/java/org/springframework/integration/util/AnnotationMethodResolver.java similarity index 65% rename from org.springframework.integration/src/main/java/org/springframework/integration/util/MethodUtils.java rename to org.springframework.integration/src/main/java/org/springframework/integration/util/AnnotationMethodResolver.java index 9602e1eeb0..19c4a8cb69 100644 --- a/org.springframework.integration/src/main/java/org/springframework/integration/util/MethodUtils.java +++ b/org.springframework.integration/src/main/java/org/springframework/integration/util/AnnotationMethodResolver.java @@ -17,25 +17,42 @@ package org.springframework.integration.util; import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; /** - * Helper methods for detecting Methods. + * MethodResolver implementation that finds a single Method on the + * given Class that contains the specified annotation type. * * @author Mark Fisher */ -public abstract class MethodUtils { +public class AnnotationMethodResolver implements MethodResolver { + + private Class annotationType; + + + /** + * Create a MethodResolver for the specified Method-level annotation type + */ + public AnnotationMethodResolver(Class annotationType) { + Assert.notNull(annotationType, "annotationType must not be null"); + Assert.isTrue(ObjectUtils.containsElement( + annotationType.getAnnotation(Target.class).value(), ElementType.METHOD), + "Annotation [" + annotationType + "] is not a Method-level annotation."); + this.annotationType = annotationType; + } + /** * Find a single Method on the given Class that contains the - * specified annotation type. + * annotation type for which this resolver is searching. * * @param clazz the Class instance to check for the annotation * @param annotationType the Method-level annotation type @@ -46,12 +63,11 @@ public abstract class MethodUtils { * @throws IllegalArgumentException if more than one Method has the * specified annotation */ - public static Method findMethodWithAnnotation( - final Class clazz, final Class annotationType) { + public Method findMethod(final Class clazz) { final AtomicReference annotatedMethod = new AtomicReference(); ReflectionUtils.doWithMethods(clazz, new ReflectionUtils.MethodCallback() { public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException { - T annotation = AnnotationUtils.findAnnotation(method, annotationType); + Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType); if (annotation != null) { Assert.isNull(annotatedMethod.get(), "found more than one method on target class [" + clazz + "] with the annotation type [" + annotationType + "]"); @@ -62,25 +78,4 @@ public abstract class MethodUtils { return annotatedMethod.get(); } - /** - * Find all public Methods of a given Class. - * - * @param clazz the class to search - * @param includeMethodsDeclaredOnObject whether to include Methods - * that are declared on the Object class - * - * @return array of public Methods - */ - public static Method[] findPublicMethods( - final Class clazz, final boolean includeMethodsDeclaredOnObject) { - final List methods = new ArrayList(); - for (Method method : clazz.getMethods()) { - if (includeMethodsDeclaredOnObject - || !method.getDeclaringClass().equals(Object.class)) { - methods.add(method); - } - } - return methods.toArray(new Method[methods.size()]); - } - } diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/util/DefaultMethodResolver.java b/org.springframework.integration/src/main/java/org/springframework/integration/util/DefaultMethodResolver.java new file mode 100644 index 0000000000..ed0c953021 --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/util/DefaultMethodResolver.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2008 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.util; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.springframework.util.Assert; + +/** + * Default MethodResolver implementation. It first checks for a single Method + * with the specified annotation (if not null), and then falls back to a single + * public Method if available. + * + * @author Mark Fisher + */ +public class DefaultMethodResolver implements MethodResolver { + + private final AnnotationMethodResolver annotationMethodResolver; + + + public DefaultMethodResolver() { + this(null); + } + + public DefaultMethodResolver(Class annotationType) { + this.annotationMethodResolver = (annotationType != null) ? + new AnnotationMethodResolver(annotationType) : null; + } + + + public Method findMethod(Class clazz) { + Assert.notNull(clazz, "Class must not be null"); + if (this.annotationMethodResolver != null) { + Method method = this.annotationMethodResolver.findMethod(clazz); + if (method != null) { + return method; + } + } + return this.findSinglePublicMethod(clazz); + } + + private Method findSinglePublicMethod(Class clazz) { + Method result = null; + for (Method method : clazz.getMethods()) { + if (!method.getDeclaringClass().equals(Object.class)) { + if (result != null) { + throw new IllegalArgumentException( + "Class [" + clazz + "] contains more than one public Method."); + } + result = method; + } + } + return result; + } + +} diff --git a/org.springframework.integration/src/main/java/org/springframework/integration/util/MethodResolver.java b/org.springframework.integration/src/main/java/org/springframework/integration/util/MethodResolver.java new file mode 100644 index 0000000000..ddbd4024da --- /dev/null +++ b/org.springframework.integration/src/main/java/org/springframework/integration/util/MethodResolver.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2008 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.util; + +import java.lang.reflect.Method; + +/** + * Strategy interface for detecting a single Method on a Class. + * + * @author Mark Fisher + */ +public interface MethodResolver { + + /** + * Find a single Method on the provided Class that matches this resolver's + * criteria. + * + * @param clazz the Class on which to search for a Method + * + * @return a single Method or null if no Method matching this + * resolver's criteria can be found. + * + * @throws IllegalArgumentException if more than one Method defined on the + * given Class matches this resolver's criteria + */ + Method findMethod(Class clazz) throws IllegalArgumentException; + +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/endpoint/ServiceActivatorMethodResolutionTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/endpoint/ServiceActivatorMethodResolutionTests.java new file mode 100644 index 0000000000..ac1ea5f2e1 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/endpoint/ServiceActivatorMethodResolutionTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2008 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.endpoint; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +import org.springframework.integration.annotation.ServiceActivator; +import org.springframework.integration.channel.QueueChannel; +import org.springframework.integration.message.Message; +import org.springframework.integration.message.StringMessage; + +/** + * @author Mark Fisher + */ +public class ServiceActivatorMethodResolutionTests { + + @Test + public void singleAnnotationMatches() { + SingleAnnotationTestBean testBean = new SingleAnnotationTestBean(); + ServiceActivatorEndpoint endpoint = new ServiceActivatorEndpoint(testBean); + QueueChannel outputChannel = new QueueChannel(); + endpoint.setOutputChannel(outputChannel); + endpoint.afterPropertiesSet(); + endpoint.onMessage(new StringMessage("foo")); + Message result = outputChannel.receive(0); + assertEquals("FOO", result.getPayload()); + } + + @Test(expected = IllegalArgumentException.class) + public void multipleAnnotationFails() { + MultipleAnnotationTestBean testBean = new MultipleAnnotationTestBean(); + new ServiceActivatorEndpoint(testBean); + } + + @Test + public void singlePublicMethodMatches() { + SinglePublicMethodTestBean testBean = new SinglePublicMethodTestBean(); + ServiceActivatorEndpoint endpoint = new ServiceActivatorEndpoint(testBean); + QueueChannel outputChannel = new QueueChannel(); + endpoint.setOutputChannel(outputChannel); + endpoint.afterPropertiesSet(); + endpoint.onMessage(new StringMessage("foo")); + Message result = outputChannel.receive(0); + assertEquals("FOO", result.getPayload()); + } + + @Test(expected = IllegalArgumentException.class) + public void multiplePublicMethodFails() { + MultiplePublicMethodTestBean testBean = new MultiplePublicMethodTestBean(); + new ServiceActivatorEndpoint(testBean); + } + + + private static class SingleAnnotationTestBean { + + @ServiceActivator + public String upperCase(String s) { + return s.toUpperCase(); + } + + public String lowerCase(String s) { + return s.toLowerCase(); + } + } + + + private static class MultipleAnnotationTestBean { + + @ServiceActivator + public String upperCase(String s) { + return s.toUpperCase(); + } + + @ServiceActivator + public String lowerCase(String s) { + return s.toLowerCase(); + } + } + + + private static class SinglePublicMethodTestBean { + + public String upperCase(String s) { + return s.toUpperCase(); + } + + String lowerCase(String s) { + return s.toLowerCase(); + } + } + + + private static class MultiplePublicMethodTestBean { + + public String upperCase(String s) { + return s.toUpperCase(); + } + + public String lowerCase(String s) { + return s.toLowerCase(); + } + } + +} diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/message/MessageMappingMethodInvokerAnnotationTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/util/AnnotationMethodResolverTests.java similarity index 56% rename from org.springframework.integration/src/test/java/org/springframework/integration/message/MessageMappingMethodInvokerAnnotationTests.java rename to org.springframework.integration/src/test/java/org/springframework/integration/util/AnnotationMethodResolverTests.java index 357f95260e..d8adc3df99 100644 --- a/org.springframework.integration/src/test/java/org/springframework/integration/message/MessageMappingMethodInvokerAnnotationTests.java +++ b/org.springframework.integration/src/test/java/org/springframework/integration/util/AnnotationMethodResolverTests.java @@ -14,38 +14,42 @@ * limitations under the License. */ -package org.springframework.integration.message; +package org.springframework.integration.util; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.lang.reflect.Method; import org.junit.Test; /** * @author Mark Fisher */ -public class MessageMappingMethodInvokerAnnotationTests { +public class AnnotationMethodResolverTests { @Test - public void singleAnnotationMatches() { - SingleAnnotationTestBean testBean = new SingleAnnotationTestBean(); - MessageMappingMethodInvoker invoker = - new MessageMappingMethodInvoker(testBean, TestAnnotation.class); - invoker.afterPropertiesSet(); - String result = (String) invoker.invokeMethod("foo"); - assertEquals("FOO", result); + public void singleAnnotation() { + AnnotationMethodResolver resolver = new AnnotationMethodResolver(TestAnnotation.class); + Method method = resolver.findMethod(SingleAnnotationTestBean.class); + assertNotNull(method); } @Test(expected = IllegalArgumentException.class) - public void multipleAnnotationMatches() { - MultipleAnnotationTestBean testBean = new MultipleAnnotationTestBean(); - MessageMappingMethodInvoker invoker = - new MessageMappingMethodInvoker(testBean, TestAnnotation.class); - invoker.afterPropertiesSet(); + public void multipleAnnotations() { + AnnotationMethodResolver resolver = new AnnotationMethodResolver(TestAnnotation.class); + resolver.findMethod(MultipleAnnotationTestBean.class); + } + + @Test + public void noAnnotations() { + AnnotationMethodResolver resolver = new AnnotationMethodResolver(TestAnnotation.class); + Method method = resolver.findMethod(NoAnnotationTestBean.class); + assertNull(method); } @@ -61,6 +65,10 @@ public class MessageMappingMethodInvokerAnnotationTests { public String upperCase(String s) { return s.toUpperCase(); } + + public String lowerCase(String s) { + return s.toLowerCase(); + } } @@ -77,4 +85,16 @@ public class MessageMappingMethodInvokerAnnotationTests { } } + + private static class NoAnnotationTestBean { + + public String upperCase(String s) { + return s.toUpperCase(); + } + + String lowerCase(String s) { + return s.toLowerCase(); + } + } + } diff --git a/org.springframework.integration/src/test/java/org/springframework/integration/util/DefaultMethodResolverTests.java b/org.springframework.integration/src/test/java/org/springframework/integration/util/DefaultMethodResolverTests.java new file mode 100644 index 0000000000..cef1bd7147 --- /dev/null +++ b/org.springframework.integration/src/test/java/org/springframework/integration/util/DefaultMethodResolverTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2008 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.util; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.reflect.Method; + +import org.junit.Test; + +/** + * @author Mark Fisher + */ +public class DefaultMethodResolverTests { + + @Test + public void singleAnnotation() { + DefaultMethodResolver resolver = new DefaultMethodResolver(TestAnnotation.class); + Method method = resolver.findMethod(SingleAnnotationTestBean.class); + assertNotNull(method); + } + + @Test(expected = IllegalArgumentException.class) + public void multipleAnnotations() { + DefaultMethodResolver resolver = new DefaultMethodResolver(TestAnnotation.class); + resolver.findMethod(MultipleAnnotationTestBean.class); + } + + @Test + public void singlePublicMethod() { + DefaultMethodResolver resolver = new DefaultMethodResolver(TestAnnotation.class); + Method method = resolver.findMethod(SinglePublicMethodTestBean.class); + assertNotNull(method); + } + + @Test(expected = IllegalArgumentException.class) + public void multiplePublicMethods() { + DefaultMethodResolver resolver = new DefaultMethodResolver(TestAnnotation.class); + resolver.findMethod(MultiplePublicMethodTestBean.class); + } + + @Test + public void noPublicMethods() { + DefaultMethodResolver resolver = new DefaultMethodResolver(TestAnnotation.class); + Method method = resolver.findMethod(NoPublicMethodTestBean.class); + assertNull(method); + } + + + @Target(ElementType.METHOD) + @Retention(RetentionPolicy.RUNTIME) + private static @interface TestAnnotation { + } + + + private static class SingleAnnotationTestBean { + + @TestAnnotation + public String upperCase(String s) { + return s.toUpperCase(); + } + + public String lowerCase(String s) { + return s.toLowerCase(); + } + } + + + private static class MultipleAnnotationTestBean { + + @TestAnnotation + public String upperCase(String s) { + return s.toUpperCase(); + } + + @TestAnnotation + public String lowerCase(String s) { + return s.toLowerCase(); + } + } + + + private static class SinglePublicMethodTestBean { + + public String upperCase(String s) { + return s.toUpperCase(); + } + + String lowerCase(String s) { + return s.toLowerCase(); + } + } + + + private static class MultiplePublicMethodTestBean { + + public String upperCase(String s) { + return s.toUpperCase(); + } + + public String lowerCase(String s) { + return s.toLowerCase(); + } + } + + + private static class NoPublicMethodTestBean { + + String lowerCase(String s) { + return s.toLowerCase(); + } + } + +}