Async return values refactoring in Spring MVC

Revise Javadoc on AsyncHandlerMethodReturnValueHandler to clarify its
main purpose is to prioritze custom async return value handlers ahead
of built-in ones. Also replace the interface from built-in handlers
which are prioritized already.

Remove DeferredResultAdapter and ResponseBodyEmitterAdapter --
introduced in 4.3 for custom async return value handling, since for
5.0 we will add built-in support for reactive types and the value of
these contracts becomes very marginal.

Issue: SPR-15365
This commit is contained in:
Rossen Stoyanchev
2017-04-02 20:30:24 -04:00
parent cfc89ebe16
commit ae1ed16cb8
10 changed files with 143 additions and 391 deletions

View File

@@ -21,7 +21,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.WebAsyncTask;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -30,7 +30,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Rossen Stoyanchev
* @since 3.2
*/
public class AsyncTaskMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
public class AsyncTaskMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
private final BeanFactory beanFactory;
@@ -45,11 +45,6 @@ public class AsyncTaskMethodReturnValueHandler implements AsyncHandlerMethodRetu
return WebAsyncTask.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
return (returnValue != null && returnValue instanceof WebAsyncTask);
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2017 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.
@@ -21,7 +21,7 @@ import java.util.concurrent.Callable;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
@@ -30,18 +30,13 @@ import org.springframework.web.method.support.ModelAndViewContainer;
* @author Rossen Stoyanchev
* @since 3.2
*/
public class CallableMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return Callable.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
return (returnValue != null && returnValue instanceof Callable);
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

View File

@@ -1,36 +0,0 @@
/*
* Copyright 2002-2016 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.web.servlet.mvc.method.annotation;
import org.springframework.web.context.request.async.DeferredResult;
/**
* Contract to adapt a single-value async return value to {@code DeferredResult}.
*
* @author Rossen Stoyanchev
* @since 4.3
*/
public interface DeferredResultAdapter {
/**
* Create a {@code DeferredResult} for the given return value.
* @param returnValue the return value (never {@code null})
* @return the DeferredResult
*/
DeferredResult<?> adaptToDeferredResult(Object returnValue);
}

View File

