diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/ResolvableMethod.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/ResolvableMethod.java new file mode 100644 index 0000000000..7f4a367370 --- /dev/null +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/ResolvableMethod.java @@ -0,0 +1,684 @@ +/* + * Copyright 2002-2018 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.messaging.handler; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInterceptor; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.aop.target.EmptyTargetSource; +import org.springframework.cglib.core.SpringNamingPolicy; +import org.springframework.cglib.proxy.Callback; +import org.springframework.cglib.proxy.Enhancer; +import org.springframework.cglib.proxy.Factory; +import org.springframework.cglib.proxy.MethodProxy; +import org.springframework.core.LocalVariableTableParameterNameDiscoverer; +import org.springframework.core.MethodIntrospector; +import org.springframework.core.MethodParameter; +import org.springframework.core.ParameterNameDiscoverer; +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.SynthesizingMethodParameter; +import org.springframework.lang.Nullable; +import org.springframework.objenesis.ObjenesisException; +import org.springframework.objenesis.SpringObjenesis; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; + +import static java.util.stream.Collectors.*; + +/** + * Convenience class to resolve method parameters from hints. + * + *

Background

+ * + *

When testing annotated methods we create test classes such as + * "TestController" with a diverse range of method signatures representing + * supported annotations and argument types. It becomes challenging to use + * naming strategies to keep track of methods and arguments especially in + * combination with variables for reflection metadata. + * + *

The idea with {@link ResolvableMethod} is NOT to rely on naming techniques + * but to use hints to zero in on method parameters. Such hints can be strongly + * typed and explicit about what is being tested. + * + *

1. Declared Return Type

+ * + * When testing return types it's likely to have many methods with a unique + * return type, possibly with or without an annotation. + * + *
+ * import static org.springframework.web.method.ResolvableMethod.on;
+ * import static org.springframework.web.method.MvcAnnotationPredicates.requestMapping;
+ *
+ * // Return type
+ * on(TestController.class).resolveReturnType(Foo.class);
+ * on(TestController.class).resolveReturnType(List.class, Foo.class);
+ * on(TestController.class).resolveReturnType(Mono.class, responseEntity(Foo.class));
+ *
+ * // Annotation + return type
+ * on(TestController.class).annotPresent(RequestMapping.class).resolveReturnType(Bar.class);
+ *
+ * // Annotation not present
+ * on(TestController.class).annotNotPresent(RequestMapping.class).resolveReturnType();
+ *
+ * // Annotation with attributes
+ * on(TestController.class).annot(requestMapping("/foo").params("p")).resolveReturnType();
+ * 
+ * + *

2. Method Arguments

+ * + * When testing method arguments it's more likely to have one or a small number + * of methods with a wide array of argument types and parameter annotations. + * + *
+ * import static org.springframework.web.method.MvcAnnotationPredicates.requestParam;
+ *
+ * ResolvableMethod testMethod = ResolvableMethod.on(getClass()).named("handle").build();
+ *
+ * testMethod.arg(Foo.class);
+ * testMethod.annotPresent(RequestParam.class).arg(Integer.class);
+ * testMethod.annotNotPresent(RequestParam.class)).arg(Integer.class);
+ * testMethod.annot(requestParam().name("c").notRequired()).arg(Integer.class);
+ * 
+ * + *

3. Mock Handler Method Invocation

+ * + * Locate a method by invoking it through a proxy of the target handler: + * + *
+ * ResolvableMethod.on(TestController.class).mockCall(o -> o.handle(null)).method();
+ * 
+ * + * @author Rossen Stoyanchev + * @since 5.0 + */ +public class ResolvableMethod { + + private static final Log logger = LogFactory.getLog(ResolvableMethod.class); + + private static final SpringObjenesis objenesis = new SpringObjenesis(); + + private static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + + // Matches ValueConstants.DEFAULT_NONE (spring-web and spring-messaging) + private static final String DEFAULT_VALUE_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; + + + private final Method method; + + + private ResolvableMethod(Method method) { + Assert.notNull(method, "'method' is required"); + this.method = method; + } + + + /** + * Return the resolved method. + */ + public Method method() { + return this.method; + } + + /** + * Return the declared return type of the resolved method. + */ + public MethodParameter returnType() { + return new SynthesizingMethodParameter(this.method, -1); + } + + /** + * Find a unique argument matching the given type. + * @param type the expected type + * @param generics optional array of generic types + */ + public MethodParameter arg(Class type, Class... generics) { + return new ArgResolver().arg(type, generics); + } + + /** + * Find a unique argument matching the given type. + * @param type the expected type + * @param generic at least one generic type + * @param generics optional array of generic types + */ + public MethodParameter arg(Class type, ResolvableType generic, ResolvableType... generics) { + return new ArgResolver().arg(type, generic, generics); + } + + /** + * Find a unique argument matching the given type. + * @param type the expected type + */ + public MethodParameter arg(ResolvableType type) { + return new ArgResolver().arg(type); + } + + /** + * Filter on method arguments with annotation. + */ + @SafeVarargs + public final ArgResolver annot(Predicate... filter) { + return new ArgResolver(filter); + } + + @SafeVarargs + public final ArgResolver annotPresent(Class... annotationTypes) { + return new ArgResolver().annotPresent(annotationTypes); + } + + /** + * Filter on method arguments that don't have the given annotation type(s). + * @param annotationTypes the annotation types + */ + @SafeVarargs + public final ArgResolver annotNotPresent(Class... annotationTypes) { + return new ArgResolver().annotNotPresent(annotationTypes); + } + + + @Override + public String toString() { + return "ResolvableMethod=" + formatMethod(); + } + + + private String formatMethod() { + return (method().getName() + + Arrays.stream(this.method.getParameters()) + .map(this::formatParameter) + .collect(joining(",\n\t", "(\n\t", "\n)"))); + } + + private String formatParameter(Parameter param) { + Annotation[] anns = param.getAnnotations(); + return (anns.length > 0 ? + Arrays.stream(anns).map(this::formatAnnotation).collect(joining(",", "[", "]")) + " " + param : + param.toString()); + } + + private String formatAnnotation(Annotation annotation) { + Map map = AnnotationUtils.getAnnotationAttributes(annotation); + map.forEach((key, value) -> { + if (value.equals(DEFAULT_VALUE_NONE)) { + map.put(key, "NONE"); + } + }); + return annotation.annotationType().getName() + map; + } + + private static ResolvableType toResolvableType(Class type, Class... generics) { + return (ObjectUtils.isEmpty(generics) ? ResolvableType.forClass(type) : + ResolvableType.forClassWithGenerics(type, generics)); + } + + private static ResolvableType toResolvableType(Class type, ResolvableType generic, ResolvableType... generics) { + ResolvableType[] genericTypes = new ResolvableType[generics.length + 1]; + genericTypes[0] = generic; + System.arraycopy(generics, 0, genericTypes, 1, generics.length); + return ResolvableType.forClassWithGenerics(type, genericTypes); + } + + + /** + * Create a {@code ResolvableMethod} builder for the given handler class. + */ + public static Builder on(Class objectClass) { + return new Builder<>(objectClass); + } + + + /** + * Builder for {@code ResolvableMethod}. + */ + public static class Builder { + + private final Class objectClass; + + private final List> filters = new ArrayList<>(4); + + + private Builder(Class objectClass) { + Assert.notNull(objectClass, "Class must not be null"); + this.objectClass = objectClass; + } + + + private void addFilter(String message, Predicate filter) { + this.filters.add(new LabeledPredicate<>(message, filter)); + } + + /** + * Filter on methods with the given name. + */ + public Builder named(String methodName) { + addFilter("methodName=" + methodName, method -> method.getName().equals(methodName)); + return this; + } + + /** + * Filter on methods with the given parameter types. + */ + public Builder argTypes(Class... argTypes) { + addFilter("argTypes=" + Arrays.toString(argTypes), method -> + ObjectUtils.isEmpty(argTypes) ? method.getParameterCount() == 0 : + Arrays.equals(method.getParameterTypes(), argTypes)); + return this; + } + + /** + * Filter on annotated methods. + */ + @SafeVarargs + public final Builder annot(Predicate... filters) { + this.filters.addAll(Arrays.asList(filters)); + return this; + } + + /** + * Filter on methods annotated with the given annotation type. + * @see #annot(Predicate[]) + */ + @SafeVarargs + public final Builder annotPresent(Class... annotationTypes) { + String message = "annotationPresent=" + Arrays.toString(annotationTypes); + addFilter(message, method -> + Arrays.stream(annotationTypes).allMatch(annotType -> + AnnotatedElementUtils.findMergedAnnotation(method, annotType) != null)); + return this; + } + + /** + * Filter on methods not annotated with the given annotation type. + */ + @SafeVarargs + public final Builder annotNotPresent(Class... annotationTypes) { + String message = "annotationNotPresent=" + Arrays.toString(annotationTypes); + addFilter(message, method -> { + if (annotationTypes.length != 0) { + return Arrays.stream(annotationTypes).noneMatch(annotType -> + AnnotatedElementUtils.findMergedAnnotation(method, annotType) != null); + } + else { + return method.getAnnotations().length == 0; + } + }); + return this; + } + + /** + * Filter on methods returning the given type. + * @param returnType the return type + * @param generics optional array of generic types + */ + public Builder returning(Class returnType, Class... generics) { + return returning(toResolvableType(returnType, generics)); + } + + /** + * Filter on methods returning the given type with generics. + * @param returnType the return type + * @param generic at least one generic type + * @param generics optional extra generic types + */ + public Builder returning(Class returnType, ResolvableType generic, ResolvableType... generics) { + return returning(toResolvableType(returnType, generic, generics)); + } + + /** + * Filter on methods returning the given type. + * @param returnType the return type + */ + public Builder returning(ResolvableType returnType) { + String expected = returnType.toString(); + String message = "returnType=" + expected; + addFilter(message, m -> expected.equals(ResolvableType.forMethodReturnType(m).toString())); + return this; + } + + /** + * Build a {@code ResolvableMethod} from the provided filters which must + * resolve to a unique, single method. + *

See additional resolveXxx shortcut methods going directly to + * {@link Method} or return type parameter. + * @throws IllegalStateException for no match or multiple matches + */ + public ResolvableMethod build() { + Set methods = MethodIntrospector.selectMethods(this.objectClass, this::isMatch); + Assert.state(!methods.isEmpty(), () -> "No matching method: " + this); + Assert.state(methods.size() == 1, () -> "Multiple matching methods: " + this + formatMethods(methods)); + return new ResolvableMethod(methods.iterator().next()); + } + + private boolean isMatch(Method method) { + return this.filters.stream().allMatch(p -> p.test(method)); + } + + private String formatMethods(Set methods) { + return "\nMatched:\n" + methods.stream() + .map(Method::toGenericString).collect(joining(",\n\t", "[\n\t", "\n]")); + } + + public ResolvableMethod mockCall(Consumer invoker) { + MethodInvocationInterceptor interceptor = new MethodInvocationInterceptor(); + T proxy = initProxy(this.objectClass, interceptor); + invoker.accept(proxy); + Method method = interceptor.getInvokedMethod(); + return new ResolvableMethod(method); + } + + + // Build & resolve shortcuts... + + /** + * Resolve and return the {@code Method} equivalent to: + *

{@code build().method()} + */ + public final Method resolveMethod() { + return build().method(); + } + + /** + * Resolve and return the {@code Method} equivalent to: + *

{@code named(methodName).build().method()} + */ + public Method resolveMethod(String methodName) { + return named(methodName).build().method(); + } + + /** + * Resolve and return the declared return type equivalent to: + *

{@code build().returnType()} + */ + public final MethodParameter resolveReturnType() { + return build().returnType(); + } + + /** + * Shortcut to the unique return type equivalent to: + *

{@code returning(returnType).build().returnType()} + * @param returnType the return type + * @param generics optional array of generic types + */ + public MethodParameter resolveReturnType(Class returnType, Class... generics) { + return returning(returnType, generics).build().returnType(); + } + + /** + * Shortcut to the unique return type equivalent to: + *

{@code returning(returnType).build().returnType()} + * @param returnType the return type + * @param generic at least one generic type + * @param generics optional extra generic types + */ + public MethodParameter resolveReturnType(Class returnType, ResolvableType generic, + ResolvableType... generics) { + + return returning(returnType, generic, generics).build().returnType(); + } + + public MethodParameter resolveReturnType(ResolvableType returnType) { + return returning(returnType).build().returnType(); + } + + + @Override + public String toString() { + return "ResolvableMethod.Builder[\n" + + "\tobjectClass = " + this.objectClass.getName() + ",\n" + + "\tfilters = " + formatFilters() + "\n]"; + } + + private String formatFilters() { + return this.filters.stream().map(Object::toString) + .collect(joining(",\n\t\t", "[\n\t\t", "\n\t]")); + } + } + + + /** + * Predicate with a descriptive label. + */ + private static class LabeledPredicate implements Predicate { + + private final String label; + + private final Predicate delegate; + + + private LabeledPredicate(String label, Predicate delegate) { + this.label = label; + this.delegate = delegate; + } + + + @Override + public boolean test(T method) { + return this.delegate.test(method); + } + + @Override + public Predicate and(Predicate other) { + return this.delegate.and(other); + } + + @Override + public Predicate negate() { + return this.delegate.negate(); + } + + @Override + public Predicate or(Predicate other) { + return this.delegate.or(other); + } + + @Override + public String toString() { + return this.label; + } + } + + + /** + * Resolver for method arguments. + */ + public class ArgResolver { + + private final List> filters = new ArrayList<>(4); + + + @SafeVarargs + private ArgResolver(Predicate... filter) { + this.filters.addAll(Arrays.asList(filter)); + } + + /** + * Filter on method arguments with annotations. + */ + @SafeVarargs + public final ArgResolver annot(Predicate... filters) { + this.filters.addAll(Arrays.asList(filters)); + return this; + } + + /** + * Filter on method arguments that have the given annotations. + * @param annotationTypes the annotation types + * @see #annot(Predicate[]) + */ + @SafeVarargs + public final ArgResolver annotPresent(Class... annotationTypes) { + this.filters.add(param -> Arrays.stream(annotationTypes).allMatch(param::hasParameterAnnotation)); + return this; + } + + /** + * Filter on method arguments that don't have the given annotations. + * @param annotationTypes the annotation types + */ + @SafeVarargs + public final ArgResolver annotNotPresent(Class... annotationTypes) { + this.filters.add(param -> + (annotationTypes.length > 0 ? + Arrays.stream(annotationTypes).noneMatch(param::hasParameterAnnotation) : + param.getParameterAnnotations().length == 0)); + return this; + } + + /** + * Resolve the argument also matching to the given type. + * @param type the expected type + */ + public MethodParameter arg(Class type, Class... generics) { + return arg(toResolvableType(type, generics)); + } + + /** + * Resolve the argument also matching to the given type. + * @param type the expected type + */ + public MethodParameter arg(Class type, ResolvableType generic, ResolvableType... generics) { + return arg(toResolvableType(type, generic, generics)); + } + + /** + * Resolve the argument also matching to the given type. + * @param type the expected type + */ + public MethodParameter arg(ResolvableType type) { + this.filters.add(p -> type.toString().equals(ResolvableType.forMethodParameter(p).toString())); + return arg(); + } + + /** + * Resolve the argument. + */ + public final MethodParameter arg() { + List matches = applyFilters(); + Assert.state(!matches.isEmpty(), () -> + "No matching arg in method\n" + formatMethod()); + Assert.state(matches.size() == 1, () -> + "Multiple matching args in method\n" + formatMethod() + "\nMatches:\n\t" + matches); + return matches.get(0); + } + + + private List applyFilters() { + List matches = new ArrayList<>(); + for (int i = 0; i < method.getParameterCount(); i++) { + MethodParameter param = new SynthesizingMethodParameter(method, i); + param.initParameterNameDiscovery(nameDiscoverer); + if (this.filters.stream().allMatch(p -> p.test(param))) { + matches.add(param); + } + } + return matches; + } + } + + + private static class MethodInvocationInterceptor + implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor { + + private Method invokedMethod; + + + Method getInvokedMethod() { + return this.invokedMethod; + } + + @Override + @Nullable + public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) { + if (ReflectionUtils.isObjectMethod(method)) { + return ReflectionUtils.invokeMethod(method, object, args); + } + else { + this.invokedMethod = method; + return null; + } + } + + @Override + @Nullable + public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable { + return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null); + } + } + + @SuppressWarnings("unchecked") + private static T initProxy(Class type, MethodInvocationInterceptor interceptor) { + Assert.notNull(type, "'type' must not be null"); + if (type.isInterface()) { + ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE); + factory.addInterface(type); + factory.addInterface(Supplier.class); + factory.addAdvice(interceptor); + return (T) factory.getProxy(); + } + + else { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(type); + enhancer.setInterfaces(new Class[] {Supplier.class}); + enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); + enhancer.setCallbackType(org.springframework.cglib.proxy.MethodInterceptor.class); + + Class proxyClass = enhancer.createClass(); + Object proxy = null; + + if (objenesis.isWorthTrying()) { + try { + proxy = objenesis.newInstance(proxyClass, enhancer.getUseCache()); + } + catch (ObjenesisException ex) { + logger.debug("Objenesis failed, falling back to default constructor", ex); + } + } + + if (proxy == null) { + try { + proxy = ReflectionUtils.accessibleConstructor(proxyClass).newInstance(); + } + catch (Throwable ex) { + throw new IllegalStateException("Unable to instantiate proxy " + + "via both Objenesis and default constructor fails as well", ex); + } + } + + ((Factory) proxy).setCallbacks(new Callback[] {interceptor}); + return (T) proxy; + } + } + +} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index 6a960ee956..fbb0350d1b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -21,8 +21,9 @@ import java.lang.reflect.Method; import org.junit.Test; import org.springframework.core.MethodParameter; +import org.springframework.lang.Nullable; import org.springframework.messaging.Message; -import org.springframework.util.ClassUtils; +import org.springframework.messaging.handler.ResolvableMethod; import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; @@ -37,15 +38,15 @@ public class InvocableHandlerMethodTests { private final Message message = mock(Message.class); - private final HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite(); + private final HandlerMethodArgumentResolverComposite resolvers = new HandlerMethodArgumentResolverComposite(); @Test public void resolveArg() throws Exception { - this.composite.addResolver(new StubArgumentResolver(99)); - this.composite.addResolver(new StubArgumentResolver("value")); - - Object value = getInvocable("handle", Integer.class, String.class).invoke(this.message); + this.resolvers.addResolver(new StubArgumentResolver(99)); + this.resolvers.addResolver(new StubArgumentResolver("value")); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); + Object value = invoke(new Handler(), method); assertEquals(1, getStubResolver(0).getResolvedParameters().size()); assertEquals(1, getStubResolver(1).getResolvedParameters().size()); @@ -56,20 +57,21 @@ public class InvocableHandlerMethodTests { @Test public void resolveNoArgValue() throws Exception { - this.composite.addResolver(new StubArgumentResolver(Integer.class)); - this.composite.addResolver(new StubArgumentResolver(String.class)); - - Object returnValue = getInvocable("handle", Integer.class, String.class).invoke(this.message); + this.resolvers.addResolver(new StubArgumentResolver(Integer.class)); + this.resolvers.addResolver(new StubArgumentResolver(String.class)); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); + Object value = invoke(new Handler(), method); assertEquals(1, getStubResolver(0).getResolvedParameters().size()); assertEquals(1, getStubResolver(1).getResolvedParameters().size()); - assertEquals("null-null", returnValue); + assertEquals("null-null", value); } @Test public void cannotResolveArg() throws Exception { try { - getInvocable("handle", Integer.class, String.class).invoke(this.message); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); + invoke(new Handler(), method); fail("Expected exception"); } catch (MethodArgumentResolutionException ex) { @@ -80,7 +82,8 @@ public class InvocableHandlerMethodTests { @Test public void resolveProvidedArg() throws Exception { - Object value = getInvocable("handle", Integer.class, String.class).invoke(this.message, 99, "value"); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); + Object value = invoke(new Handler(), method, 99, "value"); assertNotNull(value); assertEquals(String.class, value.getClass()); @@ -89,18 +92,20 @@ public class InvocableHandlerMethodTests { @Test public void resolveProvidedArgFirst() throws Exception { - this.composite.addResolver(new StubArgumentResolver(1)); - this.composite.addResolver(new StubArgumentResolver("value1")); - Object value = getInvocable("handle", Integer.class, String.class).invoke(this.message, 2, "value2"); + this.resolvers.addResolver(new StubArgumentResolver(1)); + this.resolvers.addResolver(new StubArgumentResolver("value1")); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); + Object value = invoke(new Handler(), method, 2, "value2"); assertEquals("2-value2", value); } @Test public void exceptionInResolvingArg() throws Exception { - this.composite.addResolver(new ExceptionRaisingArgumentResolver()); + this.resolvers.addResolver(new ExceptionRaisingArgumentResolver()); try { - getInvocable("handle", Integer.class, String.class).invoke(this.message); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); + invoke(new Handler(), method); fail("Expected exception"); } catch (IllegalArgumentException ex) { @@ -110,10 +115,11 @@ public class InvocableHandlerMethodTests { @Test public void illegalArgumentException() throws Exception { - this.composite.addResolver(new StubArgumentResolver(Integer.class, "__not_an_int__")); - this.composite.addResolver(new StubArgumentResolver("value")); + this.resolvers.addResolver(new StubArgumentResolver(Integer.class, "__not_an_int__")); + this.resolvers.addResolver(new StubArgumentResolver("value")); try { - getInvocable("handle", Integer.class, String.class).invoke(this.message); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0, "")).method(); + invoke(new Handler(), method); fail("Expected exception"); } catch (IllegalStateException ex) { @@ -129,10 +135,12 @@ public class InvocableHandlerMethodTests { @Test public void invocationTargetException() throws Exception { + Handler handler = new Handler(); + Method method = ResolvableMethod.on(Handler.class).argTypes(Throwable.class).resolveMethod(); Throwable expected = null; try { expected = new RuntimeException("error"); - getInvocable("handleWithException", Throwable.class).invoke(this.message, expected); + invoke(handler, method, expected); fail("Expected exception"); } catch (RuntimeException actual) { @@ -140,7 +148,7 @@ public class InvocableHandlerMethodTests { } try { expected = new Error("error"); - getInvocable("handleWithException", Throwable.class).invoke(this.message, expected); + invoke(handler, method, expected); fail("Expected exception"); } catch (Error actual) { @@ -148,15 +156,15 @@ public class InvocableHandlerMethodTests { } try { expected = new Exception("error"); - getInvocable("handleWithException", Throwable.class).invoke(this.message, expected); + invoke(handler, method, expected); fail("Expected exception"); } catch (Exception actual) { assertSame(expected, actual); } try { - expected = new Throwable("error"); - getInvocable("handleWithException", Throwable.class).invoke(this.message, expected); + expected = new Throwable("error", expected); + invoke(handler, method, expected); fail("Expected exception"); } catch (IllegalStateException actual) { @@ -168,9 +176,10 @@ public class InvocableHandlerMethodTests { @Test // Based on SPR-13917 (spring-web) public void invocationErrorMessage() throws Exception { - this.composite.addResolver(new StubArgumentResolver(double.class)); + this.resolvers.addResolver(new StubArgumentResolver(double.class)); try { - getInvocable("handle", double.class).invoke(this.message); + Method method = ResolvableMethod.on(Handler.class).mockCall(c -> c.handle(0.0)).method(); + invoke(new Handler(), method); fail(); } catch (IllegalStateException ex) { @@ -178,15 +187,15 @@ public class InvocableHandlerMethodTests { } } - public InvocableHandlerMethod getInvocable(String methodName, Class... argTypes) { - Method method = ClassUtils.getMethod(Handler.class, methodName, argTypes); - InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new Handler(), method); - handlerMethod.setMessageMethodArgumentResolvers(this.composite); - return handlerMethod; + @Nullable + private Object invoke(Object handler, Method method, Object... providedArgs) throws Exception { + InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); + handlerMethod.setMessageMethodArgumentResolvers(this.resolvers); + return handlerMethod.invoke(this.message, providedArgs); } private StubArgumentResolver getStubResolver(int index) { - return (StubArgumentResolver) this.composite.getResolvers().get(index); + return (StubArgumentResolver) this.resolvers.getResolvers().get(index); } diff --git a/spring-web/src/test/java/org/springframework/web/method/ResolvableMethod.java b/spring-web/src/test/java/org/springframework/web/method/ResolvableMethod.java index 984649f0af..edf2ab4ba7 100644 --- a/spring-web/src/test/java/org/springframework/web/method/ResolvableMethod.java +++ b/spring-web/src/test/java/org/springframework/web/method/ResolvableMethod.java @@ -53,7 +53,6 @@ import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; -import org.springframework.web.bind.annotation.ValueConstants; import static java.util.stream.Collectors.*; @@ -131,11 +130,15 @@ public class ResolvableMethod { private static final ParameterNameDiscoverer nameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); + // Matches ValueConstants.DEFAULT_NONE (spring-web and spring-messaging) + private static final String DEFAULT_VALUE_NONE = "\n\t\t\n\t\t\n\uE000\uE001\uE002\n\t\t\t\t\n"; + + private final Method method; private ResolvableMethod(Method method) { - Assert.notNull(method, "Method is required"); + Assert.notNull(method, "'method' is required"); this.method = method; } @@ -183,7 +186,7 @@ public class ResolvableMethod { /** * Filter on method arguments with annotation. - * See {@link MvcAnnotationPredicates}. + * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final ArgResolver annot(Predicate... filter) { @@ -204,6 +207,7 @@ public class ResolvableMethod { return new ArgResolver().annotNotPresent(annotationTypes); } + @Override public String toString() { return "ResolvableMethod=" + formatMethod(); @@ -227,7 +231,7 @@ public class ResolvableMethod { private String formatAnnotation(Annotation annotation) { Map map = AnnotationUtils.getAnnotationAttributes(annotation); map.forEach((key, value) -> { - if (value.equals(ValueConstants.DEFAULT_NONE)) { + if (value.equals(DEFAULT_VALUE_NONE)) { map.put(key, "NONE"); } }); @@ -264,11 +268,13 @@ public class ResolvableMethod { private final List> filters = new ArrayList<>(4); + private Builder(Class objectClass) { Assert.notNull(objectClass, "Class must not be null"); this.objectClass = objectClass; } + private void addFilter(String message, Predicate filter) { this.filters.add(new LabeledPredicate<>(message, filter)); } @@ -283,7 +289,6 @@ public class ResolvableMethod { /** * Filter on methods with the given parameter types. - * @since 5.1 */ public Builder argTypes(Class... argTypes) { addFilter("argTypes=" + Arrays.toString(argTypes), method -> @@ -294,7 +299,7 @@ public class ResolvableMethod { /** * Filter on annotated methods. - * See {@link MvcAnnotationPredicates}. + * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final Builder annot(Predicate... filters) { @@ -305,7 +310,7 @@ public class ResolvableMethod { /** * Filter on methods annotated with the given annotation type. * @see #annot(Predicate[]) - * @see MvcAnnotationPredicates + * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final Builder annotPresent(Class... annotationTypes) { @@ -395,6 +400,7 @@ public class ResolvableMethod { return new ResolvableMethod(method); } + // Build & resolve shortcuts... /** @@ -448,6 +454,7 @@ public class ResolvableMethod { return returning(returnType).build().returnType(); } + @Override public String toString() { return "ResolvableMethod.Builder[\n" + @@ -471,6 +478,7 @@ public class ResolvableMethod { private final Predicate delegate; + private LabeledPredicate(String label, Predicate delegate) { this.label = label; this.delegate = delegate; @@ -511,6 +519,7 @@ public class ResolvableMethod { private final List> filters = new ArrayList<>(4); + @SafeVarargs private ArgResolver(Predicate... filter) { this.filters.addAll(Arrays.asList(filter)); @@ -518,7 +527,7 @@ public class ResolvableMethod { /** * Filter on method arguments with annotations. - * See {@link MvcAnnotationPredicates}. + * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final ArgResolver annot(Predicate... filters) { @@ -530,7 +539,7 @@ public class ResolvableMethod { * Filter on method arguments that have the given annotations. * @param annotationTypes the annotation types * @see #annot(Predicate[]) - * @see MvcAnnotationPredicates + * See {@link org.springframework.web.method.MvcAnnotationPredicates}. */ @SafeVarargs public final ArgResolver annotPresent(Class... annotationTypes) { @@ -608,6 +617,7 @@ public class ResolvableMethod { private Method invokedMethod; + Method getInvokedMethod() { return this.invokedMethod; }