Consistent InvocableHandlerMethod implementations

This commit makes the 3 existing InvocableHandlerMethod types more
consistent and comparable with each other.

1. Use of consistent method names and method order.

2. Consistent error formatting.

3. Explicit for loops for resolving argument values in webflux variant
because that makes it more readable, creates less garabage, and it's
the only way to bring consistency since the other two variants cannot
throw exceptions inside Optional lambdas (vs webflux variant which can
wrap it in a Mono).

4. Use package private HandlerMethodArgumentComposite in webflux
variant in order to pick up the resolver argument caching that the
other two variants have.

5. Polish tests.

6. Add missing tests for messaging variant.
This commit is contained in:
Rossen Stoyanchev
2018-10-30 16:36:01 -04:00
parent 991e9f4269
commit 7c36549e3a
14 changed files with 795 additions and 386 deletions

View File

@@ -246,7 +246,7 @@ public class ResolvableMethod {
/**
* Main entry point providing access to a {@code ResolvableMethod} builder.
* Create a {@code ResolvableMethod} builder for the given handler class.
*/
public static <T> Builder<T> on(Class<T> objectClass) {
return new Builder<>(objectClass);
@@ -281,6 +281,16 @@ public class ResolvableMethod {
return this;
}
/**
* Filter on methods with the given parameter types.
*/
public Builder<T> argTypes(Class<?>... argTypes) {
addFilter("argTypes=" + Arrays.toString(argTypes), m ->
ObjectUtils.isEmpty(argTypes) ?
m.getParameterTypes().length == 0 : Arrays.equals(m.getParameterTypes(), argTypes));
return this;
}
/**
* Filter on annotated methods.
* See {@link MvcAnnotationPredicates}.

View File

@@ -32,56 +32,53 @@ import static org.junit.Assert.*;
*/
public class HandlerMethodArgumentResolverCompositeTests {
private HandlerMethodArgumentResolverComposite resolvers;
private HandlerMethodArgumentResolverComposite resolverComposite;
private MethodParameter paramInt;
private MethodParameter paramStr;
@Before
public void setUp() throws Exception {
resolvers = new HandlerMethodArgumentResolverComposite();
this.resolverComposite = new HandlerMethodArgumentResolverComposite();
Method method = getClass().getDeclaredMethod("handle", Integer.class, String.class);
paramInt = new MethodParameter(method, 0);
paramStr = new MethodParameter(method, 1);
}
@Test
public void supportsParameter() throws Exception {
registerResolver(Integer.class, null);
assertTrue(this.resolvers.supportsParameter(paramInt));
assertFalse(this.resolvers.supportsParameter(paramStr));
@Test
public void supportsParameter() {
this.resolverComposite.addResolver(new StubArgumentResolver(Integer.class));
assertTrue(this.resolverComposite.supportsParameter(paramInt));
assertFalse(this.resolverComposite.supportsParameter(paramStr));
}
@Test
public void resolveArgument() throws Exception {
registerResolver(Integer.class, Integer.valueOf(55));
Object resolvedValue = this.resolvers.resolveArgument(paramInt, null, null, null);
this.resolverComposite.addResolver(new StubArgumentResolver(55));
Object resolvedValue = this.resolverComposite.resolveArgument(paramInt, null, null, null);
assertEquals(Integer.valueOf(55), resolvedValue);
assertEquals(55, resolvedValue);
}
@Test
public void checkArgumentResolverOrder() throws Exception {
registerResolver(Integer.class, Integer.valueOf(1));
registerResolver(Integer.class, Integer.valueOf(2));
Object resolvedValue = this.resolvers.resolveArgument(paramInt, null, null, null);
this.resolverComposite.addResolver(new StubArgumentResolver(1));
this.resolverComposite.addResolver(new StubArgumentResolver(2));
Object resolvedValue = this.resolverComposite.resolveArgument(paramInt, null, null, null);
assertEquals("Didn't use the first registered resolver", Integer.valueOf(1), resolvedValue);
assertEquals("Didn't use the first registered resolver", 1, resolvedValue);
}
@Test(expected = IllegalArgumentException.class)
public void noSuitableArgumentResolver() throws Exception {
this.resolvers.resolveArgument(paramStr, null, null, null);
this.resolverComposite.resolveArgument(paramStr, null, null, null);
}
protected StubArgumentResolver registerResolver(Class<?> supportedType, Object stubValue) {
StubArgumentResolver resolver = new StubArgumentResolver(supportedType, stubValue);
this.resolvers.addResolver(resolver);
return resolver;
}
@SuppressWarnings("unused")
private void handle(Integer arg1, String arg2) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* 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.
@@ -22,74 +22,64 @@ import org.junit.Before;
import org.junit.Test;
import org.springframework.core.MethodParameter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.mock.web.test.MockHttpServletRequest;
import org.springframework.mock.web.test.MockHttpServletResponse;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.ResolvableMethod;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
/**
* Test fixture for {@link InvocableHandlerMethod} unit tests.
* Unit tests for {@link InvocableHandlerMethod}.
*
* @author Rossen Stoyanchev
*/
public class InvocableHandlerMethodTests {
private InvocableHandlerMethod handlerMethod;
private NativeWebRequest request;
private NativeWebRequest webRequest;
private final HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
@Before
public void setUp() throws Exception {
Method method = Handler.class.getDeclaredMethod("handle", Integer.class, String.class);
this.handlerMethod = new InvocableHandlerMethod(new Handler(), method);
this.webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
this.request = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
}
@Test
public void resolveArg() throws Exception {
StubArgumentResolver intResolver = new StubArgumentResolver(Integer.class, 99);
StubArgumentResolver stringResolver = new StubArgumentResolver(String.class, "value");
this.composite.addResolver(new StubArgumentResolver(99));
this.composite.addResolver(new StubArgumentResolver("value"));
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolver(intResolver);
composite.addResolver(stringResolver);
handlerMethod.setHandlerMethodArgumentResolvers(composite);
Object value = getInvocable(Integer.class, String.class).invokeForRequest(request, null);
Object returnValue = handlerMethod.invokeForRequest(webRequest, null);
assertEquals(1, intResolver.getResolvedParameters().size());
assertEquals(1, stringResolver.getResolvedParameters().size());
assertEquals("99-value", returnValue);
assertEquals("intArg", intResolver.getResolvedParameters().get(0).getParameterName());
assertEquals("stringArg", stringResolver.getResolvedParameters().get(0).getParameterName());
assertEquals(1, getStubResolver(0).getResolvedParameters().size());
assertEquals(1, getStubResolver(1).getResolvedParameters().size());
assertEquals("99-value", value);
assertEquals("intArg", getStubResolver(0).getResolvedParameters().get(0).getParameterName());
assertEquals("stringArg", getStubResolver(1).getResolvedParameters().get(0).getParameterName());
}
@Test
public void resolveNullArg() throws Exception {
StubArgumentResolver intResolver = new StubArgumentResolver(Integer.class, null);
StubArgumentResolver stringResolver = new StubArgumentResolver(String.class, null);
this.composite.addResolver(new StubArgumentResolver(Integer.class));
this.composite.addResolver(new StubArgumentResolver(String.class));
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolver(intResolver);
composite.addResolver(stringResolver);
handlerMethod.setHandlerMethodArgumentResolvers(composite);
Object returnValue = getInvocable(Integer.class, String.class).invokeForRequest(request, null);
Object returnValue = handlerMethod.invokeForRequest(webRequest, null);
assertEquals(1, intResolver.getResolvedParameters().size());
assertEquals(1, stringResolver.getResolvedParameters().size());
assertEquals(1, getStubResolver(0).getResolvedParameters().size());
assertEquals(1, getStubResolver(1).getResolvedParameters().size());
assertEquals("null-null", returnValue);
}
@Test
public void cannotResolveArg() throws Exception {
try {
handlerMethod.invokeForRequest(webRequest, null);
getInvocable(Integer.class, String.class).invokeForRequest(request, null);
fail("Expected exception");
}
catch (IllegalStateException ex) {
@@ -99,53 +89,40 @@ public class InvocableHandlerMethodTests {
@Test
public void resolveProvidedArg() throws Exception {
Object returnValue = handlerMethod.invokeForRequest(webRequest, null, 99, "value");
Object value = getInvocable(Integer.class, String.class).invokeForRequest(request, null, 99, "value");
assertEquals(String.class, returnValue.getClass());
assertEquals("99-value", returnValue);
assertNotNull(value);
assertEquals(String.class, value.getClass());
assertEquals("99-value", value);
}
@Test
public void resolveProvidedArgFirst() throws Exception {
StubArgumentResolver intResolver = new StubArgumentResolver(Integer.class, 1);
StubArgumentResolver stringResolver = new StubArgumentResolver(String.class, "value1");
this.composite.addResolver(new StubArgumentResolver(1));
this.composite.addResolver(new StubArgumentResolver("value1"));
Object value = getInvocable(Integer.class, String.class).invokeForRequest(request, null, 2, "value2");
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolver(intResolver);
composite.addResolver(stringResolver);
handlerMethod.setHandlerMethodArgumentResolvers(composite);
Object returnValue = handlerMethod.invokeForRequest(webRequest, null, 2, "value2");
assertEquals("2-value2", returnValue);
assertEquals("2-value2", value);
}
@Test
public void exceptionInResolvingArg() throws Exception {
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolver(new ExceptionRaisingArgumentResolver());
handlerMethod.setHandlerMethodArgumentResolvers(composite);
this.composite.addResolver(new ExceptionRaisingArgumentResolver());
try {
handlerMethod.invokeForRequest(webRequest, null);
getInvocable(Integer.class, String.class).invokeForRequest(request, null);
fail("Expected exception");
}
catch (HttpMessageNotReadableException ex) {
catch (IllegalArgumentException ex) {
// expected - allow HandlerMethodArgumentResolver exceptions to propagate
}
}
@Test
public void illegalArgumentException() throws Exception {
StubArgumentResolver intResolver = new StubArgumentResolver(Integer.class, "__invalid__");
StubArgumentResolver stringResolver = new StubArgumentResolver(String.class, "value");
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolver(intResolver);
composite.addResolver(stringResolver);
handlerMethod.setHandlerMethodArgumentResolvers(composite);
this.composite.addResolver(new StubArgumentResolver(Integer.class, "__not_an_int__"));
this.composite.addResolver(new StubArgumentResolver("value"));
try {
handlerMethod.invokeForRequest(webRequest, null);
getInvocable(Integer.class, String.class).invokeForRequest(request, null);
fail("Expected exception");
}
catch (IllegalStateException ex) {
@@ -154,7 +131,7 @@ public class InvocableHandlerMethodTests {
assertTrue(ex.getMessage().contains("Controller ["));
assertTrue(ex.getMessage().contains("Method ["));
assertTrue(ex.getMessage().contains("with argument values:"));
assertTrue(ex.getMessage().contains("[0] [type=java.lang.String] [value=__invalid__]"));
assertTrue(ex.getMessage().contains("[0] [type=java.lang.String] [value=__not_an_int__]"));
assertTrue(ex.getMessage().contains("[1] [type=java.lang.String] [value=value"));
}
}
@@ -163,7 +140,8 @@ public class InvocableHandlerMethodTests {
public void invocationTargetException() throws Exception {
Throwable expected = new RuntimeException("error");
try {
invokeExceptionRaisingHandler(expected);
getInvocable(Throwable.class).invokeForRequest(this.request, null, expected);
fail("Expected exception");
}
catch (RuntimeException actual) {
assertSame(expected, actual);
@@ -171,7 +149,8 @@ public class InvocableHandlerMethodTests {
expected = new Error("error");
try {
invokeExceptionRaisingHandler(expected);
getInvocable(Throwable.class).invokeForRequest(this.request, null, expected);
fail("Expected exception");
}
catch (Error actual) {
assertSame(expected, actual);
@@ -179,7 +158,8 @@ public class InvocableHandlerMethodTests {
expected = new Exception("error");
try {
invokeExceptionRaisingHandler(expected);
getInvocable(Throwable.class).invokeForRequest(this.request, null, expected);
fail("Expected exception");
}
catch (Exception actual) {
assertSame(expected, actual);
@@ -187,7 +167,8 @@ public class InvocableHandlerMethodTests {
expected = new Throwable("error");
try {
invokeExceptionRaisingHandler(expected);
getInvocable(Throwable.class).invokeForRequest(this.request, null, expected);
fail("Expected exception");
}
catch (IllegalStateException actual) {
assertNotNull(actual.getCause());
@@ -198,16 +179,9 @@ public class InvocableHandlerMethodTests {
@Test // SPR-13917
public void invocationErrorMessage() throws Exception {
HandlerMethodArgumentResolverComposite composite = new HandlerMethodArgumentResolverComposite();
composite.addResolver(new StubArgumentResolver(double.class, null));
Method method = Handler.class.getDeclaredMethod("handle", double.class);
Object handler = new Handler();
InvocableHandlerMethod hm = new InvocableHandlerMethod(handler, method);
hm.setHandlerMethodArgumentResolvers(composite);
this.composite.addResolver(new StubArgumentResolver(double.class));
try {
hm.invokeForRequest(this.webRequest, new ModelAndViewContainer());
getInvocable(double.class).invokeForRequest(this.request, null);
fail();
}
catch (IllegalStateException ex) {
@@ -215,14 +189,18 @@ public class InvocableHandlerMethodTests {
}
}
private void invokeExceptionRaisingHandler(Throwable expected) throws Exception {
Method method = ExceptionRaisingHandler.class.getDeclaredMethod("raiseException");
Object handler = new ExceptionRaisingHandler(expected);
new InvocableHandlerMethod(handler, method).invokeForRequest(webRequest, null);
fail("Expected exception");
private InvocableHandlerMethod getInvocable(Class<?>... argTypes) {
Method method = ResolvableMethod.on(Handler.class).argTypes(argTypes).resolveMethod();
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new Handler(), method);
handlerMethod.setHandlerMethodArgumentResolvers(this.composite);
return handlerMethod;
}
private StubArgumentResolver getStubResolver(int index) {
return (StubArgumentResolver) this.composite.getResolvers().get(index);
}
@SuppressWarnings("unused")
private static class Handler {
@@ -233,20 +211,9 @@ public class InvocableHandlerMethodTests {
public void handle(double amount) {
}
}
@SuppressWarnings("unused")
private static class ExceptionRaisingHandler {
private final Throwable t;
public ExceptionRaisingHandler(Throwable t) {
this.t = t;
}
public void raiseException() throws Throwable {
throw t;
public void handleWithException(Throwable ex) throws Throwable {
throw ex;
}
}
@@ -260,8 +227,9 @@ public class InvocableHandlerMethodTests {
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
throw new HttpMessageNotReadableException("oops, can't read");
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
throw new IllegalArgumentException("oops, can't read");
}
}

View File

@@ -20,41 +20,55 @@ import java.util.ArrayList;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
/**
* Supports parameters of a given type and resolves them using a stub value.
* Also records the resolved parameter value.
* Stub resolver for a fixed value type and/or value.
*
* @author Rossen Stoyanchev
*/
public class StubArgumentResolver implements HandlerMethodArgumentResolver {
private final Class<?> parameterType;
private final Class<?> valueType;
private final Object stubValue;
@Nullable
private final Object value;
private List<MethodParameter> resolvedParameters = new ArrayList<>();
public StubArgumentResolver(Class<?> supportedParameterType, Object stubValue) {
this.parameterType = supportedParameterType;
this.stubValue = stubValue;
public StubArgumentResolver(Object value) {
this(value.getClass(), value);
}
public StubArgumentResolver(Class<?> valueType) {
this(valueType, null);
}
public StubArgumentResolver(Class<?> valueType, Object value) {
this.valueType = valueType;
this.value = value;
}
public List<MethodParameter> getResolvedParameters() {
return resolvedParameters;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(this.parameterType);
return parameter.getParameterType().equals(this.valueType);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) {
this.resolvedParameters.add(parameter);
return this.stubValue;
return this.value;
}
}