@@ -16,70 +16,34 @@
package org.springframework.web.servlet.mvc.method.annotation;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import java.util.function.BiFunction;
import org.springframework.core.MethodParameter;
import org.springframework.util.Assert;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handler for return values of type {@link DeferredResult}, {@link ListenableFuture},
* {@link CompletionStage} and any other async type with a {@link #getAdapterMap()
* registered adapter}.
* Handler for return values of type {@link DeferredResult},
* {@link ListenableFuture}, and {@link CompletionStage}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
private final Map<Class<?>, DeferredResultAdapter> adapterMap;
public DeferredResultMethodReturnValueHandler() {
this.adapterMap = new HashMap<>(5);
this.adapterMap.put(DeferredResult.class, new SimpleDeferredResultAdapter());
this.adapterMap.put(ListenableFuture.class, new ListenableFutureAdapter());
this.adapterMap.put(CompletionStage.class, new CompletionStageAdapter());
}
/**
* Return the map with {@code DeferredResult} adapters.
* <p>By default the map contains adapters for {@code DeferredResult}, which
* simply downcasts, {@link ListenableFuture}, and {@link CompletionStage}.
* @return the map of adapters
*/
public Map<Class<?>, DeferredResultAdapter> getAdapterMap() {
return this.adapterMap;
}
private DeferredResultAdapter getAdapterFor(Class<?> type) {
for (Class<?> adapteeType : getAdapterMap().keySet()) {
if (adapteeType.isAssignableFrom(type)) {
return getAdapterMap().get(adapteeType);
}
}
return null;
}
public class DeferredResultMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (getAdapterFor(returnType.getParameterType()) != null);
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
return (returnValue != null && (getAdapterFor(returnValue.getClass()) != null));
Class<?> type = returnType.getParameterType();
return DeferredResult.class.isAssignableFrom(type) ||
ListenableFuture.class.isAssignableFrom(type) ||
CompletionStage.class.isAssignableFrom(type);
}
@Override
@@ -91,78 +55,52 @@ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMetho
return;
}
DeferredResultAdapter adapter = getAdapterFor(returnValue.getClass());
if (adapter == null) {
throw new IllegalStateException(
"Could not find DeferredResultAdapter for return value type: " + returnValue.getClass());
DeferredResult<?> result;
if (returnValue instanceof DeferredResult) {
result = (DeferredResult<?>) returnValue;
}
DeferredResult<?> result = adapter.adaptToDeferredResult(returnValue);
else if (returnValue instanceof ListenableFuture) {
result = adaptListenableFuture((ListenableFuture<?>) returnValue);
}
else if (returnValue instanceof CompletionStage) {
result = adaptCompletionStage((CompletionStage<?>) returnValue);
}
else {
// Should not happen...
throw new IllegalStateException("Unexpected return value type: " + returnValue);
}
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(result, mavContainer);
}
/**
* Adapter for {@code DeferredResult} return values.
*/
private static class SimpleDeferredResultAdapter implements DeferredResultAdapter {
@Override
public DeferredResult<?> adaptToDeferredResult(Object returnValue) {
Assert.isInstanceOf(DeferredResult.class, returnValue, "DeferredResult expected");
return (DeferredResult<?>) returnValue;
}
private DeferredResult<Object> adaptListenableFuture(ListenableFuture<?> future) {
DeferredResult<Object> result = new DeferredResult<>();
future.addCallback(new ListenableFutureCallback<Object>() {
@Override
public void onSuccess(Object value) {
result.setResult(value);
}
@Override
public void onFailure(Throwable ex) {
result.setErrorResult(ex);
}
});
return result;
}
/**
* Adapter for {@code ListenableFuture} return values.
*/
private static class ListenableFutureAdapter implements DeferredResultAdapter {
@Override
public DeferredResult<?> adaptToDeferredResult(Object returnValue) {
Assert.isInstanceOf(ListenableFuture.class, returnValue, "ListenableFuture expected");
final DeferredResult<Object> result = new DeferredResult<>();
((ListenableFuture<?>) returnValue).addCallback(new ListenableFutureCallback<Object>() {
@Override
public void onSuccess(Object value) {
result.setResult(value);
}
@Override
public void onFailure(Throwable ex) {
result.setErrorResult(ex);
}
});
return result;
}
}
/**
* Adapter for {@code CompletionStage} return values.
*/
private static class CompletionStageAdapter implements DeferredResultAdapter {
@Override
public DeferredResult<?> adaptToDeferredResult(Object returnValue) {
Assert.isInstanceOf(CompletionStage.class, returnValue, "CompletionStage expected");
final DeferredResult<Object> result = new DeferredResult<>();
@SuppressWarnings("unchecked")
CompletionStage<?> future = (CompletionStage<?>) returnValue;
future.handle(new BiFunction<Object, Throwable, Object>() {
@Override
public Object apply(Object value, Throwable ex) {
if (ex != null) {
result.setErrorResult(ex);
}
else {
result.setResult(value);
}
return null;
}
});
return result;
}
private DeferredResult<Object> adaptCompletionStage(CompletionStage<?> future) {
DeferredResult<Object> result = new DeferredResult<>();
future.handle((BiFunction<Object, Throwable, Object>) (value, ex) -> {
if (ex != null) {
result.setErrorResult(ex);
}
else {
result.setResult(value);
}
return null;
});
return result;
}
}

View File

@@ -1,40 +0,0 @@
/*
* Copyright 2002-2016 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.web.servlet.mvc.method.annotation;
import org.springframework.http.server.ServerHttpResponse;
/**
* Contract to adapt streaming async types to {@code ResponseBodyEmitter}.
*
* @author Rossen Stoyanchev
* @since 4.3
*/
@FunctionalInterface
public interface ResponseBodyEmitterAdapter {
/**
* Obtain a {@code ResponseBodyEmitter} for the given return value.
* If the return is the body {@code ResponseEntity} then the given
* {@code ServerHttpResponse} contains its status and headers.
* @param returnValue the return value (never {@code null})
* @param response the response
* @return the return value adapted to a {@code ResponseBodyEmitter}
*/
ResponseBodyEmitter adaptToEmitter(Object returnValue, ServerHttpResponse response);
}

View File

@@ -18,9 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -41,81 +39,43 @@ import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.context.request.async.DeferredResult;
import org.springframework.web.context.request.async.WebAsyncUtils;
import org.springframework.web.filter.ShallowEtagHeaderFilter;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handler for return values of type {@link ResponseBodyEmitter} (and the
* {@code ResponseEntity<ResponseBodyEmitter>} sub-class) as well as any other
* async type with a {@link #getAdapterMap() registered adapter}.
* Handler for return values of type {@link ResponseBodyEmitter} and sub-classes
* such as {@link SseEmitter} including the same types wrapped with
* {@link ResponseEntity}.
*
* @author Rossen Stoyanchev
* @since 4.2
*/
public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodReturnValueHandler {
private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class);
private final List<HttpMessageConverter<?>> messageConverters;
private final Map<Class<?>, ResponseBodyEmitterAdapter> adapterMap;
public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) {
Assert.notEmpty(messageConverters, "HttpMessageConverter List must not be empty");
this.messageConverters = messageConverters;
this.adapterMap = new HashMap<>(4);
this.adapterMap.put(ResponseBodyEmitter.class, new SimpleResponseBodyEmitterAdapter());
}
/**
* Return the map with {@code ResponseBodyEmitter} adapters.
* By default the map contains a single adapter {@code ResponseBodyEmitter}
* that simply downcasts the return value.
* @return the map of adapters
*/
public Map<Class<?>, ResponseBodyEmitterAdapter> getAdapterMap() {
return this.adapterMap;
}
private ResponseBodyEmitterAdapter getAdapterFor(Class<?> type) {
if (type != null) {
for (Class<?> adapteeType : getAdapterMap().keySet()) {
if (adapteeType.isAssignableFrom(type)) {
return getAdapterMap().get(adapteeType);
}
}
}
return null;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> bodyType;
if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
}
else {
bodyType = returnType.getParameterType();
}
return (getAdapterFor(bodyType) != null);
Class<?> bodyType = ResponseEntity.class.isAssignableFrom(returnType.getParameterType()) ?
ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve() :
returnType.getParameterType();
return bodyType != null && supportsBodyType(bodyType);
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
if (returnValue != null) {
Object adaptFrom = returnValue;
if (returnValue instanceof ResponseEntity) {
adaptFrom = ((ResponseEntity) returnValue).getBody();
}
if (adaptFrom != null) {
return (getAdapterFor(adaptFrom.getClass()) != null);
}
}
return false;
private boolean supportsBodyType(Class<?> bodyType) {
return ResponseBodyEmitter.class.isAssignableFrom(bodyType);
}
@Override
@@ -145,12 +105,15 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod
ServletRequest request = webRequest.getNativeRequest(ServletRequest.class);
ShallowEtagHeaderFilter.disableContentCaching(request);
ResponseBodyEmitterAdapter adapter = getAdapterFor(returnValue.getClass());
if (adapter == null) {
throw new IllegalStateException(
"Could not find ResponseBodyEmitterAdapter for return value type: " + returnValue.getClass());
ResponseBodyEmitter emitter;
if (returnValue instanceof ResponseBodyEmitter) {
emitter = (ResponseBodyEmitter) returnValue;
}
ResponseBodyEmitter emitter = adapter.adaptToEmitter(returnValue, outputMessage);
else {
throw new IllegalStateException("Unexpected return value type: " + returnValue);
}
emitter.extendResponse(outputMessage);
// Commit the response and wrap to ignore further header changes
@@ -166,18 +129,6 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod
}
/**
* Adapter for {@code ResponseBodyEmitter} return values.
*/
private static class SimpleResponseBodyEmitterAdapter implements ResponseBodyEmitterAdapter {
@Override
public ResponseBodyEmitter adaptToEmitter(Object returnValue, ServerHttpResponse response) {
Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue, "ResponseBodyEmitter expected");
return (ResponseBodyEmitter) returnValue;
}
}
/**
* ResponseBodyEmitter.Handler that writes with HttpMessageConverter's.
*/