Revised InvocableHandlerMethod exception handling

Issue: SPR-11281
(cherry picked from commit 1a1c72c)
This commit is contained in:
Juergen Hoeller
2014-02-12 15:13:28 +01:00
parent 85e336e6da
commit 29b40b9d0e
2 changed files with 74 additions and 53 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2014 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.
@@ -47,15 +47,15 @@ import org.springframework.web.method.HandlerMethod;
*/
public class InvocableHandlerMethod extends HandlerMethod {
private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
private WebDataBinderFactory dataBinderFactory;
private HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite();
private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
/**
* Creates an instance from the given handler and method.
* Create an instance from the given handler and method.
*/
public InvocableHandlerMethod(Object bean, Method method) {
super(bean, method);
@@ -69,19 +69,19 @@ public class InvocableHandlerMethod extends HandlerMethod {
}
/**
* Constructs a new handler method with the given bean instance, method name and parameters.
* Construct a new handler method with the given bean instance, method name and parameters.
* @param bean the object bean
* @param methodName the method name
* @param parameterTypes the method parameter types
* @throws NoSuchMethodException when the method cannot be found
*/
public InvocableHandlerMethod(
Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
public InvocableHandlerMethod(Object bean, String methodName, Class<?>... parameterTypes) throws NoSuchMethodException {
super(bean, methodName, parameterTypes);
}
/**
* Sets the {@link WebDataBinderFactory} to be passed to argument resolvers allowing them to create
* Set the {@link WebDataBinderFactory} to be passed to argument resolvers allowing them to create
* a {@link WebDataBinder} for data binding and type conversion purposes.
* @param dataBinderFactory the data binder factory.
*/
@@ -97,78 +97,74 @@ public class InvocableHandlerMethod extends HandlerMethod {
}
/**
* Set the ParameterNameDiscoverer for resolving parameter names when needed (e.g. default request attribute name).
* <p>Default is an {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer} instance.
* Set the ParameterNameDiscoverer for resolving parameter names when needed
* (e.g. default request attribute name).
* <p>Default is a {@link org.springframework.core.LocalVariableTableParameterNameDiscoverer} instance.
*/
public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
/**
* Invoke the method after resolving its argument values in the context of the given request. <p>Argument
* values are commonly resolved through {@link HandlerMethodArgumentResolver}s. The {@code provideArgs}
* parameter however may supply argument values to be used directly, i.e. without argument resolution.
* Examples of provided argument values include a {@link WebDataBinder}, a {@link SessionStatus}, or
* a thrown exception instance. Provided argument values are checked before argument resolvers.
*
* @param request the current request
* @param mavContainer the ModelAndViewContainer for this request
* @param providedArgs "given" arguments matched by type, not resolved
* @return the raw value returned by the invoked method
* @exception Exception raised if no suitable argument resolver can be found, or the method raised an exception
*/
public final Object invokeForRequest(NativeWebRequest request,
ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
public final Object invokeForRequest(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
StringBuilder builder = new StringBuilder("Invoking [");
builder.append(this.getMethod().getName()).append("] method with arguments ");
builder.append(Arrays.asList(args));
logger.trace(builder.toString());
StringBuilder sb = new StringBuilder("Invoking [");
sb.append(getBeanType().getSimpleName()).append(".");
sb.append(getMethod().getName()).append("] method with arguments ");
sb.append(Arrays.asList(args));
logger.trace(sb.toString());
}
Object returnValue = invoke(args);
if (logger.isTraceEnabled()) {
logger.trace("Method [" + this.getMethod().getName() + "] returned [" + returnValue + "]");
logger.trace("Method [" + getMethod().getName() + "] returned [" + returnValue + "]");
}
return returnValue;
}
/**
* Get the method argument values for the current request.
*/
private Object[] getMethodArgumentValues(
NativeWebRequest request, ModelAndViewContainer mavContainer,
private Object[] getMethodArgumentValues(NativeWebRequest request, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(parameterNameDiscoverer);
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
GenericTypeResolver.resolveParameterType(parameter, getBean().getClass());
args[i] = resolveProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (argumentResolvers.supportsParameter(parameter)) {
if (this.argumentResolvers.supportsParameter(parameter)) {
try {
args[i] = argumentResolvers.resolveArgument(parameter, mavContainer, request, dataBinderFactory);
args[i] = this.argumentResolvers.resolveArgument(
parameter, mavContainer, request, this.dataBinderFactory);
continue;
} catch (Exception ex) {
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(getArgumentResolutionErrorMessage("Error resolving argument", i), ex);
}
throw ex;
}
}
if (args[i] == null) {
String msg = getArgumentResolutionErrorMessage("No suitable resolver for argument", i);
throw new IllegalStateException(msg);
@@ -214,17 +210,17 @@ public class InvocableHandlerMethod extends HandlerMethod {
* Invoke the handler method with the given argument values.
*/
private Object invoke(Object... args) throws Exception {
ReflectionUtils.makeAccessible(this.getBridgedMethod());
ReflectionUtils.makeAccessible(getBridgedMethod());
try {
return getBridgedMethod().invoke(getBean(), args);
}
catch (IllegalArgumentException e) {
String msg = getInvocationErrorMessage(e.getMessage(), args);
throw new IllegalArgumentException(msg, e);
catch (IllegalArgumentException ex) {
assertTargetBean(getBridgedMethod(), getBean(), args);
throw new IllegalStateException(getInvocationErrorMessage(ex.getMessage(), args), ex);
}
catch (InvocationTargetException e) {
catch (InvocationTargetException ex) {
// Unwrap for HandlerExceptionResolvers ...
Throwable targetException = e.getTargetException();
Throwable targetException = ex.getTargetException();
if (targetException instanceof RuntimeException) {
throw (RuntimeException) targetException;
}
@@ -241,6 +237,25 @@ public class InvocableHandlerMethod extends HandlerMethod {
}
}
/**
* Assert that the target bean class is an instance of the class where the given
* method is declared. In some cases the actual controller instance at request-
* processing time may be a JDK dynamic proxy (lazy initialization, prototype
* beans, and others). {@code @Controller}'s that require proxying should prefer
* class-based proxy mechanisms.
*/
private void assertTargetBean(Method method, Object targetBean, Object[] args) {
Class<?> methodDeclaringClass = method.getDeclaringClass();
Class<?> targetBeanClass = targetBean.getClass();
if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) {
String msg = "The mapped controller method class '" + methodDeclaringClass.getName() +
"' is not an instance of the actual controller bean instance '" +
targetBeanClass.getName() + "'. If the controller requires proxying " +
"(e.g. due to @Transactional), please use class-based proxying.";
throw new IllegalStateException(getInvocationErrorMessage(msg, args));
}
}
private String getInvocationErrorMessage(String message, Object[] resolvedArgs) {
StringBuilder sb = new StringBuilder(getDetailedErrorMessage(message));
sb.append("Resolved arguments: \n");

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2014 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.
@@ -45,6 +45,7 @@ public class InvocableHandlerMethodTests {
private NativeWebRequest webRequest;
@Before
public void setUp() throws Exception {
Method method = Handler.class.getDeclaredMethod("handle", Integer.class, String.class);
@@ -52,6 +53,7 @@ public class InvocableHandlerMethodTests {
this.webRequest = new ServletWebRequest(new MockHttpServletRequest(), new MockHttpServletResponse());
}
@Test
public void resolveArg() throws Exception {
StubArgumentResolver intResolver = new StubArgumentResolver(Integer.class, 99);
@@ -63,11 +65,9 @@ public class InvocableHandlerMethodTests {
handlerMethod.setHandlerMethodArgumentResolvers(composite);
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());
}
@@ -83,7 +83,6 @@ public class InvocableHandlerMethodTests {
handlerMethod.setHandlerMethodArgumentResolvers(composite);
Object returnValue = handlerMethod.invokeForRequest(webRequest, null);
assertEquals(1, intResolver.getResolvedParameters().size());
assertEquals(1, stringResolver.getResolvedParameters().size());
assertEquals("null-null", returnValue);
@@ -94,7 +93,8 @@ public class InvocableHandlerMethodTests {
try {
handlerMethod.invokeForRequest(webRequest, null);
fail("Expected exception");
} catch (IllegalStateException ex) {
}
catch (IllegalStateException ex) {
assertTrue(ex.getMessage().contains("No suitable resolver for argument [0] [type=java.lang.Integer]"));
}
}
@@ -118,7 +118,6 @@ public class InvocableHandlerMethodTests {
handlerMethod.setHandlerMethodArgumentResolvers(composite);
Object returnValue = handlerMethod.invokeForRequest(webRequest, null, 2, "value2");
assertEquals("2-value2", returnValue);
}
@@ -131,9 +130,9 @@ public class InvocableHandlerMethodTests {
try {
handlerMethod.invokeForRequest(webRequest, null);
fail("Expected exception");
} catch (HttpMessageNotReadableException ex) {
// Expected..
// Allow HandlerMethodArgumentResolver exceptions to propagate..
}
catch (HttpMessageNotReadableException ex) {
// expected - allow HandlerMethodArgumentResolver exceptions to propagate
}
}
@@ -150,7 +149,8 @@ public class InvocableHandlerMethodTests {
try {
handlerMethod.invokeForRequest(webRequest, null);
fail("Expected exception");
} catch (IllegalArgumentException ex) {
}
catch (IllegalStateException ex) {
assertNotNull("Exception not wrapped", ex.getCause());
assertTrue(ex.getCause() instanceof IllegalArgumentException);
assertTrue(ex.getMessage().contains("Controller ["));
@@ -166,28 +166,32 @@ public class InvocableHandlerMethodTests {
Throwable expected = new RuntimeException("error");
try {
invokeExceptionRaisingHandler(expected);
} catch (RuntimeException actual) {
}
catch (RuntimeException actual) {
assertSame(expected, actual);
}
expected = new Error("error");
try {
invokeExceptionRaisingHandler(expected);
} catch (Error actual) {
}
catch (Error actual) {
assertSame(expected, actual);
}
expected = new Exception("error");
try {
invokeExceptionRaisingHandler(expected);
} catch (Exception actual) {
}
catch (Exception actual) {
assertSame(expected, actual);
}
expected = new Throwable("error");
try {
invokeExceptionRaisingHandler(expected);
} catch (IllegalStateException actual) {
}
catch (IllegalStateException actual) {
assertNotNull(actual.getCause());
assertSame(expected, actual.getCause());
assertTrue(actual.getMessage().contains("Failed to invoke controller method"));
@@ -201,6 +205,7 @@ public class InvocableHandlerMethodTests {
fail("Expected exception");
}
@SuppressWarnings("unused")
private static class Handler {
@@ -209,6 +214,7 @@ public class InvocableHandlerMethodTests {
}
}
@SuppressWarnings("unused")
private static class ExceptionRaisingHandler {
@@ -221,9 +227,9 @@ public class InvocableHandlerMethodTests {
public void raiseException() throws Throwable {
throw t;
}
}
private static class ExceptionRaisingArgumentResolver implements HandlerMethodArgumentResolver {
@Override