提交 956b66bb 编写于 作者: R Rossen Stoyanchev

Fix issue with async return value type determination

Before this change, the type of asynchronously produced return values
(e.g. Callable, DeferredResult, ListenableFuture) could not be
properly determined with an actual resulting value of null. Or even
with an actual value returned, the generic type could not be properly
determined. This change fixes both of those issues.

Issue: SPR-12287
上级 6f987a9c
...@@ -247,7 +247,7 @@ public class HandlerMethod { ...@@ -247,7 +247,7 @@ public class HandlerMethod {
/** /**
* A MethodParameter with HandlerMethod-specific behavior. * A MethodParameter with HandlerMethod-specific behavior.
*/ */
private class HandlerMethodParameter extends MethodParameter { protected class HandlerMethodParameter extends MethodParameter {
public HandlerMethodParameter(int index) { public HandlerMethodParameter(int index) {
super(HandlerMethod.this.bridgedMethod, index); super(HandlerMethod.this.bridgedMethod, index);
......
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
...@@ -19,8 +19,11 @@ package org.springframework.web.servlet.mvc.method.annotation; ...@@ -19,8 +19,11 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException; import java.io.IOException;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
...@@ -51,6 +54,9 @@ import org.springframework.web.util.NestedServletException; ...@@ -51,6 +54,9 @@ import org.springframework.web.util.NestedServletException;
*/ */
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
private static final Method CALLABLE_METHOD = ClassUtils.getMethod(Callable.class, "call");
private HttpStatus responseStatus; private HttpStatus responseStatus;
private String responseReason; private String responseReason;
...@@ -174,46 +180,46 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { ...@@ -174,46 +180,46 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
} }
/** /**
* Create a ServletInvocableHandlerMethod that will return the given value from an * Create a nested ServletInvocableHandlerMethod sub-class that returns the
* async operation instead of invoking the controller method again. The async result * the given value (or raises an Exception if the value is one) rather than
* value is then either processed as if the controller method returned it or an * actually invoking the controller method. This is useful when processing
* exception is raised if the async result value itself is an Exception. * async return values (e.g. Callable, DeferredResult, ListenableFuture).
*/ */
ServletInvocableHandlerMethod wrapConcurrentResult(final Object result) { ServletInvocableHandlerMethod wrapConcurrentResult(Object result) {
return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result));
return new CallableHandlerMethod(new Callable<Object>() {
@Override
public Object call() throws Exception {
if (result instanceof Exception) {
throw (Exception) result;
}
else if (result instanceof Throwable) {
throw new NestedServletException("Async processing failed", (Throwable) result);
}
return result;
}
});
} }
/** /**
* A sub-class of {@link HandlerMethod} that invokes the given {@link Callable} * A nested sub-class of {@code ServletInvocableHandlerMethod} that uses a
* instead of the target controller method. This is useful for resuming processing * simple {@link Callable} instead of the original controller as the handler in
* with the result of an async operation. The goal is to process the value returned * order to return the fixed (concurrent) result value given to it. Effectively
* from the Callable as if it was returned by the target controller method, i.e. * "resumes" processing with the asynchronously produced return value.
* taking into consideration both method and type-level controller annotations (e.g.
* {@code @ResponseBody}, {@code @ResponseStatus}, etc).
*/ */
private class CallableHandlerMethod extends ServletInvocableHandlerMethod { private class ConcurrentResultHandlerMethod extends ServletInvocableHandlerMethod {
public CallableHandlerMethod(Callable<?> callable) { private final MethodParameter returnType;
super(callable, ClassUtils.getMethod(callable.getClass(), "call"));
this.setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
public ConcurrentResultHandlerMethod(final Object result, ConcurrentResultMethodParameter returnType) {
super(new Callable<Object>() {
@Override
public Object call() throws Exception {
if (result instanceof Exception) {
throw (Exception) result;
}
else if (result instanceof Throwable) {
throw new NestedServletException("Async processing failed", (Throwable) result);
}
return result;
}
}, CALLABLE_METHOD);
setHandlerMethodReturnValueHandlers(ServletInvocableHandlerMethod.this.returnValueHandlers);
this.returnType = returnType;
} }
/** /**
* Bridge to type-level annotations of the target controller method. * Bridge to actual controller type-level annotations.
*/ */
@Override @Override
public Class<?> getBeanType() { public Class<?> getBeanType() {
...@@ -221,7 +227,16 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { ...@@ -221,7 +227,16 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
} }
/** /**
* Bridge to method-level annotations of the target controller method. * Bridge to actual return value or generic type within the declared
* async return type, e.g. Foo instead of {@code DeferredResult<Foo>}.
*/
@Override
public MethodParameter getReturnValueType(Object returnValue) {
return this.returnType;
}
/**
* Bridge to controller method-level annotations.
*/ */
@Override @Override
public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) { public <A extends Annotation> A getMethodAnnotation(Class<A> annotationType) {
...@@ -229,4 +244,33 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { ...@@ -229,4 +244,33 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
} }
} }
/**
* MethodParameter sub-class based on the actual return value type or if
* that's null falling back on the generic type within the declared async
* return type, e.g. Foo instead of {@code DeferredResult<Foo>}.
*/
private class ConcurrentResultMethodParameter extends HandlerMethodParameter {
private final Object returnValue;
private final ResolvableType returnType;
public ConcurrentResultMethodParameter(Object returnValue) {
super(-1);
this.returnValue = returnValue;
this.returnType = ResolvableType.forType(super.getGenericParameterType()).getGeneric(0);
}
@Override
public Class<?> getParameterType() {
return (returnValue != null ? returnValue.getClass() : this.returnType.getRawClass());
}
@Override
public Type getGenericParameterType() {
return this.returnType.getType();
}
}
} }
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
package org.springframework.web.servlet.mvc.method.annotation; package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
...@@ -45,6 +46,8 @@ import org.springframework.web.method.support.ModelAndViewContainer; ...@@ -45,6 +46,8 @@ 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.*; import static org.junit.Assert.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/** /**
* Test fixture with {@link ServletInvocableHandlerMethod}. * Test fixture with {@link ServletInvocableHandlerMethod}.
...@@ -183,6 +186,37 @@ public class ServletInvocableHandlerMethodTests { ...@@ -183,6 +186,37 @@ public class ServletInvocableHandlerMethodTests {
assertEquals("bar", response.getContentAsString()); assertEquals("bar", response.getContentAsString());
} }
// SPR-12287
@Test
public void wrapConcurrentResult_ResponseEntityNullBody() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new StringHttpMessageConverter());
List<Object> advice = Arrays.asList(mock(ResponseBodyAdvice.class));
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
returnValueHandlers.addHandler(processor);
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handle");
handlerMethod = handlerMethod.wrapConcurrentResult(new ResponseEntity<>(HttpStatus.OK));
handlerMethod.invokeAndHandle(webRequest, mavContainer);
assertEquals(200, response.getStatus());
assertEquals("", response.getContentAsString());
}
@Test
public void wrapConcurrentResult_ResponseEntityNullReturnValue() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<HttpMessageConverter<?>>();
converters.add(new StringHttpMessageConverter());
List<Object> advice = Arrays.asList(mock(ResponseBodyAdvice.class));
HttpEntityMethodProcessor processor = new HttpEntityMethodProcessor(converters, null, advice);
returnValueHandlers.addHandler(processor);
ServletInvocableHandlerMethod handlerMethod = getHandlerMethod(new ResponseEntityHandler(), "handle");
handlerMethod = handlerMethod.wrapConcurrentResult(null);
handlerMethod.invokeAndHandle(webRequest, mavContainer);
assertEquals(200, response.getStatus());
assertEquals("", response.getContentAsString());
}
private ServletInvocableHandlerMethod getHandlerMethod(Object controller, private ServletInvocableHandlerMethod getHandlerMethod(Object controller,
String methodName, Class<?>... argTypes) throws NoSuchMethodException { String methodName, Class<?>... argTypes) throws NoSuchMethodException {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册