Support ResponseStatus on reactive controllers

This commit adds support for `@ResponseStatus` annotations on reactive
controller methods. `HandlerResultHandler`s implementations now
set the status on the `ServerWebExchange`, if and only if the
invocation of the controller method succeeded.

Issue: SPR-14830
This commit is contained in:
Brian Clozel 2016-10-23 13:58:16 +02:00
parent 87e01513fd
commit 0006957274
6 changed files with 65 additions and 13 deletions

View File

@ -23,10 +23,13 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.server.ServerWebExchange;
@ -38,7 +41,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class ContentNegotiatingResultHandlerSupport implements Ordered {
public abstract class AbstractHandlerResultHandler implements Ordered {
private static final MediaType MEDIA_TYPE_APPLICATION_ALL = new MediaType("application");
@ -50,11 +53,11 @@ public abstract class ContentNegotiatingResultHandlerSupport implements Ordered
private int order = LOWEST_PRECEDENCE;
protected ContentNegotiatingResultHandlerSupport(RequestedContentTypeResolver contentTypeResolver) {
protected AbstractHandlerResultHandler(RequestedContentTypeResolver contentTypeResolver) {
this(contentTypeResolver, new ReactiveAdapterRegistry());
}
protected ContentNegotiatingResultHandlerSupport(RequestedContentTypeResolver contentTypeResolver,
protected AbstractHandlerResultHandler(RequestedContentTypeResolver contentTypeResolver,
ReactiveAdapterRegistry adapterRegistry) {
Assert.notNull(contentTypeResolver, "'contentTypeResolver' is required.");
@ -151,4 +154,17 @@ public abstract class ContentNegotiatingResultHandlerSupport implements Ordered
return (comparator.compare(acceptable, producible) <= 0 ? acceptable : producible);
}
/**
* Optionally set the response status using the information provided by {@code @ResponseStatus}.
* @param methodParameter the controller method return parameter
* @param exchange the server exchange being handled
*/
protected void updateResponseStatus(MethodParameter methodParameter, ServerWebExchange exchange) {
ResponseStatus annotation = methodParameter.getMethodAnnotation(ResponseStatus.class);
if (annotation != null) {
annotation = AnnotationUtils.synthesizeAnnotation(annotation, methodParameter.getMethod());
exchange.getResponse().setStatusCode(annotation.code());
}
}
}

View File

@ -33,7 +33,7 @@ import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport;
import org.springframework.web.reactive.result.AbstractHandlerResultHandler;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
@ -44,7 +44,7 @@ import org.springframework.web.server.ServerWebExchange;
* @author Rossen Stoyanchev
* @since 5.0
*/
public abstract class AbstractMessageWriterResultHandler extends ContentNegotiatingResultHandlerSupport {
public abstract class AbstractMessageWriterResultHandler extends AbstractHandlerResultHandler {
private final List<HttpMessageWriter<?>> messageWriters;
@ -110,7 +110,8 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat
}
if (void.class == elementType.getRawClass() || Void.class == elementType.getRawClass()) {
return Mono.from((Publisher<Void>) publisher);
return Mono.from((Publisher<Void>) publisher)
.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
}
List<MediaType> producibleTypes = getProducibleMediaTypes(elementType);
@ -125,11 +126,12 @@ public abstract class AbstractMessageWriterResultHandler extends ContentNegotiat
if (bestMediaType != null) {
for (HttpMessageWriter<?> messageWriter : getMessageWriters()) {
if (messageWriter.canWrite(elementType, bestMediaType)) {
return (messageWriter instanceof ServerHttpMessageWriter ?
((ServerHttpMessageWriter<?>)messageWriter).write((Publisher) publisher,
Mono<Void> bodyWriter = (messageWriter instanceof ServerHttpMessageWriter ?
((ServerHttpMessageWriter<?>) messageWriter).write((Publisher) publisher,
bodyType, elementType, bestMediaType, request, response, Collections.emptyMap()) :
messageWriter.write((Publisher) publisher, elementType,
bestMediaType, response, Collections.emptyMap()));
return bodyWriter.doOnSubscribe(sub -> updateResponseStatus(bodyParameter, exchange));
}
}
}

View File

@ -43,7 +43,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.HandlerResultHandler;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.ContentNegotiatingResultHandlerSupport;
import org.springframework.web.reactive.result.AbstractHandlerResultHandler;
import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.HttpRequestPathHelper;
@ -74,7 +74,7 @@ import org.springframework.web.util.HttpRequestPathHelper;
* @author Rossen Stoyanchev
* @since 5.0
*/
public class ViewResolutionResultHandler extends ContentNegotiatingResultHandlerSupport
public class ViewResolutionResultHandler extends AbstractHandlerResultHandler
implements HandlerResultHandler, Ordered {
private final List<ViewResolver> viewResolvers = new ArrayList<>(4);
@ -208,6 +208,7 @@ public class ViewResolutionResultHandler extends ContentNegotiatingResultHandler
}
Map<String, ?> model = result.getModel();
return viewMono.then(view -> {
updateResponseStatus(result.getReturnTypeSource(), exchange);
if (view instanceof View) {
return ((View) view).render(model, null, exchange);
}

View File

@ -46,10 +46,10 @@ import static org.springframework.http.MediaType.TEXT_PLAIN;
import static org.springframework.web.reactive.HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE;
/**
* Unit tests for {@link ContentNegotiatingResultHandlerSupport}.
* Unit tests for {@link AbstractHandlerResultHandler}.
* @author Rossen Stoyanchev
*/
public class ContentNegotiatingResultHandlerSupportTests {
public class HandlerResultHandlerTests {
private TestResultHandler resultHandler;
@ -117,7 +117,7 @@ public class ContentNegotiatingResultHandlerSupportTests {
@SuppressWarnings("WeakerAccess")
private static class TestResultHandler extends ContentNegotiatingResultHandlerSupport {
private static class TestResultHandler extends AbstractHandlerResultHandler {
protected TestResultHandler() {
this(new HeaderContentTypeResolver());

View File

@ -23,12 +23,14 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import rx.Completable;
import rx.Single;
import org.springframework.core.codec.ByteBufferEncoder;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
@ -42,6 +44,7 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerResult;
@ -114,6 +117,22 @@ public class ResponseBodyResultHandlerTests {
testSupports(controller, "handleToMonoResponseEntity", false);
}
@Test
public void writeResponseStatus() throws NoSuchMethodException {
Object controller = new TestRestController();
HandlerMethod hm = handlerMethod(controller, "handleToString");
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode());
hm = handlerMethod(controller, "handleToMonoVoid");
handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
assertEquals(HttpStatus.CREATED, this.response.getStatusCode());
}
private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException {
HandlerMethod hm = handlerMethod(controller, method);
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap());
@ -134,6 +153,10 @@ public class ResponseBodyResultHandlerTests {
@RestController @SuppressWarnings("unused")
private static class TestRestController {
@ResponseStatus(code = HttpStatus.CREATED)
public Mono<Void> handleToMonoVoid() { return null;}
@ResponseStatus(code = HttpStatus.NO_CONTENT)
public String handleToString() {
return null;
}

View File

@ -42,6 +42,7 @@ import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.core.io.buffer.support.DataBufferTestUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
@ -50,6 +51,7 @@ import org.springframework.ui.ExtendedModelMap;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
@ -128,18 +130,22 @@ public class ViewResolutionResultHandlerTests {
returnType = ResolvableType.forClass(View.class);
returnValue = new TestView("account");
testHandle("/path", returnType, returnValue, "account: {id=123}");
assertEquals(HttpStatus.NO_CONTENT, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClassWithGenerics(Mono.class, View.class);
returnValue = Mono.just(new TestView("account"));
testHandle("/path", returnType, returnValue, "account: {id=123}");
assertEquals(HttpStatus.SEE_OTHER, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClass(String.class);
returnValue = "account";
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
assertEquals(HttpStatus.CREATED, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClassWithGenerics(Mono.class, String.class);
returnValue = Mono.just("account");
testHandle("/path", returnType, returnValue, "account: {id=123}", resolver);
assertEquals(HttpStatus.PARTIAL_CONTENT, this.exchange.getResponse().getStatusCode());
returnType = ResolvableType.forClass(Model.class);
returnValue = new ExtendedModelMap().addAttribute("name", "Joe");
@ -392,12 +398,16 @@ public class ViewResolutionResultHandlerTests {
@SuppressWarnings("unused")
private static class TestController {
@ResponseStatus(code = HttpStatus.CREATED)
String string() { return null; }
@ResponseStatus(HttpStatus.NO_CONTENT)
View view() { return null; }
@ResponseStatus(HttpStatus.PARTIAL_CONTENT)
Mono<String> monoString() { return null; }
@ResponseStatus(code = HttpStatus.SEE_OTHER)
Mono<View> monoView() { return null; }
Mono<Void> monoVoid() { return null; }