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:
parent
cfc89ebe16
commit
ae1ed16cb8
|
@ -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");
|
* 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,20 +19,16 @@ package org.springframework.web.method.support;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link HandlerMethodReturnValueHandler} that handles return values that
|
* A return value handler that supports async types. Such return value types
|
||||||
* represent asynchronous computation. Such handlers need to be invoked with
|
* need to be handled with priority so the async value can be "unwrapped".
|
||||||
* precedence over other handlers that might otherwise match the return value
|
|
||||||
* type: e.g. a method that returns a Promise type that is also annotated with
|
|
||||||
* {@code @ResponseBody}.
|
|
||||||
*
|
*
|
||||||
* <p>In {@link #handleReturnValue}, implementations of this class should create
|
* <p><strong>Note: </strong> implementing this contract is not required but it
|
||||||
* a {@link org.springframework.web.context.request.async.DeferredResult} or
|
* should be implemented when the handler needs to be prioritized ahead of others.
|
||||||
* adapt to it and then invoke {@code WebAsyncManager} to start async processing.
|
* For example custom (async) handlers, by default ordered after built-in
|
||||||
* For example:
|
* handlers, should take precedence over {@code @ResponseBody} or
|
||||||
* <pre>
|
* {@code @ModelAttribute} handling, which should occur once the async value is
|
||||||
* DeferredResult<?> deferredResult = (DeferredResult<?>) returnValue;
|
* ready. By contrast, built-in (async) handlers are already ordered ahead of
|
||||||
* WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(deferredResult, mavContainer);
|
* sync handlers.
|
||||||
* </pre>
|
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 4.2
|
* @since 4.2
|
||||||
|
|
|
@ -33,7 +33,7 @@ import org.springframework.web.context.request.NativeWebRequest;
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 3.1
|
* @since 3.1
|
||||||
*/
|
*/
|
||||||
public class HandlerMethodReturnValueHandlerComposite implements AsyncHandlerMethodReturnValueHandler {
|
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
|
||||||
|
|
||||||
protected final Log logger = LogFactory.getLog(getClass());
|
protected final Log logger = LogFactory.getLog(getClass());
|
||||||
|
|
||||||
|
@ -94,8 +94,7 @@ public class HandlerMethodReturnValueHandlerComposite implements AsyncHandlerMet
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean isAsyncReturnValue(Object value, MethodParameter returnType) {
|
||||||
public boolean isAsyncReturnValue(Object value, MethodParameter returnType) {
|
|
||||||
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
|
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
|
||||||
if (handler instanceof AsyncHandlerMethodReturnValueHandler) {
|
if (handler instanceof AsyncHandlerMethodReturnValueHandler) {
|
||||||
if (((AsyncHandlerMethodReturnValueHandler) handler).isAsyncReturnValue(value, returnType)) {
|
if (((AsyncHandlerMethodReturnValueHandler) handler).isAsyncReturnValue(value, returnType)) {
|
||||||
|
|
|
@ -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");
|
* 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.
|
||||||
|
@ -21,14 +21,12 @@ import org.junit.Test;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
|
|
||||||
import static org.junit.Assert.*;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.mockito.Mockito.*;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import java.lang.annotation.Documented;
|
import static org.mockito.Mockito.verify;
|
||||||
import java.lang.annotation.ElementType;
|
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||||
import java.lang.annotation.Retention;
|
import static org.mockito.Mockito.when;
|
||||||
import java.lang.annotation.RetentionPolicy;
|
|
||||||
import java.lang.annotation.Target;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test fixture with {@link HandlerMethodReturnValueHandlerComposite}.
|
* Test fixture with {@link HandlerMethodReturnValueHandlerComposite}.
|
||||||
|
@ -86,9 +84,7 @@ public class HandlerMethodReturnValueHandlerCompositeTests {
|
||||||
verifyNoMoreInteractions(anotherIntegerHandler);
|
verifyNoMoreInteractions(anotherIntegerHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPR-13083
|
@Test // SPR-13083
|
||||||
|
|
||||||
@Test
|
|
||||||
public void handleReturnValueWithAsyncHandler() throws Exception {
|
public void handleReturnValueWithAsyncHandler() throws Exception {
|
||||||
|
|
||||||
Promise<Integer> promise = new Promise<>();
|
Promise<Integer> promise = new Promise<>();
|
||||||
|
|
|
@ -21,7 +21,7 @@ import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
import org.springframework.web.context.request.async.WebAsyncTask;
|
import org.springframework.web.context.request.async.WebAsyncTask;
|
||||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
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;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,7 +30,7 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 3.2
|
* @since 3.2
|
||||||
*/
|
*/
|
||||||
public class AsyncTaskMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
|
public class AsyncTaskMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||||
|
|
||||||
private final BeanFactory beanFactory;
|
private final BeanFactory beanFactory;
|
||||||
|
|
||||||
|
@ -45,11 +45,6 @@ public class AsyncTaskMethodReturnValueHandler implements AsyncHandlerMethodRetu
|
||||||
return WebAsyncTask.class.isAssignableFrom(returnType.getParameterType());
|
return WebAsyncTask.class.isAssignableFrom(returnType.getParameterType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
|
|
||||||
return (returnValue != null && returnValue instanceof WebAsyncTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleReturnValue(Object returnValue, MethodParameter returnType,
|
public void handleReturnValue(Object returnValue, MethodParameter returnType,
|
||||||
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
|
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
|
||||||
|
|
|
@ -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");
|
* 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.
|
||||||
|
@ -21,7 +21,7 @@ import java.util.concurrent.Callable;
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
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;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -30,18 +30,13 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 3.2
|
* @since 3.2
|
||||||
*/
|
*/
|
||||||
public class CallableMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
|
public class CallableMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsReturnType(MethodParameter returnType) {
|
public boolean supportsReturnType(MethodParameter returnType) {
|
||||||
return Callable.class.isAssignableFrom(returnType.getParameterType());
|
return Callable.class.isAssignableFrom(returnType.getParameterType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
|
|
||||||
return (returnValue != null && returnValue instanceof Callable);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleReturnValue(Object returnValue, MethodParameter returnType,
|
public void handleReturnValue(Object returnValue, MethodParameter returnType,
|
||||||
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
|
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
|
@ -16,70 +16,34 @@
|
||||||
|
|
||||||
package org.springframework.web.servlet.mvc.method.annotation;
|
package org.springframework.web.servlet.mvc.method.annotation;
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.CompletionStage;
|
import java.util.concurrent.CompletionStage;
|
||||||
import java.util.function.BiFunction;
|
import java.util.function.BiFunction;
|
||||||
|
|
||||||
import org.springframework.core.MethodParameter;
|
import org.springframework.core.MethodParameter;
|
||||||
import org.springframework.util.Assert;
|
|
||||||
import org.springframework.util.concurrent.ListenableFuture;
|
import org.springframework.util.concurrent.ListenableFuture;
|
||||||
import org.springframework.util.concurrent.ListenableFutureCallback;
|
import org.springframework.util.concurrent.ListenableFutureCallback;
|
||||||
import org.springframework.web.context.request.NativeWebRequest;
|
import org.springframework.web.context.request.NativeWebRequest;
|
||||||
import org.springframework.web.context.request.async.DeferredResult;
|
import org.springframework.web.context.request.async.DeferredResult;
|
||||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
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;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for return values of type {@link DeferredResult}, {@link ListenableFuture},
|
* Handler for return values of type {@link DeferredResult},
|
||||||
* {@link CompletionStage} and any other async type with a {@link #getAdapterMap()
|
* {@link ListenableFuture}, and {@link CompletionStage}.
|
||||||
* registered adapter}.
|
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 3.2
|
* @since 3.2
|
||||||
*/
|
*/
|
||||||
public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
|
public class DeferredResultMethodReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean supportsReturnType(MethodParameter returnType) {
|
public boolean supportsReturnType(MethodParameter returnType) {
|
||||||
return (getAdapterFor(returnType.getParameterType()) != null);
|
Class<?> type = returnType.getParameterType();
|
||||||
}
|
return DeferredResult.class.isAssignableFrom(type) ||
|
||||||
|
ListenableFuture.class.isAssignableFrom(type) ||
|
||||||
@Override
|
CompletionStage.class.isAssignableFrom(type);
|
||||||
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
|
|
||||||
return (returnValue != null && (getAdapterFor(returnValue.getClass()) != null));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -91,39 +55,28 @@ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMetho
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
DeferredResultAdapter adapter = getAdapterFor(returnValue.getClass());
|
DeferredResult<?> result;
|
||||||
if (adapter == null) {
|
|
||||||
throw new IllegalStateException(
|
if (returnValue instanceof DeferredResult) {
|
||||||
"Could not find DeferredResultAdapter for return value type: " + returnValue.getClass());
|
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);
|
WebAsyncUtils.getAsyncManager(webRequest).startDeferredResultProcessing(result, mavContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private DeferredResult<Object> adaptListenableFuture(ListenableFuture<?> future) {
|
||||||
/**
|
DeferredResult<Object> result = new DeferredResult<>();
|
||||||
* Adapter for {@code DeferredResult} return values.
|
future.addCallback(new ListenableFutureCallback<Object>() {
|
||||||
*/
|
|
||||||
private static class SimpleDeferredResultAdapter implements DeferredResultAdapter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public DeferredResult<?> adaptToDeferredResult(Object returnValue) {
|
|
||||||
Assert.isInstanceOf(DeferredResult.class, returnValue, "DeferredResult expected");
|
|
||||||
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, "ListenableFuture expected");
|
|
||||||
final DeferredResult<Object> result = new DeferredResult<>();
|
|
||||||
((ListenableFuture<?>) returnValue).addCallback(new ListenableFutureCallback<Object>() {
|
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(Object value) {
|
public void onSuccess(Object value) {
|
||||||
result.setResult(value);
|
result.setResult(value);
|
||||||
|
@ -135,23 +88,10 @@ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMetho
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
private DeferredResult<Object> adaptCompletionStage(CompletionStage<?> future) {
|
||||||
/**
|
DeferredResult<Object> result = new DeferredResult<>();
|
||||||
* Adapter for {@code CompletionStage} return values.
|
future.handle((BiFunction<Object, Throwable, Object>) (value, ex) -> {
|
||||||
*/
|
|
||||||
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) {
|
if (ex != null) {
|
||||||
result.setErrorResult(ex);
|
result.setErrorResult(ex);
|
||||||
}
|
}
|
||||||
|
@ -159,10 +99,8 @@ public class DeferredResultMethodReturnValueHandler implements AsyncHandlerMetho
|
||||||
result.setResult(value);
|
result.setResult(value);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
|
||||||
|
|
||||||
}
|
|
|
@ -18,9 +18,7 @@ package org.springframework.web.servlet.mvc.method.annotation;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
import javax.servlet.ServletRequest;
|
import javax.servlet.ServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
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.DeferredResult;
|
||||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||||
import org.springframework.web.filter.ShallowEtagHeaderFilter;
|
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;
|
import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for return values of type {@link ResponseBodyEmitter} (and the
|
* Handler for return values of type {@link ResponseBodyEmitter} and sub-classes
|
||||||
* {@code ResponseEntity<ResponseBodyEmitter>} sub-class) as well as any other
|
* such as {@link SseEmitter} including the same types wrapped with
|
||||||
* async type with a {@link #getAdapterMap() registered adapter}.
|
* {@link ResponseEntity}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 4.2
|
* @since 4.2
|
||||||
*/
|
*/
|
||||||
public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
|
public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodReturnValueHandler {
|
||||||
|
|
||||||
private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class);
|
private static final Log logger = LogFactory.getLog(ResponseBodyEmitterReturnValueHandler.class);
|
||||||
|
|
||||||
|
|
||||||
private final List<HttpMessageConverter<?>> messageConverters;
|
private final List<HttpMessageConverter<?>> messageConverters;
|
||||||
|
|
||||||
private final Map<Class<?>, ResponseBodyEmitterAdapter> adapterMap;
|
|
||||||
|
|
||||||
|
|
||||||
public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) {
|
public ResponseBodyEmitterReturnValueHandler(List<HttpMessageConverter<?>> messageConverters) {
|
||||||
Assert.notEmpty(messageConverters, "HttpMessageConverter List must not be empty");
|
Assert.notEmpty(messageConverters, "HttpMessageConverter List must not be empty");
|
||||||
this.messageConverters = messageConverters;
|
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
|
@Override
|
||||||
public boolean supportsReturnType(MethodParameter returnType) {
|
public boolean supportsReturnType(MethodParameter returnType) {
|
||||||
Class<?> bodyType;
|
|
||||||
if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
|
Class<?> bodyType = ResponseEntity.class.isAssignableFrom(returnType.getParameterType()) ?
|
||||||
bodyType = ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve();
|
ResolvableType.forMethodParameter(returnType).getGeneric(0).resolve() :
|
||||||
}
|
returnType.getParameterType();
|
||||||
else {
|
|
||||||
bodyType = returnType.getParameterType();
|
return bodyType != null && supportsBodyType(bodyType);
|
||||||
}
|
|
||||||
return (getAdapterFor(bodyType) != null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private boolean supportsBodyType(Class<?> bodyType) {
|
||||||
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
|
return ResponseBodyEmitter.class.isAssignableFrom(bodyType);
|
||||||
if (returnValue != null) {
|
|
||||||
Object adaptFrom = returnValue;
|
|
||||||
if (returnValue instanceof ResponseEntity) {
|
|
||||||
adaptFrom = ((ResponseEntity) returnValue).getBody();
|
|
||||||
}
|
|
||||||
if (adaptFrom != null) {
|
|
||||||
return (getAdapterFor(adaptFrom.getClass()) != null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -145,12 +105,15 @@ public class ResponseBodyEmitterReturnValueHandler implements AsyncHandlerMethod
|
||||||
ServletRequest request = webRequest.getNativeRequest(ServletRequest.class);
|
ServletRequest request = webRequest.getNativeRequest(ServletRequest.class);
|
||||||
ShallowEtagHeaderFilter.disableContentCaching(request);
|
ShallowEtagHeaderFilter.disableContentCaching(request);
|
||||||
|
|
||||||
ResponseBodyEmitterAdapter adapter = getAdapterFor(returnValue.getClass());
|
ResponseBodyEmitter emitter;
|
||||||
if (adapter == null) {
|
|
||||||
throw new IllegalStateException(
|
if (returnValue instanceof ResponseBodyEmitter) {
|
||||||
"Could not find ResponseBodyEmitterAdapter for return value type: " + returnValue.getClass());
|
emitter = (ResponseBodyEmitter) returnValue;
|
||||||
}
|
}
|
||||||
ResponseBodyEmitter emitter = adapter.adaptToEmitter(returnValue, outputMessage);
|
else {
|
||||||
|
throw new IllegalStateException("Unexpected return value type: " + returnValue);
|
||||||
|
}
|
||||||
|
|
||||||
emitter.extendResponse(outputMessage);
|
emitter.extendResponse(outputMessage);
|
||||||
|
|
||||||
// Commit the response and wrap to ignore further header changes
|
// 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.
|
* ResponseBodyEmitter.Handler that writes with HttpMessageConverter's.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -15,7 +15,6 @@
|
||||||
*/
|
*/
|
||||||
package org.springframework.web.servlet.mvc.method.annotation;
|
package org.springframework.web.servlet.mvc.method.annotation;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
|
||||||
import java.util.concurrent.CompletableFuture;
|
import java.util.concurrent.CompletableFuture;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
|
@ -36,8 +35,8 @@ import org.springframework.web.method.support.ModelAndViewContainer;
|
||||||
|
|
||||||
import static org.junit.Assert.assertEquals;
|
import static org.junit.Assert.assertEquals;
|
||||||
import static org.junit.Assert.assertFalse;
|
import static org.junit.Assert.assertFalse;
|
||||||
import static org.junit.Assert.assertSame;
|
|
||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.springframework.web.method.ResolvableMethod.on;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unit tests for {@link DeferredResultMethodReturnValueHandler}.
|
* Unit tests for {@link DeferredResultMethodReturnValueHandler}.
|
||||||
|
@ -68,129 +67,88 @@ public class DeferredResultReturnValueHandlerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void supportsReturnType() throws Exception {
|
public void supportsReturnType() throws Exception {
|
||||||
assertTrue(this.handler.supportsReturnType(returnType("handleDeferredResult")));
|
|
||||||
assertTrue(this.handler.supportsReturnType(returnType("handleListenableFuture")));
|
assertTrue(this.handler.supportsReturnType(
|
||||||
assertTrue(this.handler.supportsReturnType(returnType("handleCompletableFuture")));
|
on(TestController.class).resolveReturnType(DeferredResult.class, String.class)));
|
||||||
assertFalse(this.handler.supportsReturnType(returnType("handleString")));
|
|
||||||
|
assertTrue(this.handler.supportsReturnType(
|
||||||
|
on(TestController.class).resolveReturnType(ListenableFuture.class, String.class)));
|
||||||
|
|
||||||
|
assertTrue(this.handler.supportsReturnType(
|
||||||
|
on(TestController.class).resolveReturnType(CompletableFuture.class, String.class)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void doesNotSupportReturnType() throws Exception {
|
||||||
|
assertFalse(this.handler.supportsReturnType(on(TestController.class).resolveReturnType(String.class)));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void deferredResult() throws Exception {
|
public void deferredResult() throws Exception {
|
||||||
MethodParameter returnType = returnType("handleDeferredResult");
|
DeferredResult<String> result = new DeferredResult<>();
|
||||||
DeferredResult<String> deferredResult = new DeferredResult<>();
|
|
||||||
handleReturnValue(deferredResult, returnType);
|
|
||||||
|
|
||||||
assertTrue(this.request.isAsyncStarted());
|
|
||||||
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
|
|
||||||
deferredResult.setResult("foo");
|
|
||||||
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
assertEquals("foo", WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void deferredResultWitError() throws Exception {
|
|
||||||
MethodParameter returnType = returnType("handleDeferredResult");
|
|
||||||
DeferredResult<String> deferredResult = new DeferredResult<>();
|
|
||||||
handleReturnValue(deferredResult, returnType);
|
|
||||||
|
|
||||||
assertTrue(this.request.isAsyncStarted());
|
|
||||||
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
|
|
||||||
IllegalStateException ex = new IllegalStateException();
|
IllegalStateException ex = new IllegalStateException();
|
||||||
deferredResult.setErrorResult(ex);
|
testHandle(result, DeferredResult.class, () -> result.setErrorResult(ex), ex);
|
||||||
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
assertSame(ex, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void listenableFuture() throws Exception {
|
public void listenableFuture() throws Exception {
|
||||||
MethodParameter returnType = returnType("handleListenableFuture");
|
|
||||||
SettableListenableFuture<String> future = new SettableListenableFuture<>();
|
SettableListenableFuture<String> future = new SettableListenableFuture<>();
|
||||||
handleReturnValue(future, returnType);
|
testHandle(future, ListenableFuture.class, () -> future.set("foo"), "foo");
|
||||||
|
|
||||||
assertTrue(this.request.isAsyncStarted());
|
|
||||||
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
|
|
||||||
future.set("foo");
|
|
||||||
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
assertEquals("foo", WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void listenableFutureWithError() throws Exception {
|
|
||||||
MethodParameter returnType = returnType("handleListenableFuture");
|
|
||||||
SettableListenableFuture<String> future = new SettableListenableFuture<>();
|
|
||||||
handleReturnValue(future, returnType);
|
|
||||||
|
|
||||||
assertTrue(this.request.isAsyncStarted());
|
|
||||||
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
|
|
||||||
IllegalStateException ex = new IllegalStateException();
|
|
||||||
future.setException(ex);
|
|
||||||
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
|
||||||
assertSame(ex, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void completableFuture() throws Exception {
|
public void completableFuture() throws Exception {
|
||||||
MethodParameter returnType = returnType("handleCompletableFuture");
|
|
||||||
SettableListenableFuture<String> future = new SettableListenableFuture<>();
|
SettableListenableFuture<String> future = new SettableListenableFuture<>();
|
||||||
handleReturnValue(future, returnType);
|
testHandle(future, CompletableFuture.class, () -> future.set("foo"), "foo");
|
||||||
|
}
|
||||||
|
|
||||||
assertTrue(this.request.isAsyncStarted());
|
@Test
|
||||||
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
public void deferredResultWitError() throws Exception {
|
||||||
|
DeferredResult<String> result = new DeferredResult<>();
|
||||||
|
testHandle(result, DeferredResult.class, () -> result.setResult("foo"), "foo");
|
||||||
|
}
|
||||||
|
|
||||||
future.set("foo");
|
@Test
|
||||||
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
public void listenableFutureWithError() throws Exception {
|
||||||
assertEquals("foo", WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
SettableListenableFuture<String> future = new SettableListenableFuture<>();
|
||||||
|
IllegalStateException ex = new IllegalStateException();
|
||||||
|
testHandle(future, ListenableFuture.class, () -> future.setException(ex), ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void completableFutureWithError() throws Exception {
|
public void completableFutureWithError() throws Exception {
|
||||||
MethodParameter returnType = returnType("handleCompletableFuture");
|
SettableListenableFuture<String> future = new SettableListenableFuture<>();
|
||||||
CompletableFuture<String> future = new CompletableFuture<>();
|
IllegalStateException ex = new IllegalStateException();
|
||||||
handleReturnValue(future, returnType);
|
testHandle(future, CompletableFuture.class, () -> future.setException(ex), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testHandle(Object returnValue, Class<?> asyncType,
|
||||||
|
Runnable setResultTask, Object expectedValue) throws Exception {
|
||||||
|
|
||||||
|
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
||||||
|
MethodParameter returnType = on(TestController.class).resolveReturnType(asyncType, String.class);
|
||||||
|
this.handler.handleReturnValue(returnValue, returnType, mavContainer, this.webRequest);
|
||||||
|
|
||||||
assertTrue(this.request.isAsyncStarted());
|
assertTrue(this.request.isAsyncStarted());
|
||||||
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
assertFalse(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
||||||
|
|
||||||
IllegalStateException ex = new IllegalStateException();
|
setResultTask.run();
|
||||||
future.completeExceptionally(ex);
|
|
||||||
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
assertTrue(WebAsyncUtils.getAsyncManager(this.webRequest).hasConcurrentResult());
|
||||||
assertSame(ex, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
assertEquals(expectedValue, WebAsyncUtils.getAsyncManager(this.webRequest).getConcurrentResult());
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void handleReturnValue(Object returnValue, MethodParameter returnType) throws Exception {
|
|
||||||
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
|
|
||||||
this.handler.handleReturnValue(returnValue, returnType, mavContainer, this.webRequest);
|
|
||||||
}
|
|
||||||
|
|
||||||
private MethodParameter returnType(String methodName) throws NoSuchMethodException {
|
|
||||||
Method method = TestController.class.getDeclaredMethod(methodName);
|
|
||||||
return new MethodParameter(method, -1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
private static class TestController {
|
static class TestController {
|
||||||
|
|
||||||
private String handleString() {
|
String handleString() { return null; }
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DeferredResult<String> handleDeferredResult() {
|
DeferredResult<String> handleDeferredResult() { return null; }
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ListenableFuture<String> handleListenableFuture() {
|
ListenableFuture<String> handleListenableFuture() { return null; }
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private CompletableFuture<String> handleCompletableFuture() {
|
CompletableFuture<String> handleCompletableFuture() { return null; }
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue