HandlerResult provides access to BindingContext

Issue: SPR-14542
This commit is contained in:
Rossen Stoyanchev 2016-11-07 11:23:07 +02:00
parent 6abd4d5ff5
commit ae003e89c1
4 changed files with 52 additions and 45 deletions

View File

@ -23,12 +23,12 @@ import reactor.core.publisher.Mono;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.ui.ConcurrentModel;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.reactive.result.method.BindingContext;
/** /**
* Represent the result of the invocation of a handler. * Represent the result of the invocation of a handler or a handler method.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @since 5.0 * @since 5.0
@ -37,12 +37,11 @@ public class HandlerResult {
private final Object handler; private final Object handler;
@SuppressWarnings("OptionalUsedAsFieldOrParameterType") private final Object returnValue;
private final Optional<Object> returnValue;
private final ResolvableType returnType; private final ResolvableType returnType;
private final Model model; private final BindingContext bindingContext;
private Function<Throwable, Mono<HandlerResult>> exceptionHandler; private Function<Throwable, Mono<HandlerResult>> exceptionHandler;
@ -62,15 +61,17 @@ public class HandlerResult {
* @param handler the handler that handled the request * @param handler the handler that handled the request
* @param returnValue the return value from the handler possibly {@code null} * @param returnValue the return value from the handler possibly {@code null}
* @param returnType the return value type * @param returnType the return value type
* @param model the model used for request handling * @param context the binding context used for request handling
*/ */
public HandlerResult(Object handler, Object returnValue, MethodParameter returnType, Model model) { public HandlerResult(Object handler, Object returnValue, MethodParameter returnType,
BindingContext context) {
Assert.notNull(handler, "'handler' is required"); Assert.notNull(handler, "'handler' is required");
Assert.notNull(returnType, "'returnType' is required"); Assert.notNull(returnType, "'returnType' is required");
this.handler = handler; this.handler = handler;
this.returnValue = Optional.ofNullable(returnValue); this.returnValue = returnValue;
this.returnType = ResolvableType.forMethodParameter(returnType); this.returnType = ResolvableType.forMethodParameter(returnType);
this.model = (model != null ? model : new ConcurrentModel()); this.bindingContext = (context != null ? context : new BindingContext());
} }
@ -85,7 +86,7 @@ public class HandlerResult {
* Return the value returned from the handler wrapped as {@link Optional}. * Return the value returned from the handler wrapped as {@link Optional}.
*/ */
public Optional<Object> getReturnValue() { public Optional<Object> getReturnValue() {
return this.returnValue; return Optional.ofNullable(this.returnValue);
} }
/** /**
@ -104,11 +105,18 @@ public class HandlerResult {
} }
/** /**
* Return the model used during request handling with attributes that may be * Return the BindingContext used for request handling.
* used to render HTML templates with. */
public BindingContext getBindingContext() {
return this.bindingContext;
}
/**
* Return the model used for request handling. This is a shortcut for
* {@code getBindingContext().getModel()}.
*/ */
public Model getModel() { public Model getModel() {
return this.model; return this.bindingContext.getModel();
} }
/** /**

View File

@ -32,8 +32,6 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.GenericTypeResolver; import org.springframework.core.GenericTypeResolver;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.ui.Model;
import org.springframework.ui.ModelMap;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@ -102,9 +100,8 @@ public class InvocableHandlerMethod extends HandlerMethod {
return resolveArguments(exchange, bindingContext, providedArgs).then(args -> { return resolveArguments(exchange, bindingContext, providedArgs).then(args -> {
try { try {
Object value = doInvoke(args); Object value = doInvoke(args);
Model model = bindingContext.getModel(); HandlerResult result = new HandlerResult(this, value, getReturnType(), bindingContext);
HandlerResult handlerResult = new HandlerResult(this, value, getReturnType(), model); return Mono.just(result);
return Mono.just(handlerResult);
} }
catch (InvocationTargetException ex) { catch (InvocationTargetException ex) {
return Mono.error(ex.getTargetException()); return Mono.error(ex.getTargetException());

View File

@ -37,11 +37,10 @@ import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageWriter;
import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2JsonEncoder;
import org.springframework.http.codec.xml.Jaxb2XmlEncoder; import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.ExtendedModelMap;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
@ -121,13 +120,13 @@ public class ResponseBodyResultHandlerTests {
public void writeResponseStatus() throws NoSuchMethodException { public void writeResponseStatus() throws NoSuchMethodException {
Object controller = new TestRestController(); Object controller = new TestRestController();
HandlerMethod hm = handlerMethod(controller, "handleToString"); HandlerMethod hm = handlerMethod(controller, "handleToString");
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType());
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify(); StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode()); assertEquals(HttpStatus.NO_CONTENT, this.response.getStatusCode());
hm = handlerMethod(controller, "handleToMonoVoid"); hm = handlerMethod(controller, "handleToMonoVoid");
handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); handlerResult = new HandlerResult(hm, null, hm.getReturnType());
StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify(); StepVerifier.create(this.resultHandler.handleResult(this.exchange, handlerResult)).expectComplete().verify();
assertEquals(HttpStatus.CREATED, this.response.getStatusCode()); assertEquals(HttpStatus.CREATED, this.response.getStatusCode());
@ -135,7 +134,7 @@ public class ResponseBodyResultHandlerTests {
private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException { private void testSupports(Object controller, String method, boolean result) throws NoSuchMethodException {
HandlerMethod hm = handlerMethod(controller, method); HandlerMethod hm = handlerMethod(controller, method);
HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType(), new ExtendedModelMap()); HandlerResult handlerResult = new HandlerResult(hm, null, hm.getReturnType());
assertEquals(result, this.resultHandler.supports(handlerResult)); assertEquals(result, this.resultHandler.supports(handlerResult));
} }

View File

@ -25,6 +25,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.TreeMap;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -45,7 +46,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.ui.ExtendedModelMap; import org.springframework.ui.ConcurrentModel;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.ResponseStatus;
@ -53,6 +54,7 @@ import org.springframework.web.reactive.HandlerResult;
import org.springframework.web.reactive.accept.HeaderContentTypeResolver; import org.springframework.web.reactive.accept.HeaderContentTypeResolver;
import org.springframework.web.reactive.accept.RequestedContentTypeResolver; import org.springframework.web.reactive.accept.RequestedContentTypeResolver;
import org.springframework.web.reactive.result.ResolvableMethod; import org.springframework.web.reactive.result.ResolvableMethod;
import org.springframework.web.reactive.result.method.BindingContext;
import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.NotAcceptableStatusException;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
@ -61,7 +63,6 @@ import org.springframework.web.server.session.WebSessionManager;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.springframework.core.ResolvableType.forClass; import static org.springframework.core.ResolvableType.forClass;
import static org.springframework.core.ResolvableType.forClassWithGenerics; import static org.springframework.core.ResolvableType.forClassWithGenerics;
@ -80,7 +81,7 @@ public class ViewResolutionResultHandlerTests {
private ServerWebExchange exchange; private ServerWebExchange exchange;
private Model model = new ExtendedModelMap(); private final BindingContext bindingContext = new BindingContext();
@Before @Before
@ -114,7 +115,7 @@ public class ViewResolutionResultHandlerTests {
private void testSupports(ResolvableMethod resolvableMethod, boolean result) { private void testSupports(ResolvableMethod resolvableMethod, boolean result) {
ViewResolutionResultHandler resultHandler = resultHandler(mock(ViewResolver.class)); ViewResolutionResultHandler resultHandler = resultHandler(mock(ViewResolver.class));
MethodParameter returnType = resolvableMethod.resolveReturnType(); MethodParameter returnType = resolvableMethod.resolveReturnType();
HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.model); HandlerResult handlerResult = new HandlerResult(new Object(), null, returnType, this.bindingContext);
assertEquals(result, resultHandler.supports(handlerResult)); assertEquals(result, resultHandler.supports(handlerResult));
} }
@ -156,7 +157,7 @@ public class ViewResolutionResultHandlerTests {
assertEquals(HttpStatus.PARTIAL_CONTENT, this.exchange.getResponse().getStatusCode()); assertEquals(HttpStatus.PARTIAL_CONTENT, this.exchange.getResponse().getStatusCode());
returnType = forClass(Model.class); returnType = forClass(Model.class);
returnValue = new ExtendedModelMap().addAttribute("name", "Joe"); returnValue = new ConcurrentModel().addAttribute("name", "Joe");
testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver); testHandle("/account", returnType, returnValue, "account: {id=123, name=Joe}", resolver);
returnType = forClass(Map.class); returnType = forClass(Map.class);
@ -190,8 +191,8 @@ public class ViewResolutionResultHandlerTests {
} }
private void testDefaultViewName(Object returnValue, ResolvableType type) throws URISyntaxException { private void testDefaultViewName(Object returnValue, ResolvableType type) throws URISyntaxException {
Model model = new ExtendedModelMap().addAttribute("id", "123"); this.bindingContext.getModel().addAttribute("id", "123");
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), model); HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), this.bindingContext);
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account")); ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
this.request.setUri("/account"); this.request.setUri("/account");
@ -210,8 +211,8 @@ public class ViewResolutionResultHandlerTests {
@Test @Test
public void unresolvedViewName() throws Exception { public void unresolvedViewName() throws Exception {
String returnValue = "account"; String returnValue = "account";
ResolvableType type = forClass(String.class); MethodParameter returnType = returnType(forClass(String.class));
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType(type), this.model); HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
this.request.setUri("/path"); this.request.setUri("/path");
Mono<Void> mono = resultHandler().handleResult(this.exchange, result); Mono<Void> mono = resultHandler().handleResult(this.exchange, result);
@ -225,8 +226,8 @@ public class ViewResolutionResultHandlerTests {
@Test @Test
public void contentNegotiation() throws Exception { public void contentNegotiation() throws Exception {
TestBean value = new TestBean("Joe"); TestBean value = new TestBean("Joe");
ResolvableType type = forClass(TestBean.class); MethodParameter returnType = returnType(forClass(TestBean.class));
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model); HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext);
this.request.setHeader("Accept", "application/json"); this.request.setHeader("Accept", "application/json");
this.request.setUri("/account"); this.request.setUri("/account");
@ -244,8 +245,8 @@ public class ViewResolutionResultHandlerTests {
@Test @Test
public void contentNegotiationWith406() throws Exception { public void contentNegotiationWith406() throws Exception {
TestBean value = new TestBean("Joe"); TestBean value = new TestBean("Joe");
ResolvableType type = forClass(TestBean.class); MethodParameter returnType = returnType(forClass(TestBean.class));
HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType(type), this.model); HandlerResult handlerResult = new HandlerResult(new Object(), value, returnType, this.bindingContext);
this.request.setHeader("Accept", "application/json"); this.request.setHeader("Accept", "application/json");
this.request.setUri("/account"); this.request.setUri("/account");
@ -260,13 +261,13 @@ public class ViewResolutionResultHandlerTests {
@Test @Test
public void modelWithAsyncAttributes() throws Exception { public void modelWithAsyncAttributes() throws Exception {
Model model = new ExtendedModelMap(); this.bindingContext.getModel()
model.addAttribute("bean1", Mono.just(new TestBean("Bean1"))); .addAttribute("bean1", Mono.just(new TestBean("Bean1")))
model.addAttribute("bean2", Single.just(new TestBean("Bean2"))); .addAttribute("bean2", Single.just(new TestBean("Bean2")))
model.addAttribute("empty", Mono.empty()); .addAttribute("empty", Mono.empty());
ResolvableType type = forClass(void.class); ResolvableType type = forClass(void.class);
HandlerResult result = new HandlerResult(new Object(), null, returnType(type), model); HandlerResult result = new HandlerResult(new Object(), null, returnType(type), this.bindingContext);
ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account")); ViewResolutionResultHandler handler = resultHandler(new TestViewResolver("account"));
this.request.setUri("/account"); this.request.setUri("/account");
@ -304,9 +305,11 @@ public class ViewResolutionResultHandlerTests {
private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue, private void testHandle(String path, ResolvableMethod resolvableMethod, Object returnValue,
String responseBody, ViewResolver... resolvers) throws URISyntaxException { String responseBody, ViewResolver... resolvers) throws URISyntaxException {
Model model = new ExtendedModelMap().addAttribute("id", "123"); Model model = this.bindingContext.getModel();
model.asMap().clear();
model.addAttribute("id", "123");
MethodParameter returnType = resolvableMethod.resolveReturnType(); MethodParameter returnType = resolvableMethod.resolveReturnType();
HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, model); HandlerResult result = new HandlerResult(new Object(), returnValue, returnType, this.bindingContext);
this.request.setUri(path); this.request.setUri(path);
resultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5)); resultHandler(resolvers).handleResult(this.exchange, result).block(Duration.ofSeconds(5));
assertResponseBody(responseBody); assertResponseBody(responseBody);
@ -375,12 +378,12 @@ public class ViewResolutionResultHandlerTests {
@Override @Override
public Mono<Void> render(Map<String, ?> model, MediaType mediaType, ServerWebExchange exchange) { public Mono<Void> render(Map<String, ?> model, MediaType mediaType, ServerWebExchange exchange) {
String value = this.name + ": " + model.toString();
assertNotNull(value);
ServerHttpResponse response = exchange.getResponse(); ServerHttpResponse response = exchange.getResponse();
if (mediaType != null) { if (mediaType != null) {
response.getHeaders().setContentType(mediaType); response.getHeaders().setContentType(mediaType);
} }
model = new TreeMap<>(model);
String value = this.name + ": " + model.toString();
ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8)); ByteBuffer byteBuffer = ByteBuffer.wrap(value.getBytes(UTF_8));
DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(byteBuffer); DataBuffer dataBuffer = new DefaultDataBufferFactory().wrap(byteBuffer);
return response.writeWith(Flux.just(dataBuffer)); return response.writeWith(Flux.just(dataBuffer));