DeferredResult/ResponseBodyEmitter adapter mechanism

The DeferredResult~ and the ResponseBodyEmitterReturnValueHandler now
each expose an adapter mechanism for plugging in other async return
value types. As a result the ListenableFutureReturnValueHandler and
CompletionStageReturnValueHandler are no longer needed and are now
deprecated.

Issue: SPR-14046
This commit is contained in:
Rossen Stoyanchev
2016-03-12 16:16:26 -05:00
parent 2152f436f9
commit 971ccab038
9 changed files with 479 additions and 59 deletions

View File

@@ -34,7 +34,11 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*
* @author Sebastien Deleuze
* @since 4.2
*
* @deprecated as of 4.3 {@link DeferredResultMethodReturnValueHandler} supports
* CompletionStage return values via an adapter mechanism.
*/
@Deprecated
@UsesJava8
public class CompletionStageReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {

View File

@@ -0,0 +1,35 @@
/*
* 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

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -16,7 +16,17 @@
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.lang.UsesJava8;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
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;
@@ -24,21 +34,56 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Handles return values of type {@link DeferredResult}.
* Handler for return values of type {@link DeferredResult}, {@link ListenableFuture},
* {@link CompletionStage} and any other async type with a {@link #getAdapterMap()
* registered adapter}.
*
* @author Rossen Stoyanchev
* @since 3.2
*/
public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
private final Map<Class<?>, DeferredResultAdapter> adapterMap;
public DeferredResultMethodReturnValueHandler() {
this.adapterMap = new HashMap<Class<?>, DeferredResultAdapter>(5);
this.adapterMap.put(DeferredResult.class, new SimpleDeferredResultAdapter());
this.adapterMap.put(ListenableFuture.class, new ListenableFutureAdapter());
if (ClassUtils.isPresent("java.util.concurrent.CompletionStage", getClass().getClassLoader())) {
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;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return DeferredResult.class.isAssignableFrom(returnType.getParameterType());
return (getAdapterFor(returnType.getParameterType()) != null);
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
return (returnValue != null && returnValue instanceof DeferredResult);
return (returnValue != null && (getAdapterFor(returnValue.getClass()) != null));
}
@Override
@@ -50,8 +95,74 @@ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMetho
return;
}
DeferredResult<?> deferredResult = (DeferredResult<?>) returnValue;
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
DeferredResultAdapter adapter = getAdapterFor(returnValue.getClass());
Assert.notNull(adapter);
DeferredResult<?> result = adapter.adaptToDeferredResult(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);
return (DeferredResult<?>) returnValue;
}
}
/**
* Adapter for {@code ListenableFuture} return values.
*/
private static class ListenableFutureAdapter implements DeferredResultAdapter {
@Override
public DeferredResult<?> adaptToDeferredResult(Object returnValue) {
Assert.isInstanceOf(ListenableFuture.class, returnValue);
final DeferredResult<Object> result = new DeferredResult<Object>();
((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.
*/
@UsesJava8
private static class CompletionStageAdapter implements DeferredResultAdapter {
@Override
public DeferredResult<?> adaptToDeferredResult(Object returnValue) {
Assert.isInstanceOf(CompletionStage.class, returnValue);
final DeferredResult<Object> result = new DeferredResult<Object>();
@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;
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -31,7 +31,11 @@ import org.springframework.web.method.support.ModelAndViewContainer;
*
* @author Rossen Stoyanchev
* @since 4.1
*
* @deprecated as of 4.3 {@link DeferredResultMethodReturnValueHandler} supports
* ListenableFuture return values via an adapter mechanism.
*/
@Deprecated
public class ListenableFutureReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
@Override

View File

@@ -48,7 +48,6 @@ import org.springframework.http.converter.support.AllEncompassingFormHttpMessage
import org.springframework.http.converter.xml.SourceHttpMessageConverter;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.web.accept.ContentNegotiationManager;
@@ -117,10 +116,6 @@ import org.springframework.web.util.WebUtils;
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
private static final boolean completionStagePresent = ClassUtils.isPresent(
"java.util.concurrent.CompletionStage", RequestMappingHandlerAdapter.class.getClassLoader());
private List<HandlerMethodArgumentResolver> customArgumentResolvers;
private HandlerMethodArgumentResolverComposite argumentResolvers;
@@ -677,10 +672,6 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));
handlers.add(new ListenableFutureReturnValueHandler());
if (completionStagePresent) {
handlers.add(new CompletionStageReturnValueHandler());
}
// Annotation-based return value types
handlers.add(new ModelAttributeMethodProcessor(false));

View File

@@ -0,0 +1,38 @@
/*
* 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
*/
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

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* 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.
@@ -18,8 +18,9 @@ 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;
@@ -44,8 +45,9 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Supports return values of type {@link ResponseBodyEmitter} and also
* {@code ResponseEntity<ResponseBodyEmitter>}.
* 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}.
*
* @author Rossen Stoyanchev
* @since 4.2
@@ -54,36 +56,61 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod
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, "'messageConverters' must not be empty");
this.messageConverters = messageConverters;
this.adapterMap = new HashMap<Class<?>, ResponseBodyEmitterAdapter>(3);
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) {
for (Class<?> adapteeType : getAdapterMap().keySet()) {
if (adapteeType.isAssignableFrom(type)) {
return getAdapterMap().get(adapteeType);
}
}
return null;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
if (ResponseBodyEmitter.class.isAssignableFrom(returnType.getParameterType())) {
return true;
Class<?> bodyType;
if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
}
else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
return (bodyType != null && ResponseBodyEmitter.class.isAssignableFrom(bodyType));
else {
bodyType = returnType.getParameterType();
}
return false;
return (getAdapterFor(bodyType) != null);
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
if (returnValue != null) {
if (returnValue instanceof ResponseBodyEmitter) {
return true;
Object adaptFrom = returnValue;
if (returnValue instanceof ResponseEntity) {
adaptFrom = ((ResponseEntity) returnValue).getBody();
}
else if (returnValue instanceof ResponseEntity) {
Object body = ((ResponseEntity) returnValue).getBody();
return (body != null && body instanceof ResponseBodyEmitter);
if (adaptFrom != null) {
return (getAdapterFor(adaptFrom.getClass()) != null);
}
}
return false;
@@ -115,8 +142,9 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod
ServletRequest request = webRequest.getNativeRequest(ServletRequest.class);
ShallowEtagHeaderFilter.disableContentCaching(request);
Assert.isInstanceOf(ResponseBodyEmitter.class, returnValue);
ResponseBodyEmitter emitter = (ResponseBodyEmitter) returnValue;
ResponseBodyEmitterAdapter adapter = getAdapterFor(returnValue.getClass());
Assert.notNull(adapter);
ResponseBodyEmitter emitter = adapter.adaptToEmitter(returnValue, outputMessage);
emitter.extendResponse(outputMessage);
// Commit the response and wrap to ignore further header changes
@@ -132,6 +160,18 @@ 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);
return (ResponseBodyEmitter) returnValue;
}
}
/**
* ResponseBodyEmitter.Handler that writes with HttpMessageConverter's.
*/