Fix issue with DeferredResult on @RestController
Before this change, async result handling on controller methods failed to observe type-level annotations annotations. The issue was never noticed until we started supporting type-level @ResponseBody and the @RestController meta annotation. Issue: SPR-10905
This commit is contained in:
@@ -174,9 +174,10 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a ServletInvocableHandlerMethod that will process the value returned
|
* Create a ServletInvocableHandlerMethod that will return the given value from an
|
||||||
* from an async operation essentially either applying return value handling or
|
* async operation instead of invoking the controller method again. The async result
|
||||||
* raising an exception if the end result is an Exception.
|
* value is then either processed as if the controller method returned it or an
|
||||||
|
* exception is raised if the async result value itself is an Exception.
|
||||||
*/
|
*/
|
||||||
ServletInvocableHandlerMethod wrapConcurrentResult(final Object result) {
|
ServletInvocableHandlerMethod wrapConcurrentResult(final Object result) {
|
||||||
|
|
||||||
@@ -197,9 +198,12 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A ServletInvocableHandlerMethod sub-class that invokes a given
|
* A sub-class of {@link HandlerMethod} that invokes the given {@link Callable}
|
||||||
* {@link Callable} and "inherits" the annotations of the containing class
|
* instead of the target controller method. This is useful for resuming processing
|
||||||
* instance, useful for invoking a Callable returned from a HandlerMethod.
|
* with the result of an async operation. The goal is to process the value returned
|
||||||
|
* from the Callable as if it was returned by the target controller method, i.e.
|
||||||
|
* taking into consideration both method and type-level controller annotations (e.g.
|
||||||
|
* {@code @ResponseBody}, {@code @ResponseStatus}, etc).
|
||||||
*/
|
*/
|
||||||
private class CallableHandlerMethod extends ServletInvocableHandlerMethod {
|
private class CallableHandlerMethod extends ServletInvocableHandlerMethod {
|
||||||
|
|
||||||
@@ -208,6 +212,17 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
|
|||||||
this.setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
|
this.setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge to type-level annotations of the target controller method.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Class<?> getBeanType() {
|
||||||
|
return ServletInvocableHandlerMethod.this.getBeanType();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bridge to method-level annotations of the target controller method.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
|
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
|
||||||
return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType);
|
return ServletInvocableHandlerMethod.this.getMethodAnnotation(annotationType);
|
||||||
|
|||||||
@@ -15,12 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
package org.springframework.web.servlet.mvc.method.annotation;
|
package org.springframework.web.servlet.mvc.method.annotation;
|
||||||
import static org.junit.Assert.assertEquals;
|
|
||||||
import static org.junit.Assert.assertNotNull;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
|
||||||
import static org.junit.Assert.fail;
|
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
|
||||||
@@ -28,13 +25,18 @@ import org.junit.Before;
|
|||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||||
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
import org.springframework.mock.web.test.MockHttpServletRequest;
|
import org.springframework.mock.web.test.MockHttpServletRequest;
|
||||||
import org.springframework.mock.web.test.MockHttpServletResponse;
|
import org.springframework.mock.web.test.MockHttpServletResponse;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseBody;
|
||||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
import org.springframework.web.context.request.ServletWebRequest;
|
import org.springframework.web.context.request.ServletWebRequest;
|
||||||
|
import org.springframework.web.context.request.async.DeferredResult;
|
||||||
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
|
import org.springframework.web.method.annotation.RequestParamMethodArgumentResolver;
|
||||||
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
|
import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite;
|
||||||
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
|
||||||
@@ -42,6 +44,8 @@ import org.springframework.web.method.support.HandlerMethodReturnValueHandlerCom
|
|||||||
import org.springframework.web.method.support.ModelAndViewContainer;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test fixture with {@link ServletInvocableHandlerMethod}.
|
* Test fixture with {@link ServletInvocableHandlerMethod}.
|
||||||
*
|
*
|
||||||
@@ -73,7 +77,7 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invokeAndHandle_VoidWithResponseStatus() throws Exception {
|
public void invokeAndHandle_VoidWithResponseStatus() throws Exception {
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod("responseStatus");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "responseStatus");
|
||||||
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
|
|
||||||
assertTrue("Null return value + @ResponseStatus should result in 'request handled'",
|
assertTrue("Null return value + @ResponseStatus should result in 'request handled'",
|
||||||
@@ -85,7 +89,7 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
public void invokeAndHandle_VoidWithHttpServletResponseArgument() throws Exception {
|
public void invokeAndHandle_VoidWithHttpServletResponseArgument() throws Exception {
|
||||||
argumentResolvers.addResolver(new ServletResponseMethodArgumentResolver());
|
argumentResolvers.addResolver(new ServletResponseMethodArgumentResolver());
|
||||||
|
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod("httpServletResponse", HttpServletResponse.class);
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "httpServletResponse", HttpServletResponse.class);
|
||||||
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
|
|
||||||
assertTrue("Null return value + HttpServletResponse arg should result in 'request handled'",
|
assertTrue("Null return value + HttpServletResponse arg should result in 'request handled'",
|
||||||
@@ -98,7 +102,7 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
int lastModifiedTimestamp = 1000 * 1000;
|
int lastModifiedTimestamp = 1000 * 1000;
|
||||||
webRequest.checkNotModified(lastModifiedTimestamp);
|
webRequest.checkNotModified(lastModifiedTimestamp);
|
||||||
|
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod("notModified");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "notModified");
|
||||||
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
|
|
||||||
assertTrue("Null return value + 'not modified' request should result in 'request handled'",
|
assertTrue("Null return value + 'not modified' request should result in 'request handled'",
|
||||||
@@ -109,7 +113,7 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void invokeAndHandle_NotVoidWithResponseStatusAndReason() throws Exception {
|
public void invokeAndHandle_NotVoidWithResponseStatusAndReason() throws Exception {
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod("responseStatusWithReason");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "responseStatusWithReason");
|
||||||
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
|
|
||||||
assertTrue("When a phrase is used, the response should not be used any more", mavContainer.isRequestHandled());
|
assertTrue("When a phrase is used, the response should not be used any more", mavContainer.isRequestHandled());
|
||||||
@@ -121,7 +125,7 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
public void invokeAndHandle_Exception() throws Exception {
|
public void invokeAndHandle_Exception() throws Exception {
|
||||||
returnValueHandlers.addHandler(new ExceptionRaisingReturnValueHandler());
|
returnValueHandlers.addHandler(new ExceptionRaisingReturnValueHandler());
|
||||||
|
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod("handle");
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "handle");
|
||||||
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
fail("Expected exception");
|
fail("Expected exception");
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,7 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
returnValueHandlers.addHandler(new ViewNameMethodReturnValueHandler());
|
returnValueHandlers.addHandler(new ViewNameMethodReturnValueHandler());
|
||||||
|
|
||||||
// Invoke without a request parameter (String return value)
|
// Invoke without a request parameter (String return value)
|
||||||
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod("dynamicReturnValue", String.class);
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new Handler(), "dynamicReturnValue", String.class);
|
||||||
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
|
|
||||||
assertNotNull(mavContainer.getView());
|
assertNotNull(mavContainer.getView());
|
||||||
@@ -146,11 +150,45 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
assertEquals("view", mavContainer.getViewName());
|
assertEquals("view", mavContainer.getViewName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapConcurrentResult_MethodLevelResponseBody() throws Exception {
|
||||||
|
wrapConcurrentResult_ResponseBody(new MethodLevelResponseBodyHandler());
|
||||||
|
}
|
||||||
|
|
||||||
private ServletInvocableHandlerMethod getHandlerMethod(String methodName, Class<?>... argTypes)
|
@Test
|
||||||
throws NoSuchMethodException {
|
public void wrapConcurrentResult_TypeLevelResponseBody() throws Exception {
|
||||||
Method method = Handler.class.getDeclaredMethod(methodName, argTypes);
|
wrapConcurrentResult_ResponseBody(new TypeLevelResponseBodyHandler());
|
||||||
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(new Handler(), method);
|
}
|
||||||
|
|
||||||
|
private void wrapConcurrentResult_ResponseBody(Object handler) throws Exception {
|
||||||
|
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
|
||||||
|
converters.add(new StringHttpMessageConverter());
|
||||||
|
returnValueHandlers.addHandler(new RequestResponseBodyMethodProcessor(converters));
|
||||||
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(handler, "handle");
|
||||||
|
handlerMethod = handlerMethod.wrapConcurrentResult("bar");
|
||||||
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
|
|
||||||
|
assertEquals("bar", response.getContentAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void wrapConcurrentResult_ResponseEntity() throws Exception {
|
||||||
|
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
|
||||||
|
converters.add(new StringHttpMessageConverter());
|
||||||
|
returnValueHandlers.addHandler(new HttpEntityMethodProcessor(converters));
|
||||||
|
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handle");
|
||||||
|
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>("bar", HttpStatus.OK));
|
||||||
|
handlerMethod.invokeAndHandle(webRequest, mavContainer);
|
||||||
|
|
||||||
|
assertEquals("bar", response.getContentAsString());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private ServletInvocableHandlerMethod getHandlerMethod(Object controller,
|
||||||
|
String methodName, Class<?>... argTypes) throws NoSuchMethodException {
|
||||||
|
|
||||||
|
Method method = controller.getClass().getDeclaredMethod(methodName, argTypes);
|
||||||
|
ServletInvocableHandlerMethod handlerMethod = new ServletInvocableHandlerMethod(controller, method);
|
||||||
handlerMethod.setHandlerMethodArgumentResolvers(argumentResolvers);
|
handlerMethod.setHandlerMethodArgumentResolvers(argumentResolvers);
|
||||||
handlerMethod.setHandlerMethodReturnValueHandlers(returnValueHandlers);
|
handlerMethod.setHandlerMethodReturnValueHandlers(returnValueHandlers);
|
||||||
return handlerMethod;
|
return handlerMethod;
|
||||||
@@ -183,6 +221,31 @@ public class ServletInvocableHandlerMethodTests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static class MethodLevelResponseBodyHandler {
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
public DeferredResult<String> handle() {
|
||||||
|
return new DeferredResult<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResponseBody
|
||||||
|
private static class TypeLevelResponseBodyHandler {
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public DeferredResult<String> handle() {
|
||||||
|
return new DeferredResult<String>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ResponseEntityHandler {
|
||||||
|
|
||||||
|
public DeferredResult<ResponseEntity<String>> handle() {
|
||||||
|
return new DeferredResult<>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private static class ExceptionRaisingReturnValueHandler implements HandlerMethodReturnValueHandler {
|
private static class ExceptionRaisingReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
Reference in New Issue
Block a